File indexing completed on 2024-11-24 04:53:30

0001 /* Copyright (C) 2014-2015 Stephan Platz <trojita@paalsteek.de>
0002 
0003    This file is part of the Trojita Qt IMAP e-mail client,
0004    http://trojita.flaska.net/
0005 
0006    This program is free software; you can redistribute it and/or
0007    modify it under the terms of the GNU General Public License as
0008    published by the Free Software Foundation; either version 2 of
0009    the License or (at your option) version 3 or any later version
0010    accepted by the membership of KDE e.V. (or its successor approved
0011    by the membership of KDE e.V.), which shall act as a proxy
0012    defined in Section 14 of version 3 of the license.
0013 
0014    This program is distributed in the hope that it will be useful,
0015    but WITHOUT ANY WARRANTY; without even the implied warranty of
0016    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0017    GNU General Public License for more details.
0018 
0019    You should have received a copy of the GNU General Public License
0020    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0021 */
0022 
0023 #include <QtTest/QtTest>
0024 
0025 #include "test_Cryptography_PGP.h"
0026 #include "configure.cmake.h"
0027 #include "crypto_test_data.h"
0028 #include "Cryptography/MessageModel.h"
0029 #include "Imap/data.h"
0030 #include "Imap/Model/ItemRoles.h"
0031 #include "Streams/FakeSocket.h"
0032 
0033 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
0034 #  ifdef TROJITA_HAVE_GPGMEPP
0035 #    include "Cryptography/GpgMe++.h"
0036 #  endif
0037 #endif
0038 
0039 /* TODO: test cases:
0040  *   * decrypt with not yet valid key
0041  *   * verify signature of modified text
0042  *   * verify signature with expired key
0043  *   * verify signature with not yet valid key
0044  *   * verify signature with unknown key
0045  *   * nested scenarios
0046  */
0047 
0048 void CryptographyPGPTest::initTestCase()
0049 {
0050     LibMailboxSync::initTestCase();
0051     if (!qputenv("GNUPGHOME", QByteArray(QCoreApplication::applicationDirPath().toUtf8() + "/keys").constData())) {
0052         QFAIL("Unable to set GNUPGHOME environment variable");
0053     }
0054 }
0055 
0056 void CryptographyPGPTest::testDecryption()
0057 {
0058     QFETCH(QByteArray, bodystructure);
0059     QFETCH(QByteArray, cyphertext);
0060     QFETCH(QByteArray, plaintext);
0061     QFETCH(QString, from);
0062     QFETCH(bool, successful);
0063 
0064     // By default, there's a 50ms delay between the time we request a part download and the time it actually happens.
0065     // That's too long for a unit test.
0066     model->setProperty("trojita-imap-delayed-fetch-part", 0);
0067 
0068     helperSyncBNoMessages();
0069     cServer("* 1 EXISTS\r\n");
0070     cClient(t.mk("UID FETCH 1:* (FLAGS)\r\n"));
0071     cServer("* 1 FETCH (UID 333 FLAGS ())\r\n" + t.last("OK fetched\r\n"));
0072 
0073     QCOMPARE(model->rowCount(msgListB), 1);
0074     QModelIndex msg = msgListB.model()->index(0, 0, msgListB);
0075     QVERIFY(msg.isValid());
0076     QCOMPARE(model->rowCount(msg), 0);
0077     cClient(t.mk("UID FETCH 333 (" FETCH_METADATA_ITEMS ")\r\n"));
0078     cServer(helperCreateTrivialEnvelope(1, 333, QStringLiteral("subj"), from, bodystructure)
0079             + t.last("OK fetched\r\n"));
0080     cEmpty();
0081     QVERIFY(model->rowCount(msg) > 0);
0082     Cryptography::MessageModel msgModel(0, msg);
0083 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
0084 #  ifdef TROJITA_HAVE_GPGMEPP
0085     msgModel.registerPartHandler(std::make_shared<Cryptography::GpgMeReplacer>());
0086 #  endif
0087 #endif
0088     QModelIndex mappedMsg = msgModel.index(0,0);
0089     QVERIFY(mappedMsg.isValid());
0090     QVERIFY(msgModel.rowCount(mappedMsg) > 0);
0091 
0092     QModelIndex data = mappedMsg.model()->index(0, 0, mappedMsg);
0093     QVERIFY(data.isValid());
0094 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
0095     QCOMPARE(msgModel.rowCount(data), 0);
0096     QCOMPARE(data.data(Imap::Mailbox::RoleIsFetched).toBool(), false);
0097 
0098     cClientRegExp(t.mk("UID FETCH 333 \\((BODY\\.PEEK\\[2\\] BODY\\.PEEK\\[1\\]|BODY.PEEK\\[1\\] BODY\\.PEEK\\[2\\])\\)"));
0099     cServer("* 1 FETCH (UID 333 BODY[2] " + asLiteral(cyphertext) + " BODY[1] " + asLiteral("Version: 1\r\n") + ")\r\n"
0100             + t.last("OK fetched"));
0101 
0102     QSignalSpy qcaSuccessSpy(&msgModel, SIGNAL(rowsInserted(const QModelIndex &,int,int)));
0103     QSignalSpy qcaErrorSpy(&msgModel, SIGNAL(error(const QModelIndex &,QString,QString)));
0104 
0105     int i = 0;
0106     while (data.isValid() && data.data(Imap::Mailbox::RolePartCryptoNotFinishedYet).toBool() && i++ < 1000) {
0107         QTest::qWait(10);
0108     }
0109     // allow for event processing, so that the model can retrieve the results
0110     QCoreApplication::processEvents();
0111     QVERIFY(!data.data(Imap::Mailbox::RolePartCryptoNotFinishedYet).toBool());
0112 
0113     if (!qcaErrorSpy.isEmpty() && successful) {
0114         qDebug() << "Unexpected failure in crypto";
0115         for (int i = 0; i < qcaErrorSpy.size(); ++i) {
0116             qDebug() << qcaErrorSpy[i][1].toString();
0117             qDebug() << qcaErrorSpy[i][2].toString();
0118         }
0119     }
0120 
0121     if (successful) {
0122         QCOMPARE(qcaErrorSpy.empty(), successful);
0123         QCOMPARE(qcaSuccessSpy.empty(), !successful);
0124     }
0125 
0126     QVERIFY(data.data(Imap::Mailbox::RoleIsFetched).toBool());
0127 
0128     cEmpty();
0129     QVERIFY(errorSpy->empty());
0130 #else
0131     QCOMPARE(msgModel.rowCount(data), 2);
0132     QCOMPARE(data.data(Imap::Mailbox::RoleIsFetched).toBool(), true);
0133 
0134     QCOMPARE(data.child(0, 0).data(Imap::Mailbox::RolePartMimeType).toString(), QLatin1String("application/pgp-encrypted"));
0135     QCOMPARE(data.child(1, 0).data(Imap::Mailbox::RolePartMimeType).toString(), QLatin1String("application/octet-stream"));
0136     cEmpty();
0137 
0138     QSKIP("Some tests were skipped because this build doesn't have GpgME++ support");
0139 #endif
0140 }
0141 
0142 void CryptographyPGPTest::testDecryption_data()
0143 {
0144     QTest::addColumn<QByteArray>("bodystructure");
0145     QTest::addColumn<QByteArray>("cyphertext");
0146     QTest::addColumn<QByteArray>("plaintext");
0147     QTest::addColumn<QString>("from");
0148     QTest::addColumn<bool>("successful");
0149 
0150     // everything is correct
0151     QTest::newRow("valid")
0152             << bsEncrypted
0153             << encValid
0154             << QByteArray("plaintext")
0155             << QStringLiteral("valid@test.trojita.flaska.net")
0156             << true;
0157 
0158     // corrupted data
0159     QTest::newRow("invalid")
0160             << bsEncrypted
0161             << encInvalid
0162             << QByteArray("plaintext")
0163             << QStringLiteral("valid@test.trojita.flaska.net")
0164             << false;
0165 
0166     // the key used for encryption is expired
0167     QTest::newRow("expiredKey")
0168             << bsEncrypted
0169             << encExpired
0170             << QByteArray("plaintext")
0171                // NOTE (jkt): This is how my QCA/2.1.0.3, GnuPG/2.0.28 behaves.
0172             << QStringLiteral("valid@test.trojita.flaska.net")
0173             << true;
0174 
0175     // we don't have any key which is needed for encryption
0176     QTest::newRow("unknownKey")
0177             << bsEncrypted
0178             << encUnknown
0179             << QByteArray("plaintext")
0180             << QStringLiteral("valid@test.trojita.flaska.net")
0181             << false;
0182 }
0183 
0184 /** @short What happens when ENVELOPE doesn't arrive at the time that parts are already there? */
0185 void CryptographyPGPTest::testDecryptWithoutEnvelope()
0186 {
0187 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
0188     model->setProperty("trojita-imap-delayed-fetch-part", 0);
0189 
0190     helperSyncBNoMessages();
0191     cServer("* 1 EXISTS\r\n");
0192     cClient(t.mk("UID FETCH 1:* (FLAGS)\r\n"));
0193     cServer("* 1 FETCH (UID 333 FLAGS ())\r\n" + t.last("OK fetched\r\n"));
0194     QCOMPARE(model->rowCount(msgListB), 1);
0195     QModelIndex msg = msgListB.model()->index(0, 0, msgListB);
0196     QVERIFY(msg.isValid());
0197     Cryptography::MessageModel msgModel(0, msg);
0198     msgModel.registerPartHandler(std::make_shared<Cryptography::GpgMeReplacer>());
0199 
0200     QCOMPARE(model->rowCount(msg), 0);
0201     cClient(t.mk("UID FETCH 333 (" FETCH_METADATA_ITEMS ")\r\n"));
0202     cServer("* 1 FETCH (UID 333 BODYSTRUCTURE (" + bsEncrypted + "))\r\n" + t.last("OK fetched\r\n"));
0203     // notice that the ENVELOPE never arrived
0204     cEmpty();
0205     QVERIFY(model->rowCount(msg) > 0);
0206     QModelIndex mappedMsg = msgModel.index(0,0);
0207     QVERIFY(mappedMsg.isValid());
0208     QVERIFY(msgModel.rowCount(mappedMsg) > 0);
0209 
0210     QModelIndex data = mappedMsg.model()->index(0, 0, mappedMsg);
0211     QVERIFY(data.isValid());
0212     QCOMPARE(msgModel.rowCount(data), 0);
0213     QCOMPARE(data.data(Imap::Mailbox::RoleIsFetched).toBool(), false);
0214 
0215     cClientRegExp(t.mk("UID FETCH 333 \\((BODY\\.PEEK\\[2\\] BODY\\.PEEK\\[1\\]|BODY.PEEK\\[1\\] BODY\\.PEEK\\[2\\])\\)"));
0216     cServer("* 1 FETCH (UID 333 BODY[2] " + asLiteral(encValid) + " BODY[1] " + asLiteral("Version: 1\r\n") + ")\r\n"
0217             + t.last("OK fetched"));
0218 
0219     QSignalSpy qcaSuccessSpy(&msgModel, SIGNAL(rowsInserted(const QModelIndex &,int,int)));
0220     QSignalSpy qcaErrorSpy(&msgModel, SIGNAL(error(const QModelIndex &,QString,QString)));
0221 
0222     int i = 0;
0223     while (data.isValid() && data.data(Imap::Mailbox::RolePartCryptoNotFinishedYet).toBool() && i++ < 1000) {
0224         QTest::qWait(10);
0225     }
0226     // allow for event processing, so that the model can retrieve the results
0227     QCoreApplication::processEvents();
0228     QVERIFY(!data.data(Imap::Mailbox::RolePartCryptoNotFinishedYet).toBool());
0229 
0230     QVERIFY(qcaSuccessSpy.isEmpty());
0231     QVERIFY(qcaErrorSpy.isEmpty());
0232 
0233     QVERIFY(!data.data(Imap::Mailbox::RoleIsFetched).toBool()); // because the ENVELOPE hasn't arrived yet
0234 
0235     cEmpty();
0236     QVERIFY(errorSpy->empty());
0237 #else
0238     QSKIP("Cannot test without GpgME++ support");
0239 #endif
0240 }
0241 
0242 void CryptographyPGPTest::testVerification()
0243 {
0244     QFETCH(QByteArray, signature);
0245     QFETCH(QByteArray, ptMimeHdr);
0246     QFETCH(QByteArray, plaintext);
0247     QFETCH(bool, successful);
0248     QFETCH(QString, from);
0249     QFETCH(QString, tldr);
0250     QFETCH(QString, longDesc);
0251     QFETCH(bool, validDisregardingTrust);
0252     QFETCH(bool, validCompletely);
0253 
0254     model->setProperty("trojita-imap-delayed-fetch-part", 0);
0255     helperSyncBNoMessages();
0256     cServer("* 1 EXISTS\r\n");
0257     cClient(t.mk("UID FETCH 1:* (FLAGS)\r\n"));
0258     cServer("* 1 FETCH (UID 333 FLAGS ())\r\n" + t.last("OK fetched\r\n"));
0259     QCOMPARE(model->rowCount(msgListB), 1);
0260     QModelIndex msg = msgListB.model()->index(0, 0, msgListB);
0261     QVERIFY(msg.isValid());
0262     QCOMPARE(model->rowCount(msg), 0);
0263     cClient(t.mk("UID FETCH 333 (" FETCH_METADATA_ITEMS ")\r\n"));
0264     cServer(helperCreateTrivialEnvelope(1, 333, QStringLiteral("subj"), from, QStringLiteral(
0265             "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 423 14 NIL NIL NIL NIL)"
0266             "(\"application\" \"pgp-signature\" NIL NIL NIL \"7bit\" 851 NIL NIL NIL NIL)"
0267             " \"signed\" (\"boundary\" \"=-=-=\" \"micalg\" \"pgp-sha256\" \"protocol\" \"application/pgp-signature\")"
0268             " NIL NIL NIL"))
0269             + t.last("OK fetched\r\n"));
0270     cEmpty();
0271     QVERIFY(model->rowCount(msg) > 0);
0272     Cryptography::MessageModel msgModel(0, msg);
0273 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
0274 #  ifdef TROJITA_HAVE_GPGMEPP
0275     msgModel.registerPartHandler(std::make_shared<Cryptography::GpgMeReplacer>());
0276 #  endif
0277 #endif
0278     QModelIndex mappedMsg = msgModel.index(0,0);
0279     QVERIFY(mappedMsg.isValid());
0280     QVERIFY(msgModel.rowCount(mappedMsg) > 0);
0281 
0282     QModelIndex data = mappedMsg.model()->index(0, 0, mappedMsg);
0283     QVERIFY(data.isValid());
0284 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
0285     QCOMPARE(msgModel.rowCount(data), 0);
0286     QCOMPARE(data.data(Imap::Mailbox::RoleIsFetched).toBool(), false);
0287 
0288     cClientRegExp(t.mk("UID FETCH 333 \\((BODY\\.PEEK\\[(2|1|1\\.MIME)\\] ?){3}\\)"));
0289     cServer("* 1 FETCH (UID 333 BODY[2] " + asLiteral(signature) + " BODY[1] " + asLiteral(plaintext)
0290             + " BODY[1.MIME] " + asLiteral(ptMimeHdr) + ")\r\n"
0291             + t.last("OK fetched"));
0292 
0293     QSignalSpy qcaErrorSpy(&msgModel, SIGNAL(error(const QModelIndex &,QString,QString)));
0294 
0295     int i = 0;
0296     while (data.isValid() && data.data(Imap::Mailbox::RolePartCryptoNotFinishedYet).toBool() && qcaErrorSpy.empty() && i++ < 1000) {
0297         QTest::qWait(10);
0298     }
0299     // allow for event processing, so that the model can retrieve the results
0300     QCoreApplication::processEvents();
0301     QVERIFY(!data.data(Imap::Mailbox::RolePartCryptoNotFinishedYet).toBool());
0302 
0303     if (!qcaErrorSpy.isEmpty() && successful) {
0304         qDebug() << "Unexpected failure in crypto";
0305         for (int i = 0; i < qcaErrorSpy.size(); ++i) {
0306             qDebug() << qcaErrorSpy[i][1].toString();
0307             qDebug() << qcaErrorSpy[i][2].toString();
0308         }
0309     }
0310 
0311     QCOMPARE(qcaErrorSpy.empty(), successful);
0312     QCOMPARE(data.data(Imap::Mailbox::RolePartCryptoTLDR).toString(), tldr);
0313     auto actualLongDesc = data.data(Imap::Mailbox::RolePartCryptoDetailedMessage).toString();
0314     if (!actualLongDesc.startsWith(longDesc)) {
0315         QCOMPARE(actualLongDesc, longDesc); // let's reuse this for debug output, and don't be scared about the misleading implications
0316     }
0317 
0318     QCOMPARE(data.data(Imap::Mailbox::RolePartSignatureVerifySupported).toBool(), successful);
0319     QCOMPARE(data.data(Imap::Mailbox::RolePartSignatureValidDisregardingTrust).toBool(), validDisregardingTrust);
0320     QCOMPARE(data.data(Imap::Mailbox::RolePartSignatureValidTrusted).toBool(), validCompletely);
0321     QCOMPARE(data.data(Imap::Mailbox::RolePartMimeType).toByteArray(), QByteArray("multipart/signed"));
0322     QVERIFY(data.data(Imap::Mailbox::RoleIsFetched).toBool() == successful);
0323 
0324     auto partIdx = data.model()->index(0, 0, data);
0325     QCOMPARE(partIdx.data(Imap::Mailbox::RolePartMimeType).toByteArray(), QByteArray("text/plain"));
0326     QCOMPARE(partIdx.data(Imap::Mailbox::RolePartUnicodeText).toString(), QString::fromUtf8(plaintext));
0327 
0328     cEmpty();
0329     QVERIFY(errorSpy->empty());
0330 #else
0331     QCOMPARE(msgModel.rowCount(data), 2);
0332     QCOMPARE(data.data(Imap::Mailbox::RoleIsFetched).toBool(), true);
0333 
0334     QCOMPARE(data.child(0, 0).data(Imap::Mailbox::RolePartMimeType).toString(), QLatin1String("application/pgp-encrypted"));
0335     QCOMPARE(data.child(1, 0).data(Imap::Mailbox::RolePartMimeType).toString(), QLatin1String("application/octet-stream"));
0336     cEmpty();
0337 
0338     QSKIP("Some tests were skipped because this build doesn't have GpgME++ support");
0339 #endif
0340 }
0341 
0342 void CryptographyPGPTest::testVerification_data()
0343 {
0344     QTest::addColumn<QByteArray>("signature");
0345     QTest::addColumn<QByteArray>("ptMimeHdr");
0346     QTest::addColumn<QByteArray>("plaintext");
0347     QTest::addColumn<bool>("successful");
0348     QTest::addColumn<QString>("from");
0349     QTest::addColumn<QString>("tldr");
0350     QTest::addColumn<QString>("longDesc");
0351     QTest::addColumn<bool>("validDisregardingTrust");
0352     QTest::addColumn<bool>("validCompletely");
0353 
0354     QByteArray ptMimeHdr = QByteArrayLiteral("Content-Type: text/plain\r\n\r\n");
0355 
0356     // everything is correct
0357     QTest::newRow("valid-me")
0358             << sigFromMe
0359             << ptMimeHdr
0360             << QByteArray("plaintext\r\n")
0361             << true
0362             << QStringLiteral("valid@test.trojita.flaska.net")
0363             << QStringLiteral("Verified signature")
0364             << QStringLiteral("Verified signature from Valid <valid@test.trojita.flaska.net> (")
0365             << true
0366             << true;
0367 
0368     // my signature, but a different identity
0369     QTest::newRow("my-signature-different-identity")
0370             << sigFromMe
0371             << ptMimeHdr
0372             << QByteArray("plaintext\r\n")
0373             << true
0374             << QStringLiteral("evil@example.org")
0375             << QStringLiteral("Signed by stranger")
0376             << QStringLiteral("Verified signature, but the signer is someone else:\nValid <valid@test.trojita.flaska.net> (")
0377             << true
0378             << false;
0379 
0380     // my signature, different data
0381     QTest::newRow("my-signature-different-data")
0382             << sigFromMe
0383             << ptMimeHdr
0384             << QByteArray("I will pay you right now\r\n")
0385             << true
0386             << QStringLiteral("valid@test.trojita.flaska.net")
0387             << QStringLiteral("Bad signature")
0388             << QStringLiteral("Bad signature by Valid <valid@test.trojita.flaska.net> (")
0389             << false
0390             << false;
0391 
0392     // A missing Content-Type header (and also an invalid signature)
0393     QTest::newRow("invalid-implicit-content-type")
0394             << sigFromMe
0395             << QByteArrayLiteral("Content-Transfer-Encoding: 8bit\r\n\r\n")
0396             << QByteArray("plaintext\r\n")
0397             << true
0398             << QStringLiteral("valid@test.trojita.flaska.net")
0399             << QStringLiteral("Bad signature")
0400             << QStringLiteral("Bad signature by Valid <valid@test.trojita.flaska.net> (")
0401             << false
0402             << false;
0403 }
0404 
0405 void CryptographyPGPTest::testMalformed()
0406 {
0407     QFETCH(QByteArray, bodystructure);
0408     QFETCH(int, rowCount);
0409     QFETCH(QString, tldr);
0410     QFETCH(QString, detail);
0411 
0412     model->setProperty("trojita-imap-delayed-fetch-part", 0);
0413     helperSyncBNoMessages();
0414     cServer("* 1 EXISTS\r\n");
0415     cClient(t.mk("UID FETCH 1:* (FLAGS)\r\n"));
0416     cServer("* 1 FETCH (UID 333 FLAGS ())\r\n" + t.last("OK fetched\r\n"));
0417     QCOMPARE(model->rowCount(msgListB), 1);
0418     QModelIndex msg = msgListB.model()->index(0, 0, msgListB);
0419     QVERIFY(msg.isValid());
0420     QCOMPARE(model->rowCount(msg), 0);
0421     cClient(t.mk("UID FETCH 333 (" FETCH_METADATA_ITEMS ")\r\n"));
0422     cServer(helperCreateTrivialEnvelope(1, 333, QStringLiteral("subj"), QStringLiteral("foo@example.org"), QString::fromUtf8(bodystructure))
0423             + t.last("OK fetched\r\n"));
0424     cEmpty();
0425     QVERIFY(model->rowCount(msg) > 0);
0426     Cryptography::MessageModel msgModel(0, msg);
0427 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
0428 #  ifdef TROJITA_HAVE_GPGMEPP
0429     msgModel.registerPartHandler(std::make_shared<Cryptography::GpgMeReplacer>());
0430 #  endif
0431 #endif
0432     QModelIndex mappedMsg = msgModel.index(0,0);
0433     QVERIFY(mappedMsg.isValid());
0434     QVERIFY(msgModel.rowCount(mappedMsg) > 0);
0435 
0436     QModelIndex data = mappedMsg.model()->index(0, 0, mappedMsg);
0437     QVERIFY(data.isValid());
0438 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
0439     QCoreApplication::processEvents();
0440     QCOMPARE(msgModel.rowCount(data), rowCount);
0441     QCOMPARE(data.data(Imap::Mailbox::RolePartCryptoTLDR).toString(), tldr);
0442     QCOMPARE(data.data(Imap::Mailbox::RolePartCryptoDetailedMessage).toString(), detail);
0443 
0444     cEmpty();
0445     QVERIFY(errorSpy->empty());
0446 #else
0447     QCOMPARE(msgModel.rowCount(data), rowCount);
0448     QCOMPARE(data.data(Imap::Mailbox::RoleIsFetched).toBool(), true);
0449 
0450     cEmpty();
0451 
0452     QSKIP("Some tests were skipped because this build doesn't have GpgME++ support");
0453 #endif
0454 }
0455 
0456 void CryptographyPGPTest::testMalformed_data()
0457 {
0458     QTest::addColumn<QByteArray>("bodystructure");
0459     QTest::addColumn<int>("rowCount");
0460     QTest::addColumn<QString>("tldr");
0461     QTest::addColumn<QString>("detail");
0462 
0463     // Due to the missing "protocol", the part replacer won't even react
0464     QTest::newRow("signed-missing-protocol-micalg")
0465             << bsMultipartSignedTextPlain
0466             << 2
0467             << QString()
0468             << QString();
0469 
0470     // A mailing list has stripped the signature
0471     QTest::newRow("signed-ml-stripped-gpg-signature")
0472             << QByteArray("(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 423 14 NIL NIL NIL NIL)"
0473                           " \"signed\" (\"boundary\" \"=-=-=\" \"micalg\" \"pgp-sha256\" \"protocol\" \"application/pgp-signature\")"
0474                           " NIL NIL NIL")
0475             << 1
0476             << QStringLiteral("Malformed Signed Message")
0477             << QStringLiteral("Expected 2 parts, but found 1.");
0478 
0479 }
0480 
0481 /** @short Check operation when some data are not available */
0482 void CryptographyPGPTest::testOffline()
0483 {
0484     QFETCH(QByteArray, bodystructure);
0485     QFETCH(QByteArray, fetchRegex);
0486 
0487     model->setProperty("trojita-imap-delayed-fetch-part", 0);
0488     helperSyncBNoMessages();
0489     cServer("* 1 EXISTS\r\n");
0490     cClient(t.mk("UID FETCH 1:* (FLAGS)\r\n"));
0491     cServer("* 1 FETCH (UID 333 FLAGS ())\r\n" + t.last("OK fetched\r\n"));
0492     QCOMPARE(model->rowCount(msgListB), 1);
0493     QModelIndex msg = msgListB.model()->index(0, 0, msgListB);
0494     QVERIFY(msg.isValid());
0495     QCOMPARE(model->rowCount(msg), 0);
0496     cClient(t.mk("UID FETCH 333 (" FETCH_METADATA_ITEMS ")\r\n"));
0497     cServer(helperCreateTrivialEnvelope(1, 333, QStringLiteral("subj"), QStringLiteral("foo@example.org"), bodystructure)
0498             + t.last("OK fetched\r\n"));
0499     cEmpty();
0500     QVERIFY(model->rowCount(msg) > 0);
0501     Cryptography::MessageModel msgModel(0, msg);
0502 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
0503 #  ifdef TROJITA_HAVE_GPGMEPP
0504     msgModel.registerPartHandler(std::make_shared<Cryptography::GpgMeReplacer>());
0505 #  endif
0506 #endif
0507     QModelIndex mappedMsg = msgModel.index(0,0);
0508     QVERIFY(mappedMsg.isValid());
0509     QVERIFY(msgModel.rowCount(mappedMsg) > 0);
0510 
0511     QModelIndex data = mappedMsg.model()->index(0, 0, mappedMsg);
0512     QVERIFY(data.isValid());
0513 #ifdef TROJITA_HAVE_CRYPTO_MESSAGES
0514     QCOMPARE(msgModel.rowCount(mappedMsg), 1);
0515     QCOMPARE(msgModel.rowCount(data), 0);
0516     QCOMPARE(data.data(Imap::Mailbox::RoleIsFetched).toBool(), false);
0517 
0518     cClientRegExp(t.mk(fetchRegex));
0519     auto fetchResp = t.last("NO offline\r\n");
0520     LibMailboxSync::setModelNetworkPolicy(model, Imap::Mailbox::NETWORK_OFFLINE);
0521     cClient(t.mk("LOGOUT\r\n"));
0522     cServer(fetchResp + t.last("OK logout\r\n"));
0523 
0524     QSignalSpy qcaErrorSpy(&msgModel, SIGNAL(error(const QModelIndex &,QString,QString)));
0525 
0526     int i = 0;
0527     while (data.isValid() && data.data(Imap::Mailbox::RolePartCryptoNotFinishedYet).toBool() && qcaErrorSpy.empty() && i++ < 1000) {
0528         QTest::qWait(10);
0529     }
0530     // allow for event processing, so that the model can retrieve the results
0531     QCoreApplication::processEvents();
0532     QVERIFY(!data.data(Imap::Mailbox::RolePartCryptoNotFinishedYet).toBool());
0533 
0534     QCOMPARE(data.data(Imap::Mailbox::RolePartCryptoTLDR), QVariant(QStringLiteral("Data Unavailable")));
0535     QCOMPARE(msgModel.rowCount(data), 2);
0536 
0537     if (!qcaErrorSpy.isEmpty()) {
0538         qDebug() << "Unexpected failure in crypto";
0539         for (int i = 0; i < qcaErrorSpy.size(); ++i) {
0540             qDebug() << qcaErrorSpy[i][1].toString();
0541             qDebug() << qcaErrorSpy[i][2].toString();
0542         }
0543     }
0544 
0545     // We're offline, we cannot call cEmpty(), that would assert-crash due to no active parsers
0546     //cEmpty();
0547 
0548     QVERIFY(errorSpy->empty());
0549 #else
0550     QCOMPARE(msgModel.rowCount(data), 2);
0551     QCOMPARE(data.data(Imap::Mailbox::RoleIsFetched).toBool(), true);
0552     cEmpty();
0553 
0554     QSKIP("Some tests were skipped because this build doesn't have GpgME++ support");
0555 #endif
0556 }
0557 
0558 void CryptographyPGPTest::testOffline_data()
0559 {
0560     QTest::addColumn<QByteArray>("bodystructure");
0561     QTest::addColumn<QByteArray>("fetchRegex");
0562 
0563     QTest::newRow("signed")
0564             << QByteArray("(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" 423 14 NIL NIL NIL NIL)"
0565             "(\"application\" \"pgp-signature\" NIL NIL NIL \"7bit\" 851 NIL NIL NIL NIL)"
0566             " \"signed\" (\"boundary\" \"=-=-=\" \"micalg\" \"pgp-sha256\" \"protocol\" \"application/pgp-signature\")"
0567             " NIL NIL NIL")
0568             << QByteArray("UID FETCH 333 \\((BODY\\.PEEK\\[(2|1|1\\.MIME)\\] ?){3}\\)");
0569 
0570     QTest::newRow("encrypted")
0571             << bsEncrypted
0572             << QByteArray("UID FETCH 333 \\((BODY\\.PEEK\\[(1|2)\\] ?){2}\\)");
0573 }
0574 
0575 QTEST_GUILESS_MAIN(CryptographyPGPTest)