diff --git a/src/core/attachmentmodel.cpp b/src/core/attachmentmodel.cpp index e71b531..9053266 100644 --- a/src/core/attachmentmodel.cpp +++ b/src/core/attachmentmodel.cpp @@ -1,413 +1,412 @@ // SPDX-FileCopyrightText: 2016 Sandro Knauß // SPDX-FileCopyCopyright: 2017 Christian Mollekopf // SPDX-License-Identifier: LGPL-2.0-or-later #include "attachmentmodel.h" #include "mimetreeparser_core_debug.h" -#include "objecttreeparser.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #include #include #include #endif namespace { QString sizeHuman(float size) { QStringList list; list << QStringLiteral("KB") << QStringLiteral("MB") << QStringLiteral("GB") << QStringLiteral("TB"); QStringListIterator i(list); QString unit = QStringLiteral("Bytes"); while (size >= 1024.0 && i.hasNext()) { unit = i.next(); size /= 1024.0; } if (unit == QStringLiteral("Bytes")) { return QString().setNum(size) + QStringLiteral(" ") + unit; } else { return QString().setNum(size, 'f', 2) + QStringLiteral(" ") + unit; } } // SPDX-SnippetBegin // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: GPL-3.0-only #define WINDOWS_DEVICES_PATTERN "(CON|AUX|PRN|NUL|COM[1-9]|LPT[1-9])(\\..*)?" // Naming a file like a device name will break on Windows, even if it is // "com1.txt". Since we are cross-platform, we generally disallow such file // names. const QRegularExpression &windowsDeviceNoSubDirPattern() { static const QRegularExpression rc(QStringLiteral("^" WINDOWS_DEVICES_PATTERN "$"), QRegularExpression::CaseInsensitiveOption); Q_ASSERT(rc.isValid()); return rc; } const QRegularExpression &windowsDeviceSubDirPattern() { static const QRegularExpression rc(QStringLiteral("^.*[/\\\\]" WINDOWS_DEVICES_PATTERN "$"), QRegularExpression::CaseInsensitiveOption); Q_ASSERT(rc.isValid()); return rc; } /* Validate a file base name, check for forbidden characters/strings. */ #define SLASHES "/\\" static const char notAllowedCharsSubDir[] = ",^@={}[]~!?:&*\"|#%<>$\"'();`' "; static const char notAllowedCharsNoSubDir[] = ",^@={}[]~!?:&*\"|#%<>$\"'();`' " SLASHES; static const char *notAllowedSubStrings[] = {".."}; bool validateFileName(const QString &name, bool allowDirectories) { if (name.isEmpty()) { return false; } // Characters const char *notAllowedChars = allowDirectories ? notAllowedCharsSubDir : notAllowedCharsNoSubDir; for (const char *c = notAllowedChars; *c; c++) { if (name.contains(QLatin1Char(*c))) { return false; } } // Substrings const int notAllowedSubStringCount = sizeof(notAllowedSubStrings) / sizeof(const char *); for (int s = 0; s < notAllowedSubStringCount; s++) { const QLatin1String notAllowedSubString(notAllowedSubStrings[s]); if (name.contains(notAllowedSubString)) { return false; } } // Windows devices bool matchesWinDevice = name.contains(windowsDeviceNoSubDirPattern()); if (!matchesWinDevice && allowDirectories) { matchesWinDevice = name.contains(windowsDeviceSubDirPattern()); } return !matchesWinDevice; } // SPDX-SnippetEnd } #ifdef Q_OS_WIN struct WindowFile { std::wstring fileName; std::wstring dirName; HANDLE handle; }; #endif class AttachmentModelPrivate { public: AttachmentModelPrivate(AttachmentModel *q_ptr, const std::shared_ptr &parser); AttachmentModel *q; QMimeDatabase mimeDb; std::shared_ptr mParser; MimeTreeParser::MessagePart::List mAttachments; #ifdef Q_OS_WIN std::vector mOpenFiles; #endif }; AttachmentModelPrivate::AttachmentModelPrivate(AttachmentModel *q_ptr, const std::shared_ptr &parser) : q(q_ptr) , mParser(parser) { mAttachments = mParser->collectAttachmentParts(); } AttachmentModel::AttachmentModel(std::shared_ptr parser) : QAbstractTableModel() , d(std::unique_ptr(new AttachmentModelPrivate(this, parser))) { } AttachmentModel::~AttachmentModel() { #ifdef Q_OS_WIN for (const auto &file : d->mOpenFiles) { // As owner of the file we need to close our handle first // With FILE_SHARE_DELETE we have ensured that all _other_ processes must // have opened the file with FILE_SHARE_DELETE, too. if (!CloseHandle(file.handle)) { // Always get the last error before calling any Qt functions that may // use Windows system calls. DWORD err = GetLastError(); qWarning() << "Unable to close handle for file" << QString::fromStdWString(file.fileName) << err; } if (!DeleteFileW(file.fileName.c_str())) { DWORD err = GetLastError(); qWarning() << "Unable to delete file" << QString::fromStdWString(file.fileName) << err; } if (!RemoveDirectoryW(file.dirName.c_str())) { DWORD err = GetLastError(); qWarning() << "Unable to delete temporary directory" << QString::fromStdWString(file.dirName) << err; } } #endif } QHash AttachmentModel::roleNames() const { return { {TypeRole, QByteArrayLiteral("type")}, {NameRole, QByteArrayLiteral("name")}, {SizeRole, QByteArrayLiteral("size")}, {IconRole, QByteArrayLiteral("iconName")}, {IsEncryptedRole, QByteArrayLiteral("encrypted")}, {IsSignedRole, QByteArrayLiteral("signed")}, }; } QVariant AttachmentModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch (section) { case NameColumn: return i18ndc("mimetreeparser", "@title:column", "Name"); case SizeColumn: return i18ndc("mimetreeparser", "@title:column", "Size"); case IsEncryptedColumn: return i18ndc("mimetreeparser", "@title:column", "Encrypted"); case IsSignedColumn: return i18ndc("mimetreeparser", "@title:column", "Signed"); } } return {}; } QVariant AttachmentModel::data(const QModelIndex &index, int role) const { const auto row = index.row(); const auto column = index.column(); const auto part = d->mAttachments.at(row); Q_ASSERT(part); auto node = part->node(); if (!node) { qWarning() << "no content for attachment"; return {}; } const auto mimetype = d->mimeDb.mimeTypeForName(QString::fromLatin1(part->mimeType())); const auto content = node->encodedContent(); switch (column) { case NameColumn: switch (role) { case TypeRole: return mimetype.name(); case Qt::DisplayRole: case NameRole: return part->filename(); case IconRole: return mimetype.iconName(); case Qt::DecorationRole: return QIcon::fromTheme(mimetype.iconName()); case SizeRole: return sizeHuman(content.size()); case IsEncryptedRole: return part->encryptions().size() > 0; case IsSignedRole: return part->signatures().size() > 0; case AttachmentPartRole: return QVariant::fromValue(part); default: return {}; } case SizeColumn: switch (role) { case Qt::DisplayRole: return sizeHuman(content.size()); default: return {}; } case IsEncryptedColumn: switch (role) { case Qt::CheckStateRole: return part->encryptions().size() > 0 ? Qt::Checked : Qt::Unchecked; default: return {}; } case IsSignedColumn: switch (role) { case Qt::CheckStateRole: return part->signatures().size() > 0 ? Qt::Checked : Qt::Unchecked; default: return {}; } default: return {}; } } QString AttachmentModel::saveAttachmentToPath(const int row, const QString &path) { const auto part = d->mAttachments.at(row); return saveAttachmentToPath(part, path); } QString AttachmentModel::saveAttachmentToPath(const MimeTreeParser::MessagePart::Ptr &part, const QString &path) { Q_ASSERT(part); auto node = part->node(); auto data = node->decodedContent(); // This is necessary to store messages embedded messages (EncapsulatedRfc822MessagePart) if (data.isEmpty()) { data = node->encodedContent(); } if (part->isText()) { // convert CRLF to LF before writing text attachments to disk data = KMime::CRLFtoLF(data); } QFile f(path); if (!f.open(QIODevice::ReadWrite)) { qCWarning(MIMETREEPARSER_CORE_LOG) << "Failed to write attachment to file:" << path << " Error: " << f.errorString(); Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to save attachment.")); return {}; } f.write(data); f.close(); qCInfo(MIMETREEPARSER_CORE_LOG) << "Wrote attachment to file: " << path; return path; } bool AttachmentModel::openAttachment(const int row) { const auto part = d->mAttachments.at(row); return openAttachment(part); } bool AttachmentModel::openAttachment(const MimeTreeParser::MessagePart::Ptr &message) { QString fileName = message->filename(); QTemporaryDir tempDir(QDir::tempPath() + QLatin1Char('/') + qGuiApp->applicationName() + QStringLiteral(".XXXXXX")); // TODO: We need some cleanup here. Otherwise the files will stay forever on Windows. tempDir.setAutoRemove(false); if (message->filename().isEmpty() || !validateFileName(fileName, false)) { const auto mimetype = d->mimeDb.mimeTypeForName(QString::fromLatin1(message->mimeType())); fileName = tempDir.filePath(i18n("attachment") + QLatin1Char('.') + mimetype.preferredSuffix()); } else { fileName = tempDir.filePath(message->filename()); } const auto filePath = saveAttachmentToPath(message, fileName); if (filePath.isEmpty()) { Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to write attachment for opening.")); return false; } #ifdef Q_OS_WIN std::wstring fileNameStr = filePath.toStdWString(); HANDLE hFile = CreateFileW(fileNameStr.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, // allow other processes to delete it NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, // Using FILE_FLAG_DELETE_ON_CLOSE causes some // applications like windows zip not to open the // file. NULL // no template ); if (hFile == INVALID_HANDLE_VALUE) { Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to open attachment.")); QFile file(fileName); file.remove(); return false; } d->mOpenFiles.push_back({fileNameStr, tempDir.path().toStdWString(), hFile}); #endif if (!QDesktopServices::openUrl(QUrl::fromLocalFile(filePath))) { Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to open attachment.")); return false; } return true; } bool AttachmentModel::importPublicKey(const int row) { const auto part = d->mAttachments.at(row); return importPublicKey(part); } bool AttachmentModel::importPublicKey(const MimeTreeParser::MessagePart::Ptr &part) { Q_ASSERT(part); const QByteArray certData = part->node()->decodedContent(); QGpgME::ImportJob *importJob = QGpgME::openpgp()->importJob(); connect(importJob, &QGpgME::AbstractImportJob::result, this, [this](const GpgME::ImportResult &result) { if (result.numConsidered() == 0) { Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "No keys were found in this attachment")); return; } else { QString message = i18ndcp("mimetreeparser", "@info", "one key imported", "%1 keys imported", result.numImported()); if (result.numUnchanged() != 0) { message += QStringLiteral("\n") + i18ndcp("mimetreeparser", "@info", "one key was already imported", "%1 keys were already imported", result.numUnchanged()); } Q_EMIT info(message); } }); GpgME::Error err = importJob->start(certData); return !err; } int AttachmentModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) { return d->mAttachments.size(); } return 0; } int AttachmentModel::columnCount(const QModelIndex &parent) const { if (!parent.isValid()) { return ColumnCount; } return 0; } #include "moc_attachmentmodel.cpp" diff --git a/src/core/messageparser.cpp b/src/core/messageparser.cpp index 84f0129..01aa3c9 100644 --- a/src/core/messageparser.cpp +++ b/src/core/messageparser.cpp @@ -1,229 +1,228 @@ // SPDX-FileCopyrightText: 2016 Christian Mollekopf // SPDX-License-Identifier: LGPL-2.0-or-later #include "messageparser.h" #include "attachmentmodel.h" #include "mimetreeparser_core_debug.h" #include "objecttreeparser.h" -#include "partmodel.h" #include #include namespace { template const T *findHeader(KMime::Content *content, KMime::Content *protectedHeaderNode) { if (protectedHeaderNode) { auto header = protectedHeaderNode->header(); if (header) { return header; } } auto header = content->header(); if (header || !content->parent()) { return header; } return findHeader(content->parent(), nullptr); } const KMime::Headers::Base *findHeader(KMime::Content *content, const char *headerType) { const auto header = content->headerByType(headerType); if (header || !content->parent()) { return header; } return findHeader(content->parent(), headerType); } } class MessagePartPrivate { public: std::shared_ptr mParser; KMime::Message::Ptr mMessage; KMime::Content *protectedHeaderNode = nullptr; std::unique_ptr ownedContent; }; MessageParser::MessageParser(QObject *parent) : QObject(parent) , d(std::unique_ptr(new MessagePartPrivate)) { } MessageParser::~MessageParser() { } KMime::Message::Ptr MessageParser::message() const { return d->mMessage; } void MessageParser::setMessage(const KMime::Message::Ptr message) { if (message == d->mMessage) { return; } if (!message) { qCWarning(MIMETREEPARSER_CORE_LOG) << Q_FUNC_INFO << "Empty message given"; return; } d->mMessage = message; QElapsedTimer time; time.start(); auto parser = std::make_shared(); parser->parseObjectTree(message.data()); qCDebug(MIMETREEPARSER_CORE_LOG) << "Message parsing took: " << time.elapsed(); parser->decryptAndVerify(); qCDebug(MIMETREEPARSER_CORE_LOG) << "Message parsing and decryption/verification: " << time.elapsed(); d->mParser = parser; const auto contentParts = parser->collectContentParts(); for (const auto &part : contentParts) { if (!part->node()) { continue; } const auto contentType = part->node()->contentType(); if (contentType && contentType->hasParameter(QStringLiteral("protected-headers"))) { const auto contentDisposition = part->node()->contentDisposition(); // Check for legacy format for protected-headers if (contentDisposition && contentDisposition->disposition() == KMime::Headers::CDinline) { d->ownedContent = std::make_unique(); // we put the decoded content as encoded content of the new node // as the header are inline in part->node() d->ownedContent->setContent(part->node()->decodedContent()); d->ownedContent->parse(); d->protectedHeaderNode = d->ownedContent.get(); break; } d->protectedHeaderNode = part->node(); break; } } Q_EMIT htmlChanged(); } bool MessageParser::loaded() const { return bool{d->mParser}; } QString MessageParser::structureAsString() const { if (!d->mParser) { return {}; } return d->mParser->structureAsString(); } PartModel *MessageParser::parts() const { if (!d->mParser) { return nullptr; } const auto model = new PartModel(d->mParser); return model; } AttachmentModel *MessageParser::attachments() const { if (!d->mParser) { return nullptr; } auto attachmentModel = new AttachmentModel(d->mParser); attachmentModel->setParent(const_cast(this)); return attachmentModel; } QString MessageParser::subject() const { if (d->mMessage) { const auto header = findHeader(d->mMessage.get(), d->protectedHeaderNode); if (header) { return header->asUnicodeString(); } } return QString(); } QString MessageParser::from() const { if (d->mMessage) { const auto header = findHeader(d->mMessage.get(), d->protectedHeaderNode); if (header) { return header->displayString(); } } return QString(); } QString MessageParser::sender() const { if (d->mMessage) { const auto header = findHeader(d->mMessage.get(), d->protectedHeaderNode); if (header) { return header->displayString(); } } return QString(); } QString MessageParser::to() const { if (d->mMessage) { const auto header = findHeader(d->mMessage.get(), d->protectedHeaderNode); if (!header) { return {}; } return header->displayString(); } return QString(); } QString MessageParser::cc() const { if (d->mMessage) { const auto header = findHeader(d->mMessage.get(), d->protectedHeaderNode); if (!header) { return {}; } return header->displayString(); } return QString(); } QString MessageParser::bcc() const { if (d->mMessage) { const auto header = findHeader(d->mMessage.get(), d->protectedHeaderNode); if (!header) { return {}; } return header->displayString(); } return QString(); } QDateTime MessageParser::date() const { if (d->mMessage) { const auto header = findHeader(d->mMessage.get(), d->protectedHeaderNode); if (header) { return header->dateTime(); } } return QDateTime(); } #include "moc_messageparser.cpp" diff --git a/src/core/utils.cpp b/src/core/utils.cpp index 31f99c8..01b93db 100644 --- a/src/core/utils.cpp +++ b/src/core/utils.cpp @@ -1,61 +1,60 @@ // SPDX-FileCopyrightText: 2016 Sandro Knauß // SPDX-FileCopyrightText: 2023 g10 Code GmbH // SPDX-FileContributor: Carl Schwan // SPDX-License-Identifier: LGPL-2.0-or-later #include "utils.h" #include -#include using namespace MimeTreeParser; KMime::Content *MimeTreeParser::findTypeInDirectChildren(KMime::Content *content, const QByteArray &mimeType) { const auto contents = content->contents(); for (const auto child : contents) { if ((!child->contentType()->isEmpty()) && (mimeType == child->contentType()->mimeType())) { return child; } } return nullptr; } QString MimeTreeParser::decryptRecipientsToHtml(const std::vector> &recipients, const QGpgME::Protocol *cryptoProto) { QString text = QStringLiteral("
    "); for (const auto &recipientIt : recipients) { const auto recipient = recipientIt.first; const auto key = recipientIt.second; if (key.keyID()) { QString displayName = QString::fromLatin1(key.userID(0).id()); if (cryptoProto == QGpgME::smime()) { Kleo::DN dn(displayName); displayName = dnToDisplayName(dn); } displayName = displayName.toHtmlEscaped(); const auto link = QStringLiteral("messageviewer:showCertificate#%1 ### %2 ### %3") .arg(cryptoProto->displayName(), cryptoProto->name(), QString::fromLatin1(key.keyID())); text += QStringLiteral("
  • %1 (0x%3)
  • ").arg(displayName, link, QString::fromLatin1(key.keyID())); } else { const auto link = QStringLiteral("messageviewer:showCertificate#%1 ### %2 ### %3") .arg(cryptoProto->displayName(), cryptoProto->name(), QString::fromLatin1(recipient.keyID())); text += QStringLiteral("
  • %1 (0x%3)
  • ").arg(i18nc("@info", "Unknown Key"), link, QString::fromLatin1(recipient.keyID())); } } text += QStringLiteral("
"); return text; } QString MimeTreeParser::dnToDisplayName(const Kleo::DN &dn) { QString displayName = dn[QStringLiteral("CN")]; if (displayName.isEmpty()) { // In case there is no CN, put the full DN as display name displayName = dn.prettyDN(); } else if (!dn[QStringLiteral("O")].isEmpty()) { displayName += i18nc("Separator", " - ") + dn[QStringLiteral("O")]; } return displayName; } diff --git a/src/widgets/messageviewer.cpp b/src/widgets/messageviewer.cpp index 94164ff..0d5b51c 100644 --- a/src/widgets/messageviewer.cpp +++ b/src/widgets/messageviewer.cpp @@ -1,408 +1,407 @@ // SPDX-FileCopyrightText: 2023 Carl Schwan // SPDX-License-Identifier: LGPL-2.0-or-later #include "messageviewer.h" #include "attachmentview_p.h" #include "messagecontainerwidget_p.h" #include "mimetreeparser_widgets_debug.h" #include "urlhandler_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include #include #include using namespace MimeTreeParser::Widgets; class MessageViewer::Private { public: Private(MessageViewer *q_ptr) : q{q_ptr} , messageWidget(new KMessageWidget(q_ptr)) { createActions(); messageWidget->setCloseButtonVisible(true); messageWidget->hide(); } MessageViewer *q; QVBoxLayout *layout = nullptr; KMime::Message::Ptr message; MessageParser parser; QScrollArea *scrollArea = nullptr; QFormLayout *formLayout = nullptr; AttachmentView *attachmentView = nullptr; MimeTreeParser::MessagePart::List selectedParts; UrlHandler *urlHandler = nullptr; KMessageWidget *const messageWidget = nullptr; QAction *saveAttachmentAction = nullptr; QAction *openAttachmentAction = nullptr; QAction *importPublicKeyAction = nullptr; void createActions() { saveAttachmentAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("&Save Attachment As..."), q); connect(saveAttachmentAction, &QAction::triggered, q, [this]() { saveSelectedAttachments(); }); openAttachmentAction = new QAction(i18nc("to open", "Open"), q); connect(openAttachmentAction, &QAction::triggered, q, [this]() { openSelectedAttachments(); }); importPublicKeyAction = new QAction(i18nc("@action:inmenu", "Import public key"), q); connect(importPublicKeyAction, &QAction::triggered, q, [this]() { importPublicKey(); }); } void openSelectedAttachments(); void saveSelectedAttachments(); void selectionChanged(); void showContextMenu(); void importPublicKey(); void recursiveBuildViewer(PartModel *parts, QVBoxLayout *layout, const QModelIndex &parent); }; void MessageViewer::Private::openSelectedAttachments() { Q_ASSERT(selectedParts.count() >= 1); for (const auto &part : std::as_const(selectedParts)) { parser.attachments()->openAttachment(part); } } void MessageViewer::Private::saveSelectedAttachments() { Q_ASSERT(selectedParts.count() >= 1); for (const auto &part : std::as_const(selectedParts)) { QString pname = part->filename(); if (pname.isEmpty()) { pname = i18nc("Fallback when file has no name", "unnamed"); } const QString path = QFileDialog::getSaveFileName(q, i18n("Save Attachment As"), pname); parser.attachments()->saveAttachmentToPath(part, path); } } void MessageViewer::Private::importPublicKey() { Q_ASSERT(selectedParts.count() == 1); parser.attachments()->importPublicKey(selectedParts[0]); } void MessageViewer::Private::showContextMenu() { const int numberOfParts(selectedParts.count()); QMenu menu; if (numberOfParts == 1) { const QString mimetype = QString::fromLatin1(selectedParts.first()->mimeType()); if (mimetype == QLatin1String("application/pgp-keys")) { menu.addAction(importPublicKeyAction); } } menu.addAction(openAttachmentAction); menu.addAction(saveAttachmentAction); menu.exec(QCursor::pos()); } void MessageViewer::Private::selectionChanged() { const QModelIndexList selectedRows = attachmentView->selectionModel()->selectedRows(); MimeTreeParser::MessagePart::List selectedParts; selectedParts.reserve(selectedRows.count()); for (const QModelIndex &index : selectedRows) { auto part = attachmentView->model()->data(index, AttachmentModel::AttachmentPartRole).value(); selectedParts.append(part); } this->selectedParts = selectedParts; } MessageViewer::MessageViewer(QWidget *parent) : QSplitter(Qt::Vertical, parent) , d(std::make_unique(this)) { setObjectName(QLatin1StringView("MessageViewerSplitter")); setChildrenCollapsible(false); setSizes({0}); addWidget(d->messageWidget); auto headersArea = new QWidget(this); headersArea->setSizePolicy(sizePolicy().horizontalPolicy(), QSizePolicy::Expanding); addWidget(headersArea); d->urlHandler = new UrlHandler(this); d->formLayout = new QFormLayout(headersArea); auto widget = new QWidget(this); d->layout = new QVBoxLayout(widget); d->layout->setSizeConstraint(QLayout::SetMinAndMaxSize); d->layout->setObjectName(QLatin1StringView("PartLayout")); d->scrollArea = new QScrollArea(this); d->scrollArea->setWidget(widget); d->scrollArea->setWidgetResizable(true); d->scrollArea->setBackgroundRole(QPalette::Base); addWidget(d->scrollArea); setStretchFactor(2, 2); d->attachmentView = new AttachmentView(this); d->attachmentView->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::BottomEdge})); d->attachmentView->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); addWidget(d->attachmentView); connect(d->attachmentView, &AttachmentView::contextMenuRequested, this, [this] { d->selectionChanged(); d->showContextMenu(); }); setMinimumSize(600, 600); } MessageViewer::~MessageViewer() { QLayoutItem *child; while ((child = d->layout->takeAt(0)) != nullptr) { delete child->widget(); delete child; } } KMime::Message::Ptr MessageViewer::message() const { return d->parser.message(); } void MessageViewer::Private::recursiveBuildViewer(PartModel *parts, QVBoxLayout *layout, const QModelIndex &parent) { for (int i = 0, count = parts->rowCount(parent); i < count; i++) { const auto type = static_cast(parts->data(parts->index(i, 0, parent), PartModel::TypeRole).toUInt()); const auto content = parts->data(parts->index(i, 0, parent), PartModel::ContentRole).toString(); const auto signatureInfo = parts->data(parts->index(i, 0, parent), PartModel::SignatureDetails).value(); const auto isSigned = parts->data(parts->index(i, 0, parent), PartModel::IsSignedRole).toBool(); const auto signatureSecurityLevel = static_cast(parts->data(parts->index(i, 0, parent), PartModel::SignatureSecurityLevelRole).toInt()); const auto encryptionInfo = parts->data(parts->index(i, 0, parent), PartModel::EncryptionDetails).value(); const auto isEncrypted = parts->data(parts->index(i, 0, parent), PartModel::IsEncryptedRole).toBool(); const auto encryptionSecurityLevel = static_cast(parts->data(parts->index(i, 0, parent), PartModel::EncryptionSecurityLevelRole).toInt()); const auto displayEncryptionInfo = i == 0 || parts->data(parts->index(i - 1, 0, parent), PartModel::EncryptionDetails).value().keyId != encryptionInfo.keyId; const auto displaySignatureInfo = i == 0 || parts->data(parts->index(i - 1, 0, parent), PartModel::SignatureDetails).value().keyId != signatureInfo.keyId; switch (type) { case PartModel::Types::Plain: { auto container = new MessageWidgetContainer(isSigned, signatureInfo, signatureSecurityLevel, displaySignatureInfo, isEncrypted, encryptionInfo, encryptionSecurityLevel, displayEncryptionInfo, urlHandler); auto label = new QLabel(content); label->setTextInteractionFlags(Qt::TextSelectableByMouse); label->setWordWrap(true); container->layout()->addWidget(label); layout->addWidget(container); break; } case PartModel::Types::Ical: { auto container = new MessageWidgetContainer(isSigned, signatureInfo, signatureSecurityLevel, displaySignatureInfo, isEncrypted, encryptionInfo, encryptionSecurityLevel, displayEncryptionInfo, urlHandler); KCalendarCore::ICalFormat format; auto incidence = format.fromString(content); auto widget = new QGroupBox(container); widget->setTitle(i18n("Invitation")); auto incidenceLayout = new QFormLayout(widget); incidenceLayout->addRow(i18n("&Summary:"), new QLabel(incidence->summary())); incidenceLayout->addRow(i18n("&Organizer:"), new QLabel(incidence->organizer().fullName())); if (incidence->location().length() > 0) { incidenceLayout->addRow(i18n("&Location:"), new QLabel(incidence->location())); } incidenceLayout->addRow(i18n("&Start date:"), new QLabel(incidence->dtStart().toLocalTime().toString())); if (const auto event = incidence.dynamicCast()) { incidenceLayout->addRow(i18n("&End date:"), new QLabel(event->dtEnd().toLocalTime().toString())); } if (incidence->description().length() > 0) { incidenceLayout->addRow(i18n("&Details:"), new QLabel(incidence->description())); } container->layout()->addWidget(widget); layout->addWidget(container); break; } case PartModel::Types::Encapsulated: { auto container = new MessageWidgetContainer(isSigned, signatureInfo, signatureSecurityLevel, displaySignatureInfo, isEncrypted, encryptionInfo, encryptionSecurityLevel, displayEncryptionInfo, urlHandler); auto groupBox = new QGroupBox(container); groupBox->setSizePolicy(QSizePolicy::MinimumExpanding, q->sizePolicy().verticalPolicy()); groupBox->setTitle(i18n("Encapsulated email")); auto encapsulatedLayout = new QVBoxLayout(groupBox); auto header = new QWidget(groupBox); auto headerLayout = new QFormLayout(header); const auto from = parts->data(parts->index(i, 0, parent), PartModel::SenderRole).toString(); const auto date = parts->data(parts->index(i, 0, parent), PartModel::DateRole).toDateTime(); headerLayout->addRow(i18n("From:"), new QLabel(from)); headerLayout->addRow(i18n("Date:"), new QLabel(date.toLocalTime().toString())); encapsulatedLayout->addWidget(header); recursiveBuildViewer(parts, encapsulatedLayout, parts->index(i, 0, parent)); container->layout()->addWidget(groupBox); layout->addWidget(container); break; } case PartModel::Types::Error: { const auto errorString = parts->data(parts->index(i, 0, parent), PartModel::ErrorString).toString(); auto errorWidget = new KMessageWidget(errorString); errorWidget->setCloseButtonVisible(false); errorWidget->setMessageType(KMessageWidget::MessageType::Error); QObject::connect(errorWidget, &KMessageWidget::linkActivated, errorWidget, [this, errorWidget](const QString &link) { QUrl url(link); if (url.path() == QStringLiteral("showCertificate")) { urlHandler->handleClick(QUrl(link), errorWidget->window()->windowHandle()); } }); errorWidget->setWordWrap(true); layout->addWidget(errorWidget); break; } default: qCWarning(MIMETREEPARSER_WIDGET_LOG) << parts->data(parts->index(i, 0, parent), PartModel::ContentRole) << type; } } } void MessageViewer::setMessage(const KMime::Message::Ptr message) { setUpdatesEnabled(false); d->parser.setMessage(message); connect(d->parser.attachments(), &AttachmentModel::info, this, [this](const QString &message) { d->messageWidget->setMessageType(KMessageWidget::Information); d->messageWidget->setText(message); d->messageWidget->animatedShow(); }); connect(d->parser.attachments(), &AttachmentModel::errorOccurred, this, [this](const QString &message) { d->messageWidget->setMessageType(KMessageWidget::Error); d->messageWidget->setText(message); d->messageWidget->animatedShow(); }); for (int i = d->formLayout->rowCount() - 1; i >= 0; i--) { d->formLayout->removeRow(i); } if (!d->parser.subject().isEmpty()) { d->formLayout->addRow(i18n("&Subject:"), new QLabel(d->parser.subject())); } if (!d->parser.from().isEmpty()) { d->formLayout->addRow(i18n("&From:"), new QLabel(d->parser.from())); } if (!d->parser.sender().isEmpty() && d->parser.from() != d->parser.sender()) { d->formLayout->addRow(i18n("&Sender:"), new QLabel(d->parser.sender())); } if (!d->parser.to().isEmpty()) { d->formLayout->addRow(i18n("&To:"), new QLabel(d->parser.to())); } if (!d->parser.cc().isEmpty()) { d->formLayout->addRow(i18n("&CC:"), new QLabel(d->parser.cc())); } if (!d->parser.bcc().isEmpty()) { d->formLayout->addRow(i18n("&BCC:"), new QLabel(d->parser.bcc())); } const auto parts = d->parser.parts(); QLayoutItem *child; while ((child = d->layout->takeAt(0)) != nullptr) { delete child->widget(); delete child; } d->recursiveBuildViewer(parts, d->layout, {}); d->layout->addStretch(); d->attachmentView->setModel(d->parser.attachments()); d->attachmentView->setVisible(d->parser.attachments()->rowCount() > 0); connect(d->attachmentView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this] { d->selectionChanged(); }); connect(d->attachmentView, &QAbstractItemView::doubleClicked, this, [this](const QModelIndex &) { // Since this is only emitted if a valid index is double clicked we can assume // that the first click of the double click set the selection accordingly. d->openSelectedAttachments(); }); setUpdatesEnabled(true); } void MessageViewer::print(QPainter *painter, int width) { const auto oldSize = size(); resize(width - 30, oldSize.height()); d->scrollArea->setFrameShape(QFrame::NoFrame); render(painter); d->scrollArea->setFrameShape(QFrame::StyledPanel); resize(oldSize); } diff --git a/src/widgets/messageviewerdialog.cpp b/src/widgets/messageviewerdialog.cpp index c0cf8cd..24e27e4 100644 --- a/src/widgets/messageviewerdialog.cpp +++ b/src/widgets/messageviewerdialog.cpp @@ -1,313 +1,310 @@ // SPDX-FileCopyrightText: 2023 g10 Code GmbH // SPDX-FileContributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "messageviewerdialog.h" #include "messageviewer.h" #include "mimetreeparser_widgets_debug.h" #include #include #include #include #include -#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include - using namespace MimeTreeParser::Widgets; namespace { /// On windows, force the filename to end with .eml /// On Linux, do nothing as this is handled by the file picker inline QString changeExtension(const QString &fileName) { #ifdef Q_OS_WIN auto renamedFileName = fileName; renamedFileName.replace(QRegularExpression(QStringLiteral("\\.(mbox|p7m|asc)$")), QStringLiteral(".eml")); // In case the file name didn't contain any of the expected extension: mbox, p7m, asc and eml // or doesn't contains an extension at all. if (!renamedFileName.endsWith(QStringLiteral(".eml"))) { renamedFileName += QStringLiteral(".eml"); } return renamedFileName; #else // Handled automatically by the file picker on linux return fileName; #endif } } class MessageViewerDialog::Private { public: int currentIndex = 0; QList messages; QString fileName; MimeTreeParser::Widgets::MessageViewer *messageViewer = nullptr; QAction *nextAction = nullptr; QAction *previousAction = nullptr; QToolBar *toolBar = nullptr; void setCurrentIndex(int currentIndex); QMenuBar *createMenuBar(QWidget *parent); private: void save(QWidget *parent); void saveDecrypted(QWidget *parent); void print(QWidget *parent); void printPreview(QWidget *parent); void printInternal(QPrinter *printer); }; void MessageViewerDialog::Private::setCurrentIndex(int index) { Q_ASSERT(index >= 0); Q_ASSERT(index < messages.count()); currentIndex = index; messageViewer->setMessage(messages[currentIndex]); previousAction->setEnabled(currentIndex != 0); nextAction->setEnabled(currentIndex != messages.count() - 1); } QMenuBar *MessageViewerDialog::Private::createMenuBar(QWidget *parent) { const auto menuBar = new QMenuBar(parent); // File menu const auto fileMenu = menuBar->addMenu(i18nc("@action:inmenu", "&File")); const auto saveAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18nc("@action:inmenu", "&Save")); QObject::connect(saveAction, &QAction::triggered, parent, [parent, this] { save(parent); }); fileMenu->addAction(saveAction); const auto saveDecryptedAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18nc("@action:inmenu", "Save Decrypted")); QObject::connect(saveDecryptedAction, &QAction::triggered, parent, [parent, this] { saveDecrypted(parent); }); fileMenu->addAction(saveDecryptedAction); const auto printPreviewAction = new QAction(QIcon::fromTheme(QStringLiteral("document-print-preview")), i18nc("@action:inmenu", "Print Preview")); QObject::connect(printPreviewAction, &QAction::triggered, parent, [parent, this] { printPreview(parent); }); fileMenu->addAction(printPreviewAction); const auto printAction = new QAction(QIcon::fromTheme(QStringLiteral("document-print")), i18nc("@action:inmenu", "&Print")); QObject::connect(printAction, &QAction::triggered, parent, [parent, this] { print(parent); }); fileMenu->addAction(printAction); // Navigation menu const auto navigationMenu = menuBar->addMenu(i18nc("@action:inmenu", "&Navigation")); previousAction = new QAction(QIcon::fromTheme(QStringLiteral("go-previous")), i18nc("@action:button Previous email", "Previous Message"), parent); previousAction->setEnabled(false); navigationMenu->addAction(previousAction); nextAction = new QAction(QIcon::fromTheme(QStringLiteral("go-next")), i18nc("@action:button Next email", "Next Message"), parent); nextAction->setEnabled(false); navigationMenu->addAction(nextAction); return menuBar; } void MessageViewerDialog::Private::save(QWidget *parent) { const QString location = QFileDialog::getSaveFileName(parent, i18nc("@title:window", "Save File"), changeExtension(fileName), i18nc("File dialog accepted files", "Email files (*.eml *.mbox)")); QSaveFile file(location); if (!file.open(QIODevice::WriteOnly)) { KMessageBox::error(parent, i18n("File %1 could not be created.", location), i18n("Error saving message")); return; } file.write(messages[currentIndex]->encodedContent()); file.commit(); } void MessageViewerDialog::Private::saveDecrypted(QWidget *parent) { const QString location = QFileDialog::getSaveFileName(parent, i18nc("@title:window", "Save Decrypted File"), changeExtension(fileName), i18nc("File dialog accepted files", "Email files (*.eml *.mbox)")); QSaveFile file(location); if (!file.open(QIODevice::WriteOnly)) { KMessageBox::error(parent, i18nc("Error message", "File %1 could not be created.", location), i18n("Error saving message")); return; } auto message = messages[currentIndex]; bool wasEncrypted = false; auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted); if (!wasEncrypted) { decryptedMessage = message; } file.write(decryptedMessage->encodedContent()); file.commit(); } void MessageViewerDialog::Private::print(QWidget *parent) { QPrinter printer; QPrintDialog dialog(&printer, parent); dialog.setWindowTitle(i18nc("@title:window", "Print Document")); if (dialog.exec() != QDialog::Accepted) return; printInternal(&printer); } void MessageViewerDialog::Private::printPreview(QWidget *parent) { auto dialog = new QPrintPreviewDialog(parent); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->resize(800, 750); dialog->setWindowTitle(i18nc("@title:window", "Print Document")); QObject::connect(dialog, &QPrintPreviewDialog::paintRequested, parent, [this](QPrinter *printer) { printInternal(printer); }); dialog->open(); } void MessageViewerDialog::Private::printInternal(QPrinter *printer) { QPainter painter; painter.begin(printer); const auto pageLayout = printer->pageLayout(); const auto pageRect = pageLayout.paintRectPixels(printer->resolution()); const double xscale = pageRect.width() / double(messageViewer->width()); const double yscale = pageRect.height() / double(messageViewer->height()); const double scale = qMin(qMin(xscale, yscale), 1.); painter.translate(pageRect.x(), pageRect.y()); painter.scale(scale, scale); messageViewer->print(&painter, pageRect.width()); } MessageViewerDialog::MessageViewerDialog(const QList &messages, QWidget *parent) : QDialog(parent) , d(std::make_unique()) { d->messages += messages; initGUI(); } MessageViewerDialog::MessageViewerDialog(const QString &fileName, QWidget *parent) : QDialog(parent) , d(std::make_unique()) { d->fileName = fileName; d->messages += MimeTreeParser::Core::FileOpener::openFile(fileName); initGUI(); } void MessageViewerDialog::initGUI() { const auto mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins({}); mainLayout->setSpacing(0); const auto layout = new QVBoxLayout; const auto menuBar = d->createMenuBar(this); mainLayout->setMenuBar(menuBar); if (d->messages.isEmpty()) { auto errorMessage = new KMessageWidget(this); errorMessage->setMessageType(KMessageWidget::Error); errorMessage->setText(i18nc("@info", "Unable to read file")); layout->addWidget(errorMessage); return; } const bool multipleMessages = d->messages.length() > 1; d->toolBar = new QToolBar(this); if (multipleMessages) { #ifdef Q_OS_UNIX d->toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle); #else // on other platforms the default is IconOnly which is bad for // accessibility and can't be changed by the user. toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); #endif d->toolBar->addAction(d->previousAction); connect(d->previousAction, &QAction::triggered, this, [this] { d->setCurrentIndex(d->currentIndex - 1); }); const auto spacer = new QWidget(this); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); d->toolBar->addWidget(spacer); d->toolBar->addAction(d->nextAction); connect(d->nextAction, &QAction::triggered, this, [this] { d->setCurrentIndex(d->currentIndex + 1); }); d->nextAction->setEnabled(true); mainLayout->addWidget(d->toolBar); } else { mainLayout->addWidget(d->toolBar); d->toolBar->hide(); } mainLayout->addLayout(layout); d->messageViewer = new MimeTreeParser::Widgets::MessageViewer(this); d->messageViewer->setMessage(d->messages[0]); layout->addWidget(d->messageViewer); auto buttonBox = new QDialogButtonBox(this); buttonBox->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin, nullptr, this), style()->pixelMetric(QStyle::PM_LayoutTopMargin, nullptr, this), style()->pixelMetric(QStyle::PM_LayoutRightMargin, nullptr, this), style()->pixelMetric(QStyle::PM_LayoutBottomMargin, nullptr, this)); auto closeButton = buttonBox->addButton(QDialogButtonBox::Close); connect(closeButton, &QPushButton::pressed, this, &QDialog::accept); layout->addWidget(buttonBox); } MessageViewerDialog::~MessageViewerDialog() = default; QToolBar *MessageViewerDialog::toolBar() const { return d->toolBar; } QList MessageViewerDialog::messages() const { return d->messages; } #include "moc_messageviewerdialog.cpp"