Messagelib

messagecomposer/src/utils/util.cpp
1 /*
2  SPDX-FileCopyrightText: 2009 Constantin Berzan <[email protected]>
3  SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, [email protected]
4  SPDX-FileCopyrightText: 2009 Leo Franchi <[email protected]>
5 
6  Parts based on KMail code by:
7 
8  SPDX-License-Identifier: LGPL-2.0-or-later
9 */
10 
11 #include "utils/util.h"
12 #include "util_p.h"
13 
14 #include "composer/composer.h"
15 #include "job/singlepartjob.h"
16 #include <QRegularExpression>
17 
18 #include <QTextBlock>
19 #include <QTextCodec>
20 #include <QTextDocument>
21 
22 #include "messagecomposer_debug.h"
23 #include <KCharsets>
24 #include <KEmailAddress>
25 #include <KLocalizedString>
26 #include <KMessageBox>
27 
28 #include <Akonadi/AgentInstance>
29 #include <Akonadi/AgentInstanceCreateJob>
30 #include <Akonadi/AgentManager>
31 #include <KMime/Content>
32 #include <KMime/Headers>
33 #include <MailTransportAkonadi/MessageQueueJob>
34 #include <MessageCore/StringUtil>
35 
36 KMime::Content *setBodyAndCTE(QByteArray &encodedBody, KMime::Headers::ContentType *contentType, KMime::Content *ret)
37 {
39  MessageComposer::SinglepartJob cteJob(&composer);
40 
41  cteJob.contentType()->setMimeType(contentType->mimeType());
42  cteJob.contentType()->setCharset(contentType->charset());
43  cteJob.setData(encodedBody);
44  cteJob.exec();
45  cteJob.content()->assemble();
46 
47  ret->contentTransferEncoding()->setEncoding(cteJob.contentTransferEncoding()->encoding());
48  ret->setBody(cteJob.content()->encodedBody());
49 
50  return ret;
51 }
52 
53 KMime::Content *MessageComposer::Util::composeHeadersAndBody(KMime::Content *orig,
54  QByteArray encodedBody,
55  Kleo::CryptoMessageFormat format,
56  bool sign,
57  const QByteArray &hashAlgo)
58 {
59  auto result = new KMime::Content;
60 
61  // called should have tested that the signing/encryption failed
62  Q_ASSERT(!encodedBody.isEmpty());
63 
64  if (!(format & Kleo::InlineOpenPGPFormat)) { // make a MIME message
65  qCDebug(MESSAGECOMPOSER_LOG) << "making MIME message, format:" << format;
66  makeToplevelContentType(result, format, sign, hashAlgo);
67 
68  if (makeMultiMime(format, sign)) { // sign/enc PGPMime, sign SMIME
69  const QByteArray boundary = KMime::multiPartBoundary();
70  result->contentType()->setBoundary(boundary);
71 
72  result->assemble();
73  // qCDebug(MESSAGECOMPOSER_LOG) << "processed header:" << result->head();
74 
75  // Build the encapsulated MIME parts.
76  // Build a MIME part holding the code information
77  // taking the body contents returned in ciphertext.
78  auto code = new KMime::Content;
79  setNestedContentType(code, format, sign);
80  setNestedContentDisposition(code, format, sign);
81 
82  if (sign) { // sign PGPMime, sign SMIME
83  if (format & Kleo::AnySMIME) { // sign SMIME
84  auto ct = code->contentTransferEncoding(); // create
85  ct->setEncoding(KMime::Headers::CEbase64);
86  ct->needToEncode();
87  code->setBody(encodedBody);
88  } else { // sign PGPMmime
89  setBodyAndCTE(encodedBody, orig->contentType(), code);
90  }
91  result->addContent(orig);
92  result->addContent(code);
93  } else { // enc PGPMime
94  setBodyAndCTE(encodedBody, orig->contentType(), code);
95 
96  // Build a MIME part holding the version information
97  // taking the body contents returned in
98  // structuring.data.bodyTextVersion.
99  auto vers = new KMime::Content;
100  vers->contentType()->setMimeType("application/pgp-encrypted");
101  vers->contentDisposition()->setDisposition(KMime::Headers::CDattachment);
102  vers->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit);
103  vers->setBody("Version: 1");
104 
105  result->addContent(vers);
106  result->addContent(code);
107  }
108  } else { // enc SMIME, sign/enc SMIMEOpaque
109  result->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64);
110  auto ct = result->contentDisposition(); // Create
111  ct->setDisposition(KMime::Headers::CDattachment);
112  ct->setFilename(QStringLiteral("smime.p7m"));
113 
114  result->assemble();
115  // qCDebug(MESSAGECOMPOSER_LOG) << "processed header:" << result->head();
116 
117  result->setBody(encodedBody);
118  }
119  } else { // sign/enc PGPInline
120  result->setHead(orig->head());
121  result->parse();
122 
123  // fixing ContentTransferEncoding
124  setBodyAndCTE(encodedBody, orig->contentType(), result);
125  }
126  return result;
127 }
128 
129 // set the correct top-level ContentType on the message
130 void MessageComposer::Util::makeToplevelContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign, const QByteArray &hashAlgo)
131 {
132  switch (format) {
133  default:
134  case Kleo::InlineOpenPGPFormat:
135  case Kleo::OpenPGPMIMEFormat: {
136  auto ct = content->contentType(); // Create
137  if (sign) {
138  ct->setMimeType(QByteArrayLiteral("multipart/signed"));
139  ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-signature"));
140  ct->setParameter(QStringLiteral("micalg"), QString::fromLatin1(QByteArray(QByteArrayLiteral("pgp-") + hashAlgo)).toLower());
141  } else {
142  ct->setMimeType(QByteArrayLiteral("multipart/encrypted"));
143  ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-encrypted"));
144  }
145  }
146  return;
147  case Kleo::SMIMEFormat: {
148  if (sign) {
149  auto ct = content->contentType(); // Create
150  qCDebug(MESSAGECOMPOSER_LOG) << "setting headers for SMIME";
151  ct->setMimeType(QByteArrayLiteral("multipart/signed"));
152  ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pkcs7-signature"));
153  ct->setParameter(QStringLiteral("micalg"), QString::fromLatin1(hashAlgo).toLower());
154  return;
155  }
156  // fall through (for encryption, there's no difference between
157  // SMIME and SMIMEOpaque, since there is no mp/encrypted for
158  // S/MIME)
159  }
160  Q_FALLTHROUGH();
161  case Kleo::SMIMEOpaqueFormat:
162 
163  qCDebug(MESSAGECOMPOSER_LOG) << "setting headers for SMIME/opaque";
164  auto ct = content->contentType(); // Create
165  ct->setMimeType(QByteArrayLiteral("application/pkcs7-mime"));
166 
167  if (sign) {
168  ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("signed-data"));
169  } else {
170  ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("enveloped-data"));
171  }
172  ct->setParameter(QStringLiteral("name"), QStringLiteral("smime.p7m"));
173  }
174 }
175 
176 void MessageComposer::Util::setNestedContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign)
177 {
178  switch (format) {
179  case Kleo::OpenPGPMIMEFormat: {
180  auto ct = content->contentType(); // Create
181  if (sign) {
182  ct->setMimeType(QByteArrayLiteral("application/pgp-signature"));
183  ct->setParameter(QStringLiteral("name"), QStringLiteral("signature.asc"));
184  content->contentDescription()->from7BitString("This is a digitally signed message part.");
185  } else {
186  ct->setMimeType(QByteArrayLiteral("application/octet-stream"));
187  }
188  }
189  return;
190  case Kleo::SMIMEFormat: {
191  if (sign) {
192  auto ct = content->contentType(); // Create
193  ct->setMimeType(QByteArrayLiteral("application/pkcs7-signature"));
194  ct->setParameter(QStringLiteral("name"), QStringLiteral("smime.p7s"));
195  return;
196  }
197  }
198  Q_FALLTHROUGH();
199  // fall through:
200  default:
201  case Kleo::InlineOpenPGPFormat:
202  case Kleo::SMIMEOpaqueFormat:;
203  }
204 }
205 
206 void MessageComposer::Util::setNestedContentDisposition(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign)
207 {
208  auto ct = content->contentDisposition();
209  if (!sign && format & Kleo::OpenPGPMIMEFormat) {
210  ct->setDisposition(KMime::Headers::CDinline);
211  ct->setFilename(QStringLiteral("msg.asc"));
212  } else if (sign && format & Kleo::SMIMEFormat) {
213  ct->setDisposition(KMime::Headers::CDattachment);
214  ct->setFilename(QStringLiteral("smime.p7s"));
215  }
216 }
217 
218 bool MessageComposer::Util::makeMultiMime(Kleo::CryptoMessageFormat format, bool sign)
219 {
220  switch (format) {
221  default:
222  case Kleo::InlineOpenPGPFormat:
223  case Kleo::SMIMEOpaqueFormat:
224  return false;
225  case Kleo::OpenPGPMIMEFormat:
226  return true;
227  case Kleo::SMIMEFormat:
228  return sign; // only on sign - there's no mp/encrypted for S/MIME
229  }
230 }
231 
232 QByteArray MessageComposer::Util::selectCharset(const QVector<QByteArray> &charsets, const QString &text)
233 {
234  for (const QByteArray &name : charsets) {
235  // We use KCharsets::codecForName() instead of QTextCodec::codecForName() here, because
236  // the former knows us-ascii is latin1.
238  if (!codec) {
239  qCWarning(MESSAGECOMPOSER_LOG) << "Could not get text codec for charset" << name;
240  continue;
241  }
242  if (codec->canEncode(text)) {
243  // Special check for us-ascii (needed because us-ascii is not exactly latin1).
244  if (name == "us-ascii" && !KMime::isUsAscii(text)) {
245  continue;
246  }
247  qCDebug(MESSAGECOMPOSER_LOG) << "Chosen charset" << name;
248  return name;
249  }
250  }
251  qCDebug(MESSAGECOMPOSER_LOG) << "No appropriate charset found.";
252  return {};
253 }
254 
255 QStringList MessageComposer::Util::AttachmentKeywords()
256 {
257  return i18nc(
258  "comma-separated list of keywords that are used to detect whether "
259  "the user forgot to attach his attachment. Do not add space between words.",
260  "attachment,attached")
261  .split(QLatin1Char(','));
262 }
263 
264 QString MessageComposer::Util::cleanedUpHeaderString(const QString &s)
265 {
266  // remove invalid characters from the header strings
267  QString res(s);
268  res.remove(QChar::fromLatin1('\r'));
269  res.replace(QChar::fromLatin1('\n'), QLatin1Char(' '));
270  return res.trimmed();
271 }
272 
273 void MessageComposer::Util::addSendReplyForwardAction(const KMime::Message::Ptr &message, MailTransport::MessageQueueJob *qjob)
274 {
275  QVector<Akonadi::Item::Id> originalMessageId;
277  if (MessageComposer::Util::getLinkInformation(message, originalMessageId, linkStatus)) {
278  for (Akonadi::Item::Id id : std::as_const(originalMessageId)) {
279  if (linkStatus.first() == Akonadi::MessageStatus::statusReplied()) {
281  } else if (linkStatus.first() == Akonadi::MessageStatus::statusForwarded()) {
283  }
284  }
285  }
286 }
287 
288 bool MessageComposer::Util::sendMailDispatcherIsOnline(QWidget *parent)
289 {
290  Akonadi::AgentInstance instance = Akonadi::AgentManager::self()->instance(QStringLiteral("akonadi_maildispatcher_agent"));
291  if (!instance.isValid()) {
292  const int rc = KMessageBox::warningYesNo(parent,
293  i18n("The mail dispatcher is not set up, so mails cannot be sent. Do you want to create a mail dispatcher?"),
294  i18n("No mail dispatcher."),
295  KGuiItem(i18nc("@action:button", "Create Mail Dispatcher")),
297  QStringLiteral("no_maildispatcher"));
298  if (rc == KMessageBox::Yes) {
299  const Akonadi::AgentType type = Akonadi::AgentManager::self()->type(QStringLiteral("akonadi_maildispatcher_agent"));
300  Q_ASSERT(type.isValid());
301  auto job = new Akonadi::AgentInstanceCreateJob(type); // async. We'll have to try again later.
302  job->start();
303  }
304  return false;
305  }
306  if (instance.isOnline()) {
307  return true;
308  } else {
309  const int rc = KMessageBox::warningYesNo(parent,
310  i18n("The mail dispatcher is offline, so mails cannot be sent. Do you want to make it online?"),
311  i18n("Mail dispatcher offline."),
312  KGuiItem(i18nc("@action:button", "Set Online")),
314  QStringLiteral("maildispatcher_put_online"));
315  if (rc == KMessageBox::Yes) {
316  instance.setIsOnline(true);
317  return true;
318  }
319  }
320  return false;
321 }
322 
323 KMime::Content *MessageComposer::Util::findTypeInMessage(KMime::Content *data, const QByteArray &mimeType, const QByteArray &subType)
324 {
325  if (!data->contentType()->isEmpty()) {
326  if (mimeType.isEmpty() || subType.isEmpty()) {
327  return data;
328  }
329  if ((mimeType == data->contentType()->mediaType()) && (subType == data->contentType(false)->subType())) {
330  return data;
331  }
332  }
333 
334  const auto contents = data->contents();
335  for (auto child : contents) {
336  if ((!child->contentType()->isEmpty()) && (mimeType == child->contentType()->mimeType()) && (subType == child->contentType()->subType())) {
337  return child;
338  }
339  auto ret = findTypeInMessage(child, mimeType, subType);
340  if (ret) {
341  return ret;
342  }
343  }
344  return nullptr;
345 }
346 
347 void MessageComposer::Util::addLinkInformation(const KMime::Message::Ptr &msg, Akonadi::Item::Id id, Akonadi::MessageStatus status)
348 {
349  Q_ASSERT(status.isReplied() || status.isForwarded() || status.isDeleted());
350 
351  QString message;
352  if (auto hrd = msg->headerByType("X-KMail-Link-Message")) {
353  message = hrd->asUnicodeString();
354  }
355  if (!message.isEmpty()) {
356  message += QChar::fromLatin1(',');
357  }
358 
359  QString type;
360  if (auto hrd = msg->headerByType("X-KMail-Link-Type")) {
361  type = hrd->asUnicodeString();
362  }
363  if (!type.isEmpty()) {
364  type += QChar::fromLatin1(',');
365  }
366 
367  message += QString::number(id);
368  if (status.isReplied()) {
369  type += QLatin1String("reply");
370  } else if (status.isForwarded()) {
371  type += QLatin1String("forward");
372  }
373 
374  auto header = new KMime::Headers::Generic("X-KMail-Link-Message");
375  header->fromUnicodeString(message, "utf-8");
376  msg->setHeader(header);
377 
378  header = new KMime::Headers::Generic("X-KMail-Link-Type");
379  header->fromUnicodeString(type, "utf-8");
380  msg->setHeader(header);
381 }
382 
383 bool MessageComposer::Util::getLinkInformation(const KMime::Message::Ptr &msg, QVector<Akonadi::Item::Id> &id, QVector<Akonadi::MessageStatus> &status)
384 {
385  auto hrdLinkMsg = msg->headerByType("X-KMail-Link-Message");
386  auto hrdLinkType = msg->headerByType("X-KMail-Link-Type");
387  if (!hrdLinkMsg || !hrdLinkType) {
388  return false;
389  }
390 
391  const QStringList messages = hrdLinkMsg->asUnicodeString().split(QLatin1Char(','), Qt::SkipEmptyParts);
392  const QStringList types = hrdLinkType->asUnicodeString().split(QLatin1Char(','), Qt::SkipEmptyParts);
393 
394  if (messages.isEmpty() || types.isEmpty()) {
395  return false;
396  }
397 
398  for (const QString &idStr : messages) {
399  id << idStr.toLongLong();
400  }
401 
402  for (const QString &typeStr : types) {
403  if (typeStr == QLatin1String("reply")) {
405  } else if (typeStr == QLatin1String("forward")) {
407  }
408  }
409  return true;
410 }
411 
412 bool MessageComposer::Util::isStandaloneMessage(const Akonadi::Item &item)
413 {
414  // standalone message have a valid payload, but are not, themselves valid items
415  return item.hasPayload<KMime::Message::Ptr>() && !item.isValid();
416 }
417 
418 KMime::Message::Ptr MessageComposer::Util::message(const Akonadi::Item &item)
419 {
420  if (!item.hasPayload<KMime::Message::Ptr>()) {
421  qCWarning(MESSAGECOMPOSER_LOG) << "Payload is not a MessagePtr!";
422  return {};
423  }
424 
425  return item.payload<KMime::Message::Ptr>();
426 }
427 
428 bool MessageComposer::Util::hasMissingAttachments(const QStringList &attachmentKeywords, QTextDocument *doc, const QString &subj)
429 {
430  if (!doc) {
431  return false;
432  }
433  QStringList attachWordsList = attachmentKeywords;
434 
435  QRegularExpression rx(QLatin1String("\\b") + attachWordsList.join(QLatin1String("\\b|\\b")) + QLatin1String("\\b"),
437 
438  // check whether the subject contains one of the attachment key words
439  // unless the message is a reply or a forwarded message
440  bool gotMatch = (MessageCore::StringUtil::stripOffPrefixes(subj) == subj) && (rx.match(subj).hasMatch());
441 
442  if (!gotMatch) {
443  // check whether the non-quoted text contains one of the attachment key
444  // words
445  QRegularExpression quotationRx(QStringLiteral("^([ \\t]*([|>:}#]|[A-Za-z]+>))+"));
446  QTextBlock end(doc->end());
447  for (QTextBlock it = doc->begin(); it != end; it = it.next()) {
448  const QString line = it.text();
449  gotMatch = (!quotationRx.match(line).hasMatch()) && (rx.match(line).hasMatch());
450  if (gotMatch) {
451  break;
452  }
453  }
454  }
455 
456  if (!gotMatch) {
457  return false;
458  }
459  return true;
460 }
461 
462 static QStringList encodeIdn(const QStringList &emails)
463 {
464  QStringList encoded;
465  encoded.reserve(emails.count());
466  for (const QString &email : emails) {
468  }
469  return encoded;
470 }
471 
472 QStringList MessageComposer::Util::cleanEmailList(const QStringList &emails)
473 {
474  QStringList clean;
475  clean.reserve(emails.count());
476  for (const QString &email : emails) {
477  clean << KEmailAddress::extractEmailAddress(email);
478  }
479  return clean;
480 }
481 
482 QStringList MessageComposer::Util::cleanUpEmailListAndEncoding(const QStringList &emails)
483 {
484  return cleanEmailList(encodeIdn(emails));
485 }
486 
487 void MessageComposer::Util::addCustomHeaders(const KMime::Message::Ptr &message, const QMap<QByteArray, QString> &custom)
488 {
489  QMapIterator<QByteArray, QString> customHeader(custom);
490  while (customHeader.hasNext()) {
491  customHeader.next();
492  auto header = new KMime::Headers::Generic(customHeader.key().constData());
493  header->fromUnicodeString(customHeader.value(), "utf-8");
494  message->setHeader(header);
495  }
496 }
void addContent(Content *content, bool prepend=false)
QTextCodec * codecForName(const QString &name) const
bool isReplied() const
bool isValid() const
QByteArray subType() const
void fromUnicodeString(const QString &s, const QByteArray &b) override
void setMimeType(const QByteArray &mimeType)
void reserve(int alloc)
bool isEmpty() const const
virtual void from7BitString(const char *s, size_t len)
bool isValid() const
T & first()
Headers::ContentDisposition * contentDisposition(bool create=true)
QString join(const QString &separator) const const
QByteArray mimeType() const
const QLatin1String name
KGuiItem cancel()
void setDisposition(contentDisposition disp)
T payload() const
QTextBlock begin() const const
The Composer class.
Definition: composer.h:33
QString number(int n, int base)
int count(const T &value) const const
QVector< Content * > contents() const
QByteArray charset() const
PartitionTable::TableType type
void addAction(Action::Type type, const QVariant &value)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
AgentInstance instance(const QString &identifier) const
void setIsOnline(bool online)
QChar fromLatin1(char c)
bool isEmpty() const override
bool isEmpty() const const
bool isValid() const
bool isEmpty() const const
void setBody(const QByteArray &body)
bool isOnline() const
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
KCODECS_EXPORT QString normalizeAddressesAndEncodeIdn(const QString &str)
static const MessageStatus statusForwarded()
Headers::ContentTransferEncoding * contentTransferEncoding(bool create=true)
Headers::ContentType * contentType(bool create=true)
AgentType type(const QString &identifier) const
SkipEmptyParts
KCODECS_EXPORT QByteArray extractEmailAddress(const QByteArray &address)
static KCharsets * charsets()
QString stripOffPrefixes(const QString &subject)
Removes the forward and reply marks (e.g.
Definition: stringutil.cpp:783
bool isForwarded() const
QString i18n(const char *text, const TYPE &arg...)
const QList< QKeySequence > & end()
QByteArray head() const
SentActionAttribute & sentActionAttribute()
bool isDeleted() const
bool canEncode(QChar ch) const const
static AgentManager * self()
void setEncoding(contentEncoding e)
QString fromLatin1(const char *str, int size)
static const MessageStatus statusReplied()
QTextBlock end() const const
QByteArray mediaType() const
bool hasPayload() const
Headers::ContentDescription * contentDescription(bool create=true)
The SinglepartJob class.
Definition: singlepartjob.h:31
Types types(const QStringList &mimeTypes)
ButtonCode warningYesNo(QWidget *parent, const QString &text, const QString &caption=QString(), const KGuiItem &buttonYes=KStandardGuiItem::yes(), const KGuiItem &buttonNo=KStandardGuiItem::no(), const QString &dontAskAgainName=QString(), Options options=Options(Notify|Dangerous))
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Sat Dec 4 2021 23:12:55 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.