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