diff --git a/server/editor/util.cpp b/server/editor/util.cpp index 7527772..22bda6a 100644 --- a/server/editor/util.cpp +++ b/server/editor/util.cpp @@ -1,412 +1,426 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net SPDX-FileCopyrightText: 2009 Leo Franchi Parts based on KMail code by: SPDX-License-Identifier: LGPL-2.0-or-later */ #include "util.h" #include "util_p.h" #include #include #include #include #include "editor_debug.h" #include #include #include #include #include #include #include "job/singlepartjob.h" #include "composer.h" static QString stripOffPrefixes(const QString &subject) { const QStringList replyPrefixes = { QStringLiteral("Re\\s*:"), QStringLiteral("Re\\[\\d+\\]:"), QStringLiteral("Re\\d+:"), }; const QStringList forwardPrefixes = { QStringLiteral("Fwd:"), QStringLiteral("FW:<"), }; const QStringList prefixRegExps = replyPrefixes + forwardPrefixes; // construct a big regexp that // 1. is anchored to the beginning of str (sans whitespace) // 2. matches at least one of the part regexps in prefixRegExps const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:"))); static QRegularExpression regex; if (regex.pattern() != bigRegExp) { // the prefixes have changed, so update the regexp regex.setPattern(bigRegExp); regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption); } if (regex.isValid()) { QRegularExpressionMatch match = regex.match(subject); if (match.hasMatch()) { return subject.mid(match.capturedEnd(0)); } } else { qCWarning(EDITOR_LOG) << "bigRegExp = \"" << bigRegExp << "\"\n" << "prefix regexp is invalid!"; } return subject; } KMime::Content *setBodyAndCTE(QByteArray &encodedBody, KMime::Headers::ContentType *contentType, KMime::Content *ret) { MessageComposer::Composer composer; MessageComposer::SinglepartJob cteJob(&composer); cteJob.contentType()->setMimeType(contentType->mimeType()); cteJob.contentType()->setCharset(contentType->charset()); cteJob.setData(encodedBody); cteJob.exec(); cteJob.content()->assemble(); ret->contentTransferEncoding()->setEncoding(cteJob.contentTransferEncoding()->encoding()); ret->setBody(cteJob.content()->encodedBody()); return ret; } KMime::Content *MessageComposer::Util::composeHeadersAndBody(KMime::Content *orig, QByteArray encodedBody, Kleo::CryptoMessageFormat format, bool sign, const QByteArray &hashAlgo) { auto result = new KMime::Content; // called should have tested that the signing/encryption failed Q_ASSERT(!encodedBody.isEmpty()); if (!(format & Kleo::InlineOpenPGPFormat)) { // make a MIME message qCDebug(EDITOR_LOG) << "making MIME message, format:" << format; makeToplevelContentType(result, format, sign, hashAlgo); if (makeMultiMime(format, sign)) { // sign/enc PGPMime, sign SMIME const QByteArray boundary = KMime::multiPartBoundary(); result->contentType()->setBoundary(boundary); result->assemble(); // qCDebug(EDITOR_LOG) << "processed header:" << result->head(); // Build the encapsulated MIME parts. // Build a MIME part holding the code information // taking the body contents returned in ciphertext. auto code = new KMime::Content; setNestedContentType(code, format, sign); setNestedContentDisposition(code, format, sign); if (sign) { // sign PGPMime, sign SMIME if (format & Kleo::AnySMIME) { // sign SMIME auto ct = code->contentTransferEncoding(); // create ct->setEncoding(KMime::Headers::CEbase64); ct->needToEncode(); code->setBody(encodedBody); } else { // sign PGPMmime setBodyAndCTE(encodedBody, orig->contentType(), code); } result->appendContent(orig); result->appendContent(code); } else { // enc PGPMime setBodyAndCTE(encodedBody, orig->contentType(), code); auto instruction = new KMime::Content; instruction->contentType()->setMimeType("text/plain"); - instruction->setHeader(new KMime::Headers::Generic("X-PGP-HIDDEN")); instruction->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); if (Kleo::DeVSCompliance::isCompliant()) { instruction->setBody("This message is VS-NfD compliant encrypted and need to be opened in a compliant E-Mail client. For example: GnuPG VS-DesktopĀ®"); } else { instruction->setBody("This message is encrypted with OpenPGP and need to be opened in a compatible client. For example: GPG4Win, KMail, Kleopatra or Thunderbird"); } // Build a MIME part holding the version information // taking the body contents returned in // structuring.data.bodyTextVersion. auto vers = new KMime::Content; vers->contentType()->setMimeType("application/pgp-encrypted"); vers->contentDisposition()->setDisposition(KMime::Headers::CDattachment); vers->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); vers->setBody("Version: 1"); result->appendContent(instruction); result->appendContent(vers); result->appendContent(code); } } else { // enc SMIME, sign/enc SMIMEOpaque - result->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64); - auto ct = result->contentDisposition(); // Create - ct->setDisposition(KMime::Headers::CDattachment); - ct->setFilename(QStringLiteral("smime.p7m")); + const QByteArray boundary = KMime::multiPartBoundary(); + result->contentType()->setBoundary(boundary); result->assemble(); // qCDebug(EDITOR_LOG) << "processed header:" << result->head(); - result->setBody(encodedBody); + // Build the encapsulated MIME parts. + // Build a MIME part holding the code information + // taking the body contents returned in ciphertext. + auto code = new KMime::Content; + setNestedContentType(code, format, sign); + setNestedContentDisposition(code, format, sign); + + setBodyAndCTE(encodedBody, orig->contentType(), code); + code->assemble(); + code->setBody(encodedBody); + + auto instruction = new KMime::Content; + instruction->contentType()->setMimeType("text/plain"); + instruction->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); + if (Kleo::DeVSCompliance::isCompliant()) { + instruction->setBody("This message is VS-NfD compliant encrypted and need to be opened in a compliant E-Mail client. For example: GnuPG VS-DesktopĀ®"); + } else { + instruction->setBody("This message is encrypted with S/MIME and need to be opened in a compatible client. For example: GPG4Win, KMail, Kleopatra or Thunderbird"); + } + + result->appendContent(instruction); + result->appendContent(code); } } else { // sign/enc PGPInline result->setHead(orig->head()); result->parse(); // fixing ContentTransferEncoding setBodyAndCTE(encodedBody, orig->contentType(), result); } return result; } // set the correct top-level ContentType on the message void MessageComposer::Util::makeToplevelContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign, const QByteArray &hashAlgo) { switch (format) { default: case Kleo::InlineOpenPGPFormat: case Kleo::OpenPGPMIMEFormat: { auto ct = content->contentType(); // Create if (sign) { ct->setMimeType(QByteArrayLiteral("multipart/signed")); ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-signature")); ct->setParameter(QStringLiteral("micalg"), QString::fromLatin1(QByteArray(QByteArrayLiteral("pgp-") + hashAlgo)).toLower()); } else { ct->setMimeType(QByteArrayLiteral("multipart/encrypted")); ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-encrypted")); } } return; + case Kleo::SMIMEOpaqueFormat: case Kleo::SMIMEFormat: { + auto ct = content->contentType(); // Create if (sign) { - auto ct = content->contentType(); // Create qCDebug(EDITOR_LOG) << "setting headers for SMIME"; ct->setMimeType(QByteArrayLiteral("multipart/signed")); ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pkcs7-signature")); ct->setParameter(QStringLiteral("micalg"), QString::fromLatin1(hashAlgo).toLower()); return; + } else { + ct->setMimeType(QByteArrayLiteral("multipart/encrypted")); + ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/smime-encrypted")); } // fall through (for encryption, there's no difference between // SMIME and SMIMEOpaque, since there is no mp/encrypted for // S/MIME) } - [[fallthrough]]; - case Kleo::SMIMEOpaqueFormat: - - qCDebug(EDITOR_LOG) << "setting headers for SMIME/opaque"; - auto ct = content->contentType(); // Create - ct->setMimeType(QByteArrayLiteral("application/pkcs7-mime")); - - if (sign) { - ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("signed-data")); - } else { - ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("enveloped-data")); - } - ct->setParameter(QStringLiteral("name"), QStringLiteral("smime.p7m")); } } void MessageComposer::Util::setNestedContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign) { switch (format) { case Kleo::OpenPGPMIMEFormat: { auto ct = content->contentType(); // Create if (sign) { ct->setMimeType(QByteArrayLiteral("application/pgp-signature")); ct->setParameter(QStringLiteral("name"), QStringLiteral("signature.asc")); content->contentDescription()->from7BitString("This is a digitally signed message part."); } else { ct->setMimeType(QByteArrayLiteral("application/octet-stream")); } } return; case Kleo::SMIMEFormat: { + auto ct = content->contentType(); // Create if (sign) { - auto ct = content->contentType(); // Create ct->setMimeType(QByteArrayLiteral("application/pkcs7-signature")); ct->setParameter(QStringLiteral("name"), QStringLiteral("smime.p7s")); - return; + ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("signed-data")); + } else { + auto ct = content->contentType(); // Create + ct->setMimeType(QByteArrayLiteral("application/pkcs7-mime")); + ct->setParameter(QStringLiteral("name"), QStringLiteral("smime.p7m")); + ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("enveloped-data")); } + + return; } - [[fallthrough]]; - // fall through: default: case Kleo::InlineOpenPGPFormat: case Kleo::SMIMEOpaqueFormat:; } } void MessageComposer::Util::setNestedContentDisposition(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign) { auto ct = content->contentDisposition(); if (!sign && format & Kleo::OpenPGPMIMEFormat) { ct->setDisposition(KMime::Headers::CDinline); ct->setFilename(QStringLiteral("msg.asc")); } else if (sign && format & Kleo::SMIMEFormat) { ct->setDisposition(KMime::Headers::CDattachment); ct->setFilename(QStringLiteral("smime.p7s")); } } bool MessageComposer::Util::makeMultiMime(Kleo::CryptoMessageFormat format, bool sign) { switch (format) { default: case Kleo::InlineOpenPGPFormat: case Kleo::SMIMEOpaqueFormat: return false; case Kleo::OpenPGPMIMEFormat: return true; case Kleo::SMIMEFormat: return sign; // only on sign - there's no mp/encrypted for S/MIME } } QByteArray MessageComposer::Util::selectCharset(const QList &charsets, const QString &text) { for (const QByteArray &name : charsets) { // We use KCharsets::codecForName() instead of QTextCodec::codecForName() here, because // the former knows us-ascii is latin1. QStringEncoder codec(name.constData()); if (!codec.isValid()) { qCWarning(EDITOR_LOG) << "Could not get text codec for charset" << name; continue; } if (codec.encode(text); !codec.hasError()) { // Special check for us-ascii (needed because us-ascii is not exactly latin1). if (name == "us-ascii" && !KMime::isUsAscii(text)) { continue; } qCDebug(EDITOR_LOG) << "Chosen charset" << name; return name; } } qCDebug(EDITOR_LOG) << "No appropriate charset found."; return {}; } QStringList MessageComposer::Util::AttachmentKeywords() { return i18nc( "comma-separated list of keywords that are used to detect whether " "the user forgot to attach his attachment. Do not add space between words.", "attachment,attached") .split(QLatin1Char(',')); } QString MessageComposer::Util::cleanedUpHeaderString(const QString &s) { // remove invalid characters from the header strings QString res(s); res.remove(QChar::fromLatin1('\r')); res.replace(QChar::fromLatin1('\n'), QLatin1Char(' ')); return res.trimmed(); } KMime::Content *MessageComposer::Util::findTypeInMessage(KMime::Content *data, const QByteArray &mimeType, const QByteArray &subType) { if (!data->contentType()->isEmpty()) { if (mimeType.isEmpty() || subType.isEmpty()) { return data; } if ((mimeType == data->contentType()->mediaType()) && (subType == data->contentType(false)->subType())) { return data; } } const auto contents = data->contents(); for (auto child : contents) { if ((!child->contentType()->isEmpty()) && (mimeType == child->contentType()->mimeType()) && (subType == child->contentType()->subType())) { return child; } auto ret = findTypeInMessage(child, mimeType, subType); if (ret) { return ret; } } return nullptr; } bool MessageComposer::Util::hasMissingAttachments(const QStringList &attachmentKeywords, QTextDocument *doc, const QString &subj) { if (!doc) { return false; } QStringList attachWordsList = attachmentKeywords; QRegularExpression rx(QLatin1String("\\b") + attachWordsList.join(QLatin1String("\\b|\\b")) + QLatin1String("\\b"), QRegularExpression::CaseInsensitiveOption); // check whether the subject contains one of the attachment key words // unless the message is a reply or a forwarded message bool gotMatch = (stripOffPrefixes(subj) == subj) && (rx.match(subj).hasMatch()); if (!gotMatch) { // check whether the non-quoted text contains one of the attachment key // words static QRegularExpression quotationRx(QStringLiteral("^([ \\t]*([|>:}#]|[A-Za-z]+>))+")); QTextBlock end(doc->end()); for (QTextBlock it = doc->begin(); it != end; it = it.next()) { const QString line = it.text(); gotMatch = (!quotationRx.match(line).hasMatch()) && (rx.match(line).hasMatch()); if (gotMatch) { break; } } } if (!gotMatch) { return false; } return true; } static QStringList encodeIdn(const QStringList &emails) { QStringList encoded; encoded.reserve(emails.count()); for (const QString &email : emails) { encoded << KEmailAddress::normalizeAddressesAndEncodeIdn(email); } return encoded; } QStringList MessageComposer::Util::cleanEmailList(const QStringList &emails) { QStringList clean; clean.reserve(emails.count()); for (const QString &email : emails) { clean << KEmailAddress::extractEmailAddress(email); } return clean; } QStringList MessageComposer::Util::cleanUpEmailListAndEncoding(const QStringList &emails) { return cleanEmailList(encodeIdn(emails)); } void MessageComposer::Util::addCustomHeaders(const KMime::Message::Ptr &message, const QMap &custom) { QMapIterator customHeader(custom); while (customHeader.hasNext()) { customHeader.next(); auto header = new KMime::Headers::Generic(customHeader.key().constData()); header->fromUnicodeString(customHeader.value(), "utf-8"); message->setHeader(header); } }