diff --git a/autotests/core/cryptohelpertest.cpp b/autotests/core/cryptohelpertest.cpp index 982df83..ed807d7 100644 --- a/autotests/core/cryptohelpertest.cpp +++ b/autotests/core/cryptohelpertest.cpp @@ -1,182 +1,186 @@ // 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(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 = QLatin1StringView("-----BEGIN PGP ") + name + QLatin1Char('\n') + blockText; QList blocks = prepareMessageForDecryption(preString.toLatin1() + text.toLatin1()); QCOMPARE(blocks.count(), 1); QCOMPARE(blocks[0].type(), UnknownBlock); 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 = 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(QLatin1StringView("openpgp-encrypted+signed.mbox"))); message->parse(); bool wasEncrypted = false; - auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted); + GpgME::Protocol protocol; + auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted, protocol); 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")); + QCOMPARE(protocol, GpgME::OpenPGP); } void CryptoHelperTest::testDecryptInlineMessage() { auto message = KMime::Message::Ptr(new KMime::Message); message->setContent(readMailFromFile(QLatin1StringView("openpgp-inline-encrypted+nonenc.mbox"))); message->parse(); qWarning() << message->decodedContent(); bool wasEncrypted = false; - auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted); + GpgME::Protocol protocol; + auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted, protocol); 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")); + QCOMPARE(protocol, GpgME::OpenPGP); } QTEST_APPLESS_MAIN(CryptoHelperTest) #include "moc_cryptohelpertest.cpp" diff --git a/src/core/cryptohelper.cpp b/src/core/cryptohelper.cpp index c728791..9f2088f 100644 --- a/src/core/cryptohelper.cpp +++ b/src/core/cryptohelper.cpp @@ -1,317 +1,317 @@ // SPDX-FileCopyrightText: 2001,2002 the KPGP authors // SPDX-FileCopyrightText: 2015 Sandro Knauß // SPDX-FileCopyrightText: 2017 Daniel Vrátil // SPDX-License-Identifier: GPL-2.0-or-later #include "cryptohelper.h" #include "mimetreeparser_core_debug.h" #include #include #include #include #include #include using namespace MimeTreeParser; PGPBlockType Block::determineType() const { const QByteArray data = text(); if (data.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")) { return NoPgpBlock; } else if (data.startsWith("-----BEGIN PGP SIGNED")) { return ClearsignedBlock; } else if (data.startsWith("-----BEGIN PGP SIGNATURE")) { return SignatureBlock; } else if (data.startsWith("-----BEGIN PGP PUBLIC")) { return PublicKeyBlock; } else if (data.startsWith("-----BEGIN PGP PRIVATE") || data.startsWith("-----BEGIN PGP SECRET")) { return PrivateKeyBlock; } else if (data.startsWith("-----BEGIN PGP MESSAGE")) { if (data.startsWith("-----BEGIN PGP MESSAGE PART")) { return MultiPgpMessageBlock; } else { return PgpMessageBlock; } } else if (data.startsWith("-----BEGIN PGP ARMORED FILE")) { return PgpMessageBlock; } else if (data.startsWith("-----BEGIN PGP ")) { return UnknownBlock; } else { return NoPgpBlock; } } QList MimeTreeParser::prepareMessageForDecryption(const QByteArray &msg) { PGPBlockType pgpBlock = NoPgpBlock; QList blocks; int start = -1; // start of the current PGP block int lastEnd = -1; // end of the last PGP block const int length = msg.length(); if (msg.isEmpty()) { return blocks; } if (msg.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")) { return blocks; } if (msg.startsWith("-----BEGIN PGP ")) { start = 0; } else { start = msg.indexOf("\n-----BEGIN PGP ") + 1; if (start == 0) { blocks.append(Block(msg, NoPgpBlock)); return blocks; } } while (start != -1) { int nextEnd; int nextStart; // is the PGP block a clearsigned block? if (!strncmp(msg.constData() + start + 15, "SIGNED", 6)) { pgpBlock = ClearsignedBlock; } else { pgpBlock = UnknownBlock; } nextEnd = msg.indexOf("\n-----END PGP ", start + 15); nextStart = msg.indexOf("\n-----BEGIN PGP ", start + 15); if (nextEnd == -1) { // Missing END PGP line if (lastEnd != -1) { blocks.append(Block(msg.mid(lastEnd + 1), UnknownBlock)); } else { blocks.append(Block(msg.mid(start), UnknownBlock)); } break; } if ((nextStart == -1) || (nextEnd < nextStart) || (pgpBlock == ClearsignedBlock)) { // most likely we found a PGP block (but we don't check if it's valid) // store the preceding non-PGP block if (start - lastEnd - 1 > 0) { blocks.append(Block(msg.mid(lastEnd + 1, start - lastEnd - 1), NoPgpBlock)); } lastEnd = msg.indexOf("\n", nextEnd + 14); if (lastEnd == -1) { if (start < length) { blocks.append(Block(msg.mid(start))); } break; } else { blocks.append(Block(msg.mid(start, lastEnd + 1 - start))); if ((nextStart != -1) && (nextEnd > nextStart)) { nextStart = msg.indexOf("\n-----BEGIN PGP ", lastEnd + 1); } } } start = nextStart; if (start == -1) { if (lastEnd + 1 < length) { // rest of mail is no PGP Block blocks.append(Block(msg.mid(lastEnd + 1), NoPgpBlock)); } break; } else { start++; // move start behind the '\n' } } return blocks; } Block::Block() = default; Block::Block(const QByteArray &m) : msg(m) { mType = determineType(); } Block::Block(const QByteArray &m, PGPBlockType t) : msg(m) , mType(t) { } QByteArray MimeTreeParser::Block::text() const { return msg; } PGPBlockType Block::type() const { return mType; } namespace { bool isPGP(const KMime::Content *part, bool allowOctetStream = false) { const auto ct = static_cast(part->headerByType("Content-Type")); return ct && (ct->isSubtype("pgp-encrypted") || ct->isSubtype("encrypted") || (allowOctetStream && ct->isMimeType("application/octet-stream"))); } bool isSMIME(const KMime::Content *part) { const auto ct = static_cast(part->headerByType("Content-Type")); return ct && (ct->isSubtype("pkcs7-mime") || ct->isSubtype("x-pkcs7-mime")); } void copyHeader(const KMime::Headers::Base *header, KMime::Message::Ptr msg) { auto newHdr = KMime::Headers::createHeader(header->type()); if (!newHdr) { newHdr = new KMime::Headers::Generic(header->type()); } newHdr->from7BitString(header->as7BitString(false)); msg->appendHeader(newHdr); } bool isContentHeader(const KMime::Headers::Base *header) { return header->is("Content-Type") || header->is("Content-Transfer-Encoding") || header->is("Content-Disposition"); } KMime::Message::Ptr assembleMessage(const KMime::Message::Ptr &orig, const KMime::Content *newContent) { auto out = KMime::Message::Ptr::create(); // Use the new content as message content out->setBody(const_cast(newContent)->encodedBody()); out->parse(); // remove default explicit content headers added by KMime::Content::parse() QList headers = out->headers(); for (const auto hdr : std::as_const(headers)) { if (isContentHeader(hdr)) { out->removeHeader(hdr->type()); } } // Copy over headers from the original message, except for CT, CTE and CD // headers, we want to preserve those from the new content headers = orig->headers(); for (const auto hdr : std::as_const(headers)) { if (isContentHeader(hdr)) { continue; } copyHeader(hdr, out); } // Overwrite some headers by those provided by the new content headers = newContent->headers(); for (const auto hdr : std::as_const(headers)) { if (isContentHeader(hdr)) { copyHeader(hdr, out); } } out->assemble(); out->parse(); return out; } } -KMime::Message::Ptr CryptoUtils::decryptMessage(const KMime::Message::Ptr &msg, bool &wasEncrypted) +KMime::Message::Ptr CryptoUtils::decryptMessage(const KMime::Message::Ptr &msg, bool &wasEncrypted, GpgME::Protocol &protoName) { - GpgME::Protocol protoName = GpgME::UnknownProtocol; + protoName = GpgME::UnknownProtocol; bool multipart = false; if (msg->contentType(false) && msg->contentType(false)->isMimeType("multipart/encrypted")) { multipart = true; const auto subparts = msg->contents(); for (auto subpart : subparts) { if (isPGP(subpart, true)) { protoName = GpgME::OpenPGP; break; } else if (isSMIME(subpart)) { protoName = GpgME::CMS; break; } } } else { if (isPGP(msg.data())) { protoName = GpgME::OpenPGP; } else if (isSMIME(msg.data())) { protoName = GpgME::CMS; } else { const auto blocks = prepareMessageForDecryption(msg->body()); QByteArray content; for (const auto &block : blocks) { if (block.type() == PgpMessageBlock) { const auto proto = QGpgME::openpgp(); wasEncrypted = true; QByteArray outData; auto inData = block.text(); auto decrypt = proto->decryptJob(); auto ctx = QGpgME::Job::context(decrypt); ctx->setDecryptionFlags(GpgME::Context::DecryptUnwrap); auto result = decrypt->exec(inData, outData); if (result.error()) { // unknown key, invalid algo, or general error qCWarning(MIMETREEPARSER_CORE_LOG) << "Failed to decrypt:" << result.error().asString(); return {}; } inData = outData; auto verify = proto->verifyOpaqueJob(true); auto resultVerify = verify->exec(inData, outData); if (resultVerify.error()) { qCWarning(MIMETREEPARSER_CORE_LOG) << "Failed to verify:" << resultVerify.error().asString(); return {}; } content += KMime::CRLFtoLF(outData); } else if (block.type() == NoPgpBlock) { content += block.text(); } } KMime::Content decCt; decCt.setBody(content); decCt.parse(); decCt.assemble(); return assembleMessage(msg, &decCt); } } if (protoName == GpgME::UnknownProtocol) { // Not encrypted, or we don't recognize the encryption wasEncrypted = false; return {}; } const auto proto = (protoName == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); wasEncrypted = true; QByteArray outData; auto inData = multipart ? msg->encodedContent() : msg->decodedContent(); // decodedContent in fact returns decoded body auto decrypt = proto->decryptJob(); auto result = decrypt->exec(inData, outData); if (result.error()) { // unknown key, invalid algo, or general error qCWarning(MIMETREEPARSER_CORE_LOG) << "Failed to decrypt:" << result.error().asString(); return {}; } KMime::Content decCt; decCt.setContent(KMime::CRLFtoLF(outData)); decCt.parse(); decCt.assemble(); return assembleMessage(msg, &decCt); } diff --git a/src/core/cryptohelper.h b/src/core/cryptohelper.h index 2dfceef..99b9369 100644 --- a/src/core/cryptohelper.h +++ b/src/core/cryptohelper.h @@ -1,55 +1,56 @@ // SPDX-FileCopyrightText: 2001,2002 the KPGP authors // SPDX-FileCopyrightText: 2015 Sandro Knauß // SPDX-FileCopyrightText: 2017 Daniel Vrátil // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "mimetreeparser_core_export.h" #include #include #include +#include namespace MimeTreeParser { enum PGPBlockType { UnknownBlock = -1, // BEGIN PGP ??? NoPgpBlock = 0, PgpMessageBlock = 1, // BEGIN PGP MESSAGE MultiPgpMessageBlock = 2, // BEGIN PGP MESSAGE, PART X[/Y] SignatureBlock = 3, // BEGIN PGP SIGNATURE ClearsignedBlock = 4, // BEGIN PGP SIGNED MESSAGE PublicKeyBlock = 5, // BEGIN PGP PUBLIC KEY BLOCK PrivateKeyBlock = 6, // BEGIN PGP PRIVATE KEY BLOCK (PGP 2.x: ...SECRET...) }; class MIMETREEPARSER_CORE_EXPORT Block { public: Block(); Block(const QByteArray &m); Block(const QByteArray &m, PGPBlockType t); [[nodiscard]] QByteArray text() const; [[nodiscard]] PGPBlockType type() const; [[nodiscard]] PGPBlockType determineType() const; QByteArray msg; PGPBlockType mType = UnknownBlock; }; /** Parses the given message and splits it into OpenPGP blocks and Non-OpenPGP blocks. */ [[nodiscard]] MIMETREEPARSER_CORE_EXPORT QList prepareMessageForDecryption(const QByteArray &msg); namespace CryptoUtils { -[[nodiscard]] MIMETREEPARSER_CORE_EXPORT KMime::Message::Ptr decryptMessage(const KMime::Message::Ptr &decrypt, bool &wasEncrypted); +[[nodiscard]] MIMETREEPARSER_CORE_EXPORT KMime::Message::Ptr decryptMessage(const KMime::Message::Ptr &decrypt, bool &wasEncrypted, GpgME::Protocol &protoName); } } // namespace MimeTreeParser Q_DECLARE_TYPEINFO(MimeTreeParser::Block, Q_MOVABLE_TYPE); diff --git a/src/widgets/messageviewerdialog.cpp b/src/widgets/messageviewerdialog.cpp index 54e73fd..165511e 100644 --- a/src/widgets/messageviewerdialog.cpp +++ b/src/widgets/messageviewerdialog.cpp @@ -1,321 +1,332 @@ // SPDX-FileCopyrightText: 2023 g10 Code GmbH // SPDX-FileContributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "messageviewerdialog.h" #include "messageviewer.h" #include "mimetreeparser_widgets_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MimeTreeParser::Widgets; namespace { -/// On windows, force the filename to end with .eml -/// On Linux, do nothing as this is handled by the file picker -inline QString changeExtension(const QString &fileName) +inline QString changeExtension(const QString &fileName, const QString &extension) { -#ifdef Q_OS_WIN auto renamedFileName = fileName; - renamedFileName.replace(QRegularExpression(QStringLiteral("\\.(mbox|p7m|asc)$")), QStringLiteral(".eml")); + renamedFileName.replace(QRegularExpression(QStringLiteral("\\.(mbox|p7m|asc)$")), extension); // In case the file name didn't contain any of the expected extension: mbox, p7m, asc and eml // or doesn't contains an extension at all. - if (!renamedFileName.endsWith(QStringLiteral(".eml"))) { - renamedFileName += QStringLiteral(".eml"); + if (!renamedFileName.endsWith(extension)) { + renamedFileName += extension; } return renamedFileName; -#else - // Handled automatically by the file picker on linux - return fileName; -#endif } - } class MessageViewerDialog::Private { public: Private(MessageViewerDialog *dialog) : q(dialog) { } MessageViewerDialog *const q; int currentIndex = 0; QList messages; QString fileName; MimeTreeParser::Widgets::MessageViewer *messageViewer = nullptr; QAction *nextAction = nullptr; QAction *previousAction = nullptr; QToolBar *toolBar = nullptr; void setCurrentIndex(int currentIndex); QMenuBar *createMenuBar(QWidget *parent); private: void save(QWidget *parent); void saveDecrypted(QWidget *parent); void print(QWidget *parent); void printPreview(QWidget *parent); void printInternal(QPrinter *printer); }; void MessageViewerDialog::Private::setCurrentIndex(int index) { Q_ASSERT(index >= 0); Q_ASSERT(index < messages.count()); currentIndex = index; messageViewer->setMessage(messages[currentIndex]); q->setWindowTitle(messageViewer->subject()); previousAction->setEnabled(currentIndex != 0); nextAction->setEnabled(currentIndex != messages.count() - 1); } QMenuBar *MessageViewerDialog::Private::createMenuBar(QWidget *parent) { const auto menuBar = new QMenuBar(parent); // File menu const auto fileMenu = menuBar->addMenu(i18nc("@action:inmenu", "&File")); const auto saveAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18nc("@action:inmenu", "&Save")); QObject::connect(saveAction, &QAction::triggered, parent, [parent, this] { save(parent); }); fileMenu->addAction(saveAction); const auto saveDecryptedAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18nc("@action:inmenu", "Save Decrypted")); QObject::connect(saveDecryptedAction, &QAction::triggered, parent, [parent, this] { saveDecrypted(parent); }); fileMenu->addAction(saveDecryptedAction); const auto printPreviewAction = new QAction(QIcon::fromTheme(QStringLiteral("document-print-preview")), i18nc("@action:inmenu", "Print Preview")); QObject::connect(printPreviewAction, &QAction::triggered, parent, [parent, this] { printPreview(parent); }); fileMenu->addAction(printPreviewAction); const auto printAction = new QAction(QIcon::fromTheme(QStringLiteral("document-print")), i18nc("@action:inmenu", "&Print")); QObject::connect(printAction, &QAction::triggered, parent, [parent, this] { print(parent); }); fileMenu->addAction(printAction); // Navigation menu const auto navigationMenu = menuBar->addMenu(i18nc("@action:inmenu", "&Navigation")); previousAction = new QAction(QIcon::fromTheme(QStringLiteral("go-previous")), i18nc("@action:button Previous email", "Previous Message"), parent); previousAction->setEnabled(false); navigationMenu->addAction(previousAction); nextAction = new QAction(QIcon::fromTheme(QStringLiteral("go-next")), i18nc("@action:button Next email", "Next Message"), parent); nextAction->setEnabled(false); navigationMenu->addAction(nextAction); return menuBar; } void MessageViewerDialog::Private::save(QWidget *parent) { - const QString location = QFileDialog::getSaveFileName(parent, - i18nc("@title:window", "Save File"), - changeExtension(fileName), - i18nc("File dialog accepted files", "Email files (*.eml *.mbox)")); + QString extension; + QString alternatives; + auto message = messages[currentIndex]; + bool wasEncrypted = false; + GpgME::Protocol protocol; + auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted, protocol); + Q_UNUSED(decryptedMessage); // we save the message without modifying it + + if (wasEncrypted) { + extension = QStringLiteral(".mime"); + if (protocol == GpgME::OpenPGP) { + alternatives = i18nc("File dialog accepted files", "Email files (*.eml *.mbox *.mime)"); + } else { + alternatives = i18nc("File dialog accepted files", "Encrypted S/MIME files (*.p7m)"); + } + } else { + extension = QStringLiteral(".eml"); + alternatives = i18nc("File dialog accepted files", "Email files (*.eml *.mbox *.mime)"); + } + + const QString location = + QFileDialog::getSaveFileName(parent, i18nc("@title:window", "Save File"), changeExtension(fileName, QStringLiteral(".mime")), alternatives); QSaveFile file(location); if (!file.open(QIODevice::WriteOnly)) { KMessageBox::error(parent, i18n("File %1 could not be created.", location), i18n("Error saving message")); return; } file.write(messages[currentIndex]->encodedContent()); file.commit(); } void MessageViewerDialog::Private::saveDecrypted(QWidget *parent) { const QString location = QFileDialog::getSaveFileName(parent, i18nc("@title:window", "Save Decrypted File"), - changeExtension(fileName), - i18nc("File dialog accepted files", "Email files (*.eml *.mbox)")); + changeExtension(fileName, QStringLiteral(".eml")), + i18nc("File dialog accepted files", "Email files (*.eml *.mbox *mime)")); QSaveFile file(location); if (!file.open(QIODevice::WriteOnly)) { KMessageBox::error(parent, i18nc("Error message", "File %1 could not be created.", location), i18n("Error saving message")); return; } auto message = messages[currentIndex]; bool wasEncrypted = false; - auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted); + GpgME::Protocol protocol; + auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted, protocol); if (!wasEncrypted) { decryptedMessage = message; } file.write(decryptedMessage->encodedContent()); file.commit(); } void MessageViewerDialog::Private::print(QWidget *parent) { QPrinter printer; QPrintDialog dialog(&printer, parent); dialog.setWindowTitle(i18nc("@title:window", "Print Document")); if (dialog.exec() != QDialog::Accepted) return; printInternal(&printer); } void MessageViewerDialog::Private::printPreview(QWidget *parent) { auto dialog = new QPrintPreviewDialog(parent); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->resize(800, 750); dialog->setWindowTitle(i18nc("@title:window", "Print Document")); QObject::connect(dialog, &QPrintPreviewDialog::paintRequested, parent, [this](QPrinter *printer) { printInternal(printer); }); dialog->open(); } void MessageViewerDialog::Private::printInternal(QPrinter *printer) { QPainter painter; painter.begin(printer); const auto pageLayout = printer->pageLayout(); const auto pageRect = pageLayout.paintRectPixels(printer->resolution()); const double xscale = pageRect.width() / double(messageViewer->width()); const double yscale = pageRect.height() / double(messageViewer->height()); const double scale = qMin(qMin(xscale, yscale), 1.); painter.translate(pageRect.x(), pageRect.y()); painter.scale(scale, scale); messageViewer->print(&painter, pageRect.width()); } MessageViewerDialog::MessageViewerDialog(const QList &messages, QWidget *parent) : QDialog(parent) , d(std::make_unique(this)) { d->messages += messages; initGUI(); } MessageViewerDialog::MessageViewerDialog(const QString &fileName, QWidget *parent) : QDialog(parent) , d(std::make_unique(this)) { d->fileName = fileName; d->messages += MimeTreeParser::Core::FileOpener::openFile(fileName); initGUI(); } void MessageViewerDialog::initGUI() { const auto mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins({}); mainLayout->setSpacing(0); const auto layout = new QVBoxLayout; const auto menuBar = d->createMenuBar(this); mainLayout->setMenuBar(menuBar); if (d->messages.isEmpty()) { auto errorMessage = new KMessageWidget(this); errorMessage->setMessageType(KMessageWidget::Error); errorMessage->setText(i18nc("@info", "Unable to read file")); layout->addWidget(errorMessage); return; } const bool multipleMessages = d->messages.length() > 1; d->toolBar = new QToolBar(this); if (multipleMessages) { #ifdef Q_OS_UNIX d->toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle); #else // on other platforms the default is IconOnly which is bad for // accessibility and can't be changed by the user. toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); #endif d->toolBar->addAction(d->previousAction); connect(d->previousAction, &QAction::triggered, this, [this] { d->setCurrentIndex(d->currentIndex - 1); }); const auto spacer = new QWidget(this); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); d->toolBar->addWidget(spacer); d->toolBar->addAction(d->nextAction); connect(d->nextAction, &QAction::triggered, this, [this] { d->setCurrentIndex(d->currentIndex + 1); }); d->nextAction->setEnabled(true); mainLayout->addWidget(d->toolBar); } else { mainLayout->addWidget(d->toolBar); d->toolBar->hide(); } mainLayout->addLayout(layout); d->messageViewer = new MimeTreeParser::Widgets::MessageViewer(this); d->messageViewer->setMessage(d->messages[0]); setWindowTitle(d->messageViewer->subject()); layout->addWidget(d->messageViewer); auto buttonBox = new QDialogButtonBox(this); buttonBox->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin, nullptr, this), style()->pixelMetric(QStyle::PM_LayoutTopMargin, nullptr, this), style()->pixelMetric(QStyle::PM_LayoutRightMargin, nullptr, this), style()->pixelMetric(QStyle::PM_LayoutBottomMargin, nullptr, this)); auto closeButton = buttonBox->addButton(QDialogButtonBox::Close); connect(closeButton, &QPushButton::pressed, this, &QDialog::accept); layout->addWidget(buttonBox); setMinimumSize(300, 300); resize(600, 600); } MessageViewerDialog::~MessageViewerDialog() = default; QToolBar *MessageViewerDialog::toolBar() const { return d->toolBar; } QList MessageViewerDialog::messages() const { return d->messages; } #include "moc_messageviewerdialog.cpp"