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

KDE's Doxygen guidelines are available online.