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 }