diff --git a/framework/qml/AttachmentDelegate.qml b/framework/qml/AttachmentDelegate.qml --- a/framework/qml/AttachmentDelegate.qml +++ b/framework/qml/AttachmentDelegate.qml @@ -23,10 +23,12 @@ id: root property string name + property string type property string icon property alias actionIcon: actionButton.iconName signal clicked; signal execute; + signal publicKeyImport; width: content.width + Kube.Units.smallSpacing * 1.5 height: content.height + Kube.Units.smallSpacing @@ -69,6 +71,14 @@ text: root.name color: Kube.Colors.backgroundColor } + Kube.IconButton { + visible: root.type == "application/pgp-keys" + iconName: Kube.Icons.key_import_inverted + height: Kube.Units.gridUnit + width: height + onClicked: root.publicKeyImport() + padding: 0 + } Kube.IconButton { id: actionButton height: Kube.Units.gridUnit diff --git a/framework/qml/Icons.qml b/framework/qml/Icons.qml --- a/framework/qml/Icons.qml +++ b/framework/qml/Icons.qml @@ -63,6 +63,7 @@ property string secure: "document-encrypt" property string insecure: "document-decrypt" property string signed: "document-sign" + property string key_import_inverted: "view-certificate-import-inverted" property string addNew: "list-add" property string remove: "kube-list-remove-inverted" diff --git a/framework/qml/MailViewer.qml b/framework/qml/MailViewer.qml --- a/framework/qml/MailViewer.qml +++ b/framework/qml/MailViewer.qml @@ -283,13 +283,15 @@ delegate: AttachmentDelegate { name: model.name + type: model.type icon: model.iconName clip: true actionIcon: Kube.Icons.save_inverted onExecute: messageParser.attachments.saveAttachmentToDisk(messageParser.attachments.index(index, 0)) onClicked: messageParser.attachments.openAttachment(messageParser.attachments.index(index, 0)) + onPublicKeyImport: messageParser.attachments.importPublicKey(messageParser.attachments.index(index, 0)) } } } diff --git a/framework/src/domain/composercontroller.cpp b/framework/src/domain/composercontroller.cpp --- a/framework/src/domain/composercontroller.cpp +++ b/framework/src/domain/composercontroller.cpp @@ -135,7 +135,7 @@ SinkLog() << "Searching key for: " << mb.address(); asyncRun>(this, [mb] { - return MailCrypto::findKeys(QStringList{} << mb.address(), false, false, MailCrypto::OPENPGP); + return MailCrypto::findKeys(QStringList{} << mb.address(), false, false); }, [this, addressee, id](const std::vector &keys) { if (!keys.empty()) { @@ -463,18 +463,25 @@ }; }); + GpgME::Key attachedKey; std::vector signingKeys; if (getSign()) { signingKeys = getPersonalKeys().value>(); + Q_ASSERT(!signingKeys.empty()); + attachedKey = signingKeys[0]; } std::vector encryptionKeys; if (getEncrypt()) { //Encrypt to self so we can read the sent message - encryptionKeys += getPersonalKeys().value>(); + auto personalKeys = getPersonalKeys().value>(); + + attachedKey = personalKeys[0]; + + encryptionKeys += personalKeys; encryptionKeys += getRecipientKeys(); } - return MailTemplates::createMessage(mExistingMessage, toAddresses, ccAddresses, bccAddresses, getIdentity(), getSubject(), getBody(), getHtmlBody(), attachments, signingKeys, encryptionKeys); + return MailTemplates::createMessage(mExistingMessage, toAddresses, ccAddresses, bccAddresses, getIdentity(), getSubject(), getBody(), getHtmlBody(), attachments, signingKeys, encryptionKeys, attachedKey); } void ComposerController::send() diff --git a/framework/src/domain/mime/attachmentmodel.h b/framework/src/domain/mime/attachmentmodel.h --- a/framework/src/domain/mime/attachmentmodel.h +++ b/framework/src/domain/mime/attachmentmodel.h @@ -56,6 +56,8 @@ Q_INVOKABLE bool saveAttachmentToDisk(const QModelIndex &parent); Q_INVOKABLE bool openAttachment(const QModelIndex &index); + Q_INVOKABLE bool importPublicKey(const QModelIndex &index); + private: std::unique_ptr d; }; diff --git a/framework/src/domain/mime/attachmentmodel.cpp b/framework/src/domain/mime/attachmentmodel.cpp --- a/framework/src/domain/mime/attachmentmodel.cpp +++ b/framework/src/domain/mime/attachmentmodel.cpp @@ -32,6 +32,11 @@ #include #include +#include +#include + +#include + QString sizeHuman(float size) { QStringList list; @@ -210,6 +215,36 @@ return false; } +bool AttachmentModel::importPublicKey(const QModelIndex &index) +{ + Q_ASSERT(index.internalPointer()); + const auto part = static_cast(index.internalPointer()); + Q_ASSERT(part); + auto pkey = part->node()->decodedContent(); + + const auto *proto = QGpgME::openpgp(); + std::unique_ptr job(proto->importJob()); + auto result = job->exec(pkey); + + bool success = true; + + QString message; + if(result.numConsidered() == 0) { + message = tr("No keys were found in this attachment"); + success = false; + } else { + message = tr("%n Key(s) imported", "", result.numImported()); + if(result.numUnchanged() != 0) { + message += "\n" + tr("%n Key(s) were already imported", "", result.numUnchanged()); + } + } + + Kube::Fabric::Fabric{}.postMessage("notification", + {{"message", message}}); + + return success; +} + QModelIndex AttachmentModel::parent(const QModelIndex &) const { return QModelIndex(); diff --git a/framework/src/domain/mime/mailcrypto.h b/framework/src/domain/mime/mailcrypto.h --- a/framework/src/domain/mime/mailcrypto.h +++ b/framework/src/domain/mime/mailcrypto.h @@ -19,19 +19,24 @@ #pragma once -#include +#include "framework/src/errors.h" + #include #include + +#include + #include +#include + +namespace MailCrypto { + +Expected> +processCrypto(std::unique_ptr content, const std::vector &signingKeys, + const std::vector &encryptionKeys, const GpgME::Key &attachedKey); + +std::vector findKeys(const QStringList &filter, bool findPrivate = false, bool remote = false); + +void importKeys(const std::vector &keys); -namespace MailCrypto -{ - enum Protocol { - OPENPGP, - SMIME - }; - KMime::Content *processCrypto(KMime::Content *content, const std::vector &signingKeys, const std::vector &encryptionKeys, MailCrypto::Protocol protocol); - KMime::Content *sign(KMime::Content *content, const std::vector &signers); - std::vector findKeys(const QStringList &filter, bool findPrivate = false, bool remote = false, Protocol protocol = OPENPGP); - void importKeys(const std::vector &keys); -}; +}; // namespace MailCrypto diff --git a/framework/src/domain/mime/mailcrypto.cpp b/framework/src/domain/mime/mailcrypto.cpp --- a/framework/src/domain/mime/mailcrypto.cpp +++ b/framework/src/domain/mime/mailcrypto.cpp @@ -20,324 +20,28 @@ 02110-1301, USA. */ #include "mailcrypto.h" -#include -#include + +#include "framework/src/errors.h" + +#include #include -#include +#include #include -#include -#include +#include +#include +#include + +#include #include -#include +#include #include -#include - -/* - * FIXME: - * - * This code is WIP. - * It currently only implements OpenPGPMIMEFormat for signing. - * All the commented code are intentional leftovers that we can clean-up - * once all necessary signing mechanisms have been implemented. - * - * 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(); +#include +#include - // // fixing ContentTransferEncoding - // setBodyAndCTE(encodedBody, orig->contentType(), result); - // } - result->assemble(); - return result; -} +#include -// 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; - // } - // } +#include +#include // replace simple LFs by CRLFs for all MIME supporting CryptPlugs // according to RfC 2633, 3.1.1 Canonicalization @@ -404,63 +108,319 @@ } -KMime::Content *MailCrypto::processCrypto(KMime::Content *content, const std::vector &signingKeys, const std::vector &encryptionKeys, MailCrypto::Protocol protocol) +/** + * Get the given `key` in the armor format. + */ +Expected exportPublicKey(const GpgME::Key &key) { - const QGpgME::Protocol *const proto = protocol == MailCrypto::SMIME ? QGpgME::smime() : QGpgME::openpgp(); - Q_ASSERT(proto); - - auto signingMode = GpgME::Detached; - bool armor = true; - bool textMode = false; - const bool sign = !signingKeys.empty(); - const bool encrypt = !encryptionKeys.empty(); - - QByteArray resultContent; - QByteArray hashAlgo; - //Trust provided keys and don't check them for validity - bool alwaysTrust = true; - if (sign && encrypt) { - std::unique_ptr job(proto->signEncryptJob(armor, textMode)); - const auto res = job->exec(signingKeys, encryptionKeys, canonicalizeContent(content), alwaysTrust, resultContent); - if (res.first.error().code()) { - qWarning() << "Signing failed:" << res.first.error().asString(); - return nullptr; - } else { - hashAlgo = res.first.createdSignature(0).hashAlgorithmAsString(); - } - if (res.second.error().code()) { - qWarning() << "Encryption failed:" << res.second.error().asString(); - return nullptr; - } - } else if (sign) { - std::unique_ptr job(proto->signJob(armor, textMode)); - auto result = job->exec(signingKeys, canonicalizeContent(content), signingMode, resultContent); - if (result.error().code()) { - qWarning() << "Signing failed:" << result.error().asString(); - return nullptr; - } - hashAlgo = result.createdSignature(0).hashAlgorithmAsString(); - } else if (encrypt) { - std::unique_ptr job(proto->encryptJob(armor, textMode)); - const auto result = job->exec(encryptionKeys, canonicalizeContent(content), alwaysTrust, resultContent); - if (result.error().code()) { - qWarning() << "Encryption failed:" << result.error().asString(); - return nullptr; - } - hashAlgo = "pgp-sha1"; - } else { - qWarning() << "Not signing or encrypting"; - return nullptr; + // Not using the Qt API because it apparently blocks (the `result` signal is never + // triggered) + std::unique_ptr ctx(GpgME::Context::createForProtocol(GpgME::OpenPGP)); + ctx->setArmor(true); + + QGpgME::QByteArrayDataProvider dp; + GpgME::Data data(&dp); + + qDebug() << "Exporting public key:" << key.shortKeyID(); + auto error = ctx->exportPublicKeys(key.keyID(), data); + + if (error.code()) { + return makeUnexpected(error); } - return composeHeadersAndBody(content, resultContent, sign, hashAlgo); + return dp.data(); } -KMime::Content *MailCrypto::sign(KMime::Content *content, const std::vector &signers) +/** + * Create an Email with `msg` as a body and `key` as an attachment. + * + * Will create the given structure: + * + * + `multipart/mixed` + * - the given `msg` + * - `application/pgp-keys` (the given `key` as attachment) + * + * Used by the `createSignedEmail` and `createEncryptedEmail` functions. + */ +Expected> +appendPublicKey(std::unique_ptr msg, const GpgME::Key &key) { - return processCrypto(content, signers, {}, OPENPGP); + const auto publicKeyExportResult = exportPublicKey(key); + + if (!publicKeyExportResult) { + // "Could not export public key" + return makeUnexpected(publicKeyExportResult.error()); + } + + const auto publicKeyData = publicKeyExportResult.value(); + + auto result = std::unique_ptr(new KMime::Content); + result->contentType()->setMimeType("multipart/mixed"); + result->contentType()->setBoundary(KMime::multiPartBoundary()); + + KMime::Content *keyAttachment = new KMime::Content; + { + keyAttachment->contentType()->setMimeType("application/pgp-keys"); + keyAttachment->contentDisposition()->setDisposition(KMime::Headers::CDattachment); + keyAttachment->contentDisposition()->setFilename(QString("0x") + key.shortKeyID() + ".asc"); + keyAttachment->setBody(publicKeyData); + } + + msg->assemble(); + + result->addContent(msg.release()); + result->addContent(keyAttachment); + + result->assemble(); + + return result; } +Expected encrypt(const QByteArray &content, const std::vector &encryptionKeys) +{ + QByteArray resultData; + + const QGpgME::Protocol *const proto = QGpgME::openpgp(); + std::unique_ptr job(proto->encryptJob(/* armor = */ true)); + const auto result = job->exec(encryptionKeys, content, /* alwaysTrust = */ true, resultData); + + if (result.error().code()) { + qWarning() << "Encryption failed:" << result.error().asString(); + return makeUnexpected(result.error()); + } + + return resultData; +} + +Expected signAndEncrypt(const QByteArray &content, + const std::vector &signingKeys, const std::vector &encryptionKeys) +{ + QByteArray resultData; + + const QGpgME::Protocol *const proto = QGpgME::openpgp(); + std::unique_ptr job(proto->signEncryptJob(/* armor = */ true)); + const auto result = job->exec(signingKeys, encryptionKeys, content, /* alwaysTrust = */ true, resultData); + + if (result.first.error().code()) { + qWarning() << "Signing failed:" << result.first.error().asString(); + return makeUnexpected(result.first.error()); + } + + if (result.second.error().code()) { + qWarning() << "Encryption failed:" << result.second.error().asString(); + return makeUnexpected(result.second.error()); + } + + return resultData; +} + +/** + * Create a message part like this (according to RFC 3156 Section 4): + * + * - multipart/encrypted + * - application/pgp-encrypted (version information) + * - application/octet-stream (given encrypted data) + * + * Should not be used directly since the public key should be attached, hence + * the `createEncryptedEmail` function. + * + * The encrypted data can be generated by the `encrypt` or `signAndEncrypt` functions. + */ +std::unique_ptr createEncryptedPart(QByteArray encryptedData) +{ + auto result = std::unique_ptr(new KMime::Content); + + result->contentType()->setMimeType("multipart/encrypted"); + result->contentType()->setBoundary(KMime::multiPartBoundary()); + result->contentType()->setParameter("protocol", "application/pgp-encrypted"); + + KMime::Content *controlInformation = new KMime::Content; + { + controlInformation->contentType()->setMimeType("application/pgp-encrypted"); + controlInformation->contentDescription()->from7BitString("PGP/MIME version identification"); + controlInformation->setBody("Version: 1"); + + result->addContent(controlInformation); + } + + KMime::Content *encryptedPartPart = new KMime::Content; + { + const QString filename = "msg.asc"; + + encryptedPartPart->contentType()->setMimeType("application/octet-stream"); + encryptedPartPart->contentType()->setName(filename, "utf-8"); + + encryptedPartPart->contentDescription()->from7BitString("OpenPGP encrypted message"); + + encryptedPartPart->contentDisposition()->setDisposition(KMime::Headers::CDinline); + encryptedPartPart->contentDisposition()->setFilename(filename); + + encryptedPartPart->setBody(encryptedData); + + result->addContent(encryptedPartPart); + } + + return result; +} + +/** + * Create an encrypted (optionally signed) email with a public key attached to it. + * + * Will create a message like this: + * + * + `multipart/mixed` + * - `multipart/encrypted` + * + `application/pgp-encrypted + * + `application/octet-stream` (a generated encrypted version of the original message) + * - `application/pgp-keys` (the public key as attachment, which is the first of the + * `signingKeys`) + */ +Expected> +createEncryptedEmail(KMime::Content *content, const std::vector &encryptionKeys, + const GpgME::Key &attachedKey, const std::vector &signingKeys = {}) +{ + auto contentToEncrypt = canonicalizeContent(content); + + auto encryptionResult = signingKeys.empty() ? + encrypt(contentToEncrypt, encryptionKeys) : + signAndEncrypt(contentToEncrypt, signingKeys, encryptionKeys); + + if (!encryptionResult) { + return makeUnexpected(encryptionResult.error()); + } + + auto encryptedPart = createEncryptedPart(encryptionResult.value()); + + auto publicKeyAppendResult = appendPublicKey(std::move(encryptedPart), attachedKey); + + if(publicKeyAppendResult) { + publicKeyAppendResult.value()->assemble(); + } + + return publicKeyAppendResult; +} + +/** + * Sign the given content and returns the signing data and the algorithm used + * for integrity check in the "pgp-" format. + */ +Expected> +sign(const QByteArray &content, const std::vector &signingKeys) +{ + QByteArray resultData; + + const QGpgME::Protocol *const proto = QGpgME::openpgp(); + std::unique_ptr job(proto->signJob(/* armor = */ true)); + const auto result = job->exec(signingKeys, content, GpgME::Detached, resultData); + + if (result.error().code()) { + qWarning() << "Signing failed:" << result.error().asString(); + return makeUnexpected(result.error()); + } + + auto algo = result.createdSignature(0).hashAlgorithmAsString(); + // RFC 3156 Section 5: + // Hash-symbols are constructed [...] by converting the text name to lower + // case and prefixing it with the four characters "pgp-". + auto micAlg = (QString("pgp-") + algo).toLower(); + + return std::pair{resultData, micAlg}; +} + +/** + * Create a message part like this (according to RFC 3156 Section 5): + * + * + `multipart/signed` + * - whatever the given original `message` is (should be canonicalized) + * - `application/octet-stream` (the given `signature`) + * + * Should not be used directly since the public key should be attached, hence + * the `createSignedEmail` function. + * + * The signature can be generated by the `sign` function. + */ +std::unique_ptr createSignedPart( + std::unique_ptr message, const QByteArray &signature, const QString &micAlg) +{ + auto result = std::unique_ptr(new KMime::Content); + + result->contentType()->setMimeType("multipart/signed"); + result->contentType()->setBoundary(KMime::multiPartBoundary()); + result->contentType()->setParameter("micalg", micAlg); + result->contentType()->setParameter("protocol", "application/pgp-signature"); + + result->addContent(message.release()); + + KMime::Content *signedPartPart = new KMime::Content; + { + signedPartPart->contentType()->setMimeType("application/pgp-signature"); + signedPartPart->contentType()->setName("signature.asc", "utf-8"); + + signedPartPart->contentDescription()->from7BitString( + "This is a digitally signed message part"); + + signedPartPart->setBody(signature); + + result->addContent(signedPartPart); + } + + return result; +} + +/** + * Create a signed email with a public key attached to it. + * + * Will create a message like this: + * + * + `multipart/mixed` + * - `multipart/signed` + * + whatever the given original `content` is (should not be canonalized) + * + `application/octet-stream` (a generated signature of the original message) + * - `application/pgp-keys` (the public key as attachment, which is the first of the + * `signingKeys`) + */ +Expected> +createSignedEmail(std::unique_ptr content, + const std::vector &signingKeys, const GpgME::Key &attachedKey) +{ + Q_ASSERT(!signingKeys.empty()); + + auto contentToSign = canonicalizeContent(content.get()); + + auto signingResult = sign(contentToSign, signingKeys); + + if (!signingResult) { + return makeUnexpected(signingResult.error()); + } + + QByteArray signingData; + QString micAlg; + std::tie(signingData, micAlg) = signingResult.value(); + + auto signedPart = createSignedPart(std::move(content), signingData, micAlg); + + auto publicKeyAppendResult = appendPublicKey(std::move(signedPart), attachedKey); + + if (publicKeyAppendResult) { + publicKeyAppendResult.value()->assemble(); + } + + return publicKeyAppendResult; +} + +Expected> +MailCrypto::processCrypto(std::unique_ptr content, const std::vector &signingKeys, + const std::vector &encryptionKeys, const GpgME::Key &attachedKey) +{ + if (!encryptionKeys.empty()) { + return createEncryptedEmail(content.release(), encryptionKeys, attachedKey, signingKeys); + } else if (!signingKeys.empty()) { + return createSignedEmail(std::move(content), signingKeys, signingKeys[0]); + } else { + qWarning() << "Processing cryptography, but neither signing nor encrypting"; + return content; + } +} void MailCrypto::importKeys(const std::vector &keys) { @@ -470,16 +430,16 @@ job->exec(keys); } -static GpgME::KeyListResult listKeys(GpgME::Protocol protocol, const QStringList &patterns, bool secretOnly, int keyListMode, std::vector &keys) +static GpgME::KeyListResult listKeys(const QStringList &patterns, bool secretOnly, int keyListMode, std::vector &keys) { QByteArrayList list; std::transform(patterns.constBegin(), patterns.constEnd(), std::back_inserter(list), [] (const QString &s) { return s.toUtf8(); }); std::vector pattern; std::transform(list.constBegin(), list.constEnd(), std::back_inserter(pattern), [] (const QByteArray &s) { return s.constData(); }); pattern.push_back(0); GpgME::initializeLibrary(); - auto ctx = QSharedPointer{GpgME::Context::createForProtocol(protocol)}; + auto ctx = QSharedPointer{GpgME::Context::createForProtocol(GpgME::OpenPGP)}; ctx->setKeyListMode(keyListMode); if (const GpgME::Error err = ctx->startKeyListing(pattern.data(), secretOnly)) { return GpgME::KeyListResult(0, err); @@ -497,10 +457,10 @@ return result; } -std::vector MailCrypto::findKeys(const QStringList &filter, bool findPrivate, bool remote, Protocol protocol) +std::vector MailCrypto::findKeys(const QStringList &filter, bool findPrivate, bool remote) { std::vector keys; - GpgME::KeyListResult res = listKeys(protocol == SMIME ? GpgME::CMS : GpgME::OpenPGP, filter, findPrivate, remote ? GpgME::Extern : GpgME::Local, keys); + GpgME::KeyListResult res = listKeys(filter, findPrivate, remote ? GpgME::Extern : GpgME::Local, keys); if (res.error()) { qWarning() << "Failed to lookup keys: " << res.error().asString(); return keys; @@ -517,4 +477,3 @@ return keys; } - diff --git a/framework/src/domain/mime/mailtemplates.h b/framework/src/domain/mime/mailtemplates.h --- a/framework/src/domain/mime/mailtemplates.h +++ b/framework/src/domain/mime/mailtemplates.h @@ -38,5 +38,5 @@ void forward(const KMime::Message::Ptr &origMsg, const std::function &callback); QString plaintextContent(const KMime::Message::Ptr &origMsg); QString body(const KMime::Message::Ptr &msg, bool &isHtml); - KMime::Message::Ptr createMessage(KMime::Message::Ptr existingMessage, const QStringList &to, const QStringList &cc, const QStringList &bcc, const KMime::Types::Mailbox &from, const QString &subject, const QString &body, bool htmlBody, const QList &attachments, const std::vector &signingKeys = {}, const std::vector &encryptionKeys = {}); + KMime::Message::Ptr createMessage(KMime::Message::Ptr existingMessage, const QStringList &to, const QStringList &cc, const QStringList &bcc, const KMime::Types::Mailbox &from, const QString &subject, const QString &body, bool htmlBody, const QList &attachments, const std::vector &signingKeys = {}, const std::vector &encryptionKeys = {}, const GpgME::Key &attachedKey = {}); }; diff --git a/framework/src/domain/mime/mailtemplates.cpp b/framework/src/domain/mime/mailtemplates.cpp --- a/framework/src/domain/mime/mailtemplates.cpp +++ b/framework/src/domain/mime/mailtemplates.cpp @@ -1025,7 +1025,11 @@ return mailboxes; } -KMime::Message::Ptr MailTemplates::createMessage(KMime::Message::Ptr existingMessage, const QStringList &to, const QStringList &cc, const QStringList &bcc, const KMime::Types::Mailbox &from, const QString &subject, const QString &body, bool htmlBody, const QList &attachments, const std::vector &signingKeys, const std::vector &encryptionKeys) +KMime::Message::Ptr MailTemplates::createMessage(KMime::Message::Ptr existingMessage, + const QStringList &to, const QStringList &cc, const QStringList &bcc, + const KMime::Types::Mailbox &from, const QString &subject, const QString &body, bool htmlBody, + const QList &attachments, const std::vector &signingKeys, + const std::vector &encryptionKeys, const GpgME::Key &attachedKey) { auto mail = existingMessage; if (!mail) { @@ -1089,12 +1093,12 @@ QByteArray bodyData; if (!signingKeys.empty() || !encryptionKeys.empty()) { - auto result = MailCrypto::processCrypto(bodyPart.get(), signingKeys, encryptionKeys, MailCrypto::OPENPGP); + auto result = MailCrypto::processCrypto(std::move(bodyPart), signingKeys, encryptionKeys, attachedKey); if (!result) { - qWarning() << "Signing failed"; + qWarning() << "Crypto failed"; return {}; } - bodyData = result->encodedContent(); + bodyData = result.value()->encodedContent(); } else { if (!bodyPart->contentType(false)) { bodyPart->contentType(true)->setMimeType("text/plain"); diff --git a/framework/src/domain/mime/tests/mailtemplatetest.cpp b/framework/src/domain/mime/tests/mailtemplatetest.cpp --- a/framework/src/domain/mime/tests/mailtemplatetest.cpp +++ b/framework/src/domain/mime/tests/mailtemplatetest.cpp @@ -31,16 +31,16 @@ if (smime) { const QGpgME::Protocol *const backend = QGpgME::smime(); Q_ASSERT(backend); - job = backend->keyListJob(false); + job = backend->keyListJob(/* remote = */ false); } else { const QGpgME::Protocol *const backend = QGpgME::openpgp(); Q_ASSERT(backend); - job = backend->keyListJob(false); + job = backend->keyListJob(/* remote = */ false); } Q_ASSERT(job); std::vector< GpgME::Key > keys; - GpgME::KeyListResult res = job->exec(QStringList(), true, keys); + GpgME::KeyListResult res = job->exec(QStringList(), /* secretOnly = */ true, keys); if (!smime) { Q_ASSERT(keys.size() == 3); @@ -401,17 +401,25 @@ std::vector keys = getKeys(); - auto result = MailTemplates::createMessage({}, to, cc, bcc, from, subject, body, false, attachments, keys); + auto result = MailTemplates::createMessage({}, to, cc, bcc, from, subject, body, false, attachments, keys, {}, keys[0]); QVERIFY(result); // qWarning() << "---------------------------------"; // qWarning().noquote() << result->encodedContent(); // qWarning() << "---------------------------------"; QCOMPARE(result->subject()->asUnicodeString(), subject); QVERIFY(result->date(false)->dateTime().isValid()); - QVERIFY(result->contentType()->isMimeType("multipart/signed")); - const auto contents = result->contents(); + QCOMPARE(result->contentType()->mimeType(), "multipart/mixed"); + auto resultAttachments = result->attachments(); + QCOMPARE(resultAttachments.size(), 1); + QCOMPARE(resultAttachments[0]->contentDisposition()->filename(), "0x8F246DE6.asc"); + + auto signedMessage = result->contents()[0]; + + QVERIFY(signedMessage->contentType()->isMimeType("multipart/signed")); + + const auto contents = signedMessage->contents(); QCOMPARE(contents.size(), 2); { auto c = contents.at(0); @@ -441,9 +449,19 @@ QVERIFY(result); QCOMPARE(result->subject()->asUnicodeString(), subject); QVERIFY(result->date(false)->dateTime().isValid()); - QVERIFY(result->contentType()->isMimeType("multipart/signed")); - const auto contents = result->contents(); + QCOMPARE(result->contentType()->mimeType(), "multipart/mixed"); + auto resultAttachments = result->attachments(); + QCOMPARE(resultAttachments.size(), 3); + // It seems KMime searches for the attachments using depth-first + // search, so the public key is last + QCOMPARE(resultAttachments[2]->contentDisposition()->filename(), "0x8F246DE6.asc"); + + auto signedMessage = result->contents()[0]; + + QVERIFY(signedMessage->contentType()->isMimeType("multipart/signed")); + + const auto contents = signedMessage->contents(); QCOMPARE(contents.size(), 2); { auto c = contents.at(0); diff --git a/framework/src/errors.h b/framework/src/errors.h new file mode 100644 --- /dev/null +++ b/framework/src/errors.h @@ -0,0 +1,308 @@ +/* + Copyright (c) 2018 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 + +// A somewhat implementation of the expected monad, proposed here: +// https://isocpp.org/files/papers/n4015.pdf + +// A class used to differentiate errors and values when they are of the same type. +template +class Unexpected +{ + + static_assert(!std::is_same::value, "Cannot have an Unexpected void"); + +public: + Unexpected() = delete; + + constexpr explicit Unexpected(const Error &error) : mValue(error) {} + constexpr explicit Unexpected(Error &&error) : mValue(std::move(error)) {} + + // For implicit conversions when doing makeUnexpected(other) + template + constexpr explicit Unexpected(const Unexpected &error) : mValue(error.value()) + { + } + template + constexpr explicit Unexpected(Unexpected &&error) : mValue(std::move(error.value())) + { + } + + constexpr const Error &value() const & + { + return mValue; + } + Error &value() & + { + return mValue; + } + + constexpr const Error &&value() const && + { + return std::move(mValue); + } + Error &&value() && + { + return std::move(mValue); + } + +private: + Error mValue; +}; + +template +Unexpected::type> makeUnexpected(Error &&e) +{ + return Unexpected::type>(std::forward(e)); +} + +template +bool operator==(const Unexpected &lhs, const Unexpected &rhs) +{ + return lhs.value() == rhs.value(); +} + +template +bool operator!=(const Unexpected &lhs, const Unexpected &rhs) +{ + return lhs.value() != rhs.value(); +} + +namespace detail { + +namespace tags { +struct Expected +{}; +struct Unexpected +{}; +} // namespace tags + +// Write functions here when storage related and when Type != void +template +struct StorageBase +{ +protected: + // Rule of 5 {{{ + + StorageBase(const StorageBase &other) : mIsValue(other.mIsValue) + { + // This is a constructor, you have to construct object, not assign them + // (hence the placement new) + // + // Here's the problem: + // + // Object that are part of a union are not initialized (which is + // normal). If we replaced the placement new by a line like this: + // + // ``` + // mValue = other.mValue; + // ``` + // + // If overloaded, this will call `mValue.operator=(other.mValue);`, but + // since we're in the constructor, mValue is not initialized. This can + // cause big issues if `Type` / `Error` is not trivially (move) + // assignable. + // + // And so, the placement new allows us to call the constructor of + // `Type` or `Error` instead of its assignment operator. + if (mIsValue) { + new (std::addressof(mValue)) Type(other.mValue); + } else { + new (std::addressof(mError)) Unexpected(other.mError); + } + } + + StorageBase(StorageBase &&other) : mIsValue(other.mIsValue) + { + // If you're thinking WTF, see the comment in the copy constructor above. + if (mIsValue) { + new (std::addressof(mValue)) Type(std::move(other.mValue)); + } else { + new (std::addressof(mError)) Unexpected(std::move(other.mError)); + } + } + + constexpr StorageBase &operator=(const StorageBase &other) + { + mIsValue = other.mIsValue; + if (mIsValue) { + mValue = other.mValue; + } else { + mError = other.mError; + } + return *this; + } + + constexpr StorageBase &operator=(StorageBase &&other) + { + this->~StorageBase(); + mIsValue = other.mIsValue; + if (mIsValue) { + mValue = std::move(other.mValue); + } else { + mError = std::move(other.mError); + } + return *this; + } + + ~StorageBase() + { + if (mIsValue) { + mValue.~Type(); + } else { + mError.~Unexpected(); + } + } + + // }}} + + template + constexpr StorageBase(tags::Expected, Args &&... args) + : mValue(std::forward(args)...), mIsValue(true) + { + } + + template + constexpr StorageBase(tags::Unexpected, Args &&... args) + : mError(std::forward(args)...), mIsValue(false) + { + } + + union + { + Unexpected mError; + Type mValue; + }; + bool mIsValue; +}; + +// Write functions here when storage related and when Type == void +template +struct StorageBase +{ +protected: + constexpr StorageBase(tags::Expected) : mIsValue(true) {} + + template + constexpr StorageBase(tags::Unexpected, Args &&... args) + : mError(std::forward(args)...), mIsValue(false) + { + } + + Unexpected mError; + bool mIsValue; +}; + +// Write functions here when storage related, whether Type is void or not +template +struct Storage : StorageBase +{ +protected: + // Forward the construction to StorageBase + using StorageBase::StorageBase; +}; + +// Write functions here when dev API related and when Type != void +template +struct ExpectedBase : detail::Storage +{ + constexpr ExpectedBase() : detail::Storage(detail::tags::Expected{}) {} + + template + constexpr ExpectedBase(const Unexpected &error) + : detail::Storage(detail::tags::Unexpected{}, error) + { + } + template + constexpr ExpectedBase(Unexpected &&error) + : detail::Storage(detail::tags::Unexpected{}, std::move(error)) + { + } + + constexpr ExpectedBase(const Type &value) + : detail::Storage(detail::tags::Expected{}, value) + { + } + constexpr ExpectedBase(Type &&value) + : detail::Storage(detail::tags::Expected{}, std::move(value)) + { + } + + // Warning: will crash if this is an error. You should always check this is + // an expected value before calling `.value()` + constexpr const Type &value() const & + { + Q_ASSERT(this->mIsValue); + return this->mValue; + } + Type &&value() && + { + Q_ASSERT(this->mIsValue); + return std::move(this->mValue); + } +}; + +// Write functions here when dev API related and when Type == void +template +struct ExpectedBase : detail::Storage +{ + // Rewrite constructors for unexpected because Expected doesn't have direct access to it. + template + constexpr ExpectedBase(const Unexpected &error) + : detail::Storage(detail::tags::Unexpected{}, error) + { + } + template + constexpr ExpectedBase(Unexpected &&error) + : detail::Storage(detail::tags::Unexpected{}, std::move(error)) + { + } +}; + +} // namespace detail + +// Write functions here when dev API related, whether Type is void or not +template +class Expected : public detail::ExpectedBase +{ + static_assert(!std::is_same::value, "Expected with void Error is not implemented"); + +public: + using detail::ExpectedBase::ExpectedBase; + + constexpr const Error &error() const & + { + return this->mError.value(); + } + + constexpr bool isValue() const + { + return this->mIsValue; + } + constexpr explicit operator bool() const + { + return this->mIsValue; + } +}; diff --git a/icons/breeze/icons/actions/22/view-certificate-import-inverted.svg b/icons/breeze/icons/actions/22/view-certificate-import-inverted.svg new file mode 100644 --- /dev/null +++ b/icons/breeze/icons/actions/22/view-certificate-import-inverted.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/icons/breeze/icons/actions/22/view-certificate-import.svg b/icons/breeze/icons/actions/22/view-certificate-import.svg new file mode 100644 --- /dev/null +++ b/icons/breeze/icons/actions/22/view-certificate-import.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/icons/breeze/icons/actions/24/view-certificate-import-inverted.svg b/icons/breeze/icons/actions/24/view-certificate-import-inverted.svg new file mode 100644 --- /dev/null +++ b/icons/breeze/icons/actions/24/view-certificate-import-inverted.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/icons/breeze/icons/actions/24/view-certificate-import.svg b/icons/breeze/icons/actions/24/view-certificate-import.svg new file mode 100644 --- /dev/null +++ b/icons/breeze/icons/actions/24/view-certificate-import.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/icons/copybreeze.sh b/icons/copybreeze.sh --- a/icons/copybreeze.sh +++ b/icons/copybreeze.sh @@ -13,6 +13,7 @@ "document-decrypt.svg", "document-edit.svg", "document-encrypt.svg", + "view-certificate-import.svg", "document-save.svg", "document-sign.svg", "edit-delete.svg", diff --git a/tests/teststore.cpp b/tests/teststore.cpp --- a/tests/teststore.cpp +++ b/tests/teststore.cpp @@ -64,6 +64,19 @@ auto ccAddresses = toStringList(object["cc"].toList()); auto bccAddresses = toStringList(object["bcc"].toList()); + QList attachments = {}; + if (object.contains("attachments")) { + auto attachmentSpecs = object["attachments"].toList(); + for (int i = 0; i < attachmentSpecs.size(); ++i) { + auto const &spec = attachmentSpecs.at(i).toMap(); + attachments << Attachment{spec["name"].toString(), + spec["name"].toString(), + spec["mimeType"].toByteArray(), + false, + spec["data"].toByteArray()}; + } + } + KMime::Types::Mailbox mb; mb.fromUnicodeString("identity@example.org"); auto msg = MailTemplates::createMessage({}, @@ -74,7 +87,7 @@ object["subject"].toString(), object["body"].toString(), object["bodyIsHtml"].toBool(), - {}, + attachments, {}, {}); if (object.contains("messageId")) { diff --git a/views/conversation/main.qml b/views/conversation/main.qml --- a/views/conversation/main.qml +++ b/views/conversation/main.qml @@ -127,6 +127,81 @@ to: ["to@example.org"], unread: true }, + { + resource: "resource1", + date: "2017-07-20T17:47:29", + subject: "WithAttachment", + body: "Hi Mélanie,\n\nI'm sorry to start this on such late notice, but we'd like to get Foo and boo to woo next week, because the following weeks are unfortunately not possible for us.\n", + to: ["to@example.org"], + unread: true, + attachments: [ + { + name: "myImage.png", + mimeType: "image/png", + data: "no real data", + } + ], + }, + { + resource: "resource1", + date: "2017-07-20T17:47:29", + subject: "WithBadPKeyAttachment", + body: "Hi Mélanie,\n\nI'm sorry to start this on such late notice, but we'd like to get Foo and boo to woo next week, because the following weeks are unfortunately not possible for us.\n", + to: ["to@example.org"], + unread: true, + attachments: [ + { + name: "myKey.asc", + mimeType: "application/pgp-keys", + data: "no real data", + } + ], + }, + { + resource: "resource1", + date: "2017-07-20T17:47:29", + subject: "WithGoodPKeyAttachment", + body: "Hi Mélanie,\n\nI'm sorry to start this on such late notice, but we'd like to get Foo and boo to woo next week, because the following weeks are unfortunately not possible for us.\n", + to: ["to@example.org"], + unread: true, + attachments: [ + { + name: "myKey.asc", + mimeType: "application/pgp-keys", + data: +"-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBEr9ij4BCADaFvyhzV7IrCAr/sCvfoPerAd4dYIGTeCeBmInu3p4oEG0rXTB +2zL2t9zd7jVwCmYLsqb0Y4+7UIulBTSVa/SxsFkxPIzQaGd+CYpIpCl2P7oXBQH/ +365i/gvng4UTb5CytBp9MToft2tUgqvK/LD30fBWbWVE1ohmuGYDviJesuqJGeRG +KPOmjRk8LcXecZoNAnahy6y/rHPQzbC7LVazrWfdYCsZ1w202kwwLAPj0aNO6d4n +M9NYo26/mB+5+odJ5gbxfdKWQQOFCha8UzEXbZzjJsRNFhUyuEDEd2gBlhDm3jrY +ACT3u1adLJ1GsY6biN3u1IEUwi/7+uofZRPXABEBAAG0K3VuaXR0ZXN0IGtleSAo +bm8gcGFzc3dvcmQpIDx0ZXN0QGtvbGFiLm9yZz6JATgEEwECACIFAkr9ij4CGwMG +CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEI2YYMWPJG3mxggH/iDnmHhKI40r +bPvDPSMVFz4pNL5oYrGMjOUIz5ibjn9N19Fz/T5kxupbYVRbdcx6kRy4uQd97sJ8 +4985JkHEr/TSHne5p0F+tQLKq+WcJST+cbvkFR9m9WTZISOo+bP/rKGsf6GOGfl/ +vzObv8tF0E8Yy0Lu1lYdynnBRygT+VKt5GzcNzsS3Af1kgrnoQ1gVWjKueSR32hJ +BILfOpQlKP/RdrOND1N0uljaJBQsUmYDJ5Gd+YL0VX4/56tfqt4gcuqhiD+Vz6BG ++55gqwuFK4/o2gawPELjOLUy5dh/b6MDvWehbasRPcyT1fFm9YY6iku4ZEx8EzLv +IJKiXLAx1+i5AQ0ESv2KPgEIAO6+rYyBG0YBfacSx36VCrzvRe8V0CqmUkzIHZJ0 +EN/s95yCQwG0yC3M0KRGDzTeCXRik68h/qdw3KEgfZzu4rJAj9w/J4JMtcuhuCYL +rL4iP32hvLfqZDqwBaRCmlEkqArF0Jahb5SW3cPYZlE+I9I2V1xYX3bSZ7jcihAx +VWtkheYtZcDY3u/7cWZNUauGNKRh4E0+ToCBI+erEd5EPCQDQrL/e5pEj+s/+Coy +BvJeQdAPX/wjfYVe8t+5GDLqOvpbUBWJWUptv/oTd3wOtJCwwr/OWNeXf7ipgtoG +KpJgr+FHLOEb3cXtF1YPzwpTOs/J/bv3JdGyQ3Kx1BlTUzUAEQEAAYkBHwQYAQIA +CQUCSv2KPgIbDAAKCRCNmGDFjyRt5h0nB/40FPmVWhD2ok3opPRTwYMzUAOHkgMU +k2bJfIH185hMvnHLAPCgUMr8xvjcx3NphiRCaC6mabIxHI9hDAbi6uyDBNTyQtm2 +sl/r1vqjFcxX49l9yt0AgMy3284IdwK9xdlwMLY/MbCL9GKe/D6RmZ6i/4wpxHdP +9X3cGh66UW09NUO1Gria0isRfwf/OxkV+KxA7qxX2bWOHS3noUAj7I43MJCvTuAP +gTIgEfjdpx1C2Tv97SxoLZ4t6raztvmwqIyCQIuzukD0H9JGFjWT9bGY7obPl7hO +Bvr+rojxTJ3X+pzb2LJQwJnALL/VdIF3yHtGu2//Yfu4oxGGA0M90KiW +=an8Y +-----END PGP PUBLIC KEY BLOCK----- +", + } + ], + }, ] }], }