summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--framework/qml/AttachmentDelegate.qml10
-rw-r--r--framework/qml/Icons.qml1
-rw-r--r--framework/qml/MailViewer.qml2
-rw-r--r--framework/src/domain/composercontroller.cpp13
-rw-r--r--framework/src/domain/mime/attachmentmodel.cpp35
-rw-r--r--framework/src/domain/mime/attachmentmodel.h2
-rw-r--r--framework/src/domain/mime/mailcrypto.cpp691
-rw-r--r--framework/src/domain/mime/mailcrypto.h29
-rw-r--r--framework/src/domain/mime/mailtemplates.cpp12
-rw-r--r--framework/src/domain/mime/mailtemplates.h2
-rw-r--r--framework/src/domain/mime/tests/mailtemplatetest.cpp34
-rw-r--r--framework/src/errors.h308
-rw-r--r--icons/breeze/icons/actions/22/view-certificate-import-inverted.svg14
-rw-r--r--icons/breeze/icons/actions/22/view-certificate-import.svg14
-rw-r--r--icons/breeze/icons/actions/24/view-certificate-import-inverted.svg13
-rw-r--r--icons/breeze/icons/actions/24/view-certificate-import.svg13
-rwxr-xr-xicons/copybreeze.sh1
-rw-r--r--tests/teststore.cpp15
-rw-r--r--views/conversation/main.qml75
19 files changed, 889 insertions, 395 deletions
diff --git a/framework/qml/AttachmentDelegate.qml b/framework/qml/AttachmentDelegate.qml
index 3c308e65..4469cbdd 100644
--- a/framework/qml/AttachmentDelegate.qml
+++ b/framework/qml/AttachmentDelegate.qml
@@ -23,10 +23,12 @@ Item {
23 id: root 23 id: root
24 24
25 property string name 25 property string name
26 property string type
26 property string icon 27 property string icon
27 property alias actionIcon: actionButton.iconName 28 property alias actionIcon: actionButton.iconName
28 signal clicked; 29 signal clicked;
29 signal execute; 30 signal execute;
31 signal publicKeyImport;
30 32
31 width: content.width + Kube.Units.smallSpacing * 1.5 33 width: content.width + Kube.Units.smallSpacing * 1.5
32 height: content.height + Kube.Units.smallSpacing 34 height: content.height + Kube.Units.smallSpacing
@@ -70,6 +72,14 @@ Item {
70 color: Kube.Colors.backgroundColor 72 color: Kube.Colors.backgroundColor
71 } 73 }
72 Kube.IconButton { 74 Kube.IconButton {
75 visible: root.type == "application/pgp-keys"
76 iconName: Kube.Icons.key_import_inverted
77 height: Kube.Units.gridUnit
78 width: height
79 onClicked: root.publicKeyImport()
80 padding: 0
81 }
82 Kube.IconButton {
73 id: actionButton 83 id: actionButton
74 height: Kube.Units.gridUnit 84 height: Kube.Units.gridUnit
75 width: height 85 width: height
diff --git a/framework/qml/Icons.qml b/framework/qml/Icons.qml
index 2afe840e..4dfae3d7 100644
--- a/framework/qml/Icons.qml
+++ b/framework/qml/Icons.qml
@@ -63,6 +63,7 @@ Item {
63 property string secure: "document-encrypt" 63 property string secure: "document-encrypt"
64 property string insecure: "document-decrypt" 64 property string insecure: "document-decrypt"
65 property string signed: "document-sign" 65 property string signed: "document-sign"
66 property string key_import_inverted: "view-certificate-import-inverted"
66 67
67 property string addNew: "list-add" 68 property string addNew: "list-add"
68 property string remove: "kube-list-remove-inverted" 69 property string remove: "kube-list-remove-inverted"
diff --git a/framework/qml/MailViewer.qml b/framework/qml/MailViewer.qml
index 565adedd..e9ffd108 100644
--- a/framework/qml/MailViewer.qml
+++ b/framework/qml/MailViewer.qml
@@ -283,6 +283,7 @@ Rectangle {
283 283
284 delegate: AttachmentDelegate { 284 delegate: AttachmentDelegate {
285 name: model.name 285 name: model.name
286 type: model.type
286 icon: model.iconName 287 icon: model.iconName
287 288
288 clip: true 289 clip: true
@@ -290,6 +291,7 @@ Rectangle {
290 actionIcon: Kube.Icons.save_inverted 291 actionIcon: Kube.Icons.save_inverted
291 onExecute: messageParser.attachments.saveAttachmentToDisk(messageParser.attachments.index(index, 0)) 292 onExecute: messageParser.attachments.saveAttachmentToDisk(messageParser.attachments.index(index, 0))
292 onClicked: messageParser.attachments.openAttachment(messageParser.attachments.index(index, 0)) 293 onClicked: messageParser.attachments.openAttachment(messageParser.attachments.index(index, 0))
294 onPublicKeyImport: messageParser.attachments.importPublicKey(messageParser.attachments.index(index, 0))
293 } 295 }
294 } 296 }
295 } 297 }
diff --git a/framework/src/domain/composercontroller.cpp b/framework/src/domain/composercontroller.cpp
index 2286a71b..a71e66f9 100644
--- a/framework/src/domain/composercontroller.cpp
+++ b/framework/src/domain/composercontroller.cpp
@@ -135,7 +135,7 @@ public:
135 SinkLog() << "Searching key for: " << mb.address(); 135 SinkLog() << "Searching key for: " << mb.address();
136 asyncRun<std::vector<GpgME::Key>>(this, 136 asyncRun<std::vector<GpgME::Key>>(this,
137 [mb] { 137 [mb] {
138 return MailCrypto::findKeys(QStringList{} << mb.address(), false, false, MailCrypto::OPENPGP); 138 return MailCrypto::findKeys(QStringList{} << mb.address(), false, false);
139 }, 139 },
140 [this, addressee, id](const std::vector<GpgME::Key> &keys) { 140 [this, addressee, id](const std::vector<GpgME::Key> &keys) {
141 if (!keys.empty()) { 141 if (!keys.empty()) {
@@ -463,18 +463,25 @@ KMime::Message::Ptr ComposerController::assembleMessage()
463 }; 463 };
464 }); 464 });
465 465
466 GpgME::Key attachedKey;
466 std::vector<GpgME::Key> signingKeys; 467 std::vector<GpgME::Key> signingKeys;
467 if (getSign()) { 468 if (getSign()) {
468 signingKeys = getPersonalKeys().value<std::vector<GpgME::Key>>(); 469 signingKeys = getPersonalKeys().value<std::vector<GpgME::Key>>();
470 Q_ASSERT(!signingKeys.empty());
471 attachedKey = signingKeys[0];
469 } 472 }
470 std::vector<GpgME::Key> encryptionKeys; 473 std::vector<GpgME::Key> encryptionKeys;
471 if (getEncrypt()) { 474 if (getEncrypt()) {
472 //Encrypt to self so we can read the sent message 475 //Encrypt to self so we can read the sent message
473 encryptionKeys += getPersonalKeys().value<std::vector<GpgME::Key>>(); 476 auto personalKeys = getPersonalKeys().value<std::vector<GpgME::Key>>();
477
478 attachedKey = personalKeys[0];
479
480 encryptionKeys += personalKeys;
474 encryptionKeys += getRecipientKeys(); 481 encryptionKeys += getRecipientKeys();
475 } 482 }
476 483
477 return MailTemplates::createMessage(mExistingMessage, toAddresses, ccAddresses, bccAddresses, getIdentity(), getSubject(), getBody(), getHtmlBody(), attachments, signingKeys, encryptionKeys); 484 return MailTemplates::createMessage(mExistingMessage, toAddresses, ccAddresses, bccAddresses, getIdentity(), getSubject(), getBody(), getHtmlBody(), attachments, signingKeys, encryptionKeys, attachedKey);
478} 485}
479 486
480void ComposerController::send() 487void ComposerController::send()
diff --git a/framework/src/domain/mime/attachmentmodel.cpp b/framework/src/domain/mime/attachmentmodel.cpp
index 2eb2cc13..8b12679b 100644
--- a/framework/src/domain/mime/attachmentmodel.cpp
+++ b/framework/src/domain/mime/attachmentmodel.cpp
@@ -32,6 +32,11 @@
32#include <QUrl> 32#include <QUrl>
33#include <QMimeDatabase> 33#include <QMimeDatabase>
34 34
35#include <QGpgME/ImportJob>
36#include <QGpgME/Protocol>
37
38#include <memory>
39
35QString sizeHuman(float size) 40QString sizeHuman(float size)
36{ 41{
37 QStringList list; 42 QStringList list;
@@ -210,6 +215,36 @@ bool AttachmentModel::openAttachment(const QModelIndex &index)
210 return false; 215 return false;
211} 216}
212 217
218bool AttachmentModel::importPublicKey(const QModelIndex &index)
219{
220 Q_ASSERT(index.internalPointer());
221 const auto part = static_cast<MimeTreeParser::MessagePart *>(index.internalPointer());
222 Q_ASSERT(part);
223 auto pkey = part->node()->decodedContent();
224
225 const auto *proto = QGpgME::openpgp();
226 std::unique_ptr<QGpgME::ImportJob> job(proto->importJob());
227 auto result = job->exec(pkey);
228
229 bool success = true;
230
231 QString message;
232 if(result.numConsidered() == 0) {
233 message = tr("No keys were found in this attachment");
234 success = false;
235 } else {
236 message = tr("%n Key(s) imported", "", result.numImported());
237 if(result.numUnchanged() != 0) {
238 message += "\n" + tr("%n Key(s) were already imported", "", result.numUnchanged());
239 }
240 }
241
242 Kube::Fabric::Fabric{}.postMessage("notification",
243 {{"message", message}});
244
245 return success;
246}
247
213QModelIndex AttachmentModel::parent(const QModelIndex &) const 248QModelIndex AttachmentModel::parent(const QModelIndex &) const
214{ 249{
215 return QModelIndex(); 250 return QModelIndex();
diff --git a/framework/src/domain/mime/attachmentmodel.h b/framework/src/domain/mime/attachmentmodel.h
index 93ba8d02..d599b1f0 100644
--- a/framework/src/domain/mime/attachmentmodel.h
+++ b/framework/src/domain/mime/attachmentmodel.h
@@ -56,6 +56,8 @@ public:
56 Q_INVOKABLE bool saveAttachmentToDisk(const QModelIndex &parent); 56 Q_INVOKABLE bool saveAttachmentToDisk(const QModelIndex &parent);
57 Q_INVOKABLE bool openAttachment(const QModelIndex &index); 57 Q_INVOKABLE bool openAttachment(const QModelIndex &index);
58 58
59 Q_INVOKABLE bool importPublicKey(const QModelIndex &index);
60
59private: 61private:
60 std::unique_ptr<AttachmentModelPrivate> d; 62 std::unique_ptr<AttachmentModelPrivate> d;
61}; 63};
diff --git a/framework/src/domain/mime/mailcrypto.cpp b/framework/src/domain/mime/mailcrypto.cpp
index 62a71e42..4e20c84b 100644
--- a/framework/src/domain/mime/mailcrypto.cpp
+++ b/framework/src/domain/mime/mailcrypto.cpp
@@ -20,324 +20,28 @@
20 02110-1301, USA. 20 02110-1301, USA.
21*/ 21*/
22#include "mailcrypto.h" 22#include "mailcrypto.h"
23#include <QGpgME/Protocol> 23
24#include <QGpgME/SignJob> 24#include "framework/src/errors.h"
25
26#include <QGpgME/DataProvider>
25#include <QGpgME/EncryptJob> 27#include <QGpgME/EncryptJob>
26#include <QGpgME/SignEncryptJob> 28#include <QGpgME/ExportJob>
27#include <QGpgME/ImportFromKeyserverJob> 29#include <QGpgME/ImportFromKeyserverJob>
28#include <gpgme++/global.h> 30#include <QGpgME/Protocol>
29#include <gpgme++/signingresult.h> 31#include <QGpgME/SignEncryptJob>
32#include <QGpgME/SignJob>
33
34#include <gpgme++/data.h>
30#include <gpgme++/encryptionresult.h> 35#include <gpgme++/encryptionresult.h>
31#include <gpgme++/keylistresult.h> 36#include <gpgme++/global.h>
32#include <gpgme++/importresult.h> 37#include <gpgme++/importresult.h>
33#include <QDebug> 38#include <gpgme++/keylistresult.h>
34 39#include <gpgme++/signingresult.h>
35/*
36 * FIXME:
37 *
38 * This code is WIP.
39 * It currently only implements OpenPGPMIMEFormat for signing.
40 * All the commented code are intentional leftovers that we can clean-up
41 * once all necessary signing mechanisms have been implemented.
42 *
43 * Creating an ecrypted mail:
44 * * get keys (email -> fingreprint -> key)
45 * * Use Kleo::OpenPGPMIMEFormat,
46 *
47 */
48
49// bool chooseCTE()
50// {
51// Q_Q(SinglepartJob);
52
53// auto allowed = KMime::encodingsForData(data);
54
55// if (!q->globalPart()->is8BitAllowed()) {
56// allowed.removeAll(KMime::Headers::CE8Bit);
57// }
58
59// #if 0 //TODO signing
60// // In the following cases only QP and Base64 are allowed:
61// // - the buffer will be OpenPGP/MIME signed and it contains trailing
62// // whitespace (cf. RFC 3156)
63// // - a line starts with "From "
64// if ((willBeSigned && cf.hasTrailingWhitespace()) ||
65// cf.hasLeadingFrom()) {
66// ret.removeAll(DwMime::kCte8bit);
67// ret.removeAll(DwMime::kCte7bit);
68// }
69// #endif
70
71// if (contentTransferEncoding) {
72// // Specific CTE set. Check that our data fits in it.
73// if (!allowed.contains(contentTransferEncoding->encoding())) {
74// q->setError(JobBase::BugError);
75// q->setErrorText(i18n("%1 Content-Transfer-Encoding cannot correctly encode this message.",
76// KMime::nameForEncoding(contentTransferEncoding->encoding())));
77// return false;
78// // TODO improve error message in case 8bit is requested but not allowed.
79// }
80// } else {
81// // No specific CTE set. Choose the best one.
82// Q_ASSERT(!allowed.isEmpty());
83// contentTransferEncoding = new KMime::Headers::ContentTransferEncoding;
84// contentTransferEncoding->setEncoding(allowed.first());
85// }
86// qCDebug(MESSAGECOMPOSER_LOG) << "Settled on encoding" << KMime::nameForEncoding(contentTransferEncoding->encoding());
87// return true;
88// }
89
90KMime::Content *createPart(const QByteArray &encodedBody, const QByteArray &mimeType, const QByteArray &charset)
91{
92 auto resultContent = new KMime::Content;
93
94 auto contentType = new KMime::Headers::ContentType;
95 contentType->setMimeType(mimeType);
96 contentType->setMimeType(charset);
97 // if (!chooseCTE()) {
98 // Q_ASSERT(error());
99 // emitResult();
100 // return;
101 // }
102
103 // Set headers.
104 // if (contentDescription) {
105 // resultContent->setHeader(contentDescription);
106 // }
107 // if (contentDisposition) {
108 // resultContent->setHeader(contentDisposition);
109 // }
110 // if (contentID) {
111 // resultContent->setHeader(contentID);
112 // }
113 // Q_ASSERT(contentTransferEncoding); // chooseCTE() created it if it didn't exist.
114 auto contentTransferEncoding = new KMime::Headers::ContentTransferEncoding;
115 auto allowed = KMime::encodingsForData(encodedBody);
116 Q_ASSERT(!allowed.isEmpty());
117 contentTransferEncoding->setEncoding(allowed.first());
118 resultContent->setHeader(contentTransferEncoding);
119
120 if (contentType) {
121 resultContent->setHeader(contentType);
122 }
123
124 // Set data.
125 resultContent->setBody(encodedBody);
126 return resultContent;
127}
128
129KMime::Content *setBodyAndCTE(QByteArray &encodedBody, KMime::Headers::ContentType *contentType, KMime::Content *ret)
130{
131 // MessageComposer::Composer composer;
132 // MessageComposer::SinglepartJob cteJob(&composer);
133 auto part = createPart(encodedBody, contentType->mimeType(), contentType->charset());
134 part->assemble();
135
136 // cteJob.contentType()->setMimeType(contentType->mimeType());
137 // cteJob.contentType()->setCharset(contentType->charset());
138 // cteJob.setData(encodedBody);
139 // cteJob.exec();
140 // cteJob.content()->assemble();
141
142 ret->contentTransferEncoding()->setEncoding(part->contentTransferEncoding()->encoding());
143 ret->setBody(part->encodedBody());
144
145 return ret;
146}
147
148void makeToplevelContentType(KMime::Content *content, bool sign, const QByteArray &hashAlgo)
149{
150 //Kleo::CryptoMessageFormat format,
151 // switch (format) {
152 // default:
153 // case Kleo::InlineOpenPGPFormat:
154 // case Kleo::OpenPGPMIMEFormat:
155 if (sign) {
156 content->contentType()->setMimeType(QByteArrayLiteral("multipart/signed"));
157 content->contentType()->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-signature"));
158 content->contentType()->setParameter(QStringLiteral("micalg"), QString::fromLatin1(QByteArray(QByteArrayLiteral("pgp-") + hashAlgo)).toLower());
159
160 } else {
161 content->contentType()->setMimeType(QByteArrayLiteral("multipart/encrypted"));
162 content->contentType()->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-encrypted"));
163 }
164 return;
165 // case Kleo::SMIMEFormat:
166 // if (sign) {
167 // qCDebug(MESSAGECOMPOSER_LOG) << "setting headers for SMIME";
168 // content->contentType()->setMimeType(QByteArrayLiteral("multipart/signed"));
169 // content->contentType()->setParameter(QStringLiteral("protocol"), QString::fromAscii("application/pkcs7-signature"));
170 // content->contentType()->setParameter(QStringLiteral("micalg"), QString::fromAscii(hashAlgo).toLower());
171 // return;
172 // }
173 // // fall through (for encryption, there's no difference between
174 // // SMIME and SMIMEOpaque, since there is no mp/encrypted for
175 // // S/MIME)
176 // case Kleo::SMIMEOpaqueFormat:
177
178 // qCDebug(MESSAGECOMPOSER_LOG) << "setting headers for SMIME/opaque";
179 // content->contentType()->setMimeType(QByteArrayLiteral("application/pkcs7-mime"));
180
181 // if (sign) {
182 // content->contentType()->setParameter(QStringLiteral("smime-type"), QString::fromAscii("signed-data"));
183 // } else {
184 // content->contentType()->setParameter(QStringLiteral("smime-type"), QString::fromAscii("enveloped-data"));
185 // }
186 // content->contentType()->setParameter(QStringLiteral("name"), QString::fromAscii("smime.p7m"));
187 // }
188}
189
190void setNestedContentType(KMime::Content *content, bool sign)
191{
192// , Kleo::CryptoMessageFormat format
193 // switch (format) {
194 // case Kleo::OpenPGPMIMEFormat:
195 if (sign) {
196 content->contentType()->setMimeType(QByteArrayLiteral("application/pgp-signature"));
197 content->contentType()->setParameter(QStringLiteral("name"), QString::fromLatin1("signature.asc"));
198 content->contentDescription()->from7BitString("This is a digitally signed message part.");
199 } else {
200 content->contentType()->setMimeType(QByteArrayLiteral("application/octet-stream"));
201 }
202 return;
203 // case Kleo::SMIMEFormat:
204 // if (sign) {
205 // content->contentType()->setMimeType(QByteArrayLiteral("application/pkcs7-signature"));
206 // content->contentType()->setParameter(QStringLiteral("name"), QString::fromAscii("smime.p7s"));
207 // return;
208 // }
209 // // fall through:
210 // default:
211 // case Kleo::InlineOpenPGPFormat:
212 // case Kleo::SMIMEOpaqueFormat:
213 // ;
214 // }
215}
216
217void setNestedContentDisposition(KMime::Content *content, bool sign)
218{
219// Kleo::CryptoMessageFormat format,
220 // if (!sign && format & Kleo::OpenPGPMIMEFormat) {
221 if (!sign) {
222 content->contentDisposition()->setDisposition(KMime::Headers::CDinline);
223 content->contentDisposition()->setFilename(QStringLiteral("msg.asc"));
224 // } else if (sign && format & Kleo::SMIMEFormat) {
225 // content->contentDisposition()->setDisposition(KMime::Headers::CDattachment);
226 // content->contentDisposition()->setFilename(QStringLiteral("smime.p7s"));
227 }
228}
229
230// bool MessageComposer::Util::makeMultiMime(Kleo::CryptoMessageFormat format, bool sign)
231// {
232// switch (format) {
233// default:
234// case Kleo::InlineOpenPGPFormat:
235// case Kleo::SMIMEOpaqueFormat: return false;
236// case Kleo::OpenPGPMIMEFormat: return true;
237// case Kleo::SMIMEFormat: return sign; // only on sign - there's no mp/encrypted for S/MIME
238// }
239// }
240
241KMime::Content *composeHeadersAndBody(KMime::Content *orig, QByteArray encodedBody, bool sign, const QByteArray &hashAlgo)
242{
243 // Kleo::CryptoMessageFormat format,
244 KMime::Content *result = new KMime::Content;
245
246 // called should have tested that the signing/encryption failed
247 Q_ASSERT(!encodedBody.isEmpty());
248
249 // if (!(format & Kleo::InlineOpenPGPFormat)) { // make a MIME message
250 // qDebug() << "making MIME message, format:" << format;
251 makeToplevelContentType(result, sign, hashAlgo);
252
253 // if (makeMultiMime(sign)) { // sign/enc PGPMime, sign SMIME
254 if (true) { // sign/enc PGPMime, sign SMIME
255
256 const QByteArray boundary = KMime::multiPartBoundary();
257 result->contentType()->setBoundary(boundary);
258
259 result->assemble();
260 //qCDebug(MESSAGECOMPOSER_LOG) << "processed header:" << result->head();
261
262 // Build the encapsulated MIME parts.
263 // Build a MIME part holding the code information
264 // taking the body contents returned in ciphertext.
265 KMime::Content *code = new KMime::Content;
266 setNestedContentType(code, sign);
267 setNestedContentDisposition(code, sign);
268
269 if (sign) { // sign PGPMime, sign SMIME
270 // if (format & Kleo::AnySMIME) { // sign SMIME
271 // code->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64);
272 // code->contentTransferEncoding()->needToEncode();
273 // code->setBody(encodedBody);
274 // } else { // sign PGPMmime
275 setBodyAndCTE(encodedBody, orig->contentType(), code);
276 // }
277 result->addContent(orig);
278 result->addContent(code);
279 } else { // enc PGPMime
280 setBodyAndCTE(encodedBody, orig->contentType(), code);
281
282 // Build a MIME part holding the version information
283 // taking the body contents returned in
284 // structuring.data.bodyTextVersion.
285 KMime::Content *vers = new KMime::Content;
286 vers->contentType()->setMimeType("application/pgp-encrypted");
287 vers->contentDisposition()->setDisposition(KMime::Headers::CDattachment);
288 vers->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit);
289 vers->setBody("Version: 1");
290
291 result->addContent(vers);
292 result->addContent(code);
293 }
294 } else { //enc SMIME, sign/enc SMIMEOpaque
295 result->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64);
296 result->contentDisposition()->setDisposition(KMime::Headers::CDattachment);
297 result->contentDisposition()->setFilename(QStringLiteral("smime.p7m"));
298
299 result->assemble();
300 //qCDebug(MESSAGECOMPOSER_LOG) << "processed header:" << result->head();
301
302 result->setBody(encodedBody);
303 }
304 // } else { // sign/enc PGPInline
305 // result->setHead(orig->head());
306 // result->parse();
307 40
308 // // fixing ContentTransferEncoding 41#include <QDebug>
309 // setBodyAndCTE(encodedBody, orig->contentType(), result);
310 // }
311 result->assemble();
312 return result;
313}
314 42
315// bool binaryHint(Kleo::CryptoMessageFormat f) 43#include <future>
316// { 44#include <utility>
317// switch (f) {
318// case Kleo::SMIMEFormat:
319// case Kleo::SMIMEOpaqueFormat:
320// return true;
321// default:
322// case Kleo::OpenPGPMIMEFormat:
323// case Kleo::InlineOpenPGPFormat:
324// return false;
325// }
326// }
327//
328 // GpgME::SignatureMode signingMode(Kleo::CryptoMessageFormat f)
329 // {
330 // switch (f) {
331 // case Kleo::SMIMEOpaqueFormat:
332 // return GpgME::NormalSignatureMode;
333 // case Kleo::InlineOpenPGPFormat:
334 // return GpgME::Clearsigned;
335 // default:
336 // case Kleo::SMIMEFormat:
337 // case Kleo::OpenPGPMIMEFormat:
338 // return GpgME::Detached;
339 // }
340 // }
341 45
342// replace simple LFs by CRLFs for all MIME supporting CryptPlugs 46// replace simple LFs by CRLFs for all MIME supporting CryptPlugs
343// according to RfC 2633, 3.1.1 Canonicalization 47// according to RfC 2633, 3.1.1 Canonicalization
@@ -404,63 +108,319 @@ static QByteArray canonicalizeContent(KMime::Content *content)
404 108
405} 109}
406 110
407KMime::Content *MailCrypto::processCrypto(KMime::Content *content, const std::vector<GpgME::Key> &signingKeys, const std::vector<GpgME::Key> &encryptionKeys, MailCrypto::Protocol protocol) 111/**
112 * Get the given `key` in the armor format.
113 */
114Expected<GpgME::Error, QByteArray> exportPublicKey(const GpgME::Key &key)
408{ 115{
409 const QGpgME::Protocol *const proto = protocol == MailCrypto::SMIME ? QGpgME::smime() : QGpgME::openpgp(); 116 // Not using the Qt API because it apparently blocks (the `result` signal is never
410 Q_ASSERT(proto); 117 // triggered)
411 118 std::unique_ptr<GpgME::Context> ctx(GpgME::Context::createForProtocol(GpgME::OpenPGP));
412 auto signingMode = GpgME::Detached; 119 ctx->setArmor(true);
413 bool armor = true; 120
414 bool textMode = false; 121 QGpgME::QByteArrayDataProvider dp;
415 const bool sign = !signingKeys.empty(); 122 GpgME::Data data(&dp);
416 const bool encrypt = !encryptionKeys.empty(); 123
417 124 qDebug() << "Exporting public key:" << key.shortKeyID();
418 QByteArray resultContent; 125 auto error = ctx->exportPublicKeys(key.keyID(), data);
419 QByteArray hashAlgo; 126
420 //Trust provided keys and don't check them for validity 127 if (error.code()) {
421 bool alwaysTrust = true; 128 return makeUnexpected(error);
422 if (sign && encrypt) {
423 std::unique_ptr<QGpgME::SignEncryptJob> job(proto->signEncryptJob(armor, textMode));
424 const auto res = job->exec(signingKeys, encryptionKeys, canonicalizeContent(content), alwaysTrust, resultContent);
425 if (res.first.error().code()) {
426 qWarning() << "Signing failed:" << res.first.error().asString();
427 return nullptr;
428 } else {
429 hashAlgo = res.first.createdSignature(0).hashAlgorithmAsString();
430 }
431 if (res.second.error().code()) {
432 qWarning() << "Encryption failed:" << res.second.error().asString();
433 return nullptr;
434 }
435 } else if (sign) {
436 std::unique_ptr<QGpgME::SignJob> job(proto->signJob(armor, textMode));
437 auto result = job->exec(signingKeys, canonicalizeContent(content), signingMode, resultContent);
438 if (result.error().code()) {
439 qWarning() << "Signing failed:" << result.error().asString();
440 return nullptr;
441 }
442 hashAlgo = result.createdSignature(0).hashAlgorithmAsString();
443 } else if (encrypt) {
444 std::unique_ptr<QGpgME::EncryptJob> job(proto->encryptJob(armor, textMode));
445 const auto result = job->exec(encryptionKeys, canonicalizeContent(content), alwaysTrust, resultContent);
446 if (result.error().code()) {
447 qWarning() << "Encryption failed:" << result.error().asString();
448 return nullptr;
449 }
450 hashAlgo = "pgp-sha1";
451 } else {
452 qWarning() << "Not signing or encrypting";
453 return nullptr;
454 } 129 }
455 130
456 return composeHeadersAndBody(content, resultContent, sign, hashAlgo); 131 return dp.data();
457} 132}
458 133
459KMime::Content *MailCrypto::sign(KMime::Content *content, const std::vector<GpgME::Key> &signers) 134/**
135 * Create an Email with `msg` as a body and `key` as an attachment.
136 *
137 * Will create the given structure:
138 *
139 * + `multipart/mixed`
140 * - the given `msg`
141 * - `application/pgp-keys` (the given `key` as attachment)
142 *
143 * Used by the `createSignedEmail` and `createEncryptedEmail` functions.
144 */
145Expected<GpgME::Error, std::unique_ptr<KMime::Content>>
146appendPublicKey(std::unique_ptr<KMime::Content> msg, const GpgME::Key &key)
460{ 147{
461 return processCrypto(content, signers, {}, OPENPGP); 148 const auto publicKeyExportResult = exportPublicKey(key);
149
150 if (!publicKeyExportResult) {
151 // "Could not export public key"
152 return makeUnexpected(publicKeyExportResult.error());
153 }
154
155 const auto publicKeyData = publicKeyExportResult.value();
156
157 auto result = std::unique_ptr<KMime::Content>(new KMime::Content);
158 result->contentType()->setMimeType("multipart/mixed");
159 result->contentType()->setBoundary(KMime::multiPartBoundary());
160
161 KMime::Content *keyAttachment = new KMime::Content;
162 {
163 keyAttachment->contentType()->setMimeType("application/pgp-keys");
164 keyAttachment->contentDisposition()->setDisposition(KMime::Headers::CDattachment);
165 keyAttachment->contentDisposition()->setFilename(QString("0x") + key.shortKeyID() + ".asc");
166 keyAttachment->setBody(publicKeyData);
167 }
168
169 msg->assemble();
170
171 result->addContent(msg.release());
172 result->addContent(keyAttachment);
173
174 result->assemble();
175
176 return result;
462} 177}
463 178
179Expected<GpgME::Error, QByteArray> encrypt(const QByteArray &content, const std::vector<GpgME::Key> &encryptionKeys)
180{
181 QByteArray resultData;
182
183 const QGpgME::Protocol *const proto = QGpgME::openpgp();
184 std::unique_ptr<QGpgME::EncryptJob> job(proto->encryptJob(/* armor = */ true));
185 const auto result = job->exec(encryptionKeys, content, /* alwaysTrust = */ true, resultData);
186
187 if (result.error().code()) {
188 qWarning() << "Encryption failed:" << result.error().asString();
189 return makeUnexpected(result.error());
190 }
191
192 return resultData;
193}
194
195Expected<GpgME::Error, QByteArray> signAndEncrypt(const QByteArray &content,
196 const std::vector<GpgME::Key> &signingKeys, const std::vector<GpgME::Key> &encryptionKeys)
197{
198 QByteArray resultData;
199
200 const QGpgME::Protocol *const proto = QGpgME::openpgp();
201 std::unique_ptr<QGpgME::SignEncryptJob> job(proto->signEncryptJob(/* armor = */ true));
202 const auto result = job->exec(signingKeys, encryptionKeys, content, /* alwaysTrust = */ true, resultData);
203
204 if (result.first.error().code()) {
205 qWarning() << "Signing failed:" << result.first.error().asString();
206 return makeUnexpected(result.first.error());
207 }
208
209 if (result.second.error().code()) {
210 qWarning() << "Encryption failed:" << result.second.error().asString();
211 return makeUnexpected(result.second.error());
212 }
213
214 return resultData;
215}
216
217/**
218 * Create a message part like this (according to RFC 3156 Section 4):
219 *
220 * - multipart/encrypted
221 * - application/pgp-encrypted (version information)
222 * - application/octet-stream (given encrypted data)
223 *
224 * Should not be used directly since the public key should be attached, hence
225 * the `createEncryptedEmail` function.
226 *
227 * The encrypted data can be generated by the `encrypt` or `signAndEncrypt` functions.
228 */
229std::unique_ptr<KMime::Content> createEncryptedPart(QByteArray encryptedData)
230{
231 auto result = std::unique_ptr<KMime::Content>(new KMime::Content);
232
233 result->contentType()->setMimeType("multipart/encrypted");
234 result->contentType()->setBoundary(KMime::multiPartBoundary());
235 result->contentType()->setParameter("protocol", "application/pgp-encrypted");
236
237 KMime::Content *controlInformation = new KMime::Content;
238 {
239 controlInformation->contentType()->setMimeType("application/pgp-encrypted");
240 controlInformation->contentDescription()->from7BitString("PGP/MIME version identification");
241 controlInformation->setBody("Version: 1");
242
243 result->addContent(controlInformation);
244 }
245
246 KMime::Content *encryptedPartPart = new KMime::Content;
247 {
248 const QString filename = "msg.asc";
249
250 encryptedPartPart->contentType()->setMimeType("application/octet-stream");
251 encryptedPartPart->contentType()->setName(filename, "utf-8");
252
253 encryptedPartPart->contentDescription()->from7BitString("OpenPGP encrypted message");
254
255 encryptedPartPart->contentDisposition()->setDisposition(KMime::Headers::CDinline);
256 encryptedPartPart->contentDisposition()->setFilename(filename);
257
258 encryptedPartPart->setBody(encryptedData);
259
260 result->addContent(encryptedPartPart);
261 }
262
263 return result;
264}
265
266/**
267 * Create an encrypted (optionally signed) email with a public key attached to it.
268 *
269 * Will create a message like this:
270 *
271 * + `multipart/mixed`
272 * - `multipart/encrypted`
273 * + `application/pgp-encrypted
274 * + `application/octet-stream` (a generated encrypted version of the original message)
275 * - `application/pgp-keys` (the public key as attachment, which is the first of the
276 * `signingKeys`)
277 */
278Expected<GpgME::Error, std::unique_ptr<KMime::Content>>
279createEncryptedEmail(KMime::Content *content, const std::vector<GpgME::Key> &encryptionKeys,
280 const GpgME::Key &attachedKey, const std::vector<GpgME::Key> &signingKeys = {})
281{
282 auto contentToEncrypt = canonicalizeContent(content);
283
284 auto encryptionResult = signingKeys.empty() ?
285 encrypt(contentToEncrypt, encryptionKeys) :
286 signAndEncrypt(contentToEncrypt, signingKeys, encryptionKeys);
287
288 if (!encryptionResult) {
289 return makeUnexpected(encryptionResult.error());
290 }
291
292 auto encryptedPart = createEncryptedPart(encryptionResult.value());
293
294 auto publicKeyAppendResult = appendPublicKey(std::move(encryptedPart), attachedKey);
295
296 if(publicKeyAppendResult) {
297 publicKeyAppendResult.value()->assemble();
298 }
299
300 return publicKeyAppendResult;
301}
302
303/**
304 * Sign the given content and returns the signing data and the algorithm used
305 * for integrity check in the "pgp-<algorithm>" format.
306 */
307Expected<GpgME::Error, std::pair<QByteArray, QString>>
308sign(const QByteArray &content, const std::vector<GpgME::Key> &signingKeys)
309{
310 QByteArray resultData;
311
312 const QGpgME::Protocol *const proto = QGpgME::openpgp();
313 std::unique_ptr<QGpgME::SignJob> job(proto->signJob(/* armor = */ true));
314 const auto result = job->exec(signingKeys, content, GpgME::Detached, resultData);
315
316 if (result.error().code()) {
317 qWarning() << "Signing failed:" << result.error().asString();
318 return makeUnexpected(result.error());
319 }
320
321 auto algo = result.createdSignature(0).hashAlgorithmAsString();
322 // RFC 3156 Section 5:
323 // Hash-symbols are constructed [...] by converting the text name to lower
324 // case and prefixing it with the four characters "pgp-".
325 auto micAlg = (QString("pgp-") + algo).toLower();
326
327 return std::pair<QByteArray, QString>{resultData, micAlg};
328}
329
330/**
331 * Create a message part like this (according to RFC 3156 Section 5):
332 *
333 * + `multipart/signed`
334 * - whatever the given original `message` is (should be canonicalized)
335 * - `application/octet-stream` (the given `signature`)
336 *
337 * Should not be used directly since the public key should be attached, hence
338 * the `createSignedEmail` function.
339 *
340 * The signature can be generated by the `sign` function.
341 */
342std::unique_ptr<KMime::Content> createSignedPart(
343 std::unique_ptr<KMime::Content> message, const QByteArray &signature, const QString &micAlg)
344{
345 auto result = std::unique_ptr<KMime::Content>(new KMime::Content);
346
347 result->contentType()->setMimeType("multipart/signed");
348 result->contentType()->setBoundary(KMime::multiPartBoundary());
349 result->contentType()->setParameter("micalg", micAlg);
350 result->contentType()->setParameter("protocol", "application/pgp-signature");
351
352 result->addContent(message.release());
353
354 KMime::Content *signedPartPart = new KMime::Content;
355 {
356 signedPartPart->contentType()->setMimeType("application/pgp-signature");
357 signedPartPart->contentType()->setName("signature.asc", "utf-8");
358
359 signedPartPart->contentDescription()->from7BitString(
360 "This is a digitally signed message part");
361
362 signedPartPart->setBody(signature);
363
364 result->addContent(signedPartPart);
365 }
366
367 return result;
368}
369
370/**
371 * Create a signed email with a public key attached to it.
372 *
373 * Will create a message like this:
374 *
375 * + `multipart/mixed`
376 * - `multipart/signed`
377 * + whatever the given original `content` is (should not be canonalized)
378 * + `application/octet-stream` (a generated signature of the original message)
379 * - `application/pgp-keys` (the public key as attachment, which is the first of the
380 * `signingKeys`)
381 */
382Expected<GpgME::Error, std::unique_ptr<KMime::Content>>
383createSignedEmail(std::unique_ptr<KMime::Content> content,
384 const std::vector<GpgME::Key> &signingKeys, const GpgME::Key &attachedKey)
385{
386 Q_ASSERT(!signingKeys.empty());
387
388 auto contentToSign = canonicalizeContent(content.get());
389
390 auto signingResult = sign(contentToSign, signingKeys);
391
392 if (!signingResult) {
393 return makeUnexpected(signingResult.error());
394 }
395
396 QByteArray signingData;
397 QString micAlg;
398 std::tie(signingData, micAlg) = signingResult.value();
399
400 auto signedPart = createSignedPart(std::move(content), signingData, micAlg);
401
402 auto publicKeyAppendResult = appendPublicKey(std::move(signedPart), attachedKey);
403
404 if (publicKeyAppendResult) {
405 publicKeyAppendResult.value()->assemble();
406 }
407
408 return publicKeyAppendResult;
409}
410
411Expected<GpgME::Error, std::unique_ptr<KMime::Content>>
412MailCrypto::processCrypto(std::unique_ptr<KMime::Content> content, const std::vector<GpgME::Key> &signingKeys,
413 const std::vector<GpgME::Key> &encryptionKeys, const GpgME::Key &attachedKey)
414{
415 if (!encryptionKeys.empty()) {
416 return createEncryptedEmail(content.release(), encryptionKeys, attachedKey, signingKeys);
417 } else if (!signingKeys.empty()) {
418 return createSignedEmail(std::move(content), signingKeys, signingKeys[0]);
419 } else {
420 qWarning() << "Processing cryptography, but neither signing nor encrypting";
421 return content;
422 }
423}
464 424
465void MailCrypto::importKeys(const std::vector<GpgME::Key> &keys) 425void MailCrypto::importKeys(const std::vector<GpgME::Key> &keys)
466{ 426{
@@ -470,7 +430,7 @@ void MailCrypto::importKeys(const std::vector<GpgME::Key> &keys)
470 job->exec(keys); 430 job->exec(keys);
471} 431}
472 432
473static GpgME::KeyListResult listKeys(GpgME::Protocol protocol, const QStringList &patterns, bool secretOnly, int keyListMode, std::vector<GpgME::Key> &keys) 433static GpgME::KeyListResult listKeys(const QStringList &patterns, bool secretOnly, int keyListMode, std::vector<GpgME::Key> &keys)
474{ 434{
475 QByteArrayList list; 435 QByteArrayList list;
476 std::transform(patterns.constBegin(), patterns.constEnd(), std::back_inserter(list), [] (const QString &s) { return s.toUtf8(); }); 436 std::transform(patterns.constBegin(), patterns.constEnd(), std::back_inserter(list), [] (const QString &s) { return s.toUtf8(); });
@@ -479,7 +439,7 @@ static GpgME::KeyListResult listKeys(GpgME::Protocol protocol, const QStringList
479 pattern.push_back(0); 439 pattern.push_back(0);
480 440
481 GpgME::initializeLibrary(); 441 GpgME::initializeLibrary();
482 auto ctx = QSharedPointer<GpgME::Context>{GpgME::Context::createForProtocol(protocol)}; 442 auto ctx = QSharedPointer<GpgME::Context>{GpgME::Context::createForProtocol(GpgME::OpenPGP)};
483 ctx->setKeyListMode(keyListMode); 443 ctx->setKeyListMode(keyListMode);
484 if (const GpgME::Error err = ctx->startKeyListing(pattern.data(), secretOnly)) { 444 if (const GpgME::Error err = ctx->startKeyListing(pattern.data(), secretOnly)) {
485 return GpgME::KeyListResult(0, err); 445 return GpgME::KeyListResult(0, err);
@@ -497,10 +457,10 @@ static GpgME::KeyListResult listKeys(GpgME::Protocol protocol, const QStringList
497 return result; 457 return result;
498} 458}
499 459
500std::vector<GpgME::Key> MailCrypto::findKeys(const QStringList &filter, bool findPrivate, bool remote, Protocol protocol) 460std::vector<GpgME::Key> MailCrypto::findKeys(const QStringList &filter, bool findPrivate, bool remote)
501{ 461{
502 std::vector<GpgME::Key> keys; 462 std::vector<GpgME::Key> keys;
503 GpgME::KeyListResult res = listKeys(protocol == SMIME ? GpgME::CMS : GpgME::OpenPGP, filter, findPrivate, remote ? GpgME::Extern : GpgME::Local, keys); 463 GpgME::KeyListResult res = listKeys(filter, findPrivate, remote ? GpgME::Extern : GpgME::Local, keys);
504 if (res.error()) { 464 if (res.error()) {
505 qWarning() << "Failed to lookup keys: " << res.error().asString(); 465 qWarning() << "Failed to lookup keys: " << res.error().asString();
506 return keys; 466 return keys;
@@ -517,4 +477,3 @@ std::vector<GpgME::Key> MailCrypto::findKeys(const QStringList &filter, bool fin
517 477
518 return keys; 478 return keys;
519} 479}
520
diff --git a/framework/src/domain/mime/mailcrypto.h b/framework/src/domain/mime/mailcrypto.h
index 0a6c2f4c..832f68ec 100644
--- a/framework/src/domain/mime/mailcrypto.h
+++ b/framework/src/domain/mime/mailcrypto.h
@@ -19,19 +19,24 @@
19 19
20#pragma once 20#pragma once
21 21
22#include <QByteArray> 22#include "framework/src/errors.h"
23
23#include <KMime/Message> 24#include <KMime/Message>
24#include <gpgme++/key.h> 25#include <gpgme++/key.h>
26
27#include <QByteArray>
28
25#include <functional> 29#include <functional>
30#include <memory>
31
32namespace MailCrypto {
33
34Expected<GpgME::Error, std::unique_ptr<KMime::Content>>
35processCrypto(std::unique_ptr<KMime::Content> content, const std::vector<GpgME::Key> &signingKeys,
36 const std::vector<GpgME::Key> &encryptionKeys, const GpgME::Key &attachedKey);
37
38std::vector<GpgME::Key> findKeys(const QStringList &filter, bool findPrivate = false, bool remote = false);
39
40void importKeys(const std::vector<GpgME::Key> &keys);
26 41
27namespace MailCrypto 42}; // namespace MailCrypto
28{
29 enum Protocol {
30 OPENPGP,
31 SMIME
32 };
33 KMime::Content *processCrypto(KMime::Content *content, const std::vector<GpgME::Key> &signingKeys, const std::vector<GpgME::Key> &encryptionKeys, MailCrypto::Protocol protocol);
34 KMime::Content *sign(KMime::Content *content, const std::vector<GpgME::Key> &signers);
35 std::vector<GpgME::Key> findKeys(const QStringList &filter, bool findPrivate = false, bool remote = false, Protocol protocol = OPENPGP);
36 void importKeys(const std::vector<GpgME::Key> &keys);
37};
diff --git a/framework/src/domain/mime/mailtemplates.cpp b/framework/src/domain/mime/mailtemplates.cpp
index 30f9a48d..997eb3ae 100644
--- a/framework/src/domain/mime/mailtemplates.cpp
+++ b/framework/src/domain/mime/mailtemplates.cpp
@@ -1025,7 +1025,11 @@ static KMime::Types::Mailbox::List stringListToMailboxes(const QStringList &list
1025 return mailboxes; 1025 return mailboxes;
1026} 1026}
1027 1027
1028KMime::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<Attachment> &attachments, const std::vector<GpgME::Key> &signingKeys, const std::vector<GpgME::Key> &encryptionKeys) 1028KMime::Message::Ptr MailTemplates::createMessage(KMime::Message::Ptr existingMessage,
1029 const QStringList &to, const QStringList &cc, const QStringList &bcc,
1030 const KMime::Types::Mailbox &from, const QString &subject, const QString &body, bool htmlBody,
1031 const QList<Attachment> &attachments, const std::vector<GpgME::Key> &signingKeys,
1032 const std::vector<GpgME::Key> &encryptionKeys, const GpgME::Key &attachedKey)
1029{ 1033{
1030 auto mail = existingMessage; 1034 auto mail = existingMessage;
1031 if (!mail) { 1035 if (!mail) {
@@ -1089,12 +1093,12 @@ KMime::Message::Ptr MailTemplates::createMessage(KMime::Message::Ptr existingMes
1089 1093
1090 QByteArray bodyData; 1094 QByteArray bodyData;
1091 if (!signingKeys.empty() || !encryptionKeys.empty()) { 1095 if (!signingKeys.empty() || !encryptionKeys.empty()) {
1092 auto result = MailCrypto::processCrypto(bodyPart.get(), signingKeys, encryptionKeys, MailCrypto::OPENPGP); 1096 auto result = MailCrypto::processCrypto(std::move(bodyPart), signingKeys, encryptionKeys, attachedKey);
1093 if (!result) { 1097 if (!result) {
1094 qWarning() << "Signing failed"; 1098 qWarning() << "Crypto failed";
1095 return {}; 1099 return {};
1096 } 1100 }
1097 bodyData = result->encodedContent(); 1101 bodyData = result.value()->encodedContent();
1098 } else { 1102 } else {
1099 if (!bodyPart->contentType(false)) { 1103 if (!bodyPart->contentType(false)) {
1100 bodyPart->contentType(true)->setMimeType("text/plain"); 1104 bodyPart->contentType(true)->setMimeType("text/plain");
diff --git a/framework/src/domain/mime/mailtemplates.h b/framework/src/domain/mime/mailtemplates.h
index 9447e169..154b76a2 100644
--- a/framework/src/domain/mime/mailtemplates.h
+++ b/framework/src/domain/mime/mailtemplates.h
@@ -38,5 +38,5 @@ namespace MailTemplates
38 void forward(const KMime::Message::Ptr &origMsg, const std::function<void(const KMime::Message::Ptr &result)> &callback); 38 void forward(const KMime::Message::Ptr &origMsg, const std::function<void(const KMime::Message::Ptr &result)> &callback);
39 QString plaintextContent(const KMime::Message::Ptr &origMsg); 39 QString plaintextContent(const KMime::Message::Ptr &origMsg);
40 QString body(const KMime::Message::Ptr &msg, bool &isHtml); 40 QString body(const KMime::Message::Ptr &msg, bool &isHtml);
41 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<Attachment> &attachments, const std::vector<GpgME::Key> &signingKeys = {}, const std::vector<GpgME::Key> &encryptionKeys = {}); 41 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<Attachment> &attachments, const std::vector<GpgME::Key> &signingKeys = {}, const std::vector<GpgME::Key> &encryptionKeys = {}, const GpgME::Key &attachedKey = {});
42}; 42};
diff --git a/framework/src/domain/mime/tests/mailtemplatetest.cpp b/framework/src/domain/mime/tests/mailtemplatetest.cpp
index 6338cd58..d5642bb6 100644
--- a/framework/src/domain/mime/tests/mailtemplatetest.cpp
+++ b/framework/src/domain/mime/tests/mailtemplatetest.cpp
@@ -31,16 +31,16 @@ static std::vector< GpgME::Key, std::allocator< GpgME::Key > > getKeys(bool smim
31 if (smime) { 31 if (smime) {
32 const QGpgME::Protocol *const backend = QGpgME::smime(); 32 const QGpgME::Protocol *const backend = QGpgME::smime();
33 Q_ASSERT(backend); 33 Q_ASSERT(backend);
34 job = backend->keyListJob(false); 34 job = backend->keyListJob(/* remote = */ false);
35 } else { 35 } else {
36 const QGpgME::Protocol *const backend = QGpgME::openpgp(); 36 const QGpgME::Protocol *const backend = QGpgME::openpgp();
37 Q_ASSERT(backend); 37 Q_ASSERT(backend);
38 job = backend->keyListJob(false); 38 job = backend->keyListJob(/* remote = */ false);
39 } 39 }
40 Q_ASSERT(job); 40 Q_ASSERT(job);
41 41
42 std::vector< GpgME::Key > keys; 42 std::vector< GpgME::Key > keys;
43 GpgME::KeyListResult res = job->exec(QStringList(), true, keys); 43 GpgME::KeyListResult res = job->exec(QStringList(), /* secretOnly = */ true, keys);
44 44
45 if (!smime) { 45 if (!smime) {
46 Q_ASSERT(keys.size() == 3); 46 Q_ASSERT(keys.size() == 3);
@@ -401,7 +401,7 @@ private slots:
401 401
402 std::vector<GpgME::Key> keys = getKeys(); 402 std::vector<GpgME::Key> keys = getKeys();
403 403
404 auto result = MailTemplates::createMessage({}, to, cc, bcc, from, subject, body, false, attachments, keys); 404 auto result = MailTemplates::createMessage({}, to, cc, bcc, from, subject, body, false, attachments, keys, {}, keys[0]);
405 405
406 QVERIFY(result); 406 QVERIFY(result);
407 // qWarning() << "---------------------------------"; 407 // qWarning() << "---------------------------------";
@@ -409,9 +409,17 @@ private slots:
409 // qWarning() << "---------------------------------"; 409 // qWarning() << "---------------------------------";
410 QCOMPARE(result->subject()->asUnicodeString(), subject); 410 QCOMPARE(result->subject()->asUnicodeString(), subject);
411 QVERIFY(result->date(false)->dateTime().isValid()); 411 QVERIFY(result->date(false)->dateTime().isValid());
412 QVERIFY(result->contentType()->isMimeType("multipart/signed"));
413 412
414 const auto contents = result->contents(); 413 QCOMPARE(result->contentType()->mimeType(), "multipart/mixed");
414 auto resultAttachments = result->attachments();
415 QCOMPARE(resultAttachments.size(), 1);
416 QCOMPARE(resultAttachments[0]->contentDisposition()->filename(), "0x8F246DE6.asc");
417
418 auto signedMessage = result->contents()[0];
419
420 QVERIFY(signedMessage->contentType()->isMimeType("multipart/signed"));
421
422 const auto contents = signedMessage->contents();
415 QCOMPARE(contents.size(), 2); 423 QCOMPARE(contents.size(), 2);
416 { 424 {
417 auto c = contents.at(0); 425 auto c = contents.at(0);
@@ -441,9 +449,19 @@ private slots:
441 QVERIFY(result); 449 QVERIFY(result);
442 QCOMPARE(result->subject()->asUnicodeString(), subject); 450 QCOMPARE(result->subject()->asUnicodeString(), subject);
443 QVERIFY(result->date(false)->dateTime().isValid()); 451 QVERIFY(result->date(false)->dateTime().isValid());
444 QVERIFY(result->contentType()->isMimeType("multipart/signed"));
445 452
446 const auto contents = result->contents(); 453 QCOMPARE(result->contentType()->mimeType(), "multipart/mixed");
454 auto resultAttachments = result->attachments();
455 QCOMPARE(resultAttachments.size(), 3);
456 // It seems KMime searches for the attachments using depth-first
457 // search, so the public key is last
458 QCOMPARE(resultAttachments[2]->contentDisposition()->filename(), "0x8F246DE6.asc");
459
460 auto signedMessage = result->contents()[0];
461
462 QVERIFY(signedMessage->contentType()->isMimeType("multipart/signed"));
463
464 const auto contents = signedMessage->contents();
447 QCOMPARE(contents.size(), 2); 465 QCOMPARE(contents.size(), 2);
448 { 466 {
449 auto c = contents.at(0); 467 auto c = contents.at(0);
diff --git a/framework/src/errors.h b/framework/src/errors.h
new file mode 100644
index 00000000..4249cf8d
--- /dev/null
+++ b/framework/src/errors.h
@@ -0,0 +1,308 @@
1/*
2 Copyright (c) 2018 Christian Mollekopf <mollekopf@kolabsys.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19#pragma once
20
21#include <memory>
22#include <type_traits>
23#include <utility>
24
25#include <QtGlobal>
26
27// A somewhat implementation of the expected monad, proposed here:
28// https://isocpp.org/files/papers/n4015.pdf
29
30// A class used to differentiate errors and values when they are of the same type.
31template <typename Error>
32class Unexpected
33{
34
35 static_assert(!std::is_same<Error, void>::value, "Cannot have an Unexpected void");
36
37public:
38 Unexpected() = delete;
39
40 constexpr explicit Unexpected(const Error &error) : mValue(error) {}
41 constexpr explicit Unexpected(Error &&error) : mValue(std::move(error)) {}
42
43 // For implicit conversions when doing makeUnexpected(other)
44 template <typename Other>
45 constexpr explicit Unexpected(const Unexpected<Other> &error) : mValue(error.value())
46 {
47 }
48 template <typename Other>
49 constexpr explicit Unexpected(Unexpected<Other> &&error) : mValue(std::move(error.value()))
50 {
51 }
52
53 constexpr const Error &value() const &
54 {
55 return mValue;
56 }
57 Error &value() &
58 {
59 return mValue;
60 }
61
62 constexpr const Error &&value() const &&
63 {
64 return std::move(mValue);
65 }
66 Error &&value() &&
67 {
68 return std::move(mValue);
69 }
70
71private:
72 Error mValue;
73};
74
75template <class Error>
76Unexpected<typename std::decay<Error>::type> makeUnexpected(Error &&e)
77{
78 return Unexpected<typename std::decay<Error>::type>(std::forward<Error>(e));
79}
80
81template <typename Error>
82bool operator==(const Unexpected<Error> &lhs, const Unexpected<Error> &rhs)
83{
84 return lhs.value() == rhs.value();
85}
86
87template <typename Error>
88bool operator!=(const Unexpected<Error> &lhs, const Unexpected<Error> &rhs)
89{
90 return lhs.value() != rhs.value();
91}
92
93namespace detail {
94
95namespace tags {
96struct Expected
97{};
98struct Unexpected
99{};
100} // namespace tags
101
102// Write functions here when storage related and when Type != void
103template <typename Error, typename Type>
104struct StorageBase
105{
106protected:
107 // Rule of 5 {{{
108
109 StorageBase(const StorageBase &other) : mIsValue(other.mIsValue)
110 {
111 // This is a constructor, you have to construct object, not assign them
112 // (hence the placement new)
113 //
114 // Here's the problem:
115 //
116 // Object that are part of a union are not initialized (which is
117 // normal). If we replaced the placement new by a line like this:
118 //
119 // ```
120 // mValue = other.mValue;
121 // ```
122 //
123 // If overloaded, this will call `mValue.operator=(other.mValue);`, but
124 // since we're in the constructor, mValue is not initialized. This can
125 // cause big issues if `Type` / `Error` is not trivially (move)
126 // assignable.
127 //
128 // And so, the placement new allows us to call the constructor of
129 // `Type` or `Error` instead of its assignment operator.
130 if (mIsValue) {
131 new (std::addressof(mValue)) Type(other.mValue);
132 } else {
133 new (std::addressof(mError)) Unexpected<Error>(other.mError);
134 }
135 }
136
137 StorageBase(StorageBase &&other) : mIsValue(other.mIsValue)
138 {
139 // If you're thinking WTF, see the comment in the copy constructor above.
140 if (mIsValue) {
141 new (std::addressof(mValue)) Type(std::move(other.mValue));
142 } else {
143 new (std::addressof(mError)) Unexpected<Error>(std::move(other.mError));
144 }
145 }
146
147 constexpr StorageBase &operator=(const StorageBase &other)
148 {
149 mIsValue = other.mIsValue;
150 if (mIsValue) {
151 mValue = other.mValue;
152 } else {
153 mError = other.mError;
154 }
155 return *this;
156 }
157
158 constexpr StorageBase &operator=(StorageBase &&other)
159 {
160 this->~StorageBase();
161 mIsValue = other.mIsValue;
162 if (mIsValue) {
163 mValue = std::move(other.mValue);
164 } else {
165 mError = std::move(other.mError);
166 }
167 return *this;
168 }
169
170 ~StorageBase()
171 {
172 if (mIsValue) {
173 mValue.~Type();
174 } else {
175 mError.~Unexpected<Error>();
176 }
177 }
178
179 // }}}
180
181 template <typename... Args>
182 constexpr StorageBase(tags::Expected, Args &&... args)
183 : mValue(std::forward<Args>(args)...), mIsValue(true)
184 {
185 }
186
187 template <typename... Args>
188 constexpr StorageBase(tags::Unexpected, Args &&... args)
189 : mError(std::forward<Args>(args)...), mIsValue(false)
190 {
191 }
192
193 union
194 {
195 Unexpected<Error> mError;
196 Type mValue;
197 };
198 bool mIsValue;
199};
200
201// Write functions here when storage related and when Type == void
202template <typename Error>
203struct StorageBase<Error, void>
204{
205protected:
206 constexpr StorageBase(tags::Expected) : mIsValue(true) {}
207
208 template <typename... Args>
209 constexpr StorageBase(tags::Unexpected, Args &&... args)
210 : mError(std::forward<Args>(args)...), mIsValue(false)
211 {
212 }
213
214 Unexpected<Error> mError;
215 bool mIsValue;
216};
217
218// Write functions here when storage related, whether Type is void or not
219template <typename Error, typename Type>
220struct Storage : StorageBase<Error, Type>
221{
222protected:
223 // Forward the construction to StorageBase
224 using StorageBase<Error, Type>::StorageBase;
225};
226
227// Write functions here when dev API related and when Type != void
228template <typename Error, typename Type>
229struct ExpectedBase : detail::Storage<Error, Type>
230{
231 constexpr ExpectedBase() : detail::Storage<Error, Type>(detail::tags::Expected{}) {}
232
233 template <typename OtherError>
234 constexpr ExpectedBase(const Unexpected<OtherError> &error)
235 : detail::Storage<Error, Type>(detail::tags::Unexpected{}, error)
236 {
237 }
238 template <typename OtherError>
239 constexpr ExpectedBase(Unexpected<OtherError> &&error)
240 : detail::Storage<Error, Type>(detail::tags::Unexpected{}, std::move(error))
241 {
242 }
243
244 constexpr ExpectedBase(const Type &value)
245 : detail::Storage<Error, Type>(detail::tags::Expected{}, value)
246 {
247 }
248 constexpr ExpectedBase(Type &&value)
249 : detail::Storage<Error, Type>(detail::tags::Expected{}, std::move(value))
250 {
251 }
252
253 // Warning: will crash if this is an error. You should always check this is
254 // an expected value before calling `.value()`
255 constexpr const Type &value() const &
256 {
257 Q_ASSERT(this->mIsValue);
258 return this->mValue;
259 }
260 Type &&value() &&
261 {
262 Q_ASSERT(this->mIsValue);
263 return std::move(this->mValue);
264 }
265};
266
267// Write functions here when dev API related and when Type == void
268template <typename Error>
269struct ExpectedBase<Error, void> : detail::Storage<Error, void>
270{
271 // Rewrite constructors for unexpected because Expected doesn't have direct access to it.
272 template <typename OtherError>
273 constexpr ExpectedBase(const Unexpected<OtherError> &error)
274 : detail::Storage<Error, void>(detail::tags::Unexpected{}, error)
275 {
276 }
277 template <typename OtherError>
278 constexpr ExpectedBase(Unexpected<OtherError> &&error)
279 : detail::Storage<Error, void>(detail::tags::Unexpected{}, std::move(error))
280 {
281 }
282};
283
284} // namespace detail
285
286// Write functions here when dev API related, whether Type is void or not
287template <typename Error, typename Type = void>
288class Expected : public detail::ExpectedBase<Error, Type>
289{
290 static_assert(!std::is_same<Error, void>::value, "Expected with void Error is not implemented");
291
292public:
293 using detail::ExpectedBase<Error, Type>::ExpectedBase;
294
295 constexpr const Error &error() const &
296 {
297 return this->mError.value();
298 }
299
300 constexpr bool isValue() const
301 {
302 return this->mIsValue;
303 }
304 constexpr explicit operator bool() const
305 {
306 return this->mIsValue;
307 }
308};
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
index 00000000..ff539ffc
--- /dev/null
+++ b/icons/breeze/icons/actions/22/view-certificate-import-inverted.svg
@@ -0,0 +1,14 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
2 <defs id="defs3051">
3 <style type="text/css" id="current-color-scheme">
4 .ColorScheme-Text {
5 color:#f2f2f2;
6 }
7 </style>
8 </defs>
9 <path
10 style="fill:currentColor;fill-opacity:1;stroke:none"
11 d="M 3 5 L 3 17 L 13 17 L 13 16 L 4 16 L 4 6 L 18 6 L 18 11 L 19 11 L 19 5 L 3 5 z M 7.5 7 C 6.669 7 6 7.669 6 8.5 L 6 10 L 5 10 L 5 13 L 10 13 L 10 10 L 9 10 L 9 8.5 C 9 7.669 8.331 7 7.5 7 z M 7.5 8 C 7.777 8 8 8.223 8 8.5 L 8 10 L 7 10 L 7 8.5 C 7 8.223 7.223 8 7.5 8 z M 11 8 L 11 9 L 17 9 L 17 8 L 11 8 z M 11 10 L 11 11 L 16 11 L 16 10 L 11 10 z M 11 12 L 11 13 L 15 13 L 15 12 L 11 12 z M 17.5 12 L 14.707031 14.792969 L 14 15.5 L 14.707031 16.207031 L 17.5 19 L 18.207031 18.292969 L 15.914062 16 L 21 16 L 21 15 L 20.207031 15 L 15.914062 15 L 18.207031 12.707031 L 17.5 12 z "
12 class="ColorScheme-Text"
13 />
14</svg>
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
index 00000000..ec5250cc
--- /dev/null
+++ b/icons/breeze/icons/actions/22/view-certificate-import.svg
@@ -0,0 +1,14 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
2 <defs id="defs3051">
3 <style type="text/css" id="current-color-scheme">
4 .ColorScheme-Text {
5 color:#4d4d4d;
6 }
7 </style>
8 </defs>
9 <path
10 style="fill:currentColor;fill-opacity:1;stroke:none"
11 d="M 3 5 L 3 17 L 13 17 L 13 16 L 4 16 L 4 6 L 18 6 L 18 11 L 19 11 L 19 5 L 3 5 z M 7.5 7 C 6.669 7 6 7.669 6 8.5 L 6 10 L 5 10 L 5 13 L 10 13 L 10 10 L 9 10 L 9 8.5 C 9 7.669 8.331 7 7.5 7 z M 7.5 8 C 7.777 8 8 8.223 8 8.5 L 8 10 L 7 10 L 7 8.5 C 7 8.223 7.223 8 7.5 8 z M 11 8 L 11 9 L 17 9 L 17 8 L 11 8 z M 11 10 L 11 11 L 16 11 L 16 10 L 11 10 z M 11 12 L 11 13 L 15 13 L 15 12 L 11 12 z M 17.5 12 L 14.707031 14.792969 L 14 15.5 L 14.707031 16.207031 L 17.5 19 L 18.207031 18.292969 L 15.914062 16 L 21 16 L 21 15 L 20.207031 15 L 15.914062 15 L 18.207031 12.707031 L 17.5 12 z "
12 class="ColorScheme-Text"
13 />
14</svg>
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
index 00000000..e0691d6d
--- /dev/null
+++ b/icons/breeze/icons/actions/24/view-certificate-import-inverted.svg
@@ -0,0 +1,13 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2 <defs id="defs3051">
3 <style type="text/css" id="current-color-scheme">
4 .ColorScheme-Text {
5 color:#f2f2f2;
6 }
7 </style>
8 </defs>
9 <path style="fill:currentColor;fill-opacity:1;stroke:none"
10 d="M 4 6 L 4 18 L 14 18 L 14 17 L 5 17 L 5 7 L 19 7 L 19 12 L 20 12 L 20 6 L 4 6 z M 8.5 8 C 7.669 8 7 8.669 7 9.5 L 7 11 L 6 11 L 6 14 L 11 14 L 11 11 L 10 11 L 10 9.5 C 10 8.669 9.331 8 8.5 8 z M 8.5 9 C 8.777 9 9 9.223 9 9.5 L 9 11 L 8 11 L 8 9.5 C 8 9.223 8.223 9 8.5 9 z M 12 9 L 12 10 L 18 10 L 18 9 L 12 9 z M 12 11 L 12 12 L 17 12 L 17 11 L 12 11 z M 12 13 L 12 14 L 16 14 L 16 13 L 12 13 z M 18.5 13 L 15.707031 15.792969 L 15 16.5 L 15.707031 17.207031 L 18.5 20 L 19.207031 19.292969 L 16.914062 17 L 22 17 L 22 16 L 21.207031 16 L 16.914062 16 L 19.207031 13.707031 L 18.5 13 z "
11 class="ColorScheme-Text"
12 />
13</svg>
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
index 00000000..f82c65b3
--- /dev/null
+++ b/icons/breeze/icons/actions/24/view-certificate-import.svg
@@ -0,0 +1,13 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2 <defs id="defs3051">
3 <style type="text/css" id="current-color-scheme">
4 .ColorScheme-Text {
5 color:#4d4d4d;
6 }
7 </style>
8 </defs>
9 <path style="fill:currentColor;fill-opacity:1;stroke:none"
10 d="M 4 6 L 4 18 L 14 18 L 14 17 L 5 17 L 5 7 L 19 7 L 19 12 L 20 12 L 20 6 L 4 6 z M 8.5 8 C 7.669 8 7 8.669 7 9.5 L 7 11 L 6 11 L 6 14 L 11 14 L 11 11 L 10 11 L 10 9.5 C 10 8.669 9.331 8 8.5 8 z M 8.5 9 C 8.777 9 9 9.223 9 9.5 L 9 11 L 8 11 L 8 9.5 C 8 9.223 8.223 9 8.5 9 z M 12 9 L 12 10 L 18 10 L 18 9 L 12 9 z M 12 11 L 12 12 L 17 12 L 17 11 L 12 11 z M 12 13 L 12 14 L 16 14 L 16 13 L 12 13 z M 18.5 13 L 15.707031 15.792969 L 15 16.5 L 15.707031 17.207031 L 18.5 20 L 19.207031 19.292969 L 16.914062 17 L 22 17 L 22 16 L 21.207031 16 L 16.914062 16 L 19.207031 13.707031 L 18.5 13 z "
11 class="ColorScheme-Text"
12 />
13</svg>
diff --git a/icons/copybreeze.sh b/icons/copybreeze.sh
index b7c85229..5697b4b6 100755
--- a/icons/copybreeze.sh
+++ b/icons/copybreeze.sh
@@ -13,6 +13,7 @@ wantedIcons = [
13 "document-decrypt.svg", 13 "document-decrypt.svg",
14 "document-edit.svg", 14 "document-edit.svg",
15 "document-encrypt.svg", 15 "document-encrypt.svg",
16 "view-certificate-import.svg",
16 "document-save.svg", 17 "document-save.svg",
17 "document-sign.svg", 18 "document-sign.svg",
18 "edit-delete.svg", 19 "edit-delete.svg",
diff --git a/tests/teststore.cpp b/tests/teststore.cpp
index 9d56dd33..84e2da20 100644
--- a/tests/teststore.cpp
+++ b/tests/teststore.cpp
@@ -64,6 +64,19 @@ static void createMail(const QVariantMap &object, const QByteArray &folder = {})
64 auto ccAddresses = toStringList(object["cc"].toList()); 64 auto ccAddresses = toStringList(object["cc"].toList());
65 auto bccAddresses = toStringList(object["bcc"].toList()); 65 auto bccAddresses = toStringList(object["bcc"].toList());
66 66
67 QList<Attachment> attachments = {};
68 if (object.contains("attachments")) {
69 auto attachmentSpecs = object["attachments"].toList();
70 for (int i = 0; i < attachmentSpecs.size(); ++i) {
71 auto const &spec = attachmentSpecs.at(i).toMap();
72 attachments << Attachment{spec["name"].toString(),
73 spec["name"].toString(),
74 spec["mimeType"].toByteArray(),
75 false,
76 spec["data"].toByteArray()};
77 }
78 }
79
67 KMime::Types::Mailbox mb; 80 KMime::Types::Mailbox mb;
68 mb.fromUnicodeString("identity@example.org"); 81 mb.fromUnicodeString("identity@example.org");
69 auto msg = MailTemplates::createMessage({}, 82 auto msg = MailTemplates::createMessage({},
@@ -74,7 +87,7 @@ static void createMail(const QVariantMap &object, const QByteArray &folder = {})
74 object["subject"].toString(), 87 object["subject"].toString(),
75 object["body"].toString(), 88 object["body"].toString(),
76 object["bodyIsHtml"].toBool(), 89 object["bodyIsHtml"].toBool(),
77 {}, 90 attachments,
78 {}, 91 {},
79 {}); 92 {});
80 if (object.contains("messageId")) { 93 if (object.contains("messageId")) {
diff --git a/views/conversation/main.qml b/views/conversation/main.qml
index 64f7f273..cb42fe5c 100644
--- a/views/conversation/main.qml
+++ b/views/conversation/main.qml
@@ -127,6 +127,81 @@ ApplicationWindow {
127 to: ["to@example.org"], 127 to: ["to@example.org"],
128 unread: true 128 unread: true
129 }, 129 },
130 {
131 resource: "resource1",
132 date: "2017-07-20T17:47:29",
133 subject: "WithAttachment",
134 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",
135 to: ["to@example.org"],
136 unread: true,
137 attachments: [
138 {
139 name: "myImage.png",
140 mimeType: "image/png",
141 data: "no real data",
142 }
143 ],
144 },
145 {
146 resource: "resource1",
147 date: "2017-07-20T17:47:29",
148 subject: "WithBadPKeyAttachment",
149 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",
150 to: ["to@example.org"],
151 unread: true,
152 attachments: [
153 {
154 name: "myKey.asc",
155 mimeType: "application/pgp-keys",
156 data: "no real data",
157 }
158 ],
159 },
160 {
161 resource: "resource1",
162 date: "2017-07-20T17:47:29",
163 subject: "WithGoodPKeyAttachment",
164 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",
165 to: ["to@example.org"],
166 unread: true,
167 attachments: [
168 {
169 name: "myKey.asc",
170 mimeType: "application/pgp-keys",
171 data:
172"-----BEGIN PGP PUBLIC KEY BLOCK-----
173
174mQENBEr9ij4BCADaFvyhzV7IrCAr/sCvfoPerAd4dYIGTeCeBmInu3p4oEG0rXTB
1752zL2t9zd7jVwCmYLsqb0Y4+7UIulBTSVa/SxsFkxPIzQaGd+CYpIpCl2P7oXBQH/
176365i/gvng4UTb5CytBp9MToft2tUgqvK/LD30fBWbWVE1ohmuGYDviJesuqJGeRG
177KPOmjRk8LcXecZoNAnahy6y/rHPQzbC7LVazrWfdYCsZ1w202kwwLAPj0aNO6d4n
178M9NYo26/mB+5+odJ5gbxfdKWQQOFCha8UzEXbZzjJsRNFhUyuEDEd2gBlhDm3jrY
179ACT3u1adLJ1GsY6biN3u1IEUwi/7+uofZRPXABEBAAG0K3VuaXR0ZXN0IGtleSAo
180bm8gcGFzc3dvcmQpIDx0ZXN0QGtvbGFiLm9yZz6JATgEEwECACIFAkr9ij4CGwMG
181CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEI2YYMWPJG3mxggH/iDnmHhKI40r
182bPvDPSMVFz4pNL5oYrGMjOUIz5ibjn9N19Fz/T5kxupbYVRbdcx6kRy4uQd97sJ8
1834985JkHEr/TSHne5p0F+tQLKq+WcJST+cbvkFR9m9WTZISOo+bP/rKGsf6GOGfl/
184vzObv8tF0E8Yy0Lu1lYdynnBRygT+VKt5GzcNzsS3Af1kgrnoQ1gVWjKueSR32hJ
185BILfOpQlKP/RdrOND1N0uljaJBQsUmYDJ5Gd+YL0VX4/56tfqt4gcuqhiD+Vz6BG
186+55gqwuFK4/o2gawPELjOLUy5dh/b6MDvWehbasRPcyT1fFm9YY6iku4ZEx8EzLv
187IJKiXLAx1+i5AQ0ESv2KPgEIAO6+rYyBG0YBfacSx36VCrzvRe8V0CqmUkzIHZJ0
188EN/s95yCQwG0yC3M0KRGDzTeCXRik68h/qdw3KEgfZzu4rJAj9w/J4JMtcuhuCYL
189rL4iP32hvLfqZDqwBaRCmlEkqArF0Jahb5SW3cPYZlE+I9I2V1xYX3bSZ7jcihAx
190VWtkheYtZcDY3u/7cWZNUauGNKRh4E0+ToCBI+erEd5EPCQDQrL/e5pEj+s/+Coy
191BvJeQdAPX/wjfYVe8t+5GDLqOvpbUBWJWUptv/oTd3wOtJCwwr/OWNeXf7ipgtoG
192KpJgr+FHLOEb3cXtF1YPzwpTOs/J/bv3JdGyQ3Kx1BlTUzUAEQEAAYkBHwQYAQIA
193CQUCSv2KPgIbDAAKCRCNmGDFjyRt5h0nB/40FPmVWhD2ok3opPRTwYMzUAOHkgMU
194k2bJfIH185hMvnHLAPCgUMr8xvjcx3NphiRCaC6mabIxHI9hDAbi6uyDBNTyQtm2
195sl/r1vqjFcxX49l9yt0AgMy3284IdwK9xdlwMLY/MbCL9GKe/D6RmZ6i/4wpxHdP
1969X3cGh66UW09NUO1Gria0isRfwf/OxkV+KxA7qxX2bWOHS3noUAj7I43MJCvTuAP
197gTIgEfjdpx1C2Tv97SxoLZ4t6raztvmwqIyCQIuzukD0H9JGFjWT9bGY7obPl7hO
198Bvr+rojxTJ3X+pzb2LJQwJnALL/VdIF3yHtGu2//Yfu4oxGGA0M90KiW
199=an8Y
200-----END PGP PUBLIC KEY BLOCK-----
201",
202 }
203 ],
204 },
130 ] 205 ]
131 }], 206 }],
132 } 207 }