Messagelib

messagefactoryng.cpp
1 /*
2  SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, [email protected]
3  SPDX-FileCopyrightText: 2010 Leo Franchi <[email protected]>
4  SPDX-FileCopyrightText: 2017-2023 Laurent Montel <[email protected]>
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 <KMime/DateFormatter>
27 #include <MessageCore/MailingList>
28 #include <MessageCore/StringUtil>
29 #include <MessageCore/Util>
30 #include <QRegularExpression>
31 #include <QStringDecoder>
32 #include <QStringEncoder>
33 
34 using namespace MessageComposer;
35 
36 namespace KMime
37 {
38 namespace Types
39 {
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  */
51 static 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 
66 MessageFactoryNG::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 
77 MessageFactoryNG::~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>.
81 static 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 
99 void MessageFactoryNG::slotCreateReplyDone(const KMime::Message::Ptr &msg, bool replyAll)
100 {
101  applyCharset(msg);
102 
103  MessageComposer::Util::addLinkInformation(msg, mId, Akonadi::MessageStatus::statusReplied());
104  if (mParentFolderId > 0) {
105  auto header = new KMime::Headers::Generic("X-KMail-Fcc");
106  header->fromUnicodeString(QString::number(mParentFolderId), "utf-8");
107  msg->setHeader(header);
108  }
109 
110  if (DraftEncryptionState(mOrigMsg).encryptionState()) {
111  DraftEncryptionState(msg).setState(true);
112  }
113  msg->assemble();
114 
115  MessageReply reply;
116  reply.msg = msg;
117  reply.replyAll = replyAll;
118  Q_EMIT createReplyDone(reply);
119 }
120 
121 void MessageFactoryNG::createReplyAsync()
122 {
124  QByteArray refStr;
125  bool replyAll = true;
126  KMime::Types::Mailbox::List toList;
127  KMime::Types::Mailbox::List replyToList;
128 
129  const uint originalIdentity = identityUoid(mOrigMsg);
130  MessageHelper::initFromMessage(msg, mOrigMsg, mIdentityManager, originalIdentity);
131  replyToList = mOrigMsg->replyTo()->mailboxes();
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 (!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), "utf-8");
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()), "utf-8");
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 
289 void MessageFactoryNG::slotCreateForwardDone(const KMime::Message::Ptr &msg)
290 {
291  applyCharset(msg);
292 
293  MessageComposer::Util::addLinkInformation(msg, mId, Akonadi::MessageStatus::statusForwarded());
294  msg->assemble();
295  Q_EMIT createForwardDone(msg);
296 }
297 
298 void MessageFactoryNG::createForwardAsync()
299 {
301 
302  // This is a non-multipart, non-text mail (e.g. text/calendar). Construct
303  // a multipart/mixed mail and add the original body as an attachment.
304  if (!mOrigMsg->contentType()->isMultipart()
305  && (!mOrigMsg->contentType(false)->isText()
306  || (mOrigMsg->contentType(false)->isText() && mOrigMsg->contentType(false)->subType() != "html"
307  && mOrigMsg->contentType(false)->subType() != "plain"))) {
308  const uint originalIdentity = identityUoid(mOrigMsg);
309  MessageHelper::initFromMessage(msg, mOrigMsg, mIdentityManager, originalIdentity);
310  msg->removeHeader<KMime::Headers::ContentType>();
311  msg->removeHeader<KMime::Headers::ContentTransferEncoding>();
312 
313  msg->contentType(true)->setMimeType("multipart/mixed");
314 
315  // TODO: Andras: somebody should check if this is correct. :)
316  // empty text part
317  auto msgPart = new KMime::Content;
318  msgPart->contentType()->setMimeType("text/plain");
319  msg->addContent(msgPart);
320 
321  // the old contents of the mail
322  auto secondPart = new KMime::Content;
323  secondPart->contentType()->setMimeType(mOrigMsg->contentType()->mimeType());
324  secondPart->setBody(mOrigMsg->body());
325  // use the headers of the original mail
326  secondPart->setHead(mOrigMsg->head());
327  msg->addContent(secondPart);
328  msg->assemble();
329  }
330  // Normal message (multipart or text/plain|html)
331  // Just copy the message, the template parser will do the hard work of
332  // replacing the body text in TemplateParser::addProcessedBodyToMessage()
333  else {
334  // TODO Check if this is ok
335  msg->setHead(mOrigMsg->head());
336  msg->setBody(mOrigMsg->body());
337  const QString oldContentType = msg->contentType(true)->asUnicodeString();
338  const uint originalIdentity = identityUoid(mOrigMsg);
339  MessageHelper::initFromMessage(msg, mOrigMsg, mIdentityManager, originalIdentity);
340 
341  // restore the content type, MessageHelper::initFromMessage() sets the contents type to
342  // text/plain, via initHeader(), for unclear reasons
343  msg->contentType()->fromUnicodeString(oldContentType, "utf-8");
344  msg->assemble();
345  }
346 
347  msg->subject()->fromUnicodeString(MessageCore::StringUtil::forwardSubject(mOrigMsg.data()), "utf-8");
348  auto job = new MessageFactoryForwardJob;
349  connect(job, &MessageFactoryForwardJob::forwardDone, this, &MessageFactoryNG::slotCreateForwardDone);
350  job->setIdentityManager(mIdentityManager);
351  job->setMsg(msg);
352  job->setSelection(mSelection);
353  job->setTemplate(mTemplate);
354  job->setOrigMsg(mOrigMsg);
355  job->setCollection(mCollection);
356  job->start();
357 }
358 
359 QPair<KMime::Message::Ptr, QList<KMime::Content *>> MessageFactoryNG::createAttachedForward(const Akonadi::Item::List &items)
360 {
361  // create forwarded message with original message as attachment
362  // remove headers that shouldn't be forwarded
364  QList<KMime::Content *> attachments;
365 
366  const int numberOfItems(items.count());
367  if (numberOfItems >= 2) {
368  // don't respect X-KMail-Identity headers because they might differ for
369  // the selected mails
370  MessageHelper::initHeader(msg, mIdentityManager, 0);
371  } else if (numberOfItems == 1) {
373  const uint originalIdentity = identityUoid(firstMsg);
374  MessageHelper::initFromMessage(msg, firstMsg, mIdentityManager, originalIdentity);
375  msg->subject()->fromUnicodeString(MessageCore::StringUtil::forwardSubject(firstMsg.data()), "utf-8");
376  }
377 
380  if (numberOfItems == 0) {
381  attachments << createForwardAttachmentMessage(mOrigMsg);
382  MessageComposer::Util::addLinkInformation(msg, mId, Akonadi::MessageStatus::statusForwarded());
383  } else {
384  // iterate through all the messages to be forwarded
385  attachments.reserve(items.count());
386  for (const Akonadi::Item &item : std::as_const(items)) {
387  attachments << createForwardAttachmentMessage(MessageComposer::Util::message(item));
388  MessageComposer::Util::addLinkInformation(msg, item.id(), Akonadi::MessageStatus::statusForwarded());
389  }
390  }
391 
392  applyCharset(msg);
393 
394  // msg->assemble();
396 }
397 
398 KMime::Content *MessageFactoryNG::createForwardAttachmentMessage(const KMime::Message::Ptr &fwdMsg)
399 {
400  // remove headers that shouldn't be forwarded
402  fwdMsg->removeHeader<KMime::Headers::Bcc>();
403  fwdMsg->assemble();
404  // set the part
405  auto msgPart = new KMime::Content(fwdMsg.data());
406  auto ct = msgPart->contentType();
407  ct->setMimeType("message/rfc822");
408 
409  auto cd = msgPart->contentDisposition(); // create
410  cd->setParameter(QStringLiteral("filename"), i18n("forwarded message"));
411  cd->setDisposition(KMime::Headers::CDinline);
412  const QString subject = fwdMsg->subject()->asUnicodeString();
413  ct->setParameter(QStringLiteral("name"), subject);
414  cd->fromUnicodeString(fwdMsg->from()->asUnicodeString() + QLatin1String(": ") + subject, "utf-8");
415  msgPart->setBody(fwdMsg->encodedContent());
416  msgPart->assemble();
417 
418  MessageComposer::Util::addLinkInformation(fwdMsg, 0, Akonadi::MessageStatus::statusForwarded());
419  return msgPart;
420 }
421 
422 bool MessageFactoryNG::replyAsHtml() const
423 {
424  return mReplyAsHtml;
425 }
426 
427 void MessageFactoryNG::setReplyAsHtml(bool replyAsHtml)
428 {
429  mReplyAsHtml = replyAsHtml;
430 }
431 
432 KMime::Message::Ptr MessageFactoryNG::createResend()
433 {
435  msg->setContent(mOrigMsg->encodedContent());
436  msg->parse();
437  msg->removeHeader<KMime::Headers::MessageID>();
438  uint originalIdentity = identityUoid(mOrigMsg);
439 
440  // Set the identity from above
441  auto header = new KMime::Headers::Generic("X-KMail-Identity");
442  header->fromUnicodeString(QString::number(originalIdentity), "utf-8");
443  msg->setHeader(header);
444 
445  // Restore the original bcc field as this is overwritten in applyIdentity
446  msg->bcc(mOrigMsg->bcc());
447  return msg;
448 }
449 
451 MessageFactoryNG::createRedirect(const QString &toStr, const QString &ccStr, const QString &bccStr, int transportId, const QString &fcc, int identity)
452 {
453  if (!mOrigMsg) {
454  return {};
455  }
456 
457  // copy the message 1:1
459  msg->setContent(mOrigMsg->encodedContent());
460  msg->parse();
461 
462  uint id = identity;
463  if (identity == -1) {
464  if (auto hrd = msg->headerByType("X-KMail-Identity")) {
465  const QString strId = hrd->asUnicodeString().trimmed();
466  if (!strId.isEmpty()) {
467  id = strId.toUInt();
468  }
469  }
470  }
471  const KIdentityManagementCore::Identity &ident = mIdentityManager->identityForUoidOrDefault(id);
472 
473  // X-KMail-Redirect-From: content
474  const QString strByWayOf =
475  QString::fromLocal8Bit("%1 (by way of %2 <%3>)").arg(mOrigMsg->from()->asUnicodeString(), ident.fullName(), ident.primaryEmailAddress());
476 
477  // Resent-From: content
478  const QString strFrom = QString::fromLocal8Bit("%1 <%2>").arg(ident.fullName(), ident.primaryEmailAddress());
479 
480  // format the current date to be used in Resent-Date:
481  // FIXME: generate datetime the same way as KMime, otherwise we get inconsistency
482  // in unit-tests. Unfortunately RFC2822Date is not enough for us, we need the
483  // composition hack below
485  const QString newDate = QLocale::c().toString(dt, QStringLiteral("ddd, ")) + dt.toString(Qt::RFC2822Date);
486 
487  // Clean up any resent headers
488  msg->removeHeader("Resent-Cc");
489  msg->removeHeader("Resent-Bcc");
490  msg->removeHeader("Resent-Sender");
491  // date, from to and id will be set anyway
492 
493  // prepend Resent-*: headers (c.f. RFC2822 3.6.6)
494  QString msgIdSuffix;
495  if (MessageComposer::MessageComposerSettings::useCustomMessageIdSuffix()) {
496  msgIdSuffix = MessageComposer::MessageComposerSettings::customMsgIDSuffix();
497  }
498  auto header = new KMime::Headers::Generic("Resent-Message-ID");
499  header->fromUnicodeString(MessageCore::StringUtil::generateMessageId(msg->sender()->asUnicodeString(), msgIdSuffix), "utf-8");
500  msg->setHeader(header);
501 
502  header = new KMime::Headers::Generic("Resent-Date");
503  header->fromUnicodeString(newDate, "utf-8");
504  msg->setHeader(header);
505 
506  header = new KMime::Headers::Generic("Resent-From");
507  header->fromUnicodeString(strFrom, "utf-8");
508  msg->setHeader(header);
509 
510  if (msg->to(false)) {
511  auto headerT = new KMime::Headers::To;
512  headerT->fromUnicodeString(mOrigMsg->to()->asUnicodeString(), "utf-8");
513  msg->setHeader(headerT);
514  }
515 
516  header = new KMime::Headers::Generic("Resent-To");
517  header->fromUnicodeString(toStr, "utf-8");
518  msg->setHeader(header);
519 
520  if (!ccStr.isEmpty()) {
521  header = new KMime::Headers::Generic("Resent-Cc");
522  header->fromUnicodeString(ccStr, "utf-8");
523  msg->setHeader(header);
524  }
525 
526  if (!bccStr.isEmpty()) {
527  header = new KMime::Headers::Generic("Resent-Bcc");
528  header->fromUnicodeString(bccStr, "utf-8");
529  msg->setHeader(header);
530  }
531 
532  header = new KMime::Headers::Generic("X-KMail-Redirect-From");
533  header->fromUnicodeString(strByWayOf, "utf-8");
534  msg->setHeader(header);
535 
536  if (transportId != -1) {
537  header = new KMime::Headers::Generic("X-KMail-Transport");
538  header->fromUnicodeString(QString::number(transportId), "utf-8");
539  msg->setHeader(header);
540  }
541 
542  if (!fcc.isEmpty()) {
543  header = new KMime::Headers::Generic("X-KMail-Fcc");
544  header->fromUnicodeString(fcc, "utf-8");
545  msg->setHeader(header);
546  }
547 
548  const bool fccIsDisabled = ident.disabledFcc();
549  if (fccIsDisabled) {
550  header = new KMime::Headers::Generic("X-KMail-FccDisabled");
551  header->fromUnicodeString(QStringLiteral("true"), "utf-8");
552  msg->setHeader(header);
553  } else {
554  msg->removeHeader("X-KMail-FccDisabled");
555  }
556 
557  msg->assemble();
558 
559  MessageComposer::Util::addLinkInformation(msg, mId, Akonadi::MessageStatus::statusForwarded());
560  return msg;
561 }
562 
563 KMime::Message::Ptr MessageFactoryNG::createDeliveryReceipt()
564 {
565  QString receiptTo;
566  if (auto hrd = mOrigMsg->headerByType("Disposition-Notification-To")) {
567  receiptTo = hrd->asUnicodeString();
568  }
569  if (receiptTo.trimmed().isEmpty()) {
570  return {};
571  }
572  receiptTo.remove(QChar::fromLatin1('\n'));
573 
574  KMime::Message::Ptr receipt(new KMime::Message);
575  const uint originalIdentity = identityUoid(mOrigMsg);
576  MessageHelper::initFromMessage(receipt, mOrigMsg, mIdentityManager, originalIdentity);
577  receipt->to()->fromUnicodeString(receiptTo, QStringLiteral("utf-8").toLatin1());
578  receipt->subject()->fromUnicodeString(i18n("Receipt: ") + mOrigMsg->subject()->asUnicodeString(), "utf-8");
579 
580  QString str = QStringLiteral("Your message was successfully delivered.");
581  str += QLatin1String("\n\n---------- Message header follows ----------\n");
582  str += QString::fromLatin1(mOrigMsg->head());
583  str += QLatin1String("--------------------------------------------\n");
584  // Conversion to toLatin1 is correct here as Mail headers should contain
585  // ascii only
586  receipt->setBody(str.toLatin1());
588  receipt->assemble();
589 
590  return receipt;
591 }
592 
593 KMime::Message::Ptr MessageFactoryNG::createMDN(KMime::MDN::ActionMode a,
594  KMime::MDN::DispositionType d,
595  KMime::MDN::SendingMode s,
596  int mdnQuoteOriginal,
598 {
599  // extract where to send to:
600  QString receiptTo;
601  if (auto hrd = mOrigMsg->headerByType("Disposition-Notification-To")) {
602  receiptTo = hrd->asUnicodeString();
603  }
604  if (receiptTo.trimmed().isEmpty()) {
606  }
607  receiptTo.remove(QChar::fromLatin1('\n'));
608 
609  QString special; // fill in case of error, warning or failure
610 
611  // extract where to send from:
612  QString finalRecipient = mIdentityManager->identityForUoidOrDefault(identityUoid(mOrigMsg)).fullEmailAddr();
613 
614  //
615  // Generate message:
616  //
617 
618  KMime::Message::Ptr receipt(new KMime::Message());
619  const uint originalIdentity = identityUoid(mOrigMsg);
620  MessageHelper::initFromMessage(receipt, mOrigMsg, mIdentityManager, originalIdentity);
621  auto contentType = receipt->contentType(true); // create it
622  contentType->from7BitString("multipart/report");
623  contentType->setBoundary(KMime::multiPartBoundary());
624  contentType->setCharset("us-ascii");
625  receipt->removeHeader<KMime::Headers::ContentTransferEncoding>();
626  // Modify the ContentType directly (replaces setAutomaticFields(true))
627  contentType->setParameter(QStringLiteral("report-type"), QStringLiteral("disposition-notification"));
628 
629  const QString description = replaceHeadersInString(mOrigMsg, KMime::MDN::descriptionFor(d, m));
630 
631  // text/plain part:
632  auto firstMsgPart = new KMime::Content(mOrigMsg.data());
633  auto firstMsgPartContentType = firstMsgPart->contentType(); // create it
634  firstMsgPartContentType->setMimeType("text/plain");
635  firstMsgPartContentType->setCharset("utf-8");
636  firstMsgPart->contentTransferEncoding(true)->setEncoding(KMime::Headers::CE7Bit);
637  firstMsgPart->setBody(description.toUtf8());
638  receipt->addContent(firstMsgPart);
639 
640  // message/disposition-notification part:
641  auto secondMsgPart = new KMime::Content(mOrigMsg.data());
642  secondMsgPart->contentType()->setMimeType("message/disposition-notification");
643 
644  secondMsgPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit);
645  QByteArray originalRecipient = "";
646  if (auto hrd = mOrigMsg->headerByType("Original-Recipient")) {
647  originalRecipient = hrd->as7BitString(false);
648  }
649  secondMsgPart->setBody(KMime::MDN::dispositionNotificationBodyContent(finalRecipient,
650  originalRecipient,
651  mOrigMsg->messageID()->as7BitString(false), /* Message-ID */
652  d,
653  a,
654  s,
655  m,
656  special));
657  receipt->addContent(secondMsgPart);
658 
659  if ((mdnQuoteOriginal < 0) || (mdnQuoteOriginal > 2)) {
660  mdnQuoteOriginal = 0;
661  }
662  /* 0=> Nothing, 1=>Full Message, 2=>HeadersOnly*/
663 
664  auto thirdMsgPart = new KMime::Content(mOrigMsg.data());
665  switch (mdnQuoteOriginal) {
666  case 1:
667  thirdMsgPart->contentType()->setMimeType("message/rfc822");
668  thirdMsgPart->setBody(MessageCore::StringUtil::asSendableString(mOrigMsg));
669  receipt->addContent(thirdMsgPart);
670  break;
671  case 2:
672  thirdMsgPart->contentType()->setMimeType("text/rfc822-headers");
673  thirdMsgPart->setBody(MessageCore::StringUtil::headerAsSendableString(mOrigMsg));
674  receipt->addContent(thirdMsgPart);
675  break;
676  case 0:
677  default:
678  delete thirdMsgPart;
679  break;
680  }
681 
682  receipt->to()->fromUnicodeString(receiptTo, "utf-8");
683  // Laurent: We don't translate subject ?
684  receipt->subject()->from7BitString("Message Disposition Notification");
685  auto header = new KMime::Headers::InReplyTo;
686  header->fromUnicodeString(mOrigMsg->messageID()->asUnicodeString(), "utf-8");
687  receipt->setHeader(header);
688 
689  receipt->references()->from7BitString(getRefStr(mOrigMsg));
690 
691  receipt->assemble();
692 
693  qCDebug(MESSAGECOMPOSER_LOG) << "final message:" + receipt->encodedContent();
694 
695  receipt->assemble();
696  return receipt;
697 }
698 
699 QPair<KMime::Message::Ptr, KMime::Content *> MessageFactoryNG::createForwardDigestMIME(const Akonadi::Item::List &items)
700 {
702  auto digest = new KMime::Content(msg.data());
703 
704  const QString mainPartText = i18n(
705  "\nThis is a MIME digest forward. The content of the"
706  " message is contained in the attachment(s).\n\n\n");
707 
708  auto ct = digest->contentType();
709  ct->setMimeType("multipart/digest");
710  ct->setBoundary(KMime::multiPartBoundary());
711  digest->contentDescription()->fromUnicodeString(QStringLiteral("Digest of %1 messages.").arg(items.count()), "utf8");
712  digest->contentDisposition()->setFilename(QStringLiteral("digest"));
713  digest->fromUnicodeString(mainPartText);
714 
715  int id = 0;
716  for (const Akonadi::Item &item : std::as_const(items)) {
718  if (id == 0) {
719  if (auto hrd = fMsg->headerByType("X-KMail-Identity")) {
720  id = hrd->asUnicodeString().toInt();
721  }
722  }
723 
725  fMsg->removeHeader<KMime::Headers::Bcc>();
726  fMsg->assemble();
727  auto part = new KMime::Content(digest);
728 
729  part->contentType()->setMimeType("message/rfc822");
730  part->contentType(false)->setCharset(fMsg->contentType()->charset());
731  part->contentID()->setIdentifier(fMsg->contentID()->identifier());
732  part->contentDescription()->fromUnicodeString(fMsg->contentDescription()->asUnicodeString(), "utf8");
733  part->contentDisposition()->setParameter(QStringLiteral("name"), i18n("forwarded message"));
734  part->fromUnicodeString(QString::fromLatin1(fMsg->encodedContent()));
735  part->assemble();
736  MessageComposer::Util::addLinkInformation(msg, item.id(), Akonadi::MessageStatus::statusForwarded());
737  digest->addContent(part);
738  }
739  digest->assemble();
740 
741  id = mFolderId;
742  MessageHelper::initHeader(msg, mIdentityManager, id);
743 
744  // qCDebug(MESSAGECOMPOSER_LOG) << "digest:" << digest->contents().size() << digest->encodedContent();
745 
747 }
748 
749 void MessageFactoryNG::setIdentityManager(KIdentityManagementCore::IdentityManager *ident)
750 {
751  mIdentityManager = ident;
752 }
753 
754 void MessageFactoryNG::setReplyStrategy(MessageComposer::ReplyStrategy replyStrategy)
755 {
756  mReplyStrategy = replyStrategy;
757 }
758 
759 void MessageFactoryNG::setSelection(const QString &selection)
760 {
761  mSelection = selection;
762 }
763 
764 void MessageFactoryNG::setQuote(bool quote)
765 {
766  mQuote = quote;
767 }
768 
769 void MessageFactoryNG::setTemplate(const QString &templ)
770 {
771  mTemplate = templ;
772 }
773 
774 void MessageFactoryNG::setMailingListAddresses(const KMime::Types::Mailbox::List &listAddresses)
775 {
776  mMailingListAddresses << listAddresses;
777 }
778 
779 void MessageFactoryNG::setFolderIdentity(uint folderIdentityId)
780 {
781  mFolderId = folderIdentityId;
782 }
783 
784 void MessageFactoryNG::putRepliesInSameFolder(Akonadi::Collection::Id parentColId)
785 {
786  mParentFolderId = parentColId;
787 }
788 
789 bool MessageFactoryNG::MDNRequested(const KMime::Message::Ptr &msg)
790 {
791  // extract where to send to:
792  QString receiptTo;
793  if (auto hrd = msg->headerByType("Disposition-Notification-To")) {
794  receiptTo = hrd->asUnicodeString();
795  }
796  if (receiptTo.trimmed().isEmpty()) {
797  return false;
798  }
799  receiptTo.remove(QChar::fromLatin1('\n'));
800  return !receiptTo.isEmpty();
801 }
802 
803 bool MessageFactoryNG::MDNConfirmMultipleRecipients(const KMime::Message::Ptr &msg)
804 {
805  // extract where to send to:
806  QString receiptTo;
807  if (auto hrd = msg->headerByType("Disposition-Notification-To")) {
808  receiptTo = hrd->asUnicodeString();
809  }
810  if (receiptTo.trimmed().isEmpty()) {
811  return false;
812  }
813  receiptTo.remove(QChar::fromLatin1('\n'));
814 
815  // RFC 2298: [ Confirmation from the user SHOULD be obtained (or no
816  // MDN sent) ] if there is more than one distinct address in the
817  // Disposition-Notification-To header.
818  qCDebug(MESSAGECOMPOSER_LOG) << "KEmailAddress::splitAddressList(receiptTo):" << KEmailAddress::splitAddressList(receiptTo).join(QLatin1Char('\n'));
819 
820  return KEmailAddress::splitAddressList(receiptTo).count() > 1;
821 }
822 
823 bool MessageFactoryNG::MDNReturnPathEmpty(const KMime::Message::Ptr &msg)
824 {
825  // extract where to send to:
826  QString receiptTo;
827  if (auto hrd = msg->headerByType("Disposition-Notification-To")) {
828  receiptTo = hrd->asUnicodeString();
829  }
830  if (receiptTo.trimmed().isEmpty()) {
831  return false;
832  }
833  receiptTo.remove(QChar::fromLatin1('\n'));
834 
835  // RFC 2298: MDNs SHOULD NOT be sent automatically if the address in
836  // the Disposition-Notification-To header differs from the address
837  // in the Return-Path header. [...] Confirmation from the user
838  // SHOULD be obtained (or no MDN sent) if there is no Return-Path
839  // header in the message [...]
840  KMime::Types::AddrSpecList returnPathList = MessageHelper::extractAddrSpecs(msg, "Return-Path");
841  const QString returnPath = returnPathList.isEmpty() ? QString() : returnPathList.front().localPart + QChar::fromLatin1('@') + returnPathList.front().domain;
842  qCDebug(MESSAGECOMPOSER_LOG) << "clean return path:" << returnPath;
843  return returnPath.isEmpty();
844 }
845 
846 bool MessageFactoryNG::MDNReturnPathNotInRecieptTo(const KMime::Message::Ptr &msg)
847 {
848  // extract where to send to:
849  QString receiptTo;
850  if (auto hrd = msg->headerByType("Disposition-Notification-To")) {
851  receiptTo = hrd->asUnicodeString();
852  }
853  if (receiptTo.trimmed().isEmpty()) {
854  return false;
855  }
856  receiptTo.remove(QChar::fromLatin1('\n'));
857 
858  // RFC 2298: MDNs SHOULD NOT be sent automatically if the address in
859  // the Disposition-Notification-To header differs from the address
860  // in the Return-Path header. [...] Confirmation from the user
861  // SHOULD be obtained (or no MDN sent) if there is no Return-Path
862  // header in the message [...]
863  KMime::Types::AddrSpecList returnPathList = MessageHelper::extractAddrSpecs(msg, QStringLiteral("Return-Path").toLatin1());
864  const QString returnPath = returnPathList.isEmpty() ? QString() : returnPathList.front().localPart + QChar::fromLatin1('@') + returnPathList.front().domain;
865  qCDebug(MESSAGECOMPOSER_LOG) << "clean return path:" << returnPath;
866  return !receiptTo.contains(returnPath, Qt::CaseSensitive);
867 }
868 
869 bool MessageFactoryNG::MDNMDNUnknownOption(const KMime::Message::Ptr &msg)
870 {
871  // RFC 2298: An importance of "required" indicates that
872  // interpretation of the parameter is necessary for proper
873  // generation of an MDN in response to this request. If a UA does
874  // not understand the meaning of the parameter, it MUST NOT generate
875  // an MDN with any disposition type other than "failed" in response
876  // to the request.
877  QString notificationOptions;
878  if (auto hrd = msg->headerByType("Disposition-Notification-Options")) {
879  notificationOptions = hrd->asUnicodeString();
880  }
881  if (notificationOptions.contains(QLatin1String("required"), Qt::CaseSensitive)) {
882  // ### hacky; should parse...
883  // There is a required option that we don't understand. We need to
884  // ask the user what we should do:
885  return true;
886  }
887  return false;
888 }
889 
890 uint MessageFactoryNG::identityUoid(const KMime::Message::Ptr &msg)
891 {
892  QString idString;
893  if (auto hdr = msg->headerByType("X-KMail-Identity")) {
894  idString = hdr->asUnicodeString().trimmed();
895  }
896  bool ok = false;
897  uint id = idString.toUInt(&ok);
898 
899  if (!ok || id == 0) {
900  id = MessageCore::Util::identityForMessage(msg.data(), mIdentityManager, mFolderId).uoid();
901  }
902  return id;
903 }
904 
905 QString MessageFactoryNG::replaceHeadersInString(const KMime::Message::Ptr &msg, const QString &s)
906 {
907  QString result = s;
908  static QRegularExpression rx{QStringLiteral("\\$\\{([a-z0-9-]+)\\}"), QRegularExpression::CaseInsensitiveOption};
909 
910  const QString sDate = KMime::DateFormatter::formatDate(KMime::DateFormatter::Localized, msg->date()->dateTime());
911  qCDebug(MESSAGECOMPOSER_LOG) << "creating mdn date:" << msg->date()->dateTime().toSecsSinceEpoch() << sDate;
912 
913  result.replace(QStringLiteral("${date}"), sDate);
914 
915  int idx = 0;
916  for (auto match = rx.match(result); match.hasMatch(); match = rx.match(result, idx)) {
917  idx = match.capturedStart(0);
918  const QByteArray ba = match.captured(1).toLatin1();
919  if (auto hdr = msg->headerByType(ba.constData())) {
920  const auto replacement = hdr->asUnicodeString();
921  result.replace(idx, match.capturedLength(0), replacement);
922  idx += replacement.length();
923  } else {
924  result.remove(idx, match.capturedLength(0));
925  }
926  }
927  return result;
928 }
929 
930 void MessageFactoryNG::applyCharset(const KMime::Message::Ptr msg)
931 {
932  if (MessageComposer::MessageComposerSettings::forceReplyCharset()) {
933  // first convert the body from its current encoding to unicode representation
934  QStringDecoder bodyCodec(msg->contentType()->charset().constData());
935  if (!bodyCodec.isValid()) {
936  bodyCodec = QStringDecoder(QStringDecoder::Utf8);
937  }
938  const QString body = bodyCodec.decode(msg->body());
939 
940  // then apply the encoding of the original message
941  QStringEncoder codec(mOrigMsg->contentType()->charset().constData());
942  if (!codec.isValid()) {
943  qCCritical(MESSAGECOMPOSER_LOG) << "Could not get text codec for charset" << mOrigMsg->contentType()->charset();
944  // Don't touch the message
945  } else {
946  const auto encodedBody = codec.encode(body);
947  if (codec.hasError()) { // charset can't encode body, fall back to preferred
948  const QStringList charsets = MessageComposer::MessageComposerSettings::preferredCharsets();
949 
950  QList<QByteArray> chars;
951  chars.reserve(charsets.count());
952  for (const QString &charset : charsets) {
953  chars << charset.toLatin1();
954  }
955 
956  codec = QStringEncoder(MessageComposer::Util::selectCharset(chars, body).constData());
957  if (!codec.isValid()) {
958  codec = QStringEncoder(QStringEncoder::Utf8);
959  }
960  msg->contentType()->setCharset(codec.name());
961  msg->setBody(codec.encode(body));
962  } else {
963  msg->contentType()->setCharset(mOrigMsg->contentType()->charset());
964  msg->setBody(encodedBody);
965  }
966  }
967  }
968 }
969 
970 QByteArray MessageFactoryNG::getRefStr(const KMime::Message::Ptr &msg)
971 {
972  QByteArray firstRef;
973  QByteArray lastRef;
974  QByteArray refStr;
975  QByteArray retRefStr;
976  int i;
977  int j;
978 
979  if (auto hdr = msg->references(false)) {
980  refStr = hdr->as7BitString(false).trimmed();
981  }
982 
983  if (refStr.isEmpty()) {
984  return msg->messageID()->as7BitString(false);
985  }
986 
987  i = refStr.indexOf('<');
988  j = refStr.indexOf('>');
989  firstRef = refStr.mid(i, j - i + 1);
990  if (!firstRef.isEmpty()) {
991  retRefStr = firstRef + ' ';
992  }
993 
994  i = refStr.lastIndexOf('<');
995  j = refStr.lastIndexOf('>');
996 
997  lastRef = refStr.mid(i, j - i + 1);
998  if (!lastRef.isEmpty() && lastRef != firstRef) {
999  retRefStr += lastRef + ' ';
1000  }
1001 
1002  retRefStr += msg->messageID()->as7BitString(false);
1003  return retRefStr;
1004 }
1005 
1006 #include "moc_messagefactoryng.cpp"
T & first()
void fromUnicodeString(const QString &s)
QTextStream & right(QTextStream &stream)
T * data() const const
Simple interface that both EncryptJob and SignEncryptJob implement so the composer can extract some e...
QString number(int n, int base)
QString replySubject(KMime::Message *msg)
Return this mails subject, formatted for "reply" mails.
Definition: stringutil.cpp:744
CaseSensitive
static QList< Mailbox > listFrom7BitString(const QByteArray &s)
void removePrivateHeaderFields(const KMime::Message::Ptr &message, bool cleanUpHeader)
Removes all private header fields (e.g.
Definition: stringutil.cpp:354
QString forwardSubject(KMime::Message *msg)
Return this mails subject, formatted for "forward" mails.
Definition: stringutil.cpp:736
int indexOf(char ch, int from) const const
QDateTime currentDateTime()
int count(const T &value) const const
QTextStream & left(QTextStream &stream)
QString trimmed() const const
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...
QByteArray trimmed() const const
QByteArray toLatin1() const const
void setMimeType(const QByteArray &mimeType)
void from7BitString(const QByteArray &s)
KCODECS_EXPORT QStringList splitAddressList(const QString &aStr)
KCharsets * charsets()
int lastIndexOf(char ch, int from) const const
void reserve(int alloc)
QSharedPointer< Message > Ptr
static const MessageStatus statusForwarded()
bool operator==(const Qt3DRender::QGraphicsApiFilter &reference, const Qt3DRender::QGraphicsApiFilter &sample)
void fromUnicodeString(const QString &s, const QByteArray &b) override
bool thatIsMe(const QString &addressList) const
QString i18n(const char *text, const TYPE &arg...)
QString fromLocal8Bit(const char *str, int size)
QLocale c()
void initHeader(const KMime::Message::Ptr &message, const KIdentityManagementCore::IdentityManager *identMan, uint id)
Initialize header fields.
QByteArray mid(int pos, int len) const const
bool isEmpty() const const
WaitCursor
QByteArray toUtf8() const const
int length() const const
QByteArray headerAsSendableString(const KMime::Message::Ptr &originalMessage)
Return the message header with the headers that should not be sent stripped off.
Definition: stringutil.cpp:394
QString toString(qlonglong i) const const
void fromUnicodeString(const QString &s, const QByteArray &b) override
QString join(const QString &separator) const const
uint toUInt(bool *ok, int base) const const
QByteArray asSendableString(const KMime::Message::Ptr &originalMessage)
Returns the message contents with the headers that should not be sent stripped off.
Definition: stringutil.cpp:383
ReplyStrategy
Enumeration that defines the available reply "modes".
QString & replace(int position, int n, QChar after)
QString & remove(int position, int n)
bool isEmpty() const const
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...
const char * constData() const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
static QString listToUnicodeString(const QList< Mailbox > &mailboxes)
QString fromLatin1(const char *str, int size)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
static const MessageStatus statusReplied()
Headers::ContentType * contentType(bool create=true)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QString toString(Qt::DateFormat format) const const
QString message
RFC2822Date
QString generateMessageId(const QString &address, const QString &suffix)
Generates the Message-Id.
Definition: stringutil.cpp:300
static QString formatDate(DateFormatter::FormatType ftype, const QDateTime &t, const QString &data=QString(), bool shortFormat=true)
QChar fromLatin1(char c)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sun Dec 3 2023 03:57:07 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.