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 }