diff --git a/mimetreeparser/autotests/data/openpgp-inline-multiple.mbox b/mimetreeparser/autotests/data/openpgp-inline-multiple.mbox new file mode 100644 index 00000000..e42eccd7 --- /dev/null +++ b/mimetreeparser/autotests/data/openpgp-inline-multiple.mbox @@ -0,0 +1,51 @@ +Subject: Testcase 'reply-decrytion-oracle' (PGP INLINE) +To: brucewayne45@web.de +From: brucewayne45@web.de +MIME-Version: 1.0 +Content-Type: text/plain + +Please reply to this message +. +. +. + +-----BEGIN PGP MESSAGE----- + +hQEMAwzOQ1qnzNo7AQf7BxmM0vO8nG37hKqoqOHb35JqprJM+sqF7JFmrsuWe6V2 +PAyyE2wdtq+AhvXjVnggxYLwU+DEFpBTmWr1rsanyV8hWXRbecfN/9gN/4/N9y7Z +XSx2OeE/uA5z8Kz5vrv/ywMqcVHjB5MQPTcLC2Zlg8MVltpriy6mdAkON4I3t7kl +j9uwQRY7HeKvsib63HWnYAOV/fYPXXor/lioeYIll08uuCiTh3Z9fEhXQI/az5Ft +e/xa70xGqviux+OvhoNUSZspzl7vK7e/NTBlC+LF1zVXUXT8prrd+ZFNwKvtn0Hl +W4KfNqTM9TJB8vpE5FWnH6+B365ZvxZopZ5F/9szp9JGAUCNdX5WujBreg7nTLui +UrnDNwOvjvsE/gsoO3n3jARK+Tu8PfUl8V1bHiCeGJz/mkA9uGJ/IApcT4rYsoHB +nVQjW1NJ6A== +=zrF/ +-----END PGP MESSAGE----- + +-----BEGIN PGP MESSAGE----- + +hQEMAwzOQ1qnzNo7AQf+MCqjMjAB80hAMAHfa7bdk/6L4DJQBQn+zHRv6oYzzYFC +8l79DDIE2uorQNFj1ZBw5+pi7+/2QmAANnG2ug5W0HRphg2WPXTUswy5H+mg08PM +MXRsP9lX5pAXEbLZVp61tvOQHnO/ltBhHHBwRaIq2tiirUUhy5erqLwlkSyN8xHM +Bh0u/dIJw7ewMk0l3BtF/GuP7l6PtUxT7P0Vwit4h1FV1bc9mSFmBNN16dvixJ4l +jK0mYEqT97SNZpg0MPOxx8E3xuJptzea4qmACv5zx4gYHlZRM0ZlKNqffmRauWOe +pDCjZv2F1IUJOg28NzZhKCBVhmhBmP1VmLNYFKGAsNJHAV+3uN2YYWzbhoOJAE0N +UxLI0EQN4y7OkAnGiRH45HygLxAjTk6dPiP5OD9OhUnSqofAjajlmqzfAAVMxY1a +epnRKPsnCZU= +=dqBN +-----END PGP MESSAGE----- + +-----BEGIN PGP MESSAGE----- + +hQEMAwzOQ1qnzNo7AQf+MNDSEBVsF78knI+uirDbLSLrHicrXExTocmXr2DZOggI +zMYCAHyg7ohINA40/8ZuR0bC9h6qCZjjhR+VFe2edRFshXlbuzykjpXNYcSv61Sm +9TAVpgAExzS5VhAxYIJ6+zWJR8+hgv63oREZPWlJ23utBDAMkEeY7cga3wn1HZMZ +g4XQZ94a8s9s/I+s3dLOdHGdxw+hmSnxjMhI6TMcZV/Kvr1MkkW10N0h0+hiuq2O +4owEztpm4See8fCkRfhr0TO+a8ElCtIXjVwqeB0tQh0fU3QaaNiDXYawoFMQXG8N +nwCP92glfOeAvJn9KuLwO3ee+WKwcrJhsFRMmjziDdJGAUvptVDNrk2P/0fzo/Xl +ypmw8zhir6ch+4C2+5yFCtVSmC+3Y7+NQ4YE4AR/z5rGvA1lxclulU1DSGkhFTbJ +XEVyg8o23A== +=Bs3d +-----END PGP MESSAGE----- + +whoo three encrypted parts inside. diff --git a/mimetreeparser/autotests/data/openpgp-inline-multiple.mbox.html b/mimetreeparser/autotests/data/openpgp-inline-multiple.mbox.html new file mode 100644 index 00000000..8b602a22 --- /dev/null +++ b/mimetreeparser/autotests/data/openpgp-inline-multiple.mbox.html @@ -0,0 +1,72 @@ + + + + +
+ +
+
+
Please reply to this message
+
.
+
.
+
.
+
+
+ + + + + + + + + + +
+
Encrypted messageShow Details
+
+
+
first part
+
+
End of encrypted message
+ + + + + + + + + + +
+
Encrypted messageShow Details
+
+
+
second part
+
+
End of encrypted message
+ + + + + + + + + + +
+
Encrypted messageShow Details
+
+
+
third part
+
+
End of encrypted message
+
+
whoo three encrypted parts inside.
+
+
+
+ + diff --git a/mimetreeparser/autotests/data/openpgp-inline-multiple.mbox.tree b/mimetreeparser/autotests/data/openpgp-inline-multiple.mbox.tree new file mode 100644 index 00000000..bbf0edfc --- /dev/null +++ b/mimetreeparser/autotests/data/openpgp-inline-multiple.mbox.tree @@ -0,0 +1,7 @@ + * MimeTreeParser::MessagePartList + * MimeTreeParser::TextMessagePart + * MimeTreeParser::MessagePart + * MimeTreeParser::EncryptedMessagePart + * MimeTreeParser::EncryptedMessagePart + * MimeTreeParser::EncryptedMessagePart + * MimeTreeParser::MessagePart diff --git a/mimetreeparser/src/messagepart.cpp b/mimetreeparser/src/messagepart.cpp index 991b0488..5a64a50e 100644 --- a/mimetreeparser/src/messagepart.cpp +++ b/mimetreeparser/src/messagepart.cpp @@ -1,1364 +1,1369 @@ /* Copyright (c) 2015 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 "messagepart.h" #include "mimetreeparser_debug.h" #include "cryptohelper.h" #include "objecttreeparser.h" #include "job/qgpgmejobexecutor.h" #include "memento/cryptobodypartmemento.h" #include "memento/decryptverifybodypartmemento.h" #include "memento/verifydetachedbodypartmemento.h" #include "memento/verifyopaquebodypartmemento.h" #include "bodyformatter/utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MimeTreeParser; //------MessagePart----------------------- namespace MimeTreeParser { class MessagePartPrivate { public: MessagePart *mParentPart = nullptr; QVector mBlocks; KMime::Content *mNode = nullptr; KMime::Content *mAttachmentNode = nullptr; QString mText; PartMetaData mMetaData; bool mRoot = false; bool mIsImage = false; bool mNeverDisplayInline = false; }; } MessagePart::MessagePart(ObjectTreeParser *otp, const QString &text) : mOtp(otp) , d(new MessagePartPrivate) { d->mText = text; } MessagePart::~MessagePart() = default; MessagePart *MessagePart::parentPart() const { return d->mParentPart; } void MessagePart::setParentPart(MessagePart *parentPart) { d->mParentPart = parentPart; } QString MessagePart::htmlContent() const { return text(); } QString MessagePart::plaintextContent() const { return text(); } PartMetaData *MessagePart::partMetaData() const { return &d->mMetaData; } Interface::BodyPartMemento *MessagePart::memento() const { return nodeHelper()->bodyPartMemento(content(), "__plugin__"); } void MessagePart::setMemento(Interface::BodyPartMemento *memento) { nodeHelper()->setBodyPartMemento(content(), "__plugin__", memento); } KMime::Content *MessagePart::content() const { return d->mNode; } void MessagePart::setContent(KMime::Content *node) { d->mNode = node; } KMime::Content *MessagePart::attachmentContent() const { return d->mAttachmentNode; } void MessagePart::setAttachmentContent(KMime::Content *node) { d->mAttachmentNode = node; } bool MessagePart::isAttachment() const { return d->mAttachmentNode; } QString MessagePart::attachmentIndex() const { return attachmentContent()->index().toString(); } QString MessagePart::attachmentLink() const { return mOtp->nodeHelper()->asHREF(content(), QStringLiteral("body")); } QString MessagePart::makeLink(const QString &path) const { // FIXME: use a PRNG for the first arg, instead of a serial number static int serial = 0; if (path.isEmpty()) { return {}; } return QStringLiteral("x-kmail:/bodypart/%1/%2/%3") .arg(serial++).arg(content()->index().toString()) .arg(QString::fromLatin1(QUrl::toPercentEncoding(path, "/"))); } void MessagePart::setIsRoot(bool root) { d->mRoot = root; } bool MessagePart::isRoot() const { return d->mRoot; } QString MessagePart::text() const { return d->mText; } void MessagePart::setText(const QString &text) { d->mText = text; } bool MessagePart::isHtml() const { return false; } Interface::ObjectTreeSource *MessagePart::source() const { Q_ASSERT(mOtp); return mOtp->mSource; } NodeHelper *MessagePart::nodeHelper() const { Q_ASSERT(mOtp); return mOtp->nodeHelper(); } void MessagePart::parseInternal(KMime::Content *node, bool onlyOneMimePart) { auto subMessagePart = mOtp->parseObjectTreeInternal(node, onlyOneMimePart); d->mRoot = subMessagePart->isRoot(); foreach (const auto &part, subMessagePart->subParts()) { appendSubPart(part); } } QString MessagePart::renderInternalText() const { QString text; foreach (const auto &mp, subParts()) { text += mp->text(); } return text; } void MessagePart::fix() const { foreach (const auto &mp, subParts()) { const auto m = mp.dynamicCast(); if (m) { m->fix(); } } } void MessagePart::appendSubPart(const MessagePart::Ptr &messagePart) { messagePart->setParentPart(this); d->mBlocks.append(messagePart); } const QVector &MessagePart::subParts() const { return d->mBlocks; } bool MessagePart::hasSubParts() const { return !d->mBlocks.isEmpty(); } void MessagePart::clearSubParts() { d->mBlocks.clear(); } bool MessagePart::neverDisplayInline() const { return d->mNeverDisplayInline; } void MessagePart::setNeverDisplayInline(bool displayInline) { d->mNeverDisplayInline = displayInline; } bool MessagePart::isImage() const { return d->mIsImage; } void MessagePart::setIsImage(bool image) { d->mIsImage = image; } //-----MessagePartList---------------------- MessagePartList::MessagePartList(ObjectTreeParser *otp) : MessagePart(otp, QString()) { } MessagePartList::~MessagePartList() { } 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, bool decryptMessage) : MessagePartList(otp) , mDecryptMessage(decryptMessage) { if (!node) { qCWarning(MIMETREEPARSER_LOG) << "not a valid node"; return; } setContent(node); parseContent(); } TextMessagePart::~TextMessagePart() { } bool TextMessagePart::decryptMessage() const { return mDecryptMessage; } void TextMessagePart::parseContent() { const auto aCodec = mOtp->codecFor(content()); const QString &fromAddress = mOtp->nodeHelper()->fromAsString(content()); mSignatureState = KMMsgNotSigned; mEncryptionState = KMMsgNotEncrypted; const auto blocks = prepareMessageForDecryption(content()->decodedContent()); const auto cryptProto = QGpgME::openpgp(); if (!blocks.isEmpty()) { /* 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 unencrypted */ bool fullySignedOrEncrypted = true; bool fullySignedOrEncryptedTmp = true; for (const auto &block : blocks) { if (!fullySignedOrEncryptedTmp) { fullySignedOrEncrypted = false; } if (block.type() == NoPgpBlock && !block.text().trimmed().isEmpty()) { fullySignedOrEncryptedTmp = false; appendSubPart(MessagePart::Ptr(new MessagePart(mOtp, aCodec->toUnicode(block.text())))); } else if (block.type() == PgpMessageBlock) { EncryptedMessagePart::Ptr mp(new EncryptedMessagePart(mOtp, QString(), cryptProto, fromAddress, nullptr)); mp->setDecryptMessage(decryptMessage()); mp->setIsEncrypted(true); appendSubPart(mp); if (!decryptMessage()) { continue; } mp->startDecryption(block.text(), aCodec); if (mp->partMetaData()->inProgress) { continue; } } else if (block.type() == ClearsignedBlock) { SignedMessagePart::Ptr mp(new SignedMessagePart(mOtp, QString(), cryptProto, fromAddress, nullptr)); appendSubPart(mp); mp->startVerification(block.text(), aCodec); } else { continue; } const auto mp = subParts().last().staticCast(); const PartMetaData *messagePart(mp->partMetaData()); if (!messagePart->isEncrypted && !messagePart->isSigned && !block.text().trimmed().isEmpty()) { mp->setText(aCodec->toUnicode(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 { return mEncryptionState; } KMMsgSignatureState TextMessagePart::signatureState() const { return mSignatureState; } bool TextMessagePart::showLink() const { return !temporaryFilePath().isEmpty(); } bool TextMessagePart::isFirstTextPart() const { return content()->topLevel()->textContent() == content(); } bool TextMessagePart::hasLabel() const { return !NodeHelper::fileName(content()).isEmpty(); } QString TextMessagePart::label() const { const QString name = content()->contentType()->name(); QString label = name.isEmpty() ? NodeHelper::fileName(content()) : name; if (label.isEmpty()) { label = i18nc("display name for an unnamed attachment", "Unnamed"); } return label; } QString TextMessagePart::comment() const { const QString comment = content()->contentDescription()->asUnicodeString(); if (comment == label()) { return {}; } return comment; } QString TextMessagePart::temporaryFilePath() const { return nodeHelper()->writeNodeToTempFile(content()); } //-----AttachmentMessageBlock---------------------- AttachmentMessagePart::AttachmentMessagePart(ObjectTreeParser *otp, KMime::Content *node, bool decryptMessage) : TextMessagePart(otp, node, decryptMessage) { } AttachmentMessagePart::~AttachmentMessagePart() { } //-----HtmlMessageBlock---------------------- HtmlMessagePart::HtmlMessagePart(ObjectTreeParser *otp, KMime::Content *node, Interface::ObjectTreeSource *source) : MessagePart(otp, QString()) , mSource(source) { if (!node) { qCWarning(MIMETREEPARSER_LOG) << "not a valid node"; return; } setContent(node); const QByteArray partBody(node->decodedContent()); mBodyHTML = mOtp->codecFor(node)->toUnicode(partBody); mCharset = NodeHelper::charset(node); } HtmlMessagePart::~HtmlMessagePart() { } void HtmlMessagePart::fix() const { mOtp->mHtmlContent += mBodyHTML; mOtp->mHtmlContentCharset = mCharset; } QString HtmlMessagePart::text() const { return mBodyHTML; } +QString MimeTreeParser::HtmlMessagePart::plaintextContent() const +{ + return QString(); +} + bool HtmlMessagePart::isHtml() const { return true; } QString HtmlMessagePart::bodyHtml() const { return mBodyHTML; } //-----MimeMessageBlock---------------------- MimeMessagePart::MimeMessagePart(ObjectTreeParser *otp, KMime::Content *node, bool onlyOneMimePart) : MessagePart(otp, QString()) , mOnlyOneMimePart(onlyOneMimePart) { if (!node) { qCWarning(MIMETREEPARSER_LOG) << "not a valid node"; return; } setContent(node); parseInternal(node, mOnlyOneMimePart); } 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, Util::HtmlMode preferredMode) : MessagePart(otp, QString()) , mPreferredMode(preferredMode) { setContent(node); KMime::Content *dataIcal = findTypeInDirectChilds(node, "text/calendar"); KMime::Content *dataHtml = findTypeInDirectChilds(node, "text/html"); KMime::Content *dataText = findTypeInDirectChilds(node, "text/plain"); if (!dataHtml) { // 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. dataHtml = findTypeInDirectChilds(node, "multipart/related"); // Still not found? Stupid apple mail actually puts the attachments inside of the // multipart/alternative, which is wrong. Therefore we also have to look for multipart/mixed // here. // Do this only when preferring HTML mail, though, since otherwise the attachments are hidden // when displaying plain text. if (!dataHtml) { dataHtml = findTypeInDirectChilds(node, "multipart/mixed"); } } if (dataIcal) { mChildNodes[Util::MultipartIcal] = dataIcal; } if (dataText) { mChildNodes[Util::MultipartPlain] = dataText; } if (dataHtml) { mChildNodes[Util::MultipartHtml] = dataHtml; } if (mChildNodes.isEmpty()) { qCWarning(MIMETREEPARSER_LOG) << "no valid nodes"; return; } QMapIterator i(mChildNodes); while (i.hasNext()) { i.next(); mChildParts[i.key()] = MimeMessagePart::Ptr(new MimeMessagePart(mOtp, i.value(), true)); } } AlternativeMessagePart::~AlternativeMessagePart() { } Util::HtmlMode AlternativeMessagePart::preferredMode() const { return mPreferredMode; } void AlternativeMessagePart::setPreferredMode(Util::HtmlMode preferredMode) { mPreferredMode = preferredMode; } QList AlternativeMessagePart::availableModes() { return mChildParts.keys(); } QString AlternativeMessagePart::text() const { if (mChildParts.contains(Util::MultipartPlain)) { return mChildParts[Util::MultipartPlain]->text(); } return QString(); } void AlternativeMessagePart::fix() const { if (mChildParts.contains(Util::MultipartPlain)) { mChildParts[Util::MultipartPlain]->fix(); } const auto mode = preferredMode(); if (mode != Util::MultipartPlain && mChildParts.contains(mode)) { mChildParts[mode]->fix(); } } const QMap &AlternativeMessagePart::childParts() const { return mChildParts; } bool AlternativeMessagePart::isHtml() const { return mChildParts.contains(Util::MultipartHtml); } QString AlternativeMessagePart::plaintextContent() const { return text(); } QString AlternativeMessagePart::htmlContent() const { if (mChildParts.contains(Util::MultipartHtml)) { return mChildParts[Util::MultipartHtml]->text(); } else { return plaintextContent(); } } //-----CertMessageBlock---------------------- CertMessagePart::CertMessagePart(ObjectTreeParser *otp, KMime::Content *node, const QGpgME::Protocol *cryptoProto, bool autoImport) : MessagePart(otp, QString()) , mAutoImport(autoImport) , mCryptoProto(cryptoProto) { if (!node) { qCWarning(MIMETREEPARSER_LOG) << "not a valid node"; return; } setContent(node); if (!mAutoImport) { return; } const QByteArray certData = node->decodedContent(); QGpgME::ImportJob *import = mCryptoProto->importJob(); QGpgMEJobExecutor executor; mImportResult = executor.exec(import, certData); } CertMessagePart::~CertMessagePart() { } QString CertMessagePart::text() const { return QString(); } const GpgME::ImportResult &CertMessagePart::importResult() const { return mImportResult; } //-----SignedMessageBlock--------------------- SignedMessagePart::SignedMessagePart(ObjectTreeParser *otp, const QString &text, const QGpgME::Protocol *cryptoProto, const QString &fromAddress, KMime::Content *node) : MessagePart(otp, text) , mCryptoProto(cryptoProto) , mFromAddress(fromAddress) { setContent(node); partMetaData()->technicalProblem = (mCryptoProto == nullptr); partMetaData()->isSigned = true; partMetaData()->isGoodSignature = false; partMetaData()->keyTrust = GpgME::Signature::Unknown; partMetaData()->status = i18n("Wrong Crypto Plug-In."); partMetaData()->status_code = GPGME_SIG_STAT_NONE; } SignedMessagePart::~SignedMessagePart() { } void SignedMessagePart::setIsSigned(bool isSigned) { partMetaData()->isSigned = isSigned; } bool SignedMessagePart::isSigned() const { return partMetaData()->isSigned; } bool SignedMessagePart::okVerify(const QByteArray &data, const QByteArray &signature, KMime::Content *textNode) { NodeHelper *nodeHelper = mOtp->nodeHelper(); partMetaData()->isSigned = false; partMetaData()->technicalProblem = (mCryptoProto == nullptr); partMetaData()->keyTrust = GpgME::Signature::Unknown; partMetaData()->status = i18n("Wrong Crypto Plug-In."); partMetaData()->status_code = GPGME_SIG_STAT_NONE; const QByteArray mementoName = "verification"; CryptoBodyPartMemento *m = dynamic_cast(nodeHelper->bodyPartMemento(content(), mementoName)); Q_ASSERT(!m || mCryptoProto); //No CryptoPlugin and having a bodyPartMemento -> there is something completely wrong if (!m && mCryptoProto) { if (!signature.isEmpty()) { QGpgME::VerifyDetachedJob *job = mCryptoProto->verifyDetachedJob(); if (job) { m = new VerifyDetachedBodyPartMemento(job, mCryptoProto->keyListJob(), signature, data); } } else { QGpgME::VerifyOpaqueJob *job = mCryptoProto->verifyOpaqueJob(); if (job) { m = new VerifyOpaqueBodyPartMemento(job, mCryptoProto->keyListJob(), data); } } if (m) { if (mOtp->allowAsync()) { QObject::connect(m, &CryptoBodyPartMemento::update, nodeHelper, &NodeHelper::update); if (m->start()) { partMetaData()->inProgress = true; mOtp->mHasPendingAsyncJobs = true; } } else { m->exec(); } nodeHelper->setBodyPartMemento(content(), mementoName, m); } } else if (m && m->isRunning()) { partMetaData()->inProgress = true; mOtp->mHasPendingAsyncJobs = true; } else { partMetaData()->inProgress = false; mOtp->mHasPendingAsyncJobs = false; } if (m && !partMetaData()->inProgress) { if (!signature.isEmpty()) { mVerifiedText = data; } setVerificationResult(m, textNode); } if (!m && !partMetaData()->inProgress) { QString errorMsg; QString cryptPlugLibName; QString cryptPlugDisplayName; if (mCryptoProto) { cryptPlugLibName = mCryptoProto->name(); cryptPlugDisplayName = mCryptoProto->displayName(); } if (!mCryptoProto) { if (cryptPlugDisplayName.isEmpty()) { errorMsg = i18n("No appropriate crypto plug-in was found."); } else { errorMsg = i18nc("%1 is either 'OpenPGP' or 'S/MIME'", "No %1 plug-in was found.", cryptPlugDisplayName); } } else { errorMsg = i18n("Crypto plug-in \"%1\" cannot verify signatures.", cryptPlugLibName); } partMetaData()->errorText = i18n("The message is signed, but the " "validity of the signature cannot be " "verified.
" "Reason: %1", errorMsg); } return partMetaData()->isSigned; } static int signatureToStatus(const GpgME::Signature &sig) { switch (sig.status().code()) { case GPG_ERR_NO_ERROR: return GPGME_SIG_STAT_GOOD; case GPG_ERR_BAD_SIGNATURE: return GPGME_SIG_STAT_BAD; case GPG_ERR_NO_PUBKEY: return GPGME_SIG_STAT_NOKEY; case GPG_ERR_NO_DATA: return GPGME_SIG_STAT_NOSIG; case GPG_ERR_SIG_EXPIRED: return GPGME_SIG_STAT_GOOD_EXP; case GPG_ERR_KEY_EXPIRED: return GPGME_SIG_STAT_GOOD_EXPKEY; default: return GPGME_SIG_STAT_ERROR; } } QString prettifyDN(const char *uid) { return QGpgME::DN(uid).prettyDN(); } void SignedMessagePart::sigStatusToMetaData() { GpgME::Key key; if (partMetaData()->isSigned) { GpgME::Signature signature = mSignatures.front(); partMetaData()->status_code = signatureToStatus(signature); partMetaData()->isGoodSignature = partMetaData()->status_code & GPGME_SIG_STAT_GOOD; // save extended signature status flags partMetaData()->sigSummary = signature.summary(); if (partMetaData()->isGoodSignature && !key.keyID()) { // Search for the key by its fingerprint so that we can check for // trust etc. QGpgME::KeyListJob *job = mCryptoProto->keyListJob(false, false, false); // local, no sigs if (!job) { qCDebug(MIMETREEPARSER_LOG) << "The Crypto backend does not support listing keys. "; } else { std::vector found_keys; // As we are local it is ok to make this synchronous GpgME::KeyListResult res = job->exec(QStringList(QLatin1String(signature.fingerprint())), false, found_keys); if (res.error()) { qCDebug(MIMETREEPARSER_LOG) << "Error while searching key for Fingerprint: " << signature.fingerprint(); } if (found_keys.size() > 1) { // Should not Happen qCDebug(MIMETREEPARSER_LOG) << "Oops: Found more then one Key for Fingerprint: " << signature.fingerprint(); } if (found_keys.size() != 1) { // Should not Happen at this point qCDebug(MIMETREEPARSER_LOG) << "Oops: Found no Key for Fingerprint: " << signature.fingerprint(); } else { key = found_keys[0]; } delete job; } } if (key.keyID()) { partMetaData()->keyId = key.keyID(); } if (partMetaData()->keyId.isEmpty()) { partMetaData()->keyId = signature.fingerprint(); } partMetaData()->keyTrust = signature.validity(); if (key.numUserIDs() > 0 && key.userID(0).id()) { partMetaData()->signer = prettifyDN(key.userID(0).id()); } for (uint iMail = 0; iMail < key.numUserIDs(); ++iMail) { // The following if /should/ always result in TRUE but we // won't trust implicitly the plugin that gave us these data. if (key.userID(iMail).email()) { QString email = QString::fromUtf8(key.userID(iMail).email()); // ### work around gpgme 0.3.QString text() const override;x / cryptplug bug where the // ### email addresses are specified as angle-addr, not addr-spec: if (email.startsWith(QLatin1Char('<')) && email.endsWith(QLatin1Char('>'))) { email = email.mid(1, email.length() - 2); } if (!email.isEmpty()) { partMetaData()->signerMailAddresses.append(email); } } } if (signature.creationTime()) { partMetaData()->creationTime.setSecsSinceEpoch(signature.creationTime()); } else { partMetaData()->creationTime = QDateTime(); } if (partMetaData()->signer.isEmpty()) { if (key.numUserIDs() > 0 && key.userID(0).name()) { partMetaData()->signer = prettifyDN(key.userID(0).name()); } if (!partMetaData()->signerMailAddresses.empty()) { if (partMetaData()->signer.isEmpty()) { partMetaData()->signer = partMetaData()->signerMailAddresses.front(); } else { partMetaData()->signer += QLatin1String(" <") + partMetaData()->signerMailAddresses.front() + QLatin1Char('>'); } } } } } void SignedMessagePart::startVerification(const QByteArray &text, const QTextCodec *aCodec) { startVerificationDetached(text, nullptr, QByteArray()); if (!content() && partMetaData()->isSigned) { setText(aCodec->toUnicode(mVerifiedText)); } } void SignedMessagePart::startVerificationDetached(const QByteArray &text, KMime::Content *textNode, const QByteArray &signature) { partMetaData()->isEncrypted = false; partMetaData()->isDecryptable = false; if (textNode) { parseInternal(textNode, false); } okVerify(text, signature, textNode); if (!partMetaData()->isSigned) { partMetaData()->creationTime = QDateTime(); } } void SignedMessagePart::setVerificationResult(const CryptoBodyPartMemento *m, KMime::Content *textNode) { { const auto vm = dynamic_cast(m); if (vm) { mSignatures = vm->verifyResult().signatures(); } } { const auto vm = dynamic_cast(m); if (vm) { mVerifiedText = vm->plainText(); mSignatures = vm->verifyResult().signatures(); } } { const auto vm = dynamic_cast(m); if (vm) { mVerifiedText = vm->plainText(); mSignatures = vm->verifyResult().signatures(); } } partMetaData()->auditLogError = m->auditLogError(); partMetaData()->auditLog = m->auditLogAsHtml(); partMetaData()->isSigned = !mSignatures.empty(); if (partMetaData()->isSigned) { sigStatusToMetaData(); if (content()) { mOtp->nodeHelper()->setSignatureState(content(), KMMsgFullySigned); if (!textNode) { mOtp->nodeHelper()->setPartMetaData(content(), *partMetaData()); if (!mVerifiedText.isEmpty()) { auto tempNode = new KMime::Content(); tempNode->setContent(KMime::CRLFtoLF(mVerifiedText.constData())); tempNode->parse(); if (!tempNode->head().isEmpty()) { tempNode->contentDescription()->from7BitString("signed data"); } mOtp->nodeHelper()->attachExtraContent(content(), tempNode); parseInternal(tempNode, false); } } } } } QString SignedMessagePart::plaintextContent() const { if (!content()) { return MessagePart::text(); } else { return QString(); } } QString SignedMessagePart::htmlContent() const { if (!content()) { return MessagePart::text(); } else { return QString(); } } const QGpgME::Protocol *SignedMessagePart::cryptoProto() const { return mCryptoProto; } QString SignedMessagePart::fromAddress() const { return mFromAddress; } //-----CryptMessageBlock--------------------- EncryptedMessagePart::EncryptedMessagePart(ObjectTreeParser *otp, const QString &text, const QGpgME::Protocol *cryptoProto, const QString &fromAddress, KMime::Content *node) : MessagePart(otp, text) , mPassphraseError(false) , mNoSecKey(false) , mCryptoProto(cryptoProto) , mFromAddress(fromAddress) , mDecryptMessage(false) { setContent(node); partMetaData()->technicalProblem = (mCryptoProto == nullptr); partMetaData()->isSigned = false; partMetaData()->isGoodSignature = false; partMetaData()->isEncrypted = false; partMetaData()->isDecryptable = false; partMetaData()->keyTrust = GpgME::Signature::Unknown; partMetaData()->status = i18n("Wrong Crypto Plug-In."); partMetaData()->status_code = GPGME_SIG_STAT_NONE; } EncryptedMessagePart::~EncryptedMessagePart() { } void EncryptedMessagePart::setDecryptMessage(bool decrypt) { mDecryptMessage = decrypt; } bool EncryptedMessagePart::decryptMessage() const { return mDecryptMessage; } void EncryptedMessagePart::setIsEncrypted(bool encrypted) { partMetaData()->isEncrypted = encrypted; } bool EncryptedMessagePart::isEncrypted() const { return partMetaData()->isEncrypted; } bool EncryptedMessagePart::isDecryptable() const { return partMetaData()->isDecryptable; } bool EncryptedMessagePart::isNoSecKey() const { return mNoSecKey; } bool EncryptedMessagePart::passphraseError() const { return mPassphraseError; } void EncryptedMessagePart::startDecryption(const QByteArray &text, const QTextCodec *aCodec) { KMime::Content *content = new KMime::Content; content->setBody(text); content->parse(); startDecryption(content); if (!partMetaData()->inProgress && partMetaData()->isDecryptable) { if (hasSubParts()) { auto _mp = (subParts()[0]).dynamicCast(); if (_mp) { _mp->setText(aCodec->toUnicode(mDecryptedData)); } else { setText(aCodec->toUnicode(mDecryptedData)); } } else { setText(aCodec->toUnicode(mDecryptedData)); } } } bool EncryptedMessagePart::okDecryptMIME(KMime::Content &data) { mPassphraseError = false; partMetaData()->inProgress = false; partMetaData()->errorText.clear(); partMetaData()->auditLogError = GpgME::Error(); partMetaData()->auditLog.clear(); bool bDecryptionOk = false; bool cannotDecrypt = false; NodeHelper *nodeHelper = mOtp->nodeHelper(); Q_ASSERT(decryptMessage()); // Check whether the memento contains a result from last time: const DecryptVerifyBodyPartMemento *m = dynamic_cast(nodeHelper->bodyPartMemento(&data, "decryptverify")); Q_ASSERT(!m || mCryptoProto); //No CryptoPlugin and having a bodyPartMemento -> there is something completely wrong if (!m && mCryptoProto) { QGpgME::DecryptVerifyJob *job = mCryptoProto->decryptVerifyJob(); if (!job) { cannotDecrypt = true; } else { const QByteArray ciphertext = data.decodedContent(); DecryptVerifyBodyPartMemento *newM = new DecryptVerifyBodyPartMemento(job, ciphertext); if (mOtp->allowAsync()) { QObject::connect(newM, &CryptoBodyPartMemento::update, nodeHelper, &NodeHelper::update); if (newM->start()) { partMetaData()->inProgress = true; mOtp->mHasPendingAsyncJobs = true; } else { m = newM; } } else { newM->exec(); m = newM; } nodeHelper->setBodyPartMemento(&data, "decryptverify", newM); } } else if (m && m->isRunning()) { partMetaData()->inProgress = true; mOtp->mHasPendingAsyncJobs = true; m = nullptr; } if (m) { const QByteArray &plainText = m->plainText(); const GpgME::DecryptionResult &decryptResult = m->decryptResult(); const GpgME::VerificationResult &verifyResult = m->verifyResult(); partMetaData()->isSigned = verifyResult.signatures().size() > 0; if (partMetaData()->isSigned) { auto subPart = SignedMessagePart::Ptr(new SignedMessagePart(mOtp, MessagePart::text(), mCryptoProto, mFromAddress, content())); subPart->setVerificationResult(m, nullptr); appendSubPart(subPart); } mDecryptRecipients.clear(); bDecryptionOk = !decryptResult.error(); // std::stringstream ss; // ss << decryptResult << '\n' << verifyResult; // qCDebug(MIMETREEPARSER_LOG) << ss.str().c_str(); for (const auto &recipient : decryptResult.recipients()) { if (!recipient.status()) { bDecryptionOk = true; } GpgME::Key key; QGpgME::KeyListJob *job = mCryptoProto->keyListJob(false, false, false); // local, no sigs if (!job) { qCDebug(MIMETREEPARSER_LOG) << "The Crypto backend does not support listing keys. "; } else { std::vector found_keys; // As we are local it is ok to make this synchronous GpgME::KeyListResult res = job->exec(QStringList(QLatin1String(recipient.keyID())), false, found_keys); if (res.error()) { qCDebug(MIMETREEPARSER_LOG) << "Error while searching key for Fingerprint: " << recipient.keyID(); } if (found_keys.size() > 1) { // Should not Happen qCDebug(MIMETREEPARSER_LOG) << "Oops: Found more then one Key for Fingerprint: " << recipient.keyID(); } if (found_keys.size() != 1) { // Should not Happen at this point qCDebug(MIMETREEPARSER_LOG) << "Oops: Found no Key for Fingerprint: " << recipient.keyID(); } else { key = found_keys[0]; } } mDecryptRecipients.push_back(std::make_pair(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_NO_SECKEY; partMetaData()->isEncrypted = bDecryptionOk || decryptResult.error().code() != GPG_ERR_NO_DATA; partMetaData()->errorText = QString::fromLocal8Bit(decryptResult.error().asString()); if (partMetaData()->isEncrypted && decryptResult.numRecipients() > 0) { partMetaData()->keyId = decryptResult.recipient(0).keyID(); } if (bDecryptionOk) { mDecryptedData = plainText; } else { mNoSecKey = true; foreach (const GpgME::DecryptionResult::Recipient &recipient, decryptResult.recipients()) { mNoSecKey &= (recipient.status().code() == GPG_ERR_NO_SECKEY); } if (!mPassphraseError && !mNoSecKey) { // GpgME do not detect passphrase error correctly mPassphraseError = true; } } } } if (!bDecryptionOk) { QString cryptPlugLibName; if (mCryptoProto) { cryptPlugLibName = mCryptoProto->name(); } if (!mCryptoProto) { partMetaData()->errorText = i18n("No appropriate crypto plug-in was found."); } else if (cannotDecrypt) { partMetaData()->errorText = i18n("Crypto plug-in \"%1\" cannot decrypt messages.", cryptPlugLibName); } else if (!passphraseError()) { partMetaData()->errorText = i18n("Crypto plug-in \"%1\" could not decrypt the data.", cryptPlugLibName) + QLatin1String("
") + i18n("Error: %1", partMetaData()->errorText); } } return bDecryptionOk; } void EncryptedMessagePart::startDecryption(KMime::Content *data) { if (!content() && !data) { return; } if (!data) { data = content(); } partMetaData()->isEncrypted = true; bool bOkDecrypt = okDecryptMIME(*data); if (partMetaData()->inProgress) { return; } partMetaData()->isDecryptable = bOkDecrypt; if (!partMetaData()->isDecryptable) { setText(QString::fromUtf8(mDecryptedData.constData())); } if (partMetaData()->isEncrypted && !decryptMessage()) { partMetaData()->isDecryptable = true; } if (content() && !partMetaData()->isSigned) { mOtp->nodeHelper()->setPartMetaData(content(), *partMetaData()); if (decryptMessage()) { auto tempNode = new KMime::Content(); tempNode->setContent(KMime::CRLFtoLF(mDecryptedData.constData())); tempNode->parse(); if (!tempNode->head().isEmpty()) { tempNode->contentDescription()->from7BitString("encrypted data"); } mOtp->nodeHelper()->attachExtraContent(content(), tempNode); parseInternal(tempNode, false); } } } QString EncryptedMessagePart::plaintextContent() const { if (!content()) { return MessagePart::text(); } else { return QString(); } } QString EncryptedMessagePart::htmlContent() const { if (!content()) { return MessagePart::text(); } else { return QString(); } } QString EncryptedMessagePart::text() const { if (hasSubParts()) { auto _mp = (subParts()[0]).dynamicCast(); if (_mp) { return _mp->text(); } else { return MessagePart::text(); } } else { return MessagePart::text(); } } const QGpgME::Protocol *EncryptedMessagePart::cryptoProto() const { return mCryptoProto; } QString EncryptedMessagePart::fromAddress() const { return mFromAddress; } const std::vector > &EncryptedMessagePart::decryptRecipients() const { return mDecryptRecipients; } EncapsulatedRfc822MessagePart::EncapsulatedRfc822MessagePart(ObjectTreeParser *otp, KMime::Content *node, const KMime::Message::Ptr &message) : MessagePart(otp, QString()) , mMessage(message) { setContent(node); partMetaData()->isEncrypted = false; partMetaData()->isSigned = false; partMetaData()->isEncapsulatedRfc822Message = true; mOtp->nodeHelper()->setNodeDisplayedEmbedded(node, true); mOtp->nodeHelper()->setPartMetaData(node, *partMetaData()); if (!mMessage) { qCWarning(MIMETREEPARSER_LOG) << "Node is of type message/rfc822 but doesn't have a message!"; return; } // The link to "Encapsulated message" is clickable, therefore the temp file needs to exists, // since the user can click the link and expect to have normal attachment operations there. mOtp->nodeHelper()->writeNodeToTempFile(message.data()); parseInternal(message.data(), false); } EncapsulatedRfc822MessagePart::~EncapsulatedRfc822MessagePart() { } QString EncapsulatedRfc822MessagePart::text() const { return renderInternalText(); } void EncapsulatedRfc822MessagePart::fix() const { } const KMime::Message::Ptr EncapsulatedRfc822MessagePart::message() const { return mMessage; } diff --git a/mimetreeparser/src/messagepart.h b/mimetreeparser/src/messagepart.h index 2a3e9981..6fc60855 100644 --- a/mimetreeparser/src/messagepart.h +++ b/mimetreeparser/src/messagepart.h @@ -1,419 +1,420 @@ /* Copyright (c) 2015 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. */ #ifndef MIMETREEPARSER_MESSAGEPART_H #define MIMETREEPARSER_MESSAGEPART_H #include "mimetreeparser_export.h" #include "mimetreeparser/bodypartformatter.h" #include "mimetreeparser/util.h" #include #include #include #include #include #include #include #include class QTextCodec; namespace GpgME { class ImportResult; } namespace QGpgME { class Protocol; } namespace KMime { class Content; } namespace MimeTreeParser { class CryptoBodyPartMemento; class MessagePartPrivate; namespace Interface { class ObjectTreeSource; } class MIMETREEPARSER_EXPORT MessagePart : public QObject { Q_OBJECT Q_PROPERTY(QString plaintextContent READ plaintextContent) Q_PROPERTY(QString htmlContent READ htmlContent) Q_PROPERTY(bool isAttachment READ isAttachment) Q_PROPERTY(bool root READ isRoot) Q_PROPERTY(bool isHtml READ isHtml) Q_PROPERTY(bool isImage READ isImage CONSTANT) Q_PROPERTY(bool neverDisplayInline READ neverDisplayInline CONSTANT) Q_PROPERTY(QString attachmentIndex READ attachmentIndex CONSTANT) Q_PROPERTY(QString link READ attachmentLink CONSTANT) public: typedef QSharedPointer Ptr; MessagePart(ObjectTreeParser *otp, const QString &text); ~MessagePart(); void setParentPart(MessagePart *parentPart); MessagePart *parentPart() const; virtual QString text() const; void setText(const QString &text); virtual QString plaintextContent() const; virtual QString htmlContent() const; /** The KMime::Content* node that's represented by this part. * Can be @c nullptr, e.g. for sub-parts of an inline signed body part. */ KMime::Content *content() const; void setContent(KMime::Content *node); /** The KMime::Content* node that's the source of this part. * This is not necessarily the same as content(), for example for * broken-up multipart nodes. */ KMime::Content *attachmentContent() const; void setAttachmentContent(KMime::Content *node); bool isAttachment() const; /** @see KMime::Content::index() */ QString attachmentIndex() const; /** @see NodeHelper::asHREF */ QString attachmentLink() const; /** Returns a string representation of an URL that can be used * to invoke a BodyPartURLHandler for this body part. */ QString makeLink(const QString &path) const; void setIsRoot(bool root); bool isRoot() const; virtual bool isHtml() const; bool neverDisplayInline() const; void setNeverDisplayInline(bool displayInline); bool isImage() const; void setIsImage(bool image); PartMetaData *partMetaData() const; Interface::BodyPartMemento *memento() const; void setMemento(Interface::BodyPartMemento *memento); /* only a function that should be removed if the refactoring is over */ virtual void fix() const; void appendSubPart(const MessagePart::Ptr &messagePart); const QVector &subParts() const; bool hasSubParts() const; void clearSubParts(); Interface::ObjectTreeSource *source() const; NodeHelper *nodeHelper() const; protected: void parseInternal(KMime::Content *node, bool onlyOneMimePart); QString renderInternalText() const; ObjectTreeParser *mOtp = nullptr; private: std::unique_ptr d; }; class MIMETREEPARSER_EXPORT MimeMessagePart : public MessagePart { Q_OBJECT public: typedef QSharedPointer Ptr; MimeMessagePart(MimeTreeParser::ObjectTreeParser *otp, KMime::Content *node, bool onlyOneMimePart); ~MimeMessagePart() override; QString text() const override; QString plaintextContent() const override; QString htmlContent() const override; private: bool mOnlyOneMimePart; }; class MIMETREEPARSER_EXPORT MessagePartList : public MessagePart { Q_OBJECT public: typedef QSharedPointer Ptr; explicit MessagePartList(MimeTreeParser::ObjectTreeParser *otp); ~MessagePartList() override; QString text() const override; QString plaintextContent() const override; QString htmlContent() const override; }; enum IconType { NoIcon = 0, IconExternal, IconInline }; class MIMETREEPARSER_EXPORT TextMessagePart : public MessagePartList { Q_OBJECT Q_PROPERTY(bool showLink READ showLink CONSTANT) Q_PROPERTY(bool isFirstTextPart READ isFirstTextPart CONSTANT) Q_PROPERTY(bool hasLabel READ hasLabel CONSTANT) Q_PROPERTY(QString label READ label CONSTANT) Q_PROPERTY(QString comment READ comment CONSTANT) public: typedef QSharedPointer Ptr; TextMessagePart(MimeTreeParser::ObjectTreeParser *otp, KMime::Content *node, bool decryptMessage); ~TextMessagePart() override; KMMsgSignatureState signatureState() const; KMMsgEncryptionState encryptionState() const; bool decryptMessage() const; bool showLink() const; bool isFirstTextPart() const; bool hasLabel() const; /** The attachment filename, or the closest approximation thereof we have. */ QString label() const; /** A description of this attachment, if provided. */ QString comment() const; /** Temporary file containing the part content. */ QString temporaryFilePath() const; private: void parseContent(); KMMsgSignatureState mSignatureState; KMMsgEncryptionState mEncryptionState; bool mDecryptMessage; }; class MIMETREEPARSER_EXPORT AttachmentMessagePart : public TextMessagePart { Q_OBJECT public: typedef QSharedPointer Ptr; AttachmentMessagePart(MimeTreeParser::ObjectTreeParser *otp, KMime::Content *node, bool decryptMessage); ~AttachmentMessagePart() override; }; class MIMETREEPARSER_EXPORT HtmlMessagePart : public MessagePart { Q_OBJECT public: typedef QSharedPointer Ptr; HtmlMessagePart(MimeTreeParser::ObjectTreeParser *otp, KMime::Content *node, MimeTreeParser::Interface::ObjectTreeSource *source); ~HtmlMessagePart() override; QString text() const override; + QString plaintextContent() const override; void fix() const override; bool isHtml() const override; QString bodyHtml() const; private: Interface::ObjectTreeSource *mSource; QString mBodyHTML; QByteArray mCharset; }; class MIMETREEPARSER_EXPORT AlternativeMessagePart : public MessagePart { Q_OBJECT public: typedef QSharedPointer Ptr; AlternativeMessagePart(MimeTreeParser::ObjectTreeParser *otp, KMime::Content *node, Util::HtmlMode preferredMode); ~AlternativeMessagePart() override; QString text() const override; Util::HtmlMode preferredMode() const; void setPreferredMode(Util::HtmlMode preferredMode); bool isHtml() const override; QString plaintextContent() const override; QString htmlContent() const override; QList availableModes(); void fix() const override; const QMap &childParts() const; private: Util::HtmlMode mPreferredMode; QMap mChildNodes; QMap mChildParts; }; class MIMETREEPARSER_EXPORT CertMessagePart : public MessagePart { Q_OBJECT public: typedef QSharedPointer Ptr; CertMessagePart(MimeTreeParser::ObjectTreeParser *otp, KMime::Content *node, const QGpgME::Protocol *cryptoProto, bool autoImport); ~CertMessagePart() override; QString text() const override; const GpgME::ImportResult &importResult() const; private: bool mAutoImport; GpgME::ImportResult mImportResult; const QGpgME::Protocol *mCryptoProto; }; class MIMETREEPARSER_EXPORT EncapsulatedRfc822MessagePart : public MessagePart { Q_OBJECT public: typedef QSharedPointer Ptr; EncapsulatedRfc822MessagePart(MimeTreeParser::ObjectTreeParser *otp, KMime::Content *node, const KMime::Message::Ptr &message); ~EncapsulatedRfc822MessagePart() override; QString text() const override; void fix() const override; const KMime::Message::Ptr message() const; private: const KMime::Message::Ptr mMessage; }; class MIMETREEPARSER_EXPORT EncryptedMessagePart : public MessagePart { Q_OBJECT Q_PROPERTY(bool decryptMessage READ decryptMessage WRITE setDecryptMessage) Q_PROPERTY(bool isEncrypted READ isEncrypted) Q_PROPERTY(bool isNoSecKey READ isNoSecKey) Q_PROPERTY(bool passphraseError READ passphraseError) public: typedef QSharedPointer Ptr; EncryptedMessagePart(ObjectTreeParser *otp, const QString &text, const QGpgME::Protocol *cryptoProto, const QString &fromAddress, KMime::Content *node); ~EncryptedMessagePart() override; QString text() const override; void setDecryptMessage(bool decrypt); bool decryptMessage() const; void setIsEncrypted(bool encrypted); bool isEncrypted() const; bool isDecryptable() const; bool isNoSecKey() const; bool passphraseError() const; void startDecryption(const QByteArray &text, const QTextCodec *aCodec); void startDecryption(KMime::Content *data = nullptr); QByteArray mDecryptedData; QString plaintextContent() const override; QString htmlContent() const override; const QGpgME::Protocol *cryptoProto() const; QString fromAddress() const; const std::vector > &decryptRecipients() const; private: /** Handles the decryption of a given content * returns true if the decryption was successful * if used in async mode, check if mMetaData.inPogress is true, it initiates a running decryption process. */ bool okDecryptMIME(KMime::Content &data); protected: bool mPassphraseError; bool mNoSecKey; const QGpgME::Protocol *mCryptoProto; QString mFromAddress; bool mDecryptMessage; QByteArray mVerifiedText; std::vector > mDecryptRecipients; friend class EncryptedBodyPartFormatter; }; class MIMETREEPARSER_EXPORT SignedMessagePart : public MessagePart { Q_OBJECT Q_PROPERTY(bool isSigned READ isSigned) public: typedef QSharedPointer Ptr; SignedMessagePart(ObjectTreeParser *otp, const QString &text, const QGpgME::Protocol *cryptoProto, const QString &fromAddress, KMime::Content *node); ~SignedMessagePart() override; void setIsSigned(bool isSigned); bool isSigned() const; void startVerification(const QByteArray &text, const QTextCodec *aCodec); void startVerificationDetached(const QByteArray &text, KMime::Content *textNode, const QByteArray &signature); QByteArray mDecryptedData; std::vector mSignatures; QString plaintextContent() const override; QString htmlContent() const override; const QGpgME::Protocol *cryptoProto() const; QString fromAddress() const; private: /** Handles the verification of data * If signature is empty it is handled as inline signature otherwise as detached signature mode. * Returns true if the verification was successful and the block is signed. * If used in async mode, check if mMetaData.inProgress is true, it initiates a running verification process. */ bool okVerify(const QByteArray &data, const QByteArray &signature, KMime::Content *textNode); void sigStatusToMetaData(); void setVerificationResult(const CryptoBodyPartMemento *m, KMime::Content *textNode); protected: const QGpgME::Protocol *mCryptoProto; QString mFromAddress; QByteArray mVerifiedText; friend EncryptedMessagePart; }; } #endif //__MIMETREEPARSER_MESSAGEPART_H diff --git a/templateparser/autotests/data/404698-gpg-attachments.mbox b/templateparser/autotests/data/404698-gpg-attachments.mbox new file mode 100644 index 00000000..83c2d169 --- /dev/null +++ b/templateparser/autotests/data/404698-gpg-attachments.mbox @@ -0,0 +1,72 @@ +Subject: Testcase 'reply-mix-crlf' (PGP/MIME HTML) +To: brucewayne45@web.de +From: brucewayne45@web.de +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="BOUNDARY" + +--BOUNDARY +Content-Type: text/plain + +Please reply to this message +. +. +. + + +--BOUNDARY +Content-Type: text/plain; name="text1.txt" +Content-Disposition: inline + +-----BEGIN PGP MESSAGE----- + +hQEMAwzOQ1qnzNo7AQf7BxmM0vO8nG37hKqoqOHb35JqprJM+sqF7JFmrsuWe6V2 +PAyyE2wdtq+AhvXjVnggxYLwU+DEFpBTmWr1rsanyV8hWXRbecfN/9gN/4/N9y7Z +XSx2OeE/uA5z8Kz5vrv/ywMqcVHjB5MQPTcLC2Zlg8MVltpriy6mdAkON4I3t7kl +j9uwQRY7HeKvsib63HWnYAOV/fYPXXor/lioeYIll08uuCiTh3Z9fEhXQI/az5Ft +e/xa70xGqviux+OvhoNUSZspzl7vK7e/NTBlC+LF1zVXUXT8prrd+ZFNwKvtn0Hl +W4KfNqTM9TJB8vpE5FWnH6+B365ZvxZopZ5F/9szp9JGAUCNdX5WujBreg7nTLui +UrnDNwOvjvsE/gsoO3n3jARK+Tu8PfUl8V1bHiCeGJz/mkA9uGJ/IApcT4rYsoHB +nVQjW1NJ6A== +=zrF/ +-----END PGP MESSAGE----- + +--BOUNDARY +Content-Type: text/plain; name="text2.txt" +Content-Disposition: inline + +-----BEGIN PGP MESSAGE----- + +hQEMAwzOQ1qnzNo7AQf+MCqjMjAB80hAMAHfa7bdk/6L4DJQBQn+zHRv6oYzzYFC +8l79DDIE2uorQNFj1ZBw5+pi7+/2QmAANnG2ug5W0HRphg2WPXTUswy5H+mg08PM +MXRsP9lX5pAXEbLZVp61tvOQHnO/ltBhHHBwRaIq2tiirUUhy5erqLwlkSyN8xHM +Bh0u/dIJw7ewMk0l3BtF/GuP7l6PtUxT7P0Vwit4h1FV1bc9mSFmBNN16dvixJ4l +jK0mYEqT97SNZpg0MPOxx8E3xuJptzea4qmACv5zx4gYHlZRM0ZlKNqffmRauWOe +pDCjZv2F1IUJOg28NzZhKCBVhmhBmP1VmLNYFKGAsNJHAV+3uN2YYWzbhoOJAE0N +UxLI0EQN4y7OkAnGiRH45HygLxAjTk6dPiP5OD9OhUnSqofAjajlmqzfAAVMxY1a +epnRKPsnCZU= +=dqBN +-----END PGP MESSAGE----- + + +--BOUNDARY +Content-Type: text/plain; name="text3.txt" +Content-Disposition: inline + +-----BEGIN PGP MESSAGE----- + +hQEMAwzOQ1qnzNo7AQf+MNDSEBVsF78knI+uirDbLSLrHicrXExTocmXr2DZOggI +zMYCAHyg7ohINA40/8ZuR0bC9h6qCZjjhR+VFe2edRFshXlbuzykjpXNYcSv61Sm +9TAVpgAExzS5VhAxYIJ6+zWJR8+hgv63oREZPWlJ23utBDAMkEeY7cga3wn1HZMZ +g4XQZ94a8s9s/I+s3dLOdHGdxw+hmSnxjMhI6TMcZV/Kvr1MkkW10N0h0+hiuq2O +4owEztpm4See8fCkRfhr0TO+a8ElCtIXjVwqeB0tQh0fU3QaaNiDXYawoFMQXG8N +nwCP92glfOeAvJn9KuLwO3ee+WKwcrJhsFRMmjziDdJGAUvptVDNrk2P/0fzo/Xl +ypmw8zhir6ch+4C2+5yFCtVSmC+3Y7+NQ4YE4AR/z5rGvA1lxclulU1DSGkhFTbJ +XEVyg8o23A== +=Bs3d +-----END PGP MESSAGE----- + +--BOUNDARY + +whoo three encrypted parts inside. + +--BOUNDRY-- diff --git a/templateparser/autotests/data/404698-gpg-attachments.mbox.forwarded.mbox b/templateparser/autotests/data/404698-gpg-attachments.mbox.forwarded.mbox new file mode 100644 index 00000000..1b3a24a0 --- /dev/null +++ b/templateparser/autotests/data/404698-gpg-attachments.mbox.forwarded.mbox @@ -0,0 +1,66 @@ +Subject: Testcase 'reply-mix-crlf' (PGP/MIME HTML) +To: brucewayne45@web.de +From: brucewayne45@web.de +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="BOUNDARY" + +--BOUNDARY +Content-Type: text/plain + +Please reply to this message +. +. +. + + +--BOUNDARY +Content-Type: text/plain; name="text1.txt" +Content-Disposition: inline + +-----BEGIN PGP MESSAGE----- + +hQEMAwzOQ1qnzNo7AQf7BxmM0vO8nG37hKqoqOHb35JqprJM+sqF7JFmrsuWe6V2 +PAyyE2wdtq+AhvXjVnggxYLwU+DEFpBTmWr1rsanyV8hWXRbecfN/9gN/4/N9y7Z +XSx2OeE/uA5z8Kz5vrv/ywMqcVHjB5MQPTcLC2Zlg8MVltpriy6mdAkON4I3t7kl +j9uwQRY7HeKvsib63HWnYAOV/fYPXXor/lioeYIll08uuCiTh3Z9fEhXQI/az5Ft +e/xa70xGqviux+OvhoNUSZspzl7vK7e/NTBlC+LF1zVXUXT8prrd+ZFNwKvtn0Hl +W4KfNqTM9TJB8vpE5FWnH6+B365ZvxZopZ5F/9szp9JGAUCNdX5WujBreg7nTLui +UrnDNwOvjvsE/gsoO3n3jARK+Tu8PfUl8V1bHiCeGJz/mkA9uGJ/IApcT4rYsoHB +nVQjW1NJ6A== +=zrF/ +-----END PGP MESSAGE----- + +--BOUNDARY +Content-Type: text/plain; name="text2.txt" +Content-Disposition: inline + +-----BEGIN PGP MESSAGE----- + +hQEMAwzOQ1qnzNo7AQf+MCqjMjAB80hAMAHfa7bdk/6L4DJQBQn+zHRv6oYzzYFC +8l79DDIE2uorQNFj1ZBw5+pi7+/2QmAANnG2ug5W0HRphg2WPXTUswy5H+mg08PM +MXRsP9lX5pAXEbLZVp61tvOQHnO/ltBhHHBwRaIq2tiirUUhy5erqLwlkSyN8xHM +Bh0u/dIJw7ewMk0l3BtF/GuP7l6PtUxT7P0Vwit4h1FV1bc9mSFmBNN16dvixJ4l +jK0mYEqT97SNZpg0MPOxx8E3xuJptzea4qmACv5zx4gYHlZRM0ZlKNqffmRauWOe +pDCjZv2F1IUJOg28NzZhKCBVhmhBmP1VmLNYFKGAsNJHAV+3uN2YYWzbhoOJAE0N +UxLI0EQN4y7OkAnGiRH45HygLxAjTk6dPiP5OD9OhUnSqofAjajlmqzfAAVMxY1a +epnRKPsnCZU= +=dqBN +-----END PGP MESSAGE----- + + +--BOUNDARY +Content-Type: text/plain; name="text3.txt" +Content-Disposition: inline + +-----BEGIN PGP MESSAGE----- + +hQEMAwzOQ1qnzNo7AQf+MNDSEBVsF78knI+uirDbLSLrHicrXExTocmXr2DZOggI +zMYCAHyg7ohINA40/8ZuR0bC9h6qCZjjhR+VFe2edRFshXlbuzykjpXNYcSv61Sm +9TAVpgAExzS5VhAxYIJ6+zWJR8+hgv63oREZPWlJ23utBDAMkEeY7cga3wn1HZMZ +g4XQZ94a8s9s/I+s3dLOdHGdxw+hmSnxjMhI6TMcZV/Kvr1MkkW10N0h0+hiuq2O +4owEztpm4See8fCkRfhr0TO+a8ElCtIXjVwqeB0tQh0fU3QaaNiDXYawoFMQXG8N +nwCP92glfOeAvJn9KuLwO3ee+WKwcrJhsFRMmjziDdJGAUvptVDNrk2P/0fzo/Xl +ypmw8zhir6ch+4C2+5yFCtVSmC+3Y7+NQ4YE4AR/z5rGvA1lxclulU1DSGkhFTbJ +XEVyg8o23A== +=Bs3d +-----END PGP MESSAGE----- diff --git a/templateparser/autotests/data/404698-gpg-attachments.mbox.html.reply b/templateparser/autotests/data/404698-gpg-attachments.mbox.html.reply new file mode 100644 index 00000000..dadcd3ce --- /dev/null +++ b/templateparser/autotests/data/404698-gpg-attachments.mbox.html.reply @@ -0,0 +1 @@ +Please reply to this message
.
.
.

diff --git a/templateparser/autotests/data/404698-gpg-attachments.mbox.plain.reply b/templateparser/autotests/data/404698-gpg-attachments.mbox.plain.reply new file mode 100644 index 00000000..0b58a5bd --- /dev/null +++ b/templateparser/autotests/data/404698-gpg-attachments.mbox.plain.reply @@ -0,0 +1,5 @@ +Please reply to this message +. +. +. + diff --git a/templateparser/autotests/data/404698-gpg-html.mbox b/templateparser/autotests/data/404698-gpg-html.mbox new file mode 100644 index 00000000..c1a53c26 --- /dev/null +++ b/templateparser/autotests/data/404698-gpg-html.mbox @@ -0,0 +1,131 @@ +Subject: Testcase 'reply-mix-crlf' (PGP/MIME HTML) +To: brucewayne45@web.de +From: brucewayne45@web.de +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="BOUNDARY" + +--BOUNDARY +Content-Type: text/plain + +Please reply to this message +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. + +--BOUNDARY +Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; boundary="DELIMITER" + +--DELIMITER +Content-Type: application/pgp-encrypted + +Version: 1 + +--DELIMITER +Content-Type: application/octet-stream; name="encrypted.asc" +Content-Disposition: inline; filename="encrypted.asc" + +-----BEGIN PGP MESSAGE----- + +hQELAwzOQ1qnzNo7AQf3Zp/i09urv4DVCcbCu75LQqVtkanqYIaVF+CgUX2aeReD +VIf6Q58I5lX0+ZighYMjoqFyLoIg8KfazaiCZ7prwTOjTUxgVIaEUt46E/zaIU64 +y0M09xrD0ZXvdCb2DPbzBI2YGCUOg+WRN0Bjv90DXgqXLDzdHqJ4w/iFTF3sXTW0 +nxAeOqNWwr4vSOG2AfTbIHpAspl2woqnoKds0dxA8DgGD9ffGx1/VL9aoNPuhdGf +GWko1NfHnwgTaLEBr5Yr5uaDfPvtMfIdtw6HVc5xZfmlPfES9NeL0aHmHe9htuFf +fyEHzFHJDxqny5/PqULxcxBpKJH2jQuGWtzswOj/0sD4ASnTPJ/AmGqI2pWT1KIS +m1mSw7VBTK/f47EeQhdWIbIVt6+oqfjum+aAkwEEOGcUHNcNwclKYlHT1hUesNr5 +mj0XpKVUz1WBRI0O5Vedt7xNJOUIhBSOeiDrZ3dhn8kD12aJvP0NUx88jeR87U3k +F5UvDfsWyjqN4GMzjVV3vLsGShR+8C7I2+DPebyoLUfkqeEUUJOwYg4YL+gjAVHR +VKNvQIQWC5PFMm5LasDF0UdfTtKI+kWYgGTWVKQawr3nBJmly2b2By9biUh2Z4Ly +pUHqKpHEKW5O+VLd0dgzYV9x5Wq7zkKKX44B55W050bh3u9iztfZvghd16Cy7OYB +9h20ZMKFffkZCorrhiN6mHr1YkPXI30ZwC1xkQ6YuUlwIpE5gJr4D14+/zFcJsU0 +grdLbDzFApcwrGrxjiFGMb8tbS6XYyB0uVh0NEX7YGUHYtZnE6JLR0XVYjvSUFGs +plvgdpXbohA3J9T8Grd1M5O7D+SmrXHrZGj8oJW2DST6QmbphlkVBf0LrNK+sqSU +nKxX/pzNqA4Sh/lLJOQ7eON4or2u9JpiP5Zr++Wl99xWBel5iZC9s/8= +=mtFc +-----END PGP MESSAGE----- + + +--DELIMITER-- + +--BOUNDARY + +--BOUNDARY +Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; boundary="DELIMITER" + +--DELIMITER +Content-Type: application/pgp-encrypted + +Version: 1 + +--DELIMITER +Content-Type: application/octet-stream; name="encrypted.asc" +Content-Disposition: inline; filename="encrypted.asc" + +-----BEGIN PGP MESSAGE----- + +hQEMAwzOQ1qnzNo7AQf9EgAO1PAwK3tquVubNpsRpK6kfvjAVSDXNiUtuMN5G7KG +aqnVl5PmfdLdEbGLjcHyFMsZ8k2alSyG0lFWdXQUqfYE5TSo3en5Zqyc+kK/5pDI +GZ6V5E5I0JPVIeY5lSFIVvSBXCBncK7jdRAtN3WfDL50UQYJX8fAOGc6iiMKDEb8 +jKwJbS0qwjHDwaKKwseE/yuYO3YFUWsijsnxwUXhU0iq3Yv7JIhXKldHFPe+jCOy +v2OQ2064I9mCNi6LjQPcqyXVTESB5/6Sdl1L9/ETjep5Gzlhp8WJTZNIzWJhqmPe +6hebCCJowQ6CxiqVInh46fPfSGw/YuFGRlyWZCSVa9LA+wGb+KaR0oDwHjBsUkhh +VhdNS7kRtx3kec9KaJQHCDFt0aUaMgnFdnNDS3AEQ+5PbplyKYT5PjA1TkhHGXmb +VQ07Xyab3zGPE5DUsUYt/c+bfN3EmaOc8JAZBOJzYw0nL9fSt5E1feo3Ntwse5S1 +yMMK7ALfv6W4/ItWf1KTDCo/A2QiAzZO9Gy8kFOx3nXYeh5BN2Efe5tmdtxgypQP ++b3TJbl98elyS6jiV4xB+gooVTrK3DIJbZi8xcHnBqtg3bSVML5pDJBM1MCcS3cP +eVgJ5JkE1qqiHuk4NFm0KIKMWyjgXUp8XJ7pOpHtu5COEyz3TwpOjmcYUMjQnyQs +/c9DD/oTZZq4y7VTFab9W0iBS0aadHzSR3TQdneIap7MOay79+fZQEqK8zel4qkN +ngaxDyBYA9wy3gsigbb5lBEqX9M8o7VLpz6Hn3CXqxpgaPvE+GFN+r5Os1/bcuSK +EfJTTtPuqx3MjVH9y+wW3fThb5xSoDNgb3Z93HVMGZzcKh9Kt47ZBhxMfb/4N7lE +Hh3qvh6KIJNnVlhm+JqqlVbAUEAg6bFH+CI1RtsCPnrmHjT6ysqeNKciAvVB +=VRca +-----END PGP MESSAGE----- + +--DELIMITER-- diff --git a/templateparser/autotests/data/404698-gpg-html.mbox.html.reply b/templateparser/autotests/data/404698-gpg-html.mbox.html.reply new file mode 100644 index 00000000..8fd2007f --- /dev/null +++ b/templateparser/autotests/data/404698-gpg-html.mbox.html.reply @@ -0,0 +1 @@ +Please reply to this message
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
diff --git a/templateparser/autotests/data/404698-gpg-html.mbox.plain.reply b/templateparser/autotests/data/404698-gpg-html.mbox.plain.reply new file mode 100644 index 00000000..580c8fde --- /dev/null +++ b/templateparser/autotests/data/404698-gpg-html.mbox.plain.reply @@ -0,0 +1,51 @@ +Please reply to this message +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. diff --git a/templateparser/autotests/data/404698-gpg-inline.mbox b/templateparser/autotests/data/404698-gpg-inline.mbox new file mode 100644 index 00000000..e42eccd7 --- /dev/null +++ b/templateparser/autotests/data/404698-gpg-inline.mbox @@ -0,0 +1,51 @@ +Subject: Testcase 'reply-decrytion-oracle' (PGP INLINE) +To: brucewayne45@web.de +From: brucewayne45@web.de +MIME-Version: 1.0 +Content-Type: text/plain + +Please reply to this message +. +. +. + +-----BEGIN PGP MESSAGE----- + +hQEMAwzOQ1qnzNo7AQf7BxmM0vO8nG37hKqoqOHb35JqprJM+sqF7JFmrsuWe6V2 +PAyyE2wdtq+AhvXjVnggxYLwU+DEFpBTmWr1rsanyV8hWXRbecfN/9gN/4/N9y7Z +XSx2OeE/uA5z8Kz5vrv/ywMqcVHjB5MQPTcLC2Zlg8MVltpriy6mdAkON4I3t7kl +j9uwQRY7HeKvsib63HWnYAOV/fYPXXor/lioeYIll08uuCiTh3Z9fEhXQI/az5Ft +e/xa70xGqviux+OvhoNUSZspzl7vK7e/NTBlC+LF1zVXUXT8prrd+ZFNwKvtn0Hl +W4KfNqTM9TJB8vpE5FWnH6+B365ZvxZopZ5F/9szp9JGAUCNdX5WujBreg7nTLui +UrnDNwOvjvsE/gsoO3n3jARK+Tu8PfUl8V1bHiCeGJz/mkA9uGJ/IApcT4rYsoHB +nVQjW1NJ6A== +=zrF/ +-----END PGP MESSAGE----- + +-----BEGIN PGP MESSAGE----- + +hQEMAwzOQ1qnzNo7AQf+MCqjMjAB80hAMAHfa7bdk/6L4DJQBQn+zHRv6oYzzYFC +8l79DDIE2uorQNFj1ZBw5+pi7+/2QmAANnG2ug5W0HRphg2WPXTUswy5H+mg08PM +MXRsP9lX5pAXEbLZVp61tvOQHnO/ltBhHHBwRaIq2tiirUUhy5erqLwlkSyN8xHM +Bh0u/dIJw7ewMk0l3BtF/GuP7l6PtUxT7P0Vwit4h1FV1bc9mSFmBNN16dvixJ4l +jK0mYEqT97SNZpg0MPOxx8E3xuJptzea4qmACv5zx4gYHlZRM0ZlKNqffmRauWOe +pDCjZv2F1IUJOg28NzZhKCBVhmhBmP1VmLNYFKGAsNJHAV+3uN2YYWzbhoOJAE0N +UxLI0EQN4y7OkAnGiRH45HygLxAjTk6dPiP5OD9OhUnSqofAjajlmqzfAAVMxY1a +epnRKPsnCZU= +=dqBN +-----END PGP MESSAGE----- + +-----BEGIN PGP MESSAGE----- + +hQEMAwzOQ1qnzNo7AQf+MNDSEBVsF78knI+uirDbLSLrHicrXExTocmXr2DZOggI +zMYCAHyg7ohINA40/8ZuR0bC9h6qCZjjhR+VFe2edRFshXlbuzykjpXNYcSv61Sm +9TAVpgAExzS5VhAxYIJ6+zWJR8+hgv63oREZPWlJ23utBDAMkEeY7cga3wn1HZMZ +g4XQZ94a8s9s/I+s3dLOdHGdxw+hmSnxjMhI6TMcZV/Kvr1MkkW10N0h0+hiuq2O +4owEztpm4See8fCkRfhr0TO+a8ElCtIXjVwqeB0tQh0fU3QaaNiDXYawoFMQXG8N +nwCP92glfOeAvJn9KuLwO3ee+WKwcrJhsFRMmjziDdJGAUvptVDNrk2P/0fzo/Xl +ypmw8zhir6ch+4C2+5yFCtVSmC+3Y7+NQ4YE4AR/z5rGvA1lxclulU1DSGkhFTbJ +XEVyg8o23A== +=Bs3d +-----END PGP MESSAGE----- + +whoo three encrypted parts inside. diff --git a/templateparser/autotests/data/404698-gpg-inline.mbox.html.reply b/templateparser/autotests/data/404698-gpg-inline.mbox.html.reply new file mode 100644 index 00000000..dadcd3ce --- /dev/null +++ b/templateparser/autotests/data/404698-gpg-inline.mbox.html.reply @@ -0,0 +1 @@ +Please reply to this message
.
.
.

diff --git a/templateparser/autotests/data/404698-gpg-inline.mbox.plain.reply b/templateparser/autotests/data/404698-gpg-inline.mbox.plain.reply new file mode 100644 index 00000000..0b58a5bd --- /dev/null +++ b/templateparser/autotests/data/404698-gpg-inline.mbox.plain.reply @@ -0,0 +1,5 @@ +Please reply to this message +. +. +. + diff --git a/templateparser/autotests/data/404698-gpg.mbox b/templateparser/autotests/data/404698-gpg.mbox new file mode 100644 index 00000000..ed9c0f98 --- /dev/null +++ b/templateparser/autotests/data/404698-gpg.mbox @@ -0,0 +1,120 @@ +Subject: Testcase 'reply-mix-crlf' (PGP/MIME) +To: brucewayne45@web.de +From: brucewayne45@web.de +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="BOUNDARY" + +--BOUNDARY +Content-Type: text/plain + +Please reply to this message +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. + +--BOUNDARY +Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; boundary="DELIMITER" + +--DELIMITER +Content-Type: application/pgp-encrypted + +Version: 1 + +--DELIMITER +Content-Type: application/octet-stream; name="encrypted.asc" +Content-Disposition: inline; filename="encrypted.asc" + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1 + +hIwDGJlthTT7oq0BA/9NtLLXbiIJVS6pOynwEeSznrQK7kYVla8RM43//JECCkGJ +azEaSBznabBv6epaFmQtVHLMXlCbZnMmW9loyqPBfMoAms6kKKBdG/jqhus89iXE ++seXngC233Va/gZMb2DxOqIokVNfj9tpR7xQ8wS/jHTDiLNc1GOQC7ku42z2bNLA +IQFRD/qbBFz89hU4wP4cYoAysOnEDojFrsrnCidTHJOJrndM6PPUtH/jQCyfr/EG +2tSpJwYKvmT6ly3yqaGLBtRPIxiv+dMe+7yw0t40qbjvvaTGavErEBJEKX5eWbTN +/sjajHpUHqs6SIiMheH9dr+WfzFONtVbPEgGRmOERhlgTl/nLo86AZpjJroIGKJJ +tTHCcoQGAWG+N7wrCE1RxR0kkMs4nRozj0TLu6ZyXMs+H063MewTPNxNAiQT1Nbi +udKWmfLBlxn06p+JDzUKxj8PFwObdbxTvACzbAvBY1aHMQ== +=mLl3 +-----END PGP MESSAGE----- + + +--DELIMITER-- + +--BOUNDARY + +--BOUNDARY +Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; boundary="DELIMITER" + +--DELIMITER +Content-Type: application/pgp-encrypted + +Version: 1 + +--DELIMITER +Content-Type: application/octet-stream; name="encrypted.asc" +Content-Disposition: inline; filename="encrypted.asc" + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v2.0.15 (GNU/Linux) + +hQEMAwzOQ1qnzNo7AQgAtWfDWWI2JUGuptpackiIxpWViEEpGAeruETubiIPwxNb +DNmXrMDhbm/zIbPntIGWJDgUMfABZCUgmlJLWhsceDTt+tXnWGha2VYrN2/WsF6/ +Pqs/TavTvMIJQHDaIH5yDDCaMoq/mGSbcu7go2H8Sw7aBEYlM8jGlqc1HziXnZ1q +3vDiA+4qWfvbNoSRo1kb9Pcq997yg6WqZXH2hJ7cp+hIQ4uTP1/+qgYHMvfPlzQk +XcDguGbIer88ELhuR5622unGBAB4dqp+5w6n9c6rrCH81qhV4W0nqSEvj1tBj78S +ZTi6VBAo5eS0e3iOJqMpwUZz6hQUpJw2wnNRGvLgI9KZAag0HkgPdMeANowg7vpE +L4nU7B0ybhswA2Y7QT/wwCDZu9N1JGeBmy0dgy4sA38Ki27rn2/lIaP0j14JycwM +RTJ1uwI+ZuQiwXlyYtdFZJWe8nraWARch0oKqhaR7aSsxGWo63eiGEQhkQCBFBb3 +Vg0nNCZRBauEqIESEW5EV2zrJqdfNYcz+f9IP125dnQEKgLZ6FxTt3+v +=mhNl +-----END PGP MESSAGE----- + +--DELIMITER-- diff --git a/templateparser/autotests/data/404698-gpg.mbox.html.reply b/templateparser/autotests/data/404698-gpg.mbox.html.reply new file mode 100644 index 00000000..8fd2007f --- /dev/null +++ b/templateparser/autotests/data/404698-gpg.mbox.html.reply @@ -0,0 +1 @@ +Please reply to this message
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
diff --git a/templateparser/autotests/data/404698-gpg.mbox.plain.reply b/templateparser/autotests/data/404698-gpg.mbox.plain.reply new file mode 100644 index 00000000..580c8fde --- /dev/null +++ b/templateparser/autotests/data/404698-gpg.mbox.plain.reply @@ -0,0 +1,51 @@ +Please reply to this message +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. diff --git a/templateparser/autotests/data/404698-smime.mbox b/templateparser/autotests/data/404698-smime.mbox new file mode 100644 index 00000000..7cd5a106 --- /dev/null +++ b/templateparser/autotests/data/404698-smime.mbox @@ -0,0 +1,88 @@ +Subject: Testcase 'reply-mix-crlf' (S/MIME) +To: brucewayne45@web.de +From: brucewayne45@web.de +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="BOUNDARY" + +--BOUNDARY +Content-Type: text/plain + +Please reply to this message +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. + +--BOUNDARY +Content-Type: application/pkcs7-mime; name="smime.p7m"; smime-type=enveloped-data +Content-Transfer-Encoding: base64 + +MIAGCSqGSIb3DQEHA6CAMIACAQAxgfwwgfkCAQAwYjBVMQswCQYDVQQGEwJVUzENMAsGA1UECgwE +S0RBQjEWMBQGA1UEAwwNdW5pdHRlc3QgY2VydDEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFtcGxl +LmNvbQIJANNFIDoYY4XJMA0GCSqGSIb3DQEBAQUABIGAJwmmaOeidXUHSQGOf2OBIsPYafVqdORe +y54pEXbXiAfSVUWgI4a9CsiWwcDX8vlaX9ZLLr+L2VmOfr6Yc5214yxzausZVvnUFjy6LUXotuEX +tSar4EW7XI9DjaZc1l985naMsTx9JUa5GyQ9J6PGqhosAKpKMGgKkFAHaOwE1/IwgAYJKoZIhvcN +AQcBMBQGCCqGSIb3DQMHBAieDfmz3WGbN6CABHgEpsLrNn0PAZTDUfNomDypvSCl5bQH+9cKm80m +upMV2r8RBiXS7OaP4SpCxq18afDTTPatvboHIoEX92taTbq8soiAgEs6raSGtEYZNvFL0IYqm7MA +o5HCOmjiEcInyPf14lL3HnPk10FaP3hh58qTHUh4LPYtL7UECOZELYnUfUVhAAAAAAAAAAAAAA== + +--BOUNDARY +Content-Type: application/pkcs7-mime; name="smime.p7m"; smime-type=enveloped-data +Content-Transfer-Encoding: base64 + +MIAGCSqGSIb3DQEHA6CAMIACAQAxgfwwgfkCAQAwYjBVMQswCQYDVQQGEwJVUzENMAsGA1UECgwE +S0RBQjEWMBQGA1UEAwwNdW5pdHRlc3QgY2VydDEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFtcGxl +LmNvbQIJANNFIDoYY4XJMA0GCSqGSIb3DQEBAQUABIGAJwmmaOeidXUHSQGOf2OBIsPYafVqdORe +y54pEXbXiAfSVUWgI4a9CsiWwcDX8vlaX9ZLLr+L2VmOfr6Yc5214yxzausZVvnUFjy6LUXotuEX +tSar4EW7XI9DjaZc1l985naMsTx9JUa5GyQ9J6PGqhosAKpKMGgKkFAHaOwE1/IwgAYJKoZIhvcN +AQcBMBQGCCqGSIb3DQMHBAieDfmz3WGbN6CABHgEpsLrNn0PAZTDUfNomDypvSCl5bQH+9cKm80m +upMV2r8RBiXS7OaP4SpCxq18afDTTPatvboHIoEX92taTbq8soiAgEs6raSGtEYZNvFL0IYqm7MA +o5HCOmjiEcInyPf14lL3HnPk10FaP3hh58qTHUh4LPYtL7UECOZELYnUfUVhAAAAAAAAAAAAAA== + +--BOUNDARY-- diff --git a/templateparser/autotests/data/404698-smime.mbox.html.reply b/templateparser/autotests/data/404698-smime.mbox.html.reply new file mode 100644 index 00000000..8fd2007f --- /dev/null +++ b/templateparser/autotests/data/404698-smime.mbox.html.reply @@ -0,0 +1 @@ +Please reply to this message
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
diff --git a/templateparser/autotests/data/404698-smime.mbox.plain.reply b/templateparser/autotests/data/404698-smime.mbox.plain.reply new file mode 100644 index 00000000..580c8fde --- /dev/null +++ b/templateparser/autotests/data/404698-smime.mbox.plain.reply @@ -0,0 +1,51 @@ +Please reply to this message +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. +. diff --git a/templateparser/autotests/data/html-attachment1.mbox.forwarded.mbox b/templateparser/autotests/data/html-attachment1.mbox.forwarded.mbox new file mode 100644 index 00000000..b4df51a9 --- /dev/null +++ b/templateparser/autotests/data/html-attachment1.mbox.forwarded.mbox @@ -0,0 +1,83 @@ +Return-path: +Envelope-to: gunter@ohrner.net +Delivery-date: Mon, 12 Sep 2016 13:38:39 +0200 +Received: from mail.from example.com ([178.251.88.150]) + by luggage.ohrner.net with esmtps (TLS1.0:DHE_RSA_AES_256_CBC_SHA1:256) + (Exim 4.80) + (envelope-from ) + id 1bjPYi-0000oA-Re + for gunter@ohrner.net; Mon, 12 Sep 2016 13:38:39 +0200 +Received: from example.com.local (172.17.124.205) by from example.com.local + (172.17.124.1) with Microsoft SMTP Server id 14.3.266.1; Mon, 12 Sep 2016 + 13:37:53 +0200 +From: +To: +Message-ID: <8614416.18.1473680273823.JavaMail.sender@example.com> +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_Part_16_9312243.1473680273820" +Date: Mon, 12 Sep 2016 13:37:53 +0200 +X-TM-AS-Product-Ver: SMEX-11.1.0.1278-8.000.1202-22570.004 +X-TM-AS-Result: No--23.664000-5.000000-31 +X-TM-AS-User-Approved-Sender: No +X-TM-AS-User-Blocked-Sender: No +Subject: A Subject Line + +------=_Part_16_9312243.1473680273820 +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: 8bit + +

A Body Text

+------=_Part_16_9312243.1473680273820 +Content-Type: application/pdf; name="Attachment1.pdf" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment + +JVBERi0xLjEKJeLjz9MKMSAwIG9iaiAKPDwKL1R5cGUgL0NhdGFsb2cKL1BhZ2VzIDIgMCBSCj4+ +CmVuZG9iaiAKMiAwIG9iaiAKPDwKL0tpZHMgWzMgMCBSXQovVHlwZSAvUGFnZXMKL01lZGlhQm94 +IFswIDAgMzAwIDE0NF0KL0NvdW50IDEKPj4KZW5kb2JqIAozIDAgb2JqIAo8PAovUmVzb3VyY2Vz +IAo8PAovRm9udCAKPDwKL0YxIAo8PAovU3VidHlwZSAvVHlwZTEKL1R5cGUgL0ZvbnQKL0Jhc2VG +b250IC9UaW1lcy1Sb21hbgo+Pgo+Pgo+PgovQ29udGVudHMgNCAwIFIKL1BhcmVudCAyIDAgUgov +VHlwZSAvUGFnZQovTWVkaWFCb3ggWzAgMCAzMDAgMTQ0XQo+PgplbmRvYmogCjQgMCBvYmogCjw8 +Ci9GaWx0ZXIgL0ZsYXRlRGVjb2RlCi9MZW5ndGggNTIKPj4Kc3RyZWFtCnicU1BwCuFSAAJ9N0MF +QwuFkDQwzwAIQ1LATA2P1JycfIXw/KKcFE2FkCygoGsIACsbDAQKZW5kc3RyZWFtIAplbmRvYmog +eHJlZgowIDUKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDE1IDAwMDAwIG4gCjAwMDAwMDAw +NjYgMDAwMDAgbiAKMDAwMDAwMDE0OSAwMDAwMCBuIAowMDAwMDAwMzMxIDAwMDAwIG4gCnRyYWls +ZXIKCjw8Ci9Sb290IDEgMCBSCi9TaXplIDUKPj4Kc3RhcnR4cmVmCjQ1NgolJUVPRgo= + +------=_Part_16_9312243.1473680273820 +Content-Type: application/pdf; name="Attachment2.pdf" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment + +JVBERi0xLjEKJeLjz9MKMSAwIG9iaiAKPDwKL1R5cGUgL0NhdGFsb2cKL1BhZ2VzIDIgMCBSCj4+ +CmVuZG9iaiAKMiAwIG9iaiAKPDwKL0tpZHMgWzMgMCBSXQovVHlwZSAvUGFnZXMKL01lZGlhQm94 +IFswIDAgMzAwIDE0NF0KL0NvdW50IDEKPj4KZW5kb2JqIAozIDAgb2JqIAo8PAovUmVzb3VyY2Vz +IAo8PAovRm9udCAKPDwKL0YxIAo8PAovU3VidHlwZSAvVHlwZTEKL1R5cGUgL0ZvbnQKL0Jhc2VG +b250IC9UaW1lcy1Sb21hbgo+Pgo+Pgo+PgovQ29udGVudHMgNCAwIFIKL1BhcmVudCAyIDAgUgov +VHlwZSAvUGFnZQovTWVkaWFCb3ggWzAgMCAzMDAgMTQ0XQo+PgplbmRvYmogCjQgMCBvYmogCjw8 +Ci9GaWx0ZXIgL0ZsYXRlRGVjb2RlCi9MZW5ndGggNTIKPj4Kc3RyZWFtCnicU1BwCuFSAAJ9N0MF +QwuFkDQwzwAIQ1LATA2P1JycfIXw/KKcFE2FkCygoGsIACsbDAQKZW5kc3RyZWFtIAplbmRvYmog +eHJlZgowIDUKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDE1IDAwMDAwIG4gCjAwMDAwMDAw +NjYgMDAwMDAgbiAKMDAwMDAwMDE0OSAwMDAwMCBuIAowMDAwMDAwMzMxIDAwMDAwIG4gCnRyYWls +ZXIKCjw8Ci9Sb290IDEgMCBSCi9TaXplIDUKPj4Kc3RhcnR4cmVmCjQ1NgolJUVPRgo= + +------=_Part_16_9312243.1473680273820 +Content-Type: application/pdf; name="Attachment3.pdf" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment + +JVBERi0xLjEKJeLjz9MKMSAwIG9iaiAKPDwKL1R5cGUgL0NhdGFsb2cKL1BhZ2VzIDIgMCBSCj4+ +CmVuZG9iaiAKMiAwIG9iaiAKPDwKL0tpZHMgWzMgMCBSXQovVHlwZSAvUGFnZXMKL01lZGlhQm94 +IFswIDAgMzAwIDE0NF0KL0NvdW50IDEKPj4KZW5kb2JqIAozIDAgb2JqIAo8PAovUmVzb3VyY2Vz +IAo8PAovRm9udCAKPDwKL0YxIAo8PAovU3VidHlwZSAvVHlwZTEKL1R5cGUgL0ZvbnQKL0Jhc2VG +b250IC9UaW1lcy1Sb21hbgo+Pgo+Pgo+PgovQ29udGVudHMgNCAwIFIKL1BhcmVudCAyIDAgUgov +VHlwZSAvUGFnZQovTWVkaWFCb3ggWzAgMCAzMDAgMTQ0XQo+PgplbmRvYmogCjQgMCBvYmogCjw8 +Ci9GaWx0ZXIgL0ZsYXRlRGVjb2RlCi9MZW5ndGggNTIKPj4Kc3RyZWFtCnicU1BwCuFSAAJ9N0MF +QwuFkDQwzwAIQ1LATA2P1JycfIXw/KKcFE2FkCygoGsIACsbDAQKZW5kc3RyZWFtIAplbmRvYmog +eHJlZgowIDUKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDE1IDAwMDAwIG4gCjAwMDAwMDAw +NjYgMDAwMDAgbiAKMDAwMDAwMDE0OSAwMDAwMCBuIAowMDAwMDAwMzMxIDAwMDAwIG4gCnRyYWls +ZXIKCjw8Ci9Sb290IDEgMCBSCi9TaXplIDUKPj4Kc3RhcnR4cmVmCjQ1NgolJUVPRgo= + +------=_Part_16_9312243.1473680273820-- + diff --git a/templateparser/autotests/data/html-attachment1.mbox.html.reply b/templateparser/autotests/data/html-attachment1.mbox.html.reply new file mode 100644 index 00000000..71b8c1c0 --- /dev/null +++ b/templateparser/autotests/data/html-attachment1.mbox.html.reply @@ -0,0 +1 @@ +

A Body Text

diff --git a/templateparser/autotests/data/html-attachment2.mbox.forwarded.mbox b/templateparser/autotests/data/html-attachment2.mbox.forwarded.mbox new file mode 100644 index 00000000..498cf3a9 --- /dev/null +++ b/templateparser/autotests/data/html-attachment2.mbox.forwarded.mbox @@ -0,0 +1,28 @@ +From maillists@whattf.com Tue Jul 05 19:20:58 2011 +Return-path: +Envelope-to: maillists@whattf.com +Date: Mon, 24 Oct 2016 11:53:00 +0100 +To: maillists@whattf.com +From: maillists@whattf.com +MIME-Version: 1.0 +Subject: Test +Content-Type: multipart/related; boundary="----=_Part_20324_1054669183.1477306380301" +MIME-Version: 1.0 + +------=_Part_20324_1054669183.1477306380301 +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: 7bit + +HTML Text +------=_Part_20324_1054669183.1477306380301 +Content-Type: image/png +Content-Disposition: attachment; filename="image1.png" + +Image1 +------=_Part_20324_1054669183.1477306380301 +Content-Type: image/png +Content-Disposition: attachment; filename="image2.png" + +Image2 +------=_Part_20324_1054669183.1477306380301-- + diff --git a/templateparser/autotests/data/html-attachment2.mbox.html.reply b/templateparser/autotests/data/html-attachment2.mbox.html.reply new file mode 100644 index 00000000..292a0196 --- /dev/null +++ b/templateparser/autotests/data/html-attachment2.mbox.html.reply @@ -0,0 +1 @@ +HTML Text diff --git a/templateparser/autotests/data/openpgp-encrypted.mbox b/templateparser/autotests/data/openpgp-encrypted.mbox new file mode 100644 index 00000000..54fe6530 --- /dev/null +++ b/templateparser/autotests/data/openpgp-encrypted.mbox @@ -0,0 +1,35 @@ +From test@kolab.org Wed, 08 Sep 2010 17:02:52 +0200 +From: OpenPGP Test +To: test@kolab.org +Subject: OpenPGP encrypted +Date: Wed, 08 Sep 2010 17:02:52 +0200 +User-Agent: KMail/4.6 pre (Linux/2.6.34-rc2-2-default; KDE/4.5.60; x86_64; ; ) +MIME-Version: 1.0 +Content-Type: multipart/encrypted; boundary="nextPart1357031.ppLHckZtsp"; protocol="application/pgp-encrypted" +Content-Transfer-Encoding: 7Bit + + +--nextPart1357031.ppLHckZtsp +Content-Type: application/pgp-encrypted +Content-Disposition: attachment + +Version: 1 +--nextPart1357031.ppLHckZtsp +Content-Type: application/octet-stream +Content-Disposition: inline; filename="msg.asc" + +-----BEGIN PGP MESSAGE----- + +hQEMAwzOQ1qnzNo7AQf/dJ5Pn6aE02/ImggNDDJBfvRU7vWF3OeKaGNrZd9sWp3Q +x8Tas0r+Yfaoo5cNqEJ7LXaRA9UGJ3BtDEtzLl/xrY++QyewrRtLlfSLXFSifZOA +9eBYmL4cj4Fbp4HR8iUBC2p64FzARQBe4hCyEoIIQPK7+3XqgsFdCIqmnEt1+Nys +mKXN4VSukAcTzjR8JasPKZ+Qayx9+DBu7wJJNzjlhnrIv4AMYOTetLwL7f5/c2j+ +zX/2/9ptWwcBDxMl6WKTpPF4Zf57MgeV3TI4O5FHXfhjldUh6JiSt3i+t4ev9F8A +s1pYZ15qDRwNwshZ7fujJvd1lk7ZDZdAQyKXgHNo0NKOATJ6IN0S5yvFi9FhyHM6 +sGzHXVqBgJspKqOuuoSPuX0RBvAskNPa4yERla0725n/F9AHsbiw4olQvIbKD70+ +gPd1k+MR6OfsGb+m5IIMBSjaKDLtgO9H0JLIq2U7Qf3YU+VsvvPH+PhTnUuz5/Ea +0W1dTWWU0MRh0Z8uKM9KJHjMjkNzBvO4T8uTfRwBPA== +=G9lS +-----END PGP MESSAGE----- + +--nextPart1357031.ppLHckZtsp-- diff --git a/templateparser/autotests/data/openpgp-encrypted.mbox.html.reply b/templateparser/autotests/data/openpgp-encrypted.mbox.html.reply new file mode 100644 index 00000000..57839070 --- /dev/null +++ b/templateparser/autotests/data/openpgp-encrypted.mbox.html.reply @@ -0,0 +1 @@ +encrypted text
diff --git a/templateparser/autotests/data/openpgp-encrypted.mbox.plain.reply b/templateparser/autotests/data/openpgp-encrypted.mbox.plain.reply new file mode 100644 index 00000000..965dc9a5 --- /dev/null +++ b/templateparser/autotests/data/openpgp-encrypted.mbox.plain.reply @@ -0,0 +1 @@ +encrypted text diff --git a/templateparser/autotests/data/openpgp-inline-space.mbox b/templateparser/autotests/data/openpgp-inline-space.mbox new file mode 100644 index 00000000..eaff262b --- /dev/null +++ b/templateparser/autotests/data/openpgp-inline-space.mbox @@ -0,0 +1,31 @@ +Subject: Testcase 'reply-decrytion-oracle' (PGP INLINE) +To: brucewayne45@web.de +From: brucewayne45@web.de +MIME-Version: 1.0 +Content-Type: text/plain + + + + + + + + + + + + +-----BEGIN PGP MESSAGE----- + +hQEMAwzOQ1qnzNo7AQf7BxmM0vO8nG37hKqoqOHb35JqprJM+sqF7JFmrsuWe6V2 +PAyyE2wdtq+AhvXjVnggxYLwU+DEFpBTmWr1rsanyV8hWXRbecfN/9gN/4/N9y7Z +XSx2OeE/uA5z8Kz5vrv/ywMqcVHjB5MQPTcLC2Zlg8MVltpriy6mdAkON4I3t7kl +j9uwQRY7HeKvsib63HWnYAOV/fYPXXor/lioeYIll08uuCiTh3Z9fEhXQI/az5Ft +e/xa70xGqviux+OvhoNUSZspzl7vK7e/NTBlC+LF1zVXUXT8prrd+ZFNwKvtn0Hl +W4KfNqTM9TJB8vpE5FWnH6+B365ZvxZopZ5F/9szp9JGAUCNdX5WujBreg7nTLui +UrnDNwOvjvsE/gsoO3n3jARK+Tu8PfUl8V1bHiCeGJz/mkA9uGJ/IApcT4rYsoHB +nVQjW1NJ6A== +=zrF/ +-----END PGP MESSAGE----- + +whoo three encrypted parts inside. diff --git a/templateparser/autotests/data/openpgp-inline-space.mbox.html.reply b/templateparser/autotests/data/openpgp-inline-space.mbox.html.reply new file mode 100644 index 00000000..b78a7780 --- /dev/null +++ b/templateparser/autotests/data/openpgp-inline-space.mbox.html.reply @@ -0,0 +1 @@ +first part
diff --git a/templateparser/autotests/data/openpgp-inline-space.mbox.plain.reply b/templateparser/autotests/data/openpgp-inline-space.mbox.plain.reply new file mode 100644 index 00000000..a20785d2 --- /dev/null +++ b/templateparser/autotests/data/openpgp-inline-space.mbox.plain.reply @@ -0,0 +1 @@ +first part diff --git a/templateparser/autotests/templateparserjobtest.cpp b/templateparser/autotests/templateparserjobtest.cpp index 9b278dd9..a607ce9e 100644 --- a/templateparser/autotests/templateparserjobtest.cpp +++ b/templateparser/autotests/templateparserjobtest.cpp @@ -1,338 +1,565 @@ /* Copyright (C) 2017-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 "templateparserjobtest.h" #define private public #include "templateparserjob_p.h" #include "templateparserjob.h" #undef protected #include #include #include #include #include #include #include #include using namespace MimeTreeParser; TemplateParserJobTest::TemplateParserJobTest(QObject *parent) : QObject(parent) { QStandardPaths::setTestModeEnabled(true); } TemplateParserJobTest::~TemplateParserJobTest() { // Workaround QTestLib not flushing deleteLater()s on exit, which leads to WebEngine asserts (view not deleted) QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); } void TemplateParserJobTest::test_convertedHtml_data() { QTest::addColumn("mailFileName"); QTest::addColumn("referenceFileName"); QDir dir(QStringLiteral(MAIL_DATA_DIR)); const auto l = dir.entryList(QStringList(QStringLiteral("plain*.mbox")), QDir::Files | QDir::Readable | QDir::NoSymLinks); foreach (const QString &file, l) { QTest::newRow(file.toLatin1().constData()) << QString(dir.path() + QLatin1Char('/') + file) << QString(dir.path() + QLatin1Char('/') + file + QLatin1String(".html")); } } void TemplateParserJobTest::test_convertedHtml() { QFETCH(QString, mailFileName); QFETCH(QString, referenceFileName); // load input mail QFile mailFile(mailFileName); QVERIFY(mailFile.open(QIODevice::ReadOnly)); const QByteArray mailData = KMime::CRLFtoLF(mailFile.readAll()); QVERIFY(!mailData.isEmpty()); KMime::Message::Ptr msg(new KMime::Message); KMime::Message::Ptr origMsg(new KMime::Message); origMsg->setContent(mailData); origMsg->parse(); QCOMPARE(origMsg->subject()->as7BitString(false).constData(), "Plain Message Test"); QCOMPARE(origMsg->contents().size(), 0); // load expected result QFile referenceFile(referenceFileName); QVERIFY(referenceFile.open(QIODevice::ReadOnly)); const QByteArray referenceRawData = KMime::CRLFtoLF(referenceFile.readAll()); const QString referenceData = QString::fromLatin1(referenceRawData); QVERIFY(!referenceData.isEmpty()); TemplateParser::TemplateParserJob *parser = new TemplateParser::TemplateParserJob(msg, TemplateParser::TemplateParserJob::NewMessage); KIdentityManagement::IdentityManager *identMan = new KIdentityManagement::IdentityManager; parser->setIdentityManager(identMan); parser->d->mOrigMsg = origMsg; QSignalSpy spy(parser, &TemplateParser::TemplateParserJob::parsingDone); parser->processWithTemplate(QString()); QVERIFY(spy.wait()); QVERIFY(parser->d->mOtp->htmlContent().isEmpty()); QVERIFY(!parser->d->mOtp->plainTextContent().isEmpty()); const QString convertedHtmlContent = parser->htmlMessageText(false, TemplateParser::TemplateParserJob::NoSelectionAllowed); QVERIFY(!convertedHtmlContent.isEmpty()); QCOMPARE(convertedHtmlContent, referenceData); } +void TemplateParserJobTest::test_replyHtml_data() +{ + QTest::addColumn("mailFileName"); + QTest::addColumn("referenceFileName"); + + QDir dir(QStringLiteral(MAIL_DATA_DIR)); + const auto l = dir.entryList(QStringList(QStringLiteral("*.mbox")), QDir::Files | QDir::Readable | QDir::NoSymLinks); + foreach (const QString &file, l) { + const QString expectedFile = dir.path() + QLatin1Char('/') + file + QStringLiteral(".html.reply"); + if (!QFile::exists(expectedFile)) { + continue; + } + QTest::newRow(file.toLatin1().constData()) << QString(dir.path() + QLatin1Char('/') + file) << expectedFile; + } +} + +void TemplateParserJobTest::test_replyHtml() +{ + QFETCH(QString, mailFileName); + QFETCH(QString, referenceFileName); + + // load input mail + QFile mailFile(mailFileName); + QVERIFY(mailFile.open(QIODevice::ReadOnly)); + const QByteArray mailData = KMime::CRLFtoLF(mailFile.readAll()); + QVERIFY(!mailData.isEmpty()); + KMime::Message::Ptr msg(new KMime::Message); + KMime::Message::Ptr origMsg(new KMime::Message); + origMsg->setContent(mailData); + origMsg->parse(); + + // load expected result + QFile referenceFile(referenceFileName); + QVERIFY(referenceFile.open(QIODevice::ReadOnly)); + const QByteArray referenceRawData = KMime::CRLFtoLF(referenceFile.readAll()); + const QString referenceData = QString::fromLatin1(referenceRawData); + QVERIFY(!referenceData.isEmpty()); + + TemplateParser::TemplateParserJob *parser = new TemplateParser::TemplateParserJob(msg, TemplateParser::TemplateParserJob::NewMessage); + KIdentityManagement::IdentityManager *identMan = new KIdentityManagement::IdentityManager; + parser->setIdentityManager(identMan); + + parser->d->mOrigMsg = origMsg; + + QSignalSpy spy(parser, &TemplateParser::TemplateParserJob::parsingDone); + parser->processWithTemplate(QString()); + QVERIFY(spy.wait()); + QVERIFY(parser->d->mOtp->htmlContent().isEmpty()); + QVERIFY(!parser->d->mOtp->plainTextContent().isEmpty()); + + QString convertedHtmlContent = parser->htmlMessageText(false, TemplateParser::TemplateParserJob::NoSelectionAllowed); + QVERIFY(!convertedHtmlContent.isEmpty()); + + // referenceData is read from a file and most text editors add a \n at the end of the last line + if (!convertedHtmlContent.endsWith(QStringLiteral("\n"))) { + convertedHtmlContent += QStringLiteral("\n"); + } + + QCOMPARE(convertedHtmlContent, referenceData); +} + + void TemplateParserJobTest::test_replyPlain_data() { QTest::addColumn("mailFileName"); QTest::addColumn("referenceFileName"); QDir dir(QStringLiteral(MAIL_DATA_DIR)); const auto l = dir.entryList(QStringList(QStringLiteral("*.mbox")), QDir::Files | QDir::Readable | QDir::NoSymLinks); foreach (const QString &file, l) { const QString expectedFile = dir.path() + QLatin1Char('/') + file + QStringLiteral(".plain.reply"); if (!QFile::exists(expectedFile)) { continue; } QTest::newRow(file.toLatin1().constData()) << QString(dir.path() + QLatin1Char('/') + file) << expectedFile; } } void TemplateParserJobTest::test_replyPlain() { QFETCH(QString, mailFileName); QFETCH(QString, referenceFileName); // load input mail QFile mailFile(mailFileName); QVERIFY(mailFile.open(QIODevice::ReadOnly)); const QByteArray mailData = KMime::CRLFtoLF(mailFile.readAll()); QVERIFY(!mailData.isEmpty()); KMime::Message::Ptr msg(new KMime::Message); KMime::Message::Ptr origMsg(new KMime::Message); origMsg->setContent(mailData); origMsg->parse(); // load expected result QFile referenceFile(referenceFileName); QVERIFY(referenceFile.open(QIODevice::ReadOnly)); const QByteArray referenceRawData = KMime::CRLFtoLF(referenceFile.readAll()); const QString referenceData = QString::fromLatin1(referenceRawData); QVERIFY(!referenceData.isEmpty()); TemplateParser::TemplateParserJob *parser = new TemplateParser::TemplateParserJob(msg, TemplateParser::TemplateParserJob::Reply); parser->d->mOrigMsg = origMsg; QSignalSpy spy(parser, &TemplateParser::TemplateParserJob::parsingDone); parser->processWithTemplate(QString()); QVERIFY(spy.wait()); - const QString convertedHtmlContent = parser->plainMessageText(false, TemplateParser::TemplateParserJob::NoSelectionAllowed); + const QString convertedPlainTextContent = parser->plainMessageText(false, TemplateParser::TemplateParserJob::NoSelectionAllowed); + + QCOMPARE(convertedPlainTextContent, referenceData); +} + +void TemplateParserJobTest::test_forwardPlain_data() +{ + QTest::addColumn("mailFileName"); + QTest::addColumn("referenceFileName"); + + QDir dir(QStringLiteral(MAIL_DATA_DIR)); + const auto l = dir.entryList(QStringList(QStringLiteral("*.mbox")), QDir::Files | QDir::Readable | QDir::NoSymLinks); + foreach (const QString &file, l) { + const QString expectedFile = dir.path() + QLatin1Char('/') + file + QStringLiteral(".plain.reply"); + if (!QFile::exists(expectedFile)) { + continue; + } + QTest::newRow(file.toLatin1().constData()) << QString(dir.path() + QLatin1Char('/') + file) << expectedFile; + } +} + +void TemplateParserJobTest::test_forwardPlain() +{ + QFETCH(QString, mailFileName); + QFETCH(QString, referenceFileName); + + // load input mail + QFile mailFile(mailFileName); + QVERIFY(mailFile.open(QIODevice::ReadOnly)); + const QByteArray mailData = KMime::CRLFtoLF(mailFile.readAll()); + QVERIFY(!mailData.isEmpty()); + KMime::Message::Ptr msg(new KMime::Message); + KMime::Message::Ptr origMsg(new KMime::Message); + origMsg->setContent(mailData); + origMsg->parse(); + + // load expected result + QFile referenceFile(referenceFileName); + QVERIFY(referenceFile.open(QIODevice::ReadOnly)); + const QByteArray referenceRawData = KMime::CRLFtoLF(referenceFile.readAll()); + const QString referenceData = QString::fromLatin1(referenceRawData); + QVERIFY(!referenceData.isEmpty()); + + TemplateParser::TemplateParserJob *parser = new TemplateParser::TemplateParserJob(msg, TemplateParser::TemplateParserJob::Forward); + parser->d->mOrigMsg = origMsg; + + QSignalSpy spy(parser, &TemplateParser::TemplateParserJob::parsingDone); + parser->processWithTemplate(QString()); + QVERIFY(spy.wait()); + + const QString convertedPlainTextContent = parser->plainMessageText(false, TemplateParser::TemplateParserJob::NoSelectionAllowed); + + QCOMPARE(convertedPlainTextContent, referenceData); +} + +void TemplateParserJobTest::test_forwardHtml_data() +{ + QTest::addColumn("mailFileName"); + QTest::addColumn("referenceFileName"); + + QDir dir(QStringLiteral(MAIL_DATA_DIR)); + const auto l = dir.entryList(QStringList(QStringLiteral("*.mbox")), QDir::Files | QDir::Readable | QDir::NoSymLinks); + foreach (const QString &file, l) { + const QString expectedFile = dir.path() + QLatin1Char('/') + file + QStringLiteral(".html.reply"); + if (!QFile::exists(expectedFile)) { + continue; + } + QTest::newRow(file.toLatin1().constData()) << QString(dir.path() + QLatin1Char('/') + file) << expectedFile; + } +} + +void TemplateParserJobTest::test_forwardHtml() +{ + QFETCH(QString, mailFileName); + QFETCH(QString, referenceFileName); + + // load input mail + QFile mailFile(mailFileName); + QVERIFY(mailFile.open(QIODevice::ReadOnly)); + const QByteArray mailData = KMime::CRLFtoLF(mailFile.readAll()); + QVERIFY(!mailData.isEmpty()); + KMime::Message::Ptr msg(new KMime::Message); + KMime::Message::Ptr origMsg(new KMime::Message); + origMsg->setContent(mailData); + origMsg->parse(); + + // load expected result + QFile referenceFile(referenceFileName); + QVERIFY(referenceFile.open(QIODevice::ReadOnly)); + const QByteArray referenceRawData = KMime::CRLFtoLF(referenceFile.readAll()); + const QString referenceData = QString::fromLatin1(referenceRawData); + QVERIFY(!referenceData.isEmpty()); + + TemplateParser::TemplateParserJob *parser = new TemplateParser::TemplateParserJob(msg, TemplateParser::TemplateParserJob::Forward); + parser->d->mOrigMsg = origMsg; + + QSignalSpy spy(parser, &TemplateParser::TemplateParserJob::parsingDone); + parser->processWithTemplate(QString()); + QVERIFY(spy.wait()); + + QString convertedHtmlContent = parser->htmlMessageText(false, TemplateParser::TemplateParserJob::NoSelectionAllowed); + // referenceData is read from a file and most text editors add a \n at the end of the last line + if (!convertedHtmlContent.endsWith(QStringLiteral("\n"))) { + convertedHtmlContent += QStringLiteral("\n"); + } QCOMPARE(convertedHtmlContent, referenceData); } +void TemplateParserJobTest::test_forwardedAttachments_data() +{ + QTest::addColumn("mailFileName"); + QTest::addColumn("referenceFileName"); + + QDir dir(QStringLiteral(MAIL_DATA_DIR)); + const auto l = dir.entryList(QStringList(QStringLiteral("*.mbox")), QDir::Files | QDir::Readable | QDir::NoSymLinks); + foreach (const QString &file, l) { + if (!QFile::exists(dir.path() + QLatin1Char('/') + file + QStringLiteral(".html.reply"))) { + continue; + } + QString expectedFile = dir.path() + QLatin1Char('/') + file + QStringLiteral(".forwarded.mbox"); + QTest::newRow(file.toLatin1().constData()) << QString(dir.path() + QLatin1Char('/') + file) << expectedFile; + } +} + +void TemplateParserJobTest::test_forwardedAttachments() +{ + QFETCH(QString, mailFileName); + QFETCH(QString, referenceFileName); + + // load input mail + QFile mailFile(mailFileName); + QVERIFY(mailFile.open(QIODevice::ReadOnly)); + const QByteArray mailData = KMime::CRLFtoLF(mailFile.readAll()); + QVERIFY(!mailData.isEmpty()); + KMime::Message::Ptr msg(new KMime::Message); + KMime::Message::Ptr origMsg(new KMime::Message); + origMsg->setContent(mailData); + origMsg->parse(); + + bool referenceExists = QFile::exists(referenceFileName); + + TemplateParser::TemplateParserJob *parser = new TemplateParser::TemplateParserJob(msg, TemplateParser::TemplateParserJob::Forward); + parser->d->mOrigMsg = origMsg; + + QSignalSpy spy(parser, &TemplateParser::TemplateParserJob::parsingDone); + parser->processWithTemplate(QString()); + QVERIFY(spy.wait()); + + if (referenceExists) { + QFile referenceFile(referenceFileName); + QVERIFY(referenceFile.open(QIODevice::ReadOnly)); + const QByteArray referenceRawData = KMime::CRLFtoLF(referenceFile.readAll()); + const KMime::Message::Ptr referenceMsg(new KMime::Message); + referenceMsg->setContent(referenceRawData); + referenceMsg->parse(); + + QCOMPARE(msg->contents().size(), referenceMsg->contents().size()); + for (int i=1; i < msg->contents().size(); i++) { + QCOMPARE(msg->contents()[i]->encodedContent(), referenceMsg->contents()[i]->encodedContent()); + } + } else { + QCOMPARE(msg->contents().size(), 0); + } + +} + void TemplateParserJobTest::test_processWithTemplatesForBody_data() { QTest::addColumn("command"); QTest::addColumn("text"); QTest::addColumn("expected"); QTest::addColumn("selection"); QTest::newRow("%OTEXT-plain") << "%OTEXT" << "Original text.\nLine two." << "Original text.\nLine two." << ""; QTest::newRow("%OTEXT-encrypted") << "%OTEXT" << "-----BEGIN PGP MESSAGE-----\nVersion: GnuPG v1.4.12 (GNU/Linux)\n" "\n" "hQEMAwzOQ1qnzNo7AQgA1345CrnOBTGf2eo4ABR6wkOdasI9SELRBKA1fNkFcq+Z\n" "Qg0gWB5RLapU+VFRc5hK1zPOZ1dY6j3+uPHO4RhjfUgfiZ8T7oaWav15yP+07u21\n" "EI9W9sk+eQU9GZSOayURucmZa/mbBz9hrsmePpORxD+C3uNTYa6ePTFlQP6wEZOI\n" "7E53DrtJnF0EzIsCBIVep6CyuYfuSSwQ5gMgyPzfBqiGHNw96w2i/eayErc6lquL\n" "JPFhIcMMq8w9Yo9+vXCAbkns6dtBAzlnAzuV86VFUZ/MnHTlCNk2yHyGLP6BS6hG\n" "kFEUmgdHrGRizdz1sjo1tSmOLu+Gyjlv1Ir/Sqr8etJQAeTq3heKslAfhtotAMMt\n" "R3tk228Su13Q3CAP/rktAyuGMDFtH8klW09zFdsZBDu8svE6d9e2nZ541NGspFVI\n" "6XTZHUMMdlgnTBcu3aPc0ow=\n" "=0xtc\n" "-----END PGP MESSAGE-----" << "Crypted line.\nCrypted line two." << ""; QTest::newRow("%QUOTE") << "%QUOTE" << "Quoted text.\nLine two." << "> Quoted text.\n> Line two." << ""; } void TemplateParserJobTest::test_processWithTemplatesForBody() { QFETCH(QString, command); QFETCH(QString, text); QFETCH(QString, expected); QFETCH(QString, selection); KMime::Message::Ptr msg(new KMime::Message()); - msg->setBody(text.toLocal8Bit()); - msg->parse(); + KMime::Message::Ptr origMsg(new KMime::Message()); + origMsg->setBody(text.toLocal8Bit()); + origMsg->parse(); TemplateParser::TemplateParserJob *parser = new TemplateParser::TemplateParserJob(msg, TemplateParser::TemplateParserJob::Reply); parser->setSelection(selection); KIdentityManagement::IdentityManager *identMan = new KIdentityManagement::IdentityManager; parser->setIdentityManager(identMan); parser->setAllowDecryption(true); - parser->d->mOrigMsg = msg; + parser->d->mOrigMsg = origMsg; QSignalSpy spy(parser, &TemplateParser::TemplateParserJob::parsingDone); parser->processWithTemplate(command); QVERIFY(spy.wait()); identMan->deleteLater(); QCOMPARE(QString::fromLatin1(msg->encodedBody()), expected); QCOMPARE(spy.count(), 1); } void TemplateParserJobTest::test_processWithTemplatesForContent_data() { QTest::addColumn("command"); QTest::addColumn("mailFileName"); QTest::addColumn("expectedBody"); QTest::addColumn("hasDictionary"); qputenv("TZ", "Europe/Paris"); QDir dir(QStringLiteral(MAIL_DATA_DIR)); const QString file = QStringLiteral("plain-message.mbox"); const QString fileName = QString(dir.path() + QLatin1Char('/') + file); QTest::newRow("%OTIME") << "%OTIME" << fileName << QLocale().toString(QTime(8, 0, 27), QLocale::ShortFormat) << false; QTest::newRow("%OTIMELONG") << "%OTIMELONG" << fileName << QLocale().toString(QTime(8, 0, 27), QLocale::LongFormat) << false; QTest::newRow("%OTIMELONGEN") << "%OTIMELONGEN" << fileName << QLocale(QLocale::C).toString(QTime(8, 0, 27), QLocale::LongFormat) << false; QTest::newRow("%ODATE") << "%ODATE" << fileName << QLocale().toString(QDate(2011, 8, 7), QLocale::LongFormat) << false; QTest::newRow("%ODATESHORT") << "%ODATESHORT" << fileName << QLocale().toString(QDate(2011, 8, 7), QLocale::ShortFormat) << false; QTest::newRow("%ODATEEN") << "%ODATEEN" << fileName << QLocale::c().toString(QDate(2011, 8, 7), QLocale::LongFormat) << false; QTest::newRow("%OFULLSUBJ") << "%OFULLSUBJ" << fileName << "Plain Message Test" << false; QTest::newRow("%OFULLSUBJECT") << "%OFULLSUBJECT" << fileName << "Plain Message Test" << false; QTest::newRow("%OFROMFNAME") << "%OFROMFNAME" << fileName << "Sudhendu" << false; QTest::newRow("%OFROMLNAME") << "%OFROMLNAME" << fileName << "Kumar" << false; QTest::newRow("%OFROMNAME") << "%OFROMNAME" << fileName << "Sudhendu Kumar" << false; QTest::newRow("%OFROMADDR") << "%OFROMADDR" << fileName << "Sudhendu Kumar " << false; QTest::newRow("%OTOADDR") << "%OTOADDR" << fileName << "kde " << false; QTest::newRow("%OTOFNAME") << "%OTOFNAME" << fileName << "kde" << false; QTest::newRow("%OTONAME") << "%OTONAME" << fileName << "kde" << false; QTest::newRow("%OTOLNAME") << "%OTOLNAME" << fileName << "" << false; QTest::newRow("%OTOLIST") << "%OTOLIST" << fileName << "kde " << false; QTest::newRow("%ODOW") << "%ODOW" << fileName << QLocale().dayName(7, QLocale::LongFormat) << false; QTest::newRow("%BLANK") << "%BLANK" << fileName << "" << false; QTest::newRow("%NOP") << "%NOP" << fileName << "" << false; QTest::newRow("%DICTIONARYLANGUAGE=\"en\"") << "%DICTIONARYLANGUAGE=\"en\"" << fileName << "" << true; QTest::newRow("%DICTIONARYLANGUAGE=\"\"") << "%DICTIONARYLANGUAGE=\"\"" << fileName << "" << false; QTest::newRow("%OTIMELONG %OFULLSUBJECT") << "%OTIMELONG %OFULLSUBJECT" << fileName << QLocale().toString(QTime(8, 0, 27), QLocale::LongFormat) + QStringLiteral(" Plain Message Test") << false; QTest::newRow("%OTIMELONG\n%OFULLSUBJECT") << "%OTIMELONG\n%OFULLSUBJECT" << fileName << QLocale().toString(QTime(8, 0, 27), QLocale::LongFormat) + QStringLiteral("\nPlain Message Test") << false; QTest::newRow("%REM=\"sdfsfsdsdfsdf\"") << "%REM=\"sdfsfsdsdfsdf\"" << fileName << "" << false; QTest::newRow("%CLEAR") << "%CLEAR" << fileName << "" << false; QTest::newRow("FOO foo") << "FOO foo" << fileName << "FOO foo" << false; const QString insertFileName = QString(dir.path() + QLatin1Char('/') + QLatin1String("insert-file.txt")); QString insertFileNameCommand = QStringLiteral("%INSERT=\"%1\"").arg(insertFileName); QTest::newRow("%INSERT") << insertFileNameCommand << fileName << "test insert file!\n" << false; insertFileNameCommand = QStringLiteral("%PUT=\"%1\"").arg(insertFileName); QTest::newRow("%PUT") << insertFileNameCommand << fileName << "test insert file!\n" << false; - QTest::newRow("%MSGID") << "%MSGID" << fileName << "<20150@foo.kde.org>" << false; + QTest::newRow("%OMSGID") << "%OMSGID" << fileName << "<20150@foo.kde.org>" << false; QTest::newRow("%SYSTEM") << "%SYSTEM=\"echo foo\"" << fileName << "foo\n" << false; QTest::newRow("%DEBUG") << "%DEBUG" << fileName << "" << false; QTest::newRow("%DEBUGOFF") << "%DEBUGOFF" << fileName << "" << false; QTest::newRow("%HEADER=\"Reply-To\"") << "%HEADER=\"Reply-To\"" << fileName << "bla@yoohoo.org" << false; //Header doesn't exist => don't add value QTest::newRow("%OHEADER=\"SSS\"") << "%HEADER=\"SSS\"" << fileName << "" << false; QTest::newRow("%OHEADER=\"To\"") << "%OHEADER=\"To\"" << fileName << "kde " << false; //Unknown command QTest::newRow("unknown command") << "%GGGGG" << fileName << "%GGGGG" << false; //Test bug 308444 const QString file2 = QStringLiteral("plain-message-timezone.mbox"); const QString fileName2 = QString(dir.path() + QLatin1Char('/') + file2); QTest::newRow("bug308444-%OTIMELONG") << "%OTIMELONG" << fileName2 << QLocale::system().toString(QTime(20, 31, 25), QLocale::LongFormat) << false; } void TemplateParserJobTest::test_processWithTemplatesForContent() { QFETCH(QString, command); QFETCH(QString, mailFileName); QFETCH(QString, expectedBody); QFETCH(bool, hasDictionary); QFile mailFile(mailFileName); QVERIFY(mailFile.open(QIODevice::ReadOnly)); const QByteArray mailData = KMime::CRLFtoLF(mailFile.readAll()); QVERIFY(!mailData.isEmpty()); KMime::Message::Ptr msg(new KMime::Message); - msg->setContent(mailData); - msg->parse(); + KMime::Message::Ptr origMsg(new KMime::Message); + origMsg->setContent(mailData); + origMsg->parse(); TemplateParser::TemplateParserJob *parser = new TemplateParser::TemplateParserJob(msg, TemplateParser::TemplateParserJob::Reply); KIdentityManagement::IdentityManager *identMan = new KIdentityManagement::IdentityManager; parser->setIdentityManager(identMan); parser->setAllowDecryption(false); - parser->d->mOrigMsg = msg; + parser->d->mOrigMsg = origMsg; QSignalSpy spy(parser, &TemplateParser::TemplateParserJob::parsingDone); parser->processWithTemplate(command); QVERIFY(spy.wait()); QCOMPARE(msg->hasHeader("X-KMail-Dictionary"), hasDictionary); identMan->deleteLater(); QCOMPARE(QString::fromUtf8(msg->encodedBody()), expectedBody); QCOMPARE(spy.count(), 1); } void TemplateParserJobTest::test_processWithTemplatesForContentOtherTimeZone_data() { QTest::addColumn("command"); QTest::addColumn("mailFileName"); QTest::addColumn("expectedBody"); QTest::addColumn("hasDictionary"); qputenv("TZ", "America/New_York"); QDir dir(QStringLiteral(MAIL_DATA_DIR)); //Test bug 308444 const QString file2 = QStringLiteral("plain-message-timezone.mbox"); const QString fileName2 = QString(dir.path() + QLatin1Char('/') + file2); QTest::newRow("bug308444-%OTIMELONG") << "%OTIMELONG" << fileName2 << QLocale::system().toString(QTime(14, 31, 25), QLocale::LongFormat) << false; } void TemplateParserJobTest::test_processWithTemplatesForContentOtherTimeZone() { QFETCH(QString, command); QFETCH(QString, mailFileName); QFETCH(QString, expectedBody); QFETCH(bool, hasDictionary); QFile mailFile(mailFileName); QVERIFY(mailFile.open(QIODevice::ReadOnly)); const QByteArray mailData = KMime::CRLFtoLF(mailFile.readAll()); QVERIFY(!mailData.isEmpty()); KMime::Message::Ptr msg(new KMime::Message); - msg->setContent(mailData); - msg->parse(); + KMime::Message::Ptr origMsg(new KMime::Message); + origMsg->setContent(mailData); + origMsg->parse(); TemplateParser::TemplateParserJob *parser = new TemplateParser::TemplateParserJob(msg, TemplateParser::TemplateParserJob::Reply); KIdentityManagement::IdentityManager *identMan = new KIdentityManagement::IdentityManager; parser->setIdentityManager(identMan); parser->setAllowDecryption(false); - parser->d->mOrigMsg = msg; + parser->d->mOrigMsg = origMsg; QSignalSpy spy(parser, &TemplateParser::TemplateParserJob::parsingDone); parser->processWithTemplate(command); QVERIFY(spy.wait()); QCOMPARE(msg->hasHeader("X-KMail-Dictionary"), hasDictionary); identMan->deleteLater(); QCOMPARE(QString::fromUtf8(msg->encodedBody()), expectedBody); QCOMPARE(spy.count(), 1); } QTEST_MAIN(TemplateParserJobTest) diff --git a/templateparser/autotests/templateparserjobtest.h b/templateparser/autotests/templateparserjobtest.h index 83833450..f7771e8a 100644 --- a/templateparser/autotests/templateparserjobtest.h +++ b/templateparser/autotests/templateparserjobtest.h @@ -1,55 +1,67 @@ /* Copyright (C) 2017-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 TEMPLATEPARSERJOBTEST_H #define TEMPLATEPARSERJOBTEST_H #include class TemplateParserJobTest : public QObject { Q_OBJECT public: explicit TemplateParserJobTest(QObject *parent = nullptr); ~TemplateParserJobTest(); private Q_SLOTS: /** * checks whether text/plain only mails are converted to a valid HTML */ void test_convertedHtml(); void test_convertedHtml_data(); + void test_replyHtml(); + void test_replyHtml_data(); + void test_replyPlain(); void test_replyPlain_data(); + void test_forwardPlain(); + void test_forwardPlain_data(); + + void test_forwardHtml(); + void test_forwardHtml_data(); + + void test_forwardedAttachments(); + void test_forwardedAttachments_data(); + /** * Tests whether templates are returning required body or not */ void test_processWithTemplatesForBody(); void test_processWithTemplatesForBody_data(); void test_processWithTemplatesForContent(); void test_processWithTemplatesForContent_data(); void test_processWithTemplatesForContentOtherTimeZone(); void test_processWithTemplatesForContentOtherTimeZone_data(); }; #endif // TEMPLATEPARSERJOBTEST_H diff --git a/templateparser/src/templateparserjob.cpp b/templateparser/src/templateparserjob.cpp index 58dc0c30..a93799d5 100644 --- a/templateparser/src/templateparserjob.cpp +++ b/templateparser/src/templateparserjob.cpp @@ -1,1613 +1,1645 @@ /* Copyright (C) 2017-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 "templateparserjob.h" #include "templateparserjob_p.h" #include "templateparserextracthtmlinfo.h" #include "globalsettings_templateparser.h" #include "customtemplates_kfg.h" #include "templatesconfiguration_kfg.h" #include "templatesconfiguration.h" #include #include #include +#include #include #include #include #include #include #include #include #include #include "templateparser_debug.h" #include #include #include #include #include namespace { Q_DECL_CONSTEXPR inline int pipeTimeout() { return 15 * 1000; } static QTextCodec *selectCharset(const QStringList &charsets, const QString &text) { for (const QString &name : charsets) { // We use KCharsets::codecForName() instead of QTextCodec::codecForName() here, because // the former knows us-ascii is latin1. bool ok = true; QTextCodec *codec = nullptr; if (name == QLatin1String("locale")) { codec = QTextCodec::codecForLocale(); } else { codec = KCharsets::charsets()->codecForName(name, ok); } if (!ok || !codec) { qCWarning(TEMPLATEPARSER_LOG) << "Could not get text codec for charset" << name; continue; } if (codec->canEncode(text)) { // Special check for us-ascii (needed because us-ascii is not exactly latin1). if (name == QLatin1String("us-ascii") && !KMime::isUsAscii(text)) { continue; } qCDebug(TEMPLATEPARSER_LOG) << "Chosen charset" << name << codec->name(); return codec; } } qCDebug(TEMPLATEPARSER_LOG) << "No appropriate charset found."; return KCharsets::charsets()->codecForName(QStringLiteral("utf-8")); } } using namespace TemplateParser; TemplateParserJobPrivate::TemplateParserJobPrivate(const KMime::Message::Ptr &amsg, const TemplateParserJob::Mode amode) : mMsg(amsg) , mMode(amode) { mEmptySource = new MimeTreeParser::SimpleObjectTreeSource; mEmptySource->setDecryptMessage(mAllowDecryption); mOtp = new MimeTreeParser::ObjectTreeParser(mEmptySource); mOtp->setAllowAsync(false); } TemplateParserJobPrivate::~TemplateParserJobPrivate() { delete mEmptySource; delete mOtp; } void TemplateParserJobPrivate::setAllowDecryption(const bool allowDecryption) { mAllowDecryption = allowDecryption; mEmptySource->setDecryptMessage(mAllowDecryption); } TemplateParserJob::TemplateParserJob(const KMime::Message::Ptr &amsg, const Mode amode, QObject *parent) : QObject(parent) , d(new TemplateParserJobPrivate(amsg, amode)) { } TemplateParserJob::~TemplateParserJob() = default; void TemplateParserJob::setSelection(const QString &selection) { d->mSelection = selection; } void TemplateParserJob::setAllowDecryption(const bool allowDecryption) { d->setAllowDecryption(allowDecryption); } bool TemplateParserJob::shouldStripSignature() const { // Only strip the signature when replying, it should be preserved when forwarding return (d->mMode == Reply || d->mMode == ReplyAll) && TemplateParserSettings::self()->stripSignature(); } void TemplateParserJob::setIdentityManager(KIdentityManagement::IdentityManager *ident) { d->m_identityManager = ident; } void TemplateParserJob::setCharsets(const QStringList &charsets) { d->mCharsets = charsets; } int TemplateParserJob::parseQuotes(const QString &prefix, const QString &str, QString "e) { int pos = prefix.length(); int len; const int str_len = str.length(); // Also allow the german lower double-quote sign as quote separator, not only // the standard ASCII quote ("). This fixes bug 166728. const QList< QChar > quoteChars = {QLatin1Char('"'), 0x201C}; QChar prev(QChar::Null); pos++; len = pos; while (pos < str_len) { const QChar c = str[pos]; pos++; len++; if (!prev.isNull()) { quote.append(c); prev = QChar::Null; } else { if (c == QLatin1Char('\\')) { prev = c; } else if (quoteChars.contains(c)) { break; } else { quote.append(c); } } } return len; } QString TemplateParserJob::getFirstName(const QString &str) { // simple logic: // if there is ',' in name, than format is 'Last, First' // else format is 'First Last' // last resort -- return 'name' from 'name@domain' int sep_pos; QString res; if ((sep_pos = str.indexOf(QLatin1Char('@'))) > 0) { int i; for (i = (sep_pos - 1); i >= 0; --i) { QChar c = str[i]; if (c.isLetterOrNumber()) { res.prepend(c); } else { break; } } } else if ((sep_pos = str.indexOf(QLatin1Char(','))) > 0) { int i; bool begin = false; const int strLength(str.length()); for (i = sep_pos; i < strLength; ++i) { QChar c = str[i]; if (c.isLetterOrNumber()) { begin = true; res.append(c); } else if (begin) { break; } } } else { int i; const int strLength(str.length()); for (i = 0; i < strLength; ++i) { QChar c = str[i]; if (c.isLetterOrNumber()) { res.append(c); } else { break; } } } return res; } QString TemplateParserJob::getLastName(const QString &str) { // simple logic: // if there is ',' in name, than format is 'Last, First' // else format is 'First Last' int sep_pos; QString res; if ((sep_pos = str.indexOf(QLatin1Char(','))) > 0) { int i; for (i = sep_pos; i >= 0; --i) { QChar c = str[i]; if (c.isLetterOrNumber()) { res.prepend(c); } else { break; } } } else { if ((sep_pos = str.indexOf(QLatin1Char(' '))) > 0) { bool begin = false; const int strLength(str.length()); for (int i = sep_pos; i < strLength; ++i) { QChar c = str[i]; if (c.isLetterOrNumber()) { begin = true; res.append(c); } else if (begin) { break; } } } } return res; } void TemplateParserJob::process(const KMime::Message::Ptr &aorig_msg, qint64 afolder) { if (aorig_msg == nullptr) { qCDebug(TEMPLATEPARSER_LOG) << "aorig_msg == 0!"; Q_EMIT parsingDone(d->mForceCursorPosition); deleteLater(); return; } d->mOrigMsg = aorig_msg; d->mFolder = afolder; const QString tmpl = findTemplate(); if (tmpl.isEmpty()) { Q_EMIT parsingDone(d->mForceCursorPosition); deleteLater(); return; } processWithTemplate(tmpl); } void TemplateParserJob::process(const QString &tmplName, const KMime::Message::Ptr &aorig_msg, qint64 afolder) { d->mForceCursorPosition = false; d->mOrigMsg = aorig_msg; d->mFolder = afolder; const QString tmpl = findCustomTemplate(tmplName); processWithTemplate(tmpl); } void TemplateParserJob::processWithIdentity(uint uoid, const KMime::Message::Ptr &aorig_msg, qint64 afolder) { d->mIdentity = uoid; process(aorig_msg, afolder); } +MimeTreeParser::MessagePart::Ptr toplevelTextNode(MimeTreeParser::MessagePart::Ptr messageTree) +{ + foreach (const auto &mp, messageTree->subParts()) { + auto text = mp.dynamicCast(); + const auto attach = mp.dynamicCast(); + if (text && !attach) { + // TextMessagePart can have several subparts cause of PGP inline, we search for the first part with content + foreach (const auto &sub, mp->subParts()) { + if (!sub->text().trimmed().isEmpty()) { + return sub; + } + } + return text; + } else if (const auto html = mp.dynamicCast()) { + return html; + } else if (const auto alternative = mp.dynamicCast()) { + return alternative; + } else { + auto ret = toplevelTextNode(mp); + if (ret) { + return ret; + } + } + } + return MimeTreeParser::MessagePart::Ptr(); +} + void TemplateParserJob::processWithTemplate(const QString &tmpl) { d->mOtp->parseObjectTree(d->mOrigMsg.data()); - TemplateParserExtractHtmlInfo *job = new TemplateParserExtractHtmlInfo(this); - connect(job, &TemplateParserExtractHtmlInfo::finished, this, &TemplateParserJob::slotExtractInfoDone); + const auto mp = toplevelTextNode(d->mOtp->parsedPart()); - QString plainText = d->mOtp->plainTextContent(); - if (plainText.isEmpty()) { //HTML-only mails - plainText = d->mOtp->htmlContent(); - } + QString plainText = mp->plaintextContent(); + QString htmlElement; - job->setHtmlForExtractingTextPlain(plainText); - job->setTemplate(tmpl); - - QString htmlElement = d->mOtp->htmlContent(); - - if (htmlElement.isEmpty()) { //plain mails only - QString htmlReplace = d->mOtp->plainTextContent().toHtmlEscaped(); + if (mp->isHtml()) { + htmlElement = d->mOtp->htmlContent(); + if (plainText.isEmpty()) { //HTML-only mails + plainText = htmlElement; + } + } else { //plain mails only + QString htmlReplace = plainText.toHtmlEscaped(); htmlReplace = htmlReplace.replace(QLatin1Char('\n'), QStringLiteral("
")); htmlElement = QStringLiteral("%1\n").arg(htmlReplace); } + TemplateParserExtractHtmlInfo *job = new TemplateParserExtractHtmlInfo(this); + connect(job, &TemplateParserExtractHtmlInfo::finished, this, &TemplateParserJob::slotExtractInfoDone); + + job->setHtmlForExtractingTextPlain(plainText); + job->setTemplate(tmpl); + job->setHtmlForExtractionHeaderAndBody(htmlElement); job->start(); } void TemplateParserJob::slotExtractInfoDone(const TemplateParserExtractHtmlInfoResult &result) { d->mExtractHtmlInfoResult = result; const QString tmpl = d->mExtractHtmlInfoResult.mTemplate; const int tmpl_len = tmpl.length(); QString plainBody, htmlBody; bool dnl = false; auto definedLocale = QLocale(); for (int i = 0; i < tmpl_len; ++i) { QChar c = tmpl[i]; // qCDebug(TEMPLATEPARSER_LOG) << "Next char: " << c; if (c == QLatin1Char('%')) { const QString cmd = tmpl.mid(i + 1); if (cmd.startsWith(QLatin1Char('-'))) { // dnl qCDebug(TEMPLATEPARSER_LOG) << "Command: -"; dnl = true; i += 1; } else if (cmd.startsWith(QLatin1String("REM="))) { // comments qCDebug(TEMPLATEPARSER_LOG) << "Command: REM="; QString q; const int len = parseQuotes(QStringLiteral("REM="), cmd, q); i += len; } else if (cmd.startsWith(QLatin1String("LANGUAGE="))) { QString q; const int len = parseQuotes(QStringLiteral("LANGUAGE="), cmd, q); i += len; if (!q.isEmpty()) { definedLocale = QLocale(q); } } else if (cmd.startsWith(QLatin1String("DICTIONARYLANGUAGE="))) { QString q; const int len = parseQuotes(QStringLiteral("DICTIONARYLANGUAGE="), cmd, q); i += len; if (!q.isEmpty()) { KMime::Headers::Generic *header = new KMime::Headers::Generic("X-KMail-Dictionary"); header->fromUnicodeString(q, "utf-8"); d->mMsg->setHeader(header); } } else if (cmd.startsWith(QLatin1String("INSERT=")) || cmd.startsWith(QLatin1String("PUT="))) { QString q; int len = 0; if (cmd.startsWith(QLatin1String("INSERT="))) { // insert content of specified file as is qCDebug(TEMPLATEPARSER_LOG) << "Command: INSERT="; len = parseQuotes(QStringLiteral("INSERT="), cmd, q); } else { // insert content of specified file as is qCDebug(TEMPLATEPARSER_LOG) << "Command: PUT="; len = parseQuotes(QStringLiteral("PUT="), cmd, q); } i += len; QString path = KShell::tildeExpand(q); QFileInfo finfo(path); if (finfo.isRelative()) { path = QDir::homePath() + QLatin1Char('/') + q; } QFile file(path); if (file.open(QIODevice::ReadOnly)) { const QByteArray content = file.readAll(); const QString str = QString::fromLocal8Bit(content.constData(), content.size()); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (d->mDebug) { KMessageBox::error( nullptr, i18nc("@info", "Cannot insert content from file %1: %2", path, file.errorString())); } } else if (cmd.startsWith(QLatin1String("SYSTEM="))) { // insert content of specified file as is qCDebug(TEMPLATEPARSER_LOG) << "Command: SYSTEM="; QString q; const int len = parseQuotes(QStringLiteral("SYSTEM="), cmd, q); i += len; const QString pipe_cmd = q; const QString str = pipe(pipe_cmd, QString()); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("QUOTEPIPE="))) { // pipe message body through command and insert it as quotation qCDebug(TEMPLATEPARSER_LOG) << "Command: QUOTEPIPE="; QString q; const int len = parseQuotes(QStringLiteral("QUOTEPIPE="), cmd, q); i += len; const QString pipe_cmd = q; if (d->mOrigMsg) { const QString plainStr = pipe(pipe_cmd, plainMessageText(shouldStripSignature(), NoSelectionAllowed)); QString plainQuote = quotedPlainText(plainStr); if (plainQuote.endsWith(QLatin1Char('\n'))) { plainQuote.chop(1); } plainBody.append(plainQuote); const QString htmlStr = pipe(pipe_cmd, htmlMessageText(shouldStripSignature(), NoSelectionAllowed)); const QString htmlQuote = quotedHtmlText(htmlStr); htmlBody.append(htmlQuote); } } else if (cmd.startsWith(QLatin1String("QUOTE"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: QUOTE"; i += strlen("QUOTE"); if (d->mOrigMsg) { QString plainQuote = quotedPlainText(plainMessageText(shouldStripSignature(), SelectionAllowed)); if (plainQuote.endsWith(QLatin1Char('\n'))) { plainQuote.chop(1); } plainBody.append(plainQuote); const QString htmlQuote = quotedHtmlText(htmlMessageText(shouldStripSignature(), SelectionAllowed)); htmlBody.append(htmlQuote); } } else if (cmd.startsWith(QLatin1String("FORCEDPLAIN"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: FORCEDPLAIN"; d->mQuotes = ReplyAsPlain; i += strlen("FORCEDPLAIN"); } else if (cmd.startsWith(QLatin1String("FORCEDHTML"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: FORCEDHTML"; d->mQuotes = ReplyAsHtml; i += strlen("FORCEDHTML"); } else if (cmd.startsWith(QLatin1String("QHEADERS"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: QHEADERS"; i += strlen("QHEADERS"); if (d->mOrigMsg) { QString plainQuote = quotedPlainText(QString::fromLatin1(MessageCore::StringUtil::headerAsSendableString(d->mOrigMsg))); if (plainQuote.endsWith(QLatin1Char('\n'))) { plainQuote.chop(1); } plainBody.append(plainQuote); const QString htmlQuote = quotedHtmlText(QString::fromLatin1(MessageCore::StringUtil::headerAsSendableString(d->mOrigMsg))); const QString str = plainToHtml(htmlQuote); htmlBody.append(str); } } else if (cmd.startsWith(QLatin1String("HEADERS"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: HEADERS"; i += strlen("HEADERS"); if (d->mOrigMsg) { const QString str = QString::fromLatin1(MessageCore::StringUtil::headerAsSendableString(d->mOrigMsg)); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("TEXTPIPE="))) { // pipe message body through command and insert it as is qCDebug(TEMPLATEPARSER_LOG) << "Command: TEXTPIPE="; QString q; const int len = parseQuotes(QStringLiteral("TEXTPIPE="), cmd, q); i += len; const QString pipe_cmd = q; if (d->mOrigMsg) { const QString plainStr = pipe(pipe_cmd, plainMessageText(shouldStripSignature(), NoSelectionAllowed)); plainBody.append(plainStr); const QString htmlStr = pipe(pipe_cmd, htmlMessageText(shouldStripSignature(), NoSelectionAllowed)); htmlBody.append(htmlStr); } } else if (cmd.startsWith(QLatin1String("MSGPIPE="))) { // pipe full message through command and insert result as is qCDebug(TEMPLATEPARSER_LOG) << "Command: MSGPIPE="; QString q; const int len = parseQuotes(QStringLiteral("MSGPIPE="), cmd, q); i += len; if (d->mOrigMsg) { QString pipe_cmd = q; const QString str = pipe(pipe_cmd, QString::fromLatin1(d->mOrigMsg->encodedContent())); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("BODYPIPE="))) { // pipe message body generated so far through command and insert result as is qCDebug(TEMPLATEPARSER_LOG) << "Command: BODYPIPE="; QString q; const int len = parseQuotes(QStringLiteral("BODYPIPE="), cmd, q); i += len; const QString pipe_cmd = q; const QString plainStr = pipe(pipe_cmd, plainBody); plainBody.append(plainStr); const QString htmlStr = pipe(pipe_cmd, htmlBody); const QString body = plainToHtml(htmlStr); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("CLEARPIPE="))) { // pipe message body generated so far through command and // insert result as is replacing current body qCDebug(TEMPLATEPARSER_LOG) << "Command: CLEARPIPE="; QString q; const int len = parseQuotes(QStringLiteral("CLEARPIPE="), cmd, q); i += len; const QString pipe_cmd = q; const QString plainStr = pipe(pipe_cmd, plainBody); plainBody = plainStr; const QString htmlStr = pipe(pipe_cmd, htmlBody); htmlBody = htmlStr; KMime::Headers::Generic *header = new KMime::Headers::Generic("X-KMail-CursorPos"); header->fromUnicodeString(QString::number(0), "utf-8"); d->mMsg->setHeader(header); } else if (cmd.startsWith(QLatin1String("TEXT"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: TEXT"; i += strlen("TEXT"); if (d->mOrigMsg) { const QString plainStr = plainMessageText(shouldStripSignature(), NoSelectionAllowed); plainBody.append(plainStr); const QString htmlStr = htmlMessageText(shouldStripSignature(), NoSelectionAllowed); htmlBody.append(htmlStr); } } else if (cmd.startsWith(QLatin1String("OTEXTSIZE"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OTEXTSIZE"; i += strlen("OTEXTSIZE"); if (d->mOrigMsg) { const QString str = QStringLiteral("%1").arg(d->mOrigMsg->body().length()); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OTEXT"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OTEXT"; i += strlen("OTEXT"); if (d->mOrigMsg) { const QString plainStr = plainMessageText(shouldStripSignature(), NoSelectionAllowed); plainBody.append(plainStr); const QString htmlStr = htmlMessageText(shouldStripSignature(), NoSelectionAllowed); htmlBody.append(htmlStr); } } else if (cmd.startsWith(QLatin1String("OADDRESSEESADDR"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OADDRESSEESADDR"; i += strlen("OADDRESSEESADDR"); if (d->mOrigMsg) { const QString to = d->mOrigMsg->to()->asUnicodeString(); const QString cc = d->mOrigMsg->cc()->asUnicodeString(); if (!to.isEmpty()) { const QString toLine = i18nc("@item:intext email To", "To:") + QLatin1Char(' ') + to; plainBody.append(toLine); const QString body = plainToHtml(toLine); htmlBody.append(body); } if (!to.isEmpty() && !cc.isEmpty()) { plainBody.append(QLatin1Char('\n')); const QString str = plainToHtml(QString(QLatin1Char('\n'))); htmlBody.append(str); } if (!cc.isEmpty()) { const QString ccLine = i18nc("@item:intext email CC", "CC:") + QLatin1Char(' ') + cc; plainBody.append(ccLine); const QString str = plainToHtml(ccLine); htmlBody.append(str); } } } else if (cmd.startsWith(QLatin1String("CCADDR"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: CCADDR"; i += strlen("CCADDR"); const QString str = d->mMsg->cc()->asUnicodeString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("CCNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: CCNAME"; i += strlen("CCNAME"); const QString str = d->mMsg->cc()->displayString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("CCFNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: CCFNAME"; i += strlen("CCFNAME"); const QString str = d->mMsg->cc()->displayString(); plainBody.append(getFirstName(str)); const QString body = plainToHtml(getFirstName(str)); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("CCLNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: CCLNAME"; i += strlen("CCLNAME"); const QString str = d->mMsg->cc()->displayString(); plainBody.append(getLastName(str)); const QString body = plainToHtml(getLastName(str)); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("TOADDR"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: TOADDR"; i += strlen("TOADDR"); const QString str = d->mMsg->to()->asUnicodeString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("TONAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: TONAME"; i += strlen("TONAME"); const QString str = (d->mMsg->to()->displayString()); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("TOFNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: TOFNAME"; i += strlen("TOFNAME"); const QString str = d->mMsg->to()->displayString(); plainBody.append(getFirstName(str)); const QString body = plainToHtml(getFirstName(str)); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("TOLNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: TOLNAME"; i += strlen("TOLNAME"); const QString str = d->mMsg->to()->displayString(); plainBody.append(getLastName(str)); const QString body = plainToHtml(getLastName(str)); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("TOLIST"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: TOLIST"; i += strlen("TOLIST"); const QString str = d->mMsg->to()->asUnicodeString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("FROMADDR"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: FROMADDR"; i += strlen("FROMADDR"); const QString str = d->mMsg->from()->asUnicodeString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("FROMNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: FROMNAME"; i += strlen("FROMNAME"); const QString str = d->mMsg->from()->displayString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("FROMFNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: FROMFNAME"; i += strlen("FROMFNAME"); const QString str = d->mMsg->from()->displayString(); plainBody.append(getFirstName(str)); const QString body = plainToHtml(getFirstName(str)); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("FROMLNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: FROMLNAME"; i += strlen("FROMLNAME"); const QString str = d->mMsg->from()->displayString(); plainBody.append(getLastName(str)); const QString body = plainToHtml(getLastName(str)); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("FULLSUBJECT")) || cmd.startsWith(QLatin1String("FULLSUBJ"))) { if (cmd.startsWith(QLatin1String("FULLSUBJ"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: FULLSUBJ"; i += strlen("FULLSUBJ"); } else { qCDebug(TEMPLATEPARSER_LOG) << "Command: FULLSUBJECT"; i += strlen("FULLSUBJECT"); } const QString str = d->mMsg->subject()->asUnicodeString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("MSGID"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: MSGID"; i += strlen("MSGID"); const QString str = d->mMsg->messageID()->asUnicodeString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("OHEADER="))) { // insert specified content of header from original message qCDebug(TEMPLATEPARSER_LOG) << "Command: OHEADER="; QString q; const int len = parseQuotes(QStringLiteral("OHEADER="), cmd, q); i += len; if (d->mOrigMsg) { const QString hdr = q; QString str; if (auto hrdMsgOrigin = d->mOrigMsg->headerByType(hdr.toLocal8Bit().constData())) { str = hrdMsgOrigin->asUnicodeString(); } plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("HEADER="))) { // insert specified content of header from current message qCDebug(TEMPLATEPARSER_LOG) << "Command: HEADER="; QString q; const int len = parseQuotes(QStringLiteral("HEADER="), cmd, q); i += len; const QString hdr = q; QString str; if (auto hrdMsgOrigin = d->mOrigMsg->headerByType(hdr.toLocal8Bit().constData())) { str = hrdMsgOrigin->asUnicodeString(); } plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("HEADER( "))) { // insert specified content of header from current message qCDebug(TEMPLATEPARSER_LOG) << "Command: HEADER("; QRegExp re = QRegExp(QLatin1String("^HEADER\\((.+)\\)")); re.setMinimal(true); int res = re.indexIn(cmd); if (res != 0) { // something wrong i += strlen("HEADER( "); } else { i += re.matchedLength(); const QString hdr = re.cap(1); QString str; if (auto hrdMsgOrigin = d->mOrigMsg->headerByType(hdr.toLocal8Bit().constData())) { str = hrdMsgOrigin->asUnicodeString(); } plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OCCADDR"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OCCADDR"; i += strlen("OCCADDR"); if (d->mOrigMsg) { const QString str = d->mOrigMsg->cc()->asUnicodeString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OCCNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OCCNAME"; i += strlen("OCCNAME"); if (d->mOrigMsg) { const QString str = d->mOrigMsg->cc()->displayString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OCCFNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OCCFNAME"; i += strlen("OCCFNAME"); if (d->mOrigMsg) { const QString str = d->mOrigMsg->cc()->displayString(); plainBody.append(getFirstName(str)); const QString body = plainToHtml(getFirstName(str)); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OCCLNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OCCLNAME"; i += strlen("OCCLNAME"); if (d->mOrigMsg) { const QString str = d->mOrigMsg->cc()->displayString(); plainBody.append(getLastName(str)); const QString body = plainToHtml(getLastName(str)); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OTOADDR"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OTOADDR"; i += strlen("OTOADDR"); if (d->mOrigMsg) { const QString str = d->mOrigMsg->to()->asUnicodeString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OTONAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OTONAME"; i += strlen("OTONAME"); if (d->mOrigMsg) { const QString str = d->mOrigMsg->to()->displayString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OTOFNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OTOFNAME"; i += strlen("OTOFNAME"); if (d->mOrigMsg) { const QString str = d->mOrigMsg->to()->displayString(); plainBody.append(getFirstName(str)); const QString body = plainToHtml(getFirstName(str)); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OTOLNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OTOLNAME"; i += strlen("OTOLNAME"); if (d->mOrigMsg) { const QString str = d->mOrigMsg->to()->displayString(); plainBody.append(getLastName(str)); const QString body = plainToHtml(getLastName(str)); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OTOLIST"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OTOLIST"; i += strlen("OTOLIST"); if (d->mOrigMsg) { const QString str = d->mOrigMsg->to()->asUnicodeString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OTO"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OTO"; i += strlen("OTO"); if (d->mOrigMsg) { const QString str = d->mOrigMsg->to()->asUnicodeString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OFROMADDR"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OFROMADDR"; i += strlen("OFROMADDR"); if (d->mOrigMsg) { const QString str = d->mOrigMsg->from()->asUnicodeString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OFROMNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OFROMNAME"; i += strlen("OFROMNAME"); if (d->mOrigMsg) { const QString str = d->mOrigMsg->from()->displayString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OFROMFNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OFROMFNAME"; i += strlen("OFROMFNAME"); if (d->mOrigMsg) { const QString str = d->mOrigMsg->from()->displayString(); plainBody.append(getFirstName(str)); const QString body = plainToHtml(getFirstName(str)); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OFROMLNAME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OFROMLNAME"; i += strlen("OFROMLNAME"); if (d->mOrigMsg) { const QString str = d->mOrigMsg->from()->displayString(); plainBody.append(getLastName(str)); const QString body = plainToHtml(getLastName(str)); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OFULLSUBJECT")) || cmd.startsWith(QLatin1String("OFULLSUBJ"))) { if (cmd.startsWith(QLatin1String("OFULLSUBJECT"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OFULLSUBJECT"; i += strlen("OFULLSUBJECT"); } else { qCDebug(TEMPLATEPARSER_LOG) << "Command: OFULLSUBJ"; i += strlen("OFULLSUBJ"); } if (d->mOrigMsg) { const QString str = d->mOrigMsg->subject()->asUnicodeString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OMSGID"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OMSGID"; i += strlen("OMSGID"); if (d->mOrigMsg) { const QString str = d->mOrigMsg->messageID()->asUnicodeString(); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("DATEEN"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: DATEEN"; i += strlen("DATEEN"); const QDateTime date = QDateTime::currentDateTime(); QLocale locale(QLocale::C); const QString str = locale.toString(date.date(), QLocale::LongFormat); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("DATESHORT"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: DATESHORT"; i += strlen("DATESHORT"); const QDateTime date = QDateTime::currentDateTime(); const QString str = definedLocale.toString(date.date(), QLocale::ShortFormat); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("DATE"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: DATE"; i += strlen("DATE"); const QDateTime date = QDateTime::currentDateTime(); const QString str = definedLocale.toString(date.date(), QLocale::LongFormat); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("DOW"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: DOW"; i += strlen("DOW"); const QDateTime date = QDateTime::currentDateTime(); const QString str = definedLocale.dayName(date.date().dayOfWeek(), QLocale::LongFormat); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("TIMELONGEN"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: TIMELONGEN"; i += strlen("TIMELONGEN"); const QDateTime date = QDateTime::currentDateTime(); QLocale locale(QLocale::C); const QString str = locale.toString(date.time(), QLocale::LongFormat); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("TIMELONG"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: TIMELONG"; i += strlen("TIMELONG"); const QDateTime date = QDateTime::currentDateTime(); const QString str = definedLocale.toString(date.time(), QLocale::LongFormat); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("TIME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: TIME"; i += strlen("TIME"); const QDateTime date = QDateTime::currentDateTime(); const QString str = definedLocale.toString(date.time(), QLocale::ShortFormat); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } else if (cmd.startsWith(QLatin1String("ODATEEN"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: ODATEEN"; i += strlen("ODATEEN"); if (d->mOrigMsg) { const QDateTime date = d->mOrigMsg->date()->dateTime().toLocalTime(); const QString str = QLocale::c().toString(date.date(), QLocale::LongFormat); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("ODATESHORT"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: ODATESHORT"; i += strlen("ODATESHORT"); if (d->mOrigMsg) { const QDateTime date = d->mOrigMsg->date()->dateTime().toLocalTime(); const QString str = definedLocale.toString(date.date(), QLocale::ShortFormat); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("ODATE"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: ODATE"; i += strlen("ODATE"); if (d->mOrigMsg) { const QDateTime date = d->mOrigMsg->date()->dateTime().toLocalTime(); const QString str = definedLocale.toString(date.date(), QLocale::LongFormat); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("ODOW"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: ODOW"; i += strlen("ODOW"); if (d->mOrigMsg) { const QDateTime date = d->mOrigMsg->date()->dateTime().toLocalTime(); const QString str = definedLocale.dayName(date.date().dayOfWeek(), QLocale::LongFormat); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OTIMELONGEN"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OTIMELONGEN"; i += strlen("OTIMELONGEN"); if (d->mOrigMsg) { const QDateTime date = d->mOrigMsg->date()->dateTime().toLocalTime(); QLocale locale(QLocale::C); const QString str = locale.toString(date.time(), QLocale::LongFormat); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OTIMELONG"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OTIMELONG"; i += strlen("OTIMELONG"); if (d->mOrigMsg) { const QDateTime date = d->mOrigMsg->date()->dateTime().toLocalTime(); const QString str = definedLocale.toString(date.time(), QLocale::LongFormat); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("OTIME"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: OTIME"; i += strlen("OTIME"); if (d->mOrigMsg) { const QDateTime date = d->mOrigMsg->date()->dateTime().toLocalTime(); const QString str = definedLocale.toString(date.time(), QLocale::ShortFormat); plainBody.append(str); const QString body = plainToHtml(str); htmlBody.append(body); } } else if (cmd.startsWith(QLatin1String("BLANK"))) { // do nothing qCDebug(TEMPLATEPARSER_LOG) << "Command: BLANK"; i += strlen("BLANK"); } else if (cmd.startsWith(QLatin1String("NOP"))) { // do nothing qCDebug(TEMPLATEPARSER_LOG) << "Command: NOP"; i += strlen("NOP"); } else if (cmd.startsWith(QLatin1String("CLEAR"))) { // clear body buffer; not too useful yet qCDebug(TEMPLATEPARSER_LOG) << "Command: CLEAR"; i += strlen("CLEAR"); plainBody.clear(); htmlBody.clear(); KMime::Headers::Generic *header = new KMime::Headers::Generic("X-KMail-CursorPos"); header->fromUnicodeString(QString::number(0), "utf-8"); d->mMsg->setHeader(header); } else if (cmd.startsWith(QLatin1String("DEBUGOFF"))) { // turn off debug qCDebug(TEMPLATEPARSER_LOG) << "Command: DEBUGOFF"; i += strlen("DEBUGOFF"); d->mDebug = false; } else if (cmd.startsWith(QLatin1String("DEBUG"))) { // turn on debug qCDebug(TEMPLATEPARSER_LOG) << "Command: DEBUG"; i += strlen("DEBUG"); d->mDebug = true; } else if (cmd.startsWith(QLatin1String("CURSOR"))) { // turn on debug qCDebug(TEMPLATEPARSER_LOG) << "Command: CURSOR"; int oldI = i; i += strlen("CURSOR"); KMime::Headers::Generic *header = new KMime::Headers::Generic("X-KMail-CursorPos"); header->fromUnicodeString(QString::number(plainBody.length()), "utf-8"); /* if template is: * FOOBAR * %CURSOR * * Make sure there is an empty line for the cursor otherwise it will be placed at the end of FOOBAR */ if (oldI > 0 && tmpl[ oldI - 1 ] == QLatin1Char('\n') && i == tmpl_len - 1) { plainBody.append(QLatin1Char('\n')); } d->mMsg->setHeader(header); d->mForceCursorPosition = true; //FIXME HTML part for header remaining } else if (cmd.startsWith(QLatin1String("SIGNATURE"))) { qCDebug(TEMPLATEPARSER_LOG) << "Command: SIGNATURE"; i += strlen("SIGNATURE"); plainBody.append(getPlainSignature()); htmlBody.append(getHtmlSignature()); } else { // wrong command, do nothing plainBody.append(c); htmlBody.append(c); } } else if (dnl && (c == QLatin1Char('\n') || c == QLatin1Char('\r'))) { // skip if ((tmpl.size() > i + 1) && ((c == QLatin1Char('\n') && tmpl[i + 1] == QLatin1Char('\r')) || (c == QLatin1Char('\r') && tmpl[i + 1] == QLatin1Char('\n')))) { // skip one more i += 1; } dnl = false; } else { plainBody.append(c); if (c == QLatin1Char('\n') || c == QLatin1Char('\r')) { htmlBody.append(QLatin1String("
")); htmlBody.append(c); if (tmpl.size() > i + 1 && ((c == QLatin1Char('\n') && tmpl[i + 1] == QLatin1Char('\r')) || (c == QLatin1Char('\r') && tmpl[i + 1] == QLatin1Char('\n')))) { htmlBody.append(tmpl[i + 1]); plainBody.append(tmpl[i + 1]); i += 1; } } else { htmlBody.append(c); } } } // Clear the HTML body if FORCEDPLAIN has set ReplyAsPlain, OR if, // there is no use of FORCED command but a configure setting has ReplyUsingHtml disabled, // OR the original mail has no HTML part. const KMime::Content *content = d->mOrigMsg->mainBodyPart("text/html"); if (d->mQuotes == ReplyAsPlain || (d->mQuotes != ReplyAsHtml && !TemplateParserSettings::self()->replyUsingHtml()) || (!content || !content->hasContent())) { htmlBody.clear(); } else { makeValidHtml(htmlBody); } if (d->mMode == NewMessage && plainBody.isEmpty() && !d->mExtractHtmlInfoResult.mPlainText.isEmpty()) { plainBody = d->mExtractHtmlInfoResult.mPlainText; } /* if (d->mMode == NewMessage && htmlBody.isEmpty() && !d->mExtractHtmlInfoResult.mHtmlElement.isEmpty()) { htmlBody = d->mExtractHtmlInfoResult.mHtmlElement; } */ addProcessedBodyToMessage(plainBody, htmlBody); Q_EMIT parsingDone(d->mForceCursorPosition); deleteLater(); } QString TemplateParserJob::getPlainSignature() const { const KIdentityManagement::Identity &identity = d->m_identityManager->identityForUoid(d->mIdentity); if (identity.isNull()) { return QString(); } KIdentityManagement::Signature signature = const_cast(identity).signature(); if (signature.type() == KIdentityManagement::Signature::Inlined && signature.isInlinedHtml()) { return signature.toPlainText(); } else { return signature.rawText(); } } // TODO If %SIGNATURE command is on, then override it with signature from // "KMail configure->General->identity->signature". // There should be no two signatures. QString TemplateParserJob::getHtmlSignature() const { const KIdentityManagement::Identity &identity = d->m_identityManager->identityForUoid(d->mIdentity); if (identity.isNull()) { return QString(); } KIdentityManagement::Signature signature = const_cast(identity).signature(); if (!signature.isInlinedHtml()) { signature = signature.rawText().toHtmlEscaped(); return signature.rawText().replace(QLatin1Char('\n'), QStringLiteral("
")); } return signature.rawText(); } void TemplateParserJob::addProcessedBodyToMessage(const QString &plainBody, const QString &htmlBody) const { MessageCore::ImageCollector ic; ic.collectImagesFrom(d->mOrigMsg.data()); // Now, delete the old content and set the new content, which // is either only the new text or the new text with some attachments. const auto parts = d->mMsg->contents(); for (KMime::Content *content : parts) { d->mMsg->removeContent(content, true /*delete*/); } // Set To and CC from the template if (!d->mTo.isEmpty()) { d->mMsg->to()->fromUnicodeString(d->mMsg->to()->asUnicodeString() + QLatin1Char(',') + d->mTo, "utf-8"); } if (!d->mCC.isEmpty()) { d->mMsg->cc()->fromUnicodeString(d->mMsg->cc()->asUnicodeString() + QLatin1Char(',') + d->mCC, "utf-8"); } d->mMsg->contentType()->clear(); // to get rid of old boundary //const QByteArray boundary = KMime::multiPartBoundary(); KMime::Content *const mainTextPart = htmlBody.isEmpty() ? createPlainPartContent(plainBody) : createMultipartAlternativeContent(plainBody, htmlBody); mainTextPart->assemble(); KMime::Content *textPart = mainTextPart; if (!ic.images().empty()) { textPart = createMultipartRelated(ic, mainTextPart); textPart->assemble(); } // If we have some attachments, create a multipart/mixed mail and // add the normal body as well as the attachments KMime::Content *mainPart = textPart; if (d->mMode == Forward) { auto attachments = d->mOrigMsg->attachments(); attachments += d->mOtp->nodeHelper()->attachmentsOfExtraContents(); if (!attachments.isEmpty()) { mainPart = createMultipartMixed(attachments, textPart); mainPart->assemble(); } } d->mMsg->setBody(mainPart->encodedBody()); d->mMsg->setHeader(mainPart->contentType()); d->mMsg->setHeader(mainPart->contentTransferEncoding()); d->mMsg->assemble(); d->mMsg->parse(); } KMime::Content *TemplateParserJob::createMultipartMixed(const QVector &attachments, KMime::Content *textPart) const { KMime::Content *mixedPart = new KMime::Content(d->mMsg.data()); const QByteArray boundary = KMime::multiPartBoundary(); mixedPart->contentType()->setMimeType("multipart/mixed"); mixedPart->contentType()->setBoundary(boundary); mixedPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); mixedPart->addContent(textPart); int attachmentNumber = 1; for (KMime::Content *attachment : attachments) { mixedPart->addContent(attachment); // If the content type has no name or filename parameter, add one, since otherwise the name // would be empty in the attachment view of the composer, which looks confusing if (attachment->contentType(false)) { if (!attachment->contentType()->hasParameter(QStringLiteral("name")) && !attachment->contentType()->hasParameter(QStringLiteral("filename"))) { attachment->contentType()->setParameter( QStringLiteral("name"), i18nc("@item:intext", "Attachment %1", attachmentNumber)); } } ++attachmentNumber; } return mixedPart; } KMime::Content *TemplateParserJob::createMultipartRelated(const MessageCore::ImageCollector &ic, KMime::Content *mainTextPart) const { KMime::Content *relatedPart = new KMime::Content(d->mMsg.data()); const QByteArray boundary = KMime::multiPartBoundary(); relatedPart->contentType()->setMimeType("multipart/related"); relatedPart->contentType()->setBoundary(boundary); relatedPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); relatedPart->addContent(mainTextPart); for (KMime::Content *image : ic.images()) { qCWarning(TEMPLATEPARSER_LOG) << "Adding" << image->contentID() << "as an embedded image"; relatedPart->addContent(image); } return relatedPart; } KMime::Content *TemplateParserJob::createPlainPartContent(const QString &plainBody) const { KMime::Content *textPart = new KMime::Content(d->mMsg.data()); textPart->contentType()->setMimeType("text/plain"); QTextCodec *charset = selectCharset(d->mCharsets, plainBody); textPart->contentType()->setCharset(charset->name()); textPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit); textPart->fromUnicodeString(plainBody); return textPart; } KMime::Content *TemplateParserJob::createMultipartAlternativeContent(const QString &plainBody, const QString &htmlBody) const { KMime::Content *multipartAlternative = new KMime::Content(d->mMsg.data()); multipartAlternative->contentType()->setMimeType("multipart/alternative"); const QByteArray boundary = KMime::multiPartBoundary(); multipartAlternative->contentType()->setBoundary(boundary); KMime::Content *textPart = createPlainPartContent(plainBody); multipartAlternative->addContent(textPart); KMime::Content *htmlPart = new KMime::Content(d->mMsg.data()); htmlPart->contentType()->setMimeType("text/html"); QTextCodec *charset = selectCharset(d->mCharsets, htmlBody); htmlPart->contentType()->setCharset(charset->name()); htmlPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit); htmlPart->fromUnicodeString(htmlBody); multipartAlternative->addContent(htmlPart); return multipartAlternative; } QString TemplateParserJob::findCustomTemplate(const QString &tmplName) { CTemplates t(tmplName); d->mTo = t.to(); d->mCC = t.cC(); const QString content = t.content(); if (!content.isEmpty()) { return content; } else { return findTemplate(); } } QString TemplateParserJob::findTemplate() { // qCDebug(TEMPLATEPARSER_LOG) << "Trying to find template for mode" << mode; QString tmpl; qCDebug(TEMPLATEPARSER_LOG) << "Folder identify found:" << d->mFolder; if (d->mFolder >= 0) { // only if a folder was found QString fid = QString::number(d->mFolder); Templates fconf(fid); if (fconf.useCustomTemplates()) { // does folder use custom templates? switch (d->mMode) { case NewMessage: tmpl = fconf.templateNewMessage(); break; case Reply: tmpl = fconf.templateReply(); break; case ReplyAll: tmpl = fconf.templateReplyAll(); break; case Forward: tmpl = fconf.templateForward(); break; default: qCDebug(TEMPLATEPARSER_LOG) << "Unknown message mode:" << d->mMode; return QString(); } d->mQuoteString = fconf.quoteString(); if (!tmpl.isEmpty()) { return tmpl; // use folder-specific template } } } if (!d->mIdentity) { // find identity message belongs to d->mIdentity = identityUoid(d->mMsg); if (!d->mIdentity && d->mOrigMsg) { d->mIdentity = identityUoid(d->mOrigMsg); } d->mIdentity = d->m_identityManager->identityForUoidOrDefault(d->mIdentity).uoid(); if (!d->mIdentity) { qCDebug(TEMPLATEPARSER_LOG) << "Oops! No identity for message"; } } qCDebug(TEMPLATEPARSER_LOG) << "Identity found:" << d->mIdentity; QString iid; if (d->mIdentity) { iid = TemplatesConfiguration::configIdString(d->mIdentity); // templates ID for that identity } else { iid = QStringLiteral("IDENTITY_NO_IDENTITY"); // templates ID for no identity } Templates iconf(iid); if (iconf.useCustomTemplates()) { // does identity use custom templates? switch (d->mMode) { case NewMessage: tmpl = iconf.templateNewMessage(); break; case Reply: tmpl = iconf.templateReply(); break; case ReplyAll: tmpl = iconf.templateReplyAll(); break; case Forward: tmpl = iconf.templateForward(); break; default: qCDebug(TEMPLATEPARSER_LOG) << "Unknown message mode:" << d->mMode; return QString(); } d->mQuoteString = iconf.quoteString(); if (!tmpl.isEmpty()) { return tmpl; // use identity-specific template } } switch (d->mMode) { // use the global template case NewMessage: tmpl = TemplateParserSettings::self()->templateNewMessage(); break; case Reply: tmpl = TemplateParserSettings::self()->templateReply(); break; case ReplyAll: tmpl = TemplateParserSettings::self()->templateReplyAll(); break; case Forward: tmpl = TemplateParserSettings::self()->templateForward(); break; default: qCDebug(TEMPLATEPARSER_LOG) << "Unknown message mode:" << d->mMode; return QString(); } d->mQuoteString = TemplateParserSettings::self()->quoteString(); return tmpl; } QString TemplateParserJob::pipe(const QString &cmd, const QString &buf) { KProcess process; bool success; process.setOutputChannelMode(KProcess::SeparateChannels); process.setShellCommand(cmd); process.start(); if (process.waitForStarted(pipeTimeout())) { bool finished = false; if (!buf.isEmpty()) { process.write(buf.toLatin1()); } if (buf.isEmpty() || process.waitForBytesWritten(pipeTimeout())) { if (!buf.isEmpty()) { process.closeWriteChannel(); } if (process.waitForFinished(pipeTimeout())) { success = (process.exitStatus() == QProcess::NormalExit); finished = true; } else { finished = false; success = false; } } else { success = false; finished = false; } // The process has started, but did not finish in time. Kill it. if (!finished) { process.kill(); } } else { success = false; } if (!success && d->mDebug) { KMessageBox::error( nullptr, xi18nc("@info", "Pipe command %1 failed.", cmd)); } if (success) { return QString::fromLatin1(process.readAllStandardOutput()); } else { return QString(); } } void TemplateParserJob::setWordWrap(bool wrap, int wrapColWidth) { d->mWrap = wrap; d->mColWrap = wrapColWidth; } QString TemplateParserJob::plainMessageText(bool aStripSignature, AllowSelection isSelectionAllowed) const { if (!d->mSelection.isEmpty() && (isSelectionAllowed == SelectionAllowed)) { return d->mSelection; } if (!d->mOrigMsg) { return QString(); } - QString result = d->mOtp->plainTextContent(); + const auto mp = toplevelTextNode(d->mOtp->parsedPart()); + QString result = mp->plaintextContent(); if (result.isEmpty()) { result = d->mExtractHtmlInfoResult.mPlainText; } if (aStripSignature) { result = MessageCore::StringUtil::stripSignature(result); } return result; } QString TemplateParserJob::htmlMessageText(bool aStripSignature, AllowSelection isSelectionAllowed) { if (!d->mSelection.isEmpty() && (isSelectionAllowed == SelectionAllowed)) { //TODO implement d->mSelection for HTML return d->mSelection; } d->mHeadElement = d->mExtractHtmlInfoResult.mHeaderElement; const QString bodyElement = d->mExtractHtmlInfoResult.mBodyElement; if (!bodyElement.isEmpty()) { if (aStripSignature) { //FIXME strip signature works partially for HTML mails return MessageCore::StringUtil::stripSignature(bodyElement); } return bodyElement; } if (aStripSignature) { //FIXME strip signature works partially for HTML mails return MessageCore::StringUtil::stripSignature(d->mExtractHtmlInfoResult.mHtmlElement); } return d->mExtractHtmlInfoResult.mHtmlElement; } QString TemplateParserJob::quotedPlainText(const QString &selection) const { QString content = selection; // Remove blank lines at the beginning: const int firstNonWS = content.indexOf(QRegExp(QLatin1String("\\S"))); const int lineStart = content.lastIndexOf(QLatin1Char('\n'), firstNonWS); if (lineStart >= 0) { content.remove(0, lineStart); } const QString indentStr = MessageCore::StringUtil::formatQuotePrefix(d->mQuoteString, d->mOrigMsg->from()->displayString()); if (TemplateParserSettings::self()->smartQuote() && d->mWrap) { content = MessageCore::StringUtil::smartQuote(content, d->mColWrap - indentStr.length()); } content.replace(QLatin1Char('\n'), QLatin1Char('\n') + indentStr); content.prepend(indentStr); content += QLatin1Char('\n'); return content; } QString TemplateParserJob::quotedHtmlText(const QString &selection) const { QString content = selection; //TODO 1) look for all the variations of
and remove the blank lines //2) implement vertical bar for quoted HTML mail. //3) After vertical bar is implemented, If a user wants to edit quoted message, // then the
tags below should open and close as when required. //Add blockquote tag, so that quoted message can be differentiated from normal message content = QLatin1String("
") + content + QLatin1String("
"); return content; } uint TemplateParserJob::identityUoid(const KMime::Message::Ptr &msg) const { QString idString; if (auto hrd = msg->headerByType("X-KMail-Identity")) { idString = hrd->asUnicodeString().trimmed(); } bool ok = false; unsigned int id = idString.toUInt(&ok); if (!ok || id == 0) { id = d->m_identityManager->identityForAddress( msg->to()->asUnicodeString() + QLatin1String(", ") + msg->cc()->asUnicodeString()).uoid(); } return id; } bool TemplateParserJob::isHtmlSignature() const { const KIdentityManagement::Identity &identity = d->m_identityManager->identityForUoid(d->mIdentity); if (identity.isNull()) { return false; } const KIdentityManagement::Signature signature = const_cast(identity).signature(); return signature.isInlinedHtml(); } QString TemplateParserJob::plainToHtml(const QString &body) { QString str = body; str = str.toHtmlEscaped(); str.replace(QLatin1Char('\n'), QStringLiteral("
\n")); return str; } //TODO implement this function using a DOM tree parser void TemplateParserJob::makeValidHtml(QString &body) { QRegExp regEx; regEx.setMinimal(true); regEx.setPattern(QStringLiteral("")); if (!body.isEmpty() && !body.contains(regEx)) { regEx.setPattern(QStringLiteral("")); if (!body.contains(regEx)) { body = QLatin1String("") + body + QLatin1String("
"); } regEx.setPattern(QStringLiteral("")); if (!body.contains(regEx)) { body = QLatin1String("") + d->mHeadElement + QLatin1String("") + body; } body = QLatin1String("") + body + QLatin1String(""); } }