File indexing completed on 2024-12-22 05:05:21

0001 // SPDX-FileCopyrightText: 2023 g10 Code GmbH
0002 // SPDX-FileContributor: Carl Schwan <carl.schwan@gnupg.com>
0003 // SPDX-License-Identifier: GPL-2.0-or-later
0004 
0005 #include "messageviewerdialog.h"
0006 
0007 #include "messageviewer.h"
0008 #include "mimetreeparser_widgets_debug.h"
0009 #include <MimeTreeParserCore/CryptoHelper>
0010 #include <MimeTreeParserCore/FileOpener>
0011 
0012 #include <KLocalizedString>
0013 #include <KMessageBox>
0014 #include <KMessageWidget>
0015 
0016 #include <QDialogButtonBox>
0017 #include <QFileDialog>
0018 #include <QMenuBar>
0019 #include <QPainter>
0020 #include <QPrintDialog>
0021 #include <QPrintPreviewDialog>
0022 #include <QPrinter>
0023 #include <QPushButton>
0024 #include <QRegularExpression>
0025 #include <QSaveFile>
0026 #include <QStandardPaths>
0027 #include <QStyle>
0028 #include <QToolBar>
0029 #include <QVBoxLayout>
0030 
0031 using namespace MimeTreeParser::Widgets;
0032 
0033 namespace
0034 {
0035 
0036 /// On windows, force the filename to end with .eml
0037 /// On Linux, do nothing as this is handled by the file picker
0038 inline QString changeExtension(const QString &fileName)
0039 {
0040 #ifdef Q_OS_WIN
0041     auto renamedFileName = fileName;
0042     renamedFileName.replace(QRegularExpression(QStringLiteral("\\.(mbox|p7m|asc)$")), QStringLiteral(".eml"));
0043 
0044     // In case the file name didn't contain any of the expected extension: mbox, p7m, asc and eml
0045     // or doesn't contains an extension at all.
0046     if (!renamedFileName.endsWith(QStringLiteral(".eml"))) {
0047         renamedFileName += QStringLiteral(".eml");
0048     }
0049 
0050     return renamedFileName;
0051 #else
0052     // Handled automatically by the file picker on linux
0053     return fileName;
0054 #endif
0055 }
0056 
0057 }
0058 
0059 class MessageViewerDialog::Private
0060 {
0061 public:
0062     int currentIndex = 0;
0063     QList<KMime::Message::Ptr> messages;
0064     QString fileName;
0065     MimeTreeParser::Widgets::MessageViewer *messageViewer = nullptr;
0066     QAction *nextAction = nullptr;
0067     QAction *previousAction = nullptr;
0068     QToolBar *toolBar = nullptr;
0069 
0070     void setCurrentIndex(int currentIndex);
0071     QMenuBar *createMenuBar(QWidget *parent);
0072 
0073 private:
0074     void save(QWidget *parent);
0075     void saveDecrypted(QWidget *parent);
0076     void print(QWidget *parent);
0077     void printPreview(QWidget *parent);
0078     void printInternal(QPrinter *printer);
0079 };
0080 
0081 void MessageViewerDialog::Private::setCurrentIndex(int index)
0082 {
0083     Q_ASSERT(index >= 0);
0084     Q_ASSERT(index < messages.count());
0085 
0086     currentIndex = index;
0087     messageViewer->setMessage(messages[currentIndex]);
0088 
0089     previousAction->setEnabled(currentIndex != 0);
0090     nextAction->setEnabled(currentIndex != messages.count() - 1);
0091 }
0092 
0093 QMenuBar *MessageViewerDialog::Private::createMenuBar(QWidget *parent)
0094 {
0095     const auto menuBar = new QMenuBar(parent);
0096 
0097     // File menu
0098     const auto fileMenu = menuBar->addMenu(i18nc("@action:inmenu", "&File"));
0099 
0100     const auto saveAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18nc("@action:inmenu", "&Save"));
0101     QObject::connect(saveAction, &QAction::triggered, parent, [parent, this] {
0102         save(parent);
0103     });
0104     fileMenu->addAction(saveAction);
0105 
0106     const auto saveDecryptedAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18nc("@action:inmenu", "Save Decrypted"));
0107     QObject::connect(saveDecryptedAction, &QAction::triggered, parent, [parent, this] {
0108         saveDecrypted(parent);
0109     });
0110     fileMenu->addAction(saveDecryptedAction);
0111 
0112     const auto printPreviewAction = new QAction(QIcon::fromTheme(QStringLiteral("document-print-preview")), i18nc("@action:inmenu", "Print Preview"));
0113     QObject::connect(printPreviewAction, &QAction::triggered, parent, [parent, this] {
0114         printPreview(parent);
0115     });
0116     fileMenu->addAction(printPreviewAction);
0117 
0118     const auto printAction = new QAction(QIcon::fromTheme(QStringLiteral("document-print")), i18nc("@action:inmenu", "&Print"));
0119     QObject::connect(printAction, &QAction::triggered, parent, [parent, this] {
0120         print(parent);
0121     });
0122     fileMenu->addAction(printAction);
0123 
0124     // Navigation menu
0125     const auto navigationMenu = menuBar->addMenu(i18nc("@action:inmenu", "&Navigation"));
0126     previousAction = new QAction(QIcon::fromTheme(QStringLiteral("go-previous")), i18nc("@action:button Previous email", "Previous Message"), parent);
0127     previousAction->setEnabled(false);
0128     navigationMenu->addAction(previousAction);
0129 
0130     nextAction = new QAction(QIcon::fromTheme(QStringLiteral("go-next")), i18nc("@action:button Next email", "Next Message"), parent);
0131     nextAction->setEnabled(false);
0132     navigationMenu->addAction(nextAction);
0133 
0134     return menuBar;
0135 }
0136 
0137 void MessageViewerDialog::Private::save(QWidget *parent)
0138 {
0139     const QString location = QFileDialog::getSaveFileName(parent,
0140                                                           i18nc("@title:window", "Save File"),
0141                                                           changeExtension(fileName),
0142                                                           i18nc("File dialog accepted files", "Email files (*.eml *.mbox)"));
0143 
0144     QSaveFile file(location);
0145     if (!file.open(QIODevice::WriteOnly)) {
0146         KMessageBox::error(parent, i18n("File %1 could not be created.", location), i18n("Error saving message"));
0147         return;
0148     }
0149     file.write(messages[currentIndex]->encodedContent());
0150     file.commit();
0151 }
0152 
0153 void MessageViewerDialog::Private::saveDecrypted(QWidget *parent)
0154 {
0155     const QString location = QFileDialog::getSaveFileName(parent,
0156                                                           i18nc("@title:window", "Save Decrypted File"),
0157                                                           changeExtension(fileName),
0158                                                           i18nc("File dialog accepted files", "Email files (*.eml *.mbox)"));
0159 
0160     QSaveFile file(location);
0161     if (!file.open(QIODevice::WriteOnly)) {
0162         KMessageBox::error(parent, i18nc("Error message", "File %1 could not be created.", location), i18n("Error saving message"));
0163         return;
0164     }
0165     auto message = messages[currentIndex];
0166     bool wasEncrypted = false;
0167     auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted);
0168     if (!wasEncrypted) {
0169         decryptedMessage = message;
0170     }
0171     file.write(decryptedMessage->encodedContent());
0172 
0173     file.commit();
0174 }
0175 
0176 void MessageViewerDialog::Private::print(QWidget *parent)
0177 {
0178     QPrinter printer;
0179     QPrintDialog dialog(&printer, parent);
0180     dialog.setWindowTitle(i18nc("@title:window", "Print Document"));
0181     if (dialog.exec() != QDialog::Accepted)
0182         return;
0183 
0184     printInternal(&printer);
0185 }
0186 
0187 void MessageViewerDialog::Private::printPreview(QWidget *parent)
0188 {
0189     auto dialog = new QPrintPreviewDialog(parent);
0190     dialog->setAttribute(Qt::WA_DeleteOnClose);
0191     dialog->resize(800, 750);
0192     dialog->setWindowTitle(i18nc("@title:window", "Print Document"));
0193     QObject::connect(dialog, &QPrintPreviewDialog::paintRequested, parent, [this](QPrinter *printer) {
0194         printInternal(printer);
0195     });
0196     dialog->open();
0197 }
0198 
0199 void MessageViewerDialog::Private::printInternal(QPrinter *printer)
0200 {
0201     QPainter painter;
0202     painter.begin(printer);
0203     const auto pageLayout = printer->pageLayout();
0204     const auto pageRect = pageLayout.paintRectPixels(printer->resolution());
0205     const double xscale = pageRect.width() / double(messageViewer->width());
0206     const double yscale = pageRect.height() / double(messageViewer->height());
0207     const double scale = qMin(qMin(xscale, yscale), 1.);
0208     painter.translate(pageRect.x(), pageRect.y());
0209     painter.scale(scale, scale);
0210     messageViewer->print(&painter, pageRect.width());
0211 }
0212 
0213 MessageViewerDialog::MessageViewerDialog(const QList<KMime::Message::Ptr> &messages, QWidget *parent)
0214     : QDialog(parent)
0215     , d(std::make_unique<Private>())
0216 {
0217     d->messages += messages;
0218     initGUI();
0219 }
0220 
0221 MessageViewerDialog::MessageViewerDialog(const QString &fileName, QWidget *parent)
0222     : QDialog(parent)
0223     , d(std::make_unique<Private>())
0224 {
0225     d->fileName = fileName;
0226     d->messages += MimeTreeParser::Core::FileOpener::openFile(fileName);
0227     initGUI();
0228 }
0229 
0230 void MessageViewerDialog::initGUI()
0231 {
0232     const auto mainLayout = new QVBoxLayout(this);
0233     mainLayout->setContentsMargins({});
0234     mainLayout->setSpacing(0);
0235 
0236     const auto layout = new QVBoxLayout;
0237 
0238     const auto menuBar = d->createMenuBar(this);
0239     mainLayout->setMenuBar(menuBar);
0240 
0241     if (d->messages.isEmpty()) {
0242         auto errorMessage = new KMessageWidget(this);
0243         errorMessage->setMessageType(KMessageWidget::Error);
0244         errorMessage->setText(i18nc("@info", "Unable to read file"));
0245         layout->addWidget(errorMessage);
0246         return;
0247     }
0248 
0249     const bool multipleMessages = d->messages.length() > 1;
0250     d->toolBar = new QToolBar(this);
0251 
0252     if (multipleMessages) {
0253 #ifdef Q_OS_UNIX
0254         d->toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle);
0255 #else
0256         // on other platforms the default is IconOnly which is bad for
0257         // accessibility and can't be changed by the user.
0258         toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
0259 #endif
0260 
0261         d->toolBar->addAction(d->previousAction);
0262         connect(d->previousAction, &QAction::triggered, this, [this] {
0263             d->setCurrentIndex(d->currentIndex - 1);
0264         });
0265 
0266         const auto spacer = new QWidget(this);
0267         spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
0268         d->toolBar->addWidget(spacer);
0269 
0270         d->toolBar->addAction(d->nextAction);
0271         connect(d->nextAction, &QAction::triggered, this, [this] {
0272             d->setCurrentIndex(d->currentIndex + 1);
0273         });
0274         d->nextAction->setEnabled(true);
0275 
0276         mainLayout->addWidget(d->toolBar);
0277     } else {
0278         mainLayout->addWidget(d->toolBar);
0279         d->toolBar->hide();
0280     }
0281 
0282     mainLayout->addLayout(layout);
0283 
0284     d->messageViewer = new MimeTreeParser::Widgets::MessageViewer(this);
0285     d->messageViewer->setMessage(d->messages[0]);
0286     layout->addWidget(d->messageViewer);
0287 
0288     auto buttonBox = new QDialogButtonBox(this);
0289     buttonBox->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin, nullptr, this),
0290                                   style()->pixelMetric(QStyle::PM_LayoutTopMargin, nullptr, this),
0291                                   style()->pixelMetric(QStyle::PM_LayoutRightMargin, nullptr, this),
0292                                   style()->pixelMetric(QStyle::PM_LayoutBottomMargin, nullptr, this));
0293     auto closeButton = buttonBox->addButton(QDialogButtonBox::Close);
0294     connect(closeButton, &QPushButton::pressed, this, &QDialog::accept);
0295     layout->addWidget(buttonBox);
0296 
0297     setMinimumSize(300, 300);
0298     resize(600, 600);
0299 }
0300 
0301 MessageViewerDialog::~MessageViewerDialog() = default;
0302 
0303 QToolBar *MessageViewerDialog::toolBar() const
0304 {
0305     return d->toolBar;
0306 }
0307 
0308 QList<KMime::Message::Ptr> MessageViewerDialog::messages() const
0309 {
0310     return d->messages;
0311 }
0312 
0313 #include "moc_messageviewerdialog.cpp"