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
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)
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 Headers::ContentDescription * contentDescription() const
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()
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 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 next() const const
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 Mon Nov 18 2024 12:07:25 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.