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

KDE's Doxygen guidelines are available online.