diff --git a/messageviewer/src/dkim-verify/autotests/dkiminfotest.cpp b/messageviewer/src/dkim-verify/autotests/dkiminfotest.cpp index b3cfd6ba..9a7c24b5 100644 --- a/messageviewer/src/dkim-verify/autotests/dkiminfotest.cpp +++ b/messageviewer/src/dkim-verify/autotests/dkiminfotest.cpp @@ -1,142 +1,143 @@ /* Copyright (C) 2018-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "dkiminfotest.h" #include "dkim-verify/dkiminfo.h" #include #include QTEST_GUILESS_MAIN(DKIMInfoTest) DKIMInfoTest::DKIMInfoTest(QObject *parent) : QObject(parent) { } void DKIMInfoTest::shouldHaveDefaultValue() { MessageViewer::DKIMInfo info; QCOMPARE(info.version(), -1); QVERIFY(info.hashingAlgorithm().isEmpty()); QVERIFY(info.signingAlgorithm().isEmpty()); QVERIFY(info.domain().isEmpty()); QVERIFY(info.selector().isEmpty()); QVERIFY(info.bodyHash().isEmpty()); QVERIFY(info.listSignedHeader().isEmpty()); QVERIFY(info.query().isEmpty()); QCOMPARE(info.signatureTimeStamp(), -1); QCOMPARE(info.expireTime(), -1); QVERIFY(info.signature().isEmpty()); QVERIFY(info.agentOrUserIdentifier().isEmpty()); QCOMPARE(info.bodyLengthCount(), -1); QCOMPARE(info.headerCanonization(), MessageViewer::DKIMInfo::Unknown); QCOMPARE(info.bodyCanonization(), MessageViewer::DKIMInfo::Unknown); QVERIFY(info.copiedHeaderField().isEmpty()); QVERIFY(info.iDomain().isEmpty()); } void DKIMInfoTest::shouldTestExtractDkimInfo_data() { QTest::addColumn("dkimstr"); QTest::addColumn("dkiminforesult"); QTest::addColumn("isValid"); QTest::addRow("empty") << QString() << MessageViewer::DKIMInfo() << false; QString val = QStringLiteral("a=rsa-sha1; q=dns; d=example.com; i=user@eng.example.com; s=jun2005.eng; c=relaxed/simple; t=1117574938; x=1118006938; h=from:to:subject:date; b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZVoG4ZHRNiYzR"); MessageViewer::DKIMInfo info1; info1.setVersion(1); info1.setQuery(QStringLiteral("dns")); info1.setDomain(QStringLiteral("example.com")); info1.setSigningAlgorithm(QStringLiteral("rsa")); info1.setHashingAlgorithm(QStringLiteral("sha1")); info1.setBodyCanonization(MessageViewer::DKIMInfo::Simple); info1.setHeaderCanonization(MessageViewer::DKIMInfo::Relaxed); info1.setSignatureTimeStamp(1117574938); info1.setExpireTime(1118006938); info1.setSelector(QStringLiteral("jun2005.eng")); info1.setAgentOrUserIdentifier(QStringLiteral("user")); info1.setIDomain(QStringLiteral("eng.example.com")); info1.setSignature(QStringLiteral("dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZVoG4ZHRNiYzR")); info1.setListSignedHeader(QStringList({QStringLiteral("from"), QStringLiteral("to"), QStringLiteral("subject"), QStringLiteral("date")})); QTest::addRow("test1") << val << info1 << true; val = QStringLiteral("v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; h=message-id:content-type:content-type:user-agent:content-disposition:content-transfer-encoding:mime-version:reply-to:references:in-reply-to:subject:subject" ":list-unsubscribe:list-id:from:from:date:date; s=dkim; t=1569945303; x=1570809304; bh=/TC3U+LlxLH3YGxhC7qIHK8PzGl6Zx/8P6OGhDWrcWs=; b=w4GIjJb/+yEZvzvlw9yIPiuk/eKxAGSKL4WNmgC4D3V9fNyuVOV" "IH06PzqCuU/NwLas3SdAvd3VbTCObAb38KXeXkO7meeyCoR+kDsFzHpSAUg7+IRkeDR+RmarFjXwZAtoX3OMsB8euEprhS9fgGupxCWxwu6VGKJgt3Yu3/cI="); MessageViewer::DKIMInfo info2; info2.setVersion(1); info2.setQuery(QStringLiteral("dns/txt")); info2.setDomain(QStringLiteral("example.com")); info2.setIDomain(QStringLiteral("example.com")); info2.setAgentOrUserIdentifier(QStringLiteral("@example.com")); info2.setSigningAlgorithm(QStringLiteral("rsa")); info2.setHashingAlgorithm(QStringLiteral("sha256")); info2.setBodyCanonization(MessageViewer::DKIMInfo::Relaxed); info2.setHeaderCanonization(MessageViewer::DKIMInfo::Relaxed); info2.setSignatureTimeStamp(1569945303); info2.setExpireTime(1570809304); info2.setSelector(QStringLiteral("dkim")); info2.setSignature(QStringLiteral("w4GIjJb/+yEZvzvlw9yIPiuk/eKxAGSKL4WNmgC4D3V9fNyuVOVIH06PzqCuU/NwLas3SdAvd3VbTCObAb38KXeXkO7meeyCoR+kDsFzHpSAUg7+IRkeDR+RmarFjXwZAtoX3OMsB8euEprhS9fgGupxCWxwu6VGKJgt3Yu3/cI=")); info2.setBodyHash(QStringLiteral("/TC3U+LlxLH3YGxhC7qIHK8PzGl6Zx/8P6OGhDWrcWs=")); info2.setListSignedHeader(QStringList({QStringLiteral("message-id"), QStringLiteral("content-type"), QStringLiteral("content-type"), QStringLiteral("user-agent"), QStringLiteral("content-disposition"), QStringLiteral("content-transfer-encoding"), QStringLiteral("mime-version"), QStringLiteral("reply-to"), QStringLiteral("references"), QStringLiteral("in-reply-to"), QStringLiteral("subject"), QStringLiteral("subject"), QStringLiteral("list-unsubscribe"), QStringLiteral("list-id"), QStringLiteral("from"), QStringLiteral("from"), QStringLiteral("date"), QStringLiteral("date")})); QTest::addRow("test2") << val << info2 << true; - val = QStringLiteral("v=1; a=rsa-sha1; c=relaxed; d=abonnement.radins.com; h=message-id:list-unsubscribe:from:to:reply-to:content-type:subject:content-transfer-encoding:mime-version:date; s=selector1; bh=vyAg5eFfq019WlDt9csu4bJMC54=; b=ABKgPqPe/MOGdgR2TJuiVNTLugsL8q/+ky/JxOxwZxnsPbtFnyJ+Y7Gk8bfcBL9myKPNqe7bU6Uy4IiNptn+v34rhVApm6ccoc44UXe/2A5D+6CPJHjFyf/ggjgF/BtQGYoMeQwj2+F4+QRxHSPldAcWqLCwlcRN25nPgiSAvWg="); + val = QStringLiteral( + "v=1; a=rsa-sha1; c=relaxed; d=abonnement.radins.com; h=message-id:list-unsubscribe:from:to:reply-to:content-type:subject:content-transfer-encoding:mime-version:date; s=selector1; bh=vyAg5eFfq019WlDt9csu4bJMC54=; b=ABKgPqPe/MOGdgR2TJuiVNTLugsL8q/+ky/JxOxwZxnsPbtFnyJ+Y7Gk8bfcBL9myKPNqe7bU6Uy4IiNptn+v34rhVApm6ccoc44UXe/2A5D+6CPJHjFyf/ggjgF/BtQGYoMeQwj2+F4+QRxHSPldAcWqLCwlcRN25nPgiSAvWg="); MessageViewer::DKIMInfo info3; info3.setVersion(1); info3.setQuery(QStringLiteral("dns/txt")); info3.setDomain(QStringLiteral("abonnement.radins.com")); info3.setIDomain(QStringLiteral("abonnement.radins.com")); info3.setAgentOrUserIdentifier(QStringLiteral("@abonnement.radins.com")); info3.setSigningAlgorithm(QStringLiteral("rsa")); info3.setHashingAlgorithm(QStringLiteral("sha1")); info3.setBodyCanonization(MessageViewer::DKIMInfo::Simple); info3.setHeaderCanonization(MessageViewer::DKIMInfo::Relaxed); info3.setSelector(QStringLiteral("selector1")); info3.setSignature(QStringLiteral("ABKgPqPe/MOGdgR2TJuiVNTLugsL8q/+ky/JxOxwZxnsPbtFnyJ+Y7Gk8bfcBL9myKPNqe7bU6Uy4IiNptn+v34rhVApm6ccoc44UXe/2A5D+6CPJHjFyf/ggjgF/BtQGYoMeQwj2+F4+QRxHSPldAcWqLCwlcRN25nPgiSAvWg=")); info3.setBodyHash(QStringLiteral("vyAg5eFfq019WlDt9csu4bJMC54=")); info3.setListSignedHeader(QStringList({QStringLiteral("message-id"), QStringLiteral("list-unsubscribe"), QStringLiteral("from"), QStringLiteral("to"), QStringLiteral("reply-to"), QStringLiteral("content-type"), QStringLiteral("subject"), QStringLiteral("content-transfer-encoding"), QStringLiteral("mime-version"), QStringLiteral("date")})); QTest::addRow("test3") << val << info3 << true; } void DKIMInfoTest::shouldTestExtractDkimInfo() { QFETCH(QString, dkimstr); QFETCH(MessageViewer::DKIMInfo, dkiminforesult); QFETCH(bool, isValid); MessageViewer::DKIMInfo info; QCOMPARE(info.parseDKIM(dkimstr), isValid); if (isValid) { const bool isEqual = (info == dkiminforesult); if (!isEqual) { qDebug() << " info" << info; qDebug() << " dkiminforesult" << dkiminforesult; } QVERIFY(isEqual); } } diff --git a/messageviewer/src/dkim-verify/dkimchecksignaturejob.cpp b/messageviewer/src/dkim-verify/dkimchecksignaturejob.cpp index 9b95c3f2..dae9f27a 100644 --- a/messageviewer/src/dkim-verify/dkimchecksignaturejob.cpp +++ b/messageviewer/src/dkim-verify/dkimchecksignaturejob.cpp @@ -1,584 +1,581 @@ /* Copyright (C) 2018-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "dkimchecksignaturejob.h" #include "dkimdownloadkeyjob.h" #include "dkimmanagerkey.h" #include "dkiminfo.h" #include "dkimutil.h" #include "dkimkeyrecord.h" #include "messageviewer_dkimcheckerdebug.h" #include #include #include //see https://tools.ietf.org/html/rfc6376 using namespace MessageViewer; DKIMCheckSignatureJob::DKIMCheckSignatureJob(QObject *parent) : QObject(parent) { } DKIMCheckSignatureJob::~DKIMCheckSignatureJob() { } MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult DKIMCheckSignatureJob::createCheckResult() { MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult result; result.error = mError; result.warning = mWarning; result.status = mStatus; result.item = mMessageItem; return result; } QString DKIMCheckSignatureJob::bodyCanonizationResult() const { return mBodyCanonizationResult; } QString DKIMCheckSignatureJob::headerCanonizationResult() const { return mHeaderCanonizationResult; } void DKIMCheckSignatureJob::start() { if (!mMessage) { mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } if (mMessageItem.isValid() && !mMessage) { if (mMessageItem.hasPayload()) { mMessage = mMessageItem.payload(); } } else if (mMessage) { //Nothing } else { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Item has not a message"; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } if (auto hrd = mMessage->headerByType("DKIM-Signature")) { mDkimValue = hrd->asUnicodeString(); } if (mDkimValue.isEmpty()) { mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::EmailNotSigned; Q_EMIT result(createCheckResult()); deleteLater(); return; } if (!mDkimInfo.parseDKIM(mDkimValue)) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to parse header" << mDkimValue; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } const MessageViewer::DKIMCheckSignatureJob::DKIMStatus status = checkSignature(mDkimInfo); if (status != MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid) { mStatus = status; Q_EMIT result(createCheckResult()); deleteLater(); return; } //ComputeBodyHash now. switch (mDkimInfo.bodyCanonization()) { case MessageViewer::DKIMInfo::CanonicalizationType::Unknown: mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidBodyCanonicalization; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; case MessageViewer::DKIMInfo::CanonicalizationType::Simple: mBodyCanonizationResult = bodyCanonizationSimple(); break; case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed: mBodyCanonizationResult = bodyCanonizationRelaxed(); break; } //qDebug() << " bodyCanonizationResult "<< mBodyCanonizationResult << " algorithm " << mDkimInfo.hashingAlgorithm() << mDkimInfo.bodyHash(); if (mDkimInfo.bodyLengthCount() != -1) { //Verify it. if (mDkimInfo.bodyLengthCount() < mBodyCanonizationResult.length()) { // length tag exceeds body size mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::SignatureTooLarge; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } else if (mDkimInfo.bodyLengthCount() > mBodyCanonizationResult.length()) { mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::SignatureTooSmall; } // truncated body to the length specified in the "l=" tag mBodyCanonizationResult = mBodyCanonizationResult.left(mDkimInfo.bodyLengthCount()); } QByteArray resultHash; if (mDkimInfo.hashingAlgorithm() == QLatin1String("sha1")) { resultHash = MessageViewer::DKIMUtil::generateHash(mBodyCanonizationResult.toLatin1(), QCryptographicHash::Sha1); } else if (mDkimInfo.hashingAlgorithm() == QLatin1String("sha256")) { resultHash = MessageViewer::DKIMUtil::generateHash(mBodyCanonizationResult.toLatin1(), QCryptographicHash::Sha256); } else { mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InsupportedHashAlgorithm; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } // compare body hash qDebug() << "resultHash " << resultHash << "mDkimInfo.bodyHash()" << mDkimInfo.bodyHash(); if (resultHash != mDkimInfo.bodyHash().toLatin1()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << " Corrupted body hash"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::CorruptedBodyHash; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } //Compute Hash Header switch (mDkimInfo.headerCanonization()) { case MessageViewer::DKIMInfo::CanonicalizationType::Unknown: mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidHeaderCanonicalization; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; case MessageViewer::DKIMInfo::CanonicalizationType::Simple: mHeaderCanonizationResult = headerCanonizationSimple(); break; case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed: mHeaderCanonizationResult = headerCanonizationRelaxed(); break; } // In hash step 2, the Signer/Verifier MUST pass the following to the // hash algorithm in the indicated order. // 1. The header fields specified by the "h=" tag, in the order // specified in that tag, and canonicalized using the header // canonicalization algorithm specified in the "c=" tag. Each // header field MUST be terminated with a single CRLF. // 2. The DKIM-Signature header field that exists (verifying) or will // be inserted (signing) in the message, with the value of the "b=" // tag (including all surrounding whitespace) deleted (i.e., treated // as the empty string), canonicalized using the header // canonicalization algorithm specified in the "c=" tag, and without // a trailing CRLF. // add DKIM-Signature header to the hash input // with the value of the "b=" tag (including all surrounding whitespace) deleted qDebug() << " headerCanonizationResult" << mHeaderCanonizationResult; //Add dkim-signature as lowercase QString dkimValue = mDkimValue; dkimValue = dkimValue.left(dkimValue.indexOf(QLatin1String("b=")) + 2); switch (mDkimInfo.headerCanonization()) { case MessageViewer::DKIMInfo::CanonicalizationType::Unknown: return; case MessageViewer::DKIMInfo::CanonicalizationType::Simple: mHeaderCanonizationResult += QLatin1String("\r\n") + MessageViewer::DKIMUtil::headerCanonizationSimple(QLatin1String("dkim-signature"), dkimValue); break; case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed: mHeaderCanonizationResult += QLatin1String("\r\n") + MessageViewer::DKIMUtil::headerCanonizationRelaxed(QLatin1String("dkim-signature"), dkimValue); break; } qDebug() << " headerCanonizationResult after " << mHeaderCanonizationResult; if (mSaveKey) { const QString keyValue = MessageViewer::DKIMManagerKey::self()->keyValue(mDkimInfo.selector(), mDkimInfo.domain()); if (keyValue.isEmpty()) { downloadKey(mDkimInfo); } else { parseDKIMKeyRecord(keyValue, mDkimInfo.domain(), mDkimInfo.selector(), false); } } else { downloadKey(mDkimInfo); } } QString DKIMCheckSignatureJob::bodyCanonizationSimple() const { /* * canonicalize the body using the simple algorithm * specified in Section 3.4.3 of RFC 6376 */ // The "simple" body canonicalization algorithm ignores all empty lines // at the end of the message body. An empty line is a line of zero // length after removal of the line terminator. If there is no body or // no trailing CRLF on the message body, a CRLF is added. It makes no // other changes to the message body. In more formal terms, the // "simple" body canonicalization algorithm converts "*CRLF" at the end // of the body to a single "CRLF". // Note that a completely empty or missing body is canonicalized as a // single "CRLF"; that is, the canonicalized length will be 2 octets. return MessageViewer::DKIMUtil::bodyCanonizationSimple(QString::fromLatin1(mMessage->body())); } QString DKIMCheckSignatureJob::bodyCanonizationRelaxed() const { /* * canonicalize the body using the relaxed algorithm * specified in Section 3.4.4 of RFC 6376 */ /* a. Reduce whitespace: * Ignore all whitespace at the end of lines. Implementations MUST NOT remove the CRLF at the end of the line. * Reduce all sequences of WSP within a line to a single SP character. b. Ignore all empty lines at the end of the message body. "Empty line" is defined in Section 3.4.3. If the body is non-empty but does not end with a CRLF, a CRLF is added. (For email, this is only possible when using extensions to SMTP or non-SMTP transport mechanisms.) */ return MessageViewer::DKIMUtil::bodyCanonizationRelaxed(QString::fromUtf8(mMessage->body())); } QString DKIMCheckSignatureJob::headerCanonizationSimple() const { QString headers; for (const QString &header : mDkimInfo.listSignedHeader()) { if (auto hrd = mMessage->headerByType(header.toLatin1().constData())) { headers += MessageViewer::DKIMUtil::headerCanonizationSimple(header, hrd->asUnicodeString()); } } return headers; } QString DKIMCheckSignatureJob::headerCanonizationRelaxed() const { // The "relaxed" header canonicalization algorithm MUST apply the // following steps in order: // o Convert all header field names (not the header field values) to // lowercase. For example, convert "SUBJect: AbC" to "subject: AbC". // o Unfold all header field continuation lines as described in // [RFC5322]; in particular, lines with terminators embedded in // continued header field values (that is, CRLF sequences followed by // WSP) MUST be interpreted without the CRLF. Implementations MUST // NOT remove the CRLF at the end of the header field value. // o Convert all sequences of one or more WSP characters to a single SP // character. WSP characters here include those before and after a // line folding boundary. // o Delete all WSP characters at the end of each unfolded header field // value. // o Delete any WSP characters remaining before and after the colon // separating the header field name from the header field value. The // colon separator MUST be retained. QString headers; QStringList headerAlreadyAdded; //Add support for multi headers for (const QString &header : mDkimInfo.listSignedHeader()) { if (headerAlreadyAdded.contains(header)) { continue; } if (auto hrd = mMessage->headerByType(header.toLatin1().constData())) { headerAlreadyAdded << header; if (!headers.isEmpty()) { headers += QLatin1String("\r\n"); } headers += MessageViewer::DKIMUtil::headerCanonizationRelaxed(header, hrd->asUnicodeString()); } } return headers; } void DKIMCheckSignatureJob::downloadKey(const DKIMInfo &info) { DKIMDownloadKeyJob *job = new DKIMDownloadKeyJob(this); job->setDomainName(info.domain()); job->setSelectorName(info.selector()); connect(job, &DKIMDownloadKeyJob::error, this, [this](const QString &errorString) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to start downloadkey: error returned: " << errorString; deleteLater(); }); connect(job, &DKIMDownloadKeyJob::success, this, &DKIMCheckSignatureJob::slotDownloadKeyDone); if (!job->start()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to start downloadkey"; deleteLater(); } } void DKIMCheckSignatureJob::slotDownloadKeyDone(const QList &lst, const QString &domain, const QString &selector) { QByteArray ba; if (lst.count() != 1) { for (const QByteArray &b : lst) { ba += b; } qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Key result has more that 1 element" << lst; } else { ba = lst.at(0); } parseDKIMKeyRecord(QString::fromLocal8Bit(ba), domain, selector, true); } void DKIMCheckSignatureJob::parseDKIMKeyRecord(const QString &str, const QString &domain, const QString &selector, bool storeKeyValue) { qDebug() << "void DKIMCheckSignatureJob::parseDKIMKeyRecord(const QString &str, const QString &domain, const QString &selector, bool storeKeyValue) " << str; if (!mDkimKeyRecord.parseKey(str)) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to parse key record " << str; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } if (mDkimKeyRecord.keyType() != QLatin1String("rsa")) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mDkimKeyRecord key type is unknown " << mDkimKeyRecord.keyType() << " str " << str; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } // if s flag is set in DKIM key record // AUID must be from the same domain as SDID (and not a subdomain) if (mDkimKeyRecord.flags().contains(QLatin1String("s"))) { + // s Any DKIM-Signature header fields using the "i=" tag MUST have + // the same domain value on the right-hand side of the "@" in the + // "i=" tag and the value of the "d=" tag. That is, the "i=" + // domain MUST NOT be a subdomain of "d=". Use of this flag is + // RECOMMENDED unless subdomaining is required. if (mDkimInfo.iDomain() != mDkimInfo.domain()) { mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::DomainI; Q_EMIT result(createCheckResult()); deleteLater(); return; - } - // s Any DKIM-Signature header fields using the "i=" tag MUST have - // the same domain value on the right-hand side of the "@" in the - // "i=" tag and the value of the "d=" tag. That is, the "i=" - // domain MUST NOT be a subdomain of "d=". Use of this flag is - // RECOMMENDED unless subdomaining is required. - //TODO } // check that the testing flag is not set if (mDkimKeyRecord.flags().contains(QLatin1String("y"))) { // s Any DKIM-Signature header fields using the "i=" tag MUST have // the same domain value on the right-hand side of the "@" in the // "i=" tag and the value of the "d=" tag. That is, the "i=" // domain MUST NOT be a subdomain of "d=". Use of this flag is // RECOMMENDED unless subdomaining is required. //TODO } if (mDkimKeyRecord.publicKey().isEmpty()) { // empty value means that this public key has been revoked qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mDkimKeyRecord public key is empty. It was revoked "; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyWasRevoked; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } if (storeKeyValue) { Q_EMIT storeKey(str, domain, selector); } verifyRSASignature(); } void DKIMCheckSignatureJob::verifyRSASignature() { QCA::ConvertResult conversionResult; qDebug() << "mDkimKeyRecord.publicKey() " < currentDate) { mWarning = DKIMCheckSignatureJob::DKIMWarning::SignatureCreatedInFuture; } if (info.signature().isEmpty()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Signature doesn't exist"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::MissingSignature; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (!info.listSignedHeader().contains(QLatin1String("from"), Qt::CaseInsensitive)) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "From is not include in headers list"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::MissingFrom; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (info.domain().isEmpty()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Domain is not defined."; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::DomainNotExist; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (info.query() != QLatin1String("dns/txt")) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Query is incorrect: " << info.query(); mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidQueryMethod; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (info.hashingAlgorithm().isEmpty()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "body header algorithm is empty"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidBodyHashAlgorithm; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (info.signingAlgorithm().isEmpty()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "signature algorithm is empty"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidSignAlgorithm; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (info.hashingAlgorithm() == QLatin1String("sha1")) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "hash algorithm is not secure sha1"; mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::HashAlgorithmUnsafe; } //Add more test //TODO check if info is valid return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid; } DKIMCheckSignatureJob::DKIMError DKIMCheckSignatureJob::error() const { return mError; } DKIMCheckSignatureJob::DKIMStatus DKIMCheckSignatureJob::status() const { return mStatus; } void DKIMCheckSignatureJob::setStatus(const DKIMCheckSignatureJob::DKIMStatus &status) { mStatus = status; } QString DKIMCheckSignatureJob::dkimValue() const { return mDkimValue; } diff --git a/messageviewer/src/dkim-verify/dkimchecksignaturejob.h b/messageviewer/src/dkim-verify/dkimchecksignaturejob.h index 3b6cd5f1..bacf0feb 100644 --- a/messageviewer/src/dkim-verify/dkimchecksignaturejob.h +++ b/messageviewer/src/dkim-verify/dkimchecksignaturejob.h @@ -1,157 +1,156 @@ /* Copyright (C) 2018-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef DKIMCHECKSIGNATUREJOB_H #define DKIMCHECKSIGNATUREJOB_H #include #include "messageviewer_export.h" #include #include #include #include namespace MessageViewer { class MESSAGEVIEWER_EXPORT DKIMCheckSignatureJob : public QObject { Q_OBJECT public: enum class DKIMStatus : int { Unknown = 0, Valid = 1, Invalid = 2, EmailNotSigned = 3 }; Q_ENUM(DKIMStatus) enum class DKIMError : int { Any = 0, CorruptedBodyHash = 1, DomainNotExist = 2, MissingFrom = 3, MissingSignature = 4, InvalidQueryMethod = 5, InvalidHeaderCanonicalization = 6, InvalidBodyCanonicalization = 7, InvalidBodyHashAlgorithm = 8, InvalidSignAlgorithm = 9, PublicKeyWasRevoked = 10, SignatureTooLarge = 11, InsupportedHashAlgorithm = 12, PublicKeyTooSmall = 13, ImpossibleToVerifySignature = 14, DomainI = 15, - //TODO add more }; Q_ENUM(DKIMError) enum class DKIMWarning : int { Any = 0, SignatureExpired = 1, SignatureCreatedInFuture = 2, SignatureTooSmall = 3, HashAlgorithmUnsafe = 4, }; Q_ENUM(DKIMWarning) struct CheckSignatureResult { bool isValid() const { return status != DKIMCheckSignatureJob::DKIMStatus::Unknown; } Q_REQUIRED_RESULT bool operator==(const CheckSignatureResult &other) const { return error == other.error && warning == other.warning && status == other.status && item == other.item; } Q_REQUIRED_RESULT bool operator!=(const CheckSignatureResult &other) const { return !CheckSignatureResult::operator==(other); } DKIMCheckSignatureJob::DKIMError error = DKIMCheckSignatureJob::DKIMError::Any; DKIMCheckSignatureJob::DKIMWarning warning = DKIMCheckSignatureJob::DKIMWarning::Any; DKIMCheckSignatureJob::DKIMStatus status = DKIMCheckSignatureJob::DKIMStatus::Unknown; Akonadi::Item item; }; explicit DKIMCheckSignatureJob(QObject *parent = nullptr); ~DKIMCheckSignatureJob(); void start(); Q_REQUIRED_RESULT QString dkimValue() const; Q_REQUIRED_RESULT DKIMCheckSignatureJob::DKIMStatus status() const; void setStatus(const DKIMCheckSignatureJob::DKIMStatus &status); Q_REQUIRED_RESULT MessageViewer::DKIMCheckSignatureJob::DKIMStatus checkSignature(const MessageViewer::DKIMInfo &info); Q_REQUIRED_RESULT DKIMCheckSignatureJob::DKIMError error() const; Q_REQUIRED_RESULT KMime::Message::Ptr message() const; void setMessage(const KMime::Message::Ptr &message); Q_REQUIRED_RESULT DKIMCheckSignatureJob::DKIMWarning warning() const; void setWarning(const DKIMWarning &warning); Q_REQUIRED_RESULT QString headerCanonizationResult() const; Q_REQUIRED_RESULT QString bodyCanonizationResult() const; Q_REQUIRED_RESULT Akonadi::Item item() const; void setItem(const Akonadi::Item &item); Q_REQUIRED_RESULT bool saveKey() const; void setSaveKey(bool saveKey); Q_SIGNALS: void result(const MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult &checkResult); void storeKey(const QString &key, const QString &domain, const QString &selector); private: void downloadKey(const DKIMInfo &info); void slotDownloadKeyDone(const QList &lst, const QString &domain, const QString &selector); void parseDKIMKeyRecord(const QString &str, const QString &domain, const QString &selector, bool storeKeyValue = true); Q_REQUIRED_RESULT QString headerCanonizationSimple() const; Q_REQUIRED_RESULT QString headerCanonizationRelaxed() const; Q_REQUIRED_RESULT QString bodyCanonizationRelaxed() const; Q_REQUIRED_RESULT QString bodyCanonizationSimple() const; Q_REQUIRED_RESULT MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult createCheckResult(); void verifyRSASignature(); KMime::Message::Ptr mMessage; Akonadi::Item mMessageItem; DKIMInfo mDkimInfo; DKIMKeyRecord mDkimKeyRecord; QString mDkimValue; QString mHeaderCanonizationResult; QString mBodyCanonizationResult; DKIMCheckSignatureJob::DKIMError mError = DKIMCheckSignatureJob::DKIMError::Any; DKIMCheckSignatureJob::DKIMWarning mWarning = DKIMCheckSignatureJob::DKIMWarning::Any; DKIMCheckSignatureJob::DKIMStatus mStatus = DKIMCheckSignatureJob::DKIMStatus::Unknown; bool mSaveKey = false; }; } #endif // DKIMCHECKSIGNATUREJOB_H diff --git a/messageviewer/src/dkim-verify/dkiminfo.cpp b/messageviewer/src/dkim-verify/dkiminfo.cpp index e3cf9394..c1e2091b 100644 --- a/messageviewer/src/dkim-verify/dkiminfo.cpp +++ b/messageviewer/src/dkim-verify/dkiminfo.cpp @@ -1,376 +1,376 @@ /* Copyright (C) 2018-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "dkiminfo.h" #include "dkimutil.h" #include "messageviewer_dkimcheckerdebug.h" #include #include #include "messageviewer_debug.h" using namespace MessageViewer; DKIMInfo::DKIMInfo() { } bool DKIMInfo::parseDKIM(const QString &header) { if (header.isEmpty()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Error: trying to parse empty header"; return false; } const QStringList items = header.split(QLatin1String("; ")); bool foundCanonizations = false; for (int i = 0; i < items.count(); ++i) { const QString elem = items.at(i).trimmed(); if (elem.startsWith(QLatin1String("v="))) { mVersion = elem.right(elem.length() - 2).toInt(); if (mVersion != 1) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Version is not correct " << mVersion; } } else if (elem.startsWith(QLatin1String("a="))) { //Parse it as "algorithm.signature-algorithm.hash parseAlgorithm(elem.right(elem.length() - 2)); } else if (elem.startsWith(QLatin1String("t="))) { mSignatureTimeStamp = elem.right(elem.length() - 2).toLong(); } else if (elem.startsWith(QLatin1String("c="))) { //Parse header/body canonicalization (example c=relaxed/simple) only relaxed and simple. parseCanonicalization(elem.right(elem.length() - 2)); foundCanonizations = true; } else if (elem.startsWith(QLatin1String("bh="))) { mBodyHash = elem.right(elem.length() - 3).replace(QStringLiteral(" "), QString()); } else if (elem.startsWith(QLatin1String("l="))) { mBodyLengthCount = elem.right(elem.length() - 2).toInt(); } else if (elem.startsWith(QLatin1String("i="))) { mAgentOrUserIdentifier = elem.right(elem.length() - 2); } else if (elem.startsWith(QLatin1String("q="))) { mQuery = elem.right(elem.length() - 2); if (mQuery != QLatin1String("dns/txt")) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Query is not correct and not supported " << mQuery; } } else if (elem.startsWith(QLatin1String("d="))) { mDomain = elem.right(elem.length() - 2); } else if (elem.startsWith(QLatin1String("s="))) { mSelector = elem.right(elem.length() - 2); } else if (elem.startsWith(QLatin1String("b="))) { mSignature = elem.right(elem.length() - 2); } else if (elem.startsWith(QLatin1String("h="))) { const QString str = MessageViewer::DKIMUtil::cleanString(elem.right(elem.length() - 2)); mListSignedHeader = str.split(QLatin1Char(':')); } else if (elem.startsWith(QLatin1String("x="))) { mExpireTime = elem.right(elem.length() - 2).toLong(); } else if (elem.startsWith(QLatin1String("z="))) { mCopiedHeaderField = elem.right(elem.length() - 2).split(QLatin1Char(':')); } else { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << " Unknown element type" << elem; } } if (!foundCanonizations) { //Default mHeaderCanonization = Simple; mBodyCanonization = Simple; } if (mVersion == -1) { mVersion = 1; } if (mQuery.isEmpty()) { mQuery = QLatin1String("dns/txt"); } if (mAgentOrUserIdentifier.isEmpty()) { mAgentOrUserIdentifier = QLatin1Char('@') + mDomain; mIDomain = mDomain; } else { const QStringList lst = mAgentOrUserIdentifier.split(QLatin1Char('@')); if (lst.count() == 2) { mAgentOrUserIdentifier = lst.at(0); mIDomain = lst.at(1); } } return true; } void DKIMInfo::parseAlgorithm(const QString &str) { // currently only "rsa-sha1" or "rsa-sha256" //FIXME const QStringList lst = str.split(QLatin1Char('-')); if (lst.count() != 2) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "algorithm is invalid " << str; //Error } else { mSigningAlgorithm = lst.at(0); mHashingAlgorithm = lst.at(1); } } QString DKIMInfo::iDomain() const { return mIDomain; } void DKIMInfo::setIDomain(const QString &iDomain) { mIDomain = iDomain; } void DKIMInfo::parseCanonicalization(const QString &str) { if (!str.isEmpty()) { const QStringList canonicalizations = str.split(QLatin1Char('/')); //qDebug() << " canonicalizations "<< canonicalizations; if (canonicalizations.count() >= 1) { if (canonicalizations.at(0) == QLatin1String("relaxed")) { mHeaderCanonization = DKIMInfo::Relaxed; } else if (canonicalizations.at(0) == QLatin1String("simple")) { mHeaderCanonization = DKIMInfo::Simple; } else { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "canonicalizations for header unknown " << canonicalizations.at(0); mHeaderCanonization = DKIMInfo::Unknown; return; } if (canonicalizations.count() == 1) { mBodyCanonization = DKIMInfo::Simple; } else if (canonicalizations.count() == 2) { if (canonicalizations.at(1) == QLatin1String("relaxed")) { mBodyCanonization = DKIMInfo::Relaxed; } else if (canonicalizations.at(1) == QLatin1String("simple")) { mBodyCanonization = DKIMInfo::Simple; } else { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "canonicalizations for body unknown " << canonicalizations.at(1); mBodyCanonization = DKIMInfo::Unknown; return; } } else { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << " Problem during parsing canonicalizations " << str; mHeaderCanonization = DKIMInfo::Unknown; mBodyCanonization = DKIMInfo::Unknown; } } } } QStringList DKIMInfo::copiedHeaderField() const { return mCopiedHeaderField; } void DKIMInfo::setCopiedHeaderField(const QStringList &copiedHeaderField) { mCopiedHeaderField = copiedHeaderField; } DKIMInfo::CanonicalizationType DKIMInfo::bodyCanonization() const { return mBodyCanonization; } void DKIMInfo::setBodyCanonization(const CanonicalizationType &bodyCanonization) { mBodyCanonization = bodyCanonization; } bool DKIMInfo::operator==(const DKIMInfo &other) const { return mVersion == other.version() && mHashingAlgorithm == other.hashingAlgorithm() && mSigningAlgorithm == other.signingAlgorithm() && mDomain == other.domain() && mSelector == other.selector() && mBodyHash == other.bodyHash() && mSignatureTimeStamp == other.signatureTimeStamp() && mExpireTime == other.expireTime() && mQuery == other.query() && mSignature == other.signature() && mAgentOrUserIdentifier == other.agentOrUserIdentifier() && mBodyLengthCount == other.bodyLengthCount() && mListSignedHeader == other.listSignedHeader() && mHeaderCanonization == other.headerCanonization() && mBodyCanonization == other.bodyCanonization() - && mIDomain == other.iDomain(); + && mIDomain == other.iDomain(); } DKIMInfo::CanonicalizationType DKIMInfo::headerCanonization() const { return mHeaderCanonization; } void DKIMInfo::setHeaderCanonization(const CanonicalizationType &headerCanonization) { mHeaderCanonization = headerCanonization; } int DKIMInfo::version() const { return mVersion; } void DKIMInfo::setVersion(int version) { mVersion = version; } QString DKIMInfo::hashingAlgorithm() const { return mHashingAlgorithm; } void DKIMInfo::setHashingAlgorithm(const QString &hashingAlgorithm) { mHashingAlgorithm = hashingAlgorithm; } QString DKIMInfo::domain() const { return mDomain; } void DKIMInfo::setDomain(const QString &domain) { mDomain = domain; } QString DKIMInfo::selector() const { return mSelector; } void DKIMInfo::setSelector(const QString &selector) { mSelector = selector; } QString DKIMInfo::bodyHash() const { return mBodyHash; } void DKIMInfo::setBodyHash(const QString &bodyHash) { mBodyHash = bodyHash; } bool DKIMInfo::isValid() const { if (mBodyCanonization == DKIMInfo::Unknown || mHeaderCanonization == DKIMInfo::Unknown) { return false; } return !mSelector.isEmpty() && !mDomain.isEmpty() && !mBodyHash.isEmpty() && !mHashingAlgorithm.isEmpty(); } QStringList DKIMInfo::listSignedHeader() const { return mListSignedHeader; } void DKIMInfo::setListSignedHeader(const QStringList &listSignedHeader) { mListSignedHeader = listSignedHeader; } QString DKIMInfo::signingAlgorithm() const { return mSigningAlgorithm; } void DKIMInfo::setSigningAlgorithm(const QString &signingAlgorithm) { mSigningAlgorithm = signingAlgorithm; } qint64 DKIMInfo::signatureTimeStamp() const { return mSignatureTimeStamp; } void DKIMInfo::setSignatureTimeStamp(qint64 signatureTimeStamp) { mSignatureTimeStamp = signatureTimeStamp; } QString DKIMInfo::query() const { return mQuery; } void DKIMInfo::setQuery(const QString &query) { mQuery = query; } qint64 DKIMInfo::expireTime() const { return mExpireTime; } void DKIMInfo::setExpireTime(qint64 expireTime) { mExpireTime = expireTime; } QString DKIMInfo::signature() const { return mSignature; } void DKIMInfo::setSignature(const QString &signature) { mSignature = signature; } QString DKIMInfo::agentOrUserIdentifier() const { return mAgentOrUserIdentifier; } void DKIMInfo::setAgentOrUserIdentifier(const QString &userAgent) { mAgentOrUserIdentifier = userAgent; } int DKIMInfo::bodyLengthCount() const { return mBodyLengthCount; } void DKIMInfo::setBodyLengthCount(int bodyLengthCount) { mBodyLengthCount = bodyLengthCount; } QDebug operator <<(QDebug d, const DKIMInfo &t) { d << "mVersion " << t.version(); d << "mHashingAlgorithm " << t.hashingAlgorithm(); d << "mSigningAlgorithm " << t.signingAlgorithm(); d << "mDomain " << t.domain(); d << "mSelector " << t.selector(); d << "mBodyHash " << t.bodyHash(); d << "mSignatureTimeStamp " << t.signatureTimeStamp(); d << "mExpireTime " << t.expireTime(); d << "mQuery " << t.query(); d << "mSignature " << t.signature(); d << "mAgentOrUserIdentifier " << t.agentOrUserIdentifier(); d << "mBodyLengthCount " << t.bodyLengthCount(); d << "mListSignedHeader " << t.listSignedHeader(); d << "mHeaderCanonization " << t.headerCanonization(); d << "mBodyCanonization " << t.bodyCanonization(); d << "mIdomain " << t.iDomain(); return d; } diff --git a/messageviewer/src/dkim-verify/tests/checksignature.cpp b/messageviewer/src/dkim-verify/tests/checksignature.cpp index 2de81722..d80dacba 100644 --- a/messageviewer/src/dkim-verify/tests/checksignature.cpp +++ b/messageviewer/src/dkim-verify/tests/checksignature.cpp @@ -1,78 +1,76 @@ /* Copyright (C) 2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "checksignature.h" #include #include #include #include #include CheckSignature::CheckSignature(const QString &fileName, QObject *parent) : QObject(parent) { mQcaInitializer = new QCA::Initializer(QCA::Practical, 64); MessageViewer::DKIMCheckSignatureJob *job = new MessageViewer::DKIMCheckSignatureJob(this); connect(job, &MessageViewer::DKIMCheckSignatureJob::result, this, &CheckSignature::slotResult); KMime::Message *msg = new KMime::Message; QFile file(fileName); if (file.open(QIODevice::ReadOnly)) { msg->setContent(file.readAll()); } else { qWarning() << "Couldn't read" << fileName; } msg->parse(); job->setMessage(KMime::Message::Ptr(msg)); job->start(); } CheckSignature::~CheckSignature() { - } void CheckSignature::slotResult(const MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult &checkResult) { qDebug() << "result : status " << checkResult.status << " error : " << checkResult.error << " warning " << checkResult.warning; } - int main(int argc, char **argv) { QCoreApplication app(argc, argv); QStandardPaths::setTestModeEnabled(true); QCommandLineParser parser; parser.addVersionOption(); parser.addHelpOption(); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("+[file]"), QStringLiteral("File containing an email"))); parser.process(app); QString filename; if (!parser.positionalArguments().isEmpty()) { filename = parser.positionalArguments().at(0); } CheckSignature *w = new CheckSignature(filename); app.exec(); return 0; } diff --git a/messageviewer/src/messagepartthemes/default/defaultrenderer.cpp b/messageviewer/src/messagepartthemes/default/defaultrenderer.cpp index b29d5a36..810ed886 100644 --- a/messageviewer/src/messagepartthemes/default/defaultrenderer.cpp +++ b/messageviewer/src/messagepartthemes/default/defaultrenderer.cpp @@ -1,1176 +1,1176 @@ /* Copyright (c) 2016 Sandro Knauß This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "defaultrenderer.h" #include "defaultrenderer_p.h" #include "utils/messageviewerutil.h" #include "messageviewer_debug.h" #include "converthtmltoplaintext.h" #include "messagepartrendererbase.h" #include "messagepartrendererfactory.h" #include "htmlblock.h" #include "utils/iconnamecache.h" #include "utils/mimetype.h" #include "viewer/attachmentstrategy.h" #include "viewer/csshelperbase.h" #include "messagepartrenderermanager.h" #include "htmlwriter/bufferedhtmlwriter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MimeTreeParser; using namespace MessageViewer; Q_DECLARE_METATYPE(GpgME::DecryptionResult::Recipient) Q_DECLARE_METATYPE(GpgME::Key) Q_DECLARE_METATYPE(const QGpgME::Protocol *) static const int SIG_FRAME_COL_UNDEF = 99; #define SIG_FRAME_COL_RED -1 #define SIG_FRAME_COL_YELLOW 0 #define SIG_FRAME_COL_GREEN 1 QString sigStatusToString(const QGpgME::Protocol *cryptProto, int status_code, GpgME::Signature::Summary summary, int &frameColor, bool &showKeyInfos) { // note: At the moment frameColor and showKeyInfos are // used for CMS only but not for PGP signatures // pending(khz): Implement usage of these for PGP sigs as well. showKeyInfos = true; QString result; if (cryptProto) { if (cryptProto == QGpgME::openpgp()) { // process enum according to it's definition to be read in // GNU Privacy Guard CVS repository /gpgme/gpgme/gpgme.h switch (status_code) { case 0: // GPGME_SIG_STAT_NONE result = i18n("Error: Signature not verified"); frameColor = SIG_FRAME_COL_YELLOW; break; case 1: // GPGME_SIG_STAT_GOOD result = i18n("Good signature"); frameColor = SIG_FRAME_COL_GREEN; break; case 2: // GPGME_SIG_STAT_BAD result = i18n("Bad signature"); frameColor = SIG_FRAME_COL_RED; break; case 3: // GPGME_SIG_STAT_NOKEY result = i18n("No public key to verify the signature"); frameColor = SIG_FRAME_COL_RED; break; case 4: // GPGME_SIG_STAT_NOSIG result = i18n("No signature found"); frameColor = SIG_FRAME_COL_RED; break; case 5: // GPGME_SIG_STAT_ERROR result = i18n("Error verifying the signature"); frameColor = SIG_FRAME_COL_RED; break; case 6: // GPGME_SIG_STAT_DIFF result = i18n("Different results for signatures"); frameColor = SIG_FRAME_COL_RED; break; /* PENDING(khz) Verify exact meaning of the following values: case 7: // GPGME_SIG_STAT_GOOD_EXP return i18n("Signature certificate is expired"); break; case 8: // GPGME_SIG_STAT_GOOD_EXPKEY return i18n("One of the certificate's keys is expired"); break; */ default: result.clear(); // do *not* return a default text here ! break; } } else if (cryptProto == QGpgME::smime()) { // process status bits according to SigStatus_... // definitions in kdenetwork/libkdenetwork/cryptplug.h if (summary == GpgME::Signature::None) { result = i18n("No status information available."); frameColor = SIG_FRAME_COL_YELLOW; showKeyInfos = false; return result; } if (summary & GpgME::Signature::Valid) { result = i18n("Good signature."); // Note: // Here we are work differently than KMail did before! // // The GOOD case ( == sig matching and the complete // certificate chain was verified and is valid today ) // by definition does *not* show any key // information but just states that things are OK. // (khz, according to LinuxTag 2002 meeting) frameColor = SIG_FRAME_COL_GREEN; showKeyInfos = false; return result; } // we are still there? OK, let's test the different cases: // we assume green, test for yellow or red (in this order!) frameColor = SIG_FRAME_COL_GREEN; QString result2; if (summary & GpgME::Signature::KeyExpired) { // still is green! result2 = i18n("One key has expired."); } if (summary & GpgME::Signature::SigExpired) { // and still is green! result2 += i18n("The signature has expired."); } // test for yellow: if (summary & GpgME::Signature::KeyMissing) { result2 += i18n("Unable to verify: key missing."); // if the signature certificate is missing // we cannot show information on it showKeyInfos = false; frameColor = SIG_FRAME_COL_YELLOW; } if (summary & GpgME::Signature::CrlMissing) { result2 += i18n("CRL not available."); frameColor = SIG_FRAME_COL_YELLOW; } if (summary & GpgME::Signature::CrlTooOld) { result2 += i18n("Available CRL is too old."); frameColor = SIG_FRAME_COL_YELLOW; } if (summary & GpgME::Signature::BadPolicy) { result2 += i18n("A policy was not met."); frameColor = SIG_FRAME_COL_YELLOW; } if (summary & GpgME::Signature::SysError) { result2 += i18n("A system error occurred."); // if a system error occurred // we cannot trust any information // that was given back by the plug-in showKeyInfos = false; frameColor = SIG_FRAME_COL_YELLOW; } // test for red: if (summary & GpgME::Signature::KeyRevoked) { // this is red! result2 += i18n("One key has been revoked."); frameColor = SIG_FRAME_COL_RED; } if (summary & GpgME::Signature::Red) { if (result2.isEmpty()) { // Note: // Here we are work differently than KMail did before! // // The BAD case ( == sig *not* matching ) // by definition does *not* show any key // information but just states that things are BAD. // // The reason for this: In this case ALL information // might be falsificated, we can NOT trust the data // in the body NOT the signature - so we don't show // any key/signature information at all! // (khz, according to LinuxTag 2002 meeting) showKeyInfos = false; } frameColor = SIG_FRAME_COL_RED; } else { result.clear(); } if (SIG_FRAME_COL_GREEN == frameColor) { result = i18n("Good signature."); } else if (SIG_FRAME_COL_RED == frameColor) { result = i18n("Bad signature."); } else { result.clear(); } if (!result2.isEmpty()) { if (!result.isEmpty()) { result.append(QLatin1String("
")); } result.append(result2); } } /* // add i18n support for 3rd party plug-ins here: else if ( cryptPlug->libName().contains( "yetanotherpluginname", Qt::CaseInsensitive )) { } */ } return result; } /** Checks whether @p str contains external references. To be precise, we only check whether @p str contains 'xxx="http[s]:' where xxx is not href. Obfuscated external references are ignored on purpose. */ bool containsExternalReferences(const QString &str, const QString &extraHead) { const bool hasBaseInHeader = extraHead.contains(QLatin1String( "= 0 || httpsPos >= 0) { // pos = index of next occurrence of "http: or "https: whichever comes first int pos = (httpPos < httpsPos) ? ((httpPos >= 0) ? httpPos : httpsPos) : ((httpsPos >= 0) ? httpsPos : httpPos); // look backwards for "href" if (pos > 5) { int hrefPos = str.lastIndexOf(QLatin1String("href"), pos - 5, Qt::CaseInsensitive); // if no 'href' is found or the distance between 'href' and '"http[s]:' // is larger than 7 (7 is the distance in 'href = "http[s]:') then // we assume that we have found an external reference if ((hrefPos == -1) || (pos - hrefPos > 7)) { // HTML messages created by KMail itself for now contain the following: // // Make sure not to show an external references warning for this string int dtdPos = str.indexOf(QLatin1String( "http://www.w3.org/TR/html4/loose.dtd"), pos + 1); if (dtdPos != (pos + 1)) { return true; } } } // find next occurrence of "http: or "https: if (pos == httpPos) { httpPos = str.indexOf(QLatin1String("\"http:"), httpPos + 6, Qt::CaseInsensitive); } else { httpsPos = str.indexOf(QLatin1String("\"https:"), httpsPos + 7, Qt::CaseInsensitive); } } if (str.indexOf(QRegularExpression(QLatin1String("]*>"), Qt::CaseInsensitive)).trimmed(); s = s.remove(QRegExp(QStringLiteral("^]*>"), Qt::CaseInsensitive)).trimmed(); // head s = s.replace(QRegExp(QStringLiteral("^"), Qt::CaseInsensitive), QString()).trimmed(); const int startIndex = s.indexOf(QLatin1String(""), Qt::CaseInsensitive); if (startIndex >= 0) { const auto endIndex = s.indexOf(QLatin1String(""), Qt::CaseInsensitive); if (endIndex < 0) { return htmlSource; } extraHead = s.mid(startIndex + 6, endIndex - startIndex - 6); #if QTWEBENGINEWIDGETS_VERSION < QT_VERSION_CHECK(5, 13, 0) //Remove this hack with https://codereview.qt-project.org/#/c/256100/2 is merged //Don't authorize to refresh content. if (MessageViewer::Util::excludeExtraHeader(s)) { extraHead.clear(); } #endif s = s.mid(endIndex + 7).trimmed(); } // body s = s.remove(QRegExp(QStringLiteral("]*>"), Qt::CaseInsensitive)).trimmed(); s = s.remove(QRegExp(QStringLiteral("$"), Qt::CaseInsensitive)).trimmed(); s = s.remove(QRegExp(QStringLiteral("$"), Qt::CaseInsensitive)).trimmed(); return s; } DefaultRendererPrivate::DefaultRendererPrivate(CSSHelperBase *cssHelper, const MessagePartRendererFactory *rendererFactory) : mCSSHelper(cssHelper) , mRendererFactory(rendererFactory) { } DefaultRendererPrivate::~DefaultRendererPrivate() { } CSSHelperBase *DefaultRendererPrivate::cssHelper() const { return mCSSHelper; } Interface::ObjectTreeSource *DefaultRendererPrivate::source() const { return mMsgPart->source(); } void DefaultRendererPrivate::renderSubParts(const MessagePart::Ptr &msgPart, HtmlWriter *htmlWriter) { for (const auto &m : msgPart->subParts()) { renderFactory(m, htmlWriter); } } void DefaultRendererPrivate::render(const MessagePartList::Ptr &mp, HtmlWriter *htmlWriter) { HTMLBlock::Ptr rBlock; HTMLBlock::Ptr aBlock; if (mp->isRoot()) { rBlock = HTMLBlock::Ptr(new RootBlock(htmlWriter)); } if (mp->isAttachment()) { aBlock = HTMLBlock::Ptr(new AttachmentMarkBlock(htmlWriter, mp->attachmentContent())); } renderSubParts(mp, htmlWriter); } void DefaultRendererPrivate::render(const MimeMessagePart::Ptr &mp, HtmlWriter *htmlWriter) { HTMLBlock::Ptr aBlock; HTMLBlock::Ptr rBlock; if (mp->isAttachment()) { aBlock = HTMLBlock::Ptr(new AttachmentMarkBlock(htmlWriter, mp->attachmentContent())); } if (mp->isRoot()) { rBlock = HTMLBlock::Ptr(new RootBlock(htmlWriter)); } renderSubParts(mp, htmlWriter); } void DefaultRendererPrivate::render(const EncapsulatedRfc822MessagePart::Ptr &mp, HtmlWriter *htmlWriter) { if (!mp->hasSubParts()) { return; } Grantlee::Template t = MessagePartRendererManager::self()->loadByName(QStringLiteral("encapsulatedrfc822messagepart.html")); Grantlee::Context c = MessagePartRendererManager::self()->createContext(); QObject block; c.insert(QStringLiteral("block"), &block); block.setProperty("link", mp->nodeHelper()->asHREF(mp->message().data(), QStringLiteral("body"))); c.insert(QStringLiteral("msgHeader"), mCreateMessageHeader(mp->message().data())); c.insert(QStringLiteral("content"), QVariant::fromValue([this, mp, htmlWriter](Grantlee::OutputStream *) { renderSubParts(mp, htmlWriter); })); HTMLBlock::Ptr aBlock; if (mp->isAttachment()) { aBlock = HTMLBlock::Ptr(new AttachmentMarkBlock(htmlWriter, mp->attachmentContent())); } Grantlee::OutputStream s(htmlWriter->stream()); t->render(&s, &c); } void DefaultRendererPrivate::render(const HtmlMessagePart::Ptr &mp, HtmlWriter *htmlWriter) { Grantlee::Template t = MessageViewer::MessagePartRendererManager::self()->loadByName(QStringLiteral( "htmlmessagepart.html")); Grantlee::Context c = MessageViewer::MessagePartRendererManager::self()->createContext(); QObject block; c.insert(QStringLiteral("block"), &block); auto preferredMode = mp->source()->preferredMode(); const bool isHtmlPreferred = (preferredMode == MimeTreeParser::Util::Html) || (preferredMode == MimeTreeParser::Util::MultipartHtml); block.setProperty("htmlMail", isHtmlPreferred); block.setProperty("loadExternal", htmlLoadExternal()); block.setProperty("isPrinting", isPrinting()); { QString extraHead; //laurent: FIXME port to async method webengine QString bodyText = processHtml(mp->bodyHtml(), extraHead); if (isHtmlPreferred) { mp->nodeHelper()->setNodeDisplayedEmbedded(mp->content(), true); htmlWriter->extraHead(extraHead); } block.setProperty("containsExternalReferences", containsExternalReferences(bodyText, extraHead)); c.insert(QStringLiteral("content"), bodyText); } { ConvertHtmlToPlainText convert; convert.setHtmlString(mp->bodyHtml()); QString plaintext = convert.generatePlainText(); plaintext.replace(QLatin1Char('\n'), QStringLiteral("
")); c.insert(QStringLiteral("plaintext"), plaintext); } mp->source()->setHtmlMode(MimeTreeParser::Util::Html, QList() << MimeTreeParser::Util::Html << MimeTreeParser::Util::Normal); HTMLBlock::Ptr aBlock; if (mp->isAttachment()) { aBlock = HTMLBlock::Ptr(new AttachmentMarkBlock(htmlWriter, mp->attachmentContent())); } Grantlee::OutputStream s(htmlWriter->stream()); t->render(&s, &c); } void DefaultRendererPrivate::renderEncrypted(const EncryptedMessagePart::Ptr &mp, HtmlWriter *htmlWriter) { KMime::Content *node = mp->content(); const auto metaData = *mp->partMetaData(); Grantlee::Template t = MessageViewer::MessagePartRendererManager::self()->loadByName(QStringLiteral( "encryptedmessagepart.html")); Grantlee::Context c = MessageViewer::MessagePartRendererManager::self()->createContext(); QObject block; if (node || mp->hasSubParts()) { c.insert(QStringLiteral("content"), QVariant::fromValue([this, mp, htmlWriter](Grantlee::OutputStream *) { HTMLBlock::Ptr rBlock; if (mp->content() && mp->isRoot()) { rBlock = HTMLBlock::Ptr(new RootBlock(htmlWriter)); } renderSubParts(mp, htmlWriter); })); } else if (!metaData.inProgress) { c.insert(QStringLiteral("content"), QVariant::fromValue([this, mp, htmlWriter](Grantlee::OutputStream *) { renderWithFactory(mp, htmlWriter); })); } c.insert(QStringLiteral("cryptoProto"), QVariant::fromValue(mp->cryptoProto())); if (!mp->decryptRecipients().empty()) { c.insert(QStringLiteral("decryptedRecipients"), QVariant::fromValue(mp->decryptRecipients())); } c.insert(QStringLiteral("block"), &block); block.setProperty("isPrinting", isPrinting()); block.setProperty("detailHeader", showEncryptionDetails()); block.setProperty("inProgress", metaData.inProgress); block.setProperty("isDecrypted", mp->decryptMessage()); block.setProperty("isDecryptable", metaData.isDecryptable); block.setProperty("decryptIcon", QUrl::fromLocalFile(IconNameCache::instance()->iconPath(QStringLiteral( "document-decrypt"), KIconLoader::Small)).url()); block.setProperty("errorText", metaData.errorText); block.setProperty("noSecKey", mp->isNoSecKey()); Grantlee::OutputStream s(htmlWriter->stream()); t->render(&s, &c); } void DefaultRendererPrivate::renderSigned(const SignedMessagePart::Ptr &mp, HtmlWriter *htmlWriter) { KMime::Content *node = mp->content(); const auto metaData = *mp->partMetaData(); auto cryptoProto = mp->cryptoProto(); const bool isSMIME = cryptoProto && (cryptoProto == QGpgME::smime()); Grantlee::Template t = MessageViewer::MessagePartRendererManager::self()->loadByName(QStringLiteral( "signedmessagepart.html")); Grantlee::Context c = MessageViewer::MessagePartRendererManager::self()->createContext(); QObject block; if (node) { c.insert(QStringLiteral("content"), QVariant::fromValue([this, mp, htmlWriter](Grantlee::OutputStream *) { HTMLBlock::Ptr rBlock; if (mp->isRoot()) { rBlock = HTMLBlock::Ptr(new RootBlock(htmlWriter)); } renderSubParts(mp, htmlWriter); })); } else if (!metaData.inProgress) { c.insert(QStringLiteral("content"), QVariant::fromValue([this, mp, htmlWriter](Grantlee::OutputStream *) { renderWithFactory(mp, htmlWriter); })); } c.insert(QStringLiteral("cryptoProto"), QVariant::fromValue(cryptoProto)); c.insert(QStringLiteral("block"), &block); block.setProperty("inProgress", metaData.inProgress); block.setProperty("errorText", metaData.errorText); block.setProperty("detailHeader", showSignatureDetails()); block.setProperty("isPrinting", isPrinting()); block.setProperty("addr", metaData.signerMailAddresses.join(QLatin1Char(','))); block.setProperty("technicalProblem", metaData.technicalProblem); block.setProperty("keyId", metaData.keyId); if (metaData.creationTime.isValid()) { //should be handled inside grantlee but currently not possible see: https://bugs.kde.org/363475 block.setProperty("creationTime", QLocale().toString(metaData.creationTime, QLocale::ShortFormat)); } block.setProperty("isGoodSignature", metaData.isGoodSignature); block.setProperty("isSMIME", isSMIME); if (metaData.keyTrust == GpgME::Signature::Unknown) { block.setProperty("keyTrust", QStringLiteral("unknown")); } else if (metaData.keyTrust == GpgME::Signature::Marginal) { block.setProperty("keyTrust", QStringLiteral("marginal")); } else if (metaData.keyTrust == GpgME::Signature::Full) { block.setProperty("keyTrust", QStringLiteral("full")); } else if (metaData.keyTrust == GpgME::Signature::Ultimate) { block.setProperty("keyTrust", QStringLiteral("ultimate")); } else { block.setProperty("keyTrust", QStringLiteral("untrusted")); } QString startKeyHREF; { QString keyWithWithoutURL; if (cryptoProto) { startKeyHREF = QStringLiteral("") .arg(cryptoProto->displayName(), cryptoProto->name(), QString::fromLatin1(metaData.keyId)); keyWithWithoutURL = QStringLiteral("%1%2").arg(startKeyHREF, QString::fromLatin1(QByteArray(QByteArrayLiteral( "0x") + metaData.keyId))); } else { keyWithWithoutURL = QLatin1String("0x") + QString::fromUtf8(metaData.keyId); } block.setProperty("keyWithWithoutURL", keyWithWithoutURL); } bool onlyShowKeyURL = false; bool showKeyInfos = false; bool cannotCheckSignature = true; QString signer = metaData.signer; QString statusStr; QString mClass; QString greenCaseWarning; if (metaData.inProgress) { mClass = QStringLiteral("signInProgress"); } else { const QStringList &blockAddrs(metaData.signerMailAddresses); // note: At the moment frameColor and showKeyInfos are // used for CMS only but not for PGP signatures // pending(khz): Implement usage of these for PGP sigs as well. int frameColor = SIG_FRAME_COL_UNDEF; statusStr = sigStatusToString(cryptoProto, metaData.status_code, metaData.sigSummary, frameColor, showKeyInfos); // if needed fallback to english status text // that was reported by the plugin if (statusStr.isEmpty()) { statusStr = metaData.status; } if (metaData.technicalProblem) { frameColor = SIG_FRAME_COL_YELLOW; } switch (frameColor) { case SIG_FRAME_COL_RED: cannotCheckSignature = false; break; case SIG_FRAME_COL_YELLOW: cannotCheckSignature = true; break; case SIG_FRAME_COL_GREEN: cannotCheckSignature = false; break; } // temporary hack: always show key information! showKeyInfos = true; if (isSMIME && (SIG_FRAME_COL_UNDEF != frameColor)) { switch (frameColor) { case SIG_FRAME_COL_RED: mClass = QStringLiteral("signErr"); onlyShowKeyURL = true; break; case SIG_FRAME_COL_YELLOW: if (metaData.technicalProblem) { mClass = QStringLiteral("signWarn"); } else { mClass = QStringLiteral("signOkKeyBad"); } break; case SIG_FRAME_COL_GREEN: mClass = QStringLiteral("signOkKeyOk"); // extra hint for green case // that email addresses in DN do not match fromAddress QString msgFrom(KEmailAddress::extractEmailAddress(mp->fromAddress())); QString certificate; if (metaData.keyId.isEmpty()) { certificate = i18n("certificate"); } else { certificate = startKeyHREF + i18n("certificate") + QStringLiteral(""); } if (!blockAddrs.empty()) { if (!blockAddrs.contains(msgFrom, Qt::CaseInsensitive)) { greenCaseWarning = QStringLiteral("") +i18nc("Start of warning message.", "Warning:") +QStringLiteral(" ") +i18n( "Sender's mail address is not stored in the %1 used for signing.", certificate) +QStringLiteral("
") +i18n("sender: ") +msgFrom +QStringLiteral("
") +i18n("stored: "); // We cannot use Qt's join() function here but // have to join the addresses manually to // extract the mail addresses (without '<''>') // before including it into our string: bool bStart = true; QStringList::ConstIterator end(blockAddrs.constEnd()); for (QStringList::ConstIterator it = blockAddrs.constBegin(); it != end; ++it) { if (!bStart) { greenCaseWarning.append(QLatin1String(",
   ")); } bStart = false; greenCaseWarning.append(KEmailAddress::extractEmailAddress(*it)); } } } else { greenCaseWarning = QStringLiteral("") +i18nc("Start of warning message.", "Warning:") +QStringLiteral(" ") +i18n("No mail address is stored in the %1 used for signing, " "so we cannot compare it to the sender's address %2.", certificate, msgFrom); } break; } if (showKeyInfos && !cannotCheckSignature) { if (metaData.signer.isEmpty()) { signer.clear(); } else { if (!blockAddrs.empty()) { const QUrl address = KEmailAddress::encodeMailtoUrl(blockAddrs.first()); signer = QStringLiteral("%2").arg(QLatin1String(QUrl :: toPercentEncoding( address . path())), signer); } } } } else { if (metaData.signer.isEmpty() || metaData.technicalProblem) { mClass = QStringLiteral("signWarn"); } else { // HTMLize the signer's user id and create mailto: link signer = MessageCore::StringUtil::quoteHtmlChars(signer, true); signer = QStringLiteral("%1").arg(signer); if (metaData.isGoodSignature) { if (metaData.keyTrust < GpgME::Signature::Marginal) { mClass = QStringLiteral("signOkKeyBad"); } else { mClass = QStringLiteral("signOkKeyOk"); } } else { mClass = QStringLiteral("signErr"); } } } } block.setProperty("onlyShowKeyURL", onlyShowKeyURL); block.setProperty("showKeyInfos", showKeyInfos); block.setProperty("cannotCheckSignature", cannotCheckSignature); block.setProperty("signer", signer); block.setProperty("statusStr", statusStr); block.setProperty("signClass", mClass); block.setProperty("greenCaseWarning", greenCaseWarning); Grantlee::OutputStream s(htmlWriter->stream()); t->render(&s, &c); } void DefaultRendererPrivate::render(const SignedMessagePart::Ptr &mp, HtmlWriter *htmlWriter) { const auto metaData = *mp->partMetaData(); if (metaData.isSigned || metaData.inProgress) { HTMLBlock::Ptr aBlock; if (mp->isAttachment()) { aBlock = HTMLBlock::Ptr(new AttachmentMarkBlock(htmlWriter, mp->attachmentContent())); } renderSigned(mp, htmlWriter); return; } HTMLBlock::Ptr aBlock; if (mp->isAttachment()) { aBlock = HTMLBlock::Ptr(new AttachmentMarkBlock(htmlWriter, mp->attachmentContent())); } if (mp->hasSubParts()) { renderSubParts(mp, htmlWriter); } else if (!metaData.inProgress) { renderWithFactory(mp, htmlWriter); } } void DefaultRendererPrivate::render(const EncryptedMessagePart::Ptr &mp, HtmlWriter *htmlWriter) { const auto metaData = *mp->partMetaData(); if (metaData.isEncrypted || metaData.inProgress) { HTMLBlock::Ptr aBlock; if (mp->isAttachment()) { aBlock = HTMLBlock::Ptr(new AttachmentMarkBlock(htmlWriter, mp->attachmentContent())); } renderEncrypted(mp, htmlWriter); return; } HTMLBlock::Ptr aBlock; if (mp->isAttachment()) { aBlock = HTMLBlock::Ptr(new AttachmentMarkBlock(htmlWriter, mp->attachmentContent())); } if (mp->hasSubParts()) { renderSubParts(mp, htmlWriter); } else if (!metaData.inProgress) { renderWithFactory(mp, htmlWriter); } } void DefaultRendererPrivate::render(const AlternativeMessagePart::Ptr &mp, HtmlWriter *htmlWriter) { HTMLBlock::Ptr aBlock; if (mp->isAttachment()) { aBlock = HTMLBlock::Ptr(new AttachmentMarkBlock(htmlWriter, mp->attachmentContent())); } auto mode = mp->preferredMode(); if (mode == MimeTreeParser::Util::MultipartPlain && mp->text().trimmed().isEmpty()) { for (const auto m : mp->availableModes()) { if (m != MimeTreeParser::Util::MultipartPlain) { mode = m; break; } } } MimeMessagePart::Ptr part(mp->childParts().first()); if (mp->childParts().contains(mode)) { part = mp->childParts()[mode]; } render(part, htmlWriter); } void DefaultRendererPrivate::render(const CertMessagePart::Ptr &mp, HtmlWriter *htmlWriter) { const GpgME::ImportResult &importResult(mp->importResult()); Grantlee::Template t = MessageViewer::MessagePartRendererManager::self()->loadByName(QStringLiteral( "certmessagepart.html")); Grantlee::Context c = MessageViewer::MessagePartRendererManager::self()->createContext(); QObject block; c.insert(QStringLiteral("block"), &block); block.setProperty("importError", QString::fromLocal8Bit(importResult.error().asString())); block.setProperty("nImp", importResult.numImported()); block.setProperty("nUnc", importResult.numUnchanged()); block.setProperty("nSKImp", importResult.numSecretKeysImported()); block.setProperty("nSKUnc", importResult.numSecretKeysUnchanged()); QVariantList keylist; const auto imports = importResult.imports(); auto end(imports.end()); for (auto it = imports.begin(); it != end; ++it) { QObject *key(new QObject(mp.data())); key->setProperty("error", QString::fromLocal8Bit((*it).error().asString())); key->setProperty("status", (*it).status()); key->setProperty("fingerprint", QLatin1String((*it).fingerprint())); keylist << QVariant::fromValue(key); } HTMLBlock::Ptr aBlock; if (mp->isAttachment()) { aBlock = HTMLBlock::Ptr(new AttachmentMarkBlock(htmlWriter, mp->attachmentContent())); } Grantlee::OutputStream s(htmlWriter->stream()); t->render(&s, &c); } bool DefaultRendererPrivate::renderWithFactory(const QMetaObject *mo, const MessagePart::Ptr &msgPart, HtmlWriter *htmlWriter) { if (!mRendererFactory) { return false; } for (auto r : mRendererFactory->renderersForPart(mo, msgPart)) { if (r->render(msgPart, htmlWriter, this)) { return true; } } return false; } void DefaultRendererPrivate::renderFactory(const MessagePart::Ptr &msgPart, HtmlWriter *htmlWriter) { const QString className = QString::fromUtf8(msgPart->metaObject()->className()); if (isHiddenHint(msgPart)) { QByteArray cid = msgPart->content()->contentID()->identifier(); auto mp = msgPart.dynamicCast(); if (!cid.isEmpty() && mp) { QString fileName = mp->temporaryFilePath(); QString href = QUrl::fromLocalFile(fileName).url(); htmlWriter->embedPart(cid, href); } } if (renderWithFactory(msgPart, htmlWriter)) { return; } if (className == QLatin1String("MimeTreeParser::MessagePartList")) { auto mp = msgPart.dynamicCast(); if (mp) { render(mp, htmlWriter); } } else if (className == QLatin1String("MimeTreeParser::MimeMessagePart")) { auto mp = msgPart.dynamicCast(); if (mp) { render(mp, htmlWriter); } } else if (className == QLatin1String("MimeTreeParser::EncapsulatedRfc822MessagePart")) { auto mp = msgPart.dynamicCast(); if (mp) { render(mp, htmlWriter); } } else if (className == QLatin1String("MimeTreeParser::HtmlMessagePart")) { auto mp = msgPart.dynamicCast(); if (mp) { render(mp, htmlWriter); } } else if (className == QLatin1String("MimeTreeParser::SignedMessagePart")) { auto mp = msgPart.dynamicCast(); if (mp) { render(mp, htmlWriter); } } else if (className == QLatin1String("MimeTreeParser::EncryptedMessagePart")) { auto mp = msgPart.dynamicCast(); if (mp) { render(mp, htmlWriter); } } else if (className == QLatin1String("MimeTreeParser::AlternativeMessagePart")) { auto mp = msgPart.dynamicCast(); if (mp) { render(mp, htmlWriter); } } else if (className == QLatin1String("MimeTreeParser::CertMessagePart")) { auto mp = msgPart.dynamicCast(); if (mp) { render(mp, htmlWriter); } } else { qCWarning(MESSAGEVIEWER_LOG) << "We got a unknown classname, using default behaviour for " << className; } } bool DefaultRendererPrivate::isHiddenHint(const MimeTreeParser::MessagePart::Ptr &msgPart) { auto mp = msgPart.dynamicCast(); auto content = msgPart->content(); if (!mp || !content) { return false; } if (mShowOnlyOneMimePart && mMsgPart.data() == msgPart->parentPart()) { if (mMsgPart->subParts().at(0) == msgPart.data()) { return false; } } if (msgPart->nodeHelper()->isNodeDisplayedHidden(content)) { return true; } const AttachmentStrategy *const as = mAttachmentStrategy; const bool defaultHidden(as && as->defaultDisplay(content) == AttachmentStrategy::None); auto preferredMode = source()->preferredMode(); bool isHtmlPreferred = (preferredMode == MimeTreeParser::Util::Html) || (preferredMode == MimeTreeParser::Util::MultipartHtml); QByteArray mediaType("text"); if (content->contentType(false) && !content->contentType()->mediaType().isEmpty() && !content->contentType()->subType().isEmpty()) { mediaType = content->contentType()->mediaType(); } const bool isTextPart = (mediaType == QByteArrayLiteral("text")); bool defaultAsIcon = true; if (!mp->neverDisplayInline()) { if (as) { defaultAsIcon = as->defaultDisplay(content) == AttachmentStrategy::AsIcon; } } // neither image nor text -> show as icon if (!mp->isImage() && !isTextPart) { defaultAsIcon = true; } bool hidden(false); if (isTextPart) { hidden = defaultHidden; } else { if (mp->isImage() && isHtmlPreferred && content->parent() && content->parent()->contentType(false)->subType() == "related") { hidden = true; } else { hidden = defaultHidden && content->parent(); hidden |= defaultAsIcon && defaultHidden; } } msgPart->nodeHelper()->setNodeDisplayedHidden(content, hidden); return hidden; } MimeTreeParser::IconType DefaultRendererPrivate::displayHint(const MimeTreeParser::MessagePart::Ptr &msgPart) { auto mp = msgPart.dynamicCast(); auto content = msgPart->content(); if (!content || !mp) { return MimeTreeParser::IconType::NoIcon; } const AttachmentStrategy *const as = mAttachmentStrategy; const bool defaultDisplayHidden(as && as->defaultDisplay(content) == AttachmentStrategy::None); const bool defaultDisplayInline(as && as->defaultDisplay(content) == AttachmentStrategy::Inline); const bool defaultDisplayAsIcon(as && as->defaultDisplay(content) == AttachmentStrategy::AsIcon); const bool showOnlyOneMimePart(mShowOnlyOneMimePart); auto preferredMode = source()->preferredMode(); bool isHtmlPreferred = (preferredMode == MimeTreeParser::Util::Html) || (preferredMode == MimeTreeParser::Util::MultipartHtml); QByteArray mediaType("text"); if (content->contentType(false) && !content->contentType()->mediaType().isEmpty() && !content->contentType()->subType().isEmpty()) { mediaType = content->contentType()->mediaType(); } const bool isTextPart = (mediaType == QByteArrayLiteral("text")); bool defaultAsIcon = true; if (!mp->neverDisplayInline()) { if (as) { defaultAsIcon = defaultDisplayAsIcon; } } if (mp->isImage() && showOnlyOneMimePart && !mp->neverDisplayInline()) { defaultAsIcon = false; } // neither image nor text -> show as icon if (!mp->isImage() && !isTextPart) { defaultAsIcon = true; } if (isTextPart) { if (as && !defaultDisplayInline) { return MimeTreeParser::IconExternal; } return MimeTreeParser::NoIcon; } else { if (mp->isImage() && isHtmlPreferred && content->parent() && content->parent()->contentType(false)->subType() == "related") { return MimeTreeParser::IconInline; } if (defaultDisplayHidden && !showOnlyOneMimePart && content->parent()) { return MimeTreeParser::IconInline; } if (defaultAsIcon) { return MimeTreeParser::IconExternal; } else if (mp->isImage()) { return MimeTreeParser::IconInline; } } return MimeTreeParser::NoIcon; } bool DefaultRendererPrivate::showEmoticons() const { return mShowEmoticons; } bool DefaultRendererPrivate::isPrinting() const { return mIsPrinting; } bool DefaultRendererPrivate::htmlLoadExternal() const { return mHtmlLoadExternal; } bool DefaultRendererPrivate::showExpandQuotesMark() const { return mShowExpandQuotesMark; } bool DefaultRendererPrivate::showOnlyOneMimePart() const { return mShowOnlyOneMimePart; } bool DefaultRendererPrivate::showSignatureDetails() const { return mShowSignatureDetails; } bool DefaultRendererPrivate::showEncryptionDetails() const { return mShowEncryptionDetails; } int DefaultRendererPrivate::levelQuote() const { return mLevelQuote; } DefaultRenderer::DefaultRenderer(CSSHelperBase *cssHelper) : d(new DefaultRendererPrivate(cssHelper, MessagePartRendererFactory::instance())) { } DefaultRenderer::~DefaultRenderer() { delete d; } void DefaultRenderer::setShowOnlyOneMimePart(bool onlyOneMimePart) { d->mShowOnlyOneMimePart = onlyOneMimePart; } void DefaultRenderer::setAttachmentStrategy(const AttachmentStrategy *strategy) { d->mAttachmentStrategy = strategy; } void DefaultRenderer::setShowEmoticons(bool showEmoticons) { d->mShowEmoticons = showEmoticons; } void DefaultRenderer::setIsPrinting(bool isPrinting) { d->mIsPrinting = isPrinting; } void DefaultRenderer::setShowExpandQuotesMark(bool showExpandQuotesMark) { d->mShowExpandQuotesMark = showExpandQuotesMark; } void DefaultRenderer::setShowEncryptionDetails(bool showEncryptionDetails) { d->mShowEncryptionDetails = showEncryptionDetails; } void DefaultRenderer::setShowSignatureDetails(bool showSignatureDetails) { d->mShowSignatureDetails = showSignatureDetails; } void DefaultRenderer::setLevelQuote(int levelQuote) { d->mLevelQuote = levelQuote; } void DefaultRenderer::setHtmlLoadExternal(bool htmlLoadExternal) { d->mHtmlLoadExternal = htmlLoadExternal; } void DefaultRenderer::setCreateMessageHeader(const std::function &createMessageHeader) { d->mCreateMessageHeader = createMessageHeader; } QString renderTreeHelper(const MimeTreeParser::MessagePart::Ptr &messagePart, QString indent) { QString ret = QStringLiteral("%1 * %3\n").arg(indent, QString::fromUtf8(messagePart->metaObject()->className())); indent += QLatin1Char(' '); for (const auto &subPart : messagePart->subParts()) { ret += renderTreeHelper(subPart, indent); } return ret; } void DefaultRenderer::render(const MimeTreeParser::MessagePart::Ptr &msgPart, HtmlWriter *writer) { qCDebug(MESSAGEVIEWER_LOG) << "MimeTreeParser structure:"; qCDebug(MESSAGEVIEWER_LOG) << qPrintable(renderTreeHelper(msgPart, QString())); d->mMsgPart = msgPart; d->renderFactory(d->mMsgPart, writer); } diff --git a/messageviewer/src/viewer/viewer_p.h b/messageviewer/src/viewer/viewer_p.h index 9735d2c3..3be398f4 100644 --- a/messageviewer/src/viewer/viewer_p.h +++ b/messageviewer/src/viewer/viewer_p.h @@ -1,719 +1,718 @@ /* Copyright (c) 1997 Markus Wuebben Copyright (C) 2009 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net Copyright (c) 2009 Andras Mantia This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MAILVIEWER_P_H #define MAILVIEWER_P_H #include "messageviewer_private_export.h" #include "config-messageviewer.h" #include #include "viewer.h" //not so nice, it is actually for the enums from MailViewer #include "PimCommon/ShareServiceUrlManager" #include "messageviewer/viewerplugininterface.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace KIO { class Job; } class QAction; class KActionCollection; class KSelectAction; class KToggleAction; class QMenu; class KActionMenu; class QPoint; class QSplitter; class QModelIndex; class QPrinter; namespace KPIMTextEdit { class SlideContainer; class TextToSpeechWidget; } namespace PimCommon { class ShareServiceUrlManager; } namespace MimeTreeParser { class ObjectTreeParser; } #ifdef USE_DKIM_CHECKER namespace QCA { class Initializer; } #endif namespace WebEngineViewer { class WebHitTestResult; class FindBarWebEngineView; class ZoomActionMenu; class LocalDataBaseManager; } namespace MessageViewer { class AttachmentStrategy; class HeaderStylePlugin; class HtmlWriter; class CSSHelper; class MailWebEngineView; class WebEnginePartHtmlWriter; class HtmlStatusBar; class ScamDetectionWarningWidget; class MimePartTreeView; class OpenAttachmentFolderWidget; class HeaderStyleMenuManager; class ViewerPluginToolManager; class ViewerPluginInterface; class SubmittedFormWarningWidget; class MailSourceWebEngineViewer; class MailTrackingWarningWidget; class ShowNextMessageWidget; /** \brief Private class for the Viewer, the main widget in the messageviewer library. This class creates all subwidgets, like the MailWebView, the HtmlStatusBar and the FindBarMailWebView. Also, ViewerPrivate creates and exposes all actions. \par Displaying a message Before displaying a message, a message needs to be set. This can be done in two ways, with setMessageItem() and with setMessage(). setMessageItem() is the preferred way, as the viewer can then remember the Akonadi::Item belonging to the message. The Akonadi::Item is needed when modifying the message, for example when editing or deleting an attachment. Sometimes passing an Akonadi::Item to the viewer is not possible, for example when double-clicking an attached message, in which case a new KMime::Message is constructed out of the attachment, and a separate window is opened for it. In this case, the KMime::Message has no associated Akonadi::Item. If there is an Akonadi::Item available, it will be monitored for changes and the viewer automatically updated on external changes. Once a message is set, update() is called. update() can also be called after the message has already been displayed. As an example, this is the case when the user decides to decrypt the message. The decryption can happen async, and once the decryption is finished, update() is called to display the now decrypted content. See the documentation of MimeTreeParser::ObjectTreeParser on how exactly decryption is handled. update() is just a thin wrapper that calls updateReaderWin(). The only difference is that update() has a timer that prevents too many slow calls to updateReaderWin() in a short time frame. updateReaderWin() again is only a thin wrapper that resets some state and then calls displayMessage(). displayMessage() itself is again a thin wrapper, which starts the HtmlWriter and then calls parseMsg(). Finally, parseMsg() does the real work. It uses MimeTreeParser::ObjectTreeParser parseObjectTree() to let the MimeTreeParser::ObjectTreeParser parse the message and generate the HTML code for it. As mentioned before, it can happen that the MimeTreeParser::ObjectTreeParser needs to do some operation that happens async, for example decrypting. In this case, the MimeTreeParser::ObjectTreeParser will create a BodyPartMemento, which basically is a wrapper around the job that does the async operation. Once the async operation is finished. the BodyPartMemento will trigger an update() of ViewerPrivate, so that MimeTreeParser::ObjectTreeParser parseObjectTree() gets called again and the MimeTreeParser::ObjectTreeParser then can generate HTML which has the decrypted content of the message. Again, see the documentation of MimeTreeParser::ObjectTreeParser for the details. Additionally, parseMsg() does some evil hack for saving unencrypted messages should the config option for that be set. \par Displaying a MIME part of the message The viewer can show only a part of the message, for example by clicking on a MIME part in the message structure viewer or by double-clicking an attached message. In this case, setMessagePart() is called. There are two of these functions. One even has special handling for images, special handling for binary attachments and special handling of attached messages. In the last case, a new KMime::Message is constructed and set as the main message with setMessage(). \par Attachment Handling Some of those actions are actions that operate on a single attachment. For those, there is usually a slot, like slotAttachmentCopy(). These actions are triggered from the attachment context menu, which is shown in showAttachmentPopup(). The actions are connected to slotHandleAttachment() when they are activated. The action to edit an attachment uses the EditorWatcher to detect when editing with an external editor is finished. Upon finishing, slotAttachmentEditDone() is called, which then creates an ItemModifyJob to store the changes of the attachment. A map of currently active EditorWatcher and their KMime::Content is available in mEditorWatchers. For most attachment actions, the attachment is first written to a temp file. The action is then executed on this temp file. Writing the attachment to a temp file is done with MimeTreeParser::NodeHelper::writeNodeToTempFile(). This method is called before opening or copying an attachment or when rendering the attachment list. The MimeTreeParser::ObjectTreeParser also calls MimeTreeParser::NodeHelper::writeNodeToTempFile() in some places. Once the temp file is written, MimeTreeParser::NodeHelper::tempFileUrlFromNode() can be used to get the file name of the temp file for a specific MIME part. This is for example used by the handler for 'attachment:' URLs, AttachmentURLHandler. Since URLs for attachments are in the "attachment:" scheme, dragging them as-is to outside applications wouldn't work, since other applications don't understand this scheme. Therefore, the viewer has special handling for dragging URLs: In eventFilter(), drags are detected, and the URL handler is called to deal with the drag. The attachment URL handler then starts a drag with the file:// URL of the temp file of the attachment, which it gets with MimeTreeParser::NodeHelper::tempFileUrlFromNode(). TODO: How are attachment handled that are loaded on demand? How does prepareHandleAttachment() work? TODO: This temp file handling is a big mess and could use a rewrite, especially in the face of load on demand. There shouldn't be the need to write out tempfiles until really needed. Some header styles display an attachment list in the header. The HTML code for the attachment list cannot be generated by the HeaderStyle itself, since that does not know about all attachments. Therefore, the attachment list needs to be created by ViewerPrivate. For this, the HeaderStyle writes out a placeholder for the attachment list when it creates the HTML for the header. Once the MimeTreeParser::ObjectTreeParser is finished with the message, injectAttachments() is called. injectAttachments() searches for the placeholder and replaces that with the real HTML code for the attachments. One of the attachment actions is to scoll to the attachment. That action is only available when right-clicking the header. The action scrolls to the attachment in the body and draws a yellow frame around the attachment. This is done in scrollToAttachment(). The attachment in the body and the div which is used for the colored frame are both created by the MimeTreeParser::ObjectTreeParser . \par Misc ViewerPrivate holds the MimeTreeParser::NodeHelper, which is passed on to the MimeTreeParser::ObjectTreeParser when it needs it. It also holds the HeaderStyle, HeaderStrategy, MimeTreeParser::AttachmentStrategy, CSSHelper, HtmlWriter and more, some of them again passed to the MimeTreeParser::ObjectTreeParser when it needs it. @author andras@kdab.net */ class MESSAGEVIEWER_TESTS_EXPORT ViewerPrivate : public QObject { Q_OBJECT public: ViewerPrivate(Viewer *aParent, QWidget *mainWindow, KActionCollection *actionCollection); ~ViewerPrivate() override; /** Returns message part from given URL or null if invalid. The URL's path is a KMime::ContentIndex path, or an index for the extra nodes, followed by : and the ContentIndex path. */ KMime::Content *nodeFromUrl(const QUrl &url) const; /** Open the attachment pointed to the node. * @param node the node * @param url - if not empty, use this file to load the attachment content */ void openAttachment(KMime::Content *node, const QUrl &url); /** Delete the attachment the @p node points to. Returns false if the user cancelled the deletion, true in all other cases (including failure to delete the attachment!) * @param node the node * @param showWarning whether some warning should be shown */ bool deleteAttachment(KMime::Content *node, bool showWarning = true); void attachmentProperties(KMime::Content *node); void attachmentCopy(const KMime::Content::List &contents); void scrollToAnchor(const QString &anchor); void showAttachmentPopup(KMime::Content *node, const QString &name, const QPoint &p); /** * Sets the current attachment ID and the current attachment temporary filename * to the given values. * Call this so that slotHandleAttachment() knows which attachment to handle. */ void prepareHandleAttachment(KMime::Content *node); QString createAtmFileLink(const QString &atmFileName) const; KService::Ptr getServiceOffer(KMime::Content *content); KMime::Content::List selectedContents(); void attachmentOpenWith(KMime::Content *node, const KService::Ptr &offer = KService::Ptr()); void attachmentOpen(KMime::Content *node); /** Return the HtmlWriter connected to the MailWebView we use */ HtmlWriter *htmlWriter() const; HeaderStylePlugin *headerStylePlugin() const; CSSHelper *cssHelper() const; MimeTreeParser::NodeHelper *nodeHelper() const; Viewer *viewer() const; Akonadi::Item messageItem() const; KMime::Message::Ptr message() const; /** Returns whether the message should be decrypted. */ bool decryptMessage() const; /** Display a generic HTML splash page instead of a message. */ void displaySplashPage(const QString &templateName, const QVariantHash &data, const QByteArray &domain = QByteArray()); void displaySplashPage(const QString &message); /** Enable the displaying of messages again after an splash (or other) page was displayed */ void enableMessageDisplay(); /** Feeds the HTML viewer with the contents of the given message. HTML begin/end parts are written around the message. */ void displayMessage(); /** Parse the given content and generate HTML out of it for display */ void parseContent(KMime::Content *content); /** Creates a nice mail header depending on the current selected header style. */ QString writeMessageHeader(KMime::Message *aMsg, KMime::Content *vCardNode = nullptr, bool topLevel = false); /** show window containing information about a vCard. */ void showVCard(KMime::Content *msgPart); void saveMainFrameScreenshotInFile(const QString &filename); private: /** HTML initialization. */ void initHtmlWidget(); void createOpenWithMenu(QMenu *topMenu, const QString &contentTypeStr, bool fromCurrentContent); public: void itemFetchResult(KJob *job); /** Read settings from app's config file. */ void readConfig(); /** Write settings to app's config file. Calls sync() if withSync is true. */ void writeConfig(bool withSync = true); /** Get/set the message attachment strategy. */ const AttachmentStrategy *attachmentStrategy() const; void setAttachmentStrategy(const AttachmentStrategy *strategy); /** Get selected override character encoding. @return The encoding selected by the user or an empty string if auto-detection is selected. */ QString overrideEncoding() const; /** Set the override character encoding. */ void setOverrideEncoding(const QString &encoding); /** Set printing mode */ void setPrinting(bool enable); bool printingMode() const; /** Print message. */ void printMessage(const Akonadi::Item &msg); void printPreviewMessage(const Akonadi::Item &message); void resetStateForNewMessage(); void setMessageInternal(const KMime::Message::Ptr &message, MimeTreeParser::UpdateMode updateMode); /** Set the Akonadi item that will be displayed. * @param item - the Akonadi item to be displayed. If it doesn't hold a mail (KMime::Message::Ptr as payload data), * an empty page is shown. * @param updateMode - update the display immediately or not. See MailViewer::UpdateMode. */ void setMessageItem(const Akonadi::Item &item, MimeTreeParser::UpdateMode updateMode = MimeTreeParser::Delayed); /** Set the message that shall be shown. * @param msg - the message to be shown. If 0, an empty page is displayed. * @param updateMode - update the display immediately or not. See MailViewer::UpdateMode. */ void setMessage(const KMime::Message::Ptr &msg, MimeTreeParser::UpdateMode updateMode = MimeTreeParser::Delayed); /** Instead of settings a message to be shown sets a message part to be shown */ void setMessagePart(KMime::Content *node); /** Show or hide the Mime Tree Viewer if configuration is set to smart mode. */ void showHideMimeTree(); /** View message part of type message/RFC822 in extra viewer window. */ void atmViewMsg(const KMime::Message::Ptr &message); void adjustLayout(); void createWidgets(); void createActions(); void showContextMenu(KMime::Content *content, const QPoint &point); KToggleAction *actionForAttachmentStrategy(const AttachmentStrategy *); /** Read override codec from configuration */ void readGlobalOverrideCodec(); /** Get codec corresponding to the currently selected override character encoding. @return The override codec or 0 if auto-detection is selected. */ const QTextCodec *overrideCodec() const; QString renderAttachments(KMime::Content *node, const QColor &bgColor) const; KMime::Content *findContentByType(KMime::Content *content, const QByteArray &type); //TODO(Andras) move to MimeTreeParser::NodeHelper /** Return a QTextCodec for the specified charset. * This function is a bit more tolerant, than QTextCodec::codecForName */ static const QTextCodec *codecForName(const QByteArray &_str); //TODO(Andras) move to a utility class? /** Saves the relative position of the scroll view. Call this before calling update() if you want to preserve the current view. */ void saveRelativePosition(); bool htmlMail() const; bool htmlLoadExternal() const; bool htmlMailGlobalSetting() const; /** Get the html override setting */ Viewer::DisplayFormatMessage displayFormatMessageOverwrite() const; /** Override default html mail setting */ void setDisplayFormatMessageOverwrite(Viewer::DisplayFormatMessage format); /** Get the load external references override setting */ bool htmlLoadExtOverride() const; /** Default behavior for loading external references. * Use this for specifying the external reference loading behavior as * specified in the user settings. * @see setHtmlLoadExtOverride */ void setHtmlLoadExtDefault(bool loadExtDefault); /** Override default load external references setting * @warning This must only be called when the user has explicitly * been asked to retrieve external references! * @see setHtmlLoadExtDefault */ void setHtmlLoadExtOverride(bool loadExtOverride); /** Enforce message decryption. */ void setDecryptMessageOverwrite(bool overwrite = true); /** Show signature details. */ bool showSignatureDetails() const; /** Show signature details. */ void setShowSignatureDetails(bool showDetails = true); /* show or hide encryption details */ void setShowEncryptionDetails(bool showEncDetails); bool showEncryptionDetails() const; void scrollToAttachment(KMime::Content *node); void setUseFixedFont(bool useFixedFont); void attachmentView(KMime::Content *atmNode); void setZoomFactor(qreal zoomFactor); void goOnline(); void goResourceOnline(); void showOpenAttachmentFolderWidget(const QList &urls); bool mimePartTreeIsEmpty() const; void setPluginName(const QString &pluginName); QList viewerPluginActionList( MessageViewer::ViewerPluginInterface::SpecificFeatureTypes features); QList interceptorUrlActions(const WebEngineViewer::WebHitTestResult &result) const; void setPrintElementBackground(bool printElementBackground); bool showEmoticons() const; void checkPhishingUrl(); void executeRunner(const QUrl &url); QUrl imageUrl() const; Q_REQUIRED_RESULT qreal webViewZoomFactor() const; void setWebViewZoomFactor(qreal factor); void recreateCssHelper(); void hasMultiMessages(bool messages); void updateShowMultiMessagesButton(bool enablePreviousButton, bool enableNextButton); private Q_SLOTS: void slotActivatePlugin(MessageViewer::ViewerPluginInterface *interface); void slotModifyItemDone(KJob *job); void slotMessageMayBeAScam(); void slotMessageIsNotAScam(); void slotAddToWhiteList(); void slotFormSubmittedForbidden(); void slotMailTrackingFound(const MessageViewer::BlockMailTrackingUrlInterceptor::MailTrackerBlackList &blacklist); void slotItemChanged(const Akonadi::Item &item, const QSet &partIdentifiers); void slotItemMoved(const Akonadi::Item &, const Akonadi::Collection &, const Akonadi::Collection &); void itemModifiedResult(KJob *job); void slotClear(); void slotMessageRendered(); void slotOpenWithAction(QAction *act); void slotOpenWithActionCurrentContent(QAction *act); void slotOpenWithDialog(); void slotOpenWithDialogCurrentContent(); void saveSplitterSizes() const; void slotRefreshMessage(const Akonadi::Item &item); void slotServiceUrlSelected(PimCommon::ShareServiceUrlManager::ServiceType serviceType); void slotStyleChanged(MessageViewer::HeaderStylePlugin *plugin); void slotStyleUpdated(); void slotWheelZoomChanged(int numSteps); void slotOpenInBrowser(); void slotExportHtmlPageFailed(); void slotExportHtmlPageSuccess(const QString &filename); void slotHandlePagePrinted(bool result); void slotToggleEmoticons(); public Q_SLOTS: /** An URL has been activate with a click. */ void slotUrlOpen(const QUrl &url = QUrl()); void slotOpenUrl(); /** The mouse has moved on or off an URL. */ void slotUrlOn(const QString &link); /** The user presses the right mouse button on an URL. */ void slotUrlPopup(const WebEngineViewer::WebHitTestResult &result); /** The user selected "Find" from the menu. */ void slotFind(); /** The user toggled the "Fixed Font" flag from the view menu. */ void slotToggleFixedFont(); void slotToggleMimePartTree(); /** Show the message source */ void slotShowMessageSource(); /** Refresh the reader window */ void updateReaderWin(); void slotMimePartSelected(const QModelIndex &index); void slotIconicAttachments(); void slotSmartAttachments(); void slotInlineAttachments(); void slotHideAttachments(); void slotHeaderOnlyAttachments(); /** Some attachment operations. */ void slotDelayedResize(); /** Print message. Called on as a response of finished() signal of mPartHtmlWriter after rendering is finished. In the very end it deletes the KMReaderWin window that was created for the purpose of rendering. */ void slotPrintMessage(); void slotPrintPreview(); void slotSetEncoding(); void executeCustomScriptsAfterLoading(); void slotSettingsChanged(); void slotMimeTreeContextMenuRequested(const QPoint &pos); void slotAttachmentOpenWith(); void slotAttachmentOpen(); void slotAttachmentSaveAs(); void slotAttachmentSaveAll(); void slotAttachmentView(); void slotAttachmentProperties(); void slotAttachmentCopy(); void slotLevelQuote(int l); /** Toggle display mode between HTML and plain text. */ void slotToggleHtmlMode(); void slotLoadExternalReference(); /** * Does an action for the current attachment. * The action is defined by the KMHandleAttachmentCommand::AttachmentAction * enum. * prepareHandleAttachment() needs to be called before calling this to set the * correct attachment ID. */ void slotHandleAttachment(int action); /** Copy the selected text to the clipboard */ void slotCopySelectedText(); void viewerSelectionChanged(); /** Select message body. */ void selectAll(); /** Copy URL in mUrlCurrent to clipboard. Removes "mailto:" at beginning of URL before copying. */ void slotUrlCopy(); void slotSaveMessage(); /** Re-parse the current message. */ void update(MimeTreeParser::UpdateMode updateMode = MimeTreeParser::Delayed); void slotSpeakText(); void slotCopyImageLocation(); void slotSaveMessageDisplayFormat(); void slotResetMessageDisplayFormat(); void slotGeneralFontChanged(); Q_SIGNALS: void showStatusBarMessage(const QString &message); void popupMenu(const Akonadi::Item &msg, const QUrl &url, const QUrl &imageUrl, const QPoint &mousePos); void displayPopupMenu(const Akonadi::Item &msg, const WebEngineViewer::WebHitTestResult &result, const QPoint &mousePos); void urlClicked(const Akonadi::Item &msg, const QUrl &url); void requestConfigSync(); void showReader(KMime::Content *aMsgPart, bool aHTML, const QString &encoding); void showMessage(const KMime::Message::Ptr &message, const QString &encoding); void replyMessageTo(const KMime::Message::Ptr &message, bool replyToAll); void itemRemoved(); void makeResourceOnline(MessageViewer::Viewer::ResourceOnlineMode mode); void changeDisplayMail(Viewer::DisplayFormatMessage, bool); void moveMessageToTrash(); void pageIsScrolledToBottom(bool); void printingFinished(); void zoomChanged(qreal zoomFactor); void showNextMessage(); void showPreviousMessage(); private: QString attachmentHtml() const; Akonadi::Relation relatedNoteRelation() const; void addHelpTextAction(QAction *act, const QString &text); void readGravatarConfig(); void replyMessageToAuthor(KMime::Content *atmNode); void replyMessageToAll(KMime::Content *atmNode); bool urlIsAMalwareButContinue(); void slotCheckedUrlFinished(const QUrl &url, WebEngineViewer::CheckPhishingUrlUtil::UrlStatus status); void slotDelayPrintPreview(); void applyZoomValue(qreal factor, bool saveConfig = true); void slotZoomChanged(qreal zoom); MimeTreeParser::NodeHelper *mNodeHelper = nullptr; bool mHtmlMailGlobalSetting; bool mHtmlLoadExternalDefaultSetting; bool mHtmlLoadExtOverride; public: KMime::Message::Ptr mMessage; //the current message, if it was set manually Akonadi::Item mMessageItem; //the message item from Akonadi // widgets: QSplitter *mSplitter = nullptr; QWidget *mBox = nullptr; HtmlStatusBar *mColorBar = nullptr; #ifndef QT_NO_TREEVIEW MimePartTreeView *mMimePartTree = nullptr; #endif MailWebEngineView *mViewer = nullptr; WebEngineViewer::FindBarWebEngineView *mFindBar = nullptr; const AttachmentStrategy *mAttachmentStrategy = nullptr; QTimer mUpdateReaderWinTimer; QTimer mResizeTimer; QString mOverrideEncoding; QString mOldGlobalOverrideEncoding; // used to detect changes of the global override character encoding QString mPicsPath; /// This is true if the viewer currently is displaying a message. Can be false, for example when /// the splash/busy page is displayed. bool mMsgDisplay; CSSHelper *mCSSHelper = nullptr; bool mUseFixedFont; bool mPrinting; QWidget *mMainWindow = nullptr; KActionCollection *mActionCollection = nullptr; QAction *mCopyAction = nullptr; QAction *mCopyURLAction = nullptr; QAction *mUrlOpenAction = nullptr; QAction *mSelectAllAction = nullptr; QAction *mScrollUpAction = nullptr; QAction *mScrollDownAction = nullptr; QAction *mScrollUpMoreAction = nullptr; QAction *mScrollDownMoreAction = nullptr; QAction *mViewSourceAction = nullptr; QAction *mSaveMessageAction = nullptr; QAction *mFindInMessageAction = nullptr; QAction *mSaveMessageDisplayFormat = nullptr; QAction *mResetMessageDisplayFormat = nullptr; KToggleAction *mDisableEmoticonAction = nullptr; KToggleAction *mHeaderOnlyAttachmentsAction = nullptr; KSelectAction *mSelectEncodingAction = nullptr; KToggleAction *mToggleFixFontAction = nullptr; KToggleAction *mToggleDisplayModeAction = nullptr; KToggleAction *mToggleMimePartTreeAction = nullptr; QAction *mSpeakTextAction = nullptr; QAction *mCopyImageLocation = nullptr; QUrl mHoveredUrl; QUrl mClickedUrl; QUrl mImageUrl; QPoint mLastClickPosition; bool mCanStartDrag; HtmlWriter *mHtmlWriter = nullptr; /** Used only to be able to connect and disconnect finished() signal in printMsg() and slotPrintMsg() since mHtmlWriter points only to abstract non-QObject class. */ QPointer mPartHtmlWriter; int mLevelQuote; bool mDecrytMessageOverwrite = false; bool mShowSignatureDetails = false; bool mShowEncryptionDetails = false; bool mForceEmoticons = true; int mRecursionCountForDisplayMessage; KMime::Content *mCurrentContent = nullptr; KMime::Content *mMessagePartNode = nullptr; QString mMessagePath; QColor mForegroundError; QColor mBackgroundError; Viewer *const q; Akonadi::Session *mSession = nullptr; Akonadi::Monitor mMonitor; QSet mMessageLoadedHandlers; Akonadi::Item::Id mPreviouslyViewedItem; MessageViewer::ScamDetectionWarningWidget *mScamDetectionWarning = nullptr; MessageViewer::OpenAttachmentFolderWidget *mOpenAttachmentFolderWidget = nullptr; MessageViewer::SubmittedFormWarningWidget *mSubmittedFormWarning = nullptr; MessageViewer::MailTrackingWarningWidget *mMailTrackingWarning = nullptr; KPIMTextEdit::TextToSpeechWidget *mTextToSpeechWidget = nullptr; Viewer::DisplayFormatMessage mDisplayFormatMessageOverwrite; KPIMTextEdit::SlideContainer *mSliderContainer = nullptr; PimCommon::ShareServiceUrlManager *mShareServiceManager = nullptr; KActionMenu *mShareServiceUrlMenu = nullptr; MessageViewer::HeaderStylePlugin *mHeaderStylePlugin = nullptr; MessageViewer::HeaderStyleMenuManager *mHeaderStyleMenuManager = nullptr; MessageViewer::ViewerPluginToolManager *mViewerPluginToolManager = nullptr; WebEngineViewer::ZoomActionMenu *mZoomActionMenu = nullptr; QPrinter *mCurrentPrinter = nullptr; QList > mListMailSourceViewer; WebEngineViewer::LocalDataBaseManager *mPhishingDatabase = nullptr; MessageViewer::ShowNextMessageWidget *mShowNextMessageWidget = nullptr; #ifdef USE_DKIM_CHECKER QCA::Initializer *mQcaInitializer = nullptr; #endif - }; } #endif