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 }