EasyGPG: automatic encryption and decryption of emails

Introduce Encrypt and Decrypt message filters to automaticaly perform
crypto operations on incoming emails.

Differential Revision: https://phabricator.kde.org/D7041
wilder
Daniel Vrátil 9 years ago
parent f81e2ada5c
commit bd19def5bd
  1. 4
      CMakeLists.txt
  2. 3
      src/CMakeLists.txt
  3. 18
      src/filter/autotests/CMakeLists.txt
  4. 108
      src/filter/autotests/filteractiondecrypttest.cpp
  5. 40
      src/filter/autotests/filteractiondecrypttest.h
  6. 135
      src/filter/autotests/filteractionencrypttest.cpp
  7. 38
      src/filter/autotests/filteractionencrypttest.h
  8. 18
      src/filter/autotests/gpgdata/multipart-alternative.msg
  9. 34
      src/filter/autotests/gpgdata/multipart-alternative.msg.pgp
  10. 23
      src/filter/autotests/gpgdata/multipart-alternative.msg.smime
  11. 8
      src/filter/autotests/gpgdata/text-plain.msg
  12. 32
      src/filter/autotests/gpgdata/text-plain.msg.pgp
  13. 21
      src/filter/autotests/gpgdata/text-plain.msg.smime
  14. 141
      src/filter/autotests/gpghelper.cpp
  15. 51
      src/filter/autotests/gpghelper.h
  16. 0
      src/filter/autotests/gpghome/.gpg-v21-migrated
  17. 32
      src/filter/autotests/gpghome/openpgp-revocs.d/818AE8DA30F81B0CEA4403BA358732559B8659B2.rev
  18. BIN
      src/filter/autotests/gpghome/private-keys-v1.d/1AB79D3867F2789661A1FF16675F5778804FD2AD.key
  19. BIN
      src/filter/autotests/gpghome/private-keys-v1.d/89F939DAC83D1CFDC09042B238D430FFC2BF662C.key
  20. BIN
      src/filter/autotests/gpghome/private-keys-v1.d/E16CFA285A3D4C9D4402FFF4BE8968ABDC855AD7.key
  21. BIN
      src/filter/autotests/gpghome/pubring.gpg
  22. BIN
      src/filter/autotests/gpghome/pubring.kbx
  23. BIN
      src/filter/autotests/gpghome/random_seed
  24. 0
      src/filter/autotests/gpghome/secring.gpg
  25. 21
      src/filter/autotests/gpghome/smime.cert
  26. BIN
      src/filter/autotests/gpghome/trustdb.gpg
  27. 20
      src/filter/autotests/gpghome/trustlist.txt
  28. 93
      src/filter/filteractions/filteractiondecrypt.cpp
  29. 48
      src/filter/filteractions/filteractiondecrypt.h
  30. 4
      src/filter/filteractions/filteractiondict.cpp
  31. 296
      src/filter/filteractions/filteractionencrypt.cpp
  32. 66
      src/filter/filteractions/filteractionencrypt.h
  33. 228
      src/filter/filteractions/filteractionwithcrypto.cpp
  34. 59
      src/filter/filteractions/filteractionwithcrypto.h

@ -22,9 +22,9 @@ include(ECMCoverageOption)
set(MAILCOMMON_LIB_VERSION ${PIM_VERSION})
set(AKONADIMIME_LIB_VERSION "5.6.40")
set(MESSAGELIB_LIB_VERSION "5.6.40")
set(MESSAGELIB_LIB_VERSION "5.6.42")
set(QT_REQUIRED_VERSION "5.7.0")
set(KMIME_LIB_VERSION "5.6.40")
set(KMIME_LIB_VERSION "5.6.41")
set(KMAILTRANSPORT_LIB_VERSION "5.6.40")
set(MAILIMPORTER_LIB_VERSION "5.6.40")
set(LIBKDEPIM_LIB_VERSION "5.6.40")

@ -19,8 +19,10 @@ set(libmailcommon_filter_SRCS
filter/filteractions/filteractionaddtag.cpp
filter/filteractions/filteractionaddtoaddressbook.cpp
filter/filteractions/filteractioncopy.cpp
filter/filteractions/filteractiondecrypt.cpp
filter/filteractions/filteractiondelete.cpp
filter/filteractions/filteractiondict.cpp
filter/filteractions/filteractionencrypt.cpp
filter/filteractions/filteractionexec.cpp
filter/filteractions/filteractionforward.cpp
filter/filteractions/filteractionmove.cpp
@ -40,6 +42,7 @@ set(libmailcommon_filter_SRCS
filter/filteractions/filteractionwidget.cpp
filter/filteractions/filteractionwithaddress.cpp
filter/filteractions/filteractionwithcommand.cpp
filter/filteractions/filteractionwithcrypto.cpp
filter/filteractions/filteractionwithfolder.cpp
filter/filteractions/filteractionwithnone.cpp
filter/filteractions/filteractionwithstring.cpp

@ -4,6 +4,8 @@ set(mailcommon_filter_test_LIBS Qt5::Test Qt5::Gui KF5::AkonadiCore KF5::Akonadi
KF5::MailTransport KF5::I18n
)
add_definitions(-DTEST_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}\")
macro(add_mailcommon_filter_test _name)
ecm_add_test(${ARGN}
TEST_NAME ${_name}
@ -49,6 +51,22 @@ add_mailcommon_filter_test(filteractiondeletetest
${filter_common_SRCS}
)
add_mailcommon_filter_test(filteractionencrypttest
filteractionencrypttest.cpp
gpghelper.cpp
../filteractions/filteractionencrypt.cpp
../filteractions/filteractionwithcrypto.cpp
${filter_common_SRCS}
)
add_mailcommon_filter_test(filteractiondecrypttest
filteractiondecrypttest.cpp
gpghelper.cpp
../filteractions/filteractiondecrypt.cpp
../filteractions/filteractionwithcrypto.cpp
${filter_common_SRCS}
)
add_mailcommon_filter_test(filteractionrewriteheadertest
filteractionrewriteheadertest.cpp
../filteractions/filteractionrewriteheader.cpp

@ -0,0 +1,108 @@
/*
* Copyright (c) 2017 Daniel Vrátil <dvratil@kde.org>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "filteractiondecrypttest.h"
#include "../filteractions/filteractiondecrypt.h"
#include <QTest>
void FilterActionDecryptTest::initTestCase()
{
mGpg = new GPGHelper(QString::fromUtf8(TEST_PATH) + QStringLiteral("/gpghome"));
QVERIFY(mGpg->isValid());
}
void FilterActionDecryptTest::cleanupTestCase()
{
delete mGpg;
}
void FilterActionDecryptTest::shouldDecrypt_data()
{
QTest::addColumn<QByteArray>("content");
QTest::addColumn<QByteArray>("encrypted");
QDir testDir(QString::fromUtf8(TEST_PATH) + QStringLiteral("/gpgdata"));
const auto tests = testDir.entryInfoList({ QStringLiteral("*.msg") }, QDir::Files, QDir::Name);
for (const auto test : tests) {
QFile plain(test.absoluteFilePath());
QVERIFY(plain.open(QIODevice::ReadOnly));
const auto plainData = plain.readAll();
QFile pgp(test.absoluteFilePath() + QStringLiteral(".pgp"));
QVERIFY(pgp.open(QIODevice::ReadOnly));
QTest::newRow(QStringLiteral("PGP %1").arg(test.baseName()).toUtf8().constData())
<< plainData << pgp.readAll();
QFile smime(test.absoluteFilePath() + QStringLiteral(".smime"));
QVERIFY(smime.open(QIODevice::ReadOnly));
QTest::newRow(QStringLiteral("SMIME %1").arg(test.baseName()).toUtf8().constData())
<< plainData << smime.readAll();
QTest::newRow(QStringLiteral("PLAIN %1").arg(test.baseName()).toUtf8().constData())
<< plainData << plainData;
}
}
void FilterActionDecryptTest::shouldDecrypt()
{
QFETCH(QByteArray, content);
QFETCH(QByteArray, encrypted);
MailCommon::FilterActionDecrypt action(this);
auto msg = KMime::Message::Ptr::create();
msg->setContent(encrypted);
msg->parse();
msg->assemble();
Akonadi::Item item;
item.setPayload(msg);
MailCommon::ItemContext context(item, true);
const auto result = action.process(context, false);
QCOMPARE(result, MailCommon::FilterAction::GoOn);
if (content != encrypted) {
QVERIFY(context.needsPayloadStore());
} else {
// the message is not encrypted, no change is needed
QVERIFY(!context.needsPayloadStore());
}
auto newMsg = context.item().payload<KMime::Message::Ptr>();
QCOMPARE(newMsg->from()->asUnicodeString(), msg->from()->asUnicodeString());
QCOMPARE(newMsg->to()->asUnicodeString(), msg->to()->asUnicodeString());
QCOMPARE(newMsg->date()->asUnicodeString(), msg->date()->asUnicodeString());
QCOMPARE(newMsg->subject()->asUnicodeString(), msg->subject()->asUnicodeString());
auto decrypted = newMsg->encodedContent();
KMime::Message decryptedContent;
decryptedContent.setContent(newMsg->encodedContent());
decryptedContent.parse();
KMime::Message expectedContent;
expectedContent.setContent(content);
expectedContent.parse();
QCOMPARE(decryptedContent.from()->asUnicodeString(), expectedContent.from()->asUnicodeString());
QCOMPARE(decryptedContent.to()->asUnicodeString(), expectedContent.to()->asUnicodeString());
QCOMPARE(decryptedContent.date()->asUnicodeString(), expectedContent.date()->asUnicodeString());
QCOMPARE(decryptedContent.subject()->asUnicodeString(), expectedContent.subject()->asUnicodeString());
QCOMPARE(decryptedContent.contentType()->asUnicodeString(), expectedContent.contentType()->asUnicodeString());
QCOMPARE(decryptedContent.encodedBody(), expectedContent.encodedBody());
}
QTEST_MAIN(FilterActionDecryptTest)

@ -0,0 +1,40 @@
/*
* Copyright (c) 2017 Daniel Vrátil <dvratil@kde.org>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef FILTERACTIONDECRYPTTEST_H_
#define FILTERACTIONDECRYPTTEST_H_
#include <QObject>
#include "gpghelper.h"
class FilterActionDecryptTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void cleanupTestCase();
void shouldDecrypt_data();
void shouldDecrypt();
private:
GPGHelper *mGpg = {};
};
#endif

@ -0,0 +1,135 @@
/*
* Copyright (c) 2017 Daniel Vrátil <dvratil@kde.org>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "filteractionencrypttest.h"
#include "../filteractions/filteractionencrypt.h"
#include <QTest>
#include <QFileInfo>
void FilterActionEncryptTest::initTestCase()
{
mGpg = new GPGHelper(QString::fromUtf8(TEST_PATH) + QStringLiteral("/gpghome"));
QVERIFY(mGpg->isValid());
}
void FilterActionEncryptTest::cleanupTestCase()
{
delete mGpg;
}
void FilterActionEncryptTest::shouldEncrypt_data()
{
QTest::addColumn<QString>("key");
QTest::addColumn<QByteArray>("content"); // content for decryption
QTest::addColumn<QByteArray>("expected"); // decrypted expected content
const auto smimeKey = QStringLiteral("SMIME:%1:0FDD972BCEFB5735DC7E8EE57DB7BA4E5FDBE218");
const auto pgpKey = QStringLiteral("PGP:%1:818AE8DA30F81B0CEA4403BA358732559B8659B2");
QDir testDir(QString::fromUtf8(TEST_PATH) + QStringLiteral("/gpgdata"));
const auto tests = testDir.entryInfoList({ QStringLiteral("*.msg") }, QDir::Files, QDir::Name);
for (const auto test : tests) {
QFile plain(test.absoluteFilePath());
QVERIFY(plain.open(QIODevice::ReadOnly));
const auto plainData = plain.readAll();
QTest::newRow(QStringLiteral("PGP %1").arg(test.baseName()).toUtf8().constData())
<< pgpKey.arg(0) << plainData << plainData;
QTest::newRow(QStringLiteral("SMIME %1").arg(test.baseName()).toUtf8().constData())
<< smimeKey.arg(0) << plainData << plainData;
QFile smimeFile(test.absoluteFilePath() + QStringLiteral(".smime"));
QVERIFY(smimeFile.open(QIODevice::ReadOnly));
const auto smimeData = smimeFile.readAll();
QFile pgpFile(test.absoluteFilePath() + QStringLiteral(".pgp"));
QVERIFY(pgpFile.open(QIODevice::ReadOnly));
const auto pgpData = pgpFile.readAll();
QTest::newRow(QStringLiteral("PGP %1 re-encrypt").arg(test.baseName()).toUtf8().constData())
<< pgpKey.arg(1) << smimeData << plainData;
QTest::newRow(QStringLiteral("SMIME %1 re-encrypt").arg(test.baseName()).toUtf8().constData())
<< smimeKey.arg(1) << pgpData << plainData;
QTest::newRow(QStringLiteral("PGP %1 re-encrypt same key").arg(test.baseName()).toUtf8().constData())
<< pgpKey.arg(1) << pgpData << plainData;
}
}
void FilterActionEncryptTest::shouldEncrypt()
{
QFETCH(QString, key);
QFETCH(QByteArray, content);
QFETCH(QByteArray, expected);
MailCommon::FilterActionEncrypt action(this);
action.argsFromString(key);
QVERIFY(!action.key().isNull());
QCOMPARE(action.reencrypt(), key.contains(QLatin1String(":1:")));
auto msg = KMime::Message::Ptr::create();
msg->setContent(content);
msg->parse();
msg->assemble();
Akonadi::Item item;
item.setPayload(msg);
MailCommon::ItemContext context(item, true);
const auto result = action.process(context, false);
QCOMPARE(result, MailCommon::FilterAction::GoOn);
QVERIFY(context.needsPayloadStore());
auto newMsg = context.item().payload<KMime::Message::Ptr>();
QCOMPARE(newMsg->from()->asUnicodeString(), msg->from()->asUnicodeString());
QCOMPARE(newMsg->to()->asUnicodeString(), msg->to()->asUnicodeString());
QCOMPARE(newMsg->date()->asUnicodeString(), msg->date()->asUnicodeString());
QCOMPARE(newMsg->subject()->asUnicodeString(), msg->subject()->asUnicodeString());
QString gpgexe;
QByteArray resultContent;
GPGHelper::CryptoType crypto;
if (key.startsWith(QLatin1String("PGP"))) {
QCOMPARE(newMsg->contentType()->mimeType(), QByteArray("multipart/encrypted"));
resultContent = newMsg->encodedContent();
crypto = GPGHelper::OpenPGP;
} else {
QCOMPARE(newMsg->contentType()->mimeType(), QByteArray("application/pkcs7-mime"));
resultContent = QByteArray::fromBase64(newMsg->encodedBody());
crypto = GPGHelper::SMIME;
}
// Check if the message is encrypted with the right key
const auto usedKey = mGpg->encryptionKeyFp(resultContent, crypto);
QCOMPARE(usedKey, QString::fromLatin1(action.key().primaryFingerprint()));
const auto actual = mGpg->decrypt(resultContent, crypto);
KMime::Message actualContent;
actualContent.setContent(actual);
actualContent.parse();
KMime::Message expectedContent;
expectedContent.setContent(expected);
expectedContent.parse();
QCOMPARE(actualContent.from()->asUnicodeString(), expectedContent.from()->asUnicodeString());
QCOMPARE(actualContent.to()->asUnicodeString(), expectedContent.to()->asUnicodeString());
QCOMPARE(actualContent.date()->asUnicodeString(), expectedContent.date()->asUnicodeString());
QCOMPARE(actualContent.subject()->asUnicodeString(), expectedContent.subject()->asUnicodeString());
QCOMPARE(actualContent.contentType()->asUnicodeString(), expectedContent.contentType()->asUnicodeString());
QCOMPARE(actualContent.encodedBody(), expectedContent.encodedBody());
}
QTEST_MAIN(FilterActionEncryptTest)

@ -0,0 +1,38 @@
/*
* Copyright (c) 2017 Daniel Vrátil <dvratil@kde.org>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef FILTERACTIONENCRYPTTEST_H_
#define FILTERACTIONENCRYPTTEST_H_
#include <QObject>
#include "gpghelper.h"
class FilterActionEncryptTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void cleanupTestCase();
void shouldEncrypt_data();
void shouldEncrypt();
private:
GPGHelper *mGpg = {};
};
#endif

@ -0,0 +1,18 @@
From: Daniel Vratil <dvratil@kde.org>
To: KMail Test <kmail@test.kde>
Date: Tue, 01 Aug 2017 07:50:04 +0000
Subject: It's time to get schwifty!
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="nextPart0"
--nextPart0
Content-Type: text/plain
Wubba-lubba-dub-dub!
--nextPart0
Content-Type: text/html
<html><body><strong>Wubba-lubba-dub-dub!</strong></body></html>
--nextPart0--

@ -0,0 +1,34 @@
From: Daniel Vratil <dvratil@kde.org>
To: KMail Test <kmail@test.kde>
Date: Tue, 01 Aug 2017 07:50:04 +0000
Subject: It's time to get schwifty!
MIME-Version: 1.0
Content-Type: multipart/encrypted; boundary="nextPart0"; protocol="application/pgp-encrypted"
--nextPart0
Content-Type: application/pgp-encrypted
Content-Disposition: attachment
Content-Transfer-Encoding: 7Bit
Version: 1
--nextPart0
Content-Type: application/octet-stream
Content-Disposition: inline; filename="msg.asc"
Content-Transfer-Encoding: 7Bit
-----BEGIN PGP MESSAGE-----
hQEMA3IXuHpXwGcWAQf/e5YrKiIPaaOU9zLeajnTipkHbGxgP/ZdDfxApfMMeWG+
r2Ort8PflyvRB4MlFye7PpGTbK3BIFFNzWSlWwhAukbmj0OlJzgAxJZS7OlpX9mk
mae5XkkBnAqqzpwf0d9/JWaOvx4xhzYGjRBoInTxRI+I5iFS1TveQ0j3TsSut6ou
G84zyW2lMWNsXHsl8cGstPsI190LcpoWfdAwW5FTcPr7tqE8JDeNe5BntS3eRx4l
tCvxPQrDtbRdlh6CsMlUjZsRRe1p9l7Ab551YRQFeo+Zi6lP5gnLIImx0KErfiRG
Xcg4VB30jKBj7Q5AhQFCCcKCrLKLZcgs6KUYSb0YPdLAAgEEnoeRmQ6LbUAY9OQb
6+vwM+1vZ6efEkbaBUJwDbFaT9gbdNiS0MyQiNeZav19c/rkIb7JV07ps/d4yU+i
SzqmuwXOv77CjYU+pSmlFvpjVbeZw+YjS7asTEXID6vSE7XOzmjzkjLrJ+tBtBS3
69dVFfOH/1kb1lFa+iKOkiwCnOo4/LfSGGLvx7ueJuZsqHE8rFIryc6MFtng5HlK
qP+apCdxSkbeG3kxQIC0gljhVQYJkGU25So3H6DyRa2U+B+V
=8G22
-----END PGP MESSAGE-----
--nextPart0--

@ -0,0 +1,23 @@
From: Daniel Vratil <dvratil@kde.org>
To: KMail Test <kmail@test.kde>
Date: Tue, 01 Aug 2017 07:50:04 +0000
Subject: It's time to get schwifty!
Content-Type: application/pkcs7-mime; name="smime.p7m"; smime-type="enveloped-data"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="smime.p7m"
MIME-Version: 1.0
MIAGCSqGSIb3DQEHA6CAMIACAQAxggFYMIIBVAIBADA8MDAxCzAJBgNVBAYTAkNa
MQwwCgYDVQQKEwNLREUxEzARBgNVBAMTCktNYWlsIFRlc3QCCDilUKgKqCPGMA0G
CSqGSIb3DQEBAQUABIIBAGqd1mJHm3G29UuwVYZM/TqjLa285LPTAHIqvz86ins+
7cxsM8uu9R8PgYzpkR6HEeF3X/OY5K4YFj2BO2MS0jDh0u12IFeZSat83QHUCVP8
hOQYbs6lB6duXrhSVQ/kGVL2l2Us7AWFqHbNzLZTq8sgHySfI5PRgKxmZRi+0/o+
40zN43azW1RQkcDjxZIQh4etGHbFP/ywBMvW6lShFpz33DFGZzOopdlj834O1aVy
HadFGdqe7oKziQ1xSj05mblHMNeuYHwgB2+5RdemK4y4/BUwu+7LRuUeWjzo3i05
A3mjz0jUtg6tVLx4sB9cw1Bh5kKL9G5o8FiidFkjAcAwgAYJKoZIhvcNAQcBMB0G
CWCGSAFlAwQBAgQQIRY9qD+NYirOMj+Aa4/JLqCABIHgngiipfaUSmGGW2O/YAdb
xHGYH0GwUwsWxEB2+TkSkLvzxmyXQaUoyLn2h+TJjj4ezEmH3Ir/9YQ85uUj2lpl
/KoXeN3QSsIbWaklyuca32pxSOVb5JWTTj03Kdnotnlg9wU97bcpEJ5pIlrZX4L0
kTq+WQsK1198kxzhVCmT5LfAVdIMI1YQrRLkW7uZsa83IFMvqvkofGBKHx7lhSst
nil6+bStPyjfPGJChe0UOyhJicBcrm7CNLNiZPtYLBrAEbnSELqsU6a1OO+6wZ9L
l7CvTrvSj6ZV7JY90OhhhCsEENmHHKkBN0X+259Qkhgb1l4AAAAAAAAAAAAA

@ -0,0 +1,8 @@
From: Daniel Vratil <dvratil@kde.org>
To: KMail Test <kmail@test.kde>
Date: Tue, 01 Aug 2017 07:50:04 +0000
Subject: It's time to get schwifty!
Content-Type: text/plain
MIME-Version: 1.0
Show me what you got!

@ -0,0 +1,32 @@
From: Daniel Vratil <dvratil@kde.org>
To: KMail Test <kmail@test.kde>
Date: Tue, 01 Aug 2017 07:50:04 +0000
Subject: It's time to get schwifty!
Content-Type: multipart/encrypted; boundary="nextPart"; protocol="application/pgp-encrypted"
MIME-Version: 1.0
--nextPart
Content-Type: application/pgp-encrypted
Content-Disposition: attachment
Content-Transfer-Encoding: 7Bit
Version: 1
--nextPart
Content-Type: application/octet-stream
Content-Disposition: inline; filename="msg.asc"
Content-Transfer-Encoding: 7Bit
-----BEGIN PGP MESSAGE-----
hQEMA3IXuHpXwGcWAQf/dYk2TJpb6J6Ji9mA4RzMr5CUyiMkDlOe6+l4BVtRHsLm
g9Bwe4znqCenyf/B6MdGThPAFvYRcN2UPbLEElrZQ5vsCyYAG8sespy3sVQChA1H
063pLvYZDxV/FdA2ckduaxQND9kNdfSONn7+olTaKgDiKA1oSgZ+dvRHG5cr/xQ+
AoYA/iQ65dT3MQyvWF1Iw+AKrbhR8XRfB77Hl+qL/vr62pbnVVA2rHjEu1Tjz04I
6oS55zoetonBVYiSIpQLRu5Uu0ys8jUY29qGgwd/VlBB2Y/YcYY3OTRbkvYBJ2Yd
hc1ca4YCEaJ/9e8BuhLfBzh4J9EZgJ3NYRTm3FZRadJrAftlxAnsG1HmvE3kNV6Q
zHWxlIQZQE+sSr7dbnpyObpHaiAaU1toaH22uKRh+9ZBhhiNYhCHk1KnkUKG7gdR
Wys2qPAhuMVZryoW0uQyX4lA5ZjsetBSxUVffjjHZKYP56dyLzpvMGqWvK0=
=DWvh
-----END PGP MESSAGE-----
--nextPart--

@ -0,0 +1,21 @@
From: Daniel Vratil <dvratil@kde.org>
To: KMail Test <kmail@test.kde>
Date: Tue, 01 Aug 2017 07:50:04 +0000
Subject: It's time to get schwifty!
Content-Type: application/pkcs7-mime; name="smime.p7m"; smime-type="enveloped-data"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="smime.p7m"
MIME-Version: 1.0
MIAGCSqGSIb3DQEHA6CAMIACAQAxggFYMIIBVAIBADA8MDAxCzAJBgNVBAYTAkNa
MQwwCgYDVQQKEwNLREUxEzARBgNVBAMTCktNYWlsIFRlc3QCCDilUKgKqCPGMA0G
CSqGSIb3DQEBAQUABIIBAIGrbP6RHuRHdMhJpXuyRjzWPdApinxmUBtvPCxwfLK7
0ZHa11jAwJbIvouuqblReZX/eCYbBYsgW5aSFn1dSV/lUQ49gOTRHnRXEBrQGK4z
gdNK+g+axqZO4zUtKyEDpKi7bnS2i/aCi5Q61PWFSkjgucrUuxyGP82iK5WQpcuk
0c7xR8cxaZf/tpQGnFRZ6OlUVLi2iSbv+ewbrZnaQgu3XAKQcJHrZlArgtR3cneA
X8vpCDE5R/DpZ9onJLwd/KI1EaoEjeZoa6VMVm324uxG2vAaubvg9p4lBR0Q69hL
MIpt8KGgz4psbAiE838U5jh77o5lxQmviolmuyEtZXswgAYJKoZIhvcNAQcBMB0G
CWCGSAFlAwQBAgQQrA0/KpKc4c8gmw1PZUNJXqCABDAgX5OMUtnouELP2pOKz4TN
pLHtscYX9+1XD4Rn+hqPOZ7aytiFOvpdn2qUqHtqhT8EEDumRT2M6mQ6rouGaXk7
DDAAAAAAAAAAAAAA

@ -0,0 +1,141 @@
/*
* Copyright (c) 2017 Daniel Vrátil <dvratil@kde.org>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "gpghelper.h"
#include <QFileInfo>
#include <QDebug>
#include <QProcess>
#include <QTest>
namespace {
bool copyRecursively(const QString &src, const QString &dest)
{
QFileInfo srcInfo(src);
if (srcInfo.isDir()) {
QDir destDir(dest);
destDir.cdUp();
if (!destDir.mkdir(QFileInfo(src).fileName())) {
qWarning() << "Failed to create directory" << QFileInfo(src).fileName() << "in" << destDir.path();
return false;
}
QDir srcDir(src);
const auto srcFiles = srcDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System);
for (const auto &fileName : srcFiles) {
const QString srcFile = src + QLatin1Char('/') + fileName;
const QString dstFile = dest + QLatin1Char('/') + fileName;
if (!copyRecursively(srcFile, dstFile)) {
return false;
}
}
} else {
if (!QFile::copy(src, dest)) {
qWarning() << "Failed to copy" << src << "into" << dest;
return false;
}
}
return true;
}
QString gpgexe(GPGHelper::CryptoType crypto)
{
return (crypto == GPGHelper::OpenPGP) ? QStringLiteral("gpg2") : QStringLiteral("gpgsm");
}
} // namespace
GPGHelper::GPGHelper(const QString &templateGnupgHome)
: mValid(false)
{
const auto home = gnupgHome();
mValid = copyRecursively(templateGnupgHome, home);
if (mValid) {
qputenv("GNUPGHOME", home.toUtf8());
}
}
GPGHelper::~GPGHelper()
{
// shutdown gpg-agent
QProcess gpgshutdown;
auto env = gpgshutdown.processEnvironment();
env.insert("GNUPGHOME", gnupgHome());
gpgshutdown.setProcessEnvironment(env);
gpgshutdown.start(QStringLiteral("gpg-connect-agent"));
QVERIFY(gpgshutdown.waitForStarted());
gpgshutdown.write("KILLAGENT");
gpgshutdown.closeWriteChannel();
QVERIFY(gpgshutdown.waitForFinished());
}
QString GPGHelper::gnupgHome() const
{
return mTmpDir.path() + QStringLiteral("/gpghome");
}
QByteArray GPGHelper::runGpg(const QByteArray &in, GPGHelper::CryptoType crypto,
const QStringList &args) const
{
QProcess gpg;
gpg.setReadChannel(QProcess::StandardOutput);
auto env = gpg.processEnvironment();
env.insert("GNUPGHOME", gnupgHome());
gpg.setProcessEnvironment(env);
gpg.start(gpgexe(crypto), args);
if (!gpg.waitForStarted()) {
return {};
}
gpg.write(in);
gpg.closeWriteChannel();
if (!gpg.waitForReadyRead()) {
return {};
}
const auto out = gpg.readAllStandardOutput();
if (!gpg.waitForFinished()) {
return {};
}
return out;
}
QByteArray GPGHelper::decrypt(const QByteArray &enc, GPGHelper::CryptoType crypto) const
{
return runGpg(enc, crypto, { QStringLiteral("-d") });
}
QByteArray GPGHelper::encrypt(const QByteArray &dec, GPGHelper::CryptoType crypto) const
{
return runGpg(dec, crypto, { QStringLiteral("-e") });
}
QString GPGHelper::encryptionKeyFp(const QByteArray &enc, GPGHelper::CryptoType crypto) const
{
const auto data = runGpg(enc, crypto, { QStringLiteral("--fingerprint"),
QStringLiteral("--with-colons") });
int idx = data.indexOf("\nfpr:");
if (idx == -1) {
return {};
}
// Find first non-colon character after "fpr"
for (idx = idx + 4; idx < data.size() && data[idx] == ':'; ++idx);
const int end = data.indexOf(':', idx);
return QString::fromLatin1(data.constData() + idx, end - idx);
}

@ -0,0 +1,51 @@
/*
* Copyright (c) 2017 Daniel Vrátil <dvratil@kde.org>
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef GPGHELPER_H_
#define GPGHELPER_H_
#include <QObject>
#include <QTemporaryDir>
class GPGHelper
{
public:
enum CryptoType {
OpenPGP,
SMIME
};
explicit GPGHelper(const QString &templateGnupgHome);
~GPGHelper();
bool isValid() const { return mValid; }
QString gnupgHome() const;
QByteArray decrypt(const QByteArray &enc, CryptoType crypto) const;
QByteArray encrypt(const QByteArray &dec, CryptoType crypto) const;
QString encryptionKeyFp(const QByteArray &encMsg, GPGHelper::CryptoType crypto) const;
private:
QByteArray runGpg(const QByteArray &in, CryptoType crypt, const QStringList &args) const;
bool mValid;
QTemporaryDir mTmpDir;
};
#endif

@ -0,0 +1,32 @@
This is a revocation certificate for the OpenPGP key:
pub rsa2048 2017-08-01 [SC] [expires: 2019-08-01]
818AE8DA30F81B0CEA4403BA358732559B8659B2
uid KMail Test <kmail@test.kde>
A revocation certificate is a kind of "kill switch" to publicly
declare that a key shall not anymore be used. It is not possible
to retract such a revocation certificate once it has been published.
Use it to revoke this key in case of a compromise or loss of
the secret key. However, if the secret key is still accessible,
it is better to generate a new revocation certificate and give
a reason for the revocation. For details see the description of
of the gpg command "--generate-revocation" in the GnuPG manual.
To avoid an accidental use of this file, a colon has been inserted
before the 5 dashes below. Remove this colon with a text editor
before importing and publishing this revocation certificate.
:-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: This is a revocation certificate
iQE2BCABCAAgFiEEgYro2jD4GwzqRAO6NYcyVZuGWbIFAlmAfNQCHQAACgkQNYcy
VZuGWbJ5hQgAkNopkhsaO0yyu+xs3AymBzha/YDAqQLMEH6b3URyTyF7iyYD1HbG
fwYN/PcH5H6xreSTQiQoopER7hB0H42P1ASRyDyRxTk4EuiOfrgkewa8H+jCe6OK
HPZ3c1g6zo7LZFhIldXZ0aOVaHEuqldWnWJGhxfN2ew4xNUM+7yrA1N4WDRBzI/3
CRzr+sG2wMZKixlpKqthvpH8doGFiNLpY4KmiKh9rjQ5f2KLSGcDDTNXsinRwOrr
jXIV4Y9u0ZugzmgWHlWV6bYX5ChsfI5OzjMCrCm+XuCsdFMca3tArQh/6KOY4zjW
L+iFSgdqd4gewSvpY8FTtKzkRlX2HMpYhg==
=rzUF
-----END PGP PUBLIC KEY BLOCK-----

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDNTCCAh2gAwIBAgIIOKVQqAqoI8YwDQYJKoZIhvcNAQELBQAwMDELMAkGA1UE
BhMCQ1oxDDAKBgNVBAoTA0tERTETMBEGA1UEAxMKS01haWwgVGVzdDAgFw0xNzA4
MDExNTQ0NThaGA8yMDYzMDQwNTE3MDAwMFowMDELMAkGA1UEBhMCQ1oxDDAKBgNV
BAoTA0tERTETMBEGA1UEAxMKS01haWwgVGVzdDCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAJlU6ix8++s4sVz+wuKV7krodegopIqq8JrdxmNzD0FsKDhP
dpabOGckCM3TMAQvE96PJaYKMV+QGdEvbe0qQOkABaEhjLucArBPWJAtymaEA+18
qXGoslHGSeL/x5aBEzvCvRRkHYjLWb4CFv0PfV/GY6SppYUDxgDhnbraRN24/1vH
M2ix8PkBs3Nx8Ti9kY3Da3F0NnfIyGXSKCHbSFqjmnF9DHbNih9tEcgrFO0nJMG0
56nzXKtLgIGe5RQE7jH+TaYALYOUS6FjGFxwiI22DsZmrFzzc12wgW/0jj7oFl5R
3fiUg0fNmqnwR+/pTNyJEwIVdGnQjrdfrDRb26sCAwEAAaNRME8wGQYDVR0RBBIw
EIEOa21haWxAdGVzdC5rZGUwEQYKKwYBBAHaRwICAQQDAQH/MA8GA1UdEwEB/wQF
MAMBAf8wDgYDVR0PAQH/BAQDAgTwMA0GCSqGSIb3DQEBCwUAA4IBAQBoUIiPFtvM
aQSLMh0FWfsSOUW6bmikClGGc7iykFlQbQPgxUiXr2EJlCoaLdTqsa52VdAeNiq1
vGkUTsCMWnmqkhohew3Y4tBK+GkC3J6RFjBppJoPLN0tzR3Vw83JYmvKMnxegG48
ko2qE74CH+ZCoToqrwn433FiA0wE4IQ018d9BcK936PpZbruj4yPDW4yqmnfWlME
g8lA5EbxR6oxWuT83g0LL2JIClyqr6O+dvF1u2PmaD39L3rzImaeENM0ht5sHlGM
O1WcA5efo16WOcqSX6F0cstVy7qE8WJTrzijB731afxTdsIuduHmvT6ywNUgUXeJ
ulcHRYZzTnHg
-----END CERTIFICATE-----

@ -0,0 +1,20 @@
# This is the list of trusted keys. Comment lines, like this one, as
# well as empty lines are ignored. Lines have a length limit but this
# is not a serious limitation as the format of the entries is fixed and
# checked by gpg-agent. A non-comment line starts with optional white
# space, followed by the SHA-1 fingerpint in hex, followed by a flag
# which may be one of 'P', 'S' or '*' and optionally followed by a list of
# other flags. The fingerprint may be prefixed with a '!' to mark the
# key as not trusted. You should give the gpg-agent a HUP or run the
# command "gpgconf --reload gpg-agent" after changing this file.
# Include the default trust list
include-default
# CN=KMail Test
# O=KDE
# C=CZ
0F:DD:97:2B:CE:FB:57:35:DC:7E:8E:E5:7D:B7:BA:4E:5F:DB:E2:18 S relax

@ -0,0 +1,93 @@
/*
* Copyright (c) 2017 Daniel Vrátil <dvratil@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#include "filteractiondecrypt.h"
#include "mailcommon_debug.h"
#include <KLocalizedString>
#include <KMime/Message>
#include <QGpgME/DecryptJob>
#include <QGpgME/Protocol>
#include <gpgme++/decryptionresult.h>
#include <QVector>
#include <Akonadi/KMime/MessageFlags>
using namespace MailCommon;
FilterActionDecrypt::FilterActionDecrypt(QObject *parent)
: FilterActionWithCrypto(QStringLiteral("decrypt"), i18n("Decrypt"), parent)
{
}
FilterActionDecrypt::~FilterActionDecrypt()
{
}
FilterAction *FilterActionDecrypt::newAction()
{
return new FilterActionDecrypt();
}
QString FilterActionDecrypt::displayString() const
{
return i18n("Decrypt");
}
QString FilterActionDecrypt::argsAsString() const
{
return {};
}
void FilterActionDecrypt::argsFromString(const QString &)
{
}
SearchRule::RequiredPart FilterActionDecrypt::requiredPart() const
{
return SearchRule::CompleteMessage;
}
FilterAction::ReturnCode FilterActionDecrypt::process(ItemContext &context, bool) const
{
auto &item = context.item();
if (!item.hasPayload<KMime::Message::Ptr>()) {
return ErrorNeedComplete;
}
auto msg = item.payload<KMime::Message::Ptr>();
if (!KMime::isEncrypted(msg.data())) {
return GoOn;
}
bool wasEncrypted;
auto nec = decryptMessage(msg, wasEncrypted);
if (!nec) {
return wasEncrypted ? ErrorButGoOn: GoOn;
}
context.item().setPayload(nec);
context.item().clearFlag(Akonadi::MessageFlags::Encrypted);
context.setNeedsPayloadStore();
context.setNeedsFlagStore();
return GoOn;
}

@ -0,0 +1,48 @@
/*
* Copyright (c) 2017 Daniel Vrátil <dvratil@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#ifndef MAILCOMMON_FILTERACTION_DECRYPT_H_
#define MAILCOMMON_FILTERACTION_DECRYPT_H_
#include "filteractionwithcrypto.h"
namespace MailCommon {
class FilterActionDecrypt : public FilterActionWithCrypto
{
Q_OBJECT
public:
explicit FilterActionDecrypt(QObject *parent = nullptr);
~FilterActionDecrypt() override;
static FilterAction *newAction();
QString displayString() const override;
QString argsAsString() const override;
void argsFromString(const QString &argsStr) override;
SearchRule::RequiredPart requiredPart() const override;
FilterAction::ReturnCode process(ItemContext &context, bool applyOnOutbound) const override;
};
} // namespace MailCommon
#endif

@ -23,8 +23,10 @@
#include "filteractionaddtag.h"
#include "filteractionaddtoaddressbook.h"
#include "filteractioncopy.h"
#include "filteractiondecrypt.h"
#include "filteractiondelete.h"
#include "filteractionexec.h"
#include "filteractionencrypt.h"
#include "filteractionforward.h"
#include "filteractionmove.h"
#include "filteractionpipethrough.h"
@ -74,6 +76,8 @@ void FilterActionDict::init()
insert(FilterActionAddToAddressBook::newAction);
insert(FilterActionDelete::newAction);
insert(FilterActionUnsetStatus::newAction);
insert(FilterActionEncrypt::newAction);
insert(FilterActionDecrypt::newAction);
// Register custom filter actions below this line.
}

@ -0,0 +1,296 @@
/*
* Copyright (c) 2017 Daniel Vrátil <dvratil@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#include "filteractionencrypt.h"
#include "mailcommon_debug.h"
#include <QEventLoop>
#include <QVBoxLayout>
#include <QCheckBox>
#include <QLabel>
#include <KLocalizedString>
#include <KMime/Message>
#include <QGpgME/EncryptJob>
#include <QGpgME/KeyListJob>
#include <QGpgME/ListAllKeysJob>
#include <QGpgME/Protocol>
#include <gpgme++/encryptionresult.h>
#include <gpgme++/keylistresult.h>
#include <Libkleo/KeySelectionCombo>
#include <Libkleo/DefaultKeyFilter>
#include <MessageComposer/EncryptJob>
#include <Akonadi/KMime/MessageFlags>
#include <KColorScheme>
using namespace MailCommon;
#define LISTING_FINISHED "listingFinished"
FilterActionEncrypt::FilterActionEncrypt(QObject *parent)
: FilterActionWithCrypto(QStringLiteral("encrypt"), i18n("Encrypt"), parent)
, mKeyCache(Kleo::KeyCache::instance())
{
}
FilterActionEncrypt::~FilterActionEncrypt()
{
}
FilterAction *FilterActionEncrypt::newAction()
{
return new FilterActionEncrypt();
}
QString FilterActionEncrypt::displayString() const
{
return label();
}
QString FilterActionEncrypt::argsAsString() const
{
if (mKey.isNull()) {
return {};
}
const auto proto = ((mKey.protocol() == GpgME::OpenPGP) ? QStringLiteral("PGP")
: QStringLiteral("SMIME"));
return QStringLiteral("%1:%2:%3").arg(proto, QString::number(int(mReencrypt)),
QString::fromLatin1(mKey.primaryFingerprint()));
}
void FilterActionEncrypt::argsFromString(const QString &argsStr)
{
const int pos = argsStr.indexOf(QLatin1Char(':'));
const auto protoStr = argsStr.leftRef(pos);
QGpgME::Protocol *proto = {};
if (protoStr == QLatin1String("PGP")) {
proto = QGpgME::openpgp();
} else if (protoStr == QLatin1String("SMIME")) {
proto = QGpgME::smime();
} else {
qCWarning(MAILCOMMON_LOG) << "Unknown protocol specified:" << protoStr;
return;
}
mReencrypt = static_cast<bool>(argsStr.midRef(pos + 1, 1).toInt());
const auto fp = argsStr.mid(pos + 3);
auto listJob = proto->keyListJob(false, true, true);
std::vector<GpgME::Key> keys;
auto result = listJob->exec({ fp }, true, keys);
listJob->deleteLater();
if (result.error()) {
qCWarning(MAILCOMMON_LOG) << "Failed to retrieve keys:" << result.error().asString();
return;
}
if (keys.empty()) {
qCWarning(MAILCOMMON_LOG) << "Could not obtain configured key: key expired or removed?";
// TODO: Interactively ask user to re-configure the filter
return;
}
mKey = keys[0];
}
SearchRule::RequiredPart FilterActionEncrypt::requiredPart() const
{
return SearchRule::CompleteMessage;
}
FilterAction::ReturnCode FilterActionEncrypt::process(ItemContext &context, bool) const
{
if (mKey.isNull()) {
qCWarning(MAILCOMMON_LOG) << "FilterActionEncrypt::process called without filter having a key!";
return ErrorButGoOn;
}
auto &item = context.item();
if (!item.hasPayload<KMime::Message::Ptr>()) {
qCWarning(MAILCOMMON_LOG) << "Item" << item.id() << "does not contain KMime::Message payload!";
return ErrorNeedComplete;
}
auto msg = item.payload<KMime::Message::Ptr>();
if (KMime::isEncrypted(msg.data())) {
if (mReencrypt) {
// Make sure the email is not already encrypted by the mKey - this is
// a little expensive, but still much cheaper than modifying and
// re-uploading the email to the server
const auto encryptionKeys = getEncryptionKeysFromContent(msg, mKey.protocol());
qCDebug(MAILCOMMON_LOG) << "Item" << item.id() << "encrypted by following keys: " << encryptionKeys;
if (!encryptionKeys.isEmpty()) {
if (mKey.protocol() == GpgME::OpenPGP) {
std::vector<std::string> ids;
ids.reserve(encryptionKeys.size());
for (const auto key : encryptionKeys) {
ids.push_back(key.toStdString());
}
for (const auto key : mKeyCache->findByKeyIDOrFingerprint(ids)) {
if (qstrcmp(key.primaryFingerprint(), mKey.primaryFingerprint()) == 0) {
// This email is already encrypted with the target key,
// so there's no need to re-encrypt it
qCDebug(MAILCOMMON_LOG) << "Item" << item.id() << "already encrypted with" << mKey.primaryFingerprint() << ", not re-encrypting";
return GoOn;
}
}
} else if (mKey.protocol() == GpgME::CMS) {
// We are only able to get serial
for (const auto key : mKeyCache->secretKeys()) {
if (qstrcmp(key.issuerSerial(), mKey.issuerSerial()) == 0) {
// This email is already encrypted with the target key,
// so there's no need to re-encrypt it
qCDebug(MAILCOMMON_LOG) << "Item" << item.id() << "already encrypted with" << mKey.primaryFingerprint() << ", not re-encrypting";
return GoOn;
}
}
}
}
bool dummy; // dummy
const auto decrypted = decryptMessage(msg, dummy);
if (!decrypted) {
// We failed to decrypt the encrypted email - very likely we just don't
// have the right key, so don't consider it an error
return GoOn;
} else {
msg = decrypted;
}
} else {
return GoOn;
}
}
MessageComposer::EncryptJob encrypt;
encrypt.setContent(msg.data());
encrypt.setCryptoMessageFormat(mKey.protocol() == GpgME::OpenPGP ? Kleo::OpenPGPMIMEFormat : Kleo::SMIMEFormat);
encrypt.setEncryptionKeys({ mKey });
encrypt.exec();
if (encrypt.error()) {
qCWarning(MAILCOMMON_LOG) << "Encryption error:" << encrypt.errorString();
return ErrorButGoOn;
}
KMime::Content *result = encrypt.content();
result->assemble();
auto nec = assembleMessage(msg, result);
context.item().setPayload(nec);
context.item().setFlag(Akonadi::MessageFlags::Encrypted);
context.setNeedsPayloadStore();
context.setNeedsFlagStore();
delete result;
return GoOn;
}
bool FilterActionEncrypt::isEmpty() const
{
return mKey.isNull();
}
QString FilterActionEncrypt::informationAboutNotValidAction() const
{
return i18n("No encryption key has been selected");
}
QWidget *FilterActionEncrypt::createParamWidget(QWidget *parent) const
{
auto w = new QWidget(parent);
auto l = new QVBoxLayout;
w->setLayout(l);
auto combo = new Kleo::KeySelectionCombo(w);
combo->setDefaultKey(QString::fromLatin1(mKey.primaryFingerprint()));
std::shared_ptr<Kleo::DefaultKeyFilter> filter(new Kleo::DefaultKeyFilter);
filter->setIsOpenPGP(Kleo::DefaultKeyFilter::DoesNotMatter);
filter->setCanEncrypt(Kleo::DefaultKeyFilter::Set);
filter->setHasSecret(Kleo::DefaultKeyFilter::Set);
combo->setKeyFilter(filter);
combo->setProperty(LISTING_FINISHED, false);
connect(combo, &Kleo::KeySelectionCombo::keyListingFinished,
combo, [combo] {
combo->setProperty(LISTING_FINISHED, true);
});
connect(combo, &Kleo::KeySelectionCombo::currentKeyChanged,
this, &FilterActionEncrypt::filterActionModified);
l->addWidget(combo);
auto chkBox = new QCheckBox(w);
chkBox->setText(i18n("Re-encrypt encrypted emails with this key"));
chkBox->setChecked(mReencrypt);
l->addWidget(chkBox);
auto lbl = new QLabel(w);
auto palette = lbl->palette();
palette.setColor(lbl->foregroundRole(), KColorScheme(QPalette::Normal).foreground(KColorScheme::NegativeText).color());
lbl->setPalette(palette);
lbl->setWordWrap(true);
lbl->setText(i18n("<b>Warning:</b> the encrypted emails will be uploaded back to the server!"));
lbl->setToolTip(i18n("<p>You will not be able to read the encrypted emails on any other computer "
"or email client unless you have your private key available there.</p>"
"<p>Also note that most webmail interfaces don't support encryption, so you "
"will not be able to read the encrypted emails there.</p>"));
l->addWidget(lbl);
return w;
}
void FilterActionEncrypt::setParamWidgetValue(QWidget *paramWidget) const
{
if (auto combo = paramWidget->findChild<Kleo::KeySelectionCombo*>()) {
combo->setDefaultKey(QString::fromLatin1(mKey.primaryFingerprint()));
combo->setCurrentKey(QString::fromLatin1(mKey.primaryFingerprint()));
}
if (auto chkBox = paramWidget->findChild<QCheckBox*>()) {
chkBox->setChecked(mReencrypt);
}
}
void FilterActionEncrypt::applyParamWidgetValue(QWidget *paramWidget)
{
if (auto combo = paramWidget->findChild<Kleo::KeySelectionCombo*>()) {
// FIXME: This is super-ugly, but unfortunately the filtering code generates
// several instances of this filter and passes the paramWidgets from one
// instance to another to "copy" stuff in between, which in our case leads
// to this method being called on an un-populated combobox
if (!combo->property(LISTING_FINISHED).toBool()) {
QEventLoop ev;
connect(combo, &Kleo::KeySelectionCombo::keyListingFinished,
&ev, &QEventLoop::quit, Qt::QueuedConnection);
ev.exec();
}
mKey = combo->currentKey();
}
if (auto chkBox = paramWidget->findChild<QCheckBox*>()) {
mReencrypt = chkBox->isChecked();
}
}

@ -0,0 +1,66 @@
/*
* Copyright (c) 2017 Daniel Vrátil <dvratil@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#ifndef MAILCOMMON_FILTERACTION_ENCRYPT_H_
#define MAILCOMMON_FILTERACTION_ENCRYPT_H_
#include "filteractionwithcrypto.h"
#include <gpgme++/key.h>
#include <Libkleo/KeyCache>
namespace MailCommon {
class FilterActionEncrypt : public FilterActionWithCrypto
{
Q_OBJECT
public:
explicit FilterActionEncrypt(QObject *parent = nullptr);
~FilterActionEncrypt() override;
static FilterAction *newAction();
QString displayString() const override;
QString argsAsString() const override;
void argsFromString(const QString &argsStr) override;
SearchRule::RequiredPart requiredPart() const override;
FilterAction::ReturnCode process(ItemContext &context, bool applyOnOutbound) const override;
bool isEmpty() const override;
QString informationAboutNotValidAction() const override;
QWidget *createParamWidget(QWidget *parent) const override;
void setParamWidgetValue(QWidget *paramWidget) const override;
void applyParamWidgetValue(QWidget *paramWidget) override;
GpgME::Key key() const { return mKey; }
bool reencrypt() const { return mReencrypt; }
private:
std::shared_ptr<const Kleo::KeyCache> mKeyCache;
GpgME::Key mKey;
bool mReencrypt;
};
} // namespace MailCommon
#endif

@ -0,0 +1,228 @@
/*
* Copyright (c) 2017 Daniel Vrátil <dvratil@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#include "filteractionwithcrypto.h"
#include "mailcommon_debug.h"
#include <QGpgME/Protocol>
#include <QGpgME/DecryptJob>
#include <gpgme++/decryptionresult.h>
#include <QProcess>
#include <QStandardPaths>
#include <QRegularExpression>
using namespace MailCommon;
bool FilterActionWithCrypto::isPGP(const KMime::Content *part, bool allowOctetStream) const
{
const auto ct = static_cast<KMime::Headers::ContentType*>(part->headerByType("Content-Type"));
return ct &&(ct->isSubtype("pgp-encrypted") || ct->isSubtype("encrypted")
|| (allowOctetStream && ct->isMimeType("application/octet-stream")));
}
bool FilterActionWithCrypto::isSMIME(const KMime::Content *part) const
{
const auto ct = static_cast<KMime::Headers::ContentType*>(part->headerByType("Content-Type"));
return ct && (ct->isSubtype("pkcs7-mime") || ct->isSubtype("x-pkcs7-mime"));
}
KMime::Message::Ptr FilterActionWithCrypto::decryptMessage(const KMime::Message::Ptr &msg,
bool &wasEncrypted) const
{
QGpgME::Protocol *proto = {};
if (msg->mainBodyPart("multipart/encrypted")) {
const auto subparts = msg->contents();
for (auto subpart : subparts) {
if (isPGP(subpart, true)) {
proto = QGpgME::openpgp();
break;
} else if (isSMIME(subpart)) {
proto = QGpgME::smime();
break;
}
}
} else {
if (isPGP(msg.data())) {
proto = QGpgME::openpgp();
} else if (isSMIME(msg.data())) {
proto = QGpgME::smime();
}
}
if (!proto) {
// Not encrypted, or we don't recognize the encryption
wasEncrypted = false;
return {};
}
wasEncrypted = true;
QByteArray outData, inData;
if (proto == QGpgME::smime()) {
inData = QByteArray::fromBase64(msg->encodedBody());
} else {
inData = msg->encodedContent();
}
auto decrypt = proto->decryptJob();
auto result = decrypt->exec(inData, outData);
if (result.error()) {
// unknown key, invalid algo, or general error
qCWarning(MAILCOMMON_LOG) << "Failed to decrypt:" << result.error().asString();
return {};
}
KMime::Content decCt;
decCt.setContent(outData);
decCt.parse();
decCt.assemble();
return assembleMessage(msg, &decCt);
}
void FilterActionWithCrypto::copyHeader(const KMime::Headers::Base *header,
KMime::Message::Ptr msg) const
{
auto newHdr = KMime::Headers::createHeader(header->type());
if (!newHdr) {
newHdr = new KMime::Headers::Generic(header->type());
}
newHdr->fromUnicodeString(header->asUnicodeString(), "UTF-8");
msg->appendHeader(newHdr);
}
bool FilterActionWithCrypto::isContentHeader(const KMime::Headers::Base *header) const
{
return header->is("Content-Type")
|| header->is("Content-Transfer-Encoding")
|| header->is("Content-Disposition");
}
KMime::Message::Ptr FilterActionWithCrypto::assembleMessage(const KMime::Message::Ptr &orig,
const KMime::Content *newContent) const
{
auto out = KMime::Message::Ptr::create();
// Use the new content as message content
out->setBody(const_cast<KMime::Content*>(newContent)->encodedBody());
out->parse();
// Copy over headers from the original message, except for CT, CTE and CD
// headers, we want to preserve those from the new content
QVector<KMime::Headers::Base*> headers = orig->headers();
for (const auto hdr : qAsConst(headers)) {
if (isContentHeader(hdr)) {
continue;
}
copyHeader(hdr, out);
}
// Overwrite some headers by those provided by the new content
headers = newContent->headers();
for (const auto hdr : qAsConst(headers)) {
if (isContentHeader(hdr)) {
copyHeader(hdr, out);
}
}
out->assemble();
out->parse();
return out;
}
QStringList FilterActionWithCrypto::getEncryptionKeysFromContent(const KMime::Message::Ptr &msg,
GpgME::Protocol protocol) const
{
if (protocol == GpgME::CMS && mGpgSmPath.isNull()) {
auto path = QStandardPaths::findExecutable(QStringLiteral("gpgsm"));
mGpgSmPath = path.isEmpty() ? QStringLiteral("") : path;
} else if (protocol == GpgME::OpenPGP && mGpgPath.isNull()) {
auto path = QStandardPaths::findExecutable(QStringLiteral("gpg2"));
if (path.isEmpty()) {
path = QStandardPaths::findExecutable(QStringLiteral("gpg"));
mGpgPath = path.isEmpty() ? QStringLiteral("") : path;
} else {
mGpgPath = path;
}
}
if ((protocol == GpgME::CMS && mGpgSmPath.isEmpty())
|| (protocol == GpgME::OpenPGP && mGpgPath.isEmpty())) {
return {};
}
QProcess gpg;
QStringList keyIds;
// TODO: contribute an API for this into gpgme
if (protocol == GpgME::OpenPGP) {
gpg.setProgram(mGpgPath);
// --list-packets will give us list of keys used to encrypt the message
// --batch will prevent gpg from asking for decryption password (we don't need it yet)
gpg.setArguments({ QStringLiteral("--list-packets"), QStringLiteral("--batch") });
gpg.start(QIODevice::ReadWrite);
gpg.waitForStarted();
gpg.write(msg->encodedContent());
gpg.closeWriteChannel();
gpg.waitForFinished();
while (!gpg.atEnd()) {
const auto l = gpg.readLine();
if (l.startsWith(":pubkey")) {
const int pos = l.indexOf("keyid ");
if (pos < 0) {
continue;
}
const int start = pos + 6; // strlen("keyid ")
const int len = l.size() - start - 1; // -1 to skip trailing \n
keyIds << QString::fromUtf8(l.mid(start, len));
}
}
} else if (protocol == GpgME::CMS) {
gpg.setProgram(mGpgSmPath);
// --decrypt - the only way how to get the keys from gpgsm, sadly, is to decrypt the email
// --status-fd 2 - make sure the status output is not mangled with the decrypted content
// --assume-base64 - so that we don't have to decode it ourselves
gpg.setArguments({ QStringLiteral("--decrypt"),
QStringLiteral("--status-fd"), QStringLiteral("2"),
QStringLiteral("--debug-level"), QStringLiteral("basic"),
QStringLiteral("--assume-base64") });
gpg.start(QIODevice::ReadWrite);
gpg.waitForStarted();
gpg.write(msg->encodedBody()); // just the body!
gpg.closeWriteChannel();
gpg.waitForFinished();
gpg.setReadChannel(QProcess::StandardError);
while (!gpg.atEnd()) {
const auto l = gpg.readLine();
if (l.startsWith("gpgsm: DBG: recp ")) {
const int pos = l.indexOf("serial: ");
if (pos < 0) {
continue;
}
const int start = pos + 8; // strlen("serial: ")
const int len = l.size() - start - 1; // -1 to skip trailing \n
keyIds << QString::fromUtf8(l.mid(start, len));
}
}
}
return keyIds;
}

@ -0,0 +1,59 @@
/*
* Copyright (c) 2017 Daniel Vrátil <dvratil@kde.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#ifndef MAILCOMMON_FILTERACTION_WITH_CRYPTO_H_
#define MAILCOMMON_FILTERACTION_WITH_CRYPTO_H_
#include "filteraction.h"
#include <gpgme++/global.h>
namespace MailCommon {
class FilterActionWithCrypto : public FilterAction
{
Q_OBJECT
protected:
using FilterAction::FilterAction;
KMime::Message::Ptr assembleMessage(const KMime::Message::Ptr &orig,
const KMime::Content *newContent) const;
KMime::Message::Ptr decryptMessage(const KMime::Message::Ptr &decrypt,
bool &wasEncrypted) const;
bool isPGP(const KMime::Content *content, bool allowOctetStream = false) const;
bool isSMIME(const KMime::Content *content) const;
QStringList getEncryptionKeysFromContent(const KMime::Message::Ptr &msg, GpgME::Protocol proto) const;
private:
void copyHeader(const KMime::Headers::Base *header,
KMime::Message::Ptr destMsg) const;
bool isContentHeader(const KMime::Headers::Base *header) const;
private:
// cached values
mutable QString mGpgSmPath;
mutable QString mGpgPath;
};
} // namespace MailCommon
#endif
Loading…
Cancel
Save