diff --git a/autotests/core/fileopenertest.cpp b/autotests/core/fileopenertest.cpp index b6caf02..7bd9014 100644 --- a/autotests/core/fileopenertest.cpp +++ b/autotests/core/fileopenertest.cpp @@ -1,72 +1,72 @@ // SPDX-FileCopyrightText: 2023 g10 Code GmbH // SPDX-FileContributor: Carl Schwan // SPDX-License-Identifier: LGPL-2.0-or-later #include #include #include using namespace MimeTreeParser::Core; class FileOpenerTest : public QObject { Q_OBJECT private Q_SLOTS: void openSingleMboxTest() { - const auto messages = FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("smime-opaque-enc+sign.mbox")); + const auto messages = FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1StringView("smime-opaque-enc+sign.mbox")); QCOMPARE(messages.count(), 1); } void openSingleCombinedTest() { - const auto messages = FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("combined.mbox")); + const auto messages = FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1StringView("combined.mbox")); QCOMPARE(messages.count(), 3); } void openAscTest() { - const auto messages = FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("msg.asc")); + const auto messages = FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1StringView("msg.asc")); QCOMPARE(messages.count(), 1); auto message = messages[0]; QCOMPARE(message->contentType()->mimeType(), "multipart/encrypted"); QCOMPARE(message->contents().count(), 2); auto pgpPart = message->contents()[0]; QCOMPARE(pgpPart->contentType()->mimeType(), "application/pgp-encrypted"); auto octetStreamPart = message->contents()[1]; QCOMPARE(octetStreamPart->contentType()->mimeType(), "application/octet-stream"); } void openSmimeTest() { - const auto messages = FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("smime.p7m")); + const auto messages = FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1StringView("smime.p7m")); QCOMPARE(messages.count(), 1); auto message = messages[0]; QCOMPARE(message->contentType()->mimeType(), "application/pkcs7-mime"); QCOMPARE(message->contentType()->parameter(QStringLiteral("smime-type")), QStringLiteral("enveloped-data")); QCOMPARE(message->contentDisposition()->filename(), QStringLiteral("smime.p7m")); } void openInexistingFileTest() { - const auto messages = FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("not-here.p7m")); + const auto messages = FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1StringView("not-here.p7m")); QCOMPARE(messages.count(), 0); } void openEmptyFile() { QTemporaryFile file; QVERIFY(file.open()); const auto messages = FileOpener::openFile(file.fileName()); QCOMPARE(messages.count(), 0); } }; QTEST_GUILESS_MAIN(FileOpenerTest) #include "fileopenertest.moc" diff --git a/autotests/widgets/messageviewerdialogtest.cpp b/autotests/widgets/messageviewerdialogtest.cpp index 1beccf4..1e8c0ca 100644 --- a/autotests/widgets/messageviewerdialogtest.cpp +++ b/autotests/widgets/messageviewerdialogtest.cpp @@ -1,44 +1,44 @@ // SPDX-FileCopyrightText: 2023 g10 Code GmbH // SPDX-FileContributor: Carl Schwan // SPDX-License-Identifier: LGPL-2.0-or-later #include #include #include using namespace MimeTreeParser::Widgets; class MessageViewerDialogTest : public QObject { Q_OBJECT private Q_SLOTS: void messageViewerDialogCreationMultipleTest() { - MessageViewerDialog dialog(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("combined.mbox")); + MessageViewerDialog dialog(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1StringView("combined.mbox")); QCOMPARE(dialog.messages().count(), 3); QCOMPARE(dialog.layout()->count(), 2); QVERIFY(dialog.layout()->itemAt(1)->layout()); QCOMPARE(dialog.layout()->itemAt(1)->layout()->count(), 2); const auto actions = dialog.layout()->menuBar()->actions(); QCOMPARE(actions.count(), 2); } void messageViewerDialogCreationTest() { - MessageViewerDialog dialog(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("plaintext.mbox")); + MessageViewerDialog dialog(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1StringView("plaintext.mbox")); QCOMPARE(dialog.messages().count(), 1); QCOMPARE(dialog.layout()->count(), 2); QVERIFY(!dialog.layout()->itemAt(0)->widget()->isVisible()); QVERIFY(dialog.layout()->itemAt(1)->layout()); QCOMPARE(dialog.layout()->itemAt(1)->layout()->count(), 2); } }; QTEST_MAIN(MessageViewerDialogTest) #include "messageviewerdialogtest.moc" diff --git a/autotests/widgets/messageviewertest.cpp b/autotests/widgets/messageviewertest.cpp index ca71302..574a2bf 100644 --- a/autotests/widgets/messageviewertest.cpp +++ b/autotests/widgets/messageviewertest.cpp @@ -1,50 +1,51 @@ // SPDX-FileCopyrightText: 2023 g10 Code GmbH // SPDX-FileContributor: Carl Schwan // SPDX-License-Identifier: LGPL-2.0-or-later #include "../../src/widgets/messagecontainerwidget_p.h" #include #include #include #include #include using namespace MimeTreeParser::Widgets; class MessageViewerTest : public QObject { Q_OBJECT private Q_SLOTS: void messageViewerSMimeEncrypted() { - auto messages = MimeTreeParser::Core::FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("smime-encrypted.mbox")); + auto messages = + MimeTreeParser::Core::FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1StringView("smime-encrypted.mbox")); QCOMPARE(messages.count(), 1); MessageViewer viewer; viewer.setMessage(messages[0]); auto layout = viewer.findChild(QStringLiteral("PartLayout")); QVERIFY(layout); QCOMPARE(layout->count(), 2); auto container = qobject_cast(layout->itemAt(0)->widget()); QVERIFY(container); auto encryptionMessage = container->findChild(QStringLiteral("EncryptionMessage")); QCOMPARE(encryptionMessage->messageType(), KMessageWidget::Positive); QCOMPARE(encryptionMessage->text(), QStringLiteral("This message is encrypted. Details")); encryptionMessage->linkActivated(QStringLiteral("messageviewer:showDetails")); QCOMPARE(encryptionMessage->text(), QStringLiteral("This message is encrypted. The message is encrypted for the following keys:")); auto signatureMessage = container->findChild(QStringLiteral("SignatureMessage")); QVERIFY(!signatureMessage); } }; QTEST_MAIN(MessageViewerTest) #include "messageviewertest.moc" diff --git a/src/core/bodypartformatter_impl.cpp b/src/core/bodypartformatter_impl.cpp index e4f58e6..a52486f 100644 --- a/src/core/bodypartformatter_impl.cpp +++ b/src/core/bodypartformatter_impl.cpp @@ -1,361 +1,362 @@ // SPDX-FileCopyrightText: 2003 Marc Mutz // SPDX-License-Identifier: GPL-2.0-only #include "mimetreeparser_core_debug.h" #include "bodypartformatter.h" #include "bodypartformatterbasefactory.h" #include "bodypartformatterbasefactory_p.h" #include "messagepart.h" #include "objecttreeparser.h" #include "utils.h" #include #include using namespace MimeTreeParser; using namespace MimeTreeParser::Interface; namespace MimeTreeParser { class AnyTypeBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter { }; class MessageRfc822BodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter { public: MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const Q_DECL_OVERRIDE { return MessagePart::Ptr(new EncapsulatedRfc822MessagePart(objectTreeParser, node, node->bodyAsMessage())); } }; class HeadersBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter { public: MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const Q_DECL_OVERRIDE { return MessagePart::Ptr(new HeadersPart(objectTreeParser, node)); } }; class MultiPartRelatedBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter { public: QList processList(ObjectTreeParser *objectTreeParser, KMime::Content *node) const Q_DECL_OVERRIDE { if (node->contents().isEmpty()) { return {}; } // We rely on the order of the parts. // Theoretically there could also be a Start parameter which would break this.. // https://tools.ietf.org/html/rfc2387#section-4 // We want to display attachments even if displayed inline. QList list; list.append(MimeMessagePart::Ptr(new MimeMessagePart(objectTreeParser, node->contents().at(0), true))); for (int i = 1; i < node->contents().size(); i++) { auto p = node->contents().at(i); if (KMime::isAttachment(p)) { list.append(MimeMessagePart::Ptr(new MimeMessagePart(objectTreeParser, p, true))); } } return list; } }; class MultiPartMixedBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter { public: MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const Q_DECL_OVERRIDE { const auto contents = node->contents(); if (contents.isEmpty()) { return {}; } // we need the intermediate part to preserve the headers (necessary for with protected headers using multipart mixed) auto part = MessagePart::Ptr(new MessagePart(objectTreeParser, {}, node)); part->appendSubPart(MimeMessagePart::Ptr(new MimeMessagePart(objectTreeParser, contents.at(0), false))); return part; } }; class ApplicationPGPEncryptedBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter { public: MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const Q_DECL_OVERRIDE { if (node->decodedContent().trimmed() != "Version: 1") { qCWarning(MIMETREEPARSER_CORE_LOG) << "Unknown PGP Version String:" << node->decodedContent().trimmed(); } if (!node->parent()) { return MessagePart::Ptr(); } KMime::Content *data = findTypeInDirectChildren(node->parent(), "application/octet-stream"); if (!data) { return MessagePart::Ptr(); // new MimeMessagePart(objectTreeParser, node)); } EncryptedMessagePart::Ptr mp(new EncryptedMessagePart(objectTreeParser, data->decodedText(), QGpgME::openpgp(), node, data)); mp->setIsEncrypted(true); return mp; } }; class ApplicationPkcs7MimeBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter { public: MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const Q_DECL_OVERRIDE { if (node->head().isEmpty()) { return MessagePart::Ptr(); } const QString smimeType = node->contentType()->parameter(QStringLiteral("smime-type")).toLower(); if (smimeType == QLatin1StringView("certs-only")) { return CertMessagePart::Ptr(new CertMessagePart(objectTreeParser, node, QGpgME::smime())); } bool isSigned = (smimeType == QLatin1StringView("signed-data")); bool isEncrypted = (smimeType == QLatin1StringView("enveloped-data")); // Analyze "signTestNode" node to find/verify a signature. // If zero part.objectTreeParser verification was successfully done after // decrypting via recursion by insertAndParseNewChildNode(). KMime::Content *signTestNode = isEncrypted ? nullptr : node; // We try decrypting the content // if we either *know* that it is an encrypted message part // or there is neither signed nor encrypted parameter. MessagePart::Ptr mp; if (!isSigned) { if (isEncrypted) { qCDebug(MIMETREEPARSER_CORE_LOG) << "pkcs7 mime == S/MIME TYPE: enveloped (encrypted) data"; } else { qCDebug(MIMETREEPARSER_CORE_LOG) << "pkcs7 mime - type unknown - enveloped (encrypted) data ?"; } auto _mp = EncryptedMessagePart::Ptr(new EncryptedMessagePart(objectTreeParser, node->decodedText(), QGpgME::smime(), node)); mp = _mp; _mp->setIsEncrypted(true); // PartMetaData *messagePart(_mp->partMetaData()); // if (!part.source()->decryptMessage()) { // isEncrypted = true; signTestNode = nullptr; // PENDING(marc) to be abs. sure, we'd need to have to look at the content // } else { // _mp->startDecryption(); // if (messagePart->isDecryptable) { // qCDebug(MIMETREEPARSER_CORE_LOG) << "pkcs7 mime - encryption found - enveloped (encrypted) data !"; // isEncrypted = true; // part.nodeHelper()->setEncryptionState(node, KMMsgFullyEncrypted); // signTestNode = nullptr; // } else { // // decryption failed, which could be because the part was encrypted but // // decryption failed, or because we didn't know if it was encrypted, tried, // // and failed. If the message was not actually encrypted, we continue // // assuming it's signed // if (_mp->passphraseError() || (smimeType.isEmpty() && messagePart->isEncrypted)) { // isEncrypted = true; // signTestNode = nullptr; // } // if (isEncrypted) { // qCDebug(MIMETREEPARSER_CORE_LOG) << "pkcs7 mime - ERROR: COULD NOT DECRYPT enveloped data !"; // } else { // qCDebug(MIMETREEPARSER_CORE_LOG) << "pkcs7 mime - NO encryption found"; // } // } // } } // We now try signature verification if necessarry. if (signTestNode) { if (isSigned) { qCDebug(MIMETREEPARSER_CORE_LOG) << "pkcs7 mime == S/MIME TYPE: opaque signed data"; } else { qCDebug(MIMETREEPARSER_CORE_LOG) << "pkcs7 mime - type unknown - opaque signed data ?"; } return SignedMessagePart::Ptr(new SignedMessagePart(objectTreeParser, QGpgME::smime(), nullptr, signTestNode)); } return mp; } }; class MultiPartAlternativeBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter { public: MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const Q_DECL_OVERRIDE { if (node->contents().isEmpty()) { return MessagePart::Ptr(); } AlternativeMessagePart::Ptr mp(new AlternativeMessagePart(objectTreeParser, node)); if (mp->mChildParts.isEmpty()) { return MimeMessagePart::Ptr(new MimeMessagePart(objectTreeParser, node->contents().at(0))); } return mp; } }; class MultiPartEncryptedBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter { public: MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const Q_DECL_OVERRIDE { if (node->contents().isEmpty()) { Q_ASSERT(false); return MessagePart::Ptr(); } const QGpgME::Protocol *protocol = nullptr; /* ATTENTION: This code is to be replaced by the new 'auto-detect' feature. -------------------------------------- */ KMime::Content *data = findTypeInDirectChildren(node, "application/octet-stream"); if (data) { protocol = QGpgME::openpgp(); } else { data = findTypeInDirectChildren(node, "application/pkcs7-mime"); if (data) { protocol = QGpgME::smime(); } } /* --------------------------------------------------------------------------------------------------------------- */ if (!data) { return MessagePart::Ptr(new MimeMessagePart(objectTreeParser, node->contents().at(0))); } EncryptedMessagePart::Ptr mp(new EncryptedMessagePart(objectTreeParser, data->decodedText(), protocol, node, data)); mp->setIsEncrypted(true); return mp; } }; class MultiPartSignedBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter { public: static const QGpgME::Protocol *detectProtocol(const QString &protocolContentType_, const QString &signatureContentType) { auto protocolContentType = protocolContentType_; if (protocolContentType.isEmpty()) { qCWarning(MIMETREEPARSER_CORE_LOG) << "Message doesn't set the protocol for the multipart/signed content-type, " "using content-type of the signature:" << signatureContentType; protocolContentType = signatureContentType; } const QGpgME::Protocol *protocol = nullptr; - if (protocolContentType == QLatin1StringView("application/pkcs7-signature") || protocolContentType == QLatin1String("application/x-pkcs7-signature")) { + if (protocolContentType == QLatin1StringView("application/pkcs7-signature") + || protocolContentType == QLatin1StringView("application/x-pkcs7-signature")) { protocol = QGpgME::smime(); } else if (protocolContentType == QLatin1StringView("application/pgp-signature") - || protocolContentType == QLatin1String("application/x-pgp-signature")) { + || protocolContentType == QLatin1StringView("application/x-pgp-signature")) { protocol = QGpgME::openpgp(); } return protocol; } MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const Q_DECL_OVERRIDE { if (node->contents().size() != 2) { qCDebug(MIMETREEPARSER_CORE_LOG) << "mulitpart/signed must have exactly two child parts!" << Qt::endl << "processing as multipart/mixed"; if (!node->contents().isEmpty()) { return MessagePart::Ptr(new MimeMessagePart(objectTreeParser, node->contents().at(0))); } else { return MessagePart::Ptr(); } } KMime::Content *signedData = node->contents().at(0); KMime::Content *signature = node->contents().at(1); Q_ASSERT(signedData); Q_ASSERT(signature); auto protocol = detectProtocol(node->contentType()->parameter(QStringLiteral("protocol")).toLower(), QLatin1StringView(signature->contentType()->mimeType().toLower())); if (!protocol) { return MessagePart::Ptr(new MimeMessagePart(objectTreeParser, signedData)); } return SignedMessagePart::Ptr(new SignedMessagePart(objectTreeParser, protocol, signature, signedData)); } }; class TextHtmlBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter { public: MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const Q_DECL_OVERRIDE { return HtmlMessagePart::Ptr(new HtmlMessagePart(objectTreeParser, node)); } }; class TextPlainBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter { public: MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const Q_DECL_OVERRIDE { if (KMime::isAttachment(node)) { return AttachmentMessagePart::Ptr(new AttachmentMessagePart(objectTreeParser, node)); } return TextMessagePart::Ptr(new TextMessagePart(objectTreeParser, node)); } }; } // anon namespace void BodyPartFormatterBaseFactoryPrivate::messageviewer_create_builtin_bodypart_formatters() { auto any = new AnyTypeBodyPartFormatter; auto textPlain = new TextPlainBodyPartFormatter; auto pkcs7 = new ApplicationPkcs7MimeBodyPartFormatter; auto pgp = new ApplicationPGPEncryptedBodyPartFormatter; auto html = new TextHtmlBodyPartFormatter; auto headers = new HeadersBodyPartFormatter; auto multipartAlternative = new MultiPartAlternativeBodyPartFormatter; auto multipartMixed = new MultiPartMixedBodyPartFormatter; auto multipartSigned = new MultiPartSignedBodyPartFormatter; auto multipartEncrypted = new MultiPartEncryptedBodyPartFormatter; auto message = new MessageRfc822BodyPartFormatter; auto multipartRelated = new MultiPartRelatedBodyPartFormatter; insert("application", "octet-stream", any); insert("application", "pgp", textPlain); insert("application", "pkcs7-mime", pkcs7); insert("application", "x-pkcs7-mime", pkcs7); insert("application", "pgp-encrypted", pgp); insert("application", "*", any); insert("text", "html", html); insert("text", "rtf", any); insert("text", "plain", textPlain); insert("text", "rfc822-headers", headers); insert("text", "*", textPlain); insert("image", "*", any); insert("message", "rfc822", message); insert("message", "*", any); insert("multipart", "alternative", multipartAlternative); insert("multipart", "encrypted", multipartEncrypted); insert("multipart", "signed", multipartSigned); insert("multipart", "related", multipartRelated); insert("multipart", "*", multipartMixed); insert("*", "*", any); } diff --git a/src/core/objecttreeparser.cpp b/src/core/objecttreeparser.cpp index 00e3b8c..7e776c7 100644 --- a/src/core/objecttreeparser.cpp +++ b/src/core/objecttreeparser.cpp @@ -1,492 +1,492 @@ // This file is part of KMail, the KDE mail client. // SPDX-FileCopyrightText: 2003 Marc Mutz // SPDX-FileCopyrightText: 2002-2004 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net // SPDX-FileCopyrightText: 2009 Andras Mantia // SPDX-FileCopyrightText: 2015 Sandro Knauß // SPDX-FileCopyrightText: 2017 Christian Mollekopf // SPDX-License-Identifier: GPL-2.0-or-later #include "objecttreeparser.h" #include "bodypartformatterbasefactory.h" #include "bodypartformatter.h" #include #include #include #include #include #include #include #include #include using namespace MimeTreeParser; /* * Collect message parts bottom up. * Filter to avoid evaluating a subtree. * Select parts to include it in the result set. Selecting a part in a branch will keep any parent parts from being selected. */ static QList collect(MessagePart::Ptr start, const std::function &evaluateSubtree, const std::function &select) { auto ptr = start.dynamicCast(); Q_ASSERT(ptr); MessagePart::List list; if (evaluateSubtree(ptr)) { for (const auto &p : ptr->subParts()) { list << ::collect(p, evaluateSubtree, select); } } // Don't consider this part if we already selected a subpart if (list.isEmpty()) { if (select(ptr)) { list << start; } } return list; } QString ObjectTreeParser::plainTextContent() { QString content; if (mParsedPart) { auto plainParts = ::collect( mParsedPart, [](const MessagePart::Ptr &) { return true; }, [](const MessagePart::Ptr &part) { if (part->isAttachment()) { return false; } if (dynamic_cast(part.data())) { return true; } if (dynamic_cast(part.data())) { return true; } return false; }); for (const auto &part : plainParts) { content += part->text(); } } return content; } QString ObjectTreeParser::htmlContent() { QString content; if (mParsedPart) { MessagePart::List contentParts = ::collect( mParsedPart, [](const MessagePart::Ptr &) { return true; }, [](const MessagePart::Ptr &part) { if (dynamic_cast(part.data())) { return true; } if (dynamic_cast(part.data())) { return true; } return false; }); for (const auto &part : contentParts) { if (auto p = dynamic_cast(part.data())) { content += p->htmlContent(); } else { content += part->text(); } } } return content; } bool ObjectTreeParser::hasEncryptedParts() const { bool result = false; ::collect( mParsedPart, [](const MessagePart::Ptr &) { return true; }, [&result](const MessagePart::Ptr &part) { if (const auto enc = dynamic_cast(part.data())) { result = true; } return false; }); return result; } bool ObjectTreeParser::hasSignedParts() const { bool result = false; ::collect( mParsedPart, [](const MessagePart::Ptr &) { return true; }, [&result](const MessagePart::Ptr &part) { if (const auto enc = dynamic_cast(part.data())) { result = true; } return false; }); return result; } static void print(QTextStream &stream, KMime::Content *node, const QString prefix = {}) { QByteArray mediaType("text"); QByteArray subType("plain"); if (node->contentType(false) && !node->contentType()->mediaType().isEmpty() && !node->contentType()->subType().isEmpty()) { mediaType = node->contentType()->mediaType(); subType = node->contentType()->subType(); } stream << prefix << "! " << mediaType << subType << " isAttachment: " << KMime::isAttachment(node) << "\n"; const auto contents = node->contents(); for (const auto nodeContent : contents) { print(stream, nodeContent, prefix + QLatin1StringView(" ")); } } static void print(QTextStream &stream, const MessagePart &messagePart, const QByteArray pre = {}) { stream << pre << "# " << messagePart.metaObject()->className() << " isAttachment: " << messagePart.isAttachment() << "\n"; const auto subParts = messagePart.subParts(); for (const auto &subPart : subParts) { print(stream, *subPart, pre + " "); } } QString ObjectTreeParser::structureAsString() const { QString string; QTextStream stream{&string}; if (mTopLevelContent) { ::print(stream, mTopLevelContent); } if (mParsedPart) { ::print(stream, *mParsedPart); } return string; } void ObjectTreeParser::print() { qInfo().noquote() << structureAsString(); } static KMime::Content *find(KMime::Content *node, const std::function &select) { QByteArray mediaType("text"); QByteArray subType("plain"); if (node->contentType(false) && !node->contentType()->mediaType().isEmpty() && !node->contentType()->subType().isEmpty()) { mediaType = node->contentType()->mediaType(); subType = node->contentType()->subType(); } if (select(node)) { return node; } const auto contents = node->contents(); for (const auto nodeContent : contents) { if (const auto content = find(nodeContent, select)) { return content; } } return nullptr; } KMime::Content *ObjectTreeParser::find(const std::function &select) { return ::find(mTopLevelContent, select); } MessagePart::List ObjectTreeParser::collectContentParts() { return collectContentParts(mParsedPart); } MessagePart::List ObjectTreeParser::collectContentParts(MessagePart::Ptr start) { return ::collect( start, [start](const MessagePart::Ptr &part) { // Ignore the top-level if (start.data() == part.data()) { return true; } if (auto encapsulatedPart = part.dynamicCast()) { return false; } return true; }, [start](const MessagePart::Ptr &part) { if (const auto attachment = dynamic_cast(part.data())) { return attachment->mimeType() == "text/calendar"; } else if (const auto text = dynamic_cast(part.data())) { auto enc = dynamic_cast(text->parentPart()); if (enc && enc->error()) { return false; } return true; } else if (dynamic_cast(part.data())) { return true; } else if (dynamic_cast(part.data())) { // Don't if we have an alternative part as parent return true; } else if (dynamic_cast(part.data())) { if (start.data() == part.data()) { return false; } return true; } else if (const auto enc = dynamic_cast(part.data())) { if (enc->error()) { return true; } // If we have a textpart with encrypted and unencrypted subparts we want to return the textpart if (dynamic_cast(enc->parentPart())) { return false; } } else if (const auto sig = dynamic_cast(part.data())) { // Signatures without subparts already contain the text return !sig->hasSubParts(); } return false; }); } MessagePart::List ObjectTreeParser::collectAttachmentParts() { MessagePart::List contentParts = ::collect( mParsedPart, [](const MessagePart::Ptr &) { return true; }, [](const MessagePart::Ptr &part) { return part->isAttachment(); }); return contentParts; } /* * This naive implementation assumes that there is an encrypted part wrapping a signature. * For other cases we would have to process both recursively (I think?) */ void ObjectTreeParser::decryptAndVerify() { // We first decrypt ::collect( mParsedPart, [](const MessagePart::Ptr &) { return true; }, [](const MessagePart::Ptr &part) { if (const auto enc = dynamic_cast(part.data())) { enc->startDecryption(); } return false; }); // And then verify the available signatures ::collect( mParsedPart, [](const MessagePart::Ptr &) { return true; }, [](const MessagePart::Ptr &part) { if (const auto enc = dynamic_cast(part.data())) { enc->startVerification(); } return false; }); } QString ObjectTreeParser::resolveCidLinks(const QString &html) { auto text = html; static const auto regex = QRegularExpression(QLatin1StringView("(src)\\s*=\\s*(\"|')(cid:[^\"']+)\\2")); auto it = regex.globalMatch(text); while (it.hasNext()) { const auto match = it.next(); const auto link = QUrl(match.captured(3)); auto cid = link.path(); auto mailMime = const_cast(find([=](KMime::Content *content) { if (!content || !content->contentID(false)) { return false; } return QString::fromLatin1(content->contentID(false)->identifier()) == cid; })); if (mailMime) { const auto contentType = mailMime->contentType(false); if (!contentType) { qWarning() << "No content type, skipping"; continue; } QMimeDatabase mimeDb; const auto mimetype = mimeDb.mimeTypeForName(QString::fromLatin1(contentType->mimeType())).name(); if (mimetype.startsWith(QLatin1StringView("image/"))) { // We reencode to base64 below. const auto data = mailMime->decodedContent(); if (data.isEmpty()) { qWarning() << "Attachment is empty."; continue; } text.replace(match.captured(0), QString::fromLatin1("src=\"data:%1;base64,%2\"").arg(mimetype, QString::fromLatin1(data.toBase64()))); } } else { qWarning() << "Failed to find referenced attachment: " << cid; } } return text; } //----------------------------------------------------------------------------- void ObjectTreeParser::parseObjectTree(const QByteArray &mimeMessage) { const auto mailData = KMime::CRLFtoLF(mimeMessage); mMsg = KMime::Message::Ptr(new KMime::Message); mMsg->setContent(mailData); mMsg->parse(); // We avoid using mMsg->contentType()->charset(), because that will just return kmime's defaultCharset(), ISO-8859-1 const auto charset = mMsg->contentType()->parameter(QStringLiteral("charset")).toLatin1(); if (charset.isEmpty()) { mMsg->contentType()->setCharset("us-ascii"); } parseObjectTree(mMsg.data()); } void ObjectTreeParser::parseObjectTree(KMime::Content *node) { mTopLevelContent = node; mParsedPart = parseObjectTreeInternal(node, false); } MessagePart::Ptr ObjectTreeParser::parsedPart() const { return mParsedPart; } /* * This will lookup suitable formatters based on the type, * and let them generate a list of parts. * If the formatter generated a list of parts, then those are taken, otherwise we move on to the next match. */ MessagePart::List ObjectTreeParser::processType(KMime::Content *node, const QByteArray &mediaType, const QByteArray &subType) { static MimeTreeParser::BodyPartFormatterBaseFactory factory; const auto sub = factory.subtypeRegistry(mediaType.constData()); const auto range = sub.equal_range(subType.constData()); for (auto it = range.first; it != range.second; ++it) { const auto formatter = it->second; if (!formatter) { continue; } const auto list = formatter->processList(this, node); if (!list.isEmpty()) { return list; } } return {}; } MessagePart::Ptr ObjectTreeParser::parseObjectTreeInternal(KMime::Content *node, bool onlyOneMimePart) { if (!node) { return MessagePart::Ptr(); } auto parsedPart = MessagePart::Ptr(new MessagePartList(this, node)); parsedPart->setIsRoot(node->isTopLevel()); const auto contents = node->parent() ? node->parent()->contents() : KMime::Content::List{node}; for (int i = contents.indexOf(node); i < contents.size(); ++i) { node = contents.at(i); QByteArray mediaType("text"); QByteArray subType("plain"); if (node->contentType(false) && !node->contentType()->mediaType().isEmpty() && !node->contentType()->subType().isEmpty()) { mediaType = node->contentType()->mediaType(); subType = node->contentType()->subType(); } auto messageParts = [&] { // Try the specific type handler { auto list = processType(node, mediaType, subType); if (!list.isEmpty()) { return list; } } // Fallback to the generic handler { auto list = processType(node, mediaType, "*"); if (!list.isEmpty()) { return list; } } // Fallback to the default handler return defaultHandling(node); }(); for (const auto &part : messageParts) { parsedPart->appendSubPart(part); } if (onlyOneMimePart) { break; } } return parsedPart; } QList ObjectTreeParser::defaultHandling(KMime::Content *node) { if (node->contentType()->mimeType() == QByteArrayLiteral("application/octet-stream") - && (node->contentType()->name().endsWith(QLatin1StringView("p7m")) || node->contentType()->name().endsWith(QLatin1String("p7s")) + && (node->contentType()->name().endsWith(QLatin1StringView("p7m")) || node->contentType()->name().endsWith(QLatin1StringView("p7s")) || node->contentType()->name().endsWith(QLatin1StringView("p7c")))) { auto list = processType(node, "application", "pkcs7-mime"); if (!list.isEmpty()) { return list; } } return {AttachmentMessagePart::Ptr(new AttachmentMessagePart(this, node))}; } QByteArray ObjectTreeParser::codecNameFor(KMime::Content *node) const { if (!node) { return QByteArrayLiteral("UTF-8"); } QByteArray charset = node->contentType()->charset().toLower(); // utf-8 is a superset of us-ascii, so we don't lose anything if we use it instead // utf-8 is used so widely nowadays that it is a good idea to use it to fix issues with broken clients. if (charset == "us-ascii") { charset = "utf-8"; } if (!charset.isEmpty()) { if (const QStringDecoder c(charset.constData()); c.isValid()) { return charset; } } // no charset means us-ascii (RFC 2045), so using local encoding should // be okay return QByteArrayLiteral("UTF-8"); }