1 /*
2   SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
3   SPDX-FileCopyrightText: 2010 Leo Franchi <lfranchi@kde.org>
4   SPDX-FileCopyrightText: 2017-2021 Laurent Montel <montel@kde.org>
5 
6   SPDX-License-Identifier: GPL-2.0-or-later
7 */
8 
9 #include "messagefactoryng.h"
10 
11 #include "messagefactoryforwardjob.h"
12 #include "messagefactoryreplyjob.h"
13 #include "settings/messagecomposersettings.h"
14 #include "draftstatus/draftstatus.h"
15 #include <MessageComposer/Util>
16 
17 #include <KCursorSaver>
18 
19 #include <KIdentityManagement/Identity>
20 #include <KIdentityManagement/IdentityManager>
21 
22 #include "helper/messagehelper.h"
23 #include "messagecomposer_debug.h"
24 #include <KCharsets>
25 #include <KEmailAddress>
26 #include <KLocalizedString>
27 #include <KMime/DateFormatter>
28 #include <MessageCore/MailingList>
29 #include <MessageCore/StringUtil>
30 #include <MessageCore/Util>
31 #include <QRegularExpression>
32 #include <QTextCodec>
33 
34 using namespace MessageComposer;
35 
36 namespace KMime
37 {
38 namespace Types
39 {
operator ==(const KMime::Types::Mailbox & left,const KMime::Types::Mailbox & right)40 static bool operator==(const KMime::Types::Mailbox &left, const KMime::Types::Mailbox &right)
41 {
42     return left.addrSpec().asString() == right.addrSpec().asString();
43 }
44 }
45 }
46 
47 /**
48  * Strips all the user's addresses from an address list. This is used
49  * when replying.
50  */
stripMyAddressesFromAddressList(const KMime::Types::Mailbox::List & list,const KIdentityManagement::IdentityManager * manager)51 static KMime::Types::Mailbox::List stripMyAddressesFromAddressList(const KMime::Types::Mailbox::List &list, const KIdentityManagement::IdentityManager *manager)
52 {
53     KMime::Types::Mailbox::List addresses(list);
54     for (KMime::Types::Mailbox::List::Iterator it = addresses.begin(); it != addresses.end();) {
55         if (manager->thatIsMe(it->prettyAddress())) {
56             it = addresses.erase(it);
57         } else {
58             ++it;
59         }
60     }
61 
62     return addresses;
63 }
64 
MessageFactoryNG(const KMime::Message::Ptr & origMsg,Akonadi::Item::Id id,const Akonadi::Collection & col,QObject * parent)65 MessageFactoryNG::MessageFactoryNG(const KMime::Message::Ptr &origMsg, Akonadi::Item::Id id, const Akonadi::Collection &col, QObject *parent)
66     : QObject(parent)
67     , mOrigMsg(origMsg)
68     , mFolderId(0)
69     , mParentFolderId(0)
70     , mCollection(col)
71     , mReplyStrategy(MessageComposer::ReplySmart)
72     , mId(id)
73 {
74 }
75 
~MessageFactoryNG()76 MessageFactoryNG::~MessageFactoryNG()
77 {
78 }
79 
80 // Return the addresses to use when replying to the author of msg.
81 // See <https://cr.yp.to/proto/replyto.html>.
authorMailboxes(const KMime::Message::Ptr & msg,const KMime::Types::Mailbox::List & mailingLists)82 static KMime::Types::Mailbox::List authorMailboxes(const KMime::Message::Ptr &msg, const KMime::Types::Mailbox::List &mailingLists)
83 {
84     if (auto mrt = msg->headerByType("Mail-Reply-To")) {
85         return KMime::Types::Mailbox::listFrom7BitString(mrt->as7BitString(false));
86     }
87     if (auto rt = msg->replyTo(false)) {
88         // Did a mailing list munge Reply-To?
89         auto mboxes = rt->mailboxes();
90         for (const auto &list : mailingLists) {
91             mboxes.removeAll(list);
92         }
93         if (!mboxes.isEmpty()) {
94             return mboxes;
95         }
96     }
97     return msg->from(true)->mailboxes();
98 }
99 
slotCreateReplyDone(const KMime::Message::Ptr & msg,bool replyAll)100 void MessageFactoryNG::slotCreateReplyDone(const KMime::Message::Ptr &msg, bool replyAll)
101 {
102     applyCharset(msg);
103 
104     MessageComposer::Util::addLinkInformation(msg, mId, Akonadi::MessageStatus::statusReplied());
105     if (mParentFolderId > 0) {
106         auto header = new KMime::Headers::Generic("X-KMail-Fcc");
107         header->fromUnicodeString(QString::number(mParentFolderId), "utf-8");
108         msg->setHeader(header);
109     }
110 
111     if (DraftEncryptionState(mOrigMsg).encryptionState()) {
112         DraftEncryptionState(msg).setState(true);
113     }
114     msg->assemble();
115 
116     MessageReply reply;
117     reply.msg = msg;
118     reply.replyAll = replyAll;
119     Q_EMIT createReplyDone(reply);
120 }
121 
createReplyAsync()122 void MessageFactoryNG::createReplyAsync()
123 {
124     KMime::Message::Ptr msg(new KMime::Message);
125     QByteArray refStr;
126     bool replyAll = true;
127     KMime::Types::Mailbox::List toList;
128     KMime::Types::Mailbox::List replyToList;
129 
130     const uint originalIdentity = identityUoid(mOrigMsg);
131     MessageHelper::initFromMessage(msg, mOrigMsg, mIdentityManager, originalIdentity);
132     replyToList = mOrigMsg->replyTo()->mailboxes();
133 
134     msg->contentType()->setCharset("utf-8");
135 
136     if (auto hdr = mOrigMsg->headerByType("List-Post")) {
137         static const QRegularExpression rx{QStringLiteral("<\\s*mailto\\s*:([^@>]+@[^>]+)>"), QRegularExpression::CaseInsensitiveOption};
138         const auto match = rx.match(hdr->asUnicodeString());
139         if (match.hasMatch()) {
140             KMime::Types::Mailbox mailbox;
141             mailbox.fromUnicodeString(match.captured(1));
142             mMailingListAddresses << mailbox;
143         }
144     }
145 
146     switch (mReplyStrategy) {
147     case MessageComposer::ReplySmart: {
148         if (auto hdr = mOrigMsg->headerByType("Mail-Followup-To")) {
149             toList << KMime::Types::Mailbox::listFrom7BitString(hdr->as7BitString(false));
150         } else if (!mMailingListAddresses.isEmpty()) {
151             if (replyToList.isEmpty()) {
152                 toList = (KMime::Types::Mailbox::List() << mMailingListAddresses.at(0));
153             } else {
154                 toList = replyToList;
155             }
156         } else {
157             // Doesn't seem to be a mailing list.
158             auto originalFromList = mOrigMsg->from()->mailboxes();
159             auto originalToList = mOrigMsg->to()->mailboxes();
160 
161             if (mIdentityManager->thatIsMe(KMime::Types::Mailbox::listToUnicodeString(originalFromList))
162                 && !mIdentityManager->thatIsMe(KMime::Types::Mailbox::listToUnicodeString(originalToList))) {
163                 // Sender seems to be one of our own identities and recipient is not,
164                 // so we assume that this is a reply to a "sent" mail where the user
165                 // wants to add additional information for the recipient.
166                 toList = originalToList;
167             } else {
168                 // "Normal" case:  reply to sender.
169                 toList = authorMailboxes(mOrigMsg, mMailingListAddresses);
170             }
171 
172             replyAll = false;
173         }
174         // strip all my addresses from the list of recipients
175         const KMime::Types::Mailbox::List recipients = toList;
176 
177         toList = stripMyAddressesFromAddressList(recipients, mIdentityManager);
178 
179         // ... unless the list contains only my addresses (reply to self)
180         if (toList.isEmpty() && !recipients.isEmpty()) {
181             toList << recipients.first();
182         }
183         break;
184     }
185     case MessageComposer::ReplyList: {
186         if (auto hdr = mOrigMsg->headerByType("Mail-Followup-To")) {
187             KMime::Types::Mailbox mailbox;
188             mailbox.from7BitString(hdr->as7BitString(false));
189             toList << mailbox;
190         } else if (!mMailingListAddresses.isEmpty()) {
191             toList << mMailingListAddresses[0];
192         } else if (!replyToList.isEmpty()) {
193             // assume a Reply-To header mangling mailing list
194             toList = replyToList;
195         }
196 
197         // strip all my addresses from the list of recipients
198         const KMime::Types::Mailbox::List recipients = toList;
199 
200         toList = stripMyAddressesFromAddressList(recipients, mIdentityManager);
201         break;
202     }
203     case MessageComposer::ReplyAll:
204         if (auto hdr = mOrigMsg->headerByType("Mail-Followup-To")) {
205             toList = KMime::Types::Mailbox::listFrom7BitString(hdr->as7BitString(false));
206         } else {
207             auto ccList = stripMyAddressesFromAddressList(mOrigMsg->cc()->mailboxes(), mIdentityManager);
208 
209             if (!mMailingListAddresses.isEmpty()) {
210                 toList = stripMyAddressesFromAddressList(mOrigMsg->to()->mailboxes(), mIdentityManager);
211                 bool addMailingList = true;
212                 for (const KMime::Types::Mailbox &m : std::as_const(mMailingListAddresses)) {
213                     if (toList.contains(m)) {
214                         addMailingList = false;
215                         break;
216                     }
217                 }
218                 if (addMailingList) {
219                     toList += mMailingListAddresses.front();
220                 }
221 
222                 ccList += authorMailboxes(mOrigMsg, mMailingListAddresses);
223             } else {
224                 // Doesn't seem to be a mailing list.
225                 auto originalFromList = mOrigMsg->from()->mailboxes();
226                 auto originalToList = mOrigMsg->to()->mailboxes();
227 
228                 if (mIdentityManager->thatIsMe(KMime::Types::Mailbox::listToUnicodeString(originalFromList))
229                     && !mIdentityManager->thatIsMe(KMime::Types::Mailbox::listToUnicodeString(originalToList))) {
230                     // Sender seems to be one of our own identities and recipient is not,
231                     // so we assume that this is a reply to a "sent" mail where the user
232                     // wants to add additional information for the recipient.
233                     toList = originalToList;
234                 } else {
235                     // "Normal" case:  reply to sender.
236                     toList = stripMyAddressesFromAddressList(mOrigMsg->to()->mailboxes(), mIdentityManager);
237                     toList += authorMailboxes(mOrigMsg, mMailingListAddresses);
238                 }
239             }
240 
241             for (const KMime::Types::Mailbox &mailbox : std::as_const(ccList)) {
242                 msg->cc()->addAddress(mailbox);
243             }
244         }
245         break;
246     case MessageComposer::ReplyAuthor:
247         toList = authorMailboxes(mOrigMsg, mMailingListAddresses);
248         replyAll = false;
249         break;
250     case MessageComposer::ReplyNone:
251         // the addressees will be set by the caller
252         break;
253     default:
254         Q_UNREACHABLE();
255     }
256 
257     for (const KMime::Types::Mailbox &mailbox : std::as_const(toList)) {
258         msg->to()->addAddress(mailbox);
259     }
260 
261     refStr = getRefStr(mOrigMsg);
262     if (!refStr.isEmpty()) {
263         msg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8");
264     }
265     // In-Reply-To = original msg-id
266     msg->inReplyTo()->from7BitString(mOrigMsg->messageID()->as7BitString(false));
267 
268     msg->subject()->fromUnicodeString(MessageCore::StringUtil::replySubject(mOrigMsg.data()), "utf-8");
269 
270     // If the reply shouldn't be blank, apply the template to the message
271     if (mQuote) {
272         auto job = new MessageFactoryReplyJob;
273         connect(job, &MessageFactoryReplyJob::replyDone, this, &MessageFactoryNG::slotCreateReplyDone);
274         job->setMsg(msg);
275         job->setReplyAll(replyAll);
276         job->setIdentityManager(mIdentityManager);
277         job->setSelection(mSelection);
278         job->setTemplate(mTemplate);
279         job->setOrigMsg(mOrigMsg);
280         job->setCollection(mCollection);
281         job->setReplyAsHtml(mReplyAsHtml);
282         job->start();
283     } else {
284         slotCreateReplyDone(msg, replyAll);
285     }
286 }
287 
slotCreateForwardDone(const KMime::Message::Ptr & msg)288 void MessageFactoryNG::slotCreateForwardDone(const KMime::Message::Ptr &msg)
289 {
290     applyCharset(msg);
291 
292     MessageComposer::Util::addLinkInformation(msg, mId, Akonadi::MessageStatus::statusForwarded());
293     msg->assemble();
294     Q_EMIT createForwardDone(msg);
295 }
296 
createForwardAsync()297 void MessageFactoryNG::createForwardAsync()
298 {
299     KMime::Message::Ptr msg(new KMime::Message);
300 
301     // This is a non-multipart, non-text mail (e.g. text/calendar). Construct
302     // a multipart/mixed mail and add the original body as an attachment.
303     if (!mOrigMsg->contentType()->isMultipart()
304         && (!mOrigMsg->contentType(false)->isText()
305             || (mOrigMsg->contentType(false)->isText() && mOrigMsg->contentType(false)->subType() != "html"
306                 && mOrigMsg->contentType(false)->subType() != "plain"))) {
307         const uint originalIdentity = identityUoid(mOrigMsg);
308         MessageHelper::initFromMessage(msg, mOrigMsg, mIdentityManager, originalIdentity);
309         msg->removeHeader<KMime::Headers::ContentType>();
310         msg->removeHeader<KMime::Headers::ContentTransferEncoding>();
311 
312         msg->contentType(true)->setMimeType("multipart/mixed");
313 
314         // TODO: Andras: somebody should check if this is correct. :)
315         // empty text part
316         auto msgPart = new KMime::Content;
317         msgPart->contentType()->setMimeType("text/plain");
318         msg->addContent(msgPart);
319 
320         // the old contents of the mail
321         auto secondPart = new KMime::Content;
322         secondPart->contentType()->setMimeType(mOrigMsg->contentType()->mimeType());
323         secondPart->setBody(mOrigMsg->body());
324         // use the headers of the original mail
325         secondPart->setHead(mOrigMsg->head());
326         msg->addContent(secondPart);
327         msg->assemble();
328     }
329     // Normal message (multipart or text/plain|html)
330     // Just copy the message, the template parser will do the hard work of
331     // replacing the body text in TemplateParser::addProcessedBodyToMessage()
332     else {
333         // TODO Check if this is ok
334         msg->setHead(mOrigMsg->head());
335         msg->setBody(mOrigMsg->body());
336         const QString oldContentType = msg->contentType(true)->asUnicodeString();
337         const uint originalIdentity = identityUoid(mOrigMsg);
338         MessageHelper::initFromMessage(msg, mOrigMsg, mIdentityManager, originalIdentity);
339 
340         // restore the content type, MessageHelper::initFromMessage() sets the contents type to
341         // text/plain, via initHeader(), for unclear reasons
342         msg->contentType()->fromUnicodeString(oldContentType, "utf-8");
343         msg->assemble();
344     }
345 
346     msg->subject()->fromUnicodeString(MessageCore::StringUtil::forwardSubject(mOrigMsg.data()), "utf-8");
347     auto job = new MessageFactoryForwardJob;
348     connect(job, &MessageFactoryForwardJob::forwardDone, this, &MessageFactoryNG::slotCreateForwardDone);
349     job->setIdentityManager(mIdentityManager);
350     job->setMsg(msg);
351     job->setSelection(mSelection);
352     job->setTemplate(mTemplate);
353     job->setOrigMsg(mOrigMsg);
354     job->setCollection(mCollection);
355     job->start();
356 }
357 
createAttachedForward(const Akonadi::Item::List & items)358 QPair<KMime::Message::Ptr, QVector<KMime::Content *>> MessageFactoryNG::createAttachedForward(const Akonadi::Item::List &items)
359 {
360     // create forwarded message with original message as attachment
361     // remove headers that shouldn't be forwarded
362     KMime::Message::Ptr msg(new KMime::Message);
363     QVector<KMime::Content *> attachments;
364 
365     const int numberOfItems(items.count());
366     if (numberOfItems >= 2) {
367         // don't respect X-KMail-Identity headers because they might differ for
368         // the selected mails
369         MessageHelper::initHeader(msg, mIdentityManager, 0);
370     } else if (numberOfItems == 1) {
371         KMime::Message::Ptr firstMsg = MessageComposer::Util::message(items.first());
372         const uint originalIdentity = identityUoid(firstMsg);
373         MessageHelper::initFromMessage(msg, firstMsg, mIdentityManager, originalIdentity);
374         msg->subject()->fromUnicodeString(MessageCore::StringUtil::forwardSubject(firstMsg.data()), "utf-8");
375     }
376 
377     MessageHelper::setAutomaticFields(msg, true);
378     KCursorSaver saver(Qt::WaitCursor);
379     if (numberOfItems == 0) {
380         attachments << createForwardAttachmentMessage(mOrigMsg);
381         MessageComposer::Util::addLinkInformation(msg, mId, Akonadi::MessageStatus::statusForwarded());
382     } else {
383         // iterate through all the messages to be forwarded
384         attachments.reserve(items.count());
385         for (const Akonadi::Item &item : std::as_const(items)) {
386             attachments << createForwardAttachmentMessage(MessageComposer::Util::message(item));
387             MessageComposer::Util::addLinkInformation(msg, item.id(), Akonadi::MessageStatus::statusForwarded());
388         }
389     }
390 
391     applyCharset(msg);
392 
393     // msg->assemble();
394     return QPair<KMime::Message::Ptr, QVector<KMime::Content *>>(msg, QVector<KMime::Content *>() << attachments);
395 }
396 
createForwardAttachmentMessage(const KMime::Message::Ptr & fwdMsg)397 KMime::Content *MessageFactoryNG::createForwardAttachmentMessage(const KMime::Message::Ptr &fwdMsg)
398 {
399     // remove headers that shouldn't be forwarded
400     MessageCore::StringUtil::removePrivateHeaderFields(fwdMsg);
401     fwdMsg->removeHeader<KMime::Headers::Bcc>();
402     fwdMsg->assemble();
403     // set the part
404     auto msgPart = new KMime::Content(fwdMsg.data());
405     auto ct = msgPart->contentType();
406     ct->setMimeType("message/rfc822");
407 
408     auto cd = msgPart->contentDisposition(); // create
409     cd->setParameter(QStringLiteral("filename"), i18n("forwarded message"));
410     cd->setDisposition(KMime::Headers::CDinline);
411     const QString subject = fwdMsg->subject()->asUnicodeString();
412     ct->setParameter(QStringLiteral("name"), subject);
413     cd->fromUnicodeString(fwdMsg->from()->asUnicodeString() + QLatin1String(": ") + subject, "utf-8");
414     msgPart->setBody(fwdMsg->encodedContent());
415     msgPart->assemble();
416 
417     MessageComposer::Util::addLinkInformation(fwdMsg, 0, Akonadi::MessageStatus::statusForwarded());
418     return msgPart;
419 }
420 
replyAsHtml() const421 bool MessageFactoryNG::replyAsHtml() const
422 {
423     return mReplyAsHtml;
424 }
425 
setReplyAsHtml(bool replyAsHtml)426 void MessageFactoryNG::setReplyAsHtml(bool replyAsHtml)
427 {
428     mReplyAsHtml = replyAsHtml;
429 }
430 
createResend()431 KMime::Message::Ptr MessageFactoryNG::createResend()
432 {
433     KMime::Message::Ptr msg(new KMime::Message);
434     msg->setContent(mOrigMsg->encodedContent());
435     msg->parse();
436     msg->removeHeader<KMime::Headers::MessageID>();
437     uint originalIdentity = identityUoid(mOrigMsg);
438 
439     // Set the identity from above
440     auto header = new KMime::Headers::Generic("X-KMail-Identity");
441     header->fromUnicodeString(QString::number(originalIdentity), "utf-8");
442     msg->setHeader(header);
443 
444     // Restore the original bcc field as this is overwritten in applyIdentity
445     msg->bcc(mOrigMsg->bcc());
446     return msg;
447 }
448 
449 KMime::Message::Ptr
createRedirect(const QString & toStr,const QString & ccStr,const QString & bccStr,int transportId,const QString & fcc,int identity)450 MessageFactoryNG::createRedirect(const QString &toStr, const QString &ccStr, const QString &bccStr, int transportId, const QString &fcc, int identity)
451 {
452     if (!mOrigMsg) {
453         return KMime::Message::Ptr();
454     }
455 
456     // copy the message 1:1
457     KMime::Message::Ptr msg(new KMime::Message);
458     msg->setContent(mOrigMsg->encodedContent());
459     msg->parse();
460 
461     uint id = identity;
462     if (identity == -1) {
463         if (auto hrd = msg->headerByType("X-KMail-Identity")) {
464             const QString strId = hrd->asUnicodeString().trimmed();
465             if (!strId.isEmpty()) {
466                 id = strId.toUInt();
467             }
468         }
469     }
470     const KIdentityManagement::Identity &ident = mIdentityManager->identityForUoidOrDefault(id);
471 
472     // X-KMail-Redirect-From: content
473     const QString strByWayOf =
474         QString::fromLocal8Bit("%1 (by way of %2 <%3>)").arg(mOrigMsg->from()->asUnicodeString(), ident.fullName(), ident.primaryEmailAddress());
475 
476     // Resent-From: content
477     const QString strFrom = QString::fromLocal8Bit("%1 <%2>").arg(ident.fullName(), ident.primaryEmailAddress());
478 
479     // format the current date to be used in Resent-Date:
480     // FIXME: generate datetime the same way as KMime, otherwise we get inconsistency
481     // in unit-tests. Unfortunately RFC2822Date is not enough for us, we need the
482     // composition hack below
483     const QDateTime dt = QDateTime::currentDateTime();
484     const QString newDate = QLocale::c().toString(dt, QStringLiteral("ddd, ")) + dt.toString(Qt::RFC2822Date);
485 
486     // Clean up any resent headers
487     msg->removeHeader("Resent-Cc");
488     msg->removeHeader("Resent-Bcc");
489     msg->removeHeader("Resent-Sender");
490     // date, from to and id will be set anyway
491 
492     // prepend Resent-*: headers (c.f. RFC2822 3.6.6)
493     QString msgIdSuffix;
494     if (MessageComposer::MessageComposerSettings::useCustomMessageIdSuffix()) {
495         msgIdSuffix = MessageComposer::MessageComposerSettings::customMsgIDSuffix();
496     }
497     auto header = new KMime::Headers::Generic("Resent-Message-ID");
498     header->fromUnicodeString(MessageCore::StringUtil::generateMessageId(msg->sender()->asUnicodeString(), msgIdSuffix), "utf-8");
499     msg->setHeader(header);
500 
501     header = new KMime::Headers::Generic("Resent-Date");
502     header->fromUnicodeString(newDate, "utf-8");
503     msg->setHeader(header);
504 
505     header = new KMime::Headers::Generic("Resent-From");
506     header->fromUnicodeString(strFrom, "utf-8");
507     msg->setHeader(header);
508 
509     if (msg->to(false)) {
510         auto headerT = new KMime::Headers::To;
511         headerT->fromUnicodeString(mOrigMsg->to()->asUnicodeString(), "utf-8");
512         msg->setHeader(headerT);
513     }
514 
515     header = new KMime::Headers::Generic("Resent-To");
516     header->fromUnicodeString(toStr, "utf-8");
517     msg->setHeader(header);
518 
519     if (!ccStr.isEmpty()) {
520         header = new KMime::Headers::Generic("Resent-Cc");
521         header->fromUnicodeString(ccStr, "utf-8");
522         msg->setHeader(header);
523     }
524 
525     if (!bccStr.isEmpty()) {
526         header = new KMime::Headers::Generic("Resent-Bcc");
527         header->fromUnicodeString(bccStr, "utf-8");
528         msg->setHeader(header);
529     }
530 
531     header = new KMime::Headers::Generic("X-KMail-Redirect-From");
532     header->fromUnicodeString(strByWayOf, "utf-8");
533     msg->setHeader(header);
534 
535     if (transportId != -1) {
536         header = new KMime::Headers::Generic("X-KMail-Transport");
537         header->fromUnicodeString(QString::number(transportId), "utf-8");
538         msg->setHeader(header);
539     }
540 
541     if (!fcc.isEmpty()) {
542         header = new KMime::Headers::Generic("X-KMail-Fcc");
543         header->fromUnicodeString(fcc, "utf-8");
544         msg->setHeader(header);
545     }
546 
547     const bool fccIsDisabled = ident.disabledFcc();
548     if (fccIsDisabled) {
549         header = new KMime::Headers::Generic("X-KMail-FccDisabled");
550         header->fromUnicodeString(QStringLiteral("true"), "utf-8");
551         msg->setHeader(header);
552     } else {
553         msg->removeHeader("X-KMail-FccDisabled");
554     }
555 
556     msg->assemble();
557 
558     MessageComposer::Util::addLinkInformation(msg, mId, Akonadi::MessageStatus::statusForwarded());
559     return msg;
560 }
561 
createDeliveryReceipt()562 KMime::Message::Ptr MessageFactoryNG::createDeliveryReceipt()
563 {
564     QString receiptTo;
565     if (auto hrd = mOrigMsg->headerByType("Disposition-Notification-To")) {
566         receiptTo = hrd->asUnicodeString();
567     }
568     if (receiptTo.trimmed().isEmpty()) {
569         return KMime::Message::Ptr();
570     }
571     receiptTo.remove(QChar::fromLatin1('\n'));
572 
573     KMime::Message::Ptr receipt(new KMime::Message);
574     const uint originalIdentity = identityUoid(mOrigMsg);
575     MessageHelper::initFromMessage(receipt, mOrigMsg, mIdentityManager, originalIdentity);
576     receipt->to()->fromUnicodeString(receiptTo, QStringLiteral("utf-8").toLatin1());
577     receipt->subject()->fromUnicodeString(i18n("Receipt: ") + mOrigMsg->subject()->asUnicodeString(), "utf-8");
578 
579     QString str = QStringLiteral("Your message was successfully delivered.");
580     str += QLatin1String("\n\n---------- Message header follows ----------\n");
581     str += QString::fromLatin1(mOrigMsg->head());
582     str += QLatin1String("--------------------------------------------\n");
583     // Conversion to toLatin1 is correct here as Mail headers should contain
584     // ascii only
585     receipt->setBody(str.toLatin1());
586     MessageHelper::setAutomaticFields(receipt);
587     receipt->assemble();
588 
589     return receipt;
590 }
591 
createMDN(KMime::MDN::ActionMode a,KMime::MDN::DispositionType d,KMime::MDN::SendingMode s,int mdnQuoteOriginal,const QVector<KMime::MDN::DispositionModifier> & m)592 KMime::Message::Ptr MessageFactoryNG::createMDN(KMime::MDN::ActionMode a,
593                                                 KMime::MDN::DispositionType d,
594                                                 KMime::MDN::SendingMode s,
595                                                 int mdnQuoteOriginal,
596                                                 const QVector<KMime::MDN::DispositionModifier> &m)
597 {
598     // extract where to send to:
599     QString receiptTo;
600     if (auto hrd = mOrigMsg->headerByType("Disposition-Notification-To")) {
601         receiptTo = hrd->asUnicodeString();
602     }
603     if (receiptTo.trimmed().isEmpty()) {
604         return KMime::Message::Ptr(new KMime::Message);
605     }
606     receiptTo.remove(QChar::fromLatin1('\n'));
607 
608     QString special; // fill in case of error, warning or failure
609 
610     // extract where to send from:
611     QString finalRecipient = mIdentityManager->identityForUoidOrDefault(identityUoid(mOrigMsg)).fullEmailAddr();
612 
613     //
614     // Generate message:
615     //
616 
617     KMime::Message::Ptr receipt(new KMime::Message());
618     const uint originalIdentity = identityUoid(mOrigMsg);
619     MessageHelper::initFromMessage(receipt, mOrigMsg, mIdentityManager, originalIdentity);
620     auto contentType = receipt->contentType(true); // create it
621     contentType->from7BitString("multipart/report");
622     contentType->setBoundary(KMime::multiPartBoundary());
623     contentType->setCharset("us-ascii");
624     receipt->removeHeader<KMime::Headers::ContentTransferEncoding>();
625     // Modify the ContentType directly (replaces setAutomaticFields(true))
626     contentType->setParameter(QStringLiteral("report-type"), QStringLiteral("disposition-notification"));
627 
628     const QString description = replaceHeadersInString(mOrigMsg, KMime::MDN::descriptionFor(d, m));
629 
630     // text/plain part:
631     auto firstMsgPart = new KMime::Content(mOrigMsg.data());
632     auto firstMsgPartContentType = firstMsgPart->contentType(); // create it
633     firstMsgPartContentType->setMimeType("text/plain");
634     firstMsgPartContentType->setCharset("utf-8");
635     firstMsgPart->contentTransferEncoding(true)->setEncoding(KMime::Headers::CE7Bit);
636     firstMsgPart->setBody(description.toUtf8());
637     receipt->addContent(firstMsgPart);
638 
639     // message/disposition-notification part:
640     auto secondMsgPart = new KMime::Content(mOrigMsg.data());
641     secondMsgPart->contentType()->setMimeType("message/disposition-notification");
642 
643     secondMsgPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit);
644     QByteArray originalRecipient = "";
645     if (auto hrd = mOrigMsg->headerByType("Original-Recipient")) {
646         originalRecipient = hrd->as7BitString(false);
647     }
648     secondMsgPart->setBody(KMime::MDN::dispositionNotificationBodyContent(finalRecipient,
649                                                                           originalRecipient,
650                                                                           mOrigMsg->messageID()->as7BitString(false), /* Message-ID */
651                                                                           d,
652                                                                           a,
653                                                                           s,
654                                                                           m,
655                                                                           special));
656     receipt->addContent(secondMsgPart);
657 
658     if ((mdnQuoteOriginal < 0) || (mdnQuoteOriginal > 2)) {
659         mdnQuoteOriginal = 0;
660     }
661     /* 0=> Nothing, 1=>Full Message, 2=>HeadersOnly*/
662 
663     auto thirdMsgPart = new KMime::Content(mOrigMsg.data());
664     switch (mdnQuoteOriginal) {
665     case 1:
666         thirdMsgPart->contentType()->setMimeType("message/rfc822");
667         thirdMsgPart->setBody(MessageCore::StringUtil::asSendableString(mOrigMsg));
668         receipt->addContent(thirdMsgPart);
669         break;
670     case 2:
671         thirdMsgPart->contentType()->setMimeType("text/rfc822-headers");
672         thirdMsgPart->setBody(MessageCore::StringUtil::headerAsSendableString(mOrigMsg));
673         receipt->addContent(thirdMsgPart);
674         break;
675     case 0:
676     default:
677         delete thirdMsgPart;
678         break;
679     }
680 
681     receipt->to()->fromUnicodeString(receiptTo, "utf-8");
682     // Laurent: We don't translate subject ?
683     receipt->subject()->from7BitString("Message Disposition Notification");
684     auto header = new KMime::Headers::InReplyTo;
685     header->fromUnicodeString(mOrigMsg->messageID()->asUnicodeString(), "utf-8");
686     receipt->setHeader(header);
687 
688     receipt->references()->from7BitString(getRefStr(mOrigMsg));
689 
690     receipt->assemble();
691 
692     qCDebug(MESSAGECOMPOSER_LOG) << "final message:" + receipt->encodedContent();
693 
694     receipt->assemble();
695     return receipt;
696 }
697 
createForwardDigestMIME(const Akonadi::Item::List & items)698 QPair<KMime::Message::Ptr, KMime::Content *> MessageFactoryNG::createForwardDigestMIME(const Akonadi::Item::List &items)
699 {
700     KMime::Message::Ptr msg(new KMime::Message);
701     auto digest = new KMime::Content(msg.data());
702 
703     const QString mainPartText = i18n(
704         "\nThis is a MIME digest forward. The content of the"
705         " message is contained in the attachment(s).\n\n\n");
706 
707     auto ct = digest->contentType();
708     ct->setMimeType("multipart/digest");
709     ct->setBoundary(KMime::multiPartBoundary());
710     digest->contentDescription()->fromUnicodeString(QStringLiteral("Digest of %1 messages.").arg(items.count()), "utf8");
711     digest->contentDisposition()->setFilename(QStringLiteral("digest"));
712     digest->fromUnicodeString(mainPartText);
713 
714     int id = 0;
715     for (const Akonadi::Item &item : std::as_const(items)) {
716         KMime::Message::Ptr fMsg = MessageComposer::Util::message(item);
717         if (id == 0) {
718             if (auto hrd = fMsg->headerByType("X-KMail-Identity")) {
719                 id = hrd->asUnicodeString().toInt();
720             }
721         }
722 
723         MessageCore::StringUtil::removePrivateHeaderFields(fMsg);
724         fMsg->removeHeader<KMime::Headers::Bcc>();
725         fMsg->assemble();
726         auto part = new KMime::Content(digest);
727 
728         part->contentType()->setMimeType("message/rfc822");
729         part->contentType(false)->setCharset(fMsg->contentType()->charset());
730         part->contentID()->setIdentifier(fMsg->contentID()->identifier());
731         part->contentDescription()->fromUnicodeString(fMsg->contentDescription()->asUnicodeString(), "utf8");
732         part->contentDisposition()->setParameter(QStringLiteral("name"), i18n("forwarded message"));
733         part->fromUnicodeString(QString::fromLatin1(fMsg->encodedContent()));
734         part->assemble();
735         MessageComposer::Util::addLinkInformation(msg, item.id(), Akonadi::MessageStatus::statusForwarded());
736         digest->addContent(part);
737     }
738     digest->assemble();
739 
740     id = mFolderId;
741     MessageHelper::initHeader(msg, mIdentityManager, id);
742 
743     //   qCDebug(MESSAGECOMPOSER_LOG) << "digest:" << digest->contents().size() << digest->encodedContent();
744 
745     return QPair<KMime::Message::Ptr, KMime::Content *>(msg, digest);
746 }
747 
setIdentityManager(KIdentityManagement::IdentityManager * ident)748 void MessageFactoryNG::setIdentityManager(KIdentityManagement::IdentityManager *ident)
749 {
750     mIdentityManager = ident;
751 }
752 
setReplyStrategy(MessageComposer::ReplyStrategy replyStrategy)753 void MessageFactoryNG::setReplyStrategy(MessageComposer::ReplyStrategy replyStrategy)
754 {
755     mReplyStrategy = replyStrategy;
756 }
757 
setSelection(const QString & selection)758 void MessageFactoryNG::setSelection(const QString &selection)
759 {
760     mSelection = selection;
761 }
762 
setQuote(bool quote)763 void MessageFactoryNG::setQuote(bool quote)
764 {
765     mQuote = quote;
766 }
767 
setTemplate(const QString & templ)768 void MessageFactoryNG::setTemplate(const QString &templ)
769 {
770     mTemplate = templ;
771 }
772 
setMailingListAddresses(const KMime::Types::Mailbox::List & listAddresses)773 void MessageFactoryNG::setMailingListAddresses(const KMime::Types::Mailbox::List &listAddresses)
774 {
775     mMailingListAddresses << listAddresses;
776 }
777 
setFolderIdentity(uint folderIdentityId)778 void MessageFactoryNG::setFolderIdentity(uint folderIdentityId)
779 {
780     mFolderId = folderIdentityId;
781 }
782 
putRepliesInSameFolder(Akonadi::Collection::Id parentColId)783 void MessageFactoryNG::putRepliesInSameFolder(Akonadi::Collection::Id parentColId)
784 {
785     mParentFolderId = parentColId;
786 }
787 
MDNRequested(const KMime::Message::Ptr & msg)788 bool MessageFactoryNG::MDNRequested(const KMime::Message::Ptr &msg)
789 {
790     // extract where to send to:
791     QString receiptTo;
792     if (auto hrd = msg->headerByType("Disposition-Notification-To")) {
793         receiptTo = hrd->asUnicodeString();
794     }
795     if (receiptTo.trimmed().isEmpty()) {
796         return false;
797     }
798     receiptTo.remove(QChar::fromLatin1('\n'));
799     return !receiptTo.isEmpty();
800 }
801 
MDNConfirmMultipleRecipients(const KMime::Message::Ptr & msg)802 bool MessageFactoryNG::MDNConfirmMultipleRecipients(const KMime::Message::Ptr &msg)
803 {
804     // extract where to send to:
805     QString receiptTo;
806     if (auto hrd = msg->headerByType("Disposition-Notification-To")) {
807         receiptTo = hrd->asUnicodeString();
808     }
809     if (receiptTo.trimmed().isEmpty()) {
810         return false;
811     }
812     receiptTo.remove(QChar::fromLatin1('\n'));
813 
814     // RFC 2298: [ Confirmation from the user SHOULD be obtained (or no
815     // MDN sent) ] if there is more than one distinct address in the
816     // Disposition-Notification-To header.
817     qCDebug(MESSAGECOMPOSER_LOG) << "KEmailAddress::splitAddressList(receiptTo):" << KEmailAddress::splitAddressList(receiptTo).join(QLatin1Char('\n'));
818 
819     return KEmailAddress::splitAddressList(receiptTo).count() > 1;
820 }
821 
MDNReturnPathEmpty(const KMime::Message::Ptr & msg)822 bool MessageFactoryNG::MDNReturnPathEmpty(const KMime::Message::Ptr &msg)
823 {
824     // extract where to send to:
825     QString receiptTo;
826     if (auto hrd = msg->headerByType("Disposition-Notification-To")) {
827         receiptTo = hrd->asUnicodeString();
828     }
829     if (receiptTo.trimmed().isEmpty()) {
830         return false;
831     }
832     receiptTo.remove(QChar::fromLatin1('\n'));
833 
834     // RFC 2298: MDNs SHOULD NOT be sent automatically if the address in
835     // the Disposition-Notification-To header differs from the address
836     // in the Return-Path header. [...] Confirmation from the user
837     // SHOULD be obtained (or no MDN sent) if there is no Return-Path
838     // header in the message [...]
839     KMime::Types::AddrSpecList returnPathList = MessageHelper::extractAddrSpecs(msg, "Return-Path");
840     const QString returnPath = returnPathList.isEmpty() ? QString() : returnPathList.front().localPart + QChar::fromLatin1('@') + returnPathList.front().domain;
841     qCDebug(MESSAGECOMPOSER_LOG) << "clean return path:" << returnPath;
842     return returnPath.isEmpty();
843 }
844 
MDNReturnPathNotInRecieptTo(const KMime::Message::Ptr & msg)845 bool MessageFactoryNG::MDNReturnPathNotInRecieptTo(const KMime::Message::Ptr &msg)
846 {
847     // extract where to send to:
848     QString receiptTo;
849     if (auto hrd = msg->headerByType("Disposition-Notification-To")) {
850         receiptTo = hrd->asUnicodeString();
851     }
852     if (receiptTo.trimmed().isEmpty()) {
853         return false;
854     }
855     receiptTo.remove(QChar::fromLatin1('\n'));
856 
857     // RFC 2298: MDNs SHOULD NOT be sent automatically if the address in
858     // the Disposition-Notification-To header differs from the address
859     // in the Return-Path header. [...] Confirmation from the user
860     // SHOULD be obtained (or no MDN sent) if there is no Return-Path
861     // header in the message [...]
862     KMime::Types::AddrSpecList returnPathList = MessageHelper::extractAddrSpecs(msg, QStringLiteral("Return-Path").toLatin1());
863     const QString returnPath = returnPathList.isEmpty() ? QString() : returnPathList.front().localPart + QChar::fromLatin1('@') + returnPathList.front().domain;
864     qCDebug(MESSAGECOMPOSER_LOG) << "clean return path:" << returnPath;
865     return !receiptTo.contains(returnPath, Qt::CaseSensitive);
866 }
867 
MDNMDNUnknownOption(const KMime::Message::Ptr & msg)868 bool MessageFactoryNG::MDNMDNUnknownOption(const KMime::Message::Ptr &msg)
869 {
870     // RFC 2298: An importance of "required" indicates that
871     // interpretation of the parameter is necessary for proper
872     // generation of an MDN in response to this request.  If a UA does
873     // not understand the meaning of the parameter, it MUST NOT generate
874     // an MDN with any disposition type other than "failed" in response
875     // to the request.
876     QString notificationOptions;
877     if (auto hrd = msg->headerByType("Disposition-Notification-Options")) {
878         notificationOptions = hrd->asUnicodeString();
879     }
880     if (notificationOptions.contains(QLatin1String("required"), Qt::CaseSensitive)) {
881         // ### hacky; should parse...
882         // There is a required option that we don't understand. We need to
883         // ask the user what we should do:
884         return true;
885     }
886     return false;
887 }
888 
identityUoid(const KMime::Message::Ptr & msg)889 uint MessageFactoryNG::identityUoid(const KMime::Message::Ptr &msg)
890 {
891     QString idString;
892     if (auto hdr = msg->headerByType("X-KMail-Identity")) {
893         idString = hdr->asUnicodeString().trimmed();
894     }
895     bool ok = false;
896     uint id = idString.toUInt(&ok);
897 
898     if (!ok || id == 0) {
899         id = MessageCore::Util::identityForMessage(msg.data(), mIdentityManager, mFolderId).uoid();
900     }
901     return id;
902 }
903 
replaceHeadersInString(const KMime::Message::Ptr & msg,const QString & s)904 QString MessageFactoryNG::replaceHeadersInString(const KMime::Message::Ptr &msg, const QString &s)
905 {
906     QString result = s;
907     static QRegularExpression rx{QStringLiteral("\\$\\{([a-z0-9-]+)\\}"), QRegularExpression::CaseInsensitiveOption};
908 
909     const QString sDate = KMime::DateFormatter::formatDate(KMime::DateFormatter::Localized, msg->date()->dateTime().toSecsSinceEpoch());
910     qCDebug(MESSAGECOMPOSER_LOG) << "creating mdn date:" << msg->date()->dateTime().toSecsSinceEpoch() << sDate;
911 
912     result.replace(QStringLiteral("${date}"), sDate);
913 
914     int idx = 0;
915     for (auto match = rx.match(result); match.hasMatch(); match = rx.match(result, idx)) {
916         idx = match.capturedStart(0);
917         const QByteArray ba = match.captured(1).toLatin1();
918         if (auto hdr = msg->headerByType(ba.constData())) {
919             const auto replacement = hdr->asUnicodeString();
920             result.replace(idx, match.capturedLength(0), replacement);
921             idx += replacement.length();
922         } else {
923             result.remove(idx, match.capturedLength(0));
924         }
925     }
926     return result;
927 }
928 
applyCharset(const KMime::Message::Ptr msg)929 void MessageFactoryNG::applyCharset(const KMime::Message::Ptr msg)
930 {
931     if (MessageComposer::MessageComposerSettings::forceReplyCharset()) {
932         // first convert the body from its current encoding to unicode representation
933         QTextCodec *bodyCodec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset()));
934         if (!bodyCodec) {
935             bodyCodec = KCharsets::charsets()->codecForName(QStringLiteral("UTF-8"));
936         }
937 
938         const QString body = bodyCodec->toUnicode(msg->body());
939 
940         // then apply the encoding of the original message
941         msg->contentType()->setCharset(mOrigMsg->contentType()->charset());
942 
943         QTextCodec *codec = KCharsets::charsets()->codecForName(QString::fromLatin1(msg->contentType()->charset()));
944         if (!codec) {
945             qCCritical(MESSAGECOMPOSER_LOG) << "Could not get text codec for charset" << msg->contentType()->charset();
946         } else if (!codec->canEncode(body)) { // charset can't encode body, fall back to preferred
947             const QStringList charsets = MessageComposer::MessageComposerSettings::preferredCharsets();
948 
949             QVector<QByteArray> chars;
950             chars.reserve(charsets.count());
951             for (const QString &charset : charsets) {
952                 chars << charset.toLatin1();
953             }
954 
955             QByteArray fallbackCharset = MessageComposer::Util::selectCharset(chars, body);
956             if (fallbackCharset.isEmpty()) { // UTF-8 as fall-through
957                 fallbackCharset = "UTF-8";
958             }
959 
960             codec = KCharsets::charsets()->codecForName(QString::fromLatin1(fallbackCharset));
961             msg->setBody(codec->fromUnicode(body));
962         } else {
963             msg->setBody(codec->fromUnicode(body));
964         }
965     }
966 }
967 
getRefStr(const KMime::Message::Ptr & msg)968 QByteArray MessageFactoryNG::getRefStr(const KMime::Message::Ptr &msg)
969 {
970     QByteArray firstRef;
971     QByteArray lastRef;
972     QByteArray refStr;
973     QByteArray retRefStr;
974     int i;
975     int j;
976 
977     if (auto hdr = msg->references(false)) {
978         refStr = hdr->as7BitString(false).trimmed();
979     }
980 
981     if (refStr.isEmpty()) {
982         return msg->messageID()->as7BitString(false);
983     }
984 
985     i = refStr.indexOf('<');
986     j = refStr.indexOf('>');
987     firstRef = refStr.mid(i, j - i + 1);
988     if (!firstRef.isEmpty()) {
989         retRefStr = firstRef + ' ';
990     }
991 
992     i = refStr.lastIndexOf('<');
993     j = refStr.lastIndexOf('>');
994 
995     lastRef = refStr.mid(i, j - i + 1);
996     if (!lastRef.isEmpty() && lastRef != firstRef) {
997         retRefStr += lastRef + ' ';
998     }
999 
1000     retRefStr += msg->messageID()->as7BitString(false);
1001     return retRefStr;
1002 }
1003