File indexing completed on 2024-06-23 05:18:38
0001 /* 0002 SPDX-FileCopyrightText: 2009 Constantin Berzan <exit3219@gmail.com> 0003 SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net 0004 SPDX-FileCopyrightText: 2009 Leo Franchi <lfranchi@kde.org> 0005 0006 Parts based on KMail code by: 0007 0008 SPDX-License-Identifier: LGPL-2.0-or-later 0009 */ 0010 0011 #include "utils/util.h" 0012 #include "util_p.h" 0013 0014 #include "composer/composer.h" 0015 #include "job/singlepartjob.h" 0016 0017 #include <QRegularExpression> 0018 #include <QStringEncoder> 0019 #include <QTextBlock> 0020 #include <QTextDocument> 0021 0022 #include "messagecomposer_debug.h" 0023 #include <KEmailAddress> 0024 #include <KLocalizedString> 0025 #include <KMessageBox> 0026 0027 #include <Akonadi/AgentInstance> 0028 #include <Akonadi/AgentInstanceCreateJob> 0029 #include <Akonadi/AgentManager> 0030 #include <Akonadi/MessageQueueJob> 0031 #include <KMime/Content> 0032 #include <KMime/Headers> 0033 #include <MessageCore/StringUtil> 0034 0035 KMime::Content *setBodyAndCTE(QByteArray &encodedBody, KMime::Headers::ContentType *contentType, KMime::Content *ret) 0036 { 0037 MessageComposer::Composer composer; 0038 MessageComposer::SinglepartJob cteJob(&composer); 0039 0040 cteJob.contentType()->setMimeType(contentType->mimeType()); 0041 cteJob.contentType()->setCharset(contentType->charset()); 0042 cteJob.setData(encodedBody); 0043 cteJob.exec(); 0044 cteJob.content()->assemble(); 0045 0046 ret->contentTransferEncoding()->setEncoding(cteJob.contentTransferEncoding()->encoding()); 0047 ret->setBody(cteJob.content()->encodedBody()); 0048 0049 return ret; 0050 } 0051 0052 KMime::Content *MessageComposer::Util::composeHeadersAndBody(KMime::Content *orig, 0053 QByteArray encodedBody, 0054 Kleo::CryptoMessageFormat format, 0055 bool sign, 0056 const QByteArray &hashAlgo) 0057 { 0058 auto result = new KMime::Content; 0059 0060 // called should have tested that the signing/encryption failed 0061 Q_ASSERT(!encodedBody.isEmpty()); 0062 0063 if (!(format & Kleo::InlineOpenPGPFormat)) { // make a MIME message 0064 qCDebug(MESSAGECOMPOSER_LOG) << "making MIME message, format:" << format; 0065 makeToplevelContentType(result, format, sign, hashAlgo); 0066 0067 if (makeMultiMime(format, sign)) { // sign/enc PGPMime, sign SMIME 0068 const QByteArray boundary = KMime::multiPartBoundary(); 0069 result->contentType()->setBoundary(boundary); 0070 0071 result->assemble(); 0072 // qCDebug(MESSAGECOMPOSER_LOG) << "processed header:" << result->head(); 0073 0074 // Build the encapsulated MIME parts. 0075 // Build a MIME part holding the code information 0076 // taking the body contents returned in ciphertext. 0077 auto code = new KMime::Content; 0078 setNestedContentType(code, format, sign); 0079 setNestedContentDisposition(code, format, sign); 0080 0081 if (sign) { // sign PGPMime, sign SMIME 0082 if (format & Kleo::AnySMIME) { // sign SMIME 0083 auto ct = code->contentTransferEncoding(); // create 0084 ct->setEncoding(KMime::Headers::CEbase64); 0085 ct->needToEncode(); 0086 code->setBody(encodedBody); 0087 } else { // sign PGPMmime 0088 setBodyAndCTE(encodedBody, orig->contentType(), code); 0089 } 0090 result->appendContent(orig); 0091 result->appendContent(code); 0092 } else { // enc PGPMime 0093 setBodyAndCTE(encodedBody, orig->contentType(), code); 0094 0095 // Build a MIME part holding the version information 0096 // taking the body contents returned in 0097 // structuring.data.bodyTextVersion. 0098 auto vers = new KMime::Content; 0099 vers->contentType()->setMimeType("application/pgp-encrypted"); 0100 vers->contentDisposition()->setDisposition(KMime::Headers::CDattachment); 0101 vers->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); 0102 vers->setBody("Version: 1"); 0103 0104 result->appendContent(vers); 0105 result->appendContent(code); 0106 } 0107 } else { // enc SMIME, sign/enc SMIMEOpaque 0108 result->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64); 0109 auto ct = result->contentDisposition(); // Create 0110 ct->setDisposition(KMime::Headers::CDattachment); 0111 ct->setFilename(QStringLiteral("smime.p7m")); 0112 0113 result->assemble(); 0114 // qCDebug(MESSAGECOMPOSER_LOG) << "processed header:" << result->head(); 0115 0116 result->setBody(encodedBody); 0117 } 0118 } else { // sign/enc PGPInline 0119 result->setHead(orig->head()); 0120 result->parse(); 0121 0122 // fixing ContentTransferEncoding 0123 setBodyAndCTE(encodedBody, orig->contentType(), result); 0124 } 0125 return result; 0126 } 0127 0128 // set the correct top-level ContentType on the message 0129 void MessageComposer::Util::makeToplevelContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign, const QByteArray &hashAlgo) 0130 { 0131 switch (format) { 0132 default: 0133 case Kleo::InlineOpenPGPFormat: 0134 case Kleo::OpenPGPMIMEFormat: { 0135 auto ct = content->contentType(); // Create 0136 if (sign) { 0137 ct->setMimeType(QByteArrayLiteral("multipart/signed")); 0138 ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-signature")); 0139 ct->setParameter(QStringLiteral("micalg"), QString::fromLatin1(QByteArray(QByteArrayLiteral("pgp-") + hashAlgo)).toLower()); 0140 } else { 0141 ct->setMimeType(QByteArrayLiteral("multipart/encrypted")); 0142 ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-encrypted")); 0143 } 0144 } 0145 return; 0146 case Kleo::SMIMEFormat: { 0147 if (sign) { 0148 auto ct = content->contentType(); // Create 0149 qCDebug(MESSAGECOMPOSER_LOG) << "setting headers for SMIME"; 0150 ct->setMimeType(QByteArrayLiteral("multipart/signed")); 0151 ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pkcs7-signature")); 0152 ct->setParameter(QStringLiteral("micalg"), QString::fromLatin1(hashAlgo).toLower()); 0153 return; 0154 } 0155 // fall through (for encryption, there's no difference between 0156 // SMIME and SMIMEOpaque, since there is no mp/encrypted for 0157 // S/MIME) 0158 } 0159 [[fallthrough]]; 0160 case Kleo::SMIMEOpaqueFormat: 0161 0162 qCDebug(MESSAGECOMPOSER_LOG) << "setting headers for SMIME/opaque"; 0163 auto ct = content->contentType(); // Create 0164 ct->setMimeType(QByteArrayLiteral("application/pkcs7-mime")); 0165 0166 if (sign) { 0167 ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("signed-data")); 0168 } else { 0169 ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("enveloped-data")); 0170 } 0171 ct->setParameter(QStringLiteral("name"), QStringLiteral("smime.p7m")); 0172 } 0173 } 0174 0175 void MessageComposer::Util::setNestedContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign) 0176 { 0177 switch (format) { 0178 case Kleo::OpenPGPMIMEFormat: { 0179 auto ct = content->contentType(); // Create 0180 if (sign) { 0181 ct->setMimeType(QByteArrayLiteral("application/pgp-signature")); 0182 ct->setParameter(QStringLiteral("name"), QStringLiteral("signature.asc")); 0183 content->contentDescription()->from7BitString("This is a digitally signed message part."); 0184 } else { 0185 ct->setMimeType(QByteArrayLiteral("application/octet-stream")); 0186 } 0187 } 0188 return; 0189 case Kleo::SMIMEFormat: { 0190 if (sign) { 0191 auto ct = content->contentType(); // Create 0192 ct->setMimeType(QByteArrayLiteral("application/pkcs7-signature")); 0193 ct->setParameter(QStringLiteral("name"), QStringLiteral("smime.p7s")); 0194 return; 0195 } 0196 } 0197 [[fallthrough]]; 0198 // fall through: 0199 default: 0200 case Kleo::InlineOpenPGPFormat: 0201 case Kleo::SMIMEOpaqueFormat:; 0202 } 0203 } 0204 0205 void MessageComposer::Util::setNestedContentDisposition(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign) 0206 { 0207 auto ct = content->contentDisposition(); 0208 if (!sign && format & Kleo::OpenPGPMIMEFormat) { 0209 ct->setDisposition(KMime::Headers::CDinline); 0210 ct->setFilename(QStringLiteral("msg.asc")); 0211 } else if (sign && format & Kleo::SMIMEFormat) { 0212 ct->setDisposition(KMime::Headers::CDattachment); 0213 ct->setFilename(QStringLiteral("smime.p7s")); 0214 } 0215 } 0216 0217 bool MessageComposer::Util::makeMultiMime(Kleo::CryptoMessageFormat format, bool sign) 0218 { 0219 switch (format) { 0220 default: 0221 case Kleo::InlineOpenPGPFormat: 0222 case Kleo::SMIMEOpaqueFormat: 0223 return false; 0224 case Kleo::OpenPGPMIMEFormat: 0225 return true; 0226 case Kleo::SMIMEFormat: 0227 return sign; // only on sign - there's no mp/encrypted for S/MIME 0228 } 0229 } 0230 0231 QByteArray MessageComposer::Util::selectCharset(const QList<QByteArray> &charsets, const QString &text) 0232 { 0233 for (const QByteArray &name : charsets) { 0234 // We use KCharsets::codecForName() instead of QTextCodec::codecForName() here, because 0235 // the former knows us-ascii is latin1. 0236 QStringEncoder codec(name.constData()); 0237 if (!codec.isValid()) { 0238 qCWarning(MESSAGECOMPOSER_LOG) << "Could not get text codec for charset" << name; 0239 continue; 0240 } 0241 if ([[maybe_unused]] const QByteArray encoded = codec.encode(text); !codec.hasError()) { 0242 // Special check for us-ascii (needed because us-ascii is not exactly latin1). 0243 if (name == "us-ascii" && !KMime::isUsAscii(text)) { 0244 continue; 0245 } 0246 qCDebug(MESSAGECOMPOSER_LOG) << "Chosen charset" << name; 0247 return name; 0248 } 0249 } 0250 qCDebug(MESSAGECOMPOSER_LOG) << "No appropriate charset found."; 0251 return {}; 0252 } 0253 0254 QStringList MessageComposer::Util::AttachmentKeywords() 0255 { 0256 return i18nc( 0257 "comma-separated list of keywords that are used to detect whether " 0258 "the user forgot to attach his attachment. Do not add space between words.", 0259 "attachment,attached") 0260 .split(QLatin1Char(',')); 0261 } 0262 0263 QString MessageComposer::Util::cleanedUpHeaderString(const QString &s) 0264 { 0265 // remove invalid characters from the header strings 0266 QString res(s); 0267 res.remove(QChar::fromLatin1('\r')); 0268 res.replace(QChar::fromLatin1('\n'), QLatin1Char(' ')); 0269 return res.trimmed(); 0270 } 0271 0272 void MessageComposer::Util::addSendReplyForwardAction(const KMime::Message::Ptr &message, Akonadi::MessageQueueJob *qjob) 0273 { 0274 QList<Akonadi::Item::Id> originalMessageId; 0275 QList<Akonadi::MessageStatus> linkStatus; 0276 if (MessageComposer::Util::getLinkInformation(message, originalMessageId, linkStatus)) { 0277 for (Akonadi::Item::Id id : std::as_const(originalMessageId)) { 0278 if (linkStatus.first() == Akonadi::MessageStatus::statusReplied()) { 0279 qjob->sentActionAttribute().addAction(Akonadi::SentActionAttribute::Action::MarkAsReplied, QVariant(id)); 0280 } else if (linkStatus.first() == Akonadi::MessageStatus::statusForwarded()) { 0281 qjob->sentActionAttribute().addAction(Akonadi::SentActionAttribute::Action::MarkAsForwarded, QVariant(id)); 0282 } 0283 } 0284 } 0285 } 0286 0287 bool MessageComposer::Util::sendMailDispatcherIsOnline(QWidget *parent) 0288 { 0289 Akonadi::AgentInstance instance = Akonadi::AgentManager::self()->instance(QStringLiteral("akonadi_maildispatcher_agent")); 0290 if (!instance.isValid()) { 0291 const int rc = 0292 KMessageBox::warningTwoActions(parent, 0293 i18n("The mail dispatcher is not set up, so mails cannot be sent. Do you want to create a mail dispatcher?"), 0294 i18nc("@title:window", "No mail dispatcher."), 0295 0296 KGuiItem(i18nc("@action:button", "Create Mail Dispatcher"), QIcon::fromTheme(QStringLiteral("mail-folder-outbox"))), 0297 KStandardGuiItem::cancel(), 0298 QStringLiteral("no_maildispatcher")); 0299 if (rc == KMessageBox::ButtonCode::PrimaryAction) { 0300 const Akonadi::AgentType type = Akonadi::AgentManager::self()->type(QStringLiteral("akonadi_maildispatcher_agent")); 0301 Q_ASSERT(type.isValid()); 0302 auto job = new Akonadi::AgentInstanceCreateJob(type); // async. We'll have to try again later. 0303 job->start(); 0304 } 0305 return false; 0306 } 0307 if (instance.isOnline()) { 0308 return true; 0309 } else { 0310 const int rc = KMessageBox::warningTwoActions(parent, 0311 i18n("The mail dispatcher is offline, so mails cannot be sent. Do you want to make it online?"), 0312 i18nc("@title:window", "Mail dispatcher offline."), 0313 KGuiItem(i18nc("@action:button", "Set Online"), QIcon::fromTheme(QStringLiteral("user-online"))), 0314 KStandardGuiItem::cancel(), 0315 QStringLiteral("maildispatcher_put_online")); 0316 if (rc == KMessageBox::ButtonCode::PrimaryAction) { 0317 instance.setIsOnline(true); 0318 return true; 0319 } 0320 } 0321 return false; 0322 } 0323 0324 KMime::Content *MessageComposer::Util::findTypeInMessage(KMime::Content *data, const QByteArray &mimeType, const QByteArray &subType) 0325 { 0326 if (!data->contentType()->isEmpty()) { 0327 if (mimeType.isEmpty() || subType.isEmpty()) { 0328 return data; 0329 } 0330 if ((mimeType == data->contentType()->mediaType()) && (subType == data->contentType(false)->subType())) { 0331 return data; 0332 } 0333 } 0334 0335 const auto contents = data->contents(); 0336 for (auto child : contents) { 0337 if ((!child->contentType()->isEmpty()) && (mimeType == child->contentType()->mimeType()) && (subType == child->contentType()->subType())) { 0338 return child; 0339 } 0340 auto ret = findTypeInMessage(child, mimeType, subType); 0341 if (ret) { 0342 return ret; 0343 } 0344 } 0345 return nullptr; 0346 } 0347 0348 void MessageComposer::Util::addLinkInformation(const KMime::Message::Ptr &msg, Akonadi::Item::Id id, Akonadi::MessageStatus status) 0349 { 0350 Q_ASSERT(status.isReplied() || status.isForwarded() || status.isDeleted()); 0351 0352 QString message; 0353 if (auto hrd = msg->headerByType("X-KMail-Link-Message")) { 0354 message = hrd->asUnicodeString(); 0355 } 0356 if (!message.isEmpty()) { 0357 message += QChar::fromLatin1(','); 0358 } 0359 0360 QString type; 0361 if (auto hrd = msg->headerByType("X-KMail-Link-Type")) { 0362 type = hrd->asUnicodeString(); 0363 } 0364 if (!type.isEmpty()) { 0365 type += QChar::fromLatin1(','); 0366 } 0367 0368 message += QString::number(id); 0369 if (status.isReplied()) { 0370 type += QLatin1StringView("reply"); 0371 } else if (status.isForwarded()) { 0372 type += QLatin1StringView("forward"); 0373 } 0374 0375 auto header = new KMime::Headers::Generic("X-KMail-Link-Message"); 0376 header->fromUnicodeString(message, "utf-8"); 0377 msg->setHeader(header); 0378 0379 header = new KMime::Headers::Generic("X-KMail-Link-Type"); 0380 header->fromUnicodeString(type, "utf-8"); 0381 msg->setHeader(header); 0382 } 0383 0384 bool MessageComposer::Util::getLinkInformation(const KMime::Message::Ptr &msg, QList<Akonadi::Item::Id> &id, QList<Akonadi::MessageStatus> &status) 0385 { 0386 auto hrdLinkMsg = msg->headerByType("X-KMail-Link-Message"); 0387 auto hrdLinkType = msg->headerByType("X-KMail-Link-Type"); 0388 if (!hrdLinkMsg || !hrdLinkType) { 0389 return false; 0390 } 0391 0392 const QStringList messages = hrdLinkMsg->asUnicodeString().split(QLatin1Char(','), Qt::SkipEmptyParts); 0393 const QStringList types = hrdLinkType->asUnicodeString().split(QLatin1Char(','), Qt::SkipEmptyParts); 0394 0395 if (messages.isEmpty() || types.isEmpty()) { 0396 return false; 0397 } 0398 0399 for (const QString &idStr : messages) { 0400 id << idStr.toLongLong(); 0401 } 0402 0403 for (const QString &typeStr : types) { 0404 if (typeStr == QLatin1StringView("reply")) { 0405 status << Akonadi::MessageStatus::statusReplied(); 0406 } else if (typeStr == QLatin1StringView("forward")) { 0407 status << Akonadi::MessageStatus::statusForwarded(); 0408 } 0409 } 0410 return true; 0411 } 0412 0413 bool MessageComposer::Util::isStandaloneMessage(const Akonadi::Item &item) 0414 { 0415 // standalone message have a valid payload, but are not, themselves valid items 0416 return item.hasPayload<KMime::Message::Ptr>() && !item.isValid(); 0417 } 0418 0419 KMime::Message::Ptr MessageComposer::Util::message(const Akonadi::Item &item) 0420 { 0421 if (!item.hasPayload<KMime::Message::Ptr>()) { 0422 qCWarning(MESSAGECOMPOSER_LOG) << "Payload is not a MessagePtr!"; 0423 return {}; 0424 } 0425 0426 return item.payload<KMime::Message::Ptr>(); 0427 } 0428 0429 bool MessageComposer::Util::hasMissingAttachments(const QStringList &attachmentKeywords, QTextDocument *doc, const QString &subj) 0430 { 0431 if (!doc) { 0432 return false; 0433 } 0434 QStringList attachWordsList = attachmentKeywords; 0435 0436 QRegularExpression rx(QLatin1StringView("\\b") + attachWordsList.join(QLatin1StringView("\\b|\\b")) + QLatin1StringView("\\b"), 0437 QRegularExpression::CaseInsensitiveOption); 0438 0439 // check whether the subject contains one of the attachment key words 0440 // unless the message is a reply or a forwarded message 0441 bool gotMatch = (MessageCore::StringUtil::stripOffPrefixes(subj) == subj) && (rx.match(subj).hasMatch()); 0442 0443 if (!gotMatch) { 0444 // check whether the non-quoted text contains one of the attachment key 0445 // words 0446 static QRegularExpression quotationRx(QStringLiteral("^([ \\t]*([|>:}#]|[A-Za-z]+>))+")); 0447 QTextBlock end(doc->end()); 0448 for (QTextBlock it = doc->begin(); it != end; it = it.next()) { 0449 const QString line = it.text(); 0450 gotMatch = (!quotationRx.match(line).hasMatch()) && (rx.match(line).hasMatch()); 0451 if (gotMatch) { 0452 break; 0453 } 0454 } 0455 } 0456 0457 if (!gotMatch) { 0458 return false; 0459 } 0460 return true; 0461 } 0462 0463 static QStringList encodeIdn(const QStringList &emails) 0464 { 0465 QStringList encoded; 0466 encoded.reserve(emails.count()); 0467 for (const QString &email : emails) { 0468 encoded << KEmailAddress::normalizeAddressesAndEncodeIdn(email); 0469 } 0470 return encoded; 0471 } 0472 0473 QStringList MessageComposer::Util::cleanEmailList(const QStringList &emails) 0474 { 0475 QStringList clean; 0476 clean.reserve(emails.count()); 0477 for (const QString &email : emails) { 0478 clean << KEmailAddress::extractEmailAddress(email); 0479 } 0480 return clean; 0481 } 0482 0483 QStringList MessageComposer::Util::cleanUpEmailListAndEncoding(const QStringList &emails) 0484 { 0485 return cleanEmailList(encodeIdn(emails)); 0486 } 0487 0488 void MessageComposer::Util::addCustomHeaders(const KMime::Message::Ptr &message, const QMap<QByteArray, QString> &custom) 0489 { 0490 QMapIterator<QByteArray, QString> customHeader(custom); 0491 while (customHeader.hasNext()) { 0492 customHeader.next(); 0493 auto header = new KMime::Headers::Generic(customHeader.key().constData()); 0494 header->fromUnicodeString(customHeader.value(), "utf-8"); 0495 message->setHeader(header); 0496 } 0497 }