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