Messagelib

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

KDE's Doxygen guidelines are available online.