diff --git a/autotests/core/attachmentmodeltest.cpp b/autotests/core/attachmentmodeltest.cpp index 65658d6..98def48 100644 --- a/autotests/core/attachmentmodeltest.cpp +++ b/autotests/core/attachmentmodeltest.cpp @@ -1,97 +1,97 @@ // SPDX-FileCopyrightText: 2023 g10 Code GmbH // SPDX-FileContributor: Carl Schwan // SPDX-License-Identifier: LGPL-2.0-or-later #include #include "attachmentmodel.h" #include "messageparser.h" #include #include #include KMime::Message::Ptr readMailFromFile(const QString &mailFile) { - QFile file(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile); + QFile file(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile); file.open(QIODevice::ReadOnly); Q_ASSERT(file.isOpen()); auto mailData = KMime::CRLFtoLF(file.readAll()); KMime::Message::Ptr message(new KMime::Message); message->setContent(mailData); message->parse(); return message; } class AttachmentModelTest : public QObject { Q_OBJECT private Q_SLOTS: void openMailWithOneAttachementTest() { MessageParser messageParser; - messageParser.setMessage(readMailFromFile(QLatin1String("attachment.mbox"))); + messageParser.setMessage(readMailFromFile(QLatin1StringView("attachment.mbox"))); auto attachmentModel = messageParser.attachments(); new QAbstractItemModelTester(attachmentModel); QCOMPARE(attachmentModel->rowCount(), 1); QCOMPARE(attachmentModel->data(attachmentModel->index(0, 0), AttachmentModel::TypeRole).toString(), QStringLiteral("image/jpeg")); QCOMPARE(attachmentModel->data(attachmentModel->index(0, 0), AttachmentModel::NameRole).toString(), QStringLiteral("aqnaozisxya.jpeg")); QCOMPARE(attachmentModel->data(attachmentModel->index(0, 0), AttachmentModel::SizeRole).toString(), QStringLiteral("100.22 KB")); QCOMPARE(attachmentModel->data(attachmentModel->index(0, 0), AttachmentModel::IsEncryptedRole).toBool(), false); QCOMPARE(attachmentModel->data(attachmentModel->index(0, 0), AttachmentModel::IsSignedRole).toBool(), false); QCOMPARE(attachmentModel->data(attachmentModel->index(0, AttachmentModel::IsEncryptedColumn), Qt::CheckStateRole).value(), Qt::Unchecked); QCOMPARE(attachmentModel->data(attachmentModel->index(0, AttachmentModel::IsSignedColumn), Qt::CheckStateRole).value(), Qt::Unchecked); QCOMPARE(attachmentModel->data(attachmentModel->index(0, AttachmentModel::SizeColumn), Qt::DisplayRole).toString(), QStringLiteral("100.22 KB")); } void saveTest() { MessageParser messageParser; - messageParser.setMessage(readMailFromFile(QLatin1String("attachment.mbox"))); + messageParser.setMessage(readMailFromFile(QLatin1StringView("attachment.mbox"))); auto attachmentModel = messageParser.attachments(); QTemporaryFile file; QVERIFY(file.open()); const auto fileName = attachmentModel->saveAttachmentToPath(0, file.fileName()); QFile file2(fileName); QVERIFY(file2.open(QIODevice::ReadOnly | QIODevice::Text)); QVERIFY(!file2.readAll().isEmpty()); } void openTest() { MessageParser messageParser; - messageParser.setMessage(readMailFromFile(QLatin1String("attachment.mbox"))); + messageParser.setMessage(readMailFromFile(QLatin1StringView("attachment.mbox"))); auto attachmentModel = messageParser.attachments(); QSignalSpy spy(attachmentModel, &AttachmentModel::errorOccurred); QVERIFY(spy.isValid()); attachmentModel->openAttachment(0); // Check no error occurred QCOMPARE(spy.count(), 0); } void saveInvalidPathTest() { MessageParser messageParser; - messageParser.setMessage(readMailFromFile(QLatin1String("attachment.mbox"))); + messageParser.setMessage(readMailFromFile(QLatin1StringView("attachment.mbox"))); auto attachmentModel = messageParser.attachments(); QSignalSpy spy(attachmentModel, &AttachmentModel::errorOccurred); QVERIFY(spy.isValid()); const auto fileName = attachmentModel->saveAttachmentToPath(0, QStringLiteral("/does/not/exist")); QList arguments = spy.takeFirst(); QVERIFY(arguments.at(0).userType() == QMetaType::QString); } }; QTEST_MAIN(AttachmentModelTest) #include "attachmentmodeltest.moc" diff --git a/autotests/core/attachmenttest.cpp b/autotests/core/attachmenttest.cpp index ac51e3d..2069ad6 100644 --- a/autotests/core/attachmenttest.cpp +++ b/autotests/core/attachmenttest.cpp @@ -1,57 +1,57 @@ // SPDX-FileCopyrightText: 2015 Volker Krause // SPDX-License-Identifier: LGPL-2.0-or-later #include #include "setupenv.h" #include using namespace MimeTreeParser; class AttachmentTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testEncryptedAttachment_data(); void testEncryptedAttachment(); }; QTEST_MAIN(AttachmentTest) QByteArray readMailFromFile(const QString &mailFile) { - QFile file(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile); + QFile file(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile); file.open(QIODevice::ReadOnly); Q_ASSERT(file.isOpen()); return file.readAll(); } void AttachmentTest::initTestCase() { MimeTreeParser::Test::setupEnv(); } void AttachmentTest::testEncryptedAttachment_data() { QTest::addColumn("mbox"); QTest::newRow("encrypted") << "openpgp-encrypted-two-attachments.mbox"; QTest::newRow("signed") << "openpgp-signed-two-attachments.mbox"; QTest::newRow("signed+encrypted") << "openpgp-signed-encrypted-two-attachments.mbox"; QTest::newRow("encrypted+partial signed") << "openpgp-encrypted-partially-signed-attachments.mbox"; } void AttachmentTest::testEncryptedAttachment() { QFETCH(QString, mbox); ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(mbox)); otp.decryptAndVerify(); otp.print(); auto attachmentParts = otp.collectAttachmentParts(); QCOMPARE(attachmentParts.size(), 2); } #include "attachmenttest.moc" diff --git a/autotests/core/cryptohelpertest.cpp b/autotests/core/cryptohelpertest.cpp index 374e087..982df83 100644 --- a/autotests/core/cryptohelpertest.cpp +++ b/autotests/core/cryptohelpertest.cpp @@ -1,182 +1,182 @@ // SPDX-FileCopyrightText: 2015 Sandro Knauß // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "cryptohelpertest.h" #include "cryptohelper.h" #include using namespace MimeTreeParser; QByteArray readMailFromFile(const QString &mailFile) { - QFile file(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile); + QFile file(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile); file.open(QIODevice::ReadOnly); Q_ASSERT(file.isOpen()); return file.readAll(); } void CryptoHelperTest::testPMFDEmpty() { QCOMPARE(prepareMessageForDecryption("").count(), 0); } void CryptoHelperTest::testPMFDWithNoPGPBlock() { const QByteArray text = "testblabla"; const QList blocks = prepareMessageForDecryption(text); QCOMPARE(blocks.count(), 1); QCOMPARE(blocks[0].text(), text); QCOMPARE(blocks[0].type(), NoPgpBlock); } void CryptoHelperTest::testPGPBlockType() { const QString blockText = QStringLiteral("text"); const QString preString = QStringLiteral("before\n"); for (int i = 1; i <= PrivateKeyBlock; ++i) { QString name; switch (i) { case PgpMessageBlock: name = QStringLiteral("MESSAGE"); break; case MultiPgpMessageBlock: name = QStringLiteral("MESSAGE PART"); break; case SignatureBlock: name = QStringLiteral("SIGNATURE"); break; case ClearsignedBlock: name = QStringLiteral("SIGNED MESSAGE"); break; case PublicKeyBlock: name = QStringLiteral("PUBLIC KEY BLOCK"); break; case PrivateKeyBlock: name = QStringLiteral("PRIVATE KEY BLOCK"); break; } - QString text = QLatin1String("-----BEGIN PGP ") + name + QLatin1Char('\n') + blockText; + QString text = QLatin1StringView("-----BEGIN PGP ") + name + QLatin1Char('\n') + blockText; QList blocks = prepareMessageForDecryption(preString.toLatin1() + text.toLatin1()); QCOMPARE(blocks.count(), 1); QCOMPARE(blocks[0].type(), UnknownBlock); - text += QLatin1String("\n-----END PGP ") + name + QLatin1Char('\n'); + text += QLatin1StringView("\n-----END PGP ") + name + QLatin1Char('\n'); blocks = prepareMessageForDecryption(preString.toLatin1() + text.toLatin1()); QCOMPARE(blocks.count(), 2); QCOMPARE(blocks[1].text(), text.toLatin1()); QCOMPARE(blocks[1].type(), static_cast(i)); } } void CryptoHelperTest::testDeterminePGPBlockType() { const QString blockText = QStringLiteral("text"); for (int i = 1; i <= PrivateKeyBlock; ++i) { QString name; switch (i) { case PgpMessageBlock: name = QStringLiteral("MESSAGE"); break; case MultiPgpMessageBlock: name = QStringLiteral("MESSAGE PART"); break; case SignatureBlock: name = QStringLiteral("SIGNATURE"); break; case ClearsignedBlock: name = QStringLiteral("SIGNED MESSAGE"); break; case PublicKeyBlock: name = QStringLiteral("PUBLIC KEY BLOCK"); break; case PrivateKeyBlock: name = QStringLiteral("PRIVATE KEY BLOCK"); break; } - const QString text = QLatin1String("-----BEGIN PGP ") + name + QLatin1Char('\n') + blockText + QLatin1Char('\n'); + const QString text = QLatin1StringView("-----BEGIN PGP ") + name + QLatin1Char('\n') + blockText + QLatin1Char('\n'); const Block block = Block(text.toLatin1()); QCOMPARE(block.text(), text.toLatin1()); QCOMPARE(block.type(), static_cast(i)); } } void CryptoHelperTest::testEmbededPGPBlock() { const QByteArray text = QByteArray("before\n-----BEGIN PGP MESSAGE-----\ncrypted - you see :)\n-----END PGP MESSAGE-----\nafter"); const QList blocks = prepareMessageForDecryption(text); QCOMPARE(blocks.count(), 3); QCOMPARE(blocks[0].text(), QByteArray("before\n")); QCOMPARE(blocks[1].text(), QByteArray("-----BEGIN PGP MESSAGE-----\ncrypted - you see :)\n-----END PGP MESSAGE-----\n")); QCOMPARE(blocks[2].text(), QByteArray("after")); } void CryptoHelperTest::testClearSignedMessage() { const QByteArray text = QByteArray( "before\n-----BEGIN PGP SIGNED MESSAGE-----\nsigned content\n-----BEGIN PGP SIGNATURE-----\nfancy signature\n-----END PGP SIGNATURE-----\nafter"); const QList blocks = prepareMessageForDecryption(text); QCOMPARE(blocks.count(), 3); QCOMPARE(blocks[0].text(), QByteArray("before\n")); QCOMPARE(blocks[1].text(), QByteArray("-----BEGIN PGP SIGNED MESSAGE-----\nsigned content\n-----BEGIN PGP SIGNATURE-----\nfancy signature\n-----END PGP SIGNATURE-----\n")); QCOMPARE(blocks[2].text(), QByteArray("after")); } void CryptoHelperTest::testMultipleBlockMessage() { const QByteArray text = QByteArray( "before\n-----BEGIN PGP SIGNED MESSAGE-----\nsigned content\n-----BEGIN PGP SIGNATURE-----\nfancy signature\n-----END PGP " "SIGNATURE-----\nafter\n-----BEGIN PGP MESSAGE-----\ncrypted - you see :)\n-----END PGP MESSAGE-----\n"); const QList blocks = prepareMessageForDecryption(text); QCOMPARE(blocks.count(), 4); QCOMPARE(blocks[0].text(), QByteArray("before\n")); QCOMPARE(blocks[1].text(), QByteArray("-----BEGIN PGP SIGNED MESSAGE-----\nsigned content\n-----BEGIN PGP SIGNATURE-----\nfancy signature\n-----END PGP SIGNATURE-----\n")); QCOMPARE(blocks[2].text(), QByteArray("after\n")); QCOMPARE(blocks[3].text(), QByteArray("-----BEGIN PGP MESSAGE-----\ncrypted - you see :)\n-----END PGP MESSAGE-----\n")); } void CryptoHelperTest::testDecryptMessage() { auto message = KMime::Message::Ptr(new KMime::Message); - message->setContent(readMailFromFile(QLatin1String("openpgp-encrypted+signed.mbox"))); + message->setContent(readMailFromFile(QLatin1StringView("openpgp-encrypted+signed.mbox"))); message->parse(); bool wasEncrypted = false; auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted); QVERIFY(wasEncrypted); QVERIFY(decryptedMessage); QCOMPARE(decryptedMessage->decodedContent(), QByteArray("encrypted message text")); QCOMPARE(decryptedMessage->encodedContent(), QByteArray("From test@kolab.org Wed, 08 Sep 2010 17: 02:52 +0200\nFrom: OpenPGP Test \nTo: test@kolab.org\nSubject: OpenPGP " "encrypted\nDate: Wed, 08 Sep 2010 17:02:52 +0200\nUser-Agent: KMail/4.6 pre (Linux/2.6.34-rc2-2-default; KDE/4.5.60; x86_64; ; " ")\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7Bit\nContent-Type: text/plain; charset=\"us-ascii\"\n\nencrypted message text")); } void CryptoHelperTest::testDecryptInlineMessage() { auto message = KMime::Message::Ptr(new KMime::Message); - message->setContent(readMailFromFile(QLatin1String("openpgp-inline-encrypted+nonenc.mbox"))); + message->setContent(readMailFromFile(QLatin1StringView("openpgp-inline-encrypted+nonenc.mbox"))); message->parse(); qWarning() << message->decodedContent(); bool wasEncrypted = false; auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted); QVERIFY(wasEncrypted); QVERIFY(decryptedMessage); QCOMPARE(decryptedMessage->decodedContent(), QByteArray("Not encrypted not signed :(\n\nsome random text\n")); qWarning() << decryptedMessage->encodedContent(); QCOMPARE(decryptedMessage->encodedContent(), QByteArray("From test@kolab.org Wed, 25 May 2011 23: 49:40 +0100\nFrom: OpenPGP Test \nTo: test@kolab.org\nSubject: " "inlinepgpencrypted + non enc text\nDate: Wed, 25 May 2011 23:49:40 +0100\nMessage-ID: " "<1786696.yKXrOjjflF@herrwackelpudding.localhost>\nX-KMail-Transport: GMX\nX-KMail-Fcc: 28\nX-KMail-Drafts: 7\nX-KMail-Templates: " "9\nUser-Agent: KMail/4.6 beta5 (Linux/2.6.34.7-0.7-desktop; KDE/4.6.41; x86_64;\n git-0269848; 2011-04-19)\nMIME-Version: " "1.0\nContent-Type: text/plain; charset=\"us-ascii\"\n\nNot encrypted not signed :(\n\nsome random text\n")); } QTEST_APPLESS_MAIN(CryptoHelperTest) #include "moc_cryptohelpertest.cpp" diff --git a/autotests/core/fileopenertest.cpp b/autotests/core/fileopenertest.cpp index a5043cb..b6caf02 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(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("smime-opaque-enc+sign.mbox")); + const auto messages = FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("smime-opaque-enc+sign.mbox")); QCOMPARE(messages.count(), 1); } void openSingleCombinedTest() { - const auto messages = FileOpener::openFile(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("combined.mbox")); + const auto messages = FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("combined.mbox")); QCOMPARE(messages.count(), 3); } void openAscTest() { - const auto messages = FileOpener::openFile(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("msg.asc")); + const auto messages = FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("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(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("smime.p7m")); + const auto messages = FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("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(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("not-here.p7m")); + const auto messages = FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("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/core/gpgerrortest.cpp b/autotests/core/gpgerrortest.cpp index 09210e3..b2eb26d 100644 --- a/autotests/core/gpgerrortest.cpp +++ b/autotests/core/gpgerrortest.cpp @@ -1,200 +1,200 @@ // 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); + QFile file(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile); file.open(QIODevice::ReadOnly); Q_ASSERT(file.isOpen()); return file.readAll(); } void killAgent(const QString &dir) { QProcess proc; proc.setProgram(QStringLiteral("gpg-connect-agent")); QStringList arguments; arguments << QStringLiteral("-S ") << dir + QStringLiteral("/S.gpg-agent"); proc.start(); proc.waitForStarted(); proc.write("KILLAGENT\n"); proc.write("BYE\n"); proc.closeWriteChannel(); proc.waitForFinished(); } class GpgErrorTest : public QObject { Q_OBJECT private Q_SLOTS: void testGpgConfiguredCorrectly() { setEnv("GNUPGHOME", GNUPGHOME); MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(QStringLiteral("openpgp-inline-charset-encrypted.mbox"))); otp.print(); otp.decryptAndVerify(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0]; QVERIFY(bool(part)); QVERIFY(part->text().startsWith(QStringLiteral("asdasd"))); QCOMPARE(part->encryptions().size(), 1); auto enc = part->encryptions()[0]; QCOMPARE(enc->error(), MimeTreeParser::MessagePart::NoError); // QCOMPARE((int) enc->recipients().size(), 2); } void testNoGPGInstalled_data() { QTest::addColumn("mailFileName"); QTest::newRow("openpgp-inline-charset-encrypted") << "openpgp-inline-charset-encrypted.mbox"; QTest::newRow("openpgp-encrypted-attachment-and-non-encrypted-attachment") << "openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox"; QTest::newRow("smime-encrypted") << "smime-encrypted.mbox"; } void testNoGPGInstalled() { QFETCH(QString, mailFileName); setEnv("PATH", "/nonexististing"); setGpgMEfname("/nonexisting/gpg", ""); MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(mailFileName)); otp.print(); otp.decryptAndVerify(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->encryptions().size(), 1); QVERIFY(part->text().isEmpty()); auto enc = part->encryptions()[0]; QCOMPARE(enc->error(), MimeTreeParser::MessagePart::NoKeyError); } void testGpgIncorrectGPGHOME_data() { QTest::addColumn("mailFileName"); QTest::newRow("openpgp-inline-charset-encrypted") << "openpgp-inline-charset-encrypted.mbox"; QTest::newRow("openpgp-encrypted-attachment-and-non-encrypted-attachment") << "openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox"; QTest::newRow("smime-encrypted") << "smime-encrypted.mbox"; } void testGpgIncorrectGPGHOME() { QFETCH(QString, mailFileName); setEnv("GNUPGHOME", QByteArray(GNUPGHOME) + QByteArray("noexist")); MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(readMailFromFile(mailFileName)); otp.print(); otp.decryptAndVerify(); otp.print(); auto partList = otp.collectContentParts(); QCOMPARE(partList.size(), 1); auto part = partList[0].dynamicCast(); QVERIFY(bool(part)); QCOMPARE(part->encryptions().size(), 1); QCOMPARE(part->signatures().size(), 0); QVERIFY(part->text().isEmpty()); auto enc = part->encryptions()[0]; QVERIFY(enc->isNoSecKey()); // QCOMPARE((int) enc->recipients().size(), 2); } public Q_SLOTS: void init() { mResetGpgmeEngine = false; mModifiedEnv.clear(); { gpgme_check_version(nullptr); gpgme_ctx_t ctx = nullptr; gpgme_new(&ctx); gpgme_set_protocol(ctx, GPGME_PROTOCOL_OpenPGP); gpgme_engine_info_t info = gpgme_ctx_get_engine_info(ctx); mGpgmeEngine_fname = info->file_name; gpgme_release(ctx); } mEnv = QProcessEnvironment::systemEnvironment(); unsetEnv("GNUPGHOME"); } void cleanup() { QCoreApplication::sendPostedEvents(); const QString &gnupghome = QString::fromUtf8(qgetenv("GNUPGHOME")); if (!gnupghome.isEmpty()) { killAgent(gnupghome); } resetGpgMfname(); resetEnv(); } private: void unsetEnv(const QByteArray &name) { mModifiedEnv << name; qunsetenv(name.data()); } void setEnv(const QByteArray &name, const QByteArray &value) { mModifiedEnv << name; qputenv(name.data(), value); } void resetEnv() { for (const auto &i : std::as_const(mModifiedEnv)) { const auto env = i.data(); if (mEnv.contains(QString::fromUtf8(i))) { qputenv(env, mEnv.value(QString::fromUtf8(i)).toUtf8()); } else { qunsetenv(env); } } } void resetGpgMfname() { if (mResetGpgmeEngine) { gpgme_set_engine_info(GPGME_PROTOCOL_OpenPGP, mGpgmeEngine_fname.data(), nullptr); } } void setGpgMEfname(const QByteArray &fname, const QByteArray &homedir) { mResetGpgmeEngine = true; gpgme_set_engine_info(GPGME_PROTOCOL_OpenPGP, fname.data(), homedir.data()); } QSet mModifiedEnv; QProcessEnvironment mEnv; bool mResetGpgmeEngine; QByteArray mGpgmeEngine_fname; }; QTEST_GUILESS_MAIN(GpgErrorTest) #include "gpgerrortest.moc" diff --git a/autotests/core/mimetreeparsertest.cpp b/autotests/core/mimetreeparsertest.cpp index 0856cfa..d89481e 100644 --- a/autotests/core/mimetreeparsertest.cpp +++ b/autotests/core/mimetreeparsertest.cpp @@ -1,781 +1,781 @@ // 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); + QFile file(QLatin1StringView(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"))); + otp.parseObjectTree(readMailFromFile(QLatin1StringView("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"))); + otp.parseObjectTree(readMailFromFile(QLatin1StringView("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.parseObjectTree(readMailFromFile(QLatin1StringView("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.parseObjectTree(readMailFromFile(QLatin1StringView("smime-encrypted.mbox"))); otp.print(); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox"))); otp.print(); otp.decryptAndVerify(); 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(), QList() << "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(), QList() << "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.parseObjectTree(readMailFromFile(QLatin1StringView("openpgp-inline-charset-encrypted.mbox"))); otp.print(); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("openpgp-inline-encrypted+nonenc.mbox"))); otp.print(); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox"))); otp.print(); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("openpgp-encrypted-attachment-and-non-encrypted-attachment.mbox"))); otp.print(); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("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.parseObjectTree(readMailFromFile(QLatin1StringView("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.parseObjectTree(readMailFromFile(QLatin1StringView("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.parseObjectTree(readMailFromFile(QLatin1StringView("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:"))); + QVERIFY(!resolvedContent.contains(QLatin1StringView("cid:"))); } void testCidLinkInForwardedInline() { MimeTreeParser::ObjectTreeParser otp; - otp.parseObjectTree(readMailFromFile(QLatin1String("cid-links-forwarded-inline.mbox"))); + otp.parseObjectTree(readMailFromFile(QLatin1StringView("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:"))); + QVERIFY(!resolvedContent.contains(QLatin1StringView("cid:"))); } void testOpenPGPInlineError() { MimeTreeParser::ObjectTreeParser otp; - otp.parseObjectTree(readMailFromFile(QLatin1String("inlinepgpgencrypted-error.mbox"))); + otp.parseObjectTree(readMailFromFile(QLatin1StringView("inlinepgpgencrypted-error.mbox"))); otp.print(); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("encapsulated-with-attachment.mbox"))); otp.decryptAndVerify(); 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")); + QCOMPARE(part->from(), QLatin1StringView("Thomas McGuire ")); + QCOMPARE(part->date().toString(), QLatin1StringView("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"))); + otp.parseObjectTree(readMailFromFile(QLatin1StringView("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.parseObjectTree(readMailFromFile(QLatin1StringView("openpgp-inline-signed.mbox"))); otp.decryptAndVerify(); 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()->isTrusted(), true); QCOMPARE(signaturePart->partMetaData()->sigSummary & GpgME::Signature::KeyMissing, false); QCOMPARE(signaturePart->partMetaData()->sigSummary & GpgME::Signature::KeyExpired, false); QCOMPARE(signaturePart->partMetaData()->sigSummary & GpgME::Signature::KeyRevoked, false); QCOMPARE(signaturePart->partMetaData()->sigSummary & GpgME::Signature::SigExpired, false); QCOMPARE(signaturePart->partMetaData()->sigSummary & GpgME::Signature::CrlMissing, false); QCOMPARE(signaturePart->partMetaData()->sigSummary & GpgME::Signature::CrlTooOld, false); QCOMPARE(signaturePart->partMetaData()->keyId, QByteArray{"8D9860C58F246DE6"}); - QCOMPARE(signaturePart->partMetaData()->signer, QLatin1String{"unittest key (no password) "}); + QCOMPARE(signaturePart->partMetaData()->signer, QLatin1StringView{"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.parseObjectTree(readMailFromFile(QLatin1StringView("openpgp-encrypted+signed.mbox"))); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("openpgp-multipart-embedded.mbox"))); otp.print(); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("openpgp-multipart-embedded-signed.mbox"))); otp.decryptAndVerify(); 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(false, signaturePart->partMetaData()->isGoodSignature); QCOMPARE(GpgME::Signature::KeyMissing, signaturePart->partMetaData()->sigSummary); } void testAppleHtmlWithAttachments() { MimeTreeParser::ObjectTreeParser otp; - otp.parseObjectTree(readMailFromFile(QLatin1String("applehtmlwithattachments.mbox"))); + otp.parseObjectTree(readMailFromFile(QLatin1StringView("applehtmlwithattachments.mbox"))); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("applehtmlwithattachmentsmixed.mbox"))); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("invitation.mbox"))); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("gmail-invitation.mbox"))); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("openpgp-encrypted-memoryhole.mbox"))); otp.decryptAndVerify(); 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"))); + otp.parseObjectTree(readMailFromFile(QLatin1StringView("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.parseObjectTree(readMailFromFile(QLatin1StringView("openpgp-encrypted-memoryhole2.mbox"))); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("text+html-maillinglist.mbox"))); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("crlf-encrypted-with-signature.mbox"))); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("crlf-encrypted-with-signature-multipart.mbox"))); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("outlook.mbox"))); otp.decryptAndVerify(); 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"))); + QVERIFY(!otp.htmlContent().contains(QLatin1StringView("\r\n"))); } void testOpenPGPEncryptedSignedThunderbird() { MimeTreeParser::ObjectTreeParser otp; - otp.parseObjectTree(readMailFromFile(QLatin1String("openpgp-encrypted-signed-thunderbird.mbox"))); + otp.parseObjectTree(readMailFromFile(QLatin1StringView("openpgp-encrypted-signed-thunderbird.mbox"))); otp.print(); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("signed-forward-openpgp-signed-encrypted.mbox"))); otp.print(); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("smime-opaque-sign.mbox"))); otp.print(); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("smime-encrypted.mbox"))); otp.print(); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("smime-signed-apple.mbox"))); otp.print(); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("smime-encrypted-octet-stream.mbox"))); otp.print(); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("smime-opaque-signed-encrypted-attachment.mbox"))); otp.print(); QVERIFY(otp.hasEncryptedParts()); otp.decryptAndVerify(); 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.parseObjectTree(readMailFromFile(QLatin1StringView("smime-opaque-enc+sign.mbox"))); otp.print(); QVERIFY(otp.hasEncryptedParts()); QVERIFY(!otp.hasSignedParts()); otp.decryptAndVerify(); QVERIFY(otp.hasSignedParts()); 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/autotests/core/partmodeltest.cpp b/autotests/core/partmodeltest.cpp index a3f8b15..164ee70 100644 --- a/autotests/core/partmodeltest.cpp +++ b/autotests/core/partmodeltest.cpp @@ -1,77 +1,77 @@ // SPDX-FileCopyrightText: 2017 Christian Mollekopf // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include #include "messageparser.h" #include "partmodel.h" KMime::Message::Ptr readMailFromFile(const QString &mailFile) { - QFile file(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile); + QFile file(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile); file.open(QIODevice::ReadOnly); Q_ASSERT(file.isOpen()); auto mailData = KMime::CRLFtoLF(file.readAll()); KMime::Message::Ptr message(new KMime::Message); message->setContent(mailData); message->parse(); return message; } class PartModelTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase() { } void testTrim() { - auto result = PartModel::trim(QLatin1String("

This is some funky test.

\n

--
\nChristian Mollekopf
\nSenior Software")); + auto result = PartModel::trim(QLatin1StringView("

This is some funky test.

\n

--
\nChristian Mollekopf
\nSenior Software")); QCOMPARE(result.second, true); - QCOMPARE(result.first, QLatin1String("

This is some funky test.

\n")); + QCOMPARE(result.first, QLatin1StringView("

This is some funky test.

\n")); } void testTrimFromPlain() { // Qt::convertFromPlainText inserts non-breaking spaces - auto result = PartModel::trim(Qt::convertFromPlainText(QLatin1String("This is some funky text.\n\n-- \nChristian Mollekopf\nSenior Software"))); + auto result = PartModel::trim(Qt::convertFromPlainText(QLatin1StringView("This is some funky text.\n\n-- \nChristian Mollekopf\nSenior Software"))); QCOMPARE(result.second, true); //\u00A0 is a on-breaking space const auto expected = QStringLiteral("

This is some funky text.

\n").replace(QLatin1Char(' '), QChar(0x00a0)); QCOMPARE(result.first, expected); } void testModel() { MessageParser messageParser; - messageParser.setMessage(readMailFromFile(QLatin1String("html.mbox"))); + messageParser.setMessage(readMailFromFile(QLatin1StringView("html.mbox"))); QFont font{}; font.setFamily(QStringLiteral("Noto Sans")); qGuiApp->setFont(font); auto partModel = messageParser.parts(); new QAbstractItemModelTester(partModel); QCOMPARE(partModel->rowCount(), 1); QCOMPARE(partModel->data(partModel->index(0, 0), PartModel::TypeRole).value(), PartModel::Types::Plain); QCOMPARE(partModel->data(partModel->index(0, 0), PartModel::IsEmbeddedRole).toBool(), false); QCOMPARE(partModel->data(partModel->index(0, 0), PartModel::IsErrorRole).toBool(), false); QCOMPARE( partModel->data(partModel->index(0, 0), PartModel::ContentRole).toString(), QStringLiteral("\n\n\n

HTML text

")); } }; QTEST_MAIN(PartModelTest) #include "partmodeltest.moc" diff --git a/autotests/widgets/messageviewerdialogtest.cpp b/autotests/widgets/messageviewerdialogtest.cpp index a0a0036..1beccf4 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(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("combined.mbox")); + MessageViewerDialog dialog(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("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(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("plaintext.mbox")); + MessageViewerDialog dialog(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("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 16877f9..ca71302 100644 --- a/autotests/widgets/messageviewertest.cpp +++ b/autotests/widgets/messageviewertest.cpp @@ -1,50 +1,50 @@ // 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(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("smime-encrypted.mbox")); + auto messages = MimeTreeParser::Core::FileOpener::openFile(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + QLatin1String("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/attachmentmodel.cpp b/src/core/attachmentmodel.cpp index 9053266..74ccb57 100644 --- a/src/core/attachmentmodel.cpp +++ b/src/core/attachmentmodel.cpp @@ -1,412 +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 #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]); + const QLatin1StringView 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/bodypartformatter_impl.cpp b/src/core/bodypartformatter_impl.cpp index 256c954..e4f58e6 100644 --- a/src/core/bodypartformatter_impl.cpp +++ b/src/core/bodypartformatter_impl.cpp @@ -1,360 +1,361 @@ // 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 == QLatin1String("certs-only")) { + if (smimeType == QLatin1StringView("certs-only")) { return CertMessagePart::Ptr(new CertMessagePart(objectTreeParser, node, QGpgME::smime())); } - bool isSigned = (smimeType == QLatin1String("signed-data")); - bool isEncrypted = (smimeType == QLatin1String("enveloped-data")); + 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 == QLatin1String("application/pkcs7-signature") || protocolContentType == QLatin1String("application/x-pkcs7-signature")) { + if (protocolContentType == QLatin1StringView("application/pkcs7-signature") || protocolContentType == QLatin1String("application/x-pkcs7-signature")) { protocol = QGpgME::smime(); - } else if (protocolContentType == QLatin1String("application/pgp-signature") || protocolContentType == QLatin1String("application/x-pgp-signature")) { + } else if (protocolContentType == QLatin1StringView("application/pgp-signature") + || protocolContentType == QLatin1String("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(), QLatin1String(signature->contentType()->mimeType().toLower())); + 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/htmlutils.cpp b/src/core/htmlutils.cpp index 280df39..f1898d2 100644 --- a/src/core/htmlutils.cpp +++ b/src/core/htmlutils.cpp @@ -1,261 +1,261 @@ // SPDX-FileCopyrightText: 2017 Christian Mollekopf // SPDX-License-Identifier: LGPL-2.0-or-later #include "htmlutils.h" #include #include static QString resolveEntities(const QString &in) { QString out; for (int i = 0; i < (int)in.length(); ++i) { if (in[i] == QLatin1Char('&')) { // find a semicolon ++i; int n = in.indexOf(QLatin1Char(';'), i); if (n == -1) { break; } QString type = in.mid(i, (n - i)); i = n; // should be n+1, but we'll let the loop increment do it - if (type == QLatin1String("amp")) { + if (type == QLatin1StringView("amp")) { out += QLatin1Char('&'); - } else if (type == QLatin1String("lt")) + } else if (type == QLatin1StringView("lt")) out += QLatin1Char('<'); - else if (type == QLatin1String("gt")) + else if (type == QLatin1StringView("gt")) out += QLatin1Char('>'); - else if (type == QLatin1String("quot")) + else if (type == QLatin1StringView("quot")) out += QLatin1Char('\"'); - else if (type == QLatin1String("apos")) + else if (type == QLatin1StringView("apos")) out += QLatin1Char('\''); - else if (type == QLatin1String("nbsp")) + else if (type == QLatin1StringView("nbsp")) out += QChar(0xa0); } else { out += in[i]; } } return out; } static bool linkify_pmatch(const QString &str1, int at, const QString &str2) { if (str2.length() > (str1.length() - at)) return false; for (int n = 0; n < (int)str2.length(); ++n) { if (str1.at(n + at).toLower() != str2.at(n).toLower()) return false; } return true; } static bool linkify_isOneOf(const QChar &c, const QString &charlist) { for (int i = 0; i < (int)charlist.length(); ++i) { if (c == charlist.at(i)) return true; } return false; } // encodes a few dangerous html characters static QString linkify_htmlsafe(const QString &in) { QString out; for (int n = 0; n < in.length(); ++n) { if (linkify_isOneOf(in.at(n), QStringLiteral("\"\'`<>"))) { // hex encode QString hex; hex.asprintf("%%%02X", in.at(n).toLatin1()); out.append(hex); } else { out.append(in.at(n)); } } return out; } static bool linkify_okUrl(const QString &url) { if (url.at(url.length() - 1) == QLatin1Char('.')) return false; return true; } static bool linkify_okEmail(const QString &addy) { // this makes sure that there is an '@' and a '.' after it, and that there is // at least one char for each of the three sections int n = addy.indexOf(QLatin1Char('@')); if (n == -1 || n == 0) return false; int d = addy.indexOf(QLatin1Char('.'), n + 1); if (d == -1 || d == 0) return false; if ((addy.length() - 1) - d <= 0) return false; if (addy.indexOf(QStringLiteral("..")) != -1) return false; return true; } /** * takes a richtext string and heuristically adds links for uris of common protocols * @return a richtext string with link markup added */ QString MimeTreeParser::linkify(const QString &in) { QString out = in; int x1, x2; QString linked, link, href; for (int n = 0; n < (int)out.length(); ++n) { bool isUrl = false; bool isAtStyle = false; x1 = n; if (linkify_pmatch(out, n, QStringLiteral("xmpp:"))) { n += 5; isUrl = true; href = QString(); } else if (linkify_pmatch(out, n, QStringLiteral("mailto:"))) { n += 7; isUrl = true; href = QString(); } else if (linkify_pmatch(out, n, QStringLiteral("http://"))) { n += 7; isUrl = true; href = QString(); } else if (linkify_pmatch(out, n, QStringLiteral("https://"))) { n += 8; isUrl = true; href = QString(); } else if (linkify_pmatch(out, n, QStringLiteral("ftp://"))) { n += 6; isUrl = true; href = QString(); } else if (linkify_pmatch(out, n, QStringLiteral("news://"))) { n += 7; isUrl = true; href = QString(); } else if (linkify_pmatch(out, n, QStringLiteral("ed2k://"))) { n += 7; isUrl = true; href = QString(); } else if (linkify_pmatch(out, n, QStringLiteral("magnet:"))) { n += 7; isUrl = true; href = QString(); } else if (linkify_pmatch(out, n, QStringLiteral("www."))) { isUrl = true; href = QStringLiteral("http://"); } else if (linkify_pmatch(out, n, QStringLiteral("ftp."))) { isUrl = true; href = QStringLiteral("ftp://"); } else if (linkify_pmatch(out, n, QStringLiteral("@"))) { isAtStyle = true; href = QStringLiteral("x-psi-atstyle:"); } if (isUrl) { // make sure the previous char is not alphanumeric if (x1 > 0 && out.at(x1 - 1).isLetterOrNumber()) continue; // find whitespace (or end) QMap brackets; brackets[QLatin1Char('(')] = brackets[QLatin1Char(')')] = brackets[QLatin1Char('[')] = brackets[QLatin1Char(']')] = brackets[QLatin1Char('{')] = brackets[QLatin1Char('}')] = 0; QMap openingBracket; openingBracket[QLatin1Char(')')] = QLatin1Char('('); openingBracket[QLatin1Char(']')] = QLatin1Char('['); openingBracket[QLatin1Char('}')] = QLatin1Char('{'); for (x2 = n; x2 < (int)out.length(); ++x2) { if (out.at(x2).isSpace() || linkify_isOneOf(out.at(x2), QStringLiteral("\"\'`<>")) || linkify_pmatch(out, x2, QStringLiteral(""")) || linkify_pmatch(out, x2, QStringLiteral("'")) || linkify_pmatch(out, x2, QStringLiteral(">")) || linkify_pmatch(out, x2, QStringLiteral("<"))) { break; } if (brackets.contains(out.at(x2))) { ++brackets[out.at(x2)]; } } int len = x2 - x1; QString pre = resolveEntities(out.mid(x1, x2 - x1)); // go backward hacking off unwanted punctuation int cutoff; for (cutoff = pre.length() - 1; cutoff >= 0; --cutoff) { if (!linkify_isOneOf(pre.at(cutoff), QStringLiteral("!?,.()[]{}<>\""))) break; if (linkify_isOneOf(pre.at(cutoff), QStringLiteral(")]}")) && brackets[pre.at(cutoff)] - brackets[openingBracket[pre.at(cutoff)]] <= 0) { break; // in theory, there could be == above, but these are urls, not math ;) } if (brackets.contains(pre.at(cutoff))) { --brackets[pre.at(cutoff)]; } } ++cutoff; //++x2; link = pre.mid(0, cutoff); if (!linkify_okUrl(link)) { n = x1 + link.length(); continue; } href += link; // attributes need to be encoded too. href = href.toHtmlEscaped(); href = linkify_htmlsafe(href); // printf("link: [%s], href=[%s]\n", link.latin1(), href.latin1()); linked = QStringLiteral("").arg(href) + QUrl{link}.toDisplayString(QUrl::RemoveQuery) + QStringLiteral("") + pre.mid(cutoff).toHtmlEscaped(); out.replace(x1, len, linked); n = x1 + linked.length() - 1; } else if (isAtStyle) { // go backward till we find the beginning if (x1 == 0) continue; --x1; for (; x1 >= 0; --x1) { if (!linkify_isOneOf(out.at(x1), QStringLiteral("_.-+")) && !out.at(x1).isLetterOrNumber()) break; } ++x1; // go forward till we find the end x2 = n + 1; for (; x2 < (int)out.length(); ++x2) { if (!linkify_isOneOf(out.at(x2), QStringLiteral("_.-+")) && !out.at(x2).isLetterOrNumber()) break; } int len = x2 - x1; link = out.mid(x1, len); // link = resolveEntities(link); if (!linkify_okEmail(link)) { n = x1 + link.length(); continue; } href += link; // printf("link: [%s], href=[%s]\n", link.latin1(), href.latin1()); linked = QStringLiteral("").arg(href) + link + QStringLiteral(""); out.replace(x1, len, linked); n = x1 + linked.length() - 1; } } return out; } diff --git a/src/core/messagepart.cpp b/src/core/messagepart.cpp index 36944ba..d31dcfc 100644 --- a/src/core/messagepart.cpp +++ b/src/core/messagepart.cpp @@ -1,1097 +1,1097 @@ // 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 #include #include #include #include #include #include #include #include #include #include using namespace MimeTreeParser; //------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 QList &MessagePart::subParts() const { return mBlocks; } bool MessagePart::hasSubParts() const { return !mBlocks.isEmpty(); } QList MessagePart::signatures() const { QList 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; } QList MessagePart::encryptions() const { QList 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()) { auto aCodec = QStringDecoder(mOtp->codecNameFor(mNode).constData()); const auto cryptProto = QGpgME::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.decode(KMime::CRLFtoLF(block.text()))))); } else if (block.type() == PgpMessageBlock) { auto 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) { auto 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.decode(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(QStringDecoder(mOtp->codecNameFor(mNode).constData()).decode(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) { if (auto dataIcal = findTypeInDirectChildren(mNode, "text/calendar")) { mChildParts[MultipartIcal] = MimeMessagePart::Ptr(new MimeMessagePart(mOtp, dataIcal, true)); } 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, QGpgME::Protocol *cryptoProto) : MessagePart(otp, QString(), node) , mCryptoProto(cryptoProto) { if (!mNode) { qCWarning(MIMETREEPARSER_CORE_LOG) << "not a valid node"; return; } } CertMessagePart::~CertMessagePart() { } QString CertMessagePart::text() const { return QString(); } //-----SignedMessageBlock--------------------- SignedMessagePart::SignedMessagePart(ObjectTreeParser *otp, const QGpgME::Protocol *cryptoProto, KMime::Content *node, KMime::Content *signedData, bool parseAfterDecryption) : MessagePart(otp, {}, node) , mParseAfterDecryption(parseAfterDecryption) , mCryptoProto(cryptoProto) , mSignedData(signedData) { mMetaData.isSigned = true; mMetaData.isGoodSignature = false; mMetaData.status = i18ndc("mimetreeparser", "@info:status", "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); } const QGpgME::Protocol *SignedMessagePart::cryptoProto() const { return mCryptoProto; } void SignedMessagePart::startVerification() { if (!mSignedData) { return; } mMetaData.isSigned = false; mMetaData.status = i18ndc("mimetreeparser", "@info:status", "Wrong Crypto Plug-In."); mMetaData.isEncrypted = false; mMetaData.isDecryptable = false; auto codec = QStringDecoder(mOtp->codecNameFor(mSignedData).constData()); // 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()); const auto job = mCryptoProto->verifyDetachedJob(); setVerificationResult(job->exec(signature, signedData), signedData); job->deleteLater(); setText(codec.decode(KMime::CRLFtoLF(signedData))); } else { QByteArray outdata; const auto job = mCryptoProto->verifyOpaqueJob(); setVerificationResult(job->exec(mSignedData->decodedContent(), outdata), outdata); job->deleteLater(); setText(codec.decode(KMime::CRLFtoLF(outdata))); } if (!mMetaData.isSigned) { mMetaData.creationTime = QDateTime(); } } static int signatureToStatus(const GpgME::Signature &sig) { switch (sig.status().code()) { case GPG_ERR_NO_ERROR: return GPGME_SIG_STAT_GOOD; case GPG_ERR_BAD_SIGNATURE: return GPGME_SIG_STAT_BAD; case GPG_ERR_NO_PUBKEY: return GPGME_SIG_STAT_NOKEY; case GPG_ERR_NO_DATA: return GPGME_SIG_STAT_NOSIG; case GPG_ERR_SIG_EXPIRED: return GPGME_SIG_STAT_GOOD_EXP; case GPG_ERR_KEY_EXPIRED: return GPGME_SIG_STAT_GOOD_EXPKEY; default: return GPGME_SIG_STAT_ERROR; } } void SignedMessagePart::sigStatusToMetaData() { GpgME::Key key; if (partMetaData()->isSigned) { GpgME::Signature signature = mSignatures.front(); mMetaData.status_code = signatureToStatus(signature); mMetaData.isGoodSignature = partMetaData()->status_code == GPGME_SIG_STAT_GOOD; // save extended signature status flags mMetaData.sigSummary = signature.summary(); if (partMetaData()->isGoodSignature && !key.keyID()) { // Search for the key by its fingerprint so that we can check for // trust etc. key = Kleo::KeyCache::instance()->findByFingerprint(signature.fingerprint()); if (key.isNull() && signature.fingerprint()) { // try to find a subkey that was used for signing; // assumes that the key ID is the last 16 characters of the fingerprint const auto fpr = std::string_view{signature.fingerprint()}; const auto keyID = std::string{fpr, fpr.size() - 16, 16}; const auto subkeys = Kleo::KeyCache::instance()->findSubkeysByKeyID({keyID}); if (subkeys.size() > 0) { key = subkeys[0].parent(); } } if (key.isNull()) { qCDebug(MIMETREEPARSER_CORE_LOG) << "Found no key or subkey for fingerprint" << signature.fingerprint(); } } if (key.keyID()) { partMetaData()->keyId = key.keyID(); } if (partMetaData()->keyId.isEmpty()) { partMetaData()->keyId = signature.fingerprint(); } partMetaData()->keyTrust = signature.validity(); if (key.numUserIDs() > 0 && key.userID(0).id()) { partMetaData()->signer = prettifyDN(key.userID(0).id()); } for (const auto &uid : key.userIDs()) { // The following if /should/ always result in TRUE but we // won't trust implicitly the plugin that gave us these data. if (uid.email()) { QString email = QString::fromUtf8(uid.email()); if (!email.isEmpty()) { partMetaData()->signerMailAddresses.append(email); } } } if (signature.creationTime()) { partMetaData()->creationTime.setSecsSinceEpoch(signature.creationTime()); } else { partMetaData()->creationTime = QDateTime(); } if (partMetaData()->signer.isEmpty()) { if (key.numUserIDs() > 0 && key.userID(0).name()) { partMetaData()->signer = prettifyDN(key.userID(0).name()); } if (!partMetaData()->signerMailAddresses.empty()) { if (partMetaData()->signer.isEmpty()) { partMetaData()->signer = partMetaData()->signerMailAddresses.front(); } else { - partMetaData()->signer += QLatin1String(" <") + partMetaData()->signerMailAddresses.front() + QLatin1Char('>'); + partMetaData()->signer += QLatin1StringView(" <") + partMetaData()->signerMailAddresses.front() + QLatin1Char('>'); } } } if (Kleo::DeVSCompliance::isCompliant()) { partMetaData()->isCompliant = signature.isDeVs(); partMetaData()->compliance = Kleo::DeVSCompliance::name(signature.isDeVs()); } else { partMetaData()->isCompliant = true; } } } void SignedMessagePart::setVerificationResult(const GpgME::VerificationResult &result, const QByteArray &signedData) { mSignatures = result.signatures(); // FIXME // mMetaData.auditLogError = result.error; mMetaData.isSigned = !mSignatures.empty(); if (mMetaData.isSigned) { sigStatusToMetaData(); 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 QGpgME::Protocol *cryptoProto, KMime::Content *node, KMime::Content *encryptedNode, bool parseAfterDecryption) : MessagePart(otp, text, node) , mParseAfterDecryption(parseAfterDecryption) , mPassphraseError(false) , mNoSecKey(false) , mDecryptMessage(false) , mCryptoProto(cryptoProto) , mEncryptedNode(encryptedNode) { mMetaData.isSigned = false; mMetaData.isGoodSignature = false; mMetaData.isEncrypted = false; mMetaData.isDecryptable = false; mMetaData.status = i18ndc("mimetreeparser", "@info:status", "Wrong Crypto Plug-In."); } void EncryptedMessagePart::setIsEncrypted(bool encrypted) { mMetaData.isEncrypted = encrypted; } bool EncryptedMessagePart::isEncrypted() const { return mMetaData.isEncrypted; } const QGpgME::Protocol *EncryptedMessagePart::cryptoProto() const { return mCryptoProto; } void EncryptedMessagePart::setDecryptMessage(bool decrypt) { mDecryptMessage = decrypt; } bool EncryptedMessagePart::decryptMessage() const { return mDecryptMessage; } bool EncryptedMessagePart::isDecryptable() const { return mMetaData.isDecryptable; } bool EncryptedMessagePart::isNoSecKey() const { return mNoSecKey; } bool EncryptedMessagePart::passphraseError() const { return mPassphraseError; } 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; auto job = mCryptoProto->decryptVerifyJob(); const std::pair p = job->exec(ciphertext, plainText); job->deleteLater(); auto decryptResult = p.first; auto verifyResult = p.second; mMetaData.isSigned = verifyResult.signatures().size() > 0; // Normalize CRLF's plainText = KMime::CRLFtoLF(plainText); auto codec = QStringDecoder(mOtp->codecNameFor(&data).constData()); const auto decoded = codec.decode(plainText); partMetaData()->isSigned = verifyResult.signatures().size() > 0; if (partMetaData()->isSigned) { // 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, mCryptoProto, mNode, nullptr)); subPart->setText(decoded); subPart->setVerificationResult(verifyResult, plainText); appendSubPart(subPart); } mDecryptRecipients.clear(); bool cannotDecrypt = false; bool bDecryptionOk = !decryptResult.error(); for (const auto &recipient : decryptResult.recipients()) { if (!recipient.status()) { bDecryptionOk = true; } GpgME::Key key; key = Kleo::KeyCache::instance()->findByKeyIDOrFingerprint(recipient.keyID()); if (key.isNull()) { auto ret = Kleo::KeyCache::instance()->findSubkeysByKeyID({recipient.keyID()}); if (ret.size() == 1) { key = ret.front().parent(); } if (key.isNull()) { qCDebug(MIMETREEPARSER_CORE_LOG) << "Found no Key for KeyID " << recipient.keyID(); } } mDecryptRecipients.emplace_back(recipient, key); } if (!bDecryptionOk && partMetaData()->isSigned) { // Only a signed part partMetaData()->isEncrypted = false; bDecryptionOk = true; mDecryptedData = plainText; } else { mPassphraseError = decryptResult.error().isCanceled() || decryptResult.error().code() == GPG_ERR_BAD_PASSPHRASE; mMetaData.isEncrypted = bDecryptionOk || decryptResult.error().code() != GPG_ERR_NO_DATA; if (decryptResult.error().isCanceled()) { setDecryptMessage(false); } partMetaData()->errorText = QString::fromLocal8Bit(decryptResult.error().asString()); if (Kleo::DeVSCompliance::isCompliant()) { partMetaData()->isCompliant = decryptResult.isDeVs(); partMetaData()->compliance = Kleo::DeVSCompliance::name(decryptResult.isDeVs()); } else { partMetaData()->isCompliant = true; } if (partMetaData()->isEncrypted && decryptResult.numRecipients() > 0) { partMetaData()->keyId = decryptResult.recipient(0).keyID(); } if (bDecryptionOk) { mDecryptedData = plainText; } else { mNoSecKey = true; const auto decryRecipients = decryptResult.recipients(); for (const GpgME::DecryptionResult::Recipient &recipient : decryRecipients) { mNoSecKey &= (recipient.status().code() == GPG_ERR_NO_SECKEY); } if (!mPassphraseError && !mNoSecKey) { // GpgME do not detect passphrase error correctly mPassphraseError = true; } } } if (!bDecryptionOk) { QString cryptPlugLibName; mError = UnknownError; if (mCryptoProto) { cryptPlugLibName = mCryptoProto->name(); } if (mNoSecKey) { mError = NoKeyError; } if (mPassphraseError) { mError = PassphraseError; } if (!mCryptoProto) { partMetaData()->errorText = i18n("No appropriate crypto plug-in was found."); } else if (cannotDecrypt) { partMetaData()->errorText = i18n("Crypto plug-in \"%1\" cannot decrypt messages.", cryptPlugLibName); } else if (!passphraseError()) { - partMetaData()->errorText = i18n("Crypto plug-in \"%1\" could not decrypt the data.", cryptPlugLibName) + QLatin1String("
") + partMetaData()->errorText = i18n("Crypto plug-in \"%1\" could not decrypt the data.", cryptPlugLibName) + QLatin1StringView("
") + i18n("Error: %1", partMetaData()->errorText); } } return bDecryptionOk; } void EncryptedMessagePart::startDecryption(KMime::Content *data) { mMetaData.isEncrypted = true; mMetaData.isDecryptable = decrypt(*data); if (mParseAfterDecryption && !mMetaData.isSigned) { parseInternal(mDecryptedData); } else { setText(QString::fromUtf8(mDecryptedData.constData())); } } void EncryptedMessagePart::startDecryption() { if (mEncryptedNode) { startDecryption(mEncryptedNode); } else { startDecryption(mNode); } } std::vector> EncryptedMessagePart::decryptRecipients() const { return mDecryptRecipients; } 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(); } } 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) { } #include "moc_messagepart.cpp" diff --git a/src/core/objecttreeparser.cpp b/src/core/objecttreeparser.cpp index 6383e99..00e3b8c 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 + QLatin1String(" ")); + 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(QLatin1String("(src)\\s*=\\s*(\"|')(cid:[^\"']+)\\2")); + 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(QLatin1String("image/"))) { + 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(QLatin1String("p7m")) || node->contentType()->name().endsWith(QLatin1String("p7s")) - || node->contentType()->name().endsWith(QLatin1String("p7c")))) { + && (node->contentType()->name().endsWith(QLatin1StringView("p7m")) || node->contentType()->name().endsWith(QLatin1String("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"); } diff --git a/src/core/partmodel.cpp b/src/core/partmodel.cpp index 6cba411..0f690e8 100644 --- a/src/core/partmodel.cpp +++ b/src/core/partmodel.cpp @@ -1,596 +1,596 @@ // SPDX-FileCopyrightText: 2016 Sandro Knauß // SPDX-License-Identifier: LGPL-2.0-or-later #include "partmodel.h" #include "htmlutils.h" #include "objecttreeparser.h" #include "utils.h" #include #include #include #include #include #include // We return a pair containing the trimmed string, as well as a boolean indicating whether the string was trimmed or not std::pair PartModel::trim(const QString &text) { // The delimiters have

.? prefixed including the .? because sometimes we get a byte order mark (seen with user-agent: // Microsoft-MacOutlook/10.1d.0.190908) We match both regulard withspace with \s and non-breaking spaces with \u00A0 const QList delimiters{ // English QRegularExpression{QStringLiteral("

.?-+Original(\\s|\u00A0)Message-+"), QRegularExpression::CaseInsensitiveOption}, // The remainder is not quoted QRegularExpression{QStringLiteral("

.?On.*wrote:"), QRegularExpression::CaseInsensitiveOption}, // The remainder is quoted QRegularExpression{QStringLiteral("> On.*wrote:"), QRegularExpression::CaseInsensitiveOption}, // German // Forwarded QRegularExpression{QStringLiteral("

.?Von:.*

"), QRegularExpression::CaseInsensitiveOption}, // Reply QRegularExpression{QStringLiteral("

.?Am.*schrieb.*:

"), QRegularExpression::CaseInsensitiveOption}, // Signature QRegularExpression{QStringLiteral("

.?--(\\s|\u00A0)
"), QRegularExpression::CaseInsensitiveOption}, }; for (const auto &expression : delimiters) { auto i = expression.globalMatch(text); while (i.hasNext()) { const auto match = i.next(); const int startOffset = match.capturedStart(0); // This is a very simplistic detection for an inline reply where we would have the patterns before the actual message content. // We simply ignore anything we find within the first few lines. if (startOffset >= 5) { return {text.mid(0, startOffset), true}; } else { // Search for the next delimiter continue; } } } return {text, false}; } static QString addCss(const QString &s) { // Get the default font from QApplication static const QString fontFamily = QFont{}.family(); // overflow:hidden ensures no scrollbars are ever shown. static const QString css = QStringLiteral(""); - const QString header = QLatin1String( + const QString header = QLatin1StringView( "\n" "") - + css + QLatin1String("\n\n"); + + css + QLatin1StringView("\n\n"); return header + s + QStringLiteral(""); } class PartModelPrivate { public: PartModelPrivate(PartModel *q_ptr, const std::shared_ptr &parser) : q(q_ptr) , mParser(parser) { collectContents(); } ~PartModelPrivate() = default; void checkPart(const MimeTreeParser::MessagePart::Ptr part) { mMimeTypeCache[part.data()] = part->mimeType(); // Extract the content of the part and mContents.insert(part.data(), extractContent(part.data())); } // Recursively find encapsulated messages void findEncapsulated(const MimeTreeParser::EncapsulatedRfc822MessagePart::Ptr &e) { mEncapsulatedParts[e.data()] = mParser->collectContentParts(e); for (const auto &subPart : std::as_const(mEncapsulatedParts[e.data()])) { checkPart(subPart); mParents[subPart.data()] = e.data(); if (auto encapsulatedSub = subPart.dynamicCast()) { findEncapsulated(encapsulatedSub); } } } QVariant extractContent(MimeTreeParser::MessagePart *messagePart) { if (auto alternativePart = dynamic_cast(messagePart)) { if (alternativePart->availableModes().contains(MimeTreeParser::AlternativeMessagePart::MultipartIcal)) { return alternativePart->icalContent(); } } auto preprocessPlaintext = [&](const QString &text) { // Reduce consecutive new lines to never exceed 2 auto cleaned = text; cleaned.replace(QRegularExpression(QStringLiteral("[\n\r]{2,}")), QStringLiteral("\n\n")); // We always do rich text (so we get highlighted links and stuff). const auto html = Qt::convertFromPlainText(cleaned, Qt::WhiteSpaceNormal); if (trimMail) { const auto result = PartModel::trim(html); isTrimmed = result.second; Q_EMIT q->trimMailChanged(); return MimeTreeParser::linkify(result.first); } return MimeTreeParser::linkify(html); }; if (messagePart->isHtml()) { if (dynamic_cast(messagePart)) { containsHtmlAndPlain = true; Q_EMIT q->containsHtmlChanged(); if (!showHtml) { return preprocessPlaintext(messagePart->plaintextContent()); } } return addCss(mParser->resolveCidLinks(messagePart->htmlContent())); } if (auto attachmentPart = dynamic_cast(messagePart)) { auto node = attachmentPart->node(); if (node && mMimeTypeCache[attachmentPart] == "text/calendar") { return messagePart->text(); } } return preprocessPlaintext(messagePart->text()); } QVariant contentForPart(MimeTreeParser::MessagePart *messagePart) const { return mContents.value(messagePart); } void collectContents() { mEncapsulatedParts.clear(); mParents.clear(); mContents.clear(); containsHtmlAndPlain = false; isTrimmed = false; const auto parts = mParser->collectContentParts(); MimeTreeParser::MessagePart::List filteredParts; for (const auto &part : parts) { if (part->node()) { const auto contentType = part->node()->contentType(); if (contentType && contentType->hasParameter(QStringLiteral("protected-headers"))) { const auto contentDisposition = part->node()->contentDisposition(); if (contentDisposition && contentDisposition->disposition() == KMime::Headers::CDinline) { continue; } } } filteredParts << part; } for (const auto &part : std::as_const(filteredParts)) { checkPart(part); if (auto encapsulatedPart = part.dynamicCast()) { findEncapsulated(encapsulatedPart); } } for (const auto &part : std::as_const(filteredParts)) { if (mMimeTypeCache[part.data()] == "text/calendar") { mParts.prepend(part); } else { mParts.append(part); } } } PartModel *q; MimeTreeParser::MessagePart::List mParts; QHash mMimeTypeCache; QHash mEncapsulatedParts; QHash mParents; QMap mContents; std::shared_ptr mParser; bool showHtml{false}; bool containsHtmlAndPlain{false}; bool trimMail{false}; bool isTrimmed{false}; }; PartModel::PartModel(std::shared_ptr parser) : d(std::unique_ptr(new PartModelPrivate(this, parser))) { } PartModel::~PartModel() { } void PartModel::setShowHtml(bool html) { if (d->showHtml != html) { beginResetModel(); d->showHtml = html; d->collectContents(); endResetModel(); Q_EMIT showHtmlChanged(); } } bool PartModel::showHtml() const { return d->showHtml; } void PartModel::setTrimMail(bool trim) { if (d->trimMail != trim) { beginResetModel(); d->trimMail = trim; d->collectContents(); endResetModel(); Q_EMIT trimMailChanged(); } } bool PartModel::trimMail() const { return d->trimMail; } bool PartModel::isTrimmed() const { return d->isTrimmed; } bool PartModel::containsHtml() const { return d->containsHtmlAndPlain; } QHash PartModel::roleNames() const { QHash roles; roles[TypeRole] = "type"; roles[ContentRole] = "content"; roles[IsEmbeddedRole] = "embedded"; roles[IsEncryptedRole] = "encrypted"; roles[IsSignedRole] = "signed"; roles[SecurityLevelRole] = "securityLevel"; roles[EncryptionSecurityLevelRole] = "encryptionSecurityLevel"; roles[SignatureSecurityLevelRole] = "signatureSecurityLevel"; roles[ErrorType] = "errorType"; roles[ErrorString] = "errorString"; roles[IsErrorRole] = "error"; roles[SenderRole] = "sender"; roles[SignatureDetails] = "signatureDetails"; roles[EncryptionDetails] = "encryptionDetails"; roles[DateRole] = "date"; return roles; } QModelIndex PartModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column != 0) { return QModelIndex(); } if (parent.isValid()) { const auto part = static_cast(parent.internalPointer()); auto encapsulatedPart = dynamic_cast(part); if (encapsulatedPart) { const auto parts = d->mEncapsulatedParts[encapsulatedPart]; if (row < parts.size()) { return createIndex(row, column, parts.at(row).data()); } } return QModelIndex(); } if (row < d->mParts.size()) { return createIndex(row, column, d->mParts.at(row).data()); } return QModelIndex(); } SignatureInfo encryptionInfo(MimeTreeParser::MessagePart *messagePart) { SignatureInfo signatureInfo; const auto encryptions = messagePart->encryptions(); if (encryptions.size() > 1) { qWarning() << "Can't deal with more than one encryption"; } for (const auto &encryptionPart : encryptions) { signatureInfo.keyId = encryptionPart->partMetaData()->keyId; signatureInfo.cryptoProto = encryptionPart->cryptoProto(); signatureInfo.decryptRecipients = encryptionPart->decryptRecipients(); } return signatureInfo; }; SignatureInfo signatureInfo(MimeTreeParser::MessagePart *messagePart) { SignatureInfo signatureInfo; const auto signatures = messagePart->signatures(); if (signatures.size() > 1) { qWarning() << "Can't deal with more than one signature"; } for (const auto &signaturePart : signatures) { signatureInfo.keyId = signaturePart->partMetaData()->keyId; signatureInfo.cryptoProto = signaturePart->cryptoProto(); signatureInfo.keyMissing = signaturePart->partMetaData()->sigSummary & GpgME::Signature::KeyMissing; signatureInfo.keyExpired = signaturePart->partMetaData()->sigSummary & GpgME::Signature::KeyExpired; signatureInfo.keyRevoked = signaturePart->partMetaData()->sigSummary & GpgME::Signature::KeyRevoked; signatureInfo.sigExpired = signaturePart->partMetaData()->sigSummary & GpgME::Signature::SigExpired; signatureInfo.crlMissing = signaturePart->partMetaData()->sigSummary & GpgME::Signature::CrlMissing; signatureInfo.crlTooOld = signaturePart->partMetaData()->sigSummary & GpgME::Signature::CrlTooOld; signatureInfo.signer = signaturePart->partMetaData()->signer; signatureInfo.isCompliant = signaturePart->partMetaData()->isCompliant; signatureInfo.signerMailAddresses = signaturePart->partMetaData()->signerMailAddresses; signatureInfo.signatureIsGood = signaturePart->partMetaData()->isGoodSignature; signatureInfo.keyTrust = signaturePart->partMetaData()->keyTrust; } return signatureInfo; } QVariant PartModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } if (index.internalPointer()) { const auto messagePart = static_cast(index.internalPointer()); // qWarning() << "Found message part " << messagePart->metaObject()->className() << messagePart->partMetaData()->status << messagePart->error(); Q_ASSERT(messagePart); switch (role) { case Qt::DisplayRole: return QStringLiteral("Content%1"); case SenderRole: { if (auto e = dynamic_cast(messagePart)) { return e->from(); } return {}; } case DateRole: { if (auto e = dynamic_cast(messagePart)) { return e->date(); } return {}; } case TypeRole: { if (messagePart->error()) { return QVariant::fromValue(Types::Error); } if (dynamic_cast(messagePart)) { return QVariant::fromValue(Types::Encapsulated); } if (auto alternativePart = dynamic_cast(messagePart)) { if (alternativePart->availableModes().contains(MimeTreeParser::AlternativeMessagePart::MultipartIcal)) { return QVariant::fromValue(Types::Ical); } } if (auto attachmentPart = dynamic_cast(messagePart)) { auto node = attachmentPart->node(); if (!node) { qWarning() << "no content for attachment"; return {}; } if (d->mMimeTypeCache[attachmentPart] == "text/calendar") { return QVariant::fromValue(Types::Ical); } } if (!d->showHtml && d->containsHtmlAndPlain) { return QVariant::fromValue(Types::Plain); } // For simple html we don't need a browser auto complexHtml = [&] { if (messagePart->isHtml()) { const auto text = messagePart->htmlContent(); if (text.contains(QStringLiteral("error(); case ContentRole: return d->contentForPart(messagePart); case IsEncryptedRole: return messagePart->encryptionState() != MimeTreeParser::KMMsgNotEncrypted; case IsSignedRole: return messagePart->signatureState() != MimeTreeParser::KMMsgNotSigned; case SecurityLevelRole: { auto signature = messagePart->signatureState(); auto encryption = messagePart->encryptionState(); bool messageIsSigned = signature == MimeTreeParser::KMMsgPartiallySigned || signature == MimeTreeParser::KMMsgFullySigned; bool messageIsEncrypted = encryption == MimeTreeParser::KMMsgPartiallyEncrypted || encryption == MimeTreeParser::KMMsgFullyEncrypted; if (messageIsSigned) { const auto sigInfo = signatureInfo(messagePart); if (!sigInfo.signatureIsGood) { if (sigInfo.keyMissing || sigInfo.keyExpired) { return SecurityLevel::NotSoGood; } return SecurityLevel::Bad; } } // All good if ((messageIsSigned || messageIsEncrypted) && !messagePart->error()) { return SecurityLevel::Good; } // No info return SecurityLevel::Unknow; } case EncryptionSecurityLevelRole: { auto encryption = messagePart->encryptionState(); bool messageIsEncrypted = encryption == MimeTreeParser::KMMsgPartiallyEncrypted || encryption == MimeTreeParser::KMMsgFullyEncrypted; if (messagePart->error()) { return SecurityLevel::Bad; } // All good if (messageIsEncrypted) { return SecurityLevel::Good; } // No info return SecurityLevel::Unknow; } case SignatureSecurityLevelRole: { auto signature = messagePart->signatureState(); bool messageIsSigned = signature == MimeTreeParser::KMMsgPartiallySigned || signature == MimeTreeParser::KMMsgFullySigned; if (messageIsSigned) { const auto sigInfo = signatureInfo(messagePart); if (!sigInfo.signatureIsGood) { if (sigInfo.keyMissing || sigInfo.keyExpired) { return SecurityLevel::NotSoGood; } return SecurityLevel::Bad; } return SecurityLevel::Good; } // No info return SecurityLevel::Unknow; } case SignatureDetails: return QVariant::fromValue(signatureInfo(messagePart)); case EncryptionDetails: return QVariant::fromValue(encryptionInfo(messagePart)); case ErrorType: return messagePart->error(); case ErrorString: { switch (messagePart->error()) { case MimeTreeParser::MessagePart::NoKeyError: { if (auto encryptedMessagePart = dynamic_cast(messagePart)) { if (encryptedMessagePart->isNoSecKey()) { QString errorMessage; if (encryptedMessagePart->cryptoProto() == QGpgME::smime()) { errorMessage += i18ndc("mimetreeparser", "@info:status", "This message cannot be decrypted with any S/MIME certificate in your keyring."); } else { errorMessage += i18ndc("mimetreeparser", "@info:status", "This message cannot be decrypted with any OpenPGP certificate in your keyring."); } if (!encryptedMessagePart->decryptRecipients().empty()) { errorMessage += QLatin1Char(' ') + i18ndcp("mimetreeparser", "@info:status", "The message is encrypted for the following certificate:", "The message is encrypted for the following certificates:", encryptedMessagePart->decryptRecipients().size()); errorMessage += MimeTreeParser::decryptRecipientsToHtml(encryptedMessagePart->decryptRecipients(), encryptedMessagePart->cryptoProto()); } return errorMessage; } } } return messagePart->errorString(); case MimeTreeParser::MessagePart::PassphraseError: return i18ndc("mimetreeparser", "@info:status", "Wrong passphrase."); case MimeTreeParser::MessagePart::UnknownError: break; default: break; } return messagePart->errorString(); } } } return QVariant(); } QModelIndex PartModel::parent(const QModelIndex &index) const { if (index.isValid()) { if (auto indexPart = static_cast(index.internalPointer())) { for (const auto &part : std::as_const(d->mParts)) { if (part.data() == indexPart) { return QModelIndex(); } } const auto parentPart = d->mParents[indexPart]; Q_ASSERT(parentPart); int row = 0; const auto parts = d->mEncapsulatedParts[parentPart]; for (const auto &part : parts) { if (part.data() == indexPart) { break; } row++; } return createIndex(row, 0, parentPart); } return {}; } return {}; } int PartModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { const auto part = static_cast(parent.internalPointer()); auto encapsulatedPart = dynamic_cast(part); if (encapsulatedPart) { const auto parts = d->mEncapsulatedParts[encapsulatedPart]; return parts.size(); } return 0; } return d->mParts.count(); } int PartModel::columnCount(const QModelIndex &) const { return 1; } #include "moc_partmodel.cpp" diff --git a/src/widgets/messageviewer.cpp b/src/widgets/messageviewer.cpp index d611836..420e091 100644 --- a/src/widgets/messageviewer.cpp +++ b/src/widgets/messageviewer.cpp @@ -1,440 +1,440 @@ // 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 #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")) { + if (mimetype == QLatin1StringView("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 mainWidget = new QWidget(this); auto mainLayout = new QVBoxLayout(mainWidget); mainLayout->setContentsMargins({}); mainLayout->setSpacing(0); auto headersArea = new QWidget(mainWidget); headersArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); mainLayout->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); mainLayout->addWidget(d->scrollArea); mainLayout->setStretchFactor(d->scrollArea, 2); setStretchFactor(1, 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(); }); } 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::TextBrowserInteraction); label->setOpenExternalLinks(true); 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; } } } class HeaderLabel : public QLabel { public: HeaderLabel(const QString &content) : QLabel(content) { setWordWrap(true); setTextFormat(Qt::PlainText); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); } void resizeEvent(QResizeEvent *event) override { int height = heightForWidth(width()); setMaximumHeight(height); setMinimumHeight(height); QLabel::resizeEvent(event); } }; 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()) { const auto label = new QLabel(d->parser.subject()); label->setTextFormat(Qt::PlainText); d->formLayout->addRow(i18n("&Subject:"), label); } if (!d->parser.from().isEmpty()) { d->formLayout->addRow(i18n("&From:"), new HeaderLabel(d->parser.from())); } if (!d->parser.sender().isEmpty() && d->parser.from() != d->parser.sender()) { d->formLayout->addRow(i18n("&Sender:"), new HeaderLabel(d->parser.sender())); } if (!d->parser.to().isEmpty()) { d->formLayout->addRow(i18n("&To:"), new HeaderLabel(d->parser.to())); } if (!d->parser.cc().isEmpty()) { d->formLayout->addRow(i18n("&CC:"), new HeaderLabel(d->parser.cc())); } if (!d->parser.bcc().isEmpty()) { d->formLayout->addRow(i18n("&BCC:"), new HeaderLabel(d->parser.bcc())); } if (!d->parser.date().isNull()) { d->formLayout->addRow(i18n("&Date:"), new HeaderLabel(QLocale::system().toString(d->parser.date().toLocalTime()))); } 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/urlhandler.cpp b/src/widgets/urlhandler.cpp index da925e3..fc5ddd7 100644 --- a/src/widgets/urlhandler.cpp +++ b/src/widgets/urlhandler.cpp @@ -1,78 +1,78 @@ // SPDX-FileCopyrightText: 2023 g10 Code GmbH // SPDX-FileContributor: Carl Schwan // SPDX-License-Identifier: LGPL-2.0-or-later #include "urlhandler_p.h" #include "mimetreeparser_widgets_debug.h" #include #include #include #include #include #include UrlHandler::UrlHandler(QObject *parent) : QObject(parent) { } bool UrlHandler::handleClick(const QUrl &url, QWindow *window) { if (!url.hasFragment()) { return false; } QString displayName; QString libName; QString keyId; if (!foundSMIMEData(url.path() + QLatin1Char('#') + QUrl::fromPercentEncoding(url.fragment().toLatin1()), displayName, libName, keyId)) { return false; } QStringList lst; lst << QStringLiteral("--parent-windowid") << QString::number(static_cast(window->winId())) << QStringLiteral("--query") << keyId; #ifdef Q_OS_WIN QString exec = QStandardPaths::findExecutable(QStringLiteral("kleopatra.exe"), {QCoreApplication::applicationDirPath()}); if (exec.isEmpty()) { exec = QStandardPaths::findExecutable(QStringLiteral("kleopatra.exe")); } #else const QString exec = QStandardPaths::findExecutable(QStringLiteral("kleopatra")); #endif if (exec.isEmpty()) { qCWarning(MIMETREEPARSER_WIDGET_LOG) << "Could not find kleopatra executable in PATH"; KMessageBox::errorWId(window->winId(), i18n("Could not start certificate manager. " "Please check your installation."), i18n("KMail Error")); return false; } QProcess::startDetached(exec, lst); return true; } bool UrlHandler::foundSMIMEData(const QString &aUrl, QString &displayName, QString &libName, QString &keyId) { static QString showCertMan(QStringLiteral("showCertificate#")); displayName.clear(); libName.clear(); keyId.clear(); int i1 = aUrl.indexOf(showCertMan); if (-1 < i1) { i1 += showCertMan.length(); - int i2 = aUrl.indexOf(QLatin1String(" ### "), i1); + int i2 = aUrl.indexOf(QLatin1StringView(" ### "), i1); if (i1 < i2) { displayName = aUrl.mid(i1, i2 - i1); i1 = i2 + 5; - i2 = aUrl.indexOf(QLatin1String(" ### "), i1); + i2 = aUrl.indexOf(QLatin1StringView(" ### "), i1); if (i1 < i2) { libName = aUrl.mid(i1, i2 - i1); i2 += 5; keyId = aUrl.mid(i2); } } } return !keyId.isEmpty(); }