Messagelib

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

KDE's Doxygen guidelines are available online.