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->setEncodedBody(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 code->setBody(encodedBody);
86 } else { // sign PGPMmime
87 setBodyAndCTE(encodedBody, orig->contentType(), code);
88 }
89 result->appendContent(orig);
90 result->appendContent(code);
91 } else { // enc PGPMime
92 setBodyAndCTE(encodedBody, orig->contentType(), code);
93
94 // Build a MIME part holding the version information
95 // taking the body contents returned in
96 // structuring.data.bodyTextVersion.
97 auto vers = new KMime::Content;
98 vers->contentType()->setMimeType("application/pgp-encrypted");
99 vers->contentDisposition()->setDisposition(KMime::Headers::CDattachment);
100 vers->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit);
101 vers->setBody("Version: 1");
102
103 result->appendContent(vers);
104 result->appendContent(code);
105 }
106 } else { // enc SMIME, sign/enc SMIMEOpaque
107 result->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64);
108 auto ct = result->contentDisposition(); // Create
109 ct->setDisposition(KMime::Headers::CDattachment);
110 ct->setFilename(QStringLiteral("smime.p7m"));
111
112 result->assemble();
113 // qCDebug(MESSAGECOMPOSER_LOG) << "processed header:" << result->head();
114
115 result->setBody(encodedBody);
116 }
117 } else { // sign/enc PGPInline
118 result->setHead(orig->head());
119 result->parse();
120
121 // fixing ContentTransferEncoding
122 setBodyAndCTE(encodedBody, orig->contentType(), result);
123 }
124 return result;
125}
126
127// set the correct top-level ContentType on the message
128void MessageComposer::Util::makeToplevelContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign, const QByteArray &hashAlgo)
129{
130 switch (format) {
131 default:
132 case Kleo::InlineOpenPGPFormat:
133 case Kleo::OpenPGPMIMEFormat: {
134 auto ct = content->contentType(); // Create
135 if (sign) {
136 ct->setMimeType(QByteArrayLiteral("multipart/signed"));
137 ct->setParameter(QByteArrayLiteral("protocol"), QStringLiteral("application/pgp-signature"));
138 ct->setParameter(QByteArrayLiteral("micalg"), QString::fromLatin1(QByteArray(QByteArrayLiteral("pgp-") + hashAlgo)).toLower());
139 } else {
140 ct->setMimeType(QByteArrayLiteral("multipart/encrypted"));
141 ct->setParameter(QByteArrayLiteral("protocol"), QStringLiteral("application/pgp-encrypted"));
142 }
143 }
144 return;
145 case Kleo::SMIMEFormat: {
146 if (sign) {
147 auto ct = content->contentType(); // Create
148 qCDebug(MESSAGECOMPOSER_LOG) << "setting headers for SMIME";
149 ct->setMimeType(QByteArrayLiteral("multipart/signed"));
150 ct->setParameter(QByteArrayLiteral("protocol"), QStringLiteral("application/pkcs7-signature"));
151 ct->setParameter(QByteArrayLiteral("micalg"), QString::fromLatin1(hashAlgo).toLower());
152 return;
153 }
154 // fall through (for encryption, there's no difference between
155 // SMIME and SMIMEOpaque, since there is no mp/encrypted for
156 // S/MIME)
157 }
158 [[fallthrough]];
159 case Kleo::SMIMEOpaqueFormat:
160
161 qCDebug(MESSAGECOMPOSER_LOG) << "setting headers for SMIME/opaque";
162 auto ct = content->contentType(); // Create
163 ct->setMimeType(QByteArrayLiteral("application/pkcs7-mime"));
164
165 if (sign) {
166 ct->setParameter(QByteArrayLiteral("smime-type"), QStringLiteral("signed-data"));
167 } else {
168 ct->setParameter(QByteArrayLiteral("smime-type"), QStringLiteral("enveloped-data"));
169 }
170 ct->setParameter(QByteArrayLiteral("name"), QStringLiteral("smime.p7m"));
171 }
172}
173
174void MessageComposer::Util::setNestedContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign)
175{
176 switch (format) {
177 case Kleo::OpenPGPMIMEFormat: {
178 auto ct = content->contentType(); // Create
179 if (sign) {
180 ct->setMimeType(QByteArrayLiteral("application/pgp-signature"));
181 ct->setParameter(QByteArrayLiteral("name"), QStringLiteral("signature.asc"));
182 content->contentDescription()->from7BitString("This is a digitally signed message part.");
183 } else {
184 ct->setMimeType(QByteArrayLiteral("application/octet-stream"));
185 }
186 }
187 return;
188 case Kleo::SMIMEFormat: {
189 if (sign) {
190 auto ct = content->contentType(); // Create
191 ct->setMimeType(QByteArrayLiteral("application/pkcs7-signature"));
192 ct->setParameter(QByteArrayLiteral("name"), QStringLiteral("smime.p7s"));
193 return;
194 }
195 }
196 [[fallthrough]];
197 // fall through:
198 default:
199 case Kleo::InlineOpenPGPFormat:
200 case Kleo::SMIMEOpaqueFormat:;
201 }
202}
203
204void MessageComposer::Util::setNestedContentDisposition(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign)
205{
206 auto ct = content->contentDisposition();
207 if (!sign && format & Kleo::OpenPGPMIMEFormat) {
208 ct->setDisposition(KMime::Headers::CDinline);
209 ct->setFilename(QStringLiteral("msg.asc"));
210 } else if (sign && format & Kleo::SMIMEFormat) {
211 ct->setDisposition(KMime::Headers::CDattachment);
212 ct->setFilename(QStringLiteral("smime.p7s"));
213 }
214}
215
216bool MessageComposer::Util::makeMultiMime(Kleo::CryptoMessageFormat format, bool sign)
217{
218 switch (format) {
219 default:
220 case Kleo::InlineOpenPGPFormat:
221 case Kleo::SMIMEOpaqueFormat:
222 return false;
223 case Kleo::OpenPGPMIMEFormat:
224 return true;
225 case Kleo::SMIMEFormat:
226 return sign; // only on sign - there's no mp/encrypted for S/MIME
227 }
228}
229
230QStringList MessageComposer::Util::AttachmentKeywords()
231{
232 return i18nc(
233 "comma-separated list of keywords that are used to detect whether "
234 "the user forgot to attach his attachment. Do not add space between words.",
235 "attachment,attached")
236 .split(QLatin1Char(','));
237}
238
239QString MessageComposer::Util::cleanedUpHeaderString(const QString &s)
240{
241 // remove invalid characters from the header strings
242 QString res(s);
243 res.remove(QLatin1Char('\r'));
244 res.replace(QLatin1Char('\n'), QLatin1Char(' '));
245 return res.trimmed();
246}
247
248void MessageComposer::Util::addSendReplyForwardAction(const KMime::Message::Ptr &message, Akonadi::MessageQueueJob *qjob)
249{
250 QList<Akonadi::Item::Id> originalMessageId;
252 if (MessageComposer::Util::getLinkInformation(message, originalMessageId, linkStatus)) {
253 for (Akonadi::Item::Id id : std::as_const(originalMessageId)) {
254 if (linkStatus.first() == Akonadi::MessageStatus::statusReplied()) {
256 } else if (linkStatus.first() == Akonadi::MessageStatus::statusForwarded()) {
258 }
259 }
260 }
261}
262
263bool MessageComposer::Util::sendMailDispatcherIsOnline(QWidget *parent)
264{
265 Akonadi::AgentInstance instance = Akonadi::AgentManager::self()->instance(QStringLiteral("akonadi_maildispatcher_agent"));
266 if (!instance.isValid()) {
267 const int rc =
269 i18n("The mail dispatcher is not set up, so mails cannot be sent. Do you want to create a mail dispatcher?"),
270 i18nc("@title:window", "No mail dispatcher."),
271
272 KGuiItem(i18nc("@action:button", "Create Mail Dispatcher"), QIcon::fromTheme(QStringLiteral("mail-folder-outbox"))),
274 QStringLiteral("no_maildispatcher"));
275 if (rc == KMessageBox::ButtonCode::PrimaryAction) {
276 const Akonadi::AgentType type = Akonadi::AgentManager::self()->type(QStringLiteral("akonadi_maildispatcher_agent"));
277 Q_ASSERT(type.isValid());
278 auto job = new Akonadi::AgentInstanceCreateJob(type); // async. We'll have to try again later.
279 job->start();
280 }
281 return false;
282 }
283 if (instance.isOnline()) {
284 return true;
285 } else {
286 const int rc = KMessageBox::warningTwoActions(parent,
287 i18n("The mail dispatcher is offline, so mails cannot be sent. Do you want to make it online?"),
288 i18nc("@title:window", "Mail dispatcher offline."),
289 KGuiItem(i18nc("@action:button", "Set Online"), QIcon::fromTheme(QStringLiteral("user-online"))),
291 QStringLiteral("maildispatcher_put_online"));
292 if (rc == KMessageBox::ButtonCode::PrimaryAction) {
293 instance.setIsOnline(true);
294 return true;
295 }
296 }
297 return false;
298}
299
300KMime::Content *MessageComposer::Util::findTypeInMessage(KMime::Content *data, const QByteArray &mimeType, const QByteArray &subType)
301{
302 if (!data->contentType()->isEmpty()) {
303 if (mimeType.isEmpty() || subType.isEmpty()) {
304 return data;
305 }
306 if ((mimeType == data->contentType()->mediaType()) && (subType == data->contentType(false)->subType())) {
307 return data;
308 }
309 }
310
311 const auto contents = data->contents();
312 for (auto child : contents) {
313 if ((!child->contentType()->isEmpty()) && (mimeType == child->contentType()->mimeType()) && (subType == child->contentType()->subType())) {
314 return child;
315 }
316 auto ret = findTypeInMessage(child, mimeType, subType);
317 if (ret) {
318 return ret;
319 }
320 }
321 return nullptr;
322}
323
324void MessageComposer::Util::addLinkInformation(const KMime::Message::Ptr &msg, Akonadi::Item::Id id, Akonadi::MessageStatus status)
325{
326 Q_ASSERT(status.isReplied() || status.isForwarded() || status.isDeleted());
327
328 QString message;
329 if (auto hrd = msg->headerByType("X-KMail-Link-Message")) {
330 message = hrd->asUnicodeString();
331 }
332 if (!message.isEmpty()) {
333 message += QLatin1Char(',');
334 }
335
336 QString type;
337 if (auto hrd = msg->headerByType("X-KMail-Link-Type")) {
338 type = hrd->asUnicodeString();
339 }
340 if (!type.isEmpty()) {
341 type += QLatin1Char(',');
342 }
343
344 message += QString::number(id);
345 if (status.isReplied()) {
346 type += QLatin1StringView("reply");
347 } else if (status.isForwarded()) {
348 type += QLatin1StringView("forward");
349 }
350
351 auto header = new KMime::Headers::Generic("X-KMail-Link-Message");
352 header->fromUnicodeString(message);
353 msg->setHeader(header);
354
355 header = new KMime::Headers::Generic("X-KMail-Link-Type");
356 header->fromUnicodeString(type);
357 msg->setHeader(header);
358}
359
360bool MessageComposer::Util::getLinkInformation(const KMime::Message::Ptr &msg, QList<Akonadi::Item::Id> &id, QList<Akonadi::MessageStatus> &status)
361{
362 auto hrdLinkMsg = msg->headerByType("X-KMail-Link-Message");
363 auto hrdLinkType = msg->headerByType("X-KMail-Link-Type");
364 if (!hrdLinkMsg || !hrdLinkType) {
365 return false;
366 }
367
368 const QStringList messages = hrdLinkMsg->asUnicodeString().split(QLatin1Char(','), Qt::SkipEmptyParts);
369 const QStringList types = hrdLinkType->asUnicodeString().split(QLatin1Char(','), Qt::SkipEmptyParts);
370
371 if (messages.isEmpty() || types.isEmpty()) {
372 return false;
373 }
374
375 for (const QString &idStr : messages) {
376 id << idStr.toLongLong();
377 }
378
379 for (const QString &typeStr : types) {
380 if (typeStr == QLatin1StringView("reply")) {
382 } else if (typeStr == QLatin1StringView("forward")) {
384 }
385 }
386 return true;
387}
388
389bool MessageComposer::Util::isStandaloneMessage(const Akonadi::Item &item)
390{
391 // standalone message have a valid payload, but are not, themselves valid items
392 return item.hasPayload<KMime::Message::Ptr>() && !item.isValid();
393}
394
395KMime::Message::Ptr MessageComposer::Util::message(const Akonadi::Item &item)
396{
397 if (!item.hasPayload<KMime::Message::Ptr>()) {
398 qCWarning(MESSAGECOMPOSER_LOG) << "Payload is not a MessagePtr!";
399 return {};
400 }
401
402 return item.payload<KMime::Message::Ptr>();
403}
404
405bool MessageComposer::Util::hasMissingAttachments(const QStringList &attachmentKeywords, QTextDocument *doc, const QString &subj)
406{
407 if (!doc) {
408 return false;
409 }
410 QStringList attachWordsList = attachmentKeywords;
411
412 QRegularExpression rx(QLatin1StringView("\\b") + attachWordsList.join(QLatin1StringView("\\b|\\b")) + QLatin1StringView("\\b"),
414
415 // check whether the subject contains one of the attachment key words
416 // unless the message is a reply or a forwarded message
417 bool gotMatch = (MessageCore::StringUtil::stripOffPrefixes(subj) == subj) && (rx.match(subj).hasMatch());
418
419 if (!gotMatch) {
420 // check whether the non-quoted text contains one of the attachment key
421 // words
422 static QRegularExpression quotationRx(QStringLiteral("^([ \\t]*([|>:}#]|[A-Za-z]+>))+"));
423 QTextBlock end(doc->end());
424 for (QTextBlock it = doc->begin(); it != end; it = it.next()) {
425 const QString line = it.text();
426 gotMatch = (!quotationRx.match(line).hasMatch()) && (rx.match(line).hasMatch());
427 if (gotMatch) {
428 break;
429 }
430 }
431 }
432
433 if (!gotMatch) {
434 return false;
435 }
436 return true;
437}
438
439static QStringList encodeIdn(const QStringList &emails)
440{
441 QStringList encoded;
442 encoded.reserve(emails.count());
443 for (const QString &email : emails) {
445 }
446 return encoded;
447}
448
449QStringList MessageComposer::Util::cleanEmailList(const QStringList &emails)
450{
452 clean.reserve(emails.count());
453 for (const QString &email : emails) {
455 }
456 return clean;
457}
458
459QStringList MessageComposer::Util::cleanUpEmailListAndEncoding(const QStringList &emails)
460{
461 return cleanEmailList(encodeIdn(emails));
462}
463
464void MessageComposer::Util::addCustomHeaders(const KMime::Message::Ptr &message, const QMap<QByteArray, QString> &custom)
465{
466 QMapIterator<QByteArray, QString> customHeader(custom);
467 while (customHeader.hasNext()) {
468 customHeader.next();
469 auto header = new KMime::Headers::Generic(customHeader.key().constData());
470 header->fromUnicodeString(customHeader.value());
471 message->setHeader(header);
472 }
473}
void setIsOnline(bool online)
AgentType type(const QString &identifier) const
static AgentManager * self()
AgentInstance instance(const QString &identifier) const
bool hasPayload() const
T payload() const
bool isValid() const
SentActionAttribute & sentActionAttribute()
static const MessageStatus statusReplied()
static const MessageStatus statusForwarded()
void addAction(Action::Type type, const QVariant &value)
const Headers::ContentType * contentType() const
void setEncodedBody(const QByteArray &body)
QByteArray head() const
const Headers::ContentTransferEncoding * contentTransferEncoding() const
const Headers::ContentDisposition * contentDisposition() const
QList< Content * > contents() const
const Headers::ContentDescription * contentDescription() const
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
void from7BitString(QByteArrayView s) override
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()
QAction * end(const QObject *recvr, const char *slot, QObject *parent)
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()
QString stripOffPrefixes(const QString &subject)
Removes the forward and reply marks (e.g.
bool isEmpty() const const
QIcon fromTheme(const QString &name)
qsizetype count() const const
T & first()
bool isEmpty() const const
void reserve(qsizetype size)
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 Fri Jul 26 2024 11:54:19 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.