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
35KMime::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
52KMime::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
129void 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
175void 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
205void 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
217bool 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
231QByteArray 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.
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
254QStringList 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
263QString 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
272void 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
287bool 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
324KMime::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
348void 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
384bool 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
413bool 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
419KMime::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
429bool 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
463static 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
473QStringList MessageComposer::Util::cleanEmailList(const QStringList &emails)
474{
476 clean.reserve(emails.count());
477 for (const QString &email : emails) {
479 }
480 return clean;
481}
482
483QStringList MessageComposer::Util::cleanUpEmailListAndEncoding(const QStringList &emails)
484{
485 return cleanEmailList(encodeIdn(emails));
486}
487
488void 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}
void setIsOnline(bool online)
AgentType type(const QString &identifier) const
static AgentManager * self()
AgentInstance instance(const QString &identifier) const
T payload() const
bool isValid() const
bool hasPayload() const
SentActionAttribute & sentActionAttribute()
static const MessageStatus statusReplied()
static const MessageStatus statusForwarded()
void addAction(Action::Type type, const QVariant &value)
Headers::ContentDescription * contentDescription(bool create=true)
Headers::ContentType * contentType(bool create=true)
Headers::ContentTransferEncoding * contentTransferEncoding(bool create=true)
QByteArray head() const
Headers::ContentDisposition * contentDisposition(bool create=true)
QList< Content * > contents() const
void setBody(const QByteArray &body)
void setDisposition(contentDisposition disp)
void setEncoding(contentEncoding e)
QByteArray mediaType() const
QByteArray charset() const
QByteArray subType() const
bool isEmpty() const override
void setMimeType(const QByteArray &mimeType)
QByteArray mimeType() const
virtual void from7BitString(const char *s, size_t len)
The Composer class.
Definition composer.h:35
The SinglepartJob class.
Q_SCRIPTABLE CaptureState status()
KCODECS_EXPORT QByteArray extractEmailAddress(const QByteArray &address)
KCODECS_EXPORT QString normalizeAddressesAndEncodeIdn(const QString &str)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
KCALUTILS_EXPORT QString mimeType()
QString clean(const QString &s)
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))
KGuiItem cancel()
const QList< QKeySequence > & end()
QString name(StandardShortcut id)
QString stripOffPrefixes(const QString &subject)
Removes the forward and reply marks (e.g.
bool isEmpty() const const
QChar fromLatin1(char c)
QIcon fromTheme(const QString &name)
qsizetype count() const const
T & first()
bool isEmpty() const const
void reserve(qsizetype size)
const QChar * constData() const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QString number(double n, char format, int precision)
void reserve(qsizetype size)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QString join(QChar separator) const const
SkipEmptyParts
QTextBlock begin() const const
QTextBlock end() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:12:43 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.