File indexing completed on 2025-01-19 04:51:56

0001 /*
0002     Copyright (c) 2009 Constantin Berzan <exit3219@gmail.com>
0003     Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
0004     Copyright (c) 2010 Leo Franchi <lfranchi@kde.org>
0005     Copyright (c) 2017 Christian Mollekopf <mollekopf@kolabsys.com>
0006 
0007     This library is free software; you can redistribute it and/or modify it
0008     under the terms of the GNU Library General Public License as published by
0009     the Free Software Foundation; either version 2 of the License, or (at your
0010     option) any later version.
0011 
0012     This library is distributed in the hope that it will be useful, but WITHOUT
0013     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
0014     FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
0015     License for more details.
0016 
0017     You should have received a copy of the GNU Library General Public License
0018     along with this library; see the file COPYING.LIB.  If not, write to the
0019     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
0020     02110-1301, USA.
0021 */
0022 #include "mailtemplates.h"
0023 
0024 #include <functional>
0025 #include <QByteArray>
0026 #include <QList>
0027 #include <QDebug>
0028 #include <QWebEnginePage>
0029 #include <QWebEngineProfile>
0030 #include <QWebEngineSettings>
0031 #include <QWebEngineScript>
0032 #include <QSysInfo>
0033 #include <QUuid>
0034 #include <QTextCodec>
0035 #include <QTextDocument>
0036 
0037 #include <KCodecs/KCharsets>
0038 #include <KMime/Types>
0039 
0040 #include <sink/mimetreeparser/objecttreeparser.h>
0041 
0042 #include "mailcrypto.h"
0043 
0044 QDebug operator<<(QDebug dbg, const KMime::Types::Mailbox &mb)
0045 {
0046     dbg << mb.addrSpec().asString();
0047     return dbg;
0048 }
0049 
0050 namespace KMime {
0051     namespace Types {
0052 static bool operator==(const KMime::Types::AddrSpec &left, const KMime::Types::AddrSpec &right)
0053 {
0054     return (left.asString() == right.asString());
0055 }
0056 
0057 static bool operator==(const KMime::Types::Mailbox &left, const KMime::Types::Mailbox &right)
0058 {
0059     return (left.addrSpec().asString() == right.addrSpec().asString());
0060 }
0061     }
0062 
0063     Message* contentToMessage(Content* content) {
0064         content->assemble();
0065         const auto encoded = content->encodedContent();
0066 
0067         auto message = new Message();
0068         message->setContent(encoded);
0069         message->parse();
0070 
0071         return message;
0072     }
0073 
0074 }
0075 
0076 static KMime::Types::Mailbox::List stripMyAddressesFromAddressList(const KMime::Types::Mailbox::List &list, const KMime::Types::AddrSpecList me)
0077 {
0078     KMime::Types::Mailbox::List addresses(list);
0079     for (KMime::Types::Mailbox::List::Iterator it = addresses.begin(); it != addresses.end();) {
0080         if (me.contains(it->addrSpec())) {
0081             it = addresses.erase(it);
0082         } else {
0083             ++it;
0084         }
0085     }
0086 
0087     return addresses;
0088 }
0089 
0090 static QString toPlainText(const QString &s)
0091 {
0092     QTextDocument doc;
0093     doc.setHtml(s);
0094     return doc.toPlainText();
0095 }
0096 
0097 QString replacePrefixes(const QString &str, const QStringList &prefixRegExps, const QString &newPrefix)
0098 {
0099     // construct a big regexp that
0100     // 1. is anchored to the beginning of str (sans whitespace)
0101     // 2. matches at least one of the part regexps in prefixRegExps
0102     const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:")));
0103     QRegExp rx(bigRegExp, Qt::CaseInsensitive);
0104     if (!rx.isValid()) {
0105         qWarning() << "bigRegExp = \""
0106                     << bigRegExp << "\"\n"
0107                     << "prefix regexp is invalid!";
0108         qWarning() << "Error: " << rx.errorString() << rx;
0109         Q_ASSERT(false);
0110         return str;
0111     }
0112 
0113     QString tmp = str;
0114     //We expect a match at the beginning of the string
0115     if (rx.indexIn(tmp) == 0) {
0116         return tmp.replace(0, rx.matchedLength(), newPrefix + QLatin1String(" "));
0117     }
0118     //No match, we just prefix the newPrefix
0119     return newPrefix + " " + str;
0120 }
0121 
0122 const QStringList getForwardPrefixes()
0123 {
0124     //See https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations
0125     QStringList list;
0126     //We want to be able to potentially reply to a variety of languages, so only translating is not enough
0127     list << QObject::tr("fwd");
0128     list << "fwd";
0129     list << "fw";
0130     list << "wg";
0131     list << "vs";
0132     list << "tr";
0133     list << "rv";
0134     list << "enc";
0135     return list;
0136 }
0137 
0138 
0139 static QString forwardSubject(const QString &s)
0140 {
0141     //The standandard prefix
0142     const auto localPrefix = "FW:";
0143     QStringList forwardPrefixes;
0144     for (const auto &prefix : getForwardPrefixes()) {
0145         forwardPrefixes << prefix + "\\s*:";
0146     }
0147     return replacePrefixes(s, forwardPrefixes, localPrefix);
0148 }
0149 
0150 static QStringList getReplyPrefixes()
0151 {
0152     //See https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations
0153     QStringList list;
0154     //We want to be able to potentially reply to a variety of languages, so only translating is not enough
0155     list << QObject::tr("re");
0156     list << "re";
0157     list << "aw";
0158     list << "sv";
0159     list << "antw";
0160     list << "ref";
0161     return list;
0162 }
0163 
0164 static QString replySubject(const QString &s)
0165 {
0166     //The standandard prefix (latin for "in re", in matter of)
0167     const auto localPrefix = "RE:";
0168     QStringList replyPrefixes;
0169     for (const auto &prefix : getReplyPrefixes()) {
0170         replyPrefixes << prefix + "\\s*:";
0171         replyPrefixes << prefix + "\\[.+\\]:";
0172         replyPrefixes << prefix + "\\d+:";
0173     }
0174     return replacePrefixes(s, replyPrefixes, localPrefix);
0175 }
0176 
0177 static QByteArray getRefStr(const QByteArray &references, const QByteArray &messageId)
0178 {
0179     QByteArray firstRef, lastRef, refStr{references.trimmed()}, retRefStr;
0180     int i, j;
0181 
0182     if (refStr.isEmpty()) {
0183         return messageId;
0184     }
0185 
0186     i = refStr.indexOf('<');
0187     j = refStr.indexOf('>');
0188     firstRef = refStr.mid(i, j - i + 1);
0189     if (!firstRef.isEmpty()) {
0190         retRefStr = firstRef + ' ';
0191     }
0192 
0193     i = refStr.lastIndexOf('<');
0194     j = refStr.lastIndexOf('>');
0195 
0196     lastRef = refStr.mid(i, j - i + 1);
0197     if (!lastRef.isEmpty() && lastRef != firstRef) {
0198         retRefStr += lastRef + ' ';
0199     }
0200 
0201     retRefStr += messageId;
0202     return retRefStr;
0203 }
0204 
0205 KMime::Content *createPlainPartContent(const QString &plainBody, KMime::Content *parent = nullptr)
0206 {
0207     KMime::Content *textPart = new KMime::Content(parent);
0208     textPart->contentType()->setMimeType("text/plain");
0209     //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text
0210     // QTextCodec *charset = selectCharset(m_charsets, plainBody);
0211     // textPart->contentType()->setCharset(charset->name());
0212     textPart->contentType()->setCharset("utf-8");
0213     textPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit);
0214     textPart->fromUnicodeString(plainBody);
0215     return textPart;
0216 }
0217 
0218 KMime::Content *createMultipartAlternativeContent(const QString &plainBody, const QString &htmlBody, KMime::Message *parent = nullptr)
0219 {
0220     KMime::Content *multipartAlternative = new KMime::Content(parent);
0221     multipartAlternative->contentType()->setMimeType("multipart/alternative");
0222     multipartAlternative->contentType()->setBoundary(KMime::multiPartBoundary());
0223 
0224     KMime::Content *textPart = createPlainPartContent(plainBody, multipartAlternative);
0225     multipartAlternative->addContent(textPart);
0226 
0227     KMime::Content *htmlPart = new KMime::Content(multipartAlternative);
0228     htmlPart->contentType()->setMimeType("text/html");
0229     //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text
0230     // QTextCodec *charset = selectCharset(m_charsets, htmlBody);
0231     // htmlPart->contentType()->setCharset(charset->name());
0232     htmlPart->contentType()->setCharset("utf-8");
0233     htmlPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit);
0234     htmlPart->fromUnicodeString(htmlBody);
0235     multipartAlternative->addContent(htmlPart);
0236 
0237     return multipartAlternative;
0238 }
0239 
0240 KMime::Content *createMultipartMixedContent(QVector<KMime::Content *> contents)
0241 {
0242     KMime::Content *multiPartMixed = new KMime::Content();
0243     multiPartMixed->contentType()->setMimeType("multipart/mixed");
0244     multiPartMixed->contentType()->setBoundary(KMime::multiPartBoundary());
0245 
0246     for (const auto &content : contents) {
0247         multiPartMixed->addContent(content);
0248     }
0249 
0250     return multiPartMixed;
0251 }
0252 
0253 QString plainToHtml(const QString &body)
0254 {
0255     QString str = body;
0256     str = str.toHtmlEscaped();
0257     str.replace(QStringLiteral("\n"), QStringLiteral("<br />\n"));
0258     return str;
0259 }
0260 
0261 //TODO implement this function using a DOM tree parser
0262 void makeValidHtml(QString &body, const QString &headElement)
0263 {
0264     QRegExp regEx;
0265     regEx.setMinimal(true);
0266     regEx.setPattern(QStringLiteral("<html.*>"));
0267 
0268     if (!body.isEmpty() && !body.contains(regEx)) {
0269         regEx.setPattern(QStringLiteral("<body.*>"));
0270         if (!body.contains(regEx)) {
0271             body = QLatin1String("<body>") + body + QLatin1String("<br/></body>");
0272         }
0273         regEx.setPattern(QStringLiteral("<head.*>"));
0274         if (!body.contains(regEx)) {
0275             body = QLatin1String("<head>") + headElement + QLatin1String("</head>") + body;
0276         }
0277         body = QLatin1String("<html>") + body + QLatin1String("</html>");
0278     }
0279 }
0280 
0281 //FIXME strip signature works partially for HTML mails
0282 static QString stripSignature(const QString &msg)
0283 {
0284     // Following RFC 3676, only > before --
0285     // I prefer to not delete a SB instead of delete good mail content.
0286     // We expect no CRLF from the ObjectTreeParser. The regex won't handle it.
0287     if (msg.contains("\r\n")) {
0288         qWarning() << "Message contains CRLF, but shouldn't: " << msg;
0289         Q_ASSERT(false);
0290     }
0291     const QRegExp sbDelimiterSearch = QRegExp(QLatin1String("(^|\n)[> ]*-- \n"));
0292     // The regular expression to look for prefix change
0293     const QRegExp commonReplySearch = QRegExp(QLatin1String("^[ ]*>"));
0294 
0295     QString res = msg;
0296     int posDeletingStart = 1; // to start looking at 0
0297 
0298     // While there are SB delimiters (start looking just before the deleted SB)
0299     while ((posDeletingStart = res.indexOf(sbDelimiterSearch, posDeletingStart - 1)) >= 0) {
0300         QString prefix; // the current prefix
0301         QString line; // the line to check if is part of the SB
0302         int posNewLine = -1;
0303 
0304         // Look for the SB beginning
0305         int posSignatureBlock = res.indexOf(QLatin1Char('-'), posDeletingStart);
0306         // The prefix before "-- "$
0307         if (res.at(posDeletingStart) == QLatin1Char('\n')) {
0308             ++posDeletingStart;
0309         }
0310 
0311         prefix = res.mid(posDeletingStart, posSignatureBlock - posDeletingStart);
0312         posNewLine = res.indexOf(QLatin1Char('\n'), posSignatureBlock) + 1;
0313 
0314         // now go to the end of the SB
0315         while (posNewLine < res.size() && posNewLine > 0) {
0316             // handle the undefined case for mid ( x , -n ) where n>1
0317             int nextPosNewLine = res.indexOf(QLatin1Char('\n'), posNewLine);
0318 
0319             if (nextPosNewLine < 0) {
0320                 nextPosNewLine = posNewLine - 1;
0321             }
0322 
0323             line = res.mid(posNewLine, nextPosNewLine - posNewLine);
0324 
0325             // check when the SB ends:
0326             // * does not starts with prefix or
0327             // * starts with prefix+(any substring of prefix)
0328             if ((prefix.isEmpty() && line.indexOf(commonReplySearch) < 0) ||
0329                     (!prefix.isEmpty() && line.startsWith(prefix) &&
0330                      line.mid(prefix.size()).indexOf(commonReplySearch) < 0)) {
0331                 posNewLine = res.indexOf(QLatin1Char('\n'), posNewLine) + 1;
0332             } else {
0333                 break;    // end of the SB
0334             }
0335         }
0336 
0337         // remove the SB or truncate when is the last SB
0338         if (posNewLine > 0) {
0339             res.remove(posDeletingStart, posNewLine - posDeletingStart);
0340         } else {
0341             res.truncate(posDeletingStart);
0342         }
0343     }
0344 
0345     return res;
0346 }
0347 
0348 static void setupPage(QWebEnginePage *page)
0349 {
0350         page->profile()->setHttpCacheType(QWebEngineProfile::MemoryHttpCache);
0351         page->profile()->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies);
0352         page->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, false);
0353         page->settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false);
0354         page->settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, false);
0355         page->settings()->setAttribute(QWebEngineSettings::JavascriptCanAccessClipboard, false);
0356         page->settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, false);
0357         page->settings()->setAttribute(QWebEngineSettings::XSSAuditingEnabled, false);
0358         page->settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false);
0359         page->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, false);
0360         page->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, false);
0361         page->settings()->setAttribute(QWebEngineSettings::HyperlinkAuditingEnabled, false);
0362         page->settings()->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, false);
0363         page->settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, false);
0364         page->settings()->setAttribute(QWebEngineSettings::WebGLEnabled, false);
0365         page->settings()->setAttribute(QWebEngineSettings::AutoLoadIconsForPage, false);
0366         page->settings()->setAttribute(QWebEngineSettings::Accelerated2dCanvasEnabled, false);
0367         page->settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false);
0368         page->settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, false);
0369 }
0370 
0371 static void plainMessageText(const QString &plainTextContent, const QString &htmlContent, const std::function<void(const QString &)> &callback)
0372 {
0373     const auto result = plainTextContent.isEmpty() ? toPlainText(htmlContent) : plainTextContent;
0374     callback(result);
0375 }
0376 
0377 static QString extractHeaderBodyScript()
0378 {
0379     return QStringLiteral("(function() {"
0380                           "var res = {"
0381                           "    body: document.getElementsByTagName('body')[0].innerHTML,"
0382                           "    header: document.getElementsByTagName('head')[0].innerHTML"
0383                           "};"
0384                           "return res;"
0385                           "})()");
0386 }
0387 
0388 void htmlMessageText(const QString &plainTextContent, const QString &htmlContent, const std::function<void(const QString &body, QString &head)> &callback)
0389 {
0390     QString htmlElement = htmlContent;
0391 
0392     if (htmlElement.isEmpty()) {   //plain mails only
0393         QString htmlReplace = plainTextContent.toHtmlEscaped();
0394         htmlReplace = htmlReplace.replace(QStringLiteral("\n"), QStringLiteral("<br />"));
0395         htmlElement = QStringLiteral("<html><head></head><body>%1</body></html>\n").arg(htmlReplace);
0396     }
0397 
0398     auto page = new QWebEnginePage;
0399     setupPage(page);
0400 
0401     page->setHtml(htmlElement);
0402     page->runJavaScript(extractHeaderBodyScript(), QWebEngineScript::ApplicationWorld, [=](const QVariant &result){
0403         page->deleteLater();
0404         const QVariantMap map = result.toMap();
0405         auto bodyElement = map.value(QStringLiteral("body")).toString();
0406         auto headerElement = map.value(QStringLiteral("header")).toString();
0407         if (!bodyElement.isEmpty()) {
0408             return callback(bodyElement, headerElement);
0409         }
0410 
0411         return callback(htmlElement, headerElement);
0412     });
0413 }
0414 
0415 QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString)
0416 {
0417     QString result;
0418 
0419     if (wildString.isEmpty()) {
0420         return wildString;
0421     }
0422 
0423     unsigned int strLength(wildString.length());
0424     for (uint i = 0; i < strLength;) {
0425         QChar ch = wildString[i++];
0426         if (ch == QLatin1Char('%') && i < strLength) {
0427             ch = wildString[i++];
0428             switch (ch.toLatin1()) {
0429             case 'f': { // sender's initals
0430                 if (fromDisplayString.isEmpty()) {
0431                     break;
0432                 }
0433 
0434                 uint j = 0;
0435                 const unsigned int strLength(fromDisplayString.length());
0436                 for (; j < strLength && fromDisplayString[j] > QLatin1Char(' '); ++j)
0437                     ;
0438                 for (; j < strLength && fromDisplayString[j] <= QLatin1Char(' '); ++j)
0439                     ;
0440                 result += fromDisplayString[0];
0441                 if (j < strLength && fromDisplayString[j] > QLatin1Char(' ')) {
0442                     result += fromDisplayString[j];
0443                 } else if (strLength > 1) {
0444                     if (fromDisplayString[1] > QLatin1Char(' ')) {
0445                         result += fromDisplayString[1];
0446                     }
0447                 }
0448             }
0449             break;
0450             case '_':
0451                 result += QLatin1Char(' ');
0452                 break;
0453             case '%':
0454                 result += QLatin1Char('%');
0455                 break;
0456             default:
0457                 result += QLatin1Char('%');
0458                 result += ch;
0459                 break;
0460             }
0461         } else {
0462             result += ch;
0463         }
0464     }
0465     return result;
0466 }
0467 
0468 QString quotedPlainText(const QString &selection, const QString &fromDisplayString)
0469 {
0470     QString content = selection;
0471     // Remove blank lines at the beginning:
0472     const int firstNonWS = content.indexOf(QRegExp(QLatin1String("\\S")));
0473     const int lineStart = content.lastIndexOf(QLatin1Char('\n'), firstNonWS);
0474     if (lineStart >= 0) {
0475         content.remove(0, static_cast<unsigned int>(lineStart));
0476     }
0477 
0478     const auto quoteString = QStringLiteral("> ");
0479     const QString indentStr = formatQuotePrefix(quoteString, fromDisplayString);
0480     //FIXME
0481     // if (TemplateParserSettings::self()->smartQuote() && mWrap) {
0482     //     content = MessageCore::StringUtil::smartQuote(content, mColWrap - indentStr.length());
0483     // }
0484     content.replace(QLatin1Char('\n'), QLatin1Char('\n') + indentStr);
0485     content.prepend(indentStr);
0486     content += QLatin1Char('\n');
0487 
0488     return content;
0489 }
0490 
0491 QString quotedHtmlText(const QString &selection)
0492 {
0493     QString content = selection;
0494     //TODO 1) look for all the variations of <br>  and remove the blank lines
0495     //2) implement vertical bar for quoted HTML mail.
0496     //3) After vertical bar is implemented, If a user wants to edit quoted message,
0497     // then the <blockquote> tags below should open and close as when required.
0498 
0499     //Add blockquote tag, so that quoted message can be differentiated from normal message
0500     content = QLatin1String("<blockquote>") + content + QLatin1String("</blockquote>");
0501     return content;
0502 }
0503 
0504 void applyCharset(const KMime::Message::Ptr msg, const KMime::Message::Ptr &origMsg)
0505 {
0506     // first convert the body from its current encoding to unicode representation
0507     QTextCodec *bodyCodec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset()));
0508     if (!bodyCodec) {
0509         bodyCodec = KCharsets::charsets()->codecForName(QStringLiteral("UTF-8"));
0510     }
0511 
0512     const QString body = bodyCodec->toUnicode(msg->body());
0513 
0514     // then apply the encoding of the original message
0515     msg->contentType()->setCharset(origMsg->contentType()->charset());
0516 
0517     QTextCodec *codec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset()));
0518     if (!codec) {
0519         qCritical() << "Could not get text codec for charset" << msg->contentType()->charset();
0520     } else if (!codec->canEncode(body)) {     // charset can't encode body, fall back to preferred
0521         const QStringList charsets /*= preferredCharsets() */;
0522 
0523         QList<QByteArray> chars;
0524         chars.reserve(charsets.count());
0525         foreach (const QString &charset, charsets) {
0526             chars << charset.toLatin1();
0527         }
0528 
0529         //FIXME
0530         QByteArray fallbackCharset/* = selectCharset(chars, body)*/;
0531         if (fallbackCharset.isEmpty()) { // UTF-8 as fall-through
0532             fallbackCharset = "UTF-8";
0533         }
0534 
0535         codec = KCharsets::charsets()->codecForName(QString::fromLatin1(fallbackCharset));
0536         msg->setBody(codec->fromUnicode(body));
0537     } else {
0538         msg->setBody(codec->fromUnicode(body));
0539     }
0540 }
0541 
0542 enum ReplyStrategy {
0543     ReplyList,
0544     ReplySmart,
0545     ReplyAll,
0546     ReplyAuthor,
0547     ReplyNone
0548 };
0549 
0550 
0551 static QByteArray as7BitString(const KMime::Headers::Base *h)
0552 {
0553     if (h) {
0554         return h->as7BitString(false);
0555     }
0556     return {};
0557 }
0558 
0559 
0560 static QString asUnicodeString(const KMime::Headers::Base *h)
0561 {
0562     if (h) {
0563         return h->asUnicodeString();
0564     }
0565     return {};
0566 }
0567 
0568 static KMime::Types::Mailbox::List getMailingListAddresses(const KMime::Headers::Base *listPostHeader)
0569 {
0570     KMime::Types::Mailbox::List mailingListAddresses;
0571     const QString listPost = asUnicodeString(listPostHeader);
0572     if (listPost.contains(QStringLiteral("mailto:"), Qt::CaseInsensitive)) {
0573         QRegExp rx(QStringLiteral("<mailto:([^@>]+)@([^>]+)>"), Qt::CaseInsensitive);
0574         if (rx.indexIn(listPost, 0) != -1) {   // matched
0575             KMime::Types::Mailbox mailbox;
0576             mailbox.fromUnicodeString(rx.cap(1) + QLatin1Char('@') + rx.cap(2));
0577             mailingListAddresses << mailbox;
0578         }
0579     }
0580     return mailingListAddresses;
0581 }
0582 
0583 struct RecipientMailboxes {
0584     KMime::Types::Mailbox::List to;
0585     KMime::Types::Mailbox::List cc;
0586 };
0587 
0588 static RecipientMailboxes getRecipients(const KMime::Types::Mailbox::List &from, const KMime::Types::Mailbox::List &to, const KMime::Types::Mailbox::List &cc, const KMime::Types::Mailbox::List &replyToList, const KMime::Types::Mailbox::List &mailingListAddresses, const KMime::Types::AddrSpecList &me)
0589 {
0590     KMime::Types::Mailbox::List toList;
0591     KMime::Types::Mailbox::List ccList;
0592     auto listContainsMe = [&] (const KMime::Types::Mailbox::List &list) {
0593         for (const auto &m : me) {
0594             KMime::Types::Mailbox mailbox;
0595             mailbox.setAddress(m);
0596             if (list.contains(mailbox)) {
0597                 return true;
0598             }
0599         }
0600         return false;
0601     };
0602 
0603     if (listContainsMe(from)) {
0604         // sender seems to be one of our own identities, so we assume that this
0605         // is a reply to a "sent" mail where the users wants to add additional
0606         // information for the recipient.
0607         return {to, cc};
0608     }
0609 
0610     KMime::Types::Mailbox::List recipients;
0611     KMime::Types::Mailbox::List ccRecipients;
0612 
0613     // add addresses from the Reply-To header to the list of recipients
0614     if (!replyToList.isEmpty()) {
0615         recipients = replyToList;
0616 
0617         // strip all possible mailing list addresses from the list of Reply-To addresses
0618         foreach (const KMime::Types::Mailbox &mailbox, mailingListAddresses) {
0619             foreach (const KMime::Types::Mailbox &recipient, recipients) {
0620                 if (mailbox == recipient) {
0621                     recipients.removeAll(recipient);
0622                 }
0623             }
0624         }
0625     }
0626 
0627     if (!mailingListAddresses.isEmpty()) {
0628         // this is a mailing list message
0629         if (recipients.isEmpty() && !from.isEmpty()) {
0630             // The sender didn't set a Reply-to address, so we add the From
0631             // address to the list of CC recipients.
0632             ccRecipients += from;
0633             qDebug() << "Added" << from << "to the list of CC recipients";
0634         }
0635 
0636         // if it is a mailing list, add the posting address
0637         recipients.prepend(mailingListAddresses[0]);
0638     } else {
0639         // this is a normal message
0640         if (recipients.isEmpty() && !from.isEmpty()) {
0641             // in case of replying to a normal message only then add the From
0642             // address to the list of recipients if there was no Reply-to address
0643             recipients += from;
0644             qDebug() << "Added" << from << "to the list of recipients";
0645         }
0646     }
0647 
0648     // strip all my addresses from the list of recipients
0649     toList = stripMyAddressesFromAddressList(recipients, me);
0650 
0651     // merge To header and CC header into a list of CC recipients
0652     auto appendToCcRecipients = [&](const KMime::Types::Mailbox::List & list) {
0653         foreach (const KMime::Types::Mailbox &mailbox, list) {
0654             if (!recipients.contains(mailbox) && !ccRecipients.contains(mailbox)) {
0655                 ccRecipients += mailbox;
0656                 qDebug() << "Added" << mailbox.prettyAddress() << "to the list of CC recipients";
0657             }
0658         }
0659     };
0660     appendToCcRecipients(to);
0661     appendToCcRecipients(cc);
0662 
0663     if (!ccRecipients.isEmpty()) {
0664         // strip all my addresses from the list of CC recipients
0665         ccRecipients = stripMyAddressesFromAddressList(ccRecipients, me);
0666 
0667         // in case of a reply to self, toList might be empty. if that's the case
0668         // then propagate a cc recipient to To: (if there is any).
0669         if (toList.isEmpty() && !ccRecipients.isEmpty()) {
0670             toList << ccRecipients.at(0);
0671             ccRecipients.pop_front();
0672         }
0673 
0674         ccList = ccRecipients;
0675     }
0676 
0677     if (toList.isEmpty() && !recipients.isEmpty()) {
0678         // reply to self without other recipients
0679         toList << recipients.at(0);
0680     }
0681 
0682     return {toList, ccList};
0683 }
0684 
0685 void MailTemplates::reply(const KMime::Message::Ptr &origMsg, const std::function<void(const KMime::Message::Ptr &result)> &callback, const KMime::Types::AddrSpecList &me)
0686 {
0687     //FIXME
0688     const bool alwaysPlain = true;
0689 
0690     // Decrypt what we have to
0691     MimeTreeParser::ObjectTreeParser otp;
0692     otp.parseObjectTree(origMsg.data());
0693     otp.decryptAndVerify();
0694 
0695     auto partList = otp.collectContentParts();
0696     if (partList.isEmpty()) {
0697         Q_ASSERT(false);
0698         return;
0699     }
0700     auto part = partList[0];
0701     Q_ASSERT(part);
0702 
0703     // Prepare the reply message
0704     KMime::Message::Ptr msg(new KMime::Message);
0705 
0706     msg->removeHeader<KMime::Headers::To>();
0707     msg->removeHeader<KMime::Headers::Subject>();
0708     msg->contentType(true)->setMimeType("text/plain");
0709     msg->contentType()->setCharset("utf-8");
0710 
0711     auto getMailboxes = [](const KMime::Headers::Base *h) -> KMime::Types::Mailbox::List {
0712         if (h) {
0713             return static_cast<const KMime::Headers::Generics::AddressList*>(h)->mailboxes();
0714         }
0715         return {};
0716     };
0717 
0718     auto fromHeader = static_cast<const KMime::Headers::From*>(part->header(KMime::Headers::From::staticType()));
0719     const auto recipients = getRecipients(
0720         fromHeader ? fromHeader->mailboxes() : KMime::Types::Mailbox::List{},
0721         getMailboxes(part->header(KMime::Headers::To::staticType())),
0722         getMailboxes(part->header(KMime::Headers::Cc::staticType())),
0723         getMailboxes(part->header(KMime::Headers::ReplyTo::staticType())),
0724         getMailingListAddresses(part->header("List-Post")),
0725         me
0726     );
0727     for (const auto &mailbox : recipients.to) {
0728         msg->to()->addAddress(mailbox);
0729     }
0730     for (const auto &mailbox : recipients.cc) {
0731         msg->cc(true)->addAddress(mailbox);
0732     }
0733 
0734     const auto messageId = as7BitString(part->header(KMime::Headers::MessageID::staticType()));
0735 
0736     const QByteArray refStr = getRefStr(as7BitString(part->header(KMime::Headers::References::staticType())), messageId);
0737     if (!refStr.isEmpty()) {
0738         msg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8");
0739     }
0740 
0741     //In-Reply-To = original msg-id
0742     msg->inReplyTo()->from7BitString(messageId);
0743 
0744 
0745     const auto subjectHeader = part->header(KMime::Headers::Subject::staticType());
0746     msg->subject()->fromUnicodeString(replySubject(asUnicodeString(subjectHeader)), "utf-8");
0747 
0748     auto definedLocale = QLocale::system();
0749 
0750     //Add quoted body
0751     QString plainBody;
0752     QString htmlBody;
0753 
0754     //On $datetime you wrote:
0755     auto dateHeader = static_cast<const KMime::Headers::Date*>(part->header(KMime::Headers::Date::staticType()));
0756     const QDateTime date = dateHeader ? dateHeader->dateTime() : QDateTime{};
0757     const auto dateTimeString = QString("%1 %2").arg(definedLocale.toString(date.date(), QLocale::LongFormat)).arg(definedLocale.toString(date.time(), QLocale::LongFormat));
0758     const auto onDateYouWroteLine = QString("On %1 you wrote:\n").arg(dateTimeString);
0759     plainBody.append(onDateYouWroteLine);
0760     htmlBody.append(plainToHtml(onDateYouWroteLine));
0761 
0762     const auto plainTextContent = otp.plainTextContent();
0763     const auto htmlContent = otp.htmlContent();
0764 
0765     plainMessageText(plainTextContent, htmlContent, [=] (const QString &body) {
0766         QString result = stripSignature(body);
0767         //Quoted body
0768         result = quotedPlainText(result,  fromHeader ? fromHeader->displayString() : QString{});
0769         if (result.endsWith(QLatin1Char('\n'))) {
0770             result.chop(1);
0771         }
0772         //The plain body is complete
0773         auto plainBodyResult = plainBody + result;
0774         htmlMessageText(plainTextContent, htmlContent, [=] (const QString &body, const QString &headElement) {
0775             QString result = stripSignature(body);
0776 
0777             //The html body is complete
0778             const auto htmlBodyResult = [&]() {
0779                 if (!alwaysPlain) {
0780                     auto htmlBodyResult = htmlBody + quotedHtmlText(result);
0781                     makeValidHtml(htmlBodyResult, headElement);
0782                     return htmlBodyResult;
0783                 }
0784                 return QString{};
0785             }();
0786 
0787             //Assemble the message
0788             msg->contentType()->clear(); // to get rid of old boundary
0789 
0790             KMime::Content *const mainTextPart =
0791                 htmlBodyResult.isEmpty() ?
0792                 createPlainPartContent(plainBodyResult, msg.data()) :
0793                 createMultipartAlternativeContent(plainBodyResult, htmlBodyResult, msg.data());
0794             mainTextPart->assemble();
0795 
0796             msg->setBody(mainTextPart->encodedBody());
0797             msg->setHeader(mainTextPart->contentType());
0798             msg->setHeader(mainTextPart->contentTransferEncoding());
0799             //FIXME this does more harm than good right now.
0800             // applyCharset(msg, origMsg);
0801             msg->assemble();
0802 
0803             callback(msg);
0804         });
0805     });
0806 }
0807 
0808 void MailTemplates::forward(const KMime::Message::Ptr &origMsg,
0809     const std::function<void(const KMime::Message::Ptr &result)> &callback)
0810 {
0811     MimeTreeParser::ObjectTreeParser otp;
0812     otp.parseObjectTree(origMsg.data());
0813     otp.decryptAndVerify();
0814 
0815 
0816     KMime::Message::Ptr wrapperMsg(new KMime::Message);
0817     wrapperMsg->to()->clear();
0818     wrapperMsg->cc()->clear();
0819 
0820     // Decrypt the original message, it will be encrypted again in the composer
0821     // for the right recipient
0822     KMime::Message::Ptr forwardedMessage(new KMime::Message());
0823 
0824     if (isEncrypted(origMsg.data())) {
0825         qDebug() << "Original message was encrypted, decrypting it";
0826 
0827         auto htmlContent = otp.htmlContent();
0828 
0829         KMime::Content *recreatedMsg =
0830             htmlContent.isEmpty() ? createPlainPartContent(otp.plainTextContent()) :
0831                                     createMultipartAlternativeContent(otp.plainTextContent(), htmlContent);
0832 
0833         KMime::Message::Ptr tmpForwardedMessage;
0834         auto attachments = otp.collectAttachmentParts();
0835         if (!attachments.isEmpty()) {
0836             QVector<KMime::Content *> contents = {recreatedMsg};
0837             for (const auto &attachment : attachments) {
0838                 //Copy the node, to avoid deleting the parts node.
0839                 auto c = new KMime::Content;
0840                 c->setContent(attachment->node()->encodedContent());
0841                 c->parse();
0842                 contents.append(c);
0843             }
0844 
0845             auto msg = createMultipartMixedContent(contents);
0846 
0847             tmpForwardedMessage.reset(KMime::contentToMessage(msg));
0848         } else {
0849             tmpForwardedMessage.reset(KMime::contentToMessage(recreatedMsg));
0850         }
0851 
0852         origMsg->contentType()->fromUnicodeString(tmpForwardedMessage->contentType()->asUnicodeString(), "utf-8");
0853         origMsg->assemble();
0854         forwardedMessage->setHead(origMsg->head());
0855         forwardedMessage->setBody(tmpForwardedMessage->encodedBody());
0856         forwardedMessage->parse();
0857     } else {
0858         qDebug() << "Original message was not encrypted, using it as-is";
0859         forwardedMessage = origMsg;
0860     }
0861 
0862     auto partList = otp.collectContentParts();
0863     if (partList.isEmpty()) {
0864         Q_ASSERT(false);
0865         callback({});
0866         return;
0867     }
0868     auto part = partList[0];
0869     Q_ASSERT(part);
0870 
0871     const auto subjectHeader = part->header(KMime::Headers::Subject::staticType());
0872     const auto subject = asUnicodeString(subjectHeader);
0873 
0874     const QByteArray refStr = getRefStr(
0875         as7BitString(part->header(KMime::Headers::References::staticType())),
0876         as7BitString(part->header(KMime::Headers::MessageID::staticType()))
0877     );
0878 
0879     wrapperMsg->subject()->fromUnicodeString(forwardSubject(subject), "utf-8");
0880 
0881     if (!refStr.isEmpty()) {
0882         wrapperMsg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8");
0883     }
0884 
0885     KMime::Content *fwdAttachment = new KMime::Content;
0886 
0887     fwdAttachment->contentDisposition()->setDisposition(KMime::Headers::CDinline);
0888     fwdAttachment->contentType()->setMimeType("message/rfc822");
0889     fwdAttachment->contentDisposition()->setFilename(subject + ".eml");
0890     fwdAttachment->setBody(KMime::CRLFtoLF(forwardedMessage->encodedContent(false)));
0891 
0892     wrapperMsg->addContent(fwdAttachment);
0893     wrapperMsg->assemble();
0894 
0895     callback(wrapperMsg);
0896 }
0897 
0898 QString MailTemplates::plaintextContent(const KMime::Message::Ptr &msg)
0899 {
0900     MimeTreeParser::ObjectTreeParser otp;
0901     otp.parseObjectTree(msg.data());
0902     otp.decryptAndVerify();
0903     const auto plain = otp.plainTextContent();
0904     if (plain.isEmpty()) {
0905         //Maybe not as good as the webengine version, but works at least for simple html content
0906         return toPlainText(otp.htmlContent());
0907     }
0908     return plain;
0909 }
0910 
0911 QString MailTemplates::body(const KMime::Message::Ptr &msg, bool &isHtml)
0912 {
0913     MimeTreeParser::ObjectTreeParser otp;
0914     otp.parseObjectTree(msg.data());
0915     otp.decryptAndVerify();
0916     const auto html = otp.htmlContent();
0917     if (html.isEmpty()) {
0918         isHtml = false;
0919         return otp.plainTextContent();
0920     }
0921     isHtml = true;
0922     return html;
0923 }
0924 
0925 static KMime::Content *createAttachmentPart(const QByteArray &content, const QString &filename, bool isInline, const QByteArray &mimeType, const QString &name, bool base64Encode = true)
0926 {
0927 
0928     KMime::Content *part = new KMime::Content;
0929     part->contentDisposition(true)->setFilename(filename);
0930     if (isInline) {
0931         part->contentDisposition(true)->setDisposition(KMime::Headers::CDinline);
0932     } else {
0933         part->contentDisposition(true)->setDisposition(KMime::Headers::CDattachment);
0934     }
0935 
0936     part->contentType(true)->setMimeType(mimeType);
0937     if (!name.isEmpty()) {
0938         part->contentType(true)->setName(name, "utf-8");
0939     }
0940     if(base64Encode) {
0941         part->contentTransferEncoding(true)->setEncoding(KMime::Headers::CEbase64);
0942     }
0943     part->setBody(content);
0944     return part;
0945 }
0946 
0947 static KMime::Content *createBodyPart(const QString &body, bool htmlBody) {
0948     if (htmlBody) {
0949         return createMultipartAlternativeContent(toPlainText(body), body);
0950     }
0951     return createPlainPartContent(body);
0952 }
0953 
0954 static KMime::Types::Mailbox::List stringListToMailboxes(const QStringList &list)
0955 {
0956     KMime::Types::Mailbox::List mailboxes;
0957     for (const auto &s : list) {
0958         KMime::Types::Mailbox mb;
0959         mb.fromUnicodeString(s);
0960         if (mb.hasAddress()) {
0961             mailboxes << mb;
0962         } else {
0963             qWarning() << "Got an invalid address: " << s << list;
0964             Q_ASSERT(false);
0965         }
0966     }
0967     return mailboxes;
0968 }
0969 
0970 
0971 static void setRecipients(KMime::Message &message, const Recipients &recipients)
0972 {
0973     message.to(true)->clear();
0974     for (const auto &mb : stringListToMailboxes(recipients.to)) {
0975         message.to()->addAddress(mb);
0976     }
0977     message.cc(true)->clear();
0978     for (const auto &mb : stringListToMailboxes(recipients.cc)) {
0979         message.cc()->addAddress(mb);
0980     }
0981     message.bcc(true)->clear();
0982     for (const auto &mb : stringListToMailboxes(recipients.bcc)) {
0983         message.bcc()->addAddress(mb);
0984     }
0985 }
0986 
0987 
0988 KMime::Message::Ptr MailTemplates::createMessage(KMime::Message::Ptr existingMessage,
0989     const QStringList &to, const QStringList &cc, const QStringList &bcc,
0990     const KMime::Types::Mailbox &from, const QString &subject, const QString &body, bool htmlBody,
0991     const QList<Attachment> &attachments, const std::vector<Crypto::Key> &signingKeys,
0992     const std::vector<Crypto::Key> &encryptionKeys, const Crypto::Key &attachedKey)
0993 {
0994     auto mail = existingMessage;
0995     if (!mail) {
0996         mail = KMime::Message::Ptr::create();
0997     } else {
0998         //Content type is part of the body part we're creating
0999         mail->removeHeader<KMime::Headers::ContentType>();
1000         mail->removeHeader<KMime::Headers::ContentTransferEncoding>();
1001     }
1002 
1003     mail->date()->setDateTime(QDateTime::currentDateTime());
1004     mail->userAgent()->fromUnicodeString(QString("%1/%2(%3)").arg(QString::fromLocal8Bit("Kube")).arg("0.1").arg(QSysInfo::prettyProductName()), "utf-8");
1005 
1006     setRecipients(*mail, {to, cc, bcc});
1007 
1008     mail->from(true)->clear();
1009     mail->from(true)->addAddress(from);
1010 
1011     mail->subject(true)->fromUnicodeString(subject, "utf-8");
1012     if (!mail->messageID(false)) {
1013         //A globally unique messageId that doesn't leak the local hostname
1014         const auto messageId = "<" + QUuid::createUuid().toString().mid(1, 36).remove('-') + "@kube>";
1015         mail->messageID(true)->fromUnicodeString(messageId, "utf-8");
1016     }
1017     if (!mail->date(true)->dateTime().isValid()) {
1018         mail->date(true)->setDateTime(QDateTime::currentDateTimeUtc());
1019     }
1020     mail->assemble();
1021 
1022     const bool encryptionRequired = !signingKeys.empty() || !encryptionKeys.empty();
1023     //We always attach the key when encryption is enabled.
1024     const bool attachingPersonalKey = encryptionRequired;
1025 
1026     auto allAttachments = attachments;
1027     if (attachingPersonalKey) {
1028         const auto publicKeyExportResult = Crypto::exportPublicKey(attachedKey);
1029         if (!publicKeyExportResult) {
1030             qWarning() << "Failed to export public key" << publicKeyExportResult.error();
1031             return {};
1032         }
1033         const auto publicKeyData = publicKeyExportResult.value();
1034         allAttachments << Attachment{
1035             {},
1036             QString("0x%1.asc").arg(QString{attachedKey.shortKeyId}),
1037             "application/pgp-keys",
1038             false,
1039             publicKeyData
1040         };
1041     }
1042 
1043     std::unique_ptr<KMime::Content> bodyPart{[&] {
1044         if (!allAttachments.isEmpty()) {
1045             auto bodyPart = new KMime::Content;
1046             bodyPart->contentType(true)->setMimeType("multipart/mixed");
1047             bodyPart->contentType()->setBoundary(KMime::multiPartBoundary());
1048             bodyPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit);
1049             bodyPart->setPreamble("This is a multi-part message in MIME format.\n");
1050             bodyPart->addContent(createBodyPart(body, htmlBody));
1051             for (const auto &attachment : allAttachments) {
1052 
1053                 // Just always encode attachments base64 so it's safe for binary data,
1054                 // except when it's another message or an ascii armored key
1055                 static QSet<QString> noEncodingRequired{{"message/rfc822"}, {"application/pgp-keys"}};
1056                 const bool base64Encode = !noEncodingRequired.contains(attachment.mimeType);
1057                 bodyPart->addContent(createAttachmentPart(attachment.data, attachment.filename, attachment.isInline, attachment.mimeType, attachment.name, base64Encode));
1058             }
1059             return bodyPart;
1060         } else {
1061             return createBodyPart(body, htmlBody);
1062         }
1063     }()};
1064     bodyPart->assemble();
1065 
1066     const QByteArray bodyData = [&] {
1067         if (encryptionRequired) {
1068             auto result = MailCrypto::processCrypto(std::move(bodyPart), signingKeys, encryptionKeys);
1069             if (!result) {
1070                 qWarning() << "Crypto failed" << result.error();
1071                 return QByteArray{};
1072             }
1073             result.value()->assemble();
1074             return result.value()->encodedContent();
1075         } else {
1076             if (!bodyPart->contentType(false)) {
1077                 bodyPart->contentType(true)->setMimeType("text/plain");
1078                 bodyPart->assemble();
1079             }
1080             return bodyPart->encodedContent();
1081         }
1082     }();
1083     if (bodyData.isEmpty()) {
1084         return {};
1085     }
1086 
1087     KMime::Message::Ptr resultMessage(new KMime::Message);
1088     resultMessage->setContent(mail->head() + bodyData);
1089     resultMessage->parse(); // Not strictly necessary.
1090     return resultMessage;
1091 }
1092 
1093 
1094 KMime::Message::Ptr MailTemplates::createIMipMessage(
1095     const QString &from,
1096     const Recipients &recipients,
1097     const QString &subject,
1098     const QString &body,
1099     const QString &attachment)
1100 {
1101     KMime::Message::Ptr message = KMime::Message::Ptr( new KMime::Message );
1102     message->contentTransferEncoding()->clear();  // 7Bit, decoded.
1103 
1104     // Set the headers
1105     message->userAgent()->fromUnicodeString(QString("%1/%2(%3)").arg(QString::fromLocal8Bit("Kube")).arg("0.1").arg(QSysInfo::prettyProductName()), "utf-8");
1106     message->from()->fromUnicodeString(from, "utf-8");
1107 
1108     setRecipients(*message, recipients);
1109 
1110     message->date()->setDateTime(QDateTime::currentDateTime());
1111     message->subject()->fromUnicodeString(subject, "utf-8");
1112     message->contentType()->setMimeType("multipart/alternative");
1113     message->contentType()->setBoundary(KMime::multiPartBoundary());
1114 
1115     // Set the first multipart, the body message.
1116     KMime::Content *bodyMessage = new KMime::Content{message.data()};
1117     bodyMessage->contentType()->setMimeType("text/plain");
1118     bodyMessage->contentType()->setCharset("utf-8");
1119     bodyMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr);
1120     bodyMessage->setBody(KMime::CRLFtoLF(body.toUtf8()));
1121     message->addContent( bodyMessage );
1122 
1123     // Set the second multipart, the attachment.
1124     KMime::Content *attachMessage = new KMime::Content{message.data()};
1125     attachMessage->contentDisposition()->setDisposition(KMime::Headers::CDattachment);
1126     attachMessage->contentType()->setMimeType("text/calendar");
1127     attachMessage->contentType()->setCharset("utf-8");
1128     attachMessage->contentType()->setName(QLatin1String("event.ics"), "utf-8");
1129     attachMessage->contentType()->setParameter(QLatin1String("method"), QLatin1String("REPLY"));
1130     attachMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr);
1131     attachMessage->setBody(KMime::CRLFtoLF(attachment.toUtf8()));
1132     message->addContent(attachMessage);
1133 
1134     // Job done, attach the both multiparts and assemble the message.
1135     message->assemble();
1136     return message;
1137 }