diff --git a/src/core/autotests/mimetreeparsertest.cpp b/src/core/autotests/mimetreeparsertest.cpp index bc5f008..bdcf5e1 100644 --- a/src/core/autotests/mimetreeparsertest.cpp +++ b/src/core/autotests/mimetreeparsertest.cpp @@ -1,776 +1,777 @@ // SPDX-FileCopyrightText: 2016 Sandro Knauß // SPDX-License-Identifier: LGPL-2.0-or-later #include #include #include #include QByteArray readMailFromFile(const QString &mailFile) { QFile file(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile); file.open(QIODevice::ReadOnly); Q_ASSERT(file.isOpen()); return file.readAll(); } class MimeTreeParserTest : public QObject { Q_OBJECT private Q_SLOTS: void testTextMail() { const auto expectedText = QStringLiteral( "If you can see this text it means that your email client couldn't display our newsletter properly.\nPlease visit this link to view the newsletter " "on our website: http://www.gog.com/newsletter/"); MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("plaintext.mbox"))); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QCOMPARE(part->text(), expectedText); QCOMPARE(part->charset(), QStringLiteral("utf-8").toLocal8Bit()); QCOMPARE(part->encryptions().size(), 0); QCOMPARE(part->signatures().size(), 0); QCOMPARE(otp.collectAttachmentParts().size(), 0); QCOMPARE(otp.plainTextContent(), expectedText); QVERIFY(otp.htmlContent().isEmpty()); } void testAlternative() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("alternative.mbox"))); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->plaintextContent(), QStringLiteral("If you can see this text it means that your email client couldn't display our newsletter properly.\nPlease visit this link to " "view the newsletter on our website: http://www.gog.com/newsletter/\n")); QCOMPARE(part->charset(), QStringLiteral("us-ascii").toLocal8Bit()); QCOMPARE(part->htmlContent(), QStringLiteral("

HTML text

\n\n")); QCOMPARE(otp.collectAttachmentParts().size(), 0); QCOMPARE(part->encryptions().size(), 0); QCOMPARE(part->signatures().size(), 0); } void testTextHtml() { auto expectedText = QStringLiteral("

HTML text

"); MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("html.mbox"))); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->htmlContent(), expectedText); QCOMPARE(part->charset(), QStringLiteral("windows-1252").toLocal8Bit()); QCOMPARE(part->encryptions().size(), 0); QCOMPARE(part->signatures().size(), 0); auto contentAttachmentList = otp.collectAttachmentParts(); QCOMPARE(contentAttachmentList.size(), 0); QCOMPARE(otp.htmlContent(), expectedText); QVERIFY(otp.plainTextContent().isEmpty()); } void testSMimeEncrypted() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("smime-encrypted.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->text(), QStringLiteral("The quick brown fox jumped over the lazy dog.")); QCOMPARE(part->charset(), QStringLiteral("us-ascii").toLocal8Bit()); QCOMPARE(part->encryptions().size(), 1); QCOMPARE(part->signatures().size(), 0); auto contentAttachmentList = otp.collectAttachmentParts(); QCOMPARE(contentAttachmentList.size(), 0); } void testOpenPGPEncryptedAttachment() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->text(), QStringLiteral("test text")); QCOMPARE(part->charset(), QStringLiteral("us-ascii").toLocal8Bit()); QCOMPARE(part->encryptions().size(), 1); QCOMPARE(part->signatures().size(), 1); QCOMPARE(part->encryptionState(), MimeTreeParser::KMMsgFullyEncrypted); QCOMPARE(part->signatureState(), MimeTreeParser::KMMsgFullySigned); auto contentAttachmentList = otp.collectAttachmentParts(); QCOMPARE(contentAttachmentList.size(), 2); // QCOMPARE(contentAttachmentList[0]->availableContents(), QVector() << "text/plain"); // QCOMPARE(contentAttachmentList[0]->content().size(), 1); QCOMPARE(contentAttachmentList[0]->encryptions().size(), 1); QCOMPARE(contentAttachmentList[0]->signatures().size(), 1); QCOMPARE(contentAttachmentList[0]->encryptionState(), MimeTreeParser::KMMsgFullyEncrypted); QCOMPARE(contentAttachmentList[0]->signatureState(), MimeTreeParser::KMMsgFullySigned); // QCOMPARE(contentAttachmentList[1]->availableContents(), QVector() << "image/png"); // QCOMPARE(contentAttachmentList[1]->content().size(), 1); QCOMPARE(contentAttachmentList[1]->encryptions().size(), 0); QCOMPARE(contentAttachmentList[1]->signatures().size(), 0); QCOMPARE(contentAttachmentList[1]->encryptionState(), MimeTreeParser::KMMsgNotEncrypted); QCOMPARE(contentAttachmentList[1]->signatureState(), MimeTreeParser::KMMsgNotSigned); } void testOpenPGPInline() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("openpgp-inline-charset-encrypted.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->charset(), QStringLiteral("ISO-8859-15").toLocal8Bit()); QCOMPARE(part->text(), QString::fromUtf8("asdasd asd asd asdf sadf sdaf sadf öäü")); QCOMPARE(part->encryptions().size(), 1); QCOMPARE(part->signatures().size(), 1); QCOMPARE(otp.collectAttachmentParts().size(), 0); } void testOpenPPGInlineWithNonEncText() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("openpgp-inline-encrypted+nonenc.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part1 = partList[0].dynamicCast(); QVERIFY(bool(part1)); QCOMPARE(part1->text(), QStringLiteral("Not encrypted not signed :(\n\nsome random text")); // TODO test if we get the proper subparts with the appropriate encryptions QCOMPARE(part1->charset(), QStringLiteral("us-ascii").toLocal8Bit()); QCOMPARE(part1->encryptionState(), MimeTreeParser::KMMsgPartiallyEncrypted); QCOMPARE(part1->signatureState(), MimeTreeParser::KMMsgNotSigned); // QCOMPARE(part1->text(), QStringLiteral("Not encrypted not signed :(\n\n")); // QCOMPARE(part1->charset(), QStringLiteral("us-ascii").toLocal8Bit()); // QCOMPARE(contentList[1]->content(), QStringLiteral("some random text").toLocal8Bit()); // QCOMPARE(contentList[1]->charset(), QStringLiteral("us-ascii").toLocal8Bit()); // QCOMPARE(contentList[1]->encryptions().size(), 1); // QCOMPARE(contentList[1]->signatures().size(), 0); QCOMPARE(otp.collectAttachmentParts().size(), 0); } void testEncryptionBlock() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part1 = partList[0].dynamicCast(); QVERIFY(bool(part1)); QCOMPARE(part1->encryptions().size(), 1); // auto enc = contentList[0]->encryptions()[0]; // QCOMPARE((int) enc->recipients().size(), 2); // auto r = enc->recipients()[0]; // QCOMPARE(r->keyid(),QStringLiteral("14B79E26050467AA")); // QCOMPARE(r->name(),QStringLiteral("kdetest")); // QCOMPARE(r->email(),QStringLiteral("you@you.com")); // QCOMPARE(r->comment(),QStringLiteral("")); // r = enc->recipients()[1]; // QCOMPARE(r->keyid(),QStringLiteral("8D9860C58F246DE6")); // QCOMPARE(r->name(),QStringLiteral("unittest key")); // QCOMPARE(r->email(),QStringLiteral("test@kolab.org")); // QCOMPARE(r->comment(),QStringLiteral("no password")); auto attachmentList = otp.collectAttachmentParts(); QCOMPARE(attachmentList.size(), 2); auto attachment1 = attachmentList[0]; QVERIFY(attachment1->node()); QCOMPARE(attachment1->filename(), QStringLiteral("file.txt")); auto attachment2 = attachmentList[1]; QVERIFY(attachment2->node()); QCOMPARE(attachment2->filename(), QStringLiteral("image.png")); } void testSignatureBlock() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); // QCOMPARE(contentList[0]->signatures().size(), 1); // auto sig = contentList[0]->signatures()[0]; // QCOMPARE(sig->creationDateTime(), QDateTime(QDate(2015,05,01),QTime(15,12,47))); // QCOMPARE(sig->expirationDateTime(), QDateTime()); // QCOMPARE(sig->neverExpires(), true); // auto key = sig->key(); // QCOMPARE(key->keyid(),QStringLiteral("8D9860C58F246DE6")); // QCOMPARE(key->name(),QStringLiteral("unittest key")); // QCOMPARE(key->email(),QStringLiteral("test@kolab.org")); // QCOMPARE(key->comment(),QStringLiteral("no password")); } void testRelatedAlternative() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("cid-links.mbox"))); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->encryptions().size(), 0); QCOMPARE(part->signatures().size(), 0); QCOMPARE(otp.collectAttachmentParts().size(), 1); } void testAttachmentPart() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("attachment.mbox"))); otp.print(); auto partList = otp.collectAttachmentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->mimeType(), "image/jpeg"); QCOMPARE(part->filename(), QStringLiteral("aqnaozisxya.jpeg")); } void testAttachment2Part() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("attachment2.mbox"))); otp.print(); auto partList = otp.collectAttachmentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->mimeType(), "image/jpeg"); QCOMPARE(part->filename(), QStringLiteral("aqnaozisxya.jpeg")); } void testCidLink() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("cid-links.mbox"))); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); auto resolvedContent = otp.resolveCidLinks(part->htmlContent()); QVERIFY(!resolvedContent.contains(QLatin1String("cid:"))); } void testCidLinkInForwardedInline() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("cid-links-forwarded-inline.mbox"))); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); auto resolvedContent = otp.resolveCidLinks(part->htmlContent()); QVERIFY(!resolvedContent.contains(QLatin1String("cid:"))); } void testOpenPGPInlineError() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("inlinepgpgencrypted-error.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QVERIFY(part->error()); } void testEncapsulated() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("encapsulated-with-attachment.mbox"))); otp.decryptParts(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 2); auto part = partList[1].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->from(), QLatin1String("Thomas McGuire ")); QCOMPARE(part->date().toString(), QLatin1String("Wed Aug 5 10:57:58 2009 GMT+0200")); auto subPartList = otp.collectContentParts(part); QCOMPARE(subPartList.size(), 1); qWarning() << subPartList[0]->metaObject()->className(); auto subPart = subPartList[0].dynamicCast(); QVERIFY(bool(subPart)); } void test8bitEncodedInPlaintext() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("8bitencoded.mbox"))); QVERIFY(otp.plainTextContent().contains(QString::fromUtf8("Why Pisa’s Tower"))); QVERIFY(otp.htmlContent().contains(QString::fromUtf8("Why Pisa’s Tower"))); } void testInlineSigned() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("openpgp-inline-signed.mbox"))); otp.decryptParts(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QCOMPARE(part->signatures().size(), 1); QCOMPARE(part->encryptionState(), MimeTreeParser::KMMsgNotEncrypted); QCOMPARE(part->signatureState(), MimeTreeParser::KMMsgFullySigned); QCOMPARE(part->text(), QString::fromUtf8("ohno öäü\n")); QVERIFY(otp.plainTextContent().contains(QString::fromUtf8("ohno öäü"))); auto signaturePart = part->signatures().first(); QCOMPARE(signaturePart->partMetaData()->isGoodSignature, true); QCOMPARE(signaturePart->partMetaData()->keyIsTrusted, true); QCOMPARE(signaturePart->partMetaData()->keyMissing, false); QCOMPARE(signaturePart->partMetaData()->keyExpired, false); QCOMPARE(signaturePart->partMetaData()->keyRevoked, false); QCOMPARE(signaturePart->partMetaData()->sigExpired, false); QCOMPARE(signaturePart->partMetaData()->crlMissing, false); QCOMPARE(signaturePart->partMetaData()->crlTooOld, false); QCOMPARE(signaturePart->partMetaData()->keyId, QByteArray{"8D9860C58F246DE6"}); QCOMPARE(signaturePart->partMetaData()->signer, QLatin1String{"unittest key (no password) "}); QCOMPARE(signaturePart->partMetaData()->signerMailAddresses, QStringList{{QStringLiteral("test@kolab.org")}}); } void testEncryptedAndSigned() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("openpgp-encrypted+signed.mbox"))); otp.decryptParts(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QCOMPARE(part->signatures().size(), 1); QCOMPARE(part->encryptions().size(), 1); QCOMPARE(part->encryptionState(), MimeTreeParser::KMMsgFullyEncrypted); QCOMPARE(part->signatureState(), MimeTreeParser::KMMsgFullySigned); QVERIFY(otp.plainTextContent().contains(QString::fromUtf8("encrypted message text"))); auto signaturePart = part->signatures().first(); QCOMPARE(signaturePart->partMetaData()->keyId, QByteArray{"8D9860C58F246DE6"}); QCOMPARE(signaturePart->partMetaData()->isGoodSignature, true); } void testOpenpgpMultipartEmbedded() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("openpgp-multipart-embedded.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QCOMPARE(part->encryptions().size(), 1); QCOMPARE(part->encryptionState(), MimeTreeParser::KMMsgFullyEncrypted); QCOMPARE(otp.plainTextContent(), QString::fromUtf8("sdflskjsdf\n\n-- \nThis is a HTML signature.\n")); } void testOpenpgpMultipartEmbeddedSigned() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("openpgp-multipart-embedded-signed.mbox"))); otp.decryptParts(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QCOMPARE(part->encryptions().size(), 1); QCOMPARE(part->signatures().size(), 1); QCOMPARE(part->encryptionState(), MimeTreeParser::KMMsgFullyEncrypted); QCOMPARE(part->signatureState(), MimeTreeParser::KMMsgFullySigned); QCOMPARE(otp.plainTextContent(), QString::fromUtf8("test\n\n-- \nThis is a HTML signature.\n")); auto signaturePart = part->signatures().first(); QVERIFY(signaturePart->partMetaData()->keyId.endsWith(QByteArray{"2E3B7787B1B75920"})); // We lack the public key for this message QCOMPARE(signaturePart->partMetaData()->isGoodSignature, false); QCOMPARE(signaturePart->partMetaData()->keyMissing, true); } void testAppleHtmlWithAttachments() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("applehtmlwithattachments.mbox"))); otp.decryptParts(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(part); QCOMPARE(part->encryptions().size(), 0); QCOMPARE(part->signatures().size(), 0); QVERIFY(part->isHtml()); QCOMPARE(otp.plainTextContent(), QString::fromUtf8("Hi,\n\nThis is an HTML message with attachments.\n\nCheers,\nChristian")); QCOMPARE(otp.htmlContent(), QString::fromUtf8( "
Hi,

This is an HTML message with attachments.

Cheers,
Christian
")); auto attachments = otp.collectAttachmentParts(); QCOMPARE(attachments.size(), 1); } void testAppleHtmlWithAttachmentsMixed() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("applehtmlwithattachmentsmixed.mbox"))); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(part); QCOMPARE(part->encryptions().size(), 0); QCOMPARE(part->signatures().size(), 0); QVERIFY(part->isHtml()); QCOMPARE(otp.plainTextContent(), QString::fromUtf8("Hello\n\n\n\nRegards\n\nFsdfsdf")); QCOMPARE(otp.htmlContent(), QString::fromUtf8( "Hello


Regards

Fsdfsdf
")); auto attachments = otp.collectAttachmentParts(); QCOMPARE(attachments.size(), 1); } void testInvitation() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("invitation.mbox"))); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(part); QCOMPARE(part->encryptions().size(), 0); QCOMPARE(part->signatures().size(), 0); QVERIFY(!part->isHtml()); QVERIFY(part->availableModes().contains(MimeTreeParser::AlternativeMessagePart::MultipartIcal)); auto attachments = otp.collectAttachmentParts(); QCOMPARE(attachments.size(), 0); } void testGmailInvitation() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("gmail-invitation.mbox"))); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(part); QCOMPARE(part->encryptions().size(), 0); + qWarning() << part; QCOMPARE(part->signatures().size(), 0); QVERIFY(part->isHtml()); QVERIFY(part->availableModes().contains(MimeTreeParser::AlternativeMessagePart::MultipartIcal)); auto attachments = otp.collectAttachmentParts(); QCOMPARE(attachments.size(), 1); } void testMemoryHole() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("openpgp-encrypted-memoryhole.mbox"))); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->text(), QStringLiteral("very secret mesage\n")); QCOMPARE(part->header("subject")->asUnicodeString(), QStringLiteral("hidden subject")); QCOMPARE(part->header("from")->asUnicodeString(), QStringLiteral("you@example.com")); QCOMPARE(part->header("to")->asUnicodeString(), QStringLiteral("me@example.com")); QCOMPARE(part->header("cc")->asUnicodeString(), QStringLiteral("cc@example.com")); QCOMPARE(part->header("message-id")->asUnicodeString(), QStringLiteral("")); QCOMPARE(part->header("references")->asUnicodeString(), QStringLiteral("")); QCOMPARE(part->header("in-reply-to")->asUnicodeString(), QStringLiteral("")); QCOMPARE(static_cast(part->header("date"))->dateTime(), QDateTime(QDate(2018, 1, 2), QTime(3, 4, 5), QTimeZone::utc())); } /** * Required special handling because the list replaces the toplevel part. */ void testMemoryHoleWithList() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("cid-links-forwarded-inline.mbox"))); const auto parts = otp.collectContentParts(); auto part = parts[0]; QVERIFY(part->header("references")); QCOMPARE(part->header("references")->asUnicodeString(), QStringLiteral("")); } void testMemoryHoleMultipartMixed() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("openpgp-encrypted-memoryhole2.mbox"))); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->text(), QStringLiteral("\n\n Fsdflkjdslfj\n\n\nHappy Monday!\n\nBelow you will find a quick overview of the current on-goings. Remember\n")); QCOMPARE(part->header("subject")->asUnicodeString(), QStringLiteral("This is the subject")); } void testMIMESignature() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("text+html-maillinglist.mbox"))); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); for (const auto &part : partList) { qWarning() << "found part " << part->metaObject()->className(); } QCOMPARE(partList.size(), 2); // The actual content { auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); } // The signature { auto part = partList[1].dynamicCast(); QVERIFY(bool(part)); QVERIFY(part->text().contains(QStringLiteral("bugzilla mailing list"))); } } void testCRLFEncryptedWithSignature() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("crlf-encrypted-with-signature.mbox"))); otp.decryptParts(); otp.print(); QCOMPARE(otp.plainTextContent(), QStringLiteral("CRLF file\n\n-- \nThis is a signature\nWith two lines\n\nAand another line\n")); } void testCRLFEncryptedWithSignatureMultipart() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("crlf-encrypted-with-signature-multipart.mbox"))); otp.decryptParts(); otp.print(); // QEXPECT_FAIL("", "because MessagePart::parseInternal uses \n\n to detect encapsulated messages (so 'CRLF file' ends up as header)", Continue); // QCOMPARE(otp.plainTextContent(), QStringLiteral("CRLF file\n\n-- \nThis is a signature\nWith two lines\n\nAand another line\n")); // QVERIFY(!otp.htmlContent().contains(QStringLiteral("\r\n"))); } void testCRLFOutlook() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("outlook.mbox"))); otp.decryptParts(); otp.print(); qWarning() << otp.plainTextContent(); QVERIFY(otp.plainTextContent().startsWith(QStringLiteral("Hi Christian,\n\nhabs gerade getestet:\n\n«This is a test"))); QVERIFY(!otp.htmlContent().contains(QLatin1String("\r\n"))); } void testOpenPGPEncryptedSignedThunderbird() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("openpgp-encrypted-signed-thunderbird.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->text(), QStringLiteral("sdfsdf\n")); QCOMPARE(part->charset(), QStringLiteral("utf-8").toLocal8Bit()); QCOMPARE(part->encryptions().size(), 1); QCOMPARE(part->signatures().size(), 1); QCOMPARE(part->signatures()[0]->partMetaData()->isGoodSignature, true); QCOMPARE(part->encryptionState(), MimeTreeParser::KMMsgFullyEncrypted); QCOMPARE(part->signatureState(), MimeTreeParser::KMMsgFullySigned); auto contentAttachmentList = otp.collectAttachmentParts(); QCOMPARE(contentAttachmentList.size(), 1); // QCOMPARE(contentAttachmentList[0]->content().size(), 1); QCOMPARE(contentAttachmentList[0]->encryptions().size(), 1); QCOMPARE(contentAttachmentList[0]->signatures().size(), 1); QCOMPARE(contentAttachmentList[0]->encryptionState(), MimeTreeParser::KMMsgFullyEncrypted); QCOMPARE(contentAttachmentList[0]->signatureState(), MimeTreeParser::KMMsgFullySigned); } void testSignedForwardOpenpgpSignedEncrypted() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("signed-forward-openpgp-signed-encrypted.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 2); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->text(), QStringLiteral("bla bla bla")); part = partList[1].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->text(), QString()); QCOMPARE(part->charset(), QStringLiteral("ISO-8859-1").toLocal8Bit()); QCOMPARE(part->signatures().size(), 1); QCOMPARE(part->signatures()[0]->partMetaData()->isGoodSignature, true); auto contentAttachmentList = otp.collectAttachmentParts(); QCOMPARE(contentAttachmentList.size(), 1); } void testSmimeOpaqueSign() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("smime-opaque-sign.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->text(), QStringLiteral("A simple signed only test.")); } void testSmimeEncrypted() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("smime-encrypted.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->text(), QStringLiteral("The quick brown fox jumped over the lazy dog.")); } void testSmimeSignedApple() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("smime-signed-apple.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); // QCOMPARE(part->text(), QStringLiteral("A simple signed only test.")); } void testSmimeEncryptedOctetStream() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("smime-encrypted-octet-stream.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->text(), QStringLiteral("The quick brown fox jumped over the lazy dog.")); } void testSmimeOpaqueSignedEncryptedAttachment() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("smime-opaque-signed-encrypted-attachment.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->text(), QStringLiteral("This is an Opaque S/MIME encrypted and signed message with attachment")); } void testSmimeOpaqueEncSign() { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QLatin1String("smime-opaque-enc+sign.mbox"))); otp.print(); otp.decryptParts(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->text(), QStringLiteral("Encrypted and signed mail.")); } }; QTEST_GUILESS_MAIN(MimeTreeParserTest) #include "mimetreeparsertest.moc" diff --git a/src/core/messagepart.cpp b/src/core/messagepart.cpp index 62f5b5b..e9d11f5 100644 --- a/src/core/messagepart.cpp +++ b/src/core/messagepart.cpp @@ -1,964 +1,962 @@ // SPDX-FileCopyrightText: 2015 Sandro Knauß // SPDX-FileCopyrightText: 2017 Christian Mollekopf // SPDX-License-Identifier: LGPL-2.0-or-later #include "messagepart.h" #include "cryptohelper.h" #include "mimetreeparser_core_debug.h" #include "objecttreeparser.h" #include "utils.h" #include #include #include using namespace MimeTreeParser; using namespace Crypto; //------MessagePart----------------------- MessagePart::MessagePart(ObjectTreeParser *otp, const QString &text, KMime::Content *node) : mText(text) , mOtp(otp) , mParentPart(nullptr) , mNode(node) // only null for messagepartlist , mError(NoError) , mRoot(false) { } MessagePart::~MessagePart() { for (auto n : std::as_const(mNodesToDelete)) { delete n; } } MessagePart::Disposition MessagePart::disposition() const { if (!mNode) { return Invalid; } const auto cd = mNode->contentDisposition(false); if (!cd) { return Invalid; } switch (cd->disposition()) { case KMime::Headers::CDinline: return Inline; case KMime::Headers::CDattachment: return Attachment; default: return Invalid; } } QString MessagePart::filename() const { if (!mNode) { return {}; } if (const auto cd = mNode->contentDisposition(false)) { const auto name = cd->filename(); // Allow for a fallback for mails that have a ContentDisposition header, but don't set the filename anyways. // Not the recommended way, but exists. if (!name.isEmpty()) { return name; } } if (const auto ct = mNode->contentType(false)) { return ct->name(); } return {}; } static KMime::Headers::ContentType *contentType(KMime::Content *node) { if (node) { return node->contentType(false); } return nullptr; } QByteArray MessagePart::charset() const { if (!mNode) { return "us-ascii"; } if (auto ct = contentType(mNode)) { return ct->charset(); } // Per rfc2045 us-ascii is the default return "us-ascii"; } QByteArray MessagePart::mimeType() const { if (auto ct = contentType(mNode)) { return ct->mimeType(); } return {}; } bool MessagePart::isText() const { if (auto ct = contentType(mNode)) { return ct->isText(); } return false; } MessagePart::Error MessagePart::error() const { return mError; } QString MessagePart::errorString() const { return mMetaData.errorText; } PartMetaData *MessagePart::partMetaData() { return &mMetaData; } bool MessagePart::isAttachment() const { if (mNode) { return KMime::isAttachment(mNode); } return false; } KMime::Content *MessagePart::node() const { return mNode; } void MessagePart::setIsRoot(bool root) { mRoot = root; } bool MessagePart::isRoot() const { return mRoot; } QString MessagePart::text() const { return mText; } void MessagePart::setText(const QString &text) { mText = text; } bool MessagePart::isHtml() const { return false; } MessagePart *MessagePart::parentPart() const { return mParentPart; } void MessagePart::setParentPart(MessagePart *parentPart) { mParentPart = parentPart; } QString MessagePart::htmlContent() const { return text(); } QString MessagePart::plaintextContent() const { return text(); } void MessagePart::parseInternal(KMime::Content *node, bool onlyOneMimePart) { auto subMessagePart = mOtp->parseObjectTreeInternal(node, onlyOneMimePart); mRoot = subMessagePart->isRoot(); for (const auto &part : std::as_const(subMessagePart->subParts())) { appendSubPart(part); } } void MessagePart::parseInternal(const QByteArray &data) { auto tempNode = new KMime::Content(); const auto lfData = KMime::CRLFtoLF(data); // We have to deal with both bodies and full parts. In inline encrypted/signed parts we can have nested parts, // or just plain-text, and both ends up here. setContent defaults to setting only the header, so we have to avoid this. if (lfData.contains("\n\n")) { tempNode->setContent(lfData); } else { tempNode->setBody(lfData); } tempNode->parse(); tempNode->contentType()->setCharset(charset()); bindLifetime(tempNode); if (!tempNode->head().isEmpty()) { tempNode->contentDescription()->from7BitString("temporary node"); } parseInternal(tempNode); } QString MessagePart::renderInternalText() const { QString text; for (const auto &mp : subParts()) { text += mp->text(); } return text; } void MessagePart::appendSubPart(const MessagePart::Ptr &messagePart) { messagePart->setParentPart(this); mBlocks.append(messagePart); } const QVector &MessagePart::subParts() const { return mBlocks; } bool MessagePart::hasSubParts() const { return !mBlocks.isEmpty(); } QVector MessagePart::signatures() const { QVector list; if (auto sig = dynamic_cast(const_cast(this))) { list << sig; } auto parent = parentPart(); while (parent) { if (auto sig = dynamic_cast(parent)) { list << sig; } parent = parent->parentPart(); } return list; } QVector MessagePart::encryptions() const { QVector list; if (auto sig = dynamic_cast(const_cast(this))) { list << sig; } auto parent = parentPart(); while (parent) { if (auto sig = dynamic_cast(parent)) { list << sig; } parent = parent->parentPart(); } return list; } KMMsgEncryptionState MessagePart::encryptionState() const { if (!encryptions().isEmpty()) { return KMMsgFullyEncrypted; } return KMMsgNotEncrypted; } KMMsgSignatureState MessagePart::signatureState() const { if (!signatures().isEmpty()) { return KMMsgFullySigned; } return KMMsgNotSigned; } void MessagePart::bindLifetime(KMime::Content *node) { mNodesToDelete << node; } KMime::Headers::Base *MimeTreeParser::MessagePart::header(const char *header) const { if (node() && node()->hasHeader(header)) { return node()->headerByType(header); } if (auto parent = parentPart()) { return parent->header(header); } return nullptr; } //-----MessagePartList---------------------- MessagePartList::MessagePartList(ObjectTreeParser *otp, KMime::Content *node) : MessagePart(otp, QString(), node) { } QString MessagePartList::text() const { return renderInternalText(); } QString MessagePartList::plaintextContent() const { return QString(); } QString MessagePartList::htmlContent() const { return QString(); } //-----TextMessageBlock---------------------- TextMessagePart::TextMessagePart(ObjectTreeParser *otp, KMime::Content *node) : MessagePartList(otp, node) , mSignatureState(KMMsgSignatureStateUnknown) , mEncryptionState(KMMsgEncryptionStateUnknown) { if (!mNode) { qCWarning(MIMETREEPARSER_CORE_LOG) << "not a valid node"; return; } parseContent(); } void TextMessagePart::parseContent() { mSignatureState = KMMsgNotSigned; mEncryptionState = KMMsgNotEncrypted; const auto blocks = prepareMessageForDecryption(mNode->decodedContent()); // We also get blocks for unencrypted messages if (!blocks.isEmpty()) { const auto aCodec = mOtp->codecFor(mNode); const auto cryptProto = OpenPGP; /* The (overall) signature/encrypted status is broken * if one unencrypted part is at the beginning or in the middle * because mailmain adds an unencrypted part at the end this should not break the overall status * * That's why we first set the tmp status and if one crypted/signed block comes afterwards, than * the status is set to unencryped */ bool fullySignedOrEncrypted = true; bool fullySignedOrEncryptedTmp = true; for (const auto &block : blocks) { if (!fullySignedOrEncryptedTmp) { fullySignedOrEncrypted = false; } if (block.type() == NoPgpBlock && !block.text().trimmed().isEmpty()) { fullySignedOrEncryptedTmp = false; appendSubPart(MessagePart::Ptr(new MessagePart(mOtp, aCodec->toUnicode(KMime::CRLFtoLF(block.text()))))); } else if (block.type() == PgpMessageBlock) { KMime::Content *content = new KMime::Content; content->setBody(block.text()); content->parse(); content->contentType()->setCharset(charset()); EncryptedMessagePart::Ptr mp(new EncryptedMessagePart(mOtp, QString(), cryptProto, content, content, false)); mp->bindLifetime(content); mp->setIsEncrypted(true); appendSubPart(mp); } else if (block.type() == ClearsignedBlock) { KMime::Content *content = new KMime::Content; content->setBody(block.text()); content->parse(); content->contentType()->setCharset(charset()); SignedMessagePart::Ptr mp(new SignedMessagePart(mOtp, cryptProto, nullptr, content, false)); mp->bindLifetime(content); mp->setIsSigned(true); appendSubPart(mp); } else { continue; } const auto mp = subParts().last().staticCast(); const PartMetaData *messagePart(mp->partMetaData()); if (!messagePart->isEncrypted && !messagePart->isSigned && !block.text().trimmed().isEmpty()) { mp->setText(aCodec->toUnicode(KMime::CRLFtoLF(block.text()))); } if (messagePart->isEncrypted) { mEncryptionState = KMMsgPartiallyEncrypted; } if (messagePart->isSigned) { mSignatureState = KMMsgPartiallySigned; } } // Do we have an fully Signed/Encrypted Message? if (fullySignedOrEncrypted) { if (mSignatureState == KMMsgPartiallySigned) { mSignatureState = KMMsgFullySigned; } if (mEncryptionState == KMMsgPartiallyEncrypted) { mEncryptionState = KMMsgFullyEncrypted; } } } } KMMsgEncryptionState TextMessagePart::encryptionState() const { if (mEncryptionState == KMMsgNotEncrypted) { return MessagePart::encryptionState(); } return mEncryptionState; } KMMsgSignatureState TextMessagePart::signatureState() const { if (mSignatureState == KMMsgNotSigned) { return MessagePart::signatureState(); } return mSignatureState; } //-----AttachmentMessageBlock---------------------- AttachmentMessagePart::AttachmentMessagePart(ObjectTreeParser *otp, KMime::Content *node) : TextMessagePart(otp, node) { } //-----HtmlMessageBlock---------------------- HtmlMessagePart::HtmlMessagePart(ObjectTreeParser *otp, KMime::Content *node) : MessagePart(otp, QString(), node) { if (!mNode) { qCWarning(MIMETREEPARSER_CORE_LOG) << "not a valid node"; return; } setText(mOtp->codecFor(mNode)->toUnicode(KMime::CRLFtoLF(mNode->decodedContent()))); } //-----MimeMessageBlock---------------------- MimeMessagePart::MimeMessagePart(ObjectTreeParser *otp, KMime::Content *node, bool onlyOneMimePart) : MessagePart(otp, QString(), node) { if (!mNode) { qCWarning(MIMETREEPARSER_CORE_LOG) << "not a valid node"; return; } parseInternal(mNode, onlyOneMimePart); } MimeMessagePart::~MimeMessagePart() { } QString MimeMessagePart::text() const { return renderInternalText(); } QString MimeMessagePart::plaintextContent() const { return QString(); } QString MimeMessagePart::htmlContent() const { return QString(); } //-----AlternativeMessagePart---------------------- AlternativeMessagePart::AlternativeMessagePart(ObjectTreeParser *otp, KMime::Content *node) : MessagePart(otp, QString(), node) { - qWarning() << "AlternativeMessagePart"; if (auto dataIcal = findTypeInDirectChildren(mNode, "text/calendar")) { mChildParts[MultipartIcal] = MimeMessagePart::Ptr(new MimeMessagePart(mOtp, dataIcal, true)); - return; } if (auto dataText = findTypeInDirectChildren(mNode, "text/plain")) { mChildParts[MultipartPlain] = MimeMessagePart::Ptr(new MimeMessagePart(mOtp, dataText, true)); } if (auto dataHtml = findTypeInDirectChildren(mNode, "text/html")) { mChildParts[MultipartHtml] = MimeMessagePart::Ptr(new MimeMessagePart(mOtp, dataHtml, true)); } else { // If we didn't find the HTML part as the first child of the multipart/alternative, it might // be that this is a HTML message with images, and text/plain and multipart/related are the // immediate children of this multipart/alternative node. // In this case, the HTML node is a child of multipart/related. // In the case of multipart/related we don't expect multiple html parts, it is usually used to group attachments // with html content. // // In any case, this is not a complete implementation of MIME, but an approximation for the kind of mails we actually see in the wild. auto data = [&] { if (auto d = findTypeInDirectChildren(mNode, "multipart/related")) { return d; } return findTypeInDirectChildren(mNode, "multipart/mixed"); }(); if (data) { QString htmlContent; const auto parts = data->contents(); for (auto p : parts) { if ((!p->contentType()->isEmpty()) && (p->contentType()->mimeType() == "text/html")) { htmlContent += MimeMessagePart(mOtp, p, true).text(); } else if (KMime::isAttachment(p)) { appendSubPart(MimeMessagePart::Ptr(new MimeMessagePart(otp, p, true))); } } mChildParts[MultipartHtml] = MessagePart::Ptr(new MessagePart(mOtp, htmlContent, nullptr)); } } } AlternativeMessagePart::~AlternativeMessagePart() { } QList AlternativeMessagePart::availableModes() { return mChildParts.keys(); } QString AlternativeMessagePart::text() const { if (mChildParts.contains(MultipartPlain)) { return mChildParts[MultipartPlain]->text(); } return QString(); } bool AlternativeMessagePart::isHtml() const { return mChildParts.contains(MultipartHtml); } QString AlternativeMessagePart::plaintextContent() const { return text(); } QString AlternativeMessagePart::htmlContent() const { if (mChildParts.contains(MultipartHtml)) { return mChildParts[MultipartHtml]->text(); } else { return plaintextContent(); } } QString AlternativeMessagePart::icalContent() const { if (mChildParts.contains(MultipartIcal)) { return mChildParts[MultipartIcal]->text(); } return {}; } //-----CertMessageBlock---------------------- CertMessagePart::CertMessagePart(ObjectTreeParser *otp, KMime::Content *node, const CryptoProtocol cryptoProto) : MessagePart(otp, QString(), node) , mProtocol(cryptoProto) { if (!mNode) { qCWarning(MIMETREEPARSER_CORE_LOG) << "not a valid node"; return; } } CertMessagePart::~CertMessagePart() { } void CertMessagePart::import() { importKey(mProtocol, mNode->decodedContent()); } QString CertMessagePart::text() const { return QString(); } //-----SignedMessageBlock--------------------- SignedMessagePart::SignedMessagePart(ObjectTreeParser *otp, const CryptoProtocol cryptoProto, KMime::Content *node, KMime::Content *signedData, bool parseAfterDecryption) : MessagePart(otp, {}, node) , mParseAfterDecryption(parseAfterDecryption) , mProtocol(cryptoProto) , mSignedData(signedData) { mMetaData.isSigned = true; mMetaData.isGoodSignature = false; mMetaData.status = i18n("Wrong Crypto Plug-In."); } SignedMessagePart::~SignedMessagePart() { } void SignedMessagePart::setIsSigned(bool isSigned) { mMetaData.isSigned = isSigned; } bool SignedMessagePart::isSigned() const { return mMetaData.isSigned; } static QString prettifyDN(const char *uid) { // We used to use QGpgME::DN::prettyDN here. But I'm not sure what we actually need it for. return QString::fromUtf8(uid); } static void sigStatusToMetaData(PartMetaData &mMetaData, const Signature &signature) { mMetaData.isGoodSignature = !signature.status; if (!mMetaData.isGoodSignature) { qWarning() << "The signature is bad." << signature.status; } // save extended signature status flags mMetaData.keyMissing = signature.result == Crypto::Signature::KeyNotFound; mMetaData.keyExpired = signature.result == Crypto::Signature::Expired; Key key; if (mMetaData.isGoodSignature) { // Search for the key by its fingerprint so that we can check for trust etc. const auto keys = findKeys({QString::fromUtf8(signature.fingerprint)}); if (keys.size() > 1) { // Should not happen qCDebug(MIMETREEPARSER_CORE_LOG) << "Oops: Found more then one Key for Fingerprint: " << signature.fingerprint; } if (keys.empty()) { // Should not happen at this point qCWarning(MIMETREEPARSER_CORE_LOG) << "Oops: Found no Key for Fingerprint: " << signature.fingerprint; } else { key = keys[0]; } } mMetaData.keyId = key.keyId; if (mMetaData.keyId.isEmpty()) { mMetaData.keyId = signature.fingerprint; } mMetaData.keyIsTrusted = signature.isTrusted; if (!key.userIds.empty()) { mMetaData.signer = prettifyDN(key.userIds[0].id.data()); } for (const auto &userId : key.userIds) { QString email = QString::fromUtf8(userId.email); // Work around cryptplug bug where email addresses are specified as angle-addr ( ), // not addr-spec ( person@example.org ): if (email.startsWith(QLatin1Char('<')) && email.endsWith(QLatin1Char('>'))) { email = email.mid(1, email.length() - 2); } if (!email.isEmpty()) { mMetaData.signerMailAddresses.append(email); } } mMetaData.creationTime = signature.creationTime; if (mMetaData.signer.isEmpty()) { if (!key.userIds.empty()) { mMetaData.signer = prettifyDN(key.userIds[0].name.data()); } if (!mMetaData.signerMailAddresses.empty()) { if (mMetaData.signer.isEmpty()) { mMetaData.signer = mMetaData.signerMailAddresses.front(); } else { mMetaData.signer += QLatin1String(" <") + mMetaData.signerMailAddresses.front() + QLatin1Char('>'); } } } } void SignedMessagePart::startVerification() { if (!mSignedData) { return; } mMetaData.isSigned = false; mMetaData.status = i18n("Wrong Crypto Plug-In."); mMetaData.isEncrypted = false; mMetaData.isDecryptable = false; const auto codec = mOtp->codecFor(mSignedData); // If we have a mNode, this is a detached signature if (mNode) { const auto signature = mNode->decodedContent(); // This is necessary in case the original data contained CRLF's. Otherwise the signature will not match the data (since KMIME normalizes to LF) const QByteArray signedData = KMime::LFtoCRLF(mSignedData->encodedContent()); setVerificationResult(Crypto::verifyDetachedSignature(mProtocol, signature, signedData), signedData); setText(codec->toUnicode(KMime::CRLFtoLF(signedData))); } else { QByteArray outdata; setVerificationResult(Crypto::verifyOpaqueSignature(mProtocol, mSignedData->decodedContent(), outdata), outdata); setText(codec->toUnicode(KMime::CRLFtoLF(outdata))); } if (!mMetaData.isSigned) { mMetaData.creationTime = QDateTime(); } } void SignedMessagePart::setVerificationResult(const VerificationResult &result, const QByteArray &signedData) { const auto signatures = result.signatures; // FIXME // mMetaData.auditLogError = result.error; if (!signatures.empty()) { mMetaData.isSigned = true; sigStatusToMetaData(mMetaData, signatures.front()); if (!signedData.isEmpty() && mParseAfterDecryption) { parseInternal(signedData); } } } QString SignedMessagePart::plaintextContent() const { if (!mNode) { return MessagePart::text(); } else { return QString(); } } QString SignedMessagePart::htmlContent() const { if (!mNode) { return MessagePart::text(); } else { return QString(); } } //-----CryptMessageBlock--------------------- EncryptedMessagePart::EncryptedMessagePart(ObjectTreeParser *otp, const QString &text, const CryptoProtocol cryptoProto, KMime::Content *node, KMime::Content *encryptedNode, bool parseAfterDecryption) : MessagePart(otp, text, node) , mParseAfterDecryption(parseAfterDecryption) , mProtocol(cryptoProto) , mEncryptedNode(encryptedNode) { mMetaData.isSigned = false; mMetaData.isGoodSignature = false; mMetaData.isEncrypted = false; mMetaData.isDecryptable = false; mMetaData.status = i18n("Wrong Crypto Plug-In."); } void EncryptedMessagePart::setIsEncrypted(bool encrypted) { mMetaData.isEncrypted = encrypted; } bool EncryptedMessagePart::isEncrypted() const { return mMetaData.isEncrypted; } bool EncryptedMessagePart::isDecryptable() const { return mMetaData.isDecryptable; } bool EncryptedMessagePart::decrypt(KMime::Content &data) { mError = NoError; mMetaData.errorText.clear(); // FIXME // mMetaData.auditLogError = GpgME::Error(); mMetaData.auditLog.clear(); const QByteArray ciphertext = data.decodedContent(); QByteArray plainText; DecryptionResult decryptResult; VerificationResult verifyResult; std::tie(decryptResult, verifyResult) = Crypto::decryptAndVerify(mProtocol, ciphertext, plainText); mMetaData.isSigned = verifyResult.signatures.size() > 0; // Normalize CRLF's plainText = KMime::CRLFtoLF(plainText); const auto codec = mOtp->codecFor(&data); const auto decoded = codec->toUnicode(plainText); if (verifyResult.signatures.size() > 0) { // We simply attach a signed message part to indicate that this content is also signed // We're forwarding mNode to not loose the encoding information auto subPart = SignedMessagePart::Ptr(new SignedMessagePart(mOtp, mProtocol, mNode, nullptr)); subPart->setText(decoded); subPart->setVerificationResult(verifyResult, plainText); appendSubPart(subPart); } if (decryptResult.error && mMetaData.isSigned) { // Only a signed part mMetaData.isEncrypted = false; mDecryptedData = plainText; return true; } if (mMetaData.isEncrypted) { mMetaData.keyId = [&] { for (const auto &recipient : std::as_const(decryptResult.recipients)) { if (recipient.secretKeyAvailable) { return recipient.keyId; } } return QByteArray{}; }(); } if (!decryptResult.error) { mDecryptedData = plainText; setText(decoded); } else { mMetaData.isEncrypted = decryptResult.result != Crypto::DecryptionResult::NotEncrypted; qWarning() << "Failed to decrypt : " << decryptResult.error; if (decryptResult.result == Crypto::DecryptionResult::NoSecretKeyError) { mError = NoKeyError; mMetaData.errorText = i18n("Could not decrypt the data: no key found for recipients."); } else if (decryptResult.result == Crypto::DecryptionResult::PassphraseError) { mError = PassphraseError; } else { mError = UnknownError; mMetaData.errorText = i18n("Could not decrypt the data."); } setText(QString::fromUtf8(mDecryptedData.constData())); return false; } return true; } void EncryptedMessagePart::startDecryption(KMime::Content *data) { mMetaData.isEncrypted = true; mMetaData.isDecryptable = decrypt(*data); if (mParseAfterDecryption && !mMetaData.isSigned) { parseInternal(mDecryptedData); } } void EncryptedMessagePart::startDecryption() { if (mEncryptedNode) { startDecryption(mEncryptedNode); } else { startDecryption(mNode); } } QString EncryptedMessagePart::plaintextContent() const { if (!mNode) { return MessagePart::text(); } else { return QString(); } } QString EncryptedMessagePart::htmlContent() const { if (!mNode) { return MessagePart::text(); } else { return QString(); } } QString EncryptedMessagePart::text() const { if (hasSubParts()) { auto _mp = (subParts()[0]).dynamicCast(); if (_mp) { return _mp->text(); } else { return MessagePart::text(); } } else { return MessagePart::text(); } } EncapsulatedRfc822MessagePart::EncapsulatedRfc822MessagePart(ObjectTreeParser *otp, KMime::Content *node, const KMime::Message::Ptr &message) : MessagePart(otp, QString(), node) , mMessage(message) { mMetaData.isEncrypted = false; mMetaData.isSigned = false; mMetaData.isEncapsulatedRfc822Message = true; if (!mMessage) { qCWarning(MIMETREEPARSER_CORE_LOG) << "Node is of type message/rfc822 but doesn't have a message!"; return; } parseInternal(message.data()); } QString EncapsulatedRfc822MessagePart::text() const { return renderInternalText(); } QString EncapsulatedRfc822MessagePart::from() const { if (auto from = mMessage->from(false)) { return from->asUnicodeString(); } return {}; } QDateTime EncapsulatedRfc822MessagePart::date() const { if (auto date = mMessage->date(false)) { return date->dateTime(); } return {}; } HeadersPart::HeadersPart(ObjectTreeParser *otp, KMime::Content *node) : MessagePart(otp, QString(), node) { }