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

0001 // SPDX-FileCopyrightText: 2023 Carl Schwan <carl.schwan@gnupg.com>
0002 // SPDX-License-Identifier: LGPL-2.0-or-later
0003 
0004 #include "messageviewer.h"
0005 
0006 #include "attachmentview_p.h"
0007 #include "messagecontainerwidget_p.h"
0008 #include "mimetreeparser_widgets_debug.h"
0009 #include "urlhandler_p.h"
0010 
0011 #include <KCalendarCore/Event>
0012 #include <KCalendarCore/ICalFormat>
0013 #include <KLocalizedString>
0014 #include <KMessageWidget>
0015 #include <MimeTreeParserCore/AttachmentModel>
0016 #include <MimeTreeParserCore/MessageParser>
0017 #include <MimeTreeParserCore/ObjectTreeParser>
0018 #include <MimeTreeParserCore/PartModel>
0019 
0020 #include <QAction>
0021 #include <QDesktopServices>
0022 #include <QFileDialog>
0023 #include <QFormLayout>
0024 #include <QGroupBox>
0025 #include <QLabel>
0026 #include <QMenu>
0027 #include <QScrollArea>
0028 #include <QStandardPaths>
0029 #include <QVBoxLayout>
0030 #include <qnamespace.h>
0031 
0032 using namespace MimeTreeParser::Widgets;
0033 
0034 class MessageViewer::Private
0035 {
0036 public:
0037     Private(MessageViewer *q_ptr)
0038         : q{q_ptr}
0039         , messageWidget(new KMessageWidget(q_ptr))
0040     {
0041         createActions();
0042 
0043         messageWidget->setCloseButtonVisible(true);
0044         messageWidget->hide();
0045     }
0046 
0047     MessageViewer *q;
0048 
0049     QVBoxLayout *layout = nullptr;
0050     KMime::Message::Ptr message;
0051     MessageParser parser;
0052     QScrollArea *scrollArea = nullptr;
0053     QFormLayout *formLayout = nullptr;
0054     AttachmentView *attachmentView = nullptr;
0055     MimeTreeParser::MessagePart::List selectedParts;
0056     UrlHandler *urlHandler = nullptr;
0057     KMessageWidget *const messageWidget = nullptr;
0058 
0059     QAction *saveAttachmentAction = nullptr;
0060     QAction *openAttachmentAction = nullptr;
0061     QAction *importPublicKeyAction = nullptr;
0062 
0063     void createActions()
0064     {
0065         saveAttachmentAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("&Save Attachment As..."), q);
0066         connect(saveAttachmentAction, &QAction::triggered, q, [this]() {
0067             saveSelectedAttachments();
0068         });
0069 
0070         openAttachmentAction = new QAction(i18nc("to open", "Open"), q);
0071         connect(openAttachmentAction, &QAction::triggered, q, [this]() {
0072             openSelectedAttachments();
0073         });
0074 
0075         importPublicKeyAction = new QAction(i18nc("@action:inmenu", "Import public key"), q);
0076         connect(importPublicKeyAction, &QAction::triggered, q, [this]() {
0077             importPublicKey();
0078         });
0079     }
0080 
0081     void openSelectedAttachments();
0082     void saveSelectedAttachments();
0083     void selectionChanged();
0084     void showContextMenu();
0085     void importPublicKey();
0086     void recursiveBuildViewer(PartModel *parts, QVBoxLayout *layout, const QModelIndex &parent);
0087 };
0088 
0089 void MessageViewer::Private::openSelectedAttachments()
0090 {
0091     Q_ASSERT(selectedParts.count() >= 1);
0092     for (const auto &part : std::as_const(selectedParts)) {
0093         parser.attachments()->openAttachment(part);
0094     }
0095 }
0096 
0097 void MessageViewer::Private::saveSelectedAttachments()
0098 {
0099     Q_ASSERT(selectedParts.count() >= 1);
0100 
0101     for (const auto &part : std::as_const(selectedParts)) {
0102         QString pname = part->filename();
0103         if (pname.isEmpty()) {
0104             pname = i18nc("Fallback when file has no name", "unnamed");
0105         }
0106 
0107         const QString path = QFileDialog::getSaveFileName(q, i18n("Save Attachment As"), pname);
0108         parser.attachments()->saveAttachmentToPath(part, path);
0109     }
0110 }
0111 
0112 void MessageViewer::Private::importPublicKey()
0113 {
0114     Q_ASSERT(selectedParts.count() == 1);
0115     parser.attachments()->importPublicKey(selectedParts[0]);
0116 }
0117 
0118 void MessageViewer::Private::showContextMenu()
0119 {
0120     const int numberOfParts(selectedParts.count());
0121     QMenu menu;
0122     if (numberOfParts == 1) {
0123         const QString mimetype = QString::fromLatin1(selectedParts.first()->mimeType());
0124         if (mimetype == QLatin1StringView("application/pgp-keys")) {
0125             menu.addAction(importPublicKeyAction);
0126         }
0127     }
0128 
0129     menu.addAction(openAttachmentAction);
0130     menu.addAction(saveAttachmentAction);
0131 
0132     menu.exec(QCursor::pos());
0133 }
0134 
0135 void MessageViewer::Private::selectionChanged()
0136 {
0137     const QModelIndexList selectedRows = attachmentView->selectionModel()->selectedRows();
0138     MimeTreeParser::MessagePart::List selectedParts;
0139     selectedParts.reserve(selectedRows.count());
0140     for (const QModelIndex &index : selectedRows) {
0141         auto part = attachmentView->model()->data(index, AttachmentModel::AttachmentPartRole).value<MimeTreeParser::MessagePart::Ptr>();
0142         selectedParts.append(part);
0143     }
0144     this->selectedParts = selectedParts;
0145 }
0146 
0147 MessageViewer::MessageViewer(QWidget *parent)
0148     : QSplitter(Qt::Vertical, parent)
0149     , d(std::make_unique<MessageViewer::Private>(this))
0150 {
0151     setObjectName(QLatin1StringView("MessageViewerSplitter"));
0152     setChildrenCollapsible(false);
0153     setSizes({0});
0154 
0155     addWidget(d->messageWidget);
0156 
0157     auto mainWidget = new QWidget(this);
0158     auto mainLayout = new QVBoxLayout(mainWidget);
0159     mainLayout->setContentsMargins({});
0160     mainLayout->setSpacing(0);
0161 
0162     auto headersArea = new QWidget(mainWidget);
0163     headersArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
0164     mainLayout->addWidget(headersArea);
0165 
0166     d->urlHandler = new UrlHandler(this);
0167 
0168     d->formLayout = new QFormLayout(headersArea);
0169 
0170     auto widget = new QWidget(this);
0171     d->layout = new QVBoxLayout(widget);
0172     d->layout->setSizeConstraint(QLayout::SetMinAndMaxSize);
0173     d->layout->setObjectName(QLatin1StringView("PartLayout"));
0174 
0175     d->scrollArea = new QScrollArea(this);
0176     d->scrollArea->setWidget(widget);
0177     d->scrollArea->setWidgetResizable(true);
0178     d->scrollArea->setBackgroundRole(QPalette::Base);
0179     mainLayout->addWidget(d->scrollArea);
0180     mainLayout->setStretchFactor(d->scrollArea, 2);
0181     setStretchFactor(1, 2);
0182 
0183     d->attachmentView = new AttachmentView(this);
0184     d->attachmentView->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::BottomEdge}));
0185     d->attachmentView->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
0186     addWidget(d->attachmentView);
0187 
0188     connect(d->attachmentView, &AttachmentView::contextMenuRequested, this, [this] {
0189         d->selectionChanged();
0190         d->showContextMenu();
0191     });
0192 }
0193 
0194 MessageViewer::~MessageViewer()
0195 {
0196     QLayoutItem *child;
0197     while ((child = d->layout->takeAt(0)) != nullptr) {
0198         delete child->widget();
0199         delete child;
0200     }
0201 }
0202 
0203 KMime::Message::Ptr MessageViewer::message() const
0204 {
0205     return d->parser.message();
0206 }
0207 
0208 void MessageViewer::Private::recursiveBuildViewer(PartModel *parts, QVBoxLayout *layout, const QModelIndex &parent)
0209 {
0210     for (int i = 0, count = parts->rowCount(parent); i < count; i++) {
0211         const auto type = static_cast<PartModel::Types>(parts->data(parts->index(i, 0, parent), PartModel::TypeRole).toUInt());
0212         const auto content = parts->data(parts->index(i, 0, parent), PartModel::ContentRole).toString();
0213 
0214         const auto signatureInfo = parts->data(parts->index(i, 0, parent), PartModel::SignatureDetails).value<SignatureInfo>();
0215         const auto isSigned = parts->data(parts->index(i, 0, parent), PartModel::IsSignedRole).toBool();
0216         const auto signatureSecurityLevel =
0217             static_cast<PartModel::SecurityLevel>(parts->data(parts->index(i, 0, parent), PartModel::SignatureSecurityLevelRole).toInt());
0218 
0219         const auto encryptionInfo = parts->data(parts->index(i, 0, parent), PartModel::EncryptionDetails).value<SignatureInfo>();
0220         const auto isEncrypted = parts->data(parts->index(i, 0, parent), PartModel::IsEncryptedRole).toBool();
0221         const auto encryptionSecurityLevel =
0222             static_cast<PartModel::SecurityLevel>(parts->data(parts->index(i, 0, parent), PartModel::EncryptionSecurityLevelRole).toInt());
0223 
0224         const auto displayEncryptionInfo =
0225             i == 0 || parts->data(parts->index(i - 1, 0, parent), PartModel::EncryptionDetails).value<SignatureInfo>().keyId != encryptionInfo.keyId;
0226 
0227         const auto displaySignatureInfo =
0228             i == 0 || parts->data(parts->index(i - 1, 0, parent), PartModel::SignatureDetails).value<SignatureInfo>().keyId != signatureInfo.keyId;
0229 
0230         switch (type) {
0231         case PartModel::Types::Plain: {
0232             auto container = new MessageWidgetContainer(isSigned,
0233                                                         signatureInfo,
0234                                                         signatureSecurityLevel,
0235                                                         displaySignatureInfo,
0236                                                         isEncrypted,
0237                                                         encryptionInfo,
0238                                                         encryptionSecurityLevel,
0239                                                         displayEncryptionInfo,
0240                                                         urlHandler);
0241             auto label = new QLabel(content);
0242             label->setTextInteractionFlags(Qt::TextBrowserInteraction);
0243             label->setOpenExternalLinks(true);
0244             label->setWordWrap(true);
0245             container->layout()->addWidget(label);
0246             layout->addWidget(container);
0247             break;
0248         }
0249         case PartModel::Types::Ical: {
0250             auto container = new MessageWidgetContainer(isSigned,
0251                                                         signatureInfo,
0252                                                         signatureSecurityLevel,
0253                                                         displaySignatureInfo,
0254                                                         isEncrypted,
0255                                                         encryptionInfo,
0256                                                         encryptionSecurityLevel,
0257                                                         displayEncryptionInfo,
0258                                                         urlHandler);
0259 
0260             KCalendarCore::ICalFormat format;
0261             auto incidence = format.fromString(content);
0262 
0263             auto widget = new QGroupBox(container);
0264             widget->setTitle(i18n("Invitation"));
0265 
0266             auto incidenceLayout = new QFormLayout(widget);
0267             incidenceLayout->addRow(i18n("&Summary:"), new QLabel(incidence->summary()));
0268             incidenceLayout->addRow(i18n("&Organizer:"), new QLabel(incidence->organizer().fullName()));
0269             if (incidence->location().length() > 0) {
0270                 incidenceLayout->addRow(i18n("&Location:"), new QLabel(incidence->location()));
0271             }
0272             incidenceLayout->addRow(i18n("&Start date:"), new QLabel(incidence->dtStart().toLocalTime().toString()));
0273             if (const auto event = incidence.dynamicCast<KCalendarCore::Event>()) {
0274                 incidenceLayout->addRow(i18n("&End date:"), new QLabel(event->dtEnd().toLocalTime().toString()));
0275             }
0276             if (incidence->description().length() > 0) {
0277                 incidenceLayout->addRow(i18n("&Details:"), new QLabel(incidence->description()));
0278             }
0279 
0280             container->layout()->addWidget(widget);
0281 
0282             layout->addWidget(container);
0283             break;
0284         }
0285         case PartModel::Types::Encapsulated: {
0286             auto container = new MessageWidgetContainer(isSigned,
0287                                                         signatureInfo,
0288                                                         signatureSecurityLevel,
0289                                                         displaySignatureInfo,
0290                                                         isEncrypted,
0291                                                         encryptionInfo,
0292                                                         encryptionSecurityLevel,
0293                                                         displayEncryptionInfo,
0294                                                         urlHandler);
0295 
0296             auto groupBox = new QGroupBox(container);
0297             groupBox->setSizePolicy(QSizePolicy::MinimumExpanding, q->sizePolicy().verticalPolicy());
0298             groupBox->setTitle(i18n("Encapsulated email"));
0299 
0300             auto encapsulatedLayout = new QVBoxLayout(groupBox);
0301 
0302             auto header = new QWidget(groupBox);
0303             auto headerLayout = new QFormLayout(header);
0304             const auto from = parts->data(parts->index(i, 0, parent), PartModel::SenderRole).toString();
0305             const auto date = parts->data(parts->index(i, 0, parent), PartModel::DateRole).toDateTime();
0306             headerLayout->addRow(i18n("From:"), new QLabel(from));
0307             headerLayout->addRow(i18n("Date:"), new QLabel(date.toLocalTime().toString()));
0308 
0309             encapsulatedLayout->addWidget(header);
0310 
0311             recursiveBuildViewer(parts, encapsulatedLayout, parts->index(i, 0, parent));
0312 
0313             container->layout()->addWidget(groupBox);
0314 
0315             layout->addWidget(container);
0316             break;
0317         }
0318 
0319         case PartModel::Types::Error: {
0320             const auto errorString = parts->data(parts->index(i, 0, parent), PartModel::ErrorString).toString();
0321             auto errorWidget = new KMessageWidget(errorString);
0322             errorWidget->setCloseButtonVisible(false);
0323             errorWidget->setMessageType(KMessageWidget::MessageType::Error);
0324             QObject::connect(errorWidget, &KMessageWidget::linkActivated, errorWidget, [this, errorWidget](const QString &link) {
0325                 QUrl url(link);
0326                 if (url.path() == QStringLiteral("showCertificate")) {
0327                     urlHandler->handleClick(QUrl(link), errorWidget->window()->windowHandle());
0328                 }
0329             });
0330             errorWidget->setWordWrap(true);
0331             layout->addWidget(errorWidget);
0332             break;
0333         }
0334         default:
0335             qCWarning(MIMETREEPARSER_WIDGET_LOG) << parts->data(parts->index(i, 0, parent), PartModel::ContentRole) << type;
0336         }
0337     }
0338 }
0339 
0340 class HeaderLabel : public QLabel
0341 {
0342 public:
0343     HeaderLabel(const QString &content)
0344         : QLabel(content)
0345     {
0346         setWordWrap(true);
0347         setTextFormat(Qt::PlainText);
0348         setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
0349     }
0350 
0351     void resizeEvent(QResizeEvent *event) override
0352     {
0353         int height = heightForWidth(width());
0354         setMaximumHeight(height);
0355         setMinimumHeight(height);
0356 
0357         QLabel::resizeEvent(event);
0358     }
0359 };
0360 
0361 void MessageViewer::setMessage(const KMime::Message::Ptr message)
0362 {
0363     setUpdatesEnabled(false);
0364     d->parser.setMessage(message);
0365 
0366     connect(d->parser.attachments(), &AttachmentModel::info, this, [this](const QString &message) {
0367         d->messageWidget->setMessageType(KMessageWidget::Information);
0368         d->messageWidget->setText(message);
0369         d->messageWidget->animatedShow();
0370     });
0371 
0372     connect(d->parser.attachments(), &AttachmentModel::errorOccurred, this, [this](const QString &message) {
0373         d->messageWidget->setMessageType(KMessageWidget::Error);
0374         d->messageWidget->setText(message);
0375         d->messageWidget->animatedShow();
0376     });
0377 
0378     for (int i = d->formLayout->rowCount() - 1; i >= 0; i--) {
0379         d->formLayout->removeRow(i);
0380     }
0381     if (!d->parser.subject().isEmpty()) {
0382         const auto label = new QLabel(d->parser.subject());
0383         label->setTextFormat(Qt::PlainText);
0384         d->formLayout->addRow(i18n("&Subject:"), label);
0385     }
0386     if (!d->parser.from().isEmpty()) {
0387         d->formLayout->addRow(i18n("&From:"), new HeaderLabel(d->parser.from()));
0388     }
0389     if (!d->parser.sender().isEmpty() && d->parser.from() != d->parser.sender()) {
0390         d->formLayout->addRow(i18n("&Sender:"), new HeaderLabel(d->parser.sender()));
0391     }
0392     if (!d->parser.to().isEmpty()) {
0393         d->formLayout->addRow(i18n("&To:"), new HeaderLabel(d->parser.to()));
0394     }
0395     if (!d->parser.cc().isEmpty()) {
0396         d->formLayout->addRow(i18n("&CC:"), new HeaderLabel(d->parser.cc()));
0397     }
0398     if (!d->parser.bcc().isEmpty()) {
0399         d->formLayout->addRow(i18n("&BCC:"), new HeaderLabel(d->parser.bcc()));
0400     }
0401     if (!d->parser.date().isNull()) {
0402         d->formLayout->addRow(i18n("&Date:"), new HeaderLabel(QLocale::system().toString(d->parser.date().toLocalTime())));
0403     }
0404 
0405     const auto parts = d->parser.parts();
0406 
0407     QLayoutItem *child;
0408     while ((child = d->layout->takeAt(0)) != nullptr) {
0409         delete child->widget();
0410         delete child;
0411     }
0412 
0413     d->recursiveBuildViewer(parts, d->layout, {});
0414     d->layout->addStretch();
0415 
0416     d->attachmentView->setModel(d->parser.attachments());
0417     d->attachmentView->setVisible(d->parser.attachments()->rowCount() > 0);
0418 
0419     connect(d->attachmentView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this] {
0420         d->selectionChanged();
0421     });
0422 
0423     connect(d->attachmentView, &QAbstractItemView::doubleClicked, this, [this](const QModelIndex &) {
0424         // Since this is only emitted if a valid index is double clicked we can assume
0425         // that the first click of the double click set the selection accordingly.
0426         d->openSelectedAttachments();
0427     });
0428 
0429     setUpdatesEnabled(true);
0430 }
0431 
0432 void MessageViewer::print(QPainter *painter, int width)
0433 {
0434     const auto oldSize = size();
0435     resize(width - 30, oldSize.height());
0436     d->scrollArea->setFrameShape(QFrame::NoFrame);
0437     render(painter);
0438     d->scrollArea->setFrameShape(QFrame::StyledPanel);
0439     resize(oldSize);
0440 }