diff --git a/src/core/messagepart.cpp b/src/core/messagepart.cpp index 55f79b6..b3747de 100644 --- a/src/core/messagepart.cpp +++ b/src/core/messagepart.cpp @@ -1,983 +1,975 @@ // SPDX-FileCopyrightText: 2015 Sandro Knauß <knauss@kolabsys.com> // SPDX-FileCopyrightText: 2017 Christian Mollekopf <mollekopf@kolabsys.com> // SPDX-License-Identifier: LGPL-2.0-or-later #include "messagepart.h" #include "cryptohelper.h" #include "mimetreeparser_core_debug.h" #include "objecttreeparser.h" #include "utils.h" #include <KLocalizedString> #include <KMime/Content> #include <Libkleo/Compliance> #include <Libkleo/KeyCache> #include <QGpgME/DN> #include <QGpgME/DecryptVerifyJob> #include <QGpgME/Protocol> #include <QGpgME/VerifyDetachedJob> #include <QGpgME/VerifyOpaqueJob> #include <QStringDecoder> #include <gpgme++/key.h> #include <gpgme++/keylistresult.h> #include <gpgme.h> #include <libkleo/formatting.h> using namespace MimeTreeParser; //------MessagePart----------------------- MessagePart::MessagePart(ObjectTreeParser *otp, const QString &text, KMime::Content *node) : mText(text) , mOtp(otp) , mParentPart(nullptr) , mNode(node) // only null for messagepartlist , mError(NoError) , mRoot(false) { } MessagePart::~MessagePart() { for (auto n : std::as_const(mNodesToDelete)) { delete n; } } MessagePart::Disposition MessagePart::disposition() const { if (!mNode) { return Invalid; } const auto cd = mNode->contentDisposition(false); if (!cd) { return Invalid; } switch (cd->disposition()) { case KMime::Headers::CDinline: return Inline; case KMime::Headers::CDattachment: return Attachment; default: return Invalid; } } QString MessagePart::filename() const { if (!mNode) { return {}; } if (const auto cd = mNode->contentDisposition(false)) { const auto name = cd->filename(); // Allow for a fallback for mails that have a ContentDisposition header, but don't set the filename anyways. // Not the recommended way, but exists. if (!name.isEmpty()) { return name; } } if (const auto ct = mNode->contentType(false)) { return ct->name(); } return {}; } static KMime::Headers::ContentType *contentType(KMime::Content *node) { if (node) { return node->contentType(false); } return nullptr; } QByteArray MessagePart::charset() const { if (!mNode) { return QByteArrayLiteral("us-ascii"); } if (auto ct = contentType(mNode)) { return ct->charset(); } // Per rfc2045 us-ascii is the default return QByteArrayLiteral("us-ascii"); } QByteArray MessagePart::mimeType() const { if (auto ct = contentType(mNode)) { return ct->mimeType(); } return {}; } bool MessagePart::isText() const { if (auto ct = contentType(mNode)) { return ct->isText(); } return false; } MessagePart::Error MessagePart::error() const { return mError; } QString MessagePart::errorString() const { return mMetaData.errorText; } PartMetaData *MessagePart::partMetaData() { return &mMetaData; } bool MessagePart::isAttachment() const { if (mNode) { return KMime::isAttachment(mNode); } return false; } KMime::Content *MessagePart::node() const { return mNode; } void MessagePart::setIsRoot(bool root) { mRoot = root; } bool MessagePart::isRoot() const { return mRoot; } QString MessagePart::text() const { return mText; } void MessagePart::setText(const QString &text) { mText = text; } bool MessagePart::isHtml() const { return false; } MessagePart *MessagePart::parentPart() const { return mParentPart; } void MessagePart::setParentPart(MessagePart *parentPart) { mParentPart = parentPart; } QString MessagePart::htmlContent() const { return text(); } QString MessagePart::plaintextContent() const { return text(); } void MessagePart::parseInternal(KMime::Content *node, bool onlyOneMimePart) { auto subMessagePart = mOtp->parseObjectTreeInternal(node, onlyOneMimePart); mRoot = subMessagePart->isRoot(); for (const auto &part : std::as_const(subMessagePart->subParts())) { appendSubPart(part); } } void MessagePart::parseInternal(const QByteArray &data) { auto tempNode = new KMime::Content(); const auto lfData = KMime::CRLFtoLF(data); // We have to deal with both bodies and full parts. In inline encrypted/signed parts we can have nested parts, // or just plain-text, and both ends up here. setContent defaults to setting only the header, so we have to avoid this. if (lfData.contains("\n\n")) { tempNode->setContent(lfData); } else { tempNode->setBody(lfData); } tempNode->parse(); tempNode->contentType()->setCharset(charset()); bindLifetime(tempNode); if (!tempNode->head().isEmpty()) { tempNode->contentDescription()->from7BitString(QByteArrayLiteral("temporary node")); } parseInternal(tempNode); } QString MessagePart::renderInternalText() const { QString text; for (const auto &mp : subParts()) { text += mp->text(); } return text; } void MessagePart::appendSubPart(const MessagePart::Ptr &messagePart) { messagePart->setParentPart(this); mBlocks.append(messagePart); } const QList<MessagePart::Ptr> &MessagePart::subParts() const { return mBlocks; } bool MessagePart::hasSubParts() const { return !mBlocks.isEmpty(); } QList<SignedMessagePart *> MessagePart::signatures() const { QList<SignedMessagePart *> list; if (auto sig = dynamic_cast<SignedMessagePart *>(const_cast<MessagePart *>(this))) { list << sig; } auto parent = parentPart(); while (parent) { if (auto sig = dynamic_cast<SignedMessagePart *>(parent)) { list << sig; } parent = parent->parentPart(); } return list; } QList<EncryptedMessagePart *> MessagePart::encryptions() const { QList<EncryptedMessagePart *> list; if (auto sig = dynamic_cast<EncryptedMessagePart *>(const_cast<MessagePart *>(this))) { list << sig; } auto parent = parentPart(); while (parent) { if (auto sig = dynamic_cast<EncryptedMessagePart *>(parent)) { list << sig; } parent = parent->parentPart(); } return list; } KMMsgEncryptionState MessagePart::encryptionState() const { if (!encryptions().isEmpty()) { return KMMsgFullyEncrypted; } return KMMsgNotEncrypted; } KMMsgSignatureState MessagePart::signatureState() const { if (!signatures().isEmpty()) { return KMMsgFullySigned; } return KMMsgNotSigned; } void MessagePart::bindLifetime(KMime::Content *node) { mNodesToDelete << node; } KMime::Headers::Base *MimeTreeParser::MessagePart::header(const char *header) const { if (node() && node()->hasHeader(header)) { return node()->headerByType(header); } if (auto parent = parentPart()) { return parent->header(header); } return nullptr; } //-----MessagePartList---------------------- MessagePartList::MessagePartList(ObjectTreeParser *otp, KMime::Content *node) : MessagePart(otp, QString(), node) { } QString MessagePartList::text() const { return renderInternalText(); } QString MessagePartList::plaintextContent() const { return QString(); } QString MessagePartList::htmlContent() const { return QString(); } //-----TextMessageBlock---------------------- TextMessagePart::TextMessagePart(ObjectTreeParser *otp, KMime::Content *node) : MessagePartList(otp, node) , mSignatureState(KMMsgSignatureStateUnknown) , mEncryptionState(KMMsgEncryptionStateUnknown) { if (!mNode) { qCWarning(MIMETREEPARSER_CORE_LOG) << "not a valid node"; return; } parseContent(); } void TextMessagePart::parseContent() { mSignatureState = KMMsgNotSigned; mEncryptionState = KMMsgNotEncrypted; const auto blocks = prepareMessageForDecryption(mNode->decodedContent()); // We also get blocks for unencrypted messages if (!blocks.isEmpty()) { auto aCodec = QStringDecoder(mOtp->codecNameFor(mNode).constData()); const auto cryptProto = QGpgME::openpgp(); /* The (overall) signature/encrypted status is broken * if one unencrypted part is at the beginning or in the middle * because mailmain adds an unencrypted part at the end this should not break the overall status * * That's why we first set the tmp status and if one crypted/signed block comes afterwards, than * the status is set to unencryped */ bool fullySignedOrEncrypted = true; bool fullySignedOrEncryptedTmp = true; for (const auto &block : blocks) { if (!fullySignedOrEncryptedTmp) { fullySignedOrEncrypted = false; } if (block.type() == NoPgpBlock && !block.text().trimmed().isEmpty()) { fullySignedOrEncryptedTmp = false; appendSubPart(MessagePart::Ptr(new MessagePart(mOtp, aCodec.decode(KMime::CRLFtoLF(block.text()))))); } else if (block.type() == PgpMessageBlock) { auto content = new KMime::Content; content->setBody(block.text()); content->parse(); content->contentType()->setCharset(charset()); EncryptedMessagePart::Ptr mp(new EncryptedMessagePart(mOtp, QString(), cryptProto, content, content, false)); mp->bindLifetime(content); mp->setIsEncrypted(true); appendSubPart(mp); } else if (block.type() == ClearsignedBlock) { auto content = new KMime::Content; content->setBody(block.text()); content->parse(); content->contentType()->setCharset(charset()); SignedMessagePart::Ptr mp(new SignedMessagePart(mOtp, cryptProto, nullptr, content, false)); mp->bindLifetime(content); appendSubPart(mp); } else { continue; } const auto mp = subParts().last().staticCast<MessagePart>(); const PartMetaData *messagePart(mp->partMetaData()); if (!messagePart->isEncrypted && !messagePart->isSigned() && !block.text().trimmed().isEmpty()) { mp->setText(aCodec.decode(KMime::CRLFtoLF(block.text()))); } if (messagePart->isEncrypted) { mEncryptionState = KMMsgPartiallyEncrypted; } if (messagePart->isSigned()) { mSignatureState = KMMsgPartiallySigned; } } // Do we have an fully Signed/Encrypted Message? if (fullySignedOrEncrypted) { if (mSignatureState == KMMsgPartiallySigned) { mSignatureState = KMMsgFullySigned; } if (mEncryptionState == KMMsgPartiallyEncrypted) { mEncryptionState = KMMsgFullyEncrypted; } } } } KMMsgEncryptionState TextMessagePart::encryptionState() const { if (mEncryptionState == KMMsgNotEncrypted) { return MessagePart::encryptionState(); } return mEncryptionState; } KMMsgSignatureState TextMessagePart::signatureState() const { if (mSignatureState == KMMsgNotSigned) { return MessagePart::signatureState(); } return mSignatureState; } //-----AttachmentMessageBlock---------------------- AttachmentMessagePart::AttachmentMessagePart(ObjectTreeParser *otp, KMime::Content *node) : TextMessagePart(otp, node) { } //-----HtmlMessageBlock---------------------- HtmlMessagePart::HtmlMessagePart(ObjectTreeParser *otp, KMime::Content *node) : MessagePart(otp, QString(), node) { if (!mNode) { qCWarning(MIMETREEPARSER_CORE_LOG) << "not a valid node"; return; } setText(QStringDecoder(mOtp->codecNameFor(mNode).constData()).decode(KMime::CRLFtoLF(mNode->decodedContent()))); } //-----MimeMessageBlock---------------------- MimeMessagePart::MimeMessagePart(ObjectTreeParser *otp, KMime::Content *node, bool onlyOneMimePart) : MessagePart(otp, QString(), node) { if (!mNode) { qCWarning(MIMETREEPARSER_CORE_LOG) << "not a valid node"; return; } parseInternal(mNode, onlyOneMimePart); } MimeMessagePart::~MimeMessagePart() { } QString MimeMessagePart::text() const { return renderInternalText(); } QString MimeMessagePart::plaintextContent() const { return QString(); } QString MimeMessagePart::htmlContent() const { return QString(); } //-----AlternativeMessagePart---------------------- AlternativeMessagePart::AlternativeMessagePart(ObjectTreeParser *otp, KMime::Content *node) : MessagePart(otp, QString(), node) { if (auto dataIcal = findTypeInDirectChildren(mNode, "text/calendar")) { mChildParts[MultipartIcal] = MimeMessagePart::Ptr(new MimeMessagePart(mOtp, dataIcal, true)); } if (auto dataText = findTypeInDirectChildren(mNode, "text/plain")) { mChildParts[MultipartPlain] = MimeMessagePart::Ptr(new MimeMessagePart(mOtp, dataText, true)); } if (auto dataHtml = findTypeInDirectChildren(mNode, "text/html")) { mChildParts[MultipartHtml] = MimeMessagePart::Ptr(new MimeMessagePart(mOtp, dataHtml, true)); } else { // If we didn't find the HTML part as the first child of the multipart/alternative, it might // be that this is a HTML message with images, and text/plain and multipart/related are the // immediate children of this multipart/alternative node. // In this case, the HTML node is a child of multipart/related. // In the case of multipart/related we don't expect multiple html parts, it is usually used to group attachments // with html content. // // In any case, this is not a complete implementation of MIME, but an approximation for the kind of mails we actually see in the wild. auto data = [&] { if (auto d = findTypeInDirectChildren(mNode, "multipart/related")) { return d; } return findTypeInDirectChildren(mNode, "multipart/mixed"); }(); if (data) { QString htmlContent; const auto parts = data->contents(); for (auto p : parts) { if ((!p->contentType()->isEmpty()) && (p->contentType()->mimeType() == "text/html")) { htmlContent += MimeMessagePart(mOtp, p, true).text(); } else if (KMime::isAttachment(p)) { appendSubPart(MimeMessagePart::Ptr(new MimeMessagePart(otp, p, true))); } } mChildParts[MultipartHtml] = MessagePart::Ptr(new MessagePart(mOtp, htmlContent, nullptr)); } } } AlternativeMessagePart::~AlternativeMessagePart() { } QList<AlternativeMessagePart::HtmlMode> AlternativeMessagePart::availableModes() { return mChildParts.keys(); } QString AlternativeMessagePart::text() const { if (mChildParts.contains(MultipartPlain)) { return mChildParts[MultipartPlain]->text(); } return QString(); } bool AlternativeMessagePart::isHtml() const { return mChildParts.contains(MultipartHtml); } QString AlternativeMessagePart::plaintextContent() const { return text(); } QString AlternativeMessagePart::htmlContent() const { if (mChildParts.contains(MultipartHtml)) { return mChildParts[MultipartHtml]->text(); } else { return plaintextContent(); } } QString AlternativeMessagePart::icalContent() const { if (mChildParts.contains(MultipartIcal)) { return mChildParts[MultipartIcal]->text(); } return {}; } //-----CertMessageBlock---------------------- CertMessagePart::CertMessagePart(ObjectTreeParser *otp, KMime::Content *node, QGpgME::Protocol *cryptoProto) : MessagePart(otp, QString(), node) , mCryptoProto(cryptoProto) { if (!mNode) { qCWarning(MIMETREEPARSER_CORE_LOG) << "not a valid node"; return; } } CertMessagePart::~CertMessagePart() { } QString CertMessagePart::text() const { return QString(); } //-----SignedMessageBlock--------------------- SignedMessagePart::SignedMessagePart(ObjectTreeParser *otp, const QGpgME::Protocol *cryptoProto, KMime::Content *node, KMime::Content *signedData, bool parseAfterDecryption) : MessagePart(otp, {}, node) , mParseAfterDecryption(parseAfterDecryption) , mCryptoProto(cryptoProto) , mSignedData(signedData) { mMetaData.status = i18ndc("mimetreeparser", "@info:status", "Wrong Crypto Plug-In."); - qWarning() << "created" << this; } -SignedMessagePart::~SignedMessagePart() -{ - qWarning() << "desctrued" << this; -} +SignedMessagePart::~SignedMessagePart() = default; static QString prettifyDN(const char *uid) { // We used to use QGpgME::DN::prettyDN here. But I'm not sure what we actually need it for. return QString::fromUtf8(uid); } const QGpgME::Protocol *SignedMessagePart::cryptoProto() const { return mCryptoProto; } void SignedMessagePart::startVerification() { if (!mSignedData) { return; } mMetaData.status = i18ndc("mimetreeparser", "@info:status", "Wrong Crypto Plug-In."); mMetaData.isEncrypted = false; mMetaData.isDecryptable = false; auto codec = QStringDecoder(mOtp->codecNameFor(mSignedData).constData()); // If we have a mNode, this is a detached signature if (mNode) { const auto signature = mNode->decodedContent(); // This is necessary in case the original data contained CRLF's. Otherwise the signature will not match the data (since KMIME normalizes to LF) const QByteArray signedData = KMime::LFtoCRLF(mSignedData->encodedContent()); const auto job = mCryptoProto->verifyDetachedJob(); setVerificationResult(job->exec(signature, signedData), signedData); job->deleteLater(); setText(codec.decode(KMime::CRLFtoLF(signedData))); } else { QByteArray outdata; const auto job = mCryptoProto->verifyOpaqueJob(); setVerificationResult(job->exec(mSignedData->decodedContent(), outdata), outdata); job->deleteLater(); setText(codec.decode(KMime::CRLFtoLF(outdata))); } - - if (!mMetaData.isSigned()) { - mMetaData.creationTime = QDateTime(); - } } void SignedMessagePart::setVerificationResult(const GpgME::VerificationResult &result, const QByteArray &signedData) { mMetaData.verificationResult = result; qWarning() << "set mMetaData.verificationResult" << this; if (mMetaData.isSigned()) { if (!signedData.isEmpty() && mParseAfterDecryption) { parseInternal(signedData); } } } QString SignedMessagePart::plaintextContent() const { if (!mNode) { return MessagePart::text(); } else { return QString(); } } QString SignedMessagePart::htmlContent() const { if (!mNode) { return MessagePart::text(); } else { return QString(); } } //-----CryptMessageBlock--------------------- EncryptedMessagePart::EncryptedMessagePart(ObjectTreeParser *otp, const QString &text, const QGpgME::Protocol *cryptoProto, KMime::Content *node, KMime::Content *encryptedNode, bool parseAfterDecryption) : MessagePart(otp, text, node) , mParseAfterDecryption(parseAfterDecryption) , mPassphraseError(false) , mNoSecKey(false) , mDecryptMessage(false) , mCryptoProto(cryptoProto) , mEncryptedNode(encryptedNode) { mMetaData.isEncrypted = false; mMetaData.isDecryptable = false; mMetaData.status = i18ndc("mimetreeparser", "@info:status", "Wrong Crypto Plug-In."); } void EncryptedMessagePart::setIsEncrypted(bool encrypted) { mMetaData.isEncrypted = encrypted; } bool EncryptedMessagePart::isEncrypted() const { return mMetaData.isEncrypted; } const QGpgME::Protocol *EncryptedMessagePart::cryptoProto() const { return mCryptoProto; } void EncryptedMessagePart::setDecryptMessage(bool decrypt) { mDecryptMessage = decrypt; } bool EncryptedMessagePart::decryptMessage() const { return mDecryptMessage; } bool EncryptedMessagePart::isDecryptable() const { return mMetaData.isDecryptable; } bool EncryptedMessagePart::isNoSecKey() const { return mNoSecKey; } bool EncryptedMessagePart::passphraseError() const { return mPassphraseError; } bool EncryptedMessagePart::decrypt(KMime::Content &data) { mError = NoError; mMetaData.errorText.clear(); // FIXME // mMetaData.auditLogError = GpgME::Error(); mMetaData.auditLog.clear(); const QByteArray ciphertext = data.decodedContent(); QByteArray plainText; auto job = mCryptoProto->decryptVerifyJob(); const std::pair<GpgME::DecryptionResult, GpgME::VerificationResult> p = job->exec(ciphertext, plainText); job->deleteLater(); auto decryptResult = p.first; auto verifyResult = p.second; mMetaData.verificationResult = verifyResult; // Normalize CRLF's plainText = KMime::CRLFtoLF(plainText); auto codec = QStringDecoder(mOtp->codecNameFor(&data).constData()); const auto decoded = codec.decode(plainText); if (partMetaData()->isSigned()) { // We simply attach a signed message part to indicate that this content is also signed // We're forwarding mNode to not loose the encoding information auto subPart = SignedMessagePart::Ptr(new SignedMessagePart(mOtp, mCryptoProto, mNode, nullptr)); subPart->setText(decoded); subPart->setVerificationResult(verifyResult, plainText); appendSubPart(subPart); } mDecryptRecipients.clear(); bool cannotDecrypt = false; bool bDecryptionOk = !decryptResult.error(); for (const auto &recipient : decryptResult.recipients()) { if (!recipient.status()) { bDecryptionOk = true; } GpgME::Key key; key = Kleo::KeyCache::instance()->findByKeyIDOrFingerprint(recipient.keyID()); if (key.isNull()) { auto ret = Kleo::KeyCache::instance()->findSubkeysByKeyID({recipient.keyID()}); if (ret.size() == 1) { key = ret.front().parent(); } if (key.isNull()) { qCDebug(MIMETREEPARSER_CORE_LOG) << "Found no Key for KeyID " << recipient.keyID(); } } mDecryptRecipients.emplace_back(recipient, key); } if (!bDecryptionOk && partMetaData()->isSigned()) { // Only a signed part partMetaData()->isEncrypted = false; bDecryptionOk = true; mDecryptedData = plainText; } else { mPassphraseError = decryptResult.error().isCanceled() || decryptResult.error().code() == GPG_ERR_BAD_PASSPHRASE; mMetaData.isEncrypted = bDecryptionOk || decryptResult.error().code() != GPG_ERR_NO_DATA; if (decryptResult.error().isCanceled()) { setDecryptMessage(false); } partMetaData()->errorText = QString::fromLocal8Bit(decryptResult.error().asString()); if (Kleo::DeVSCompliance::isCompliant()) { partMetaData()->isCompliant = decryptResult.isDeVs(); partMetaData()->compliance = Kleo::DeVSCompliance::name(decryptResult.isDeVs()); } else { partMetaData()->isCompliant = true; } if (partMetaData()->isEncrypted && decryptResult.numRecipients() > 0) { partMetaData()->keyId = decryptResult.recipient(0).keyID(); } if (bDecryptionOk) { mDecryptedData = plainText; } else { mNoSecKey = true; const auto decryRecipients = decryptResult.recipients(); for (const GpgME::DecryptionResult::Recipient &recipient : decryRecipients) { mNoSecKey &= (recipient.status().code() == GPG_ERR_NO_SECKEY); } if (!mPassphraseError && !mNoSecKey) { // GpgME do not detect passphrase error correctly mPassphraseError = true; } } } if (!bDecryptionOk) { QString cryptPlugLibName; mError = UnknownError; if (mCryptoProto) { cryptPlugLibName = mCryptoProto->name(); } if (mNoSecKey) { mError = NoKeyError; } if (mPassphraseError) { mError = PassphraseError; } if (!mCryptoProto) { partMetaData()->errorText = i18n("No appropriate crypto plug-in was found."); } else if (cannotDecrypt) { partMetaData()->errorText = i18n("Crypto plug-in \"%1\" cannot decrypt messages.", cryptPlugLibName); } else if (!passphraseError()) { partMetaData()->errorText = i18n("Crypto plug-in \"%1\" could not decrypt the data.", cryptPlugLibName) + QLatin1StringView("<br />") + i18n("Error: %1", partMetaData()->errorText); } } return bDecryptionOk; } void EncryptedMessagePart::startDecryption(KMime::Content *data) { mMetaData.isEncrypted = true; mMetaData.isDecryptable = decrypt(*data); if (mParseAfterDecryption && !mMetaData.isSigned()) { parseInternal(mDecryptedData); } else { setText(QString::fromUtf8(mDecryptedData.constData())); } } void EncryptedMessagePart::startDecryption() { if (mEncryptedNode) { startDecryption(mEncryptedNode); } else { startDecryption(mNode); } } std::vector<std::pair<GpgME::DecryptionResult::Recipient, GpgME::Key>> EncryptedMessagePart::decryptRecipients() const { return mDecryptRecipients; } QString EncryptedMessagePart::plaintextContent() const { if (!mNode) { return MessagePart::text(); } else { return QString(); } } QString EncryptedMessagePart::htmlContent() const { if (!mNode) { return MessagePart::text(); } else { return QString(); } } QString EncryptedMessagePart::text() const { if (hasSubParts()) { auto _mp = (subParts()[0]).dynamicCast<SignedMessagePart>(); if (_mp) { return _mp->text(); } } return MessagePart::text(); } EncapsulatedRfc822MessagePart::EncapsulatedRfc822MessagePart(ObjectTreeParser *otp, KMime::Content *node, const KMime::Message::Ptr &message) : MessagePart(otp, QString(), node) , mMessage(message) { mMetaData.isEncrypted = false; mMetaData.isEncapsulatedRfc822Message = true; if (!mMessage) { qCWarning(MIMETREEPARSER_CORE_LOG) << "Node is of type message/rfc822 but doesn't have a message!"; return; } parseInternal(message.data()); } QString EncapsulatedRfc822MessagePart::text() const { return renderInternalText(); } QString EncapsulatedRfc822MessagePart::from() const { if (auto from = mMessage->from(false)) { return from->asUnicodeString(); } return {}; } QDateTime EncapsulatedRfc822MessagePart::date() const { if (auto date = mMessage->date(false)) { return date->dateTime(); } return {}; } HeadersPart::HeadersPart(ObjectTreeParser *otp, KMime::Content *node) : MessagePart(otp, QString(), node) { } #include "moc_messagepart.cpp" diff --git a/src/core/partmetadata.h b/src/core/partmetadata.h index c07b867..1821578 100644 --- a/src/core/partmetadata.h +++ b/src/core/partmetadata.h @@ -1,58 +1,56 @@ // SPDX-FileCopyrightText: 2002-2003 Karl-Heinz Zimmer <khz@kde.org> // SPDX-FileCopyrightText: 2003 Marc Mutz <mutz@kde.org> // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include <QDateTime> #include <QStringList> #include <gpgme++/context.h> #include <gpgme++/verificationresult.h> namespace MimeTreeParser { class PartMetaData { public: PartMetaData() : isEncrypted(false) , isDecryptable(false) , inProgress(false) , technicalProblem(false) , isEncapsulatedRfc822Message(false) , isCompliant(false) , keyRevoked(false) { } inline bool isSigned() const { return verificationResult.signatures().size() > 0; } GpgME::VerificationResult verificationResult; QString signClass; QString signer; QStringList signerMailAddresses; QByteArray keyId; - QString keyTrust; QString status; // to be used for unknown plug-ins int status_code = 0; // = GPGME_SIG_STAT_NONE; to be used for i18n of OpenPGP and S/MIME CryptPlugs QString errorText; - QDateTime creationTime; QString decryptionError; QString auditLog; QString compliance; // textual representation of compliance status; empty if compliance isn't enforced GpgME::Error auditLogError; bool isEncrypted : 1; bool isDecryptable : 1; bool inProgress : 1; bool technicalProblem : 1; bool isEncapsulatedRfc822Message : 1; bool isCompliant : 1; // corresponds to the isDeVS flag of signature or decryption result bool keyRevoked : 1; }; } diff --git a/src/core/partmodel.cpp b/src/core/partmodel.cpp index 7484330..76700d4 100644 --- a/src/core/partmodel.cpp +++ b/src/core/partmodel.cpp @@ -1,892 +1,667 @@ // SPDX-FileCopyrightText: 2016 Sandro Knauß <knauss@kolabsys.com> // SPDX-License-Identifier: LGPL-2.0-or-later #include "partmodel.h" #include "enums.h" #include "htmlutils.h" #include "mimetreeparser_core_debug.h" #include "objecttreeparser.h" #include "utils.h" #include <KLocalizedString> #include <Libkleo/Compliance> #include <Libkleo/Formatting> #include <Libkleo/KeyCache> #include <QDebug> #include <QGpgME/Protocol> #include <QRegularExpression> #include <QStringLiteral> #include <QTextDocument> #include <verificationresult.h> -std::optional<GpgME::Signature> signatureFromMessagePart(MimeTreeParser::MessagePart *messagePart) +static std::optional<GpgME::Signature> signatureFromMessagePart(MimeTreeParser::MessagePart *messagePart) { const auto signatureState = messagePart->signatureState(); const bool messageIsSigned = signatureState == MimeTreeParser::KMMsgPartiallySigned || signatureState == MimeTreeParser::KMMsgFullySigned; if (!messageIsSigned) { return std::nullopt; } const auto signatureParts = messagePart->signatures(); Q_ASSERT(!signatureParts.isEmpty()); if (signatureParts.empty()) { return std::nullopt; } const auto signaturePart = signatureParts.front(); // TODO add support for multiple signature const auto signatures = signaturePart->partMetaData()->verificationResult.signatures(); Q_ASSERT(!signatures.empty()); if (signatures.empty()) { return std::nullopt; } const auto signature = signatures.front(); // TODO add support for multiple signature return signature; } -static QString formatValidSignatureWithTrustLevel(const GpgME::UserID &id) -{ - if (id.isNull()) { - return QString(); - } - switch (id.validity()) { - case GpgME::UserID::Marginal: - return i18n("The signature is valid but the trust in the certificate's validity is only marginal."); - case GpgME::UserID::Full: - return i18n("The signature is valid and the certificate's validity is fully trusted."); - case GpgME::UserID::Ultimate: - return i18n("The signature is valid and the certificate's validity is ultimately trusted."); - case GpgME::UserID::Never: - return i18n("The signature is valid but the certificate's validity is <em>not trusted</em>."); - case GpgME::UserID::Unknown: - return i18n("The signature is valid but the certificate's validity is unknown."); - case GpgME::UserID::Undefined: - default: - return i18n("The signature is valid but the certificate's validity is undefined."); - } -} - -static QString renderKeyLink(const QString &fpr, const QString &text) -{ - return QStringLiteral("<a href=\"key:%1\">%2</a>").arg(fpr, text); -} - -static QString renderKey(const GpgME::Key &key) -{ - if (key.isNull()) { - return i18n("Unknown certificate"); - } - - if (key.primaryFingerprint() && strlen(key.primaryFingerprint()) > 16 && key.numUserIDs()) { - const QString text = QStringLiteral("%1 (%2)") - .arg(Kleo::Formatting::prettyNameAndEMail(key).toHtmlEscaped()) - .arg(Kleo::Formatting::prettyID(QString::fromLocal8Bit(key.primaryFingerprint()).right(16).toLatin1().constData())); - return renderKeyLink(QLatin1StringView(key.primaryFingerprint()), text); - } - - return renderKeyLink(QLatin1StringView(key.primaryFingerprint()), Kleo::Formatting::prettyID(key.primaryFingerprint())); -} - -static QString formatDate(const QDateTime &dt) -{ - return QLocale().toString(dt); -} - -static QString formatSigningInformation(const GpgME::Signature &sig, const GpgME::Key &key) -{ - if (sig.isNull()) { - return QString(); - } - const QDateTime dt = sig.creationTime() != 0 ? QDateTime::fromSecsSinceEpoch(quint32(sig.creationTime())) : QDateTime(); - QString text; - - if (key.isNull()) { - return text += i18n("With unavailable certificate:") + QStringLiteral("<br>ID: 0x%1").arg(QString::fromLatin1(sig.fingerprint()).toUpper()); - } - - text += i18n("With certificate: %1", renderKey(key)); - - if (Kleo::DeVSCompliance::isCompliant()) { - text += (QStringLiteral("<br/>") - + (sig.isDeVs() ? i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", - "The signature is %1", - Kleo::DeVSCompliance::name(true)) - : i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", - "The signature <b>is not</b> %1.", - Kleo::DeVSCompliance::name(true)))); - } - - return text; -} - -static QString signatureSummaryToString(GpgME::Signature::Summary summary) -{ - if (summary & GpgME::Signature::None) { - return i18n("Error: Signature not verified"); - } else if (summary & GpgME::Signature::Valid || summary & GpgME::Signature::Green) { - return i18n("Good signature"); - } else if (summary & GpgME::Signature::KeyRevoked) { - return i18n("Signing certificate was revoked"); - } else if (summary & GpgME::Signature::KeyExpired) { - return i18n("Signing certificate is expired"); - } else if (summary & GpgME::Signature::KeyMissing) { - return i18n("Certificate is not available"); - } else if (summary & GpgME::Signature::SigExpired) { - return i18n("Signature expired"); - } else if (summary & GpgME::Signature::CrlMissing) { - return i18n("CRL missing"); - } else if (summary & GpgME::Signature::CrlTooOld) { - return i18n("CRL too old"); - } else if (summary & GpgME::Signature::BadPolicy) { - return i18n("Bad policy"); - } else if (summary & GpgME::Signature::SysError) { - return i18n("System error"); // ### retrieve system error details? - } else if (summary & GpgME::Signature::Red) { - return i18n("Bad signature"); - } - return QString(); -} - -static bool addrspec_equal(const KMime::Types::AddrSpec &lhs, const KMime::Types::AddrSpec &rhs, Qt::CaseSensitivity cs) -{ - return lhs.localPart.compare(rhs.localPart, cs) == 0 && lhs.domain.compare(rhs.domain, Qt::CaseInsensitive) == 0; -} - -static std::string stripAngleBrackets(const std::string &str) -{ - if (str.empty()) { - return str; - } - if (str[0] == '<' && str[str.size() - 1] == '>') { - return str.substr(1, str.size() - 2); - } - return str; -} - -static std::string email(const GpgME::UserID &uid) -{ - if (uid.parent().protocol() == GpgME::OpenPGP) { - if (const char *const email = uid.email()) { - return stripAngleBrackets(email); - } else { - return std::string(); - } - } - - Q_ASSERT(uid.parent().protocol() == GpgME::CMS); - - if (const char *const id = uid.id()) - if (*id == '<') { - return stripAngleBrackets(id); - } else { - return Kleo::DN(id)[QStringLiteral("EMAIL")].trimmed().toUtf8().constData(); - } - else { - return std::string(); - } -} - -static bool mailbox_equal(const KMime::Types::Mailbox &lhs, const KMime::Types::Mailbox &rhs, Qt::CaseSensitivity cs) -{ - return addrspec_equal(lhs.addrSpec(), rhs.addrSpec(), cs); -} - -static KMime::Types::Mailbox mailbox(const GpgME::UserID &uid) -{ - const std::string e = email(uid); - KMime::Types::Mailbox mbox; - if (!e.empty()) { - mbox.setAddress(e.c_str()); - } - return mbox; -} - -static GpgME::UserID findUserIDByMailbox(const GpgME::Key &key, const KMime::Types::Mailbox &mbox) -{ - const auto userIDs{key.userIDs()}; - for (const GpgME::UserID &id : userIDs) { - if (mailbox_equal(mailbox(id), mbox, Qt::CaseInsensitive)) { - return id; - } - } - return {}; -} - -static QString formatSignature(const GpgME::Signature &sig, const GpgME::Key &key, const KMime::Types::Mailbox &sender) -{ - if (sig.isNull()) { - return QString(); - } - - const QString text = formatSigningInformation(sig, key) + QLatin1StringView("<br/>"); - - // Green - if (sig.summary() & GpgME::Signature::Valid) { - const GpgME::UserID id = findUserIDByMailbox(key, sender); - return text + formatValidSignatureWithTrustLevel(!id.isNull() ? id : key.userID(0)); - } - - // Red - if ((sig.summary() & GpgME::Signature::Red)) { - const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary())); - if (sig.summary() & GpgME::Signature::SysError) { - return ret + QStringLiteral(" (%1)").arg(Kleo::Formatting::errorAsString(sig.status())); - } - return ret; - } - - // Key missing - if ((sig.summary() & GpgME::Signature::KeyMissing)) { - return text + i18n("You can search the certificate on a keyserver or import it from a file."); - } - - // Yellow - if ((sig.validity() & GpgME::Signature::Validity::Undefined) // - || (sig.validity() & GpgME::Signature::Validity::Unknown) // - || (sig.summary() == GpgME::Signature::Summary::None)) { - return text - + (key.protocol() == GpgME::OpenPGP - ? i18n("The used key is not certified by you or any trusted person.") - : i18n("The used certificate is not certified by a trustworthy Certificate Authority or the Certificate Authority is unknown.")); - } - - // Catch all fall through - const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary())); - if (sig.summary() & GpgME::Signature::SysError) { - return ret + QStringLiteral(" (%1)").arg(Kleo::Formatting::errorAsString(sig.status())); - } - return ret; -} - // We return a pair containing the trimmed string, as well as a boolean indicating whether the string was trimmed or not std::pair<QString, bool> PartModel::trim(const QString &text) { // The delimiters have <p>.? prefixed including the .? because sometimes we get a byte order mark <feff> (seen with user-agent: // Microsoft-MacOutlook/10.1d.0.190908) We match both regulard withspace with \s and non-breaking spaces with \u00A0 const QList<QRegularExpression> delimiters{ // English QRegularExpression{QStringLiteral("<p>.?-+Original(\\s|\u00A0)Message-+"), QRegularExpression::CaseInsensitiveOption}, // The remainder is not quoted QRegularExpression{QStringLiteral("<p>.?On.*wrote:"), QRegularExpression::CaseInsensitiveOption}, // The remainder is quoted QRegularExpression{QStringLiteral("> On.*wrote:"), QRegularExpression::CaseInsensitiveOption}, // German // Forwarded QRegularExpression{QStringLiteral("<p>.?Von:.*</p>"), QRegularExpression::CaseInsensitiveOption}, // Reply QRegularExpression{QStringLiteral("<p>.?Am.*schrieb.*:</p>"), QRegularExpression::CaseInsensitiveOption}, // Signature QRegularExpression{QStringLiteral("<p>.?--(\\s|\u00A0)<br>"), QRegularExpression::CaseInsensitiveOption}, }; for (const auto &expression : delimiters) { auto i = expression.globalMatchView(text); while (i.hasNext()) { const auto match = i.next(); const int startOffset = match.capturedStart(0); // This is a very simplistic detection for an inline reply where we would have the patterns before the actual message content. // We simply ignore anything we find within the first few lines. if (startOffset >= 5) { return {text.mid(0, startOffset), true}; } else { // Search for the next delimiter continue; } } } return {text, false}; } static QString addCss(const QString &s) { // Get the default font from QApplication static const QString fontFamily = QFont{}.family(); // overflow:hidden ensures no scrollbars are ever shown. static const QString css = QStringLiteral("<style>\n") + QStringLiteral( "body {\n" " overflow:hidden;\n" " font-family: \"%1\" ! important;\n" " color: #31363b ! important;\n" " background-color: #fcfcfc ! important\n" "}\n") .arg(fontFamily) + QStringLiteral("blockquote { \n" " border-left: 2px solid #bdc3c7 ! important;\n" "}\n") + QStringLiteral("</style>"); const QString header = QLatin1StringView( "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n" "<html><head><title></title>") + css + QLatin1StringView("</head>\n<body>\n"); return header + s + QStringLiteral("</body></html>"); } class PartModelPrivate { public: PartModelPrivate(PartModel *q_ptr, const std::shared_ptr<MimeTreeParser::ObjectTreeParser> &parser) : q(q_ptr) , mParser(parser) { collectContents(); } ~PartModelPrivate() = default; void checkPart(const MimeTreeParser::MessagePart::Ptr part) { mMimeTypeCache[part.data()] = part->mimeType(); // Extract the content of the part and mContents.insert(part.data(), extractContent(part.data())); } // Recursively find encapsulated messages void findEncapsulated(const MimeTreeParser::EncapsulatedRfc822MessagePart::Ptr &e) { mEncapsulatedParts[e.data()] = mParser->collectContentParts(e); for (const auto &subPart : std::as_const(mEncapsulatedParts[e.data()])) { checkPart(subPart); mParents[subPart.data()] = e.data(); if (auto encapsulatedSub = subPart.dynamicCast<MimeTreeParser::EncapsulatedRfc822MessagePart>()) { findEncapsulated(encapsulatedSub); } } } QVariant extractContent(MimeTreeParser::MessagePart *messagePart) { if (auto alternativePart = dynamic_cast<MimeTreeParser::AlternativeMessagePart *>(messagePart)) { if (alternativePart->availableModes().contains(MimeTreeParser::AlternativeMessagePart::MultipartIcal)) { return alternativePart->icalContent(); } } auto preprocessPlaintext = [&](const QString &text) { // Reduce consecutive new lines to never exceed 2 auto cleaned = text; cleaned.replace(QRegularExpression(QStringLiteral("[\n\r]{2,}")), QStringLiteral("\n\n")); // We always do rich text (so we get highlighted links and stuff). const auto html = Qt::convertFromPlainText(cleaned, Qt::WhiteSpaceNormal); if (trimMail) { const auto result = PartModel::trim(html); isTrimmed = result.second; Q_EMIT q->trimMailChanged(); return MimeTreeParser::linkify(result.first); } return MimeTreeParser::linkify(html); }; if (messagePart->isHtml()) { if (dynamic_cast<MimeTreeParser::AlternativeMessagePart *>(messagePart)) { containsHtmlAndPlain = true; Q_EMIT q->containsHtmlChanged(); if (!showHtml) { return preprocessPlaintext(messagePart->plaintextContent()); } } return addCss(mParser->resolveCidLinks(messagePart->htmlContent())); } if (auto attachmentPart = dynamic_cast<MimeTreeParser::AttachmentMessagePart *>(messagePart)) { auto node = attachmentPart->node(); if (node && mMimeTypeCache[attachmentPart] == QByteArrayLiteral("text/calendar")) { return messagePart->text(); } } return preprocessPlaintext(messagePart->text()); } QVariant contentForPart(MimeTreeParser::MessagePart *messagePart) const { return mContents.value(messagePart); } void collectContents() { mEncapsulatedParts.clear(); mParents.clear(); mContents.clear(); containsHtmlAndPlain = false; isTrimmed = false; const auto parts = mParser->collectContentParts(); MimeTreeParser::MessagePart::List filteredParts; for (const auto &part : parts) { if (part->node()) { const auto contentType = part->node()->contentType(); if (contentType && contentType->hasParameter("protected-headers")) { const auto contentDisposition = part->node()->contentDisposition(); if (contentDisposition && contentDisposition->disposition() == KMime::Headers::CDinline) { continue; } } } filteredParts << part; } for (const auto &part : std::as_const(filteredParts)) { checkPart(part); if (auto encapsulatedPart = part.dynamicCast<MimeTreeParser::EncapsulatedRfc822MessagePart>()) { findEncapsulated(encapsulatedPart); } } for (const auto &part : std::as_const(filteredParts)) { if (mMimeTypeCache[part.data()] == QByteArrayLiteral("text/calendar")) { mParts.prepend(part); } else { mParts.append(part); } } } PartModel *q; MimeTreeParser::MessagePart::List mParts; QHash<MimeTreeParser::MessagePart *, QByteArray> mMimeTypeCache; QHash<MimeTreeParser::MessagePart *, MimeTreeParser::MessagePart::List> mEncapsulatedParts; QHash<MimeTreeParser::MessagePart *, MimeTreeParser::MessagePart *> mParents; QMap<MimeTreeParser::MessagePart *, QVariant> mContents; std::shared_ptr<MimeTreeParser::ObjectTreeParser> mParser; bool showHtml{false}; bool containsHtmlAndPlain{false}; bool trimMail{false}; bool isTrimmed{false}; }; PartModel::PartModel(std::shared_ptr<MimeTreeParser::ObjectTreeParser> parser) : d(std::unique_ptr<PartModelPrivate>(new PartModelPrivate(this, parser))) { } PartModel::~PartModel() = default; void PartModel::setShowHtml(bool html) { if (d->showHtml != html) { beginResetModel(); d->showHtml = html; d->collectContents(); endResetModel(); Q_EMIT showHtmlChanged(); } } bool PartModel::showHtml() const { return d->showHtml; } void PartModel::setTrimMail(bool trim) { if (d->trimMail != trim) { beginResetModel(); d->trimMail = trim; d->collectContents(); endResetModel(); Q_EMIT trimMailChanged(); } } bool PartModel::trimMail() const { return d->trimMail; } bool PartModel::isTrimmed() const { return d->isTrimmed; } bool PartModel::containsHtml() const { return d->containsHtmlAndPlain; } QHash<int, QByteArray> PartModel::roleNames() const { return { {TypeRole, QByteArrayLiteral("type")}, {ContentRole, QByteArrayLiteral("content")}, {IsEmbeddedRole, QByteArrayLiteral("isEmbedded")}, {SidebarSecurityLevelRole, QByteArrayLiteral("sidebarSecurityLevel")}, {EncryptionSecurityLevelRole, QByteArrayLiteral("encryptionSecurityLevel")}, {SignatureSecurityLevelRole, QByteArrayLiteral("signatureSecurityLevel")}, {EncryptionSecurityLevelRole, QByteArrayLiteral("encryptionSecurityLevel")}, {ErrorType, QByteArrayLiteral("errorType")}, {ErrorString, QByteArrayLiteral("errorString")}, {IsErrorRole, QByteArrayLiteral("error")}, {SenderRole, QByteArrayLiteral("sender")}, {SignatureDetailsRole, QByteArrayLiteral("signatureDetails")}, {SignatureIconNameRole, QByteArrayLiteral("signatureIconName")}, {EncryptionDetails, QByteArrayLiteral("encryptionDetails")}, {EncryptionIconNameRole, QByteArrayLiteral("encryptionIconName")}, {DateRole, QByteArrayLiteral("date")}, }; } QModelIndex PartModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column != 0) { return QModelIndex(); } if (parent.isValid()) { const auto part = static_cast<MimeTreeParser::MessagePart *>(parent.internalPointer()); auto encapsulatedPart = dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(part); if (encapsulatedPart) { const auto parts = d->mEncapsulatedParts[encapsulatedPart]; if (row < parts.size()) { return createIndex(row, column, parts.at(row).data()); } } return QModelIndex(); } if (row < d->mParts.size()) { return createIndex(row, column, d->mParts.at(row).data()); } return QModelIndex(); } SignatureInfo encryptionInfo(MimeTreeParser::MessagePart *messagePart) { SignatureInfo signatureInfo; const auto encryptions = messagePart->encryptions(); if (encryptions.size() > 1) { qWarning() << "Can't deal with more than one encryption"; } for (const auto &encryptionPart : encryptions) { signatureInfo.keyId = encryptionPart->partMetaData()->keyId; signatureInfo.cryptoProto = encryptionPart->cryptoProto(); signatureInfo.decryptRecipients = encryptionPart->decryptRecipients(); } return signatureInfo; }; template<typename T> const T *findHeader(KMime::Content *content) { auto header = content->header<T>(); if (header || !content->parent()) { return header; } return findHeader<T>(content->parent()); } QVariant PartModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } if (index.internalPointer()) { const auto messagePart = static_cast<MimeTreeParser::MessagePart *>(index.internalPointer()); // qWarning() << "Found message part " << messagePart->metaObject()->className() << messagePart->partMetaData()->status << messagePart->error(); Q_ASSERT(messagePart); switch (role) { case Qt::DisplayRole: return QStringLiteral("Content%1"); case SenderRole: { if (auto e = dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(messagePart)) { return e->from(); } return {}; } case DateRole: { if (auto e = dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(messagePart)) { return e->date(); } return {}; } case TypeRole: { if (messagePart->error()) { return QVariant::fromValue(Types::Error); } if (dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(messagePart)) { return QVariant::fromValue(Types::Encapsulated); } if (auto alternativePart = dynamic_cast<MimeTreeParser::AlternativeMessagePart *>(messagePart)) { if (alternativePart->availableModes().contains(MimeTreeParser::AlternativeMessagePart::MultipartIcal)) { return QVariant::fromValue(Types::Ical); } } if (auto attachmentPart = dynamic_cast<MimeTreeParser::AttachmentMessagePart *>(messagePart)) { auto node = attachmentPart->node(); if (!node) { qWarning() << "no content for attachment"; return {}; } if (d->mMimeTypeCache[attachmentPart] == QByteArrayLiteral("text/calendar")) { return QVariant::fromValue(Types::Ical); } } if (!d->showHtml && d->containsHtmlAndPlain) { return QVariant::fromValue(Types::Plain); } // For simple html we don't need a browser auto complexHtml = [&] { if (messagePart->isHtml()) { const auto text = messagePart->htmlContent(); if (text.contains(QStringLiteral("<!DOCTYPE html PUBLIC"))) { // We can probably deal with this if it adheres to the strict dtd //(that's what our composer produces as well) if (!text.contains(QStringLiteral("http://www.w3.org/TR/REC-html40/strict.dtd"))) { return true; } } // Blockquotes don't support any styling which would be necessary so they become readable. if (text.contains(QStringLiteral("blockquote"))) { return true; } // Media queries are too advanced if (text.contains(QStringLiteral("@media"))) { return true; } // auto css properties are not supported e.g margin-left: auto; if (text.contains(QStringLiteral(": auto;"))) { return true; } return false; } else { return false; } }(); if (complexHtml) { return QVariant::fromValue(Types::Html); } return QVariant::fromValue(Types::Plain); } case IsEmbeddedRole: return false; case IsErrorRole: return messagePart->error(); case ContentRole: return d->contentForPart(messagePart); case SidebarSecurityLevelRole: { const auto signature = index.data(SignatureSecurityLevelRole).value<SecurityLevel>(); const auto encryption = index.data(EncryptionSecurityLevelRole).value<SecurityLevel>(); if (signature == SecurityLevel::Bad || encryption == SecurityLevel::Bad) { return SecurityLevel::Bad; } if (signature == SecurityLevel::NotSoGood || encryption == SecurityLevel::NotSoGood) { return SecurityLevel::NotSoGood; } if (signature == SecurityLevel::Good || encryption == SecurityLevel::Good) { return SecurityLevel::Good; } return SecurityLevel::Unknow; } case SignatureSecurityLevelRole: { // Color displayed for the signature info box auto signature = signatureFromMessagePart(messagePart); if (!signature) { return SecurityLevel::Unknow; } const auto summary = signature->summary(); if (summary & GpgME::Signature::Summary::Red) { return SecurityLevel::Bad; } if (summary & GpgME::Signature::Summary::Valid) { return SecurityLevel::Good; } return SecurityLevel::NotSoGood; } case EncryptionSecurityLevelRole: { // Color displayed for the encryption info box const auto encryption = messagePart->encryptionState(); const bool messageIsEncrypted = encryption == MimeTreeParser::KMMsgPartiallyEncrypted || encryption == MimeTreeParser::KMMsgFullyEncrypted; if (messagePart->error()) { return SecurityLevel::Bad; } return messageIsEncrypted ? SecurityLevel::Good : SecurityLevel::Unknow; } case EncryptionIconNameRole: { const auto encryption = messagePart->encryptionState(); const bool messageIsEncrypted = encryption == MimeTreeParser::KMMsgPartiallyEncrypted || encryption == MimeTreeParser::KMMsgFullyEncrypted; if (messagePart->error()) { return QStringLiteral("data-error"); } return messageIsEncrypted ? QStringLiteral("mail-encrypted") : QString(); } case SignatureIconNameRole: { auto signature = signatureFromMessagePart(messagePart); if (!signature) { return QString{}; } const auto summary = signature->summary(); if (summary & GpgME::Signature::Valid) { return QStringLiteral("mail-signed"); } else if (summary & GpgME::Signature::Red) { return QStringLiteral("data-error"); } else { return QStringLiteral("data-warning"); } } case SignatureDetailsRole: { auto signature = signatureFromMessagePart(messagePart); if (!signature) { return QString{}; } GpgME::Key key = signature->key(); if (key.isNull()) { key = Kleo::KeyCache::instance()->findByFingerprint(signature->fingerprint()); } if (key.isNull() && signature->fingerprint()) { // try to find a subkey that was used for signing; // assumes that the key ID is the last 16 characters of the fingerprint const auto fpr = std::string_view{signature->fingerprint()}; const auto keyID = std::string{fpr, fpr.size() - 16, 16}; const auto subkeys = Kleo::KeyCache::instance()->findSubkeysByKeyID({keyID}); if (subkeys.size() > 0) { key = subkeys[0].parent(); } } if (key.isNull()) { qCWarning(MIMETREEPARSER_CORE_LOG) << "Found no key or subkey for fingerprint" << signature->fingerprint(); } QStringList senderUserId; for (int i = 0, count = key.userIDs().size(); i < count; i++) { senderUserId << QString::fromUtf8(key.userID(i).email()); } // guess sender from mime node or parent node auto from = findHeader<KMime::Headers::From>(messagePart->node()); if (from) { const auto mailboxes = from->mailboxes(); if (!mailboxes.isEmpty()) { auto mailBox = mailboxes.front(); if (mailBox.hasAddress()) { - return formatSignature(*signature, key, mailboxes.front()); + return Kleo::Formatting::prettySignature(*signature, mailboxes.front()); } } } - - // use first non empty userId as fallback - KMime::Types::Mailbox mailBox; - for (int i = 0, count = key.userIDs().size(); i < count; i++) { - const auto address = QString::fromUtf8(key.userID(1).email()); - if (!address.isEmpty()) { - mailBox.fromUnicodeString(address); - break; - } - } - - return formatSignature(*signature, key, mailBox); + return Kleo::Formatting::prettySignature(*signature, {}); } case EncryptionDetails: return QVariant::fromValue(encryptionInfo(messagePart)); case ErrorType: return messagePart->error(); case ErrorString: { switch (messagePart->error()) { case MimeTreeParser::MessagePart::NoKeyError: { if (auto encryptedMessagePart = dynamic_cast<MimeTreeParser::EncryptedMessagePart *>(messagePart)) { if (encryptedMessagePart->isNoSecKey()) { QString errorMessage; if (encryptedMessagePart->cryptoProto() == QGpgME::smime()) { errorMessage += i18ndc("mimetreeparser", "@info:status", "You cannot decrypt this message."); } else { errorMessage += i18ndc("mimetreeparser", "@info:status", "You cannot decrypt this message."); } if (!encryptedMessagePart->decryptRecipients().empty()) { errorMessage += QLatin1Char(' ') + i18ndcp("mimetreeparser", "@info:status", "The message is encrypted for the following recipient:", "The message is encrypted for the following recipients:", encryptedMessagePart->decryptRecipients().size()); errorMessage += MimeTreeParser::decryptRecipientsToHtml(encryptedMessagePart->decryptRecipients(), encryptedMessagePart->cryptoProto()); } return errorMessage; } } } return messagePart->errorString(); case MimeTreeParser::MessagePart::PassphraseError: return i18ndc("mimetreeparser", "@info:status", "Wrong passphrase."); case MimeTreeParser::MessagePart::UnknownError: break; default: break; } return messagePart->errorString(); } } } return QVariant(); } QModelIndex PartModel::parent(const QModelIndex &index) const { if (index.isValid()) { if (auto indexPart = static_cast<MimeTreeParser::MessagePart *>(index.internalPointer())) { for (const auto &part : std::as_const(d->mParts)) { if (part.data() == indexPart) { return QModelIndex(); } } const auto parentPart = d->mParents[indexPart]; Q_ASSERT(parentPart); int row = 0; const auto parts = d->mEncapsulatedParts[parentPart]; for (const auto &part : parts) { if (part.data() == indexPart) { break; } row++; } return createIndex(row, 0, parentPart); } return {}; } return {}; } int PartModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { const auto part = static_cast<MimeTreeParser::MessagePart *>(parent.internalPointer()); auto encapsulatedPart = dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(part); if (encapsulatedPart) { const auto parts = d->mEncapsulatedParts[encapsulatedPart]; return parts.size(); } return 0; } return d->mParts.count(); } int PartModel::columnCount(const QModelIndex &) const { return 1; } #include "moc_partmodel.cpp"