diff --git a/framework/src/domain/mime/mailcrypto.cpp b/framework/src/domain/mime/mailcrypto.cpp index e429a212..5b127b3b 100644 --- a/framework/src/domain/mime/mailcrypto.cpp +++ b/framework/src/domain/mime/mailcrypto.cpp @@ -1,319 +1,200 @@ /* Copyright (c) 2009 Constantin Berzan Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Copyright (c) 2010 Leo Franchi Copyright (c) 2017 Christian Mollekopf 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 "mailcrypto.h" #include "framework/src/errors.h" #include "crypto.h" #include #include #include using namespace MailCrypto; using namespace Crypto; static QByteArray canonicalizeContent(KMime::Content *content) { // if (d->format & Kleo::InlineOpenPGPFormat) { // return d->content->body(); // } else if (!(d->format & Kleo::SMIMEOpaqueFormat)) { // replace "From " and "--" at the beginning of lines // with encoded versions according to RfC 3156, 3 // Note: If any line begins with the string "From ", it is strongly // suggested that either the Quoted-Printable or Base64 MIME encoding // be applied. const auto encoding = content->contentTransferEncoding()->encoding(); if ((encoding == KMime::Headers::CEquPr || encoding == KMime::Headers::CE7Bit) && !content->contentType(false)) { QByteArray body = content->encodedBody(); bool changed = false; QList search; QList replacements; search << "From " << "from " << "-"; replacements << "From=20" << "from=20" << "=2D"; if (content->contentTransferEncoding()->encoding() == KMime::Headers::CE7Bit) { for (int i = 0; i < search.size(); ++i) { const auto pos = body.indexOf(search[i]); if (pos == 0 || (pos > 0 && body.at(pos - 1) == '\n')) { changed = true; break; } } if (changed) { content->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr); content->assemble(); body = content->encodedBody(); } } for (int i = 0; i < search.size(); ++i) { const auto pos = body.indexOf(search[i]); if (pos == 0 || (pos > 0 && body.at(pos - 1) == '\n')) { changed = true; body.replace(pos, search[i].size(), replacements[i]); } } if (changed) { qDebug() << "Content changed"; content->setBody(body); content->contentTransferEncoding()->setDecoded(false); } } return KMime::LFtoCRLF(content->encodedContent()); // } else { // SMimeOpaque doesn't need LFtoCRLF, else it gets munged // return content->encodedContent(); // } } -/** - * Create an Email with `msg` as a body and `key` as an attachment. - * - * Will create the given structure: - * - * + `multipart/mixed` - * - the given `msg` - * - `application/pgp-keys` (the given `key` as attachment) - * - * Used by the `createSignedEmail` and `createEncryptedEmail` functions. - */ -Expected> -appendPublicKey(std::unique_ptr msg, const Key &key) -{ - const auto publicKeyExportResult = exportPublicKey(key); - - if (!publicKeyExportResult) { - // "Could not export public key" - return makeUnexpected(Error{publicKeyExportResult.error()}); - } - - const auto publicKeyData = publicKeyExportResult.value(); - - auto result = std::unique_ptr(new KMime::Content); - result->contentType()->setMimeType("multipart/mixed"); - result->contentType()->setBoundary(KMime::multiPartBoundary()); - - KMime::Content *keyAttachment = new KMime::Content; - { - keyAttachment->contentType()->setMimeType("application/pgp-keys"); - keyAttachment->contentDisposition()->setDisposition(KMime::Headers::CDattachment); - keyAttachment->contentDisposition()->setFilename(QString("0x") + key.shortKeyId + ".asc"); - keyAttachment->setBody(publicKeyData); - } - - msg->assemble(); - - result->addContent(msg.release()); - result->addContent(keyAttachment); - - result->assemble(); - - return result; -} - /** * Create a message part like this (according to RFC 3156 Section 4): * * - multipart/encrypted * - application/pgp-encrypted (version information) * - application/octet-stream (given encrypted data) * - * Should not be used directly since the public key should be attached, hence - * the `createEncryptedEmail` function. - * * The encrypted data can be generated by the `encrypt` or `signAndEncrypt` functions. */ std::unique_ptr createEncryptedPart(QByteArray encryptedData) { auto result = std::unique_ptr(new KMime::Content); result->contentType()->setMimeType("multipart/encrypted"); result->contentType()->setBoundary(KMime::multiPartBoundary()); result->contentType()->setParameter("protocol", "application/pgp-encrypted"); KMime::Content *controlInformation = new KMime::Content; { controlInformation->contentType()->setMimeType("application/pgp-encrypted"); controlInformation->contentDescription()->from7BitString("PGP/MIME version identification"); controlInformation->setBody("Version: 1"); result->addContent(controlInformation); } KMime::Content *encryptedPartPart = new KMime::Content; { const QString filename = "msg.asc"; encryptedPartPart->contentType()->setMimeType("application/octet-stream"); encryptedPartPart->contentType()->setName(filename, "utf-8"); encryptedPartPart->contentDescription()->from7BitString("OpenPGP encrypted message"); encryptedPartPart->contentDisposition()->setDisposition(KMime::Headers::CDinline); encryptedPartPart->contentDisposition()->setFilename(filename); encryptedPartPart->setBody(encryptedData); result->addContent(encryptedPartPart); } return result; } -/** - * Create an encrypted (optionally signed) email with a public key attached to it. - * - * Will create a message like this: - * - * + `multipart/mixed` - * - `multipart/encrypted` - * + `application/pgp-encrypted - * + `application/octet-stream` (a generated encrypted version of the original message) - * - `application/pgp-keys` (the public key as attachment, which is the first of the - * `signingKeys`) - */ -Expected> -createEncryptedEmail(KMime::Content *content, const std::vector &encryptionKeys, - const Key &attachedKey, const std::vector &signingKeys = {}) -{ - auto encryptionResult = signAndEncrypt(canonicalizeContent(content), encryptionKeys, signingKeys); - - if (!encryptionResult) { - return makeUnexpected(Error{encryptionResult.error()}); - } - - auto encryptedPart = createEncryptedPart(encryptionResult.value()); - - auto publicKeyAppendResult = appendPublicKey(std::move(encryptedPart), attachedKey); - - if(publicKeyAppendResult) { - publicKeyAppendResult.value()->assemble(); - } - - return publicKeyAppendResult; -} - /** * Create a message part like this (according to RFC 3156 Section 5): * * + `multipart/signed` * - whatever the given original `message` is (should be canonicalized) * - `application/octet-stream` (the given `signature`) * - * Should not be used directly since the public key should be attached, hence - * the `createSignedEmail` function. - * * The signature can be generated by the `sign` function. */ -std::unique_ptr createSignedPart( - std::unique_ptr message, const QByteArray &signature, const QString &micAlg) +std::unique_ptr createSignedPart(std::unique_ptr message, const QByteArray &signature, const QString &micAlg) { auto result = std::unique_ptr(new KMime::Content); result->contentType()->setMimeType("multipart/signed"); result->contentType()->setBoundary(KMime::multiPartBoundary()); result->contentType()->setParameter("micalg", micAlg); result->contentType()->setParameter("protocol", "application/pgp-signature"); result->addContent(message.release()); KMime::Content *signedPartPart = new KMime::Content; - { - signedPartPart->contentType()->setMimeType("application/pgp-signature"); - signedPartPart->contentType()->setName("signature.asc", "utf-8"); - - signedPartPart->contentDescription()->from7BitString( - "This is a digitally signed message part"); - - signedPartPart->setBody(signature); - - result->addContent(signedPartPart); - } + signedPartPart->contentType()->setMimeType("application/pgp-signature"); + signedPartPart->contentType()->setName("signature.asc", "utf-8"); + signedPartPart->contentDisposition(true)->setDisposition(KMime::Headers::CDattachment); + signedPartPart->contentDisposition(true)->setFilename("signature.asc"); + signedPartPart->contentDescription()->from7BitString("OpenPGP digital signature"); + signedPartPart->setBody(signature); + result->addContent(signedPartPart); return result; } -/** - * Create a signed email with a public key attached to it. - * - * Will create a message like this: - * - * + `multipart/mixed` - * - `multipart/signed` - * + whatever the given original `content` is (should not be canonalized) - * + `application/octet-stream` (a generated signature of the original message) - * - `application/pgp-keys` (the public key as attachment, which is the first of the - * `signingKeys`) - */ -Expected> -createSignedEmail(std::unique_ptr content, - const std::vector &signingKeys, const Key &attachedKey) -{ - Q_ASSERT(!signingKeys.empty()); - - auto contentToSign = canonicalizeContent(content.get()); - - auto signingResult = sign(contentToSign, signingKeys); - - if (!signingResult) { - return makeUnexpected(Error{signingResult.error()}); - } - - QByteArray signingData; - QString micAlg; - std::tie(signingData, micAlg) = signingResult.value(); - - auto signedPart = createSignedPart(std::move(content), signingData, micAlg); - - auto publicKeyAppendResult = appendPublicKey(std::move(signedPart), attachedKey); - - if (publicKeyAppendResult) { - publicKeyAppendResult.value()->assemble(); - } - - return publicKeyAppendResult; -} - Expected> -MailCrypto::processCrypto(std::unique_ptr content, const std::vector &signingKeys, - const std::vector &encryptionKeys, const Key &attachedKey) +MailCrypto::processCrypto(std::unique_ptr content, const std::vector &signingKeys, const std::vector &encryptionKeys) { if (!encryptionKeys.empty()) { - return createEncryptedEmail(content.release(), encryptionKeys, attachedKey, signingKeys); + auto encryptionResult = signAndEncrypt(canonicalizeContent(content.get()), encryptionKeys, signingKeys); + if (!encryptionResult) { + return makeUnexpected(Error{encryptionResult.error()}); + } + return createEncryptedPart(encryptionResult.value()); } else if (!signingKeys.empty()) { - return createSignedEmail(std::move(content), signingKeys, signingKeys[0]); + auto signingResult = sign(canonicalizeContent(content.get()), signingKeys); + if (!signingResult) { + return makeUnexpected(Error{signingResult.error()}); + } + QByteArray signingData; + QString micAlg; + std::tie(signingData, micAlg) = signingResult.value(); + return createSignedPart(std::move(content), signingData, micAlg); } else { qWarning() << "Processing cryptography, but neither signing nor encrypting"; + Q_ASSERT(false); return content; } } diff --git a/framework/src/domain/mime/mailcrypto.h b/framework/src/domain/mime/mailcrypto.h index 4e2036bd..fd4e6248 100644 --- a/framework/src/domain/mime/mailcrypto.h +++ b/framework/src/domain/mime/mailcrypto.h @@ -1,34 +1,34 @@ /* Copyright (c) 2016 Christian Mollekopf 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. */ #pragma once #include #include #include #include namespace MailCrypto { -Expected> processCrypto(std::unique_ptr content, const std::vector &signingKeys, const std::vector &encryptionKeys, const Crypto::Key &attachedKey); +Expected> processCrypto(std::unique_ptr content, const std::vector &signingKeys, const std::vector &encryptionKeys); }; diff --git a/framework/src/domain/mime/mailtemplates.cpp b/framework/src/domain/mime/mailtemplates.cpp index 45f3e196..237e45c9 100644 --- a/framework/src/domain/mime/mailtemplates.cpp +++ b/framework/src/domain/mime/mailtemplates.cpp @@ -1,1072 +1,1103 @@ /* Copyright (c) 2009 Constantin Berzan Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Copyright (c) 2010 Leo Franchi Copyright (c) 2017 Christian Mollekopf 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 "mailtemplates.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mailcrypto.h" namespace KMime { namespace Types { static bool operator==(const KMime::Types::AddrSpec &left, const KMime::Types::AddrSpec &right) { return (left.asString() == right.asString()); } static bool operator==(const KMime::Types::Mailbox &left, const KMime::Types::Mailbox &right) { return (left.addrSpec().asString() == right.addrSpec().asString()); } } Message* contentToMessage(Content* content) { content->assemble(); const auto encoded = content->encodedContent(); auto message = new Message(); message->setContent(encoded); message->parse(); return message; } } static KMime::Types::Mailbox::List stripMyAddressesFromAddressList(const KMime::Types::Mailbox::List &list, const KMime::Types::AddrSpecList me) { KMime::Types::Mailbox::List addresses(list); for (KMime::Types::Mailbox::List::Iterator it = addresses.begin(); it != addresses.end();) { if (me.contains(it->addrSpec())) { it = addresses.erase(it); } else { ++it; } } return addresses; } static QString toPlainText(const QString &s) { QTextDocument doc; doc.setHtml(s); return doc.toPlainText(); } QString replacePrefixes(const QString &str, const QStringList &prefixRegExps, const QString &newPrefix) { // construct a big regexp that // 1. is anchored to the beginning of str (sans whitespace) // 2. matches at least one of the part regexps in prefixRegExps const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:"))); QRegExp rx(bigRegExp, Qt::CaseInsensitive); if (!rx.isValid()) { qWarning() << "bigRegExp = \"" << bigRegExp << "\"\n" << "prefix regexp is invalid!"; qWarning() << "Error: " << rx.errorString() << rx; Q_ASSERT(false); return str; } QString tmp = str; //We expect a match at the beginning of the string if (rx.indexIn(tmp) == 0) { return tmp.replace(0, rx.matchedLength(), newPrefix + QLatin1String(" ")); } //No match, we just prefix the newPrefix return newPrefix + " " + str; } const QStringList getForwardPrefixes() { //See https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations QStringList list; //We want to be able to potentially reply to a variety of languages, so only translating is not enough list << QObject::tr("fwd"); list << "fwd"; list << "fw"; list << "wg"; list << "vs"; list << "tr"; list << "rv"; list << "enc"; return list; } static QString forwardSubject(const QString &s) { //The standandard prefix const auto localPrefix = "FW:"; QStringList forwardPrefixes; for (const auto &prefix : getForwardPrefixes()) { forwardPrefixes << prefix + "\\s*:"; } return replacePrefixes(s, forwardPrefixes, localPrefix); } static QStringList getReplyPrefixes() { //See https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations QStringList list; //We want to be able to potentially reply to a variety of languages, so only translating is not enough list << QObject::tr("re"); list << "re"; list << "aw"; list << "sv"; list << "antw"; list << "ref"; return list; } static QString replySubject(const QString &s) { //The standandard prefix (latin for "in re", in matter of) const auto localPrefix = "RE:"; QStringList replyPrefixes; for (const auto &prefix : getReplyPrefixes()) { replyPrefixes << prefix + "\\s*:"; replyPrefixes << prefix + "\\[.+\\]:"; replyPrefixes << prefix + "\\d+:"; } return replacePrefixes(s, replyPrefixes, localPrefix); } QByteArray getRefStr(const KMime::Message::Ptr &msg) { QByteArray firstRef, lastRef, refStr, retRefStr; int i, j; if (auto hdr = msg->references(false)) { refStr = hdr->as7BitString(false).trimmed(); } if (refStr.isEmpty()) { return msg->messageID()->as7BitString(false); } i = refStr.indexOf('<'); j = refStr.indexOf('>'); firstRef = refStr.mid(i, j - i + 1); if (!firstRef.isEmpty()) { retRefStr = firstRef + ' '; } i = refStr.lastIndexOf('<'); j = refStr.lastIndexOf('>'); lastRef = refStr.mid(i, j - i + 1); if (!lastRef.isEmpty() && lastRef != firstRef) { retRefStr += lastRef + ' '; } retRefStr += msg->messageID()->as7BitString(false); return retRefStr; } KMime::Content *createPlainPartContent(const QString &plainBody, KMime::Content *parent = nullptr) { KMime::Content *textPart = new KMime::Content(parent); textPart->contentType()->setMimeType("text/plain"); //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text // QTextCodec *charset = selectCharset(m_charsets, plainBody); // textPart->contentType()->setCharset(charset->name()); textPart->contentType()->setCharset("utf-8"); textPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit); textPart->fromUnicodeString(plainBody); return textPart; } KMime::Content *createMultipartAlternativeContent(const QString &plainBody, const QString &htmlBody, KMime::Message *parent = nullptr) { KMime::Content *multipartAlternative = new KMime::Content(parent); multipartAlternative->contentType()->setMimeType("multipart/alternative"); multipartAlternative->contentType()->setBoundary(KMime::multiPartBoundary()); KMime::Content *textPart = createPlainPartContent(plainBody, multipartAlternative); multipartAlternative->addContent(textPart); KMime::Content *htmlPart = new KMime::Content(multipartAlternative); htmlPart->contentType()->setMimeType("text/html"); //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text // QTextCodec *charset = selectCharset(m_charsets, htmlBody); // htmlPart->contentType()->setCharset(charset->name()); htmlPart->contentType()->setCharset("utf-8"); htmlPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit); htmlPart->fromUnicodeString(htmlBody); multipartAlternative->addContent(htmlPart); return multipartAlternative; } KMime::Content *createMultipartMixedContent(QVector contents) { KMime::Content *multiPartMixed = new KMime::Content(); multiPartMixed->contentType()->setMimeType("multipart/mixed"); multiPartMixed->contentType()->setBoundary(KMime::multiPartBoundary()); for (const auto &content : contents) { multiPartMixed->addContent(content); } return multiPartMixed; } QString plainToHtml(const QString &body) { QString str = body; str = str.toHtmlEscaped(); str.replace(QStringLiteral("\n"), QStringLiteral("
\n")); return str; } //TODO implement this function using a DOM tree parser void makeValidHtml(QString &body, const QString &headElement) { 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("") + headElement + QLatin1String("") + body; } body = QLatin1String("") + body + QLatin1String(""); } } //FIXME strip signature works partially for HTML mails QString stripSignature(const QString &msg) { // Following RFC 3676, only > before -- // I prefer to not delete a SB instead of delete good mail content. const QRegExp sbDelimiterSearch = QRegExp(QLatin1String("(^|\n)[> ]*-- \n")); // The regular expression to look for prefix change const QRegExp commonReplySearch = QRegExp(QLatin1String("^[ ]*>")); QString res = msg; int posDeletingStart = 1; // to start looking at 0 // While there are SB delimiters (start looking just before the deleted SB) while ((posDeletingStart = res.indexOf(sbDelimiterSearch, posDeletingStart - 1)) >= 0) { QString prefix; // the current prefix QString line; // the line to check if is part of the SB int posNewLine = -1; // Look for the SB beginning int posSignatureBlock = res.indexOf(QLatin1Char('-'), posDeletingStart); // The prefix before "-- "$ if (res.at(posDeletingStart) == QLatin1Char('\n')) { ++posDeletingStart; } prefix = res.mid(posDeletingStart, posSignatureBlock - posDeletingStart); posNewLine = res.indexOf(QLatin1Char('\n'), posSignatureBlock) + 1; // now go to the end of the SB while (posNewLine < res.size() && posNewLine > 0) { // handle the undefined case for mid ( x , -n ) where n>1 int nextPosNewLine = res.indexOf(QLatin1Char('\n'), posNewLine); if (nextPosNewLine < 0) { nextPosNewLine = posNewLine - 1; } line = res.mid(posNewLine, nextPosNewLine - posNewLine); // check when the SB ends: // * does not starts with prefix or // * starts with prefix+(any substring of prefix) if ((prefix.isEmpty() && line.indexOf(commonReplySearch) < 0) || (!prefix.isEmpty() && line.startsWith(prefix) && line.mid(prefix.size()).indexOf(commonReplySearch) < 0)) { posNewLine = res.indexOf(QLatin1Char('\n'), posNewLine) + 1; } else { break; // end of the SB } } // remove the SB or truncate when is the last SB if (posNewLine > 0) { res.remove(posDeletingStart, posNewLine - posDeletingStart); } else { res.truncate(posDeletingStart); } } return res; } void setupPage(QWebEnginePage *page) { page->profile()->setHttpCacheType(QWebEngineProfile::MemoryHttpCache); page->profile()->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies); page->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, false); page->settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false); page->settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false); page->settings()->setAttribute(QWebEngineSettings::JavascriptCanAccessClipboard, false); page->settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, false); page->settings()->setAttribute(QWebEngineSettings::XSSAuditingEnabled, false); page->settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); page->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, false); page->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, false); page->settings()->setAttribute(QWebEngineSettings::HyperlinkAuditingEnabled, false); page->settings()->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, false); page->settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, false); page->settings()->setAttribute(QWebEngineSettings::WebGLEnabled, false); page->settings()->setAttribute(QWebEngineSettings::AutoLoadIconsForPage, false); page->settings()->setAttribute(QWebEngineSettings::Accelerated2dCanvasEnabled, false); #if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0) page->settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false); page->settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, false); #endif } void plainMessageText(const QString &plainTextContent, const QString &htmlContent, bool aStripSignature, const std::function &callback) { QString result = plainTextContent; if (plainTextContent.isEmpty()) { //HTML-only mails callback(toPlainText(htmlContent)); return; } if (aStripSignature) { result = stripSignature(result); } callback(result); } QString extractHeaderBodyScript() { const QString source = QStringLiteral("(function() {" "var res = {" " body: document.getElementsByTagName('body')[0].innerHTML," " header: document.getElementsByTagName('head')[0].innerHTML" "};" "return res;" "})()"); return source; } void htmlMessageText(const QString &plainTextContent, const QString &htmlContent, bool aStripSignature, const std::function &callback) { QString htmlElement = htmlContent; if (htmlElement.isEmpty()) { //plain mails only QString htmlReplace = plainTextContent.toHtmlEscaped(); htmlReplace = htmlReplace.replace(QStringLiteral("\n"), QStringLiteral("
")); htmlElement = QStringLiteral("%1\n").arg(htmlReplace); } auto page = new QWebEnginePage; setupPage(page); page->setHtml(htmlElement); page->runJavaScript(extractHeaderBodyScript(), QWebEngineScript::ApplicationWorld, [=](const QVariant &result){ page->deleteLater(); const QVariantMap map = result.toMap(); auto bodyElement = map.value(QStringLiteral("body")).toString(); auto headerElement = map.value(QStringLiteral("header")).toString(); if (!bodyElement.isEmpty()) { if (aStripSignature) { callback(stripSignature(bodyElement), headerElement); } return callback(bodyElement, headerElement); } if (aStripSignature) { return callback(stripSignature(htmlElement), headerElement); } return callback(htmlElement, headerElement); }); } QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString) { QString result; if (wildString.isEmpty()) { return wildString; } unsigned int strLength(wildString.length()); for (uint i = 0; i < strLength;) { QChar ch = wildString[i++]; if (ch == QLatin1Char('%') && i < strLength) { ch = wildString[i++]; switch (ch.toLatin1()) { case 'f': { // sender's initals if (fromDisplayString.isEmpty()) { break; } uint j = 0; const unsigned int strLength(fromDisplayString.length()); for (; j < strLength && fromDisplayString[j] > QLatin1Char(' '); ++j) ; for (; j < strLength && fromDisplayString[j] <= QLatin1Char(' '); ++j) ; result += fromDisplayString[0]; if (j < strLength && fromDisplayString[j] > QLatin1Char(' ')) { result += fromDisplayString[j]; } else if (strLength > 1) { if (fromDisplayString[1] > QLatin1Char(' ')) { result += fromDisplayString[1]; } } } break; case '_': result += QLatin1Char(' '); break; case '%': result += QLatin1Char('%'); break; default: result += QLatin1Char('%'); result += ch; break; } } else { result += ch; } } return result; } QString quotedPlainText(const QString &selection, const QString &fromDisplayString) { 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, static_cast(lineStart)); } const auto quoteString = QStringLiteral("> "); const QString indentStr = formatQuotePrefix(quoteString, fromDisplayString); //FIXME // if (TemplateParserSettings::self()->smartQuote() && mWrap) { // content = MessageCore::StringUtil::smartQuote(content, mColWrap - indentStr.length()); // } content.replace(QLatin1Char('\n'), QLatin1Char('\n') + indentStr); content.prepend(indentStr); content += QLatin1Char('\n'); return content; } QString quotedHtmlText(const QString &selection) { 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; } void applyCharset(const KMime::Message::Ptr msg, const KMime::Message::Ptr &origMsg) { // first convert the body from its current encoding to unicode representation QTextCodec *bodyCodec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset())); if (!bodyCodec) { bodyCodec = KCharsets::charsets()->codecForName(QStringLiteral("UTF-8")); } const QString body = bodyCodec->toUnicode(msg->body()); // then apply the encoding of the original message msg->contentType()->setCharset(origMsg->contentType()->charset()); QTextCodec *codec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset())); if (!codec) { qCritical() << "Could not get text codec for charset" << msg->contentType()->charset(); } else if (!codec->canEncode(body)) { // charset can't encode body, fall back to preferred const QStringList charsets /*= preferredCharsets() */; QList chars; chars.reserve(charsets.count()); foreach (const QString &charset, charsets) { chars << charset.toLatin1(); } //FIXME QByteArray fallbackCharset/* = selectCharset(chars, body)*/; if (fallbackCharset.isEmpty()) { // UTF-8 as fall-through fallbackCharset = "UTF-8"; } codec = KCharsets::charsets()->codecForName(QString::fromLatin1(fallbackCharset)); msg->setBody(codec->fromUnicode(body)); } else { msg->setBody(codec->fromUnicode(body)); } } enum ReplyStrategy { ReplyList, ReplySmart, ReplyAll, ReplyAuthor, ReplyNone }; static KMime::Types::Mailbox::List getMailingListAddresses(const KMime::Message::Ptr &origMsg) { KMime::Types::Mailbox::List mailingListAddresses; if (origMsg->headerByType("List-Post") && origMsg->headerByType("List-Post")->asUnicodeString().contains(QStringLiteral("mailto:"), Qt::CaseInsensitive)) { const QString listPost = origMsg->headerByType("List-Post")->asUnicodeString(); QRegExp rx(QStringLiteral("]+)@([^>]+)>"), Qt::CaseInsensitive); if (rx.indexIn(listPost, 0) != -1) { // matched KMime::Types::Mailbox mailbox; mailbox.fromUnicodeString(rx.cap(1) + QLatin1Char('@') + rx.cap(2)); mailingListAddresses << mailbox; } } return mailingListAddresses; } struct Recipients { KMime::Types::Mailbox::List to; KMime::Types::Mailbox::List cc; }; static Recipients getRecipients(const KMime::Message::Ptr &origMsg, const KMime::Types::AddrSpecList &me) { const KMime::Types::Mailbox::List replyToList = origMsg->replyTo()->mailboxes(); const KMime::Types::Mailbox::List mailingListAddresses = getMailingListAddresses(origMsg); KMime::Types::Mailbox::List toList; KMime::Types::Mailbox::List ccList; //FIXME const ReplyStrategy replyStrategy = ReplyAll; switch (replyStrategy) { case ReplySmart: { if (auto hdr = origMsg->headerByType("Mail-Followup-To")) { toList << KMime::Types::Mailbox::listFrom7BitString(hdr->as7BitString(false)); } else if (!replyToList.isEmpty()) { toList = replyToList; } else if (!mailingListAddresses.isEmpty()) { toList = (KMime::Types::Mailbox::List() << mailingListAddresses.at(0)); } else { // doesn't seem to be a mailing list, reply to From: address toList = origMsg->from()->mailboxes(); bool listContainsMe = false; for (const auto &m : me) { KMime::Types::Mailbox mailbox; mailbox.setAddress(m); if (toList.contains(mailbox)) { listContainsMe = true; } } if (listContainsMe) { // sender seems to be one of our own identities, so we assume that this // is a reply to a "sent" mail where the users wants to add additional // information for the recipient. toList = origMsg->to()->mailboxes(); } } // strip all my addresses from the list of recipients const KMime::Types::Mailbox::List recipients = toList; toList = stripMyAddressesFromAddressList(recipients, me); // ... unless the list contains only my addresses (reply to self) if (toList.isEmpty() && !recipients.isEmpty()) { toList << recipients.first(); } } break; case ReplyList: { if (auto hdr = origMsg->headerByType("Mail-Followup-To")) { KMime::Types::Mailbox mailbox; mailbox.from7BitString(hdr->as7BitString(false)); toList << mailbox; } else if (!mailingListAddresses.isEmpty()) { toList << mailingListAddresses[ 0 ]; } else if (!replyToList.isEmpty()) { // assume a Reply-To header mangling mailing list toList = replyToList; } //FIXME // strip all my addresses from the list of recipients const KMime::Types::Mailbox::List recipients = toList; toList = stripMyAddressesFromAddressList(recipients, me); } break; case ReplyAll: { KMime::Types::Mailbox::List recipients; KMime::Types::Mailbox::List ccRecipients; // add addresses from the Reply-To header to the list of recipients if (!replyToList.isEmpty()) { recipients = replyToList; // strip all possible mailing list addresses from the list of Reply-To addresses foreach (const KMime::Types::Mailbox &mailbox, mailingListAddresses) { foreach (const KMime::Types::Mailbox &recipient, recipients) { if (mailbox == recipient) { recipients.removeAll(recipient); } } } } if (!mailingListAddresses.isEmpty()) { // this is a mailing list message if (recipients.isEmpty() && !origMsg->from()->asUnicodeString().isEmpty()) { // The sender didn't set a Reply-to address, so we add the From // address to the list of CC recipients. ccRecipients += origMsg->from()->mailboxes(); qDebug() << "Added" << origMsg->from()->asUnicodeString() << "to the list of CC recipients"; } // if it is a mailing list, add the posting address recipients.prepend(mailingListAddresses[ 0 ]); } else { // this is a normal message if (recipients.isEmpty() && !origMsg->from()->asUnicodeString().isEmpty()) { // in case of replying to a normal message only then add the From // address to the list of recipients if there was no Reply-to address recipients += origMsg->from()->mailboxes(); qDebug() << "Added" << origMsg->from()->asUnicodeString() << "to the list of recipients"; } } // strip all my addresses from the list of recipients toList = stripMyAddressesFromAddressList(recipients, me); // merge To header and CC header into a list of CC recipients if (!origMsg->cc()->asUnicodeString().isEmpty() || !origMsg->to()->asUnicodeString().isEmpty()) { KMime::Types::Mailbox::List list; if (!origMsg->to()->asUnicodeString().isEmpty()) { list += origMsg->to()->mailboxes(); } if (!origMsg->cc()->asUnicodeString().isEmpty()) { list += origMsg->cc()->mailboxes(); } foreach (const KMime::Types::Mailbox &mailbox, list) { if (!recipients.contains(mailbox) && !ccRecipients.contains(mailbox)) { ccRecipients += mailbox; qDebug() << "Added" << mailbox.prettyAddress() << "to the list of CC recipients"; } } } if (!ccRecipients.isEmpty()) { // strip all my addresses from the list of CC recipients ccRecipients = stripMyAddressesFromAddressList(ccRecipients, me); // in case of a reply to self, toList might be empty. if that's the case // then propagate a cc recipient to To: (if there is any). if (toList.isEmpty() && !ccRecipients.isEmpty()) { toList << ccRecipients.at(0); ccRecipients.pop_front(); } ccList = ccRecipients; } if (toList.isEmpty() && !recipients.isEmpty()) { // reply to self without other recipients toList << recipients.at(0); } } break; case ReplyAuthor: { if (!replyToList.isEmpty()) { KMime::Types::Mailbox::List recipients = replyToList; // strip the mailing list post address from the list of Reply-To // addresses since we want to reply in private foreach (const KMime::Types::Mailbox &mailbox, mailingListAddresses) { foreach (const KMime::Types::Mailbox &recipient, recipients) { if (mailbox == recipient) { recipients.removeAll(recipient); } } } if (!recipients.isEmpty()) { toList = recipients; } else { // there was only the mailing list post address in the Reply-To header, // so use the From address instead toList = origMsg->from()->mailboxes(); } } else if (!origMsg->from()->asUnicodeString().isEmpty()) { toList = origMsg->from()->mailboxes(); } } break; case ReplyNone: // the addressees will be set by the caller break; } return {toList, ccList}; } void MailTemplates::reply(const KMime::Message::Ptr &origMsg, const std::function &callback, const KMime::Types::AddrSpecList &me) { //FIXME const bool alwaysPlain = true; KMime::Message::Ptr msg(new KMime::Message); msg->removeHeader(); msg->removeHeader(); msg->contentType(true)->setMimeType("text/plain"); msg->contentType()->setCharset("utf-8"); const auto recipients = getRecipients(origMsg, me); for (const auto &mailbox : recipients.to) { msg->to()->addAddress(mailbox); } for (const auto &mailbox : recipients.cc) { msg->cc(true)->addAddress(mailbox); } const QByteArray refStr = getRefStr(origMsg); if (!refStr.isEmpty()) { msg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8"); } //In-Reply-To = original msg-id msg->inReplyTo()->from7BitString(origMsg->messageID()->as7BitString(false)); msg->subject()->fromUnicodeString(replySubject(origMsg->subject()->asUnicodeString()), "utf-8"); auto definedLocale = QLocale::system(); //Add quoted body QString plainBody; QString htmlBody; //On $datetime you wrote: const QDateTime date = origMsg->date()->dateTime(); const auto dateTimeString = QString("%1 %2").arg(definedLocale.toString(date.date(), QLocale::LongFormat)).arg(definedLocale.toString(date.time(), QLocale::LongFormat)); const auto onDateYouWroteLine = QString("On %1 you wrote:\n").arg(dateTimeString); plainBody.append(onDateYouWroteLine); htmlBody.append(plainToHtml(onDateYouWroteLine)); //Strip signature for replies const bool stripSignature = true; MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(origMsg.data()); otp.decryptAndVerify(); const auto plainTextContent = otp.plainTextContent(); const auto htmlContent = otp.htmlContent(); plainMessageText(plainTextContent, htmlContent, stripSignature, [=] (const QString &body) { //Quoted body QString plainQuote = quotedPlainText(body, origMsg->from()->displayString()); if (plainQuote.endsWith(QLatin1Char('\n'))) { plainQuote.chop(1); } //The plain body is complete auto plainBodyResult = plainBody + plainQuote; htmlMessageText(plainTextContent, htmlContent, stripSignature, [=] (const QString &body, const QString &headElement) { //The html body is complete const auto htmlBodyResult = [&]() { if (!alwaysPlain) { auto htmlBodyResult = htmlBody + quotedHtmlText(body); makeValidHtml(htmlBodyResult, headElement); return htmlBodyResult; } return QString{}; }(); //Assemble the message msg->contentType()->clear(); // to get rid of old boundary KMime::Content *const mainTextPart = htmlBodyResult.isEmpty() ? createPlainPartContent(plainBodyResult, msg.data()) : createMultipartAlternativeContent(plainBodyResult, htmlBodyResult, msg.data()); mainTextPart->assemble(); msg->setBody(mainTextPart->encodedBody()); msg->setHeader(mainTextPart->contentType()); msg->setHeader(mainTextPart->contentTransferEncoding()); //FIXME this does more harm than good right now. // applyCharset(msg, origMsg); msg->assemble(); callback(msg); }); }); } void MailTemplates::forward(const KMime::Message::Ptr &origMsg, const std::function &callback) { KMime::Message::Ptr wrapperMsg(new KMime::Message); wrapperMsg->to()->clear(); wrapperMsg->cc()->clear(); // Decrypt the original message, it will be encrypted again in the composer // for the right recipient KMime::Message::Ptr forwardedMessage(new KMime::Message()); if (isEncrypted(origMsg.data())) { qDebug() << "Original message was encrypted, decrypting it"; MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(origMsg.data()); otp.decryptAndVerify(); auto htmlContent = otp.htmlContent(); KMime::Content *recreatedMsg = htmlContent.isEmpty() ? createPlainPartContent(otp.plainTextContent()) : createMultipartAlternativeContent(otp.plainTextContent(), htmlContent); KMime::Message::Ptr tmpForwardedMessage; auto attachments = otp.collectAttachmentParts(); if (!attachments.isEmpty()) { QVector contents = {recreatedMsg}; for (const auto &attachment : attachments) { contents.append(attachment->node()); } auto msg = createMultipartMixedContent(contents); tmpForwardedMessage.reset(KMime::contentToMessage(msg)); } else { tmpForwardedMessage.reset(KMime::contentToMessage(recreatedMsg)); } origMsg->contentType()->fromUnicodeString(tmpForwardedMessage->contentType()->asUnicodeString(), "utf-8"); origMsg->assemble(); forwardedMessage->setHead(origMsg->head()); forwardedMessage->setBody(tmpForwardedMessage->encodedBody()); forwardedMessage->parse(); } else { qDebug() << "Original message was not encrypted, using it as-is"; forwardedMessage = origMsg; } wrapperMsg->subject()->fromUnicodeString( forwardSubject(forwardedMessage->subject()->asUnicodeString()), "utf-8"); const QByteArray refStr = getRefStr(forwardedMessage); if (!refStr.isEmpty()) { wrapperMsg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8"); } KMime::Content *fwdAttachment = new KMime::Content; fwdAttachment->contentDisposition()->setDisposition(KMime::Headers::CDinline); fwdAttachment->contentType()->setMimeType("message/rfc822"); fwdAttachment->contentDisposition()->setFilename(forwardedMessage->subject()->asUnicodeString() + ".eml"); fwdAttachment->setBody(KMime::CRLFtoLF(forwardedMessage->encodedContent(false))); wrapperMsg->addContent(fwdAttachment); wrapperMsg->assemble(); callback(wrapperMsg); } QString MailTemplates::plaintextContent(const KMime::Message::Ptr &msg) { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(msg.data()); const auto plain = otp.plainTextContent(); if (plain.isEmpty()) { //Maybe not as good as the webengine version, but works at least for simple html content return toPlainText(otp.htmlContent()); } return plain; } QString MailTemplates::body(const KMime::Message::Ptr &msg, bool &isHtml) { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(msg.data()); const auto html = otp.htmlContent(); if (html.isEmpty()) { isHtml = false; return otp.plainTextContent(); } isHtml = true; return html; } -static KMime::Content *createAttachmentPart(const QByteArray &content, const QString &filename, bool isInline, const QByteArray &mimeType, const QString &name) +static KMime::Content *createAttachmentPart(const QByteArray &content, const QString &filename, bool isInline, const QByteArray &mimeType, const QString &name, bool base64Encode = true) { KMime::Content *part = new KMime::Content; part->contentDisposition(true)->setFilename(filename); if (isInline) { part->contentDisposition(true)->setDisposition(KMime::Headers::CDinline); } else { part->contentDisposition(true)->setDisposition(KMime::Headers::CDattachment); } part->contentType(true)->setMimeType(mimeType); - part->contentType(true)->setName(name, "utf-8"); - // Just always encode attachments base64 so it's safe for binary data, - // except when it's another message - if(mimeType != "message/rfc822") { + if (!name.isEmpty()) { + part->contentType(true)->setName(name, "utf-8"); + } + if(base64Encode) { part->contentTransferEncoding(true)->setEncoding(KMime::Headers::CEbase64); } part->setBody(content); return part; } static KMime::Content *createBodyPart(const QString &body, bool htmlBody) { if (htmlBody) { return createMultipartAlternativeContent(toPlainText(body), body); } return createPlainPartContent(body); } static KMime::Types::Mailbox::List stringListToMailboxes(const QStringList &list) { KMime::Types::Mailbox::List mailboxes; for (const auto &s : list) { KMime::Types::Mailbox mb; mb.fromUnicodeString(s); mailboxes << mb; } return mailboxes; } KMime::Message::Ptr MailTemplates::createMessage(KMime::Message::Ptr existingMessage, const QStringList &to, const QStringList &cc, const QStringList &bcc, const KMime::Types::Mailbox &from, const QString &subject, const QString &body, bool htmlBody, const QList &attachments, const std::vector &signingKeys, const std::vector &encryptionKeys, const Crypto::Key &attachedKey) { auto mail = existingMessage; if (!mail) { mail = KMime::Message::Ptr::create(); } else { //Content type is part of the body part we're creating mail->removeHeader(); mail->removeHeader(); } mail->date()->setDateTime(QDateTime::currentDateTime()); mail->userAgent()->fromUnicodeString(QString("%1/%2(%3)").arg(QString::fromLocal8Bit("Kube")).arg("0.1").arg(QSysInfo::prettyProductName()), "utf-8"); mail->to(true)->clear(); for (const auto &mb : stringListToMailboxes(to)) { mail->to()->addAddress(mb); } mail->cc(true)->clear(); for (const auto &mb : stringListToMailboxes(cc)) { mail->cc()->addAddress(mb); } mail->bcc(true)->clear(); for (const auto &mb : stringListToMailboxes(bcc)) { mail->bcc()->addAddress(mb); } mail->from(true)->clear(); mail->from(true)->addAddress(from); mail->subject(true)->fromUnicodeString(subject, "utf-8"); if (!mail->messageID(false)) { //A globally unique messageId that doesn't leak the local hostname const auto messageId = "<" + QUuid::createUuid().toString().mid(1, 36).remove('-') + "@kube>"; mail->messageID(true)->fromUnicodeString(messageId, "utf-8"); } if (!mail->date(true)->dateTime().isValid()) { mail->date(true)->setDateTime(QDateTime::currentDateTimeUtc()); } mail->assemble(); + const bool encryptionRequired = !signingKeys.empty() || !encryptionKeys.empty(); + //We always attach the key when encryption is enabled. + const bool attachingPersonalKey = encryptionRequired; + + auto allAttachments = attachments; + if (attachingPersonalKey) { + const auto publicKeyExportResult = Crypto::exportPublicKey(attachedKey); + if (!publicKeyExportResult) { + qWarning() << "Failed to export public key" << publicKeyExportResult.error(); + return {}; + } + const auto publicKeyData = publicKeyExportResult.value(); + allAttachments << Attachment{ + {}, + QString("0x%1.asc").arg(QString{attachedKey.shortKeyId}), + "application/pgp-keys", + false, + publicKeyData + }; + } + std::unique_ptr bodyPart{[&] { - if (!attachments.isEmpty()) { + if (!allAttachments.isEmpty()) { auto bodyPart = new KMime::Content; bodyPart->contentType(true)->setMimeType("multipart/mixed"); bodyPart->contentType()->setBoundary(KMime::multiPartBoundary()); bodyPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); bodyPart->setPreamble("This is a multi-part message in MIME format.\n"); bodyPart->addContent(createBodyPart(body, htmlBody)); - for (const auto &attachment : attachments) { - bodyPart->addContent(createAttachmentPart(attachment.data, attachment.filename, attachment.isInline, attachment.mimeType, attachment.name)); + for (const auto &attachment : allAttachments) { + + // Just always encode attachments base64 so it's safe for binary data, + // except when it's another message or an ascii armored key + static QSet noEncodingRequired{{"message/rfc822"}, {"application/pgp-keys"}}; + const bool base64Encode = !noEncodingRequired.contains(attachment.mimeType); + bodyPart->addContent(createAttachmentPart(attachment.data, attachment.filename, attachment.isInline, attachment.mimeType, attachment.name, base64Encode)); } return bodyPart; } else { return createBodyPart(body, htmlBody); } }()}; bodyPart->assemble(); - QByteArray bodyData; - if (!signingKeys.empty() || !encryptionKeys.empty()) { - auto result = MailCrypto::processCrypto(std::move(bodyPart), signingKeys, encryptionKeys, attachedKey); - if (!result) { - qWarning() << "Crypto failed"; - return {}; - } - bodyData = result.value()->encodedContent(); - } else { - if (!bodyPart->contentType(false)) { - bodyPart->contentType(true)->setMimeType("text/plain"); - bodyPart->assemble(); + const QByteArray bodyData = [&] { + if (encryptionRequired) { + auto result = MailCrypto::processCrypto(std::move(bodyPart), signingKeys, encryptionKeys); + if (!result) { + qWarning() << "Crypto failed" << result.error(); + return QByteArray{}; + } + result.value()->assemble(); + return result.value()->encodedContent(); + } else { + if (!bodyPart->contentType(false)) { + bodyPart->contentType(true)->setMimeType("text/plain"); + bodyPart->assemble(); + } + return bodyPart->encodedContent(); } - bodyData = bodyPart->encodedContent(); + }(); + if (bodyData.isEmpty()) { + return {}; } KMime::Message::Ptr resultMessage(new KMime::Message); resultMessage->setContent(mail->head() + bodyData); resultMessage->parse(); // Not strictly necessary. return resultMessage; } diff --git a/framework/src/domain/mime/tests/mailtemplatetest.cpp b/framework/src/domain/mime/tests/mailtemplatetest.cpp index 508af9aa..16d576c0 100644 --- a/framework/src/domain/mime/tests/mailtemplatetest.cpp +++ b/framework/src/domain/mime/tests/mailtemplatetest.cpp @@ -1,451 +1,446 @@ #include #include #include #include #include #include #include #include "mailtemplates.h" #include "mailcrypto.h" static KMime::Content *getSubpart(KMime::Content *msg, const QByteArray &mimeType) { for (const auto c : msg->contents()) { if (c->contentType(false)->mimeType() == mimeType) { return c; } } return nullptr; } static QByteArray readMailFromFile(const QString &mailFile) { Q_ASSERT(!QString::fromLatin1(MAIL_DATA_DIR).isEmpty()); QFile file(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile); file.open(QIODevice::ReadOnly); Q_ASSERT(file.isOpen()); return file.readAll(); } static KMime::Message::Ptr readMail(const QString &mailFile) { auto msg = KMime::Message::Ptr::create(); msg->setContent(readMailFromFile(mailFile)); msg->parse(); return msg; } static QString removeFirstLine(const QString &s) { return s.mid(s.indexOf("\n") + 1); } static QString normalize(const QString &s) { auto text = s; text.replace(">", ""); text.replace("\n", ""); text.replace("=", ""); text.replace(" ", ""); return text; } static QString unquote(const QString &s) { auto text = s; text.replace("> ", ""); return text; } class MailTemplateTest : public QObject { Q_OBJECT bool validate(KMime::Message::Ptr msg) { const auto data = msg->encodedContent(); //IMAP compat: The ASCII NUL character, %x00, MUST NOT be used at any time. if (data.contains('\0')) { return false; } return true; } private slots: void initTestCase() { QtWebEngine::initialize(); } void testPlainReply() { auto msg = readMail("plaintext.mbox"); KMime::Message::Ptr result; MailTemplates::reply(msg, [&] (const KMime::Message::Ptr &r) { result = r; }); QTRY_VERIFY(result); QCOMPARE(normalize(removeFirstLine(result->body())), normalize(msg->body())); QCOMPARE(result->to()->addresses(), {{"konqi@example.org"}}); QCOMPARE(result->subject()->asUnicodeString(), {"RE: A random subject with alternative contenttype"}); } void testHtmlReply() { auto msg = readMail("html.mbox"); KMime::Message::Ptr result; MailTemplates::reply(msg, [&] (const KMime::Message::Ptr &r) { result = r; }); QTRY_VERIFY(result); QCOMPARE(unquote(removeFirstLine(result->body())), QLatin1String("HTML text")); } void testHtml8BitEncodedReply() { auto msg = readMail("8bitencoded.mbox"); KMime::Message::Ptr result; MailTemplates::reply(msg, [&] (const KMime::Message::Ptr &r) { result = r; }); QTRY_VERIFY(result); QVERIFY(MailTemplates::plaintextContent(result).contains(QString::fromUtf8("Why Pisa’s Tower"))); } void testMultipartSignedReply() { auto msg = readMail("openpgp-signed-mailinglist.mbox"); KMime::Message::Ptr result; MailTemplates::reply(msg, [&] (const KMime::Message::Ptr &r) { result = r; }); QTRY_VERIFY(result); auto content = removeFirstLine(result->body()); QVERIFY(!content.isEmpty()); QVERIFY(content.contains("i noticed a new branch")); } void testMultipartAlternativeReply() { auto msg = readMail("alternative.mbox"); KMime::Message::Ptr result; MailTemplates::reply(msg, [&] (const KMime::Message::Ptr &r) { result = r; }); QTRY_VERIFY(result); auto content = removeFirstLine(result->body()); QVERIFY(!content.isEmpty()); QCOMPARE(unquote(content), QLatin1String("If you can see this text it means that your email client couldn't display our newsletter properly.\nPlease visit this link to view the newsletter on our website: http://www.gog.com/newsletter/\n")); } void testAttachmentReply() { auto msg = readMail("plaintextattachment.mbox"); KMime::Message::Ptr result; MailTemplates::reply(msg, [&] (const KMime::Message::Ptr &r) { result = r; }); QTRY_VERIFY(result); auto content = removeFirstLine(result->body()); QVERIFY(!content.isEmpty()); QCOMPARE(unquote(content), QLatin1String("sdlkjsdjf")); } void testMultiRecipientReply() { auto msg = readMail("multirecipients.mbox"); KMime::Message::Ptr result; MailTemplates::reply(msg, [&] (const KMime::Message::Ptr &r) { result = r; }); QTRY_VERIFY(result); auto content = removeFirstLine(result->body()); QVERIFY(!content.isEmpty()); QCOMPARE(unquote(content), QLatin1String("test")); QCOMPARE(result->to()->addresses(), {{"konqi@example.org"}}); auto l = QVector{{"release-team@kde.org"}, {"kde-devel@kde.org"}}; QCOMPARE(result->cc()->addresses(), l); } void testMultiRecipientReplyFilteringMe() { KMime::Types::AddrSpecList me; KMime::Types::Mailbox mb; mb.setAddress("release-team@kde.org"); me << mb.addrSpec(); auto msg = readMail("multirecipients.mbox"); KMime::Message::Ptr result; MailTemplates::reply(msg, [&] (const KMime::Message::Ptr &r) { result = r; }, me); QTRY_VERIFY(result); auto content = removeFirstLine(result->body()); QVERIFY(!content.isEmpty()); QCOMPARE(unquote(content), QLatin1String("test")); QCOMPARE(result->to()->addresses(), {{"konqi@example.org"}}); auto l = QVector{{"kde-devel@kde.org"}}; QCOMPARE(result->cc()->addresses(), l); } void testForwardAsAttachment() { auto msg = readMail("plaintext.mbox"); KMime::Message::Ptr result; MailTemplates::forward(msg, [&] (const KMime::Message::Ptr &r) { result = r; }); QTRY_VERIFY(result); QCOMPARE(result->subject(false)->asUnicodeString(), {"FW: A random subject with alternative contenttype"}); QCOMPARE(result->to()->addresses(), {}); QCOMPARE(result->cc()->addresses(), {}); auto attachments = result->attachments(); QCOMPARE(attachments.size(), 1); auto attachment = attachments[0]; QCOMPARE(attachment->contentDisposition(false)->disposition(), KMime::Headers::CDinline); QCOMPARE(attachment->contentDisposition(false)->filename(), {"A random subject with alternative contenttype.eml"}); QVERIFY(attachment->bodyIsMessage()); attachment->parse(); auto origMsg = attachment->bodyAsMessage(); QCOMPARE(origMsg->subject(false)->asUnicodeString(), {"A random subject with alternative contenttype"}); } void testEncryptedForwardAsAttachment() { auto msg = readMail("openpgp-encrypted.mbox"); KMime::Message::Ptr result; MailTemplates::forward(msg, [&](const KMime::Message::Ptr &r) { result = r; }); QTRY_VERIFY(result); QCOMPARE(result->subject(false)->asUnicodeString(), {"FW: OpenPGP encrypted"}); QCOMPARE(result->to()->addresses(), {}); QCOMPARE(result->cc()->addresses(), {}); auto attachments = result->attachments(); QCOMPARE(attachments.size(), 1); auto attachment = attachments[0]; QCOMPARE(attachment->contentDisposition(false)->disposition(), KMime::Headers::CDinline); QCOMPARE(attachment->contentDisposition(false)->filename(), {"OpenPGP encrypted.eml"}); QVERIFY(attachment->bodyIsMessage()); attachment->parse(); auto origMsg = attachment->bodyAsMessage(); QCOMPARE(origMsg->subject(false)->asUnicodeString(), {"OpenPGP encrypted"}); } void testEncryptedWithAttachmentsForwardAsAttachment() { auto msg = readMail("openpgp-encrypted-two-attachments.mbox"); KMime::Message::Ptr result; MailTemplates::forward(msg, [&](const KMime::Message::Ptr &r) { result = r; }); QTRY_VERIFY(result); QCOMPARE(result->subject(false)->asUnicodeString(), {"FW: OpenPGP encrypted with 2 text attachments"}); QCOMPARE(result->to()->addresses(), {}); QCOMPARE(result->cc()->addresses(), {}); auto attachments = result->attachments(); QCOMPARE(attachments.size(), 1); auto attachment = attachments[0]; QCOMPARE(attachment->contentDisposition(false)->disposition(), KMime::Headers::CDinline); QCOMPARE(attachment->contentDisposition(false)->filename(), {"OpenPGP encrypted with 2 text attachments.eml"}); QVERIFY(attachment->bodyIsMessage()); attachment->parse(); auto origMsg = attachment->bodyAsMessage(); QCOMPARE(origMsg->subject(false)->asUnicodeString(), {"OpenPGP encrypted with 2 text attachments"}); auto attattachments = origMsg->attachments(); QCOMPARE(attattachments.size(), 2); QCOMPARE(attattachments[0]->contentDisposition(false)->filename(), {"attachment1.txt"}); QCOMPARE(attattachments[1]->contentDisposition(false)->filename(), {"attachment2.txt"}); } void testCreatePlainMail() { QStringList to = {{"to@example.org"}}; QStringList cc = {{"cc@example.org"}}; QStringList bcc = {{"bcc@example.org"}};; KMime::Types::Mailbox from; from.fromUnicodeString("from@example.org"); QString subject = "subject"; QString body = "body"; QList attachments; auto result = MailTemplates::createMessage({}, to, cc, bcc, from, subject, body, false, attachments); QVERIFY(result); QVERIFY(validate(result)); QCOMPARE(result->subject()->asUnicodeString(), subject); QCOMPARE(result->body(), body.toUtf8()); QVERIFY(result->date(false)->dateTime().isValid()); QVERIFY(result->contentType()->isMimeType("text/plain")); QVERIFY(result->messageID(false) && !result->messageID(false)->isEmpty()); } void testCreateHtmlMail() { QStringList to = {{"to@example.org"}}; QStringList cc = {{"cc@example.org"}}; QStringList bcc = {{"bcc@example.org"}};; KMime::Types::Mailbox from; from.fromUnicodeString("from@example.org"); QString subject = "subject"; QString body = "body"; QList attachments; auto result = MailTemplates::createMessage({}, to, cc, bcc, from, subject, body, true, attachments); QVERIFY(result); QVERIFY(validate(result)); QCOMPARE(result->subject()->asUnicodeString(), subject); QVERIFY(result->date(false)->dateTime().isValid()); QVERIFY(result->contentType()->isMimeType("multipart/alternative")); const auto contents = result->contents(); //1 Plain + 1 Html QCOMPARE(contents.size(), 2); } void testCreatePlainMailWithAttachments() { QStringList to = {{"to@example.org"}}; QStringList cc = {{"cc@example.org"}};; QStringList bcc = {{"bcc@example.org"}};; KMime::Types::Mailbox from; from.fromUnicodeString("from@example.org"); QString subject = "subject"; QString body = "body"; QList attachments = {{"name", "filename", "mimetype", true, "inlineAttachment"}, {"name", "filename", "mimetype", false, "nonInlineAttachment"}}; auto result = MailTemplates::createMessage({}, to, cc, bcc, from, subject, body, false, attachments); QVERIFY(result); QVERIFY(validate(result)); QCOMPARE(result->subject()->asUnicodeString(), subject); QVERIFY(result->contentType()->isMimeType("multipart/mixed")); QVERIFY(result->date(false)->dateTime().isValid()); const auto contents = result->contents(); //1 Plain + 2 Attachments QCOMPARE(contents.size(), 3); auto p = getSubpart(result.data(), "text/plain"); QVERIFY(p); } void testCreateHtmlMailWithAttachments() { QStringList to = {{"to@example.org"}}; QStringList cc = {{"cc@example.org"}};; QStringList bcc = {{"bcc@example.org"}};; KMime::Types::Mailbox from; from.fromUnicodeString("from@example.org"); QString subject = "subject"; QString body = "body"; QList attachments = {{"name", "filename", "mimetype", true, "inlineAttachment"}, {"name", "filename", "mimetype", false, "nonInlineAttachment"}}; auto result = MailTemplates::createMessage({}, to, cc, bcc, from, subject, body, true, attachments); QVERIFY(result); QVERIFY(validate(result)); QCOMPARE(result->subject()->asUnicodeString(), subject); QVERIFY(result->contentType()->isMimeType("multipart/mixed")); QVERIFY(result->date(false)->dateTime().isValid()); const auto contents = result->contents(); //1 alternative + 2 Attachments QCOMPARE(contents.size(), 3); auto p = getSubpart(result.data(), "multipart/alternative"); QVERIFY(p); } void testCreatePlainMailSigned() { QStringList to = {{"to@example.org"}}; QStringList cc = {{"cc@example.org"}};; QStringList bcc = {{"bcc@example.org"}};; KMime::Types::Mailbox from; from.fromUnicodeString("from@example.org"); QString subject = "subject"; QString body = "body"; QList attachments; auto keys = Crypto::findKeys({}, true, false); auto result = MailTemplates::createMessage({}, to, cc, bcc, from, subject, body, false, attachments, keys, {}, keys[0]); QVERIFY(result); QVERIFY(validate(result)); // qWarning() << "---------------------------------"; // qWarning().noquote() << result->encodedContent(); // qWarning() << "---------------------------------"; QCOMPARE(result->subject()->asUnicodeString(), subject); QVERIFY(result->date(false)->dateTime().isValid()); - QCOMPARE(result->contentType()->mimeType(), QByteArray{"multipart/mixed"}); - auto resultAttachments = result->attachments(); - QCOMPARE(resultAttachments.size(), 1); - QCOMPARE(resultAttachments[0]->contentDisposition()->filename(), {"0x8F246DE6.asc"}); + QCOMPARE(result->contentType()->mimeType(), QByteArray{"multipart/signed"}); + QCOMPARE(result->attachments().size(), 1); + QCOMPARE(result->attachments()[0]->contentDisposition()->filename(), {"0x8F246DE6.asc"}); + QCOMPARE(result->contents().size(), 2); auto signedMessage = result->contents()[0]; - - QVERIFY(signedMessage->contentType()->isMimeType("multipart/signed")); - + QVERIFY(signedMessage->contentType()->isMimeType("multipart/mixed")); const auto contents = signedMessage->contents(); QCOMPARE(contents.size(), 2); - { - auto c = contents.at(0); - QVERIFY(c->contentType()->isMimeType("text/plain")); - } - { - auto c = contents.at(1); - QVERIFY(c->contentType()->isMimeType("application/pgp-signature")); - } + QCOMPARE(contents[0]->contentType()->mimeType(), QByteArray{"text/plain"}); + QCOMPARE(contents[1]->contentType()->mimeType(), QByteArray{"application/pgp-keys"}); + QCOMPARE(contents[1]->contentDisposition()->filename(), QByteArray{"0x8F246DE6.asc"}); + + auto signature = result->contents()[1]; + QCOMPARE(signature->contentDisposition()->filename(), QByteArray{"signature.asc"}); + QVERIFY(signature->contentType()->isMimeType("application/pgp-signature")); } void testCreatePlainMailWithAttachmentsSigned() { QStringList to = {{"to@example.org"}}; QStringList cc = {{"cc@example.org"}};; QStringList bcc = {{"bcc@example.org"}};; KMime::Types::Mailbox from; from.fromUnicodeString("from@example.org"); QString subject = "subject"; QString body = "body"; - QList attachments = {{"name", "filename", "mimetype", true, "inlineAttachment"}, {"name", "filename", "mimetype", false, "nonInlineAttachment"}}; + QList attachments = {{"name", "filename1", "mimetype1", true, "inlineAttachment"}, {"name", "filename2", "mimetype2", false, "nonInlineAttachment"}}; - auto result = MailTemplates::createMessage({}, to, cc, bcc, from, subject, body, false, attachments, Crypto::findKeys({}, true, false)); + auto signingKeys = Crypto::findKeys({}, true, false); + auto result = MailTemplates::createMessage({}, to, cc, bcc, from, subject, body, false, attachments, signingKeys, {}, signingKeys[0]); QVERIFY(result); QVERIFY(validate(result)); + qWarning() << "---------------------------------"; + qWarning().noquote() << result->encodedContent(); + qWarning() << "---------------------------------"; QCOMPARE(result->subject()->asUnicodeString(), subject); QVERIFY(result->date(false)->dateTime().isValid()); - QCOMPARE(result->contentType()->mimeType(), QByteArray{"multipart/mixed"}); - auto resultAttachments = result->attachments(); - QCOMPARE(resultAttachments.size(), 3); - // It seems KMime searches for the attachments using depth-first - // search, so the public key is last - QCOMPARE(resultAttachments[2]->contentDisposition()->filename(), {"0x8F246DE6.asc"}); + QCOMPARE(result->contentType()->mimeType(), QByteArray{"multipart/signed"}); + QCOMPARE(result->attachments().size(), 3); + QCOMPARE(result->attachments()[0]->contentDisposition()->filename(), {"filename1"}); + QCOMPARE(result->attachments()[1]->contentDisposition()->filename(), {"filename2"}); + QCOMPARE(result->attachments()[2]->contentDisposition()->filename(), {"0x8F246DE6.asc"}); - auto signedMessage = result->contents()[0]; - - QVERIFY(signedMessage->contentType()->isMimeType("multipart/signed")); + QCOMPARE(result->contents().size(), 2); + auto signedMessage = result->contents()[0]; + QVERIFY(signedMessage->contentType()->isMimeType("multipart/mixed")); const auto contents = signedMessage->contents(); - QCOMPARE(contents.size(), 2); - { - auto c = contents.at(0); - QVERIFY(c->contentType()->isMimeType("multipart/mixed")); - //1 text + 2 attachments - QCOMPARE(c->contents().size(), 3); - } - { - auto c = contents.at(1); - QVERIFY(c->contentType()->isMimeType("application/pgp-signature")); - } + QCOMPARE(contents.size(), 4); + QCOMPARE(contents[0]->contentType()->mimeType(), QByteArray{"text/plain"}); + QCOMPARE(contents[1]->contentDisposition()->filename(), QByteArray{"filename1"}); + QCOMPARE(contents[2]->contentDisposition()->filename(), QByteArray{"filename2"}); + QCOMPARE(contents[3]->contentType()->mimeType(), QByteArray{"application/pgp-keys"}); + QCOMPARE(contents[3]->contentDisposition()->filename(), QByteArray{"0x8F246DE6.asc"}); } }; QTEST_MAIN(MailTemplateTest) #include "mailtemplatetest.moc"