From b7e18a461fd14ec34723d689f644880964314f1b Mon Sep 17 00:00:00 2001 From: Christian Mollekopf Date: Thu, 3 Aug 2017 10:00:35 -0600 Subject: Commit missing files --- framework/src/CMakeLists.txt | 2 + framework/src/domain/mime/mailcrypto.cpp | 444 +++++++++++++++++++++ framework/src/domain/mime/mailcrypto.h | 30 ++ framework/src/domain/mime/mailtemplates.cpp | 5 +- .../src/domain/mime/tests/mailtemplatetest.cpp | 30 ++ 5 files changed, 507 insertions(+), 4 deletions(-) create mode 100644 framework/src/domain/mime/mailcrypto.cpp create mode 100644 framework/src/domain/mime/mailcrypto.h (limited to 'framework/src') diff --git a/framework/src/CMakeLists.txt b/framework/src/CMakeLists.txt index 85ad8344..8436705c 100644 --- a/framework/src/CMakeLists.txt +++ b/framework/src/CMakeLists.txt @@ -35,6 +35,7 @@ set(SRCS domain/mime/attachmentmodel.cpp domain/mime/partmodel.cpp domain/mime/mailtemplates.cpp + domain/mime/mailcrypto.cpp accounts/accountfactory.cpp accounts/accountsmodel.cpp fabric.cpp @@ -56,6 +57,7 @@ target_link_libraries(frameworkplugin KF5::Contacts KF5::Package KAsync + QGpgme ) install(TARGETS frameworkplugin DESTINATION ${FRAMEWORK_INSTALL_DIR}) diff --git a/framework/src/domain/mime/mailcrypto.cpp b/framework/src/domain/mime/mailcrypto.cpp new file mode 100644 index 00000000..a5565b7d --- /dev/null +++ b/framework/src/domain/mime/mailcrypto.cpp @@ -0,0 +1,444 @@ +/* + Copyright (c) 2016 Christian Mollekopf + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ +#include "mailcrypto.h" +#include +#include +#include +#include +#include + +/* + * Creating an ecrypted mail: + * * get keys (email -> fingreprint -> key) + * * Use Kleo::OpenPGPMIMEFormat, + * + */ + +// bool chooseCTE() +// { +// Q_Q(SinglepartJob); + +// auto allowed = KMime::encodingsForData(data); + +// if (!q->globalPart()->is8BitAllowed()) { +// allowed.removeAll(KMime::Headers::CE8Bit); +// } + +// #if 0 //TODO signing +// // In the following cases only QP and Base64 are allowed: +// // - the buffer will be OpenPGP/MIME signed and it contains trailing +// // whitespace (cf. RFC 3156) +// // - a line starts with "From " +// if ((willBeSigned && cf.hasTrailingWhitespace()) || +// cf.hasLeadingFrom()) { +// ret.removeAll(DwMime::kCte8bit); +// ret.removeAll(DwMime::kCte7bit); +// } +// #endif + +// if (contentTransferEncoding) { +// // Specific CTE set. Check that our data fits in it. +// if (!allowed.contains(contentTransferEncoding->encoding())) { +// q->setError(JobBase::BugError); +// q->setErrorText(i18n("%1 Content-Transfer-Encoding cannot correctly encode this message.", +// KMime::nameForEncoding(contentTransferEncoding->encoding()))); +// return false; +// // TODO improve error message in case 8bit is requested but not allowed. +// } +// } else { +// // No specific CTE set. Choose the best one. +// Q_ASSERT(!allowed.isEmpty()); +// contentTransferEncoding = new KMime::Headers::ContentTransferEncoding; +// contentTransferEncoding->setEncoding(allowed.first()); +// } +// qCDebug(MESSAGECOMPOSER_LOG) << "Settled on encoding" << KMime::nameForEncoding(contentTransferEncoding->encoding()); +// return true; +// } + +KMime::Content *createPart(const QByteArray &encodedBody, const QByteArray &mimeType, const QByteArray &charset) +{ + auto resultContent = new KMime::Content; + + auto contentType = new KMime::Headers::ContentType; + contentType->setMimeType(mimeType); + contentType->setMimeType(charset); + // if (!chooseCTE()) { + // Q_ASSERT(error()); + // emitResult(); + // return; + // } + + // Set headers. + // if (contentDescription) { + // resultContent->setHeader(contentDescription); + // } + // if (contentDisposition) { + // resultContent->setHeader(contentDisposition); + // } + // if (contentID) { + // resultContent->setHeader(contentID); + // } + // Q_ASSERT(contentTransferEncoding); // chooseCTE() created it if it didn't exist. + auto contentTransferEncoding = new KMime::Headers::ContentTransferEncoding; + auto allowed = KMime::encodingsForData(encodedBody); + Q_ASSERT(!allowed.isEmpty()); + contentTransferEncoding->setEncoding(allowed.first()); + resultContent->setHeader(contentTransferEncoding); + + if (contentType) { + resultContent->setHeader(contentType); + } + + // Set data. + resultContent->setBody(encodedBody); + return resultContent; +} + +KMime::Content *setBodyAndCTE(QByteArray &encodedBody, KMime::Headers::ContentType *contentType, KMime::Content *ret) +{ + // MessageComposer::Composer composer; + // MessageComposer::SinglepartJob cteJob(&composer); + auto part = createPart(encodedBody, contentType->mimeType(), contentType->charset()); + part->assemble(); + + // cteJob.contentType()->setMimeType(contentType->mimeType()); + // cteJob.contentType()->setCharset(contentType->charset()); + // cteJob.setData(encodedBody); + // cteJob.exec(); + // cteJob.content()->assemble(); + + ret->contentTransferEncoding()->setEncoding(part->contentTransferEncoding()->encoding()); + ret->setBody(part->encodedBody()); + + return ret; +} + +void makeToplevelContentType(KMime::Content *content, bool sign, const QByteArray &hashAlgo) +{ + //Kleo::CryptoMessageFormat format, + // switch (format) { + // default: + // case Kleo::InlineOpenPGPFormat: + // case Kleo::OpenPGPMIMEFormat: + if (sign) { + content->contentType()->setMimeType(QByteArrayLiteral("multipart/signed")); + content->contentType()->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-signature")); + content->contentType()->setParameter(QStringLiteral("micalg"), QString::fromLatin1(QByteArray(QByteArrayLiteral("pgp-") + hashAlgo)).toLower()); + + } else { + content->contentType()->setMimeType(QByteArrayLiteral("multipart/encrypted")); + content->contentType()->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-encrypted")); + } + return; + // case Kleo::SMIMEFormat: + // if (sign) { + // qCDebug(MESSAGECOMPOSER_LOG) << "setting headers for SMIME"; + // content->contentType()->setMimeType(QByteArrayLiteral("multipart/signed")); + // content->contentType()->setParameter(QStringLiteral("protocol"), QString::fromAscii("application/pkcs7-signature")); + // content->contentType()->setParameter(QStringLiteral("micalg"), QString::fromAscii(hashAlgo).toLower()); + // return; + // } + // // fall through (for encryption, there's no difference between + // // SMIME and SMIMEOpaque, since there is no mp/encrypted for + // // S/MIME) + // case Kleo::SMIMEOpaqueFormat: + + // qCDebug(MESSAGECOMPOSER_LOG) << "setting headers for SMIME/opaque"; + // content->contentType()->setMimeType(QByteArrayLiteral("application/pkcs7-mime")); + + // if (sign) { + // content->contentType()->setParameter(QStringLiteral("smime-type"), QString::fromAscii("signed-data")); + // } else { + // content->contentType()->setParameter(QStringLiteral("smime-type"), QString::fromAscii("enveloped-data")); + // } + // content->contentType()->setParameter(QStringLiteral("name"), QString::fromAscii("smime.p7m")); + // } +} + +void setNestedContentType(KMime::Content *content, bool sign) +{ +// , Kleo::CryptoMessageFormat format + // switch (format) { + // case Kleo::OpenPGPMIMEFormat: + if (sign) { + content->contentType()->setMimeType(QByteArrayLiteral("application/pgp-signature")); + content->contentType()->setParameter(QStringLiteral("name"), QString::fromLatin1("signature.asc")); + content->contentDescription()->from7BitString("This is a digitally signed message part."); + } else { + content->contentType()->setMimeType(QByteArrayLiteral("application/octet-stream")); + } + return; + // case Kleo::SMIMEFormat: + // if (sign) { + // content->contentType()->setMimeType(QByteArrayLiteral("application/pkcs7-signature")); + // content->contentType()->setParameter(QStringLiteral("name"), QString::fromAscii("smime.p7s")); + // return; + // } + // // fall through: + // default: + // case Kleo::InlineOpenPGPFormat: + // case Kleo::SMIMEOpaqueFormat: + // ; + // } +} + +void setNestedContentDisposition(KMime::Content *content, bool sign) +{ +// Kleo::CryptoMessageFormat format, + // if (!sign && format & Kleo::OpenPGPMIMEFormat) { + if (!sign) { + content->contentDisposition()->setDisposition(KMime::Headers::CDinline); + content->contentDisposition()->setFilename(QStringLiteral("msg.asc")); + // } else if (sign && format & Kleo::SMIMEFormat) { + // content->contentDisposition()->setDisposition(KMime::Headers::CDattachment); + // content->contentDisposition()->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 +// } +// } + +KMime::Content *composeHeadersAndBody(KMime::Content *orig, QByteArray encodedBody, bool sign, const QByteArray &hashAlgo) +{ + // Kleo::CryptoMessageFormat format, + KMime::Content *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 + // qDebug() << "making MIME message, format:" << format; + makeToplevelContentType(result, sign, hashAlgo); + + // if (makeMultiMime(sign)) { // sign/enc PGPMime, sign SMIME + if (true) { // sign/enc PGPMime, sign SMIME + + const QByteArray boundary = KMime::multiPartBoundary(); + result->contentType()->setBoundary(boundary); + + result->assemble(); + //qCDebug(MESSAGECOMPOSER_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. + KMime::Content *code = new KMime::Content; + setNestedContentType(code, sign); + setNestedContentDisposition(code, sign); + + if (sign) { // sign PGPMime, sign SMIME + // if (format & Kleo::AnySMIME) { // sign SMIME + // code->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64); + // code->contentTransferEncoding()->needToEncode(); + // code->setBody(encodedBody); + // } else { // sign PGPMmime + setBodyAndCTE(encodedBody, orig->contentType(), code); + // } + result->addContent(orig); + result->addContent(code); + } else { // enc PGPMime + setBodyAndCTE(encodedBody, orig->contentType(), code); + + // Build a MIME part holding the version information + // taking the body contents returned in + // structuring.data.bodyTextVersion. + KMime::Content *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->addContent(vers); + result->addContent(code); + } + } else { //enc SMIME, sign/enc SMIMEOpaque + result->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64); + result->contentDisposition()->setDisposition(KMime::Headers::CDattachment); + result->contentDisposition()->setFilename(QStringLiteral("smime.p7m")); + + result->assemble(); + //qCDebug(MESSAGECOMPOSER_LOG) << "processed header:" << result->head(); + + result->setBody(encodedBody); + } + // } else { // sign/enc PGPInline + // result->setHead(orig->head()); + // result->parse(); + + // // fixing ContentTransferEncoding + // setBodyAndCTE(encodedBody, orig->contentType(), result); + // } + result->assemble(); + return result; +} + +// bool binaryHint(Kleo::CryptoMessageFormat f) +// { +// switch (f) { +// case Kleo::SMIMEFormat: +// case Kleo::SMIMEOpaqueFormat: +// return true; +// default: +// case Kleo::OpenPGPMIMEFormat: +// case Kleo::InlineOpenPGPFormat: +// return false; +// } +// } +// + // GpgME::SignatureMode signingMode(Kleo::CryptoMessageFormat f) + // { + // switch (f) { + // case Kleo::SMIMEOpaqueFormat: + // return GpgME::NormalSignatureMode; + // case Kleo::InlineOpenPGPFormat: + // return GpgME::Clearsigned; + // default: + // case Kleo::SMIMEFormat: + // case Kleo::OpenPGPMIMEFormat: + // return GpgME::Detached; + // } + // } + +//Hardcoded OpenPGPGMIMEFormat for now +KMime::Content *MailCrypto::sign(KMime::Content *content, const std::vector &signers) +{ + + // if setContent hasn't been called, we assume that a subjob was added + // and we want to use that + // if (!d->content) { + // Q_ASSERT(d->subjobContents.size() == 1); + // d->content = d->subjobContents.first(); + // } + + //d->resultContent = new KMime::Content; + + // const QGpgME::Protocol *proto = nullptr; + // if (d->format & Kleo::AnyOpenPGP) { + // proto = QGpgME::openpgp(); + // } else if (d->format & Kleo::AnySMIME) { + // proto = QGpgME::smime(); + // } + + const QGpgME::Protocol *proto = QGpgME::openpgp(); + Q_ASSERT(proto); + + qDebug() << "creating signJob from:" << proto->name() << proto->displayName(); + // std::unique_ptr job(proto->signJob(!d->binaryHint(d->format), d->format == Kleo::InlineOpenPGPFormat)); + bool armor = true; + bool textMode = false; + std::unique_ptr job(proto->signJob(armor, textMode)); + // for now just do the main recipients + QByteArray signature; + + content->assemble(); + + // replace simple LFs by CRLFs for all MIME supporting CryptPlugs + // according to RfC 2633, 3.1.1 Canonicalization + QByteArray contentData; + // if (d->format & Kleo::InlineOpenPGPFormat) { + // content = d->content->body(); + // } else if (!(d->format & Kleo::SMIMEOpaqueFormat)) { + + // replace "From " and "--" at the beginning of lines + // with encoded versions according to RfC 3156, 3 + // Note: If any line begins with the string "From ", it is strongly + // suggested that either the Quoted-Printable or Base64 MIME encoding + // be applied. + const auto encoding = content->contentTransferEncoding()->encoding(); + if ((encoding == KMime::Headers::CEquPr || encoding == KMime::Headers::CE7Bit) + && !content->contentType(false)) { + QByteArray body = content->encodedBody(); + bool changed = false; + QList search; + QList replacements; + + search << "From " + << "from " + << "-"; + replacements << "From=20" + << "from=20" + << "=2D"; + + if (content->contentTransferEncoding()->encoding() == KMime::Headers::CE7Bit) { + for (int i = 0; i < search.size(); ++i) { + const auto pos = body.indexOf(search[i]); + if (pos == 0 || (pos > 0 && body.at(pos - 1) == '\n')) { + changed = true; + break; + } + } + if (changed) { + content->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr); + content->assemble(); + body = content->encodedBody(); + } + } + + for (int i = 0; i < search.size(); ++i) { + const auto pos = body.indexOf(search[i]); + if (pos == 0 || (pos > 0 && body.at(pos - 1) == '\n')) { + changed = true; + body.replace(pos, search[i].size(), replacements[i]); + } + } + + if (changed) { + qDebug() << "Content changed"; + content->setBody(body); + content->contentTransferEncoding()->setDecoded(false); + } + } + + contentData = KMime::LFtoCRLF(content->encodedContent()); + // } else { // SMimeOpaque doesn't need LFtoCRLF, else it gets munged + // contentData = content->encodedContent(); + // } + + auto signingMode = GpgME::Detached; + + // FIXME: Make this async + GpgME::SigningResult res = job->exec(signers, + contentData, + signingMode, + signature); + + // exec'ed jobs don't delete themselves + job->deleteLater(); + + if (res.error().code()) { + qWarning() << "signing failed:" << res.error().asString(); + // job->showErrorDialog( globalPart()->parentWidgetForGui() ); + // setError(res.error().code()); + // setErrorText(QString::fromLocal8Bit(res.error().asString())); + } else { + QByteArray signatureHashAlgo = res.createdSignature(0).hashAlgorithmAsString(); + bool sign = true; + return composeHeadersAndBody(content, signature, sign, signatureHashAlgo); + } + return nullptr; +} + diff --git a/framework/src/domain/mime/mailcrypto.h b/framework/src/domain/mime/mailcrypto.h new file mode 100644 index 00000000..2261182d --- /dev/null +++ b/framework/src/domain/mime/mailcrypto.h @@ -0,0 +1,30 @@ +/* + Copyright (c) 2016 Christian Mollekopf + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA. +*/ + +#pragma once + +#include +#include +#include +#include + +namespace MailCrypto +{ + KMime::Content *sign(KMime::Content *content, const std::vector &signers); +}; diff --git a/framework/src/domain/mime/mailtemplates.cpp b/framework/src/domain/mime/mailtemplates.cpp index dc713ee4..0380fa5c 100644 --- a/framework/src/domain/mime/mailtemplates.cpp +++ b/framework/src/domain/mime/mailtemplates.cpp @@ -977,11 +977,8 @@ KMime::Message::Ptr MailTemplates::createMessage(KMime::Message::Ptr existingMes for (const auto &attachment : attachments) { mail->addContent(createAttachmentPart(attachment.data, attachment.filename, attachment.isInline, attachment.mimeType, attachment.name)); } - bodyPart = createBodyPart(body.toUtf8()); - } else { - //FIXME same implementation as above for attachments - bodyPart = createBodyPart(body.toUtf8()); } + bodyPart = createBodyPart(body.toUtf8()); mail->assemble(); KMime::Content *signedResult = nullptr; diff --git a/framework/src/domain/mime/tests/mailtemplatetest.cpp b/framework/src/domain/mime/tests/mailtemplatetest.cpp index 62b17a6c..e814f75f 100644 --- a/framework/src/domain/mime/tests/mailtemplatetest.cpp +++ b/framework/src/domain/mime/tests/mailtemplatetest.cpp @@ -214,6 +214,36 @@ private slots: qWarning() << "---------------------------------"; QCOMPARE(result->subject()->asUnicodeString(), subject); QVERIFY(result->contentType()->isMimeType("multipart/signed")); + + const auto contents = result->contents(); + QCOMPARE(contents.size(), 2); + { + auto c = contents.at(0); + QVERIFY(c->contentType()->isMimeType("text/plain")); + } + { + auto c = contents.at(1); + QVERIFY(c->contentType()->isMimeType("application/pgp-signature")); + } + } + + void testCreatePlainMailWithAttachmentsSigned() + { + QStringList to = {{"to@example.org"}}; + QStringList cc = {{"cc@example.org"}};; + QStringList bcc = {{"bcc@example.org"}};; + KMime::Types::Mailbox from; + from.fromUnicodeString("from@example.org"); + QString subject = "subject"; + QString body = "body"; + QList attachments = {{"name", "filename", "mimetype", true, "inlineAttachment"}, {"name", "filename", "mimetype", false, "nonInlineAttachment"}}; + + std::vector keys = getKeys(); + + auto result = MailTemplates::createMessage({}, to, cc, bcc, from, subject, body, attachments, keys); + + QVERIFY(result); + QVERIFY(result->contentType()->isMimeType("multipart/signed")); } }; -- cgit v1.2.3