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

KDE's Doxygen guidelines are available online.