summaryrefslogtreecommitdiffstats
path: root/framework
diff options
context:
space:
mode:
authorRémi Nicole <nicole@kolabsystems.com>2018-03-09 13:32:10 +0100
committerChristian Mollekopf <chrigi_1@fastmail.fm>2018-03-09 13:32:24 +0100
commitd5b5c33a0cb3fbe02d011a14f2028249220b0656 (patch)
treedb0ebb30db599a6889ea6066ec33bf5f862c8444 /framework
parent93e9c10d1894797b5826bbdfcc324f4235c9e193 (diff)
downloadkube-d5b5c33a0cb3fbe02d011a14f2028249220b0656.tar.gz
kube-d5b5c33a0cb3fbe02d011a14f2028249220b0656.zip
Automatic key import / export + Expected monad
Summary: There are many things going on here (perhaps a bit much for a single patch): - When an attachment is of mime type "application/pgp-keys", a button is added to import the key to GPG - When sending a mail and crypto is enabled (encryption, signing or both), the public key of the first private key found is sent as an un-encrypted attachment (T6994) - The `mailcrypto.{h,cpp}` was, for the most part, rewritten - Introduction of the expected monad, inspired by what was proposed for C++ [here](https://isocpp.org/files/papers/n4015.pdf), but not at all a strict implementation of this specification. We may want to add some more features of this standard later. The rationale for some of the choices: - I found mailcrypto a bit hard to edit to add new features, and a great part was commented code to prepare for the support the SMIME crypto format, which would (in my current knowledge) not be used for sending emails. - One thing I found that may be missing in the code base was a standardized way of handling errors in C++ code. Since exceptions are disabled I think that the functional way is the way to go. After some research I found the Expected monad / tagged union / sum type, which seemed to suit the problem particularly well. In the long run, I hope we would move the entire code base to use `Expected` to indicate if a function might fail. Of course every choice made here is to be considered as a proposition for doing things / RFC, critics wholeheartedly accepted. Reviewers: cmollekopf Tags: #kube Maniphest Tasks: T6994, T8147, T6995 Differential Revision: https://phabricator.kde.org/D11158
Diffstat (limited to 'framework')
-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
12 files changed, 745 insertions, 394 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};