diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,7 +22,7 @@ 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(KMAILTRANSPORT_LIB_VERSION "5.6.40") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,9 +19,11 @@ 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/filteractionexec.cpp + filter/filteractions/filteractionencrypt.cpp filter/filteractions/filteractionforward.cpp filter/filteractions/filteractionmove.cpp filter/filteractions/filteractionpipethrough.cpp diff --git a/src/filter/autotests/CMakeLists.txt b/src/filter/autotests/CMakeLists.txt --- a/src/filter/autotests/CMakeLists.txt +++ b/src/filter/autotests/CMakeLists.txt @@ -4,6 +4,8 @@ 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,20 @@ ${filter_common_SRCS} ) +add_mailcommon_filter_test(filteractionencrypttest + filteractionencrypttest.cpp + gpghelper.cpp + ../filteractions/filteractionencrypt.cpp + ${filter_common_SRCS} +) + +add_mailcommon_filter_test(filteractiondecrypttest + filteractiondecrypttest.cpp + gpghelper.cpp + ../filteractions/filteractiondecrypt.cpp + ${filter_common_SRCS} +) + add_mailcommon_filter_test(filteractionrewriteheadertest filteractionrewriteheadertest.cpp ../filteractions/filteractionrewriteheader.cpp diff --git a/src/filter/autotests/filteractiondecrypttest.h b/src/filter/autotests/filteractiondecrypttest.h new file mode 100644 --- /dev/null +++ b/src/filter/autotests/filteractiondecrypttest.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * 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 + +#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 + diff --git a/src/filter/autotests/filteractiondecrypttest.cpp b/src/filter/autotests/filteractiondecrypttest.cpp new file mode 100644 --- /dev/null +++ b/src/filter/autotests/filteractiondecrypttest.cpp @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * 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 + +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("content"); + QTest::addColumn("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(); + 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) diff --git a/src/filter/autotests/filteractionencrypttest.h b/src/filter/autotests/filteractionencrypttest.h new file mode 100644 --- /dev/null +++ b/src/filter/autotests/filteractionencrypttest.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * 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 +#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 diff --git a/src/filter/autotests/filteractionencrypttest.cpp b/src/filter/autotests/filteractionencrypttest.cpp new file mode 100644 --- /dev/null +++ b/src/filter/autotests/filteractionencrypttest.cpp @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * 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 +#include + +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("key"); + QTest::addColumn("content"); + + const auto smimeKey = QStringLiteral("SMIME:0FDD972BCEFB5735DC7E8EE57DB7BA4E5FDBE218"); + const auto pgpKey = QStringLiteral("PGP: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 << plainData; + QTest::newRow(QStringLiteral("SMIME %1").arg(test.baseName()).toUtf8().constData()) + << smimeKey << plainData; + } +} + +void FilterActionEncryptTest::shouldEncrypt() +{ + QFETCH(QString, key); + QFETCH(QByteArray, content); + + MailCommon::FilterActionEncrypt action(this); + action.argsFromString(key); + QVERIFY(!action.key().isNull()); + + 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(); + 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; + if (key.startsWith(QLatin1String("PGP"))) { + QCOMPARE(newMsg->contentType()->mimeType(), QByteArray("multipart/encrypted")); + resultContent = newMsg->encodedContent(); + } else { + QCOMPARE(newMsg->contentType()->mimeType(), QByteArray("application/pkcs7-mime")); + resultContent = QByteArray::fromBase64(newMsg->encodedBody()); + } + + const auto crypto = key.startsWith(QLatin1String("PGP")) ? GPGHelper::OpenPGP + : GPGHelper::SMIME; + const auto actual = mGpg->decrypt(resultContent, crypto); + + KMime::Message actualContent; + actualContent.setContent(actual); + actualContent.parse(); + QCOMPARE(actualContent.from()->asUnicodeString(), msg->from()->asUnicodeString()); + QCOMPARE(actualContent.to()->asUnicodeString(), msg->to()->asUnicodeString()); + QCOMPARE(actualContent.date()->asUnicodeString(), msg->date()->asUnicodeString()); + QCOMPARE(actualContent.subject()->asUnicodeString(), msg->subject()->asUnicodeString()); + QCOMPARE(actualContent.contentType()->asUnicodeString(), msg->contentType()->asUnicodeString()); + QCOMPARE(actualContent.encodedBody(), msg->encodedBody()); +} + +QTEST_MAIN(FilterActionEncryptTest) diff --git a/src/filter/autotests/gpgdata/multipart-alternative.msg b/src/filter/autotests/gpgdata/multipart-alternative.msg new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpgdata/multipart-alternative.msg @@ -0,0 +1,18 @@ +From: Daniel Vratil +To: KMail Test +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 + +Wubba-lubba-dub-dub! + +--nextPart0-- diff --git a/src/filter/autotests/gpgdata/multipart-alternative.msg.pgp b/src/filter/autotests/gpgdata/multipart-alternative.msg.pgp new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpgdata/multipart-alternative.msg.pgp @@ -0,0 +1,34 @@ +From: Daniel Vratil +To: KMail Test +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-- diff --git a/src/filter/autotests/gpgdata/multipart-alternative.msg.smime b/src/filter/autotests/gpgdata/multipart-alternative.msg.smime new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpgdata/multipart-alternative.msg.smime @@ -0,0 +1,23 @@ +From: Daniel Vratil +To: KMail Test +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 diff --git a/src/filter/autotests/gpgdata/text-plain.msg b/src/filter/autotests/gpgdata/text-plain.msg new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpgdata/text-plain.msg @@ -0,0 +1,8 @@ +From: Daniel Vratil +To: KMail Test +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! diff --git a/src/filter/autotests/gpgdata/text-plain.msg.pgp b/src/filter/autotests/gpgdata/text-plain.msg.pgp new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpgdata/text-plain.msg.pgp @@ -0,0 +1,32 @@ +From: Daniel Vratil +To: KMail Test +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-- diff --git a/src/filter/autotests/gpgdata/text-plain.msg.smime b/src/filter/autotests/gpgdata/text-plain.msg.smime new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpgdata/text-plain.msg.smime @@ -0,0 +1,21 @@ +From: Daniel Vratil +To: KMail Test +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 + diff --git a/src/filter/autotests/gpghelper.h b/src/filter/autotests/gpghelper.h new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpghelper.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * 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 +#include + +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; + +private: + QByteArray runGpg(const QByteArray &in, CryptoType crypt, const QStringList &args) const; + + bool mValid; + QTemporaryDir mTmpDir; +}; + +#endif + diff --git a/src/filter/autotests/gpghelper.cpp b/src/filter/autotests/gpghelper.cpp new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpghelper.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * 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 +#include +#include +#include + +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") }); +} diff --git a/src/filter/autotests/gpghome/.gpg-v21-migrated b/src/filter/autotests/gpghome/.gpg-v21-migrated new file mode 100644 diff --git a/src/filter/autotests/gpghome/openpgp-revocs.d/818AE8DA30F81B0CEA4403BA358732559B8659B2.rev b/src/filter/autotests/gpghome/openpgp-revocs.d/818AE8DA30F81B0CEA4403BA358732559B8659B2.rev new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpghome/openpgp-revocs.d/818AE8DA30F81B0CEA4403BA358732559B8659B2.rev @@ -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 + +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----- diff --git a/src/filter/autotests/gpghome/private-keys-v1.d/1AB79D3867F2789661A1FF16675F5778804FD2AD.key b/src/filter/autotests/gpghome/private-keys-v1.d/1AB79D3867F2789661A1FF16675F5778804FD2AD.key new file mode 100644 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@ + * + * 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 "filteraction.h" + + +namespace MailCommon { + +class FilterActionDecrypt : public FilterAction +{ + 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 diff --git a/src/filter/filteractions/filteractiondecrypt.cpp b/src/filter/filteractions/filteractiondecrypt.cpp new file mode 100644 --- /dev/null +++ b/src/filter/filteractions/filteractiondecrypt.cpp @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * 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 + +#include + +#include +#include +#include + +#include + +using namespace MailCommon; + +namespace { + +inline bool isPGP(KMime::Headers::ContentType *ct, bool allowOctetStream = false) +{ + return ct->isSubtype("pgp-encrypted") || ct->isSubtype("encrypted") + || (allowOctetStream && ct->isMimeType("application/octet-stream")); +} + +inline bool isSMIME(KMime::Headers::ContentType *ct) +{ + return ct->isSubtype("pkcs7-mime") || ct->isSubtype("x-pkcs7-mime"); +} + +} // namespace + + +FilterActionDecrypt::FilterActionDecrypt(QObject *parent) + : FilterAction(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()) { + return ErrorNeedComplete; + } + + auto msg = item.payload(); + if (!KMime::isEncrypted(msg.data())) { + return GoOn; + } + + QGpgME::Protocol *proto = {}; + if (msg->mainBodyPart("multipart/encrypted")) { + const auto subparts = msg->contents(); + for (auto subpart : subparts) { + if (auto subct = subpart->contentType(false)) { + if (isPGP(subct, true)) { + proto = QGpgME::openpgp(); + break; + } else if (isSMIME(subct)) { + proto = QGpgME::smime(); + break; + } + } + } + } else if (auto ct = msg->contentType(false)) { + if (isPGP(ct)) { + proto = QGpgME::openpgp(); + } else if (isSMIME(ct)) { + proto = QGpgME::smime(); + } + } + + if (!proto) { + return GoOn; // no encryption in this message + } + + 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()) { + qCWarning(MAILCOMMON_LOG) << "Decryption error:" << result.error().asString(); + return ErrorButGoOn; + } + + KMime::Content decCt; + decCt.setContent(outData); + decCt.parse(); + decCt.assemble(); + + auto nec = KMime::Message::Ptr::create(); + nec->setHead(msg->head()); + nec->parse(); + auto ct = static_cast(nec->headerByType("Content-Type")); + ct->from7BitString(decCt.headerByType("Content-Type")->as7BitString(false)); + nec->setBody(decCt.encodedBody()); + nec->assemble(); + context.item().setPayload(nec); + context.setNeedsPayloadStore(); + return GoOn; +} diff --git a/src/filter/filteractions/filteractiondict.cpp b/src/filter/filteractions/filteractiondict.cpp --- a/src/filter/filteractions/filteractiondict.cpp +++ b/src/filter/filteractions/filteractiondict.cpp @@ -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 @@ insert(FilterActionAddToAddressBook::newAction); insert(FilterActionDelete::newAction); insert(FilterActionUnsetStatus::newAction); + insert(FilterActionEncrypt::newAction); + insert(FilterActionDecrypt::newAction); // Register custom filter actions below this line. } diff --git a/src/filter/filteractions/filteractionencrypt.h b/src/filter/filteractions/filteractionencrypt.h new file mode 100644 --- /dev/null +++ b/src/filter/filteractions/filteractionencrypt.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * 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 "filteraction.h" + +#include + +namespace MailCommon { + +class FilterActionEncrypt : public FilterAction +{ + 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; } +private: + GpgME::Key mKey; +}; + +} // namespace MailCommon + +#endif diff --git a/src/filter/filteractions/filteractionencrypt.cpp b/src/filter/filteractions/filteractionencrypt.cpp new file mode 100644 --- /dev/null +++ b/src/filter/filteractions/filteractionencrypt.cpp @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * 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 + +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +using namespace MailCommon; + +#define LISTING_FINISHED "listingFinished" + +FilterActionEncrypt::FilterActionEncrypt(QObject *parent) + : FilterAction(QStringLiteral("encrypt"), i18n("Encrypt"), parent) +{ +} + +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 proto + 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; + } + + const auto fp = argsStr.mid(pos + 1); + auto listJob = proto->keyListJob(false, true, true); + + std::vector 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]; + qCDebug(MAILCOMMON_LOG) << "Using key" << mKey.primaryFingerprint(); +} + +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()) { + qCWarning(MAILCOMMON_LOG) << "Item" << item.id() << "does not contain KMime::Message payload!"; + return ErrorNeedComplete; + } + + auto msg = item.payload(); + + 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 = KMime::Message::Ptr::create(); + nec->setHead(msg->head()); + nec->parse(); + auto ct = static_cast(nec->headerByType("Content-Type")); + ct->from7BitString(result->headerByType("Content-Type")->as7BitString(false)); + nec->setBody(result->encodedBody()); + nec->assemble(); + context.item().setPayload(nec); + context.setNeedsPayloadStore(); + + 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 combo = new Kleo::KeySelectionCombo(parent); + combo->setDefaultKey(QString::fromLatin1(mKey.primaryFingerprint())); + + std::shared_ptr 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); + + return combo; +} + +void FilterActionEncrypt::setParamWidgetValue(QWidget *paramWidget) const +{ + if (auto combo = qobject_cast(paramWidget)) { + combo->setDefaultKey(QString::fromLatin1(mKey.primaryFingerprint())); + combo->setCurrentKey(QString::fromLatin1(mKey.primaryFingerprint())); + } +} + +void FilterActionEncrypt::applyParamWidgetValue(QWidget *paramWidget) +{ + if (auto combo = qobject_cast(paramWidget)) { + // 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(); + } +}