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 }