Warning, file /pim/trojita/tests/Imap/test_Imap_BodyParts.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
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 <algorithm>
0024 #include <QtTest>
0025 #include "data.h"
0026 #include "test_Imap_BodyParts.h"
0027 #include "Utils/FakeCapabilitiesInjector.h"
0028 #include "Streams/FakeSocket.h"
0029 #include "Imap/Model/ItemRoles.h"
0030 #include "Imap/Model/MailboxTree.h"
0031 
0032 struct Data {
0033     QString key;
0034     QString partId;
0035     QByteArray text;
0036     QString mimeType;
0037 
0038     typedef enum {INVALID_INDEX, NO_FETCHING, REGULAR} ItemType;
0039     ItemType itemType;
0040 
0041     Data(const QString &key, const QString &partId, const QByteArray &text, const QString &mimeType=QString()):
0042         key(key), partId(partId), text(text), mimeType(mimeType), itemType(REGULAR)
0043     {
0044     }
0045 
0046     Data(const QString &key, const ItemType itemType=INVALID_INDEX, const QString &mimeType=QString()):
0047         key(key), mimeType(mimeType), itemType(itemType)
0048     {
0049     }
0050 };
0051 
0052 Q_DECLARE_METATYPE(QList<Data>)
0053 
0054 #if QT_VERSION < QT_VERSION_CHECK(5, 13, 0)
0055 namespace QTest {
0056 template <>
0057 char *toString(const QModelIndex &index)
0058 {
0059     QString buf;
0060     QDebug(&buf) << index;
0061     return qstrdup(buf.toUtf8().constData());
0062 }
0063 }
0064 #endif
0065 
0066 using namespace Imap::Mailbox;
0067 
0068 /** @short Check that the part numbering works properly */
0069 void BodyPartsTest::testPartIds()
0070 {
0071     QFETCH(QByteArray, bodystructure);
0072     QFETCH(QList<Data>, mapping);
0073 
0074     // By default, there's a 50ms delay between the time we request a part download and the time it actually happens.
0075     // That's too long for a unit test.
0076     model->setProperty("trojita-imap-delayed-fetch-part", 0);
0077 
0078     helperSyncBNoMessages();
0079     cServer("* 1 EXISTS\r\n");
0080     cClient(t.mk("UID FETCH 1:* (FLAGS)\r\n"));
0081     cServer("* 1 FETCH (UID 333 FLAGS ())\r\n" + t.last("OK fetched\r\n"));
0082 
0083     QCOMPARE(model->rowCount(msgListB), 1);
0084     QModelIndex msg = msgListB.model()->index(0, 0, msgListB);
0085     QVERIFY(msg.isValid());
0086     QCOMPARE(model->rowCount(msg), 0);
0087     cClient(t.mk("UID FETCH 333 (" FETCH_METADATA_ITEMS ")\r\n"));
0088     cServer("* 1 FETCH (UID 333 BODYSTRUCTURE (" + bodystructure + "))\r\n" + t.last("OK fetched\r\n"));
0089     QVERIFY(model->rowCount(msg) > 0);
0090 
0091     const QString wherePrefix = QString::number(idxB.row()) + QLatin1Char('.') +
0092             QString::number(msgListB.row()) + QLatin1Char('.') + QString::number(msg.row()) + QLatin1Char('.');
0093     QCOMPARE(findIndexByPosition(model, wherePrefix.left(wherePrefix.size() - 1)), msg);
0094 
0095     for (auto it = mapping.constBegin(); it != mapping.constEnd(); ++it) {
0096         QModelIndex idx = findIndexByPosition(model, wherePrefix + it->key);
0097         if (it->itemType == Data::INVALID_INDEX) {
0098             if (idx.isValid()) {
0099                 qDebug() << "Index " << it->key << " is valid";
0100                 QFAIL("Unexpected valid index");
0101             }
0102             continue;
0103         }
0104         QVERIFY(idx.isValid());
0105         QString partId = idx.data(Imap::Mailbox::RolePartId).toString();
0106         QCOMPARE(partId, it->partId);
0107 
0108         QCOMPARE(idx.data(Imap::Mailbox::RolePartData).toString(), QString());
0109 
0110         if (it->itemType == Data::REGULAR) {
0111             cClient(t.mk("UID FETCH 333 (BODY.PEEK[") + partId.toUtf8() + "])\r\n");
0112             cServer("* 1 FETCH (UID 333 BODY[" + partId.toUtf8() + "] " + asLiteral(it->text) + ")\r\n"
0113                     + t.last("OK fetched\r\n"));
0114             QCOMPARE(idx.data(Imap::Mailbox::RolePartData).toByteArray(), it->text);
0115         } else if (it->itemType == Data::NO_FETCHING) {
0116             cEmpty();
0117             QCOMPARE(idx.data(Imap::Mailbox::RolePartData).toByteArray(), QByteArray());
0118         }
0119 
0120         if (!it->mimeType.isNull()) {
0121             QCOMPARE(idx.data(Imap::Mailbox::RolePartMimeType).toString(), it->mimeType);
0122         }
0123     }
0124 
0125     cEmpty();
0126     QVERIFY(errorSpy->isEmpty());
0127 }
0128 
0129 
0130 void BodyPartsTest::testPartIds_data()
0131 {
0132     QTest::addColumn<QByteArray>("bodystructure");
0133     QTest::addColumn<QList<Data>>("mapping");
0134 
0135 #define COLUMN_HEADER ("c" + QByteArray::number(Imap::Mailbox::TreeItem::OFFSET_HEADER))
0136 #define COLUMN_TEXT ("c" + QByteArray::number(Imap::Mailbox::TreeItem::OFFSET_TEXT))
0137 #define COLUMN_MIME ("c" + QByteArray::number(Imap::Mailbox::TreeItem::OFFSET_MIME))
0138 #define COLUMN_RAW_CONTENTS ("c" + QByteArray::number(Imap::Mailbox::TreeItem::OFFSET_RAW_CONTENTS))
0139 
0140     QTest::newRow("plaintext")
0141         << bsPlaintext
0142         << (QList<Data>()
0143             // Part 1, a text/plain thing
0144             << Data(QStringLiteral("0"), QStringLiteral("1"), "blesmrt")
0145             // The MIME header of the whole message
0146             << Data(QString("0" + COLUMN_HEADER), QStringLiteral("HEADER"), "raw headers")
0147             // No other items
0148             << Data(QStringLiteral("0.1"))
0149             << Data(QStringLiteral("1"))
0150             );
0151 
0152     QTest::newRow("multipart-signed")
0153             << bsMultipartSignedTextPlain
0154             << (QList<Data>()
0155                 //<< Data("0", "1", "blesmrt")
0156                 << Data(QStringLiteral("0.0"), QStringLiteral("1"), "blesmrt")
0157                 << Data(QStringLiteral("0.1"), QStringLiteral("2"), "signature")
0158                 // No other parts shall be defined
0159                 << Data(QStringLiteral("0.2"))
0160                 << Data(QStringLiteral("0.0.0"))
0161                 << Data(QStringLiteral("0.1.0"))
0162                 << Data(QStringLiteral("1"))
0163                 << Data(QStringLiteral("2"))
0164                 );
0165 
0166     QTest::newRow("torture-test")
0167             << bsTortureTest
0168             << (QList<Data>()
0169                 // Just a top-level child, the multipart/mixed one
0170                 << Data(QStringLiteral("1"))
0171                 // The root is a multipart/mixed item. It's not directly fetchable because it has no "part ID" in IMAP because
0172                 // it's a "top-level multipart", i.e. a multipart which is a child of a message/rfc822.
0173                 << Data(QStringLiteral("0"), Data::NO_FETCHING, QStringLiteral("multipart/mixed"))
0174                 << Data(QString("0" + COLUMN_TEXT), QStringLiteral("TEXT"), "meh")
0175                 << Data(QString("0" + COLUMN_HEADER), QStringLiteral("HEADER"), "meh")
0176                 // There are no MIME or RAW modifier for the root message/rfc822
0177                 << Data(QString("0" + COLUMN_MIME))
0178                 << Data(QString("0" + COLUMN_RAW_CONTENTS))
0179                 // The multipart/mixed is a top-level multipart, and as such it doesn't have the special children
0180                 << Data(QString("0.0" + COLUMN_TEXT))
0181                 << Data(QString("0.0" + COLUMN_HEADER))
0182                 << Data(QString("0.0" + COLUMN_MIME))
0183                 << Data(QString("0.0" + COLUMN_RAW_CONTENTS))
0184                 << Data(QStringLiteral("0.0"), QStringLiteral("1"), "plaintext", QStringLiteral("text/plain"))
0185                 << Data(QString("0.0.0" + COLUMN_MIME), QStringLiteral("1.MIME"), "Content-Type: blabla", QStringLiteral("text/plain"))
0186                 // A text/plain part does not, however, support the TEXT and HEADER modifiers
0187                 << Data(QString("0.0.0" + COLUMN_TEXT))
0188                 << Data(QString("0.0.0" + COLUMN_HEADER))
0189                 << Data(QStringLiteral("0.1.0.0"), QStringLiteral("2.1"), "plaintext another", QStringLiteral("text/plain"))
0190                 << Data(QStringLiteral("0.1.0.1"), QStringLiteral("2.2"), "multipart mixed", QStringLiteral("multipart/mixed"))
0191                 << Data(QStringLiteral("0.1.0.1.0"), QStringLiteral("2.2.1"), "text richtext", QStringLiteral("text/richtext"))
0192                 << Data(QStringLiteral("0.1.0.2"), QStringLiteral("2.3"), "andrew thingy", QStringLiteral("application/andrew-inset"))
0193                 );
0194 
0195     QTest::newRow("message-directly-within-message")
0196             << bsSignedInsideMessageInsideMessage
0197             << (QList<Data>()
0198                 << Data(QStringLiteral("1"))
0199                 << Data(QStringLiteral("0"), QStringLiteral("1"), "aaa", QStringLiteral("message/rfc822"))
0200                 << Data(QStringLiteral("0.0"), Data::NO_FETCHING, QStringLiteral("multipart/signed"))
0201                 << Data(QString("0.0" + COLUMN_TEXT), QStringLiteral("1.TEXT"), "meh")
0202                 << Data(QStringLiteral("0.0.0"), QStringLiteral("1.1"), "bbb", QStringLiteral("text/plain"))
0203                 << Data(QStringLiteral("0.0.1"), QStringLiteral("1.2"), "ccc", QStringLiteral("application/pgp-signature"))
0204                 );
0205 }
0206 
0207 /** @short Check that we catch responses which refer to invalid data */
0208 void BodyPartsTest::testInvalidPartFetch()
0209 {
0210     QFETCH(QByteArray, bodystructure);
0211     QFETCH(QString, partId);
0212 
0213     // By default, there's a 50ms delay between the time we request a part download and the time it actually happens.
0214     // That's too long for a unit test.
0215     model->setProperty("trojita-imap-delayed-fetch-part", 0);
0216 
0217     helperSyncBNoMessages();
0218     cServer("* 1 EXISTS\r\n");
0219     cClient(t.mk("UID FETCH 1:* (FLAGS)\r\n"));
0220     cServer("* 1 FETCH (UID 333 FLAGS ())\r\n" + t.last("OK fetched\r\n"));
0221 
0222     QCOMPARE(model->rowCount(msgListB), 1);
0223     QModelIndex msg = msgListB.model()->index(0, 0, msgListB);
0224     QVERIFY(msg.isValid());
0225     QCOMPARE(model->rowCount(msg), 0);
0226     cClient(t.mk("UID FETCH 333 (" FETCH_METADATA_ITEMS ")\r\n"));
0227     cServer("* 1 FETCH (UID 333 BODYSTRUCTURE (" + bodystructure + "))\r\n" + t.last("OK fetched\r\n"));
0228     QVERIFY(model->rowCount(msg) > 0);
0229 
0230     {
0231         ExpectSingleErrorHere blocker(this);
0232         cServer("* 1 FETCH (UID 333 BODY[" + partId.toUtf8() + "] \"pwn\")\r\n");
0233     }
0234     QVERIFY(errorSpy->isEmpty());
0235 }
0236 
0237 void BodyPartsTest::testInvalidPartFetch_data()
0238 {
0239     QTest::addColumn<QByteArray>("bodystructure");
0240     // QString allows us to use string literals
0241     QTest::addColumn<QString>("partId");
0242 
0243     QTest::newRow("extra-part-plaintext") << bsPlaintext << "2";
0244     QTest::newRow("extra-part-plaintext-child") << bsPlaintext << "1.1";
0245     QTest::newRow("extra-part-plaintext-zero") << bsPlaintext << "0";
0246     QTest::newRow("extra-part-signed-zero") << bsMultipartSignedTextPlain << "0";
0247     QTest::newRow("extra-part-signed-child-1") << bsMultipartSignedTextPlain << "1.0";
0248     QTest::newRow("extra-part-signed-child-2") << bsMultipartSignedTextPlain << "2.0";
0249     QTest::newRow("extra-part-signed-extra") << bsMultipartSignedTextPlain << "3";
0250     QTest::newRow("extra-part-signed-MIME") << bsMultipartSignedTextPlain << "MIME";
0251     QTest::newRow("extra-part-signed-1-TEXT") << bsMultipartSignedTextPlain << "1.TEXT";
0252     QTest::newRow("extra-part-signed-1-HEADER") << bsMultipartSignedTextPlain << "1.HEADER";
0253 }
0254 
0255 /** @short Check how fetching the raw part data is handled */
0256 void BodyPartsTest::testFetchingRawParts()
0257 {
0258     FakeCapabilitiesInjector injector(model);
0259     injector.injectCapability(QStringLiteral("BINARY"));
0260     model->setProperty("trojita-imap-delayed-fetch-part", 0);
0261     helperSyncBNoMessages();
0262     cServer("* 1 EXISTS\r\n");
0263     cClient(t.mk("UID FETCH 1:* (FLAGS)\r\n"));
0264     cServer("* 1 FETCH (UID 333 FLAGS ())\r\n" + t.last("OK fetched\r\n"));
0265     QCOMPARE(model->rowCount(msgListB), 1);
0266     QModelIndex msg = msgListB.model()->index(0, 0, msgListB);
0267     QVERIFY(msg.isValid());
0268     QCOMPARE(model->rowCount(msg), 0);
0269     cClient(t.mk("UID FETCH 333 (" FETCH_METADATA_ITEMS ")\r\n"));
0270     cServer("* 1 FETCH (UID 333 BODYSTRUCTURE (" + bsManyPlaintexts + "))\r\n" + t.last("OK fetched\r\n"));
0271     QCOMPARE(model->rowCount(msg), 1);
0272     QModelIndex rootMultipart = msg.model()->index(0, 0, msg);
0273     QVERIFY(rootMultipart.isValid());
0274     QCOMPARE(model->rowCount(rootMultipart), 5);
0275 
0276     QSignalSpy dataChangedSpy(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
0277 
0278 #define CHECK_DATACHANGED(ROW, INDEX) \
0279     QCOMPARE(dataChangedSpy[ROW][0].toModelIndex(), INDEX); \
0280     QCOMPARE(dataChangedSpy[ROW][1].toModelIndex(), INDEX);
0281 
0282     QModelIndex part, rawPart;
0283     QByteArray fakePartData = "Canary 1";
0284 
0285     // First make sure that we can fetch the raw version of a simple part which has not been fetched before.
0286     part = rootMultipart.model()->index(0, 0, rootMultipart);
0287     QCOMPARE(part.data(RolePartId).toString(), QString("1"));
0288     rawPart = part.model()->index(0, TreeItem::OFFSET_RAW_CONTENTS, part);
0289     QVERIFY(rawPart.isValid());
0290     QCOMPARE(rawPart.data(RolePartData).toByteArray(), QByteArray());
0291     cClient(t.mk("UID FETCH 333 (BODY.PEEK[1])\r\n"));
0292     cServer("* 1 FETCH (UID 333 BODY[1] \"" + fakePartData.toBase64() + "\")\r\n" + t.last("OK fetched\r\n"));
0293     QCOMPARE(dataChangedSpy.size(), 1);
0294     CHECK_DATACHANGED(0, rawPart);
0295     QVERIFY(!part.data(RoleIsFetched).toBool());
0296     QVERIFY(rawPart.data(RoleIsFetched).toBool());
0297     QCOMPARE(rawPart.data(RolePartData).toByteArray(), fakePartData.toBase64());
0298     QVERIFY(model->cache()->messagePart("b", 333, "1").isNull());
0299     QCOMPARE(model->cache()->messagePart("b", 333, "1.X-RAW"), fakePartData.toBase64());
0300     cEmpty();
0301     dataChangedSpy.clear();
0302 
0303     // Check that the same fetch is repeated when a request for raw, unprocessed data is made again
0304     part = rootMultipart.model()->index(1, 0, rootMultipart);
0305     QCOMPARE(part.data(RolePartId).toString(), QString("2"));
0306     rawPart = part.model()->index(0, TreeItem::OFFSET_RAW_CONTENTS, part);
0307     QVERIFY(rawPart.isValid());
0308     QCOMPARE(part.data(RolePartData).toByteArray(), QByteArray());
0309     cClient(t.mk("UID FETCH 333 (BINARY.PEEK[2])\r\n"));
0310     cServer("* 1 FETCH (UID 333 BINARY[2] \"ahoj\")\r\n" + t.last("OK fetched\r\n"));
0311     QCOMPARE(dataChangedSpy.size(), 1);
0312     CHECK_DATACHANGED(0, part);
0313     dataChangedSpy.clear();
0314     QVERIFY(part.data(RoleIsFetched).toBool());
0315     QVERIFY(!rawPart.data(RoleIsFetched).toBool());
0316     QCOMPARE(part.data(RolePartData).toString(), QString("ahoj"));
0317     QCOMPARE(model->cache()->messagePart("b", 333, "2"), QByteArray("ahoj"));
0318     QCOMPARE(model->cache()->messagePart("b", 333, "2.X-RAW").isNull(), true);
0319     // Trigger fetching of the raw data.
0320     // Make sure that we do *not* overwrite the already decoded data needlessly.
0321     // If the server is broken and performs the CTE decoding in a wrong way, let's just silently ignore this.
0322     fakePartData = "Canary 2";
0323     QCOMPARE(rawPart.data(RolePartData).toByteArray(), QByteArray());
0324     cClient(t.mk("UID FETCH 333 (BODY.PEEK[2])\r\n"));
0325     cServer("* 1 FETCH (UID 333 BODY[2] \"" + fakePartData.toBase64() + "\")\r\n" + t.last("OK fetched\r\n"));
0326     QCOMPARE(dataChangedSpy.size(), 1);
0327     CHECK_DATACHANGED(0, rawPart);
0328     QVERIFY(part.data(RoleIsFetched).toBool());
0329     QVERIFY(rawPart.data(RoleIsFetched).toBool());
0330     QCOMPARE(part.data(RolePartData).toString(), QString("ahoj"));
0331     QCOMPARE(rawPart.data(RolePartData).toByteArray(), fakePartData.toBase64());
0332     QVERIFY(model->cache()->messagePart("b", 333, "2").isNull());
0333     QCOMPARE(model->cache()->messagePart("b", 333, "2.X-RAW"), fakePartData.toBase64());
0334     cEmpty();
0335     dataChangedSpy.clear();
0336 
0337     // Make sure that requests for part whose raw form was already loaded is accommodated locally
0338     fakePartData = "Canary 3";
0339     part = rootMultipart.model()->index(2, 0, rootMultipart);
0340     QCOMPARE(part.data(RolePartId).toString(), QString("3"));
0341     rawPart = part.model()->index(0, TreeItem::OFFSET_RAW_CONTENTS, part);
0342     QVERIFY(rawPart.isValid());
0343     QCOMPARE(rawPart.data(RolePartData).toByteArray(), QByteArray());
0344     cClient(t.mk("UID FETCH 333 (BODY.PEEK[3])\r\n"));
0345     cServer("* 1 FETCH (UID 333 BODY[3] \"" + fakePartData.toBase64() + "\")\r\n" + t.last("OK fetched\r\n"));
0346     QCOMPARE(dataChangedSpy.size(), 1);
0347     CHECK_DATACHANGED(0, rawPart);
0348     QVERIFY(!part.data(RoleIsFetched).toBool());
0349     QVERIFY(rawPart.data(RoleIsFetched).toBool());
0350     QCOMPARE(rawPart.data(RolePartData).toByteArray(), fakePartData.toBase64());
0351     QCOMPARE(model->cache()->messagePart("b", 333, "3").isNull(), true);
0352     QCOMPARE(model->cache()->messagePart("b", 333, "3.X-RAW"), fakePartData.toBase64());
0353     cEmpty();
0354     dataChangedSpy.clear();
0355     // Now the request for actual part data shall be accommodated from the cache.
0356     // As this is a first request ever, there's no need to emit dataChanged. The on-disk cache is not populated.
0357     QCOMPARE(part.data(RolePartData).toByteArray(), fakePartData);
0358     QVERIFY(part.data(RoleIsFetched).toBool());
0359     QCOMPARE(dataChangedSpy.size(), 0);
0360     QCOMPARE(model->cache()->messagePart("b", 333, "3").isNull(), true);
0361 
0362     // Make sure that requests for already processed part are accommodated from the cache if possible
0363     fakePartData = "Canary 4";
0364     part = rootMultipart.model()->index(3, 0, rootMultipart);
0365     QCOMPARE(part.data(RolePartId).toString(), QString("4"));
0366     rawPart = part.model()->index(0, TreeItem::OFFSET_RAW_CONTENTS, part);
0367     QVERIFY(rawPart.isValid());
0368     model->cache()->setMsgPart(QStringLiteral("b"), 333, "4.X-RAW", fakePartData.toBase64());
0369     QVERIFY(!part.data(RoleIsFetched).toBool());
0370     QVERIFY(!rawPart.data(RoleIsFetched).toBool());
0371     QCOMPARE(part.data(RolePartData).toByteArray(), fakePartData);
0372     QCOMPARE(dataChangedSpy.size(), 0);
0373     QCOMPARE(model->cache()->messagePart("b", 333, "4").isNull(), true);
0374     cEmpty();
0375     dataChangedSpy.clear();
0376 
0377     // Make sure that requests for already processed part and the raw data are merged if they happen close enough and in the correct order
0378     fakePartData = "Canary 5";
0379     part = rootMultipart.model()->index(4, 0, rootMultipart);
0380     QCOMPARE(part.data(RolePartId).toString(), QString("5"));
0381     rawPart = part.model()->index(0, TreeItem::OFFSET_RAW_CONTENTS, part);
0382     QVERIFY(rawPart.isValid());
0383     QCOMPARE(rawPart.data(RolePartData).toByteArray(), QByteArray());
0384     QCOMPARE(part.data(RolePartData).toByteArray(), QByteArray());
0385     cClient(t.mk("UID FETCH 333 (BODY.PEEK[5])\r\n"));
0386     cServer("* 1 FETCH (UID 333 BODY[5] \"" + fakePartData.toBase64() + "\")\r\n" + t.last("OK fetched\r\n"));
0387     QCOMPARE(dataChangedSpy.size(), 2);
0388     CHECK_DATACHANGED(0, rawPart);
0389     CHECK_DATACHANGED(1, part);
0390     dataChangedSpy.clear();
0391     QVERIFY(part.data(RoleIsFetched).toBool());
0392     QVERIFY(rawPart.data(RoleIsFetched).toBool());
0393     QCOMPARE(part.data(RolePartData).toString(), QString(fakePartData));
0394     QCOMPARE(rawPart.data(RolePartData).toByteArray(), fakePartData.toBase64());
0395     QVERIFY(model->cache()->messagePart("b", 333, "5").isNull());
0396     QCOMPARE(model->cache()->messagePart("b", 333, "5.X-RAW"), fakePartData.toBase64());
0397     cEmpty();
0398 }
0399 
0400 void BodyPartsTest::testFilenameExtraction()
0401 {
0402     QFETCH(QByteArray, bodystructure);
0403     QFETCH(QString, partId);
0404     QFETCH(QString, filename);
0405 
0406     model->setProperty("trojita-imap-delayed-fetch-part", 0);
0407     helperSyncBNoMessages();
0408     cServer("* 1 EXISTS\r\n");
0409     cClient(t.mk("UID FETCH 1:* (FLAGS)\r\n"));
0410     cServer("* 1 FETCH (UID 333 FLAGS ())\r\n" + t.last("OK fetched\r\n"));
0411 
0412     QCOMPARE(model->rowCount(msgListB), 1);
0413     QModelIndex msg = msgListB.model()->index(0, 0, msgListB);
0414     QVERIFY(msg.isValid());
0415     QCOMPARE(model->rowCount(msg), 0);
0416     cClient(t.mk("UID FETCH 333 (" FETCH_METADATA_ITEMS ")\r\n"));
0417     cServer("* 1 FETCH (UID 333 BODYSTRUCTURE (" + bodystructure + "))\r\n" + t.last("OK fetched\r\n"));
0418     QVERIFY(model->rowCount(msg) > 0);
0419 
0420     const QString wherePrefix = QString::number(idxB.row()) + QLatin1Char('.') +
0421             QString::number(msgListB.row()) + QLatin1Char('.') + QString::number(msg.row()) + QLatin1Char('.');
0422     QCOMPARE(findIndexByPosition(model, wherePrefix.left(wherePrefix.size() - 1)), msg);
0423 
0424     QModelIndex idx = findIndexByPosition(model, wherePrefix + partId);
0425     QVERIFY(idx.isValid());
0426     QCOMPARE(idx.data(Imap::Mailbox::RolePartFileName).toString(), filename);
0427     QVERIFY(errorSpy->isEmpty());
0428     cEmpty();
0429 }
0430 
0431 void BodyPartsTest::testFilenameExtraction_data()
0432 {
0433     QTest::addColumn<QByteArray>("bodystructure");
0434     QTest::addColumn<QString>("partId");
0435     QTest::addColumn<QString>("filename");
0436 
0437 
0438     QTest::newRow("evernote-plaintext-0") << bsEvernote << QStringLiteral("0") << QString(); // multipart/mixed
0439     QTest::newRow("evernote-plaintext-0.0") << bsEvernote << QStringLiteral("0.0") << QString(); // multipart/alternative
0440     QTest::newRow("evernote-plaintext-0.0.0") << bsEvernote << QStringLiteral("0.0.0") << QString(); // text/plain
0441     QTest::newRow("evernote-plaintext-0.0.1") << bsEvernote << QStringLiteral("0.0.1") << QString(); // text/html
0442     QTest::newRow("evernote-plaintext-0.1") << bsEvernote << QStringLiteral("0.1") << QStringLiteral("CAN0000009221(1)"); // application/octet-stream
0443 
0444     QTest::newRow("plaintext-just-filename") << bsPlaintextWithFilenameAsFilename << QStringLiteral("0") << QStringLiteral("pwn.txt");
0445     QTest::newRow("plaintext-just-obsolete-name") << bsPlaintextWithFilenameAsName << QStringLiteral("0") << QStringLiteral("pwn.txt");
0446     QTest::newRow("plaintext-filename-preferred-over-name") << bsPlaintextWithFilenameAsBoth << QStringLiteral("0") << QStringLiteral("pwn.txt");
0447     QTest::newRow("name-overwrites-empty-filename") << bsPlaintextEmptyFilename << QStringLiteral("0") << QStringLiteral("actual");
0448 }
0449 
0450 /** @short Ensure that an [UNKNOWN-CTE] with BINARY results in a fallback to regular FETCH */
0451 void BodyPartsTest::testBinaryFallback()
0452 {
0453     FakeCapabilitiesInjector injector(model);
0454     injector.injectCapability(QStringLiteral("BINARY"));
0455     model->setProperty("trojita-imap-delayed-fetch-part", 10);
0456     helperSyncBNoMessages();
0457     cServer("* 1 EXISTS\r\n");
0458     cClient(t.mk("UID FETCH 1:* (FLAGS)\r\n"));
0459     cServer("* 1 FETCH (UID 333 FLAGS ())\r\n" + t.last("OK fetched\r\n"));
0460     QCOMPARE(model->rowCount(msgListB), 1);
0461     QModelIndex msg = msgListB.model()->index(0, 0, msgListB);
0462     QVERIFY(msg.isValid());
0463     QCOMPARE(model->rowCount(msg), 0);
0464     cClient(t.mk("UID FETCH 333 (" FETCH_METADATA_ITEMS ")\r\n"));
0465     cServer("* 1 FETCH (UID 333 BODYSTRUCTURE (" + bsManyPlaintexts + "))\r\n" + t.last("OK fetched\r\n"));
0466     QCOMPARE(model->rowCount(msg), 1);
0467     QModelIndex rootMultipart = msg.model()->index(0, 0, msg);
0468     QVERIFY(rootMultipart.isValid());
0469     QCOMPARE(model->rowCount(rootMultipart), 5);
0470 
0471     QSignalSpy dataChangedSpy(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
0472 
0473     {
0474         // One BINARY item fails, the other one is successfully retrieved
0475         auto part1 = rootMultipart.model()->index(0, 0, rootMultipart);
0476         auto part2 = rootMultipart.model()->index(1, 0, rootMultipart);
0477         QCOMPARE(part1.data(RolePartId).toString(), QString("1"));
0478         QCOMPARE(part1.data(RolePartData).toByteArray(), QByteArray());
0479         QCOMPARE(part2.data(RolePartId).toString(), QString("2"));
0480         QCOMPARE(part2.data(RolePartData).toByteArray(), QByteArray());
0481         QTest::qWait(15);
0482         cClientRegExp(t.mk("UID FETCH 333 \\((BINARY\\.PEEK\\[(2|1)\\] ?){2}\\)"));
0483         cServer("* 1 FETCH (UID 333 BINARY[2] \"ahoj\")\r\n");
0484         cServer(t.last("OK [UNKNOWN-CTE] some items failed to fetch\r\n"));
0485         QCOMPARE(dataChangedSpy.size(), 1);
0486         CHECK_DATACHANGED(0, part2);
0487         QVERIFY(!part1.data(RoleIsFetched).toBool());
0488         QVERIFY(!part1.data(RoleIsUnavailable).toBool());
0489         QVERIFY(part2.data(RoleIsFetched).toBool());
0490         QVERIFY(model->cache()->messagePart("b", 333, "1").isNull());
0491         QCOMPARE(model->cache()->messagePart("b", 333, "2"), QByteArray("ahoj"));
0492         dataChangedSpy.clear();
0493         // check that a retry worked
0494         QTest::qWait(15);
0495         cClient(t.mk("UID FETCH 333 (BODY.PEEK[1])\r\n"));
0496         cServer("* 1 FETCH (UID 333 BODY[1] \"" + QByteArray("recovered").toBase64() + "\")\r\n");
0497         cServer(t.last("OK fetched this time\r\n"));
0498         QCOMPARE(dataChangedSpy.size(), 1);
0499         CHECK_DATACHANGED(0, part1);
0500         QVERIFY(part1.data(RoleIsFetched).toBool());
0501         QCOMPARE(model->cache()->messagePart("b", 333, "1"), QByteArray("recovered"));
0502         dataChangedSpy.clear();
0503         cEmpty();
0504     }
0505 
0506     {
0507         // A retry of a failed BINARY fails with a NO [UNKNOWN-CTE], too.
0508         // A real server shouldn't do that, but we shouldn't enter an infinite loop, either.
0509         auto part3 = rootMultipart.model()->index(2, 0, rootMultipart);
0510         QCOMPARE(part3.data(RolePartId).toString(), QString("3"));
0511         QCOMPARE(part3.data(RolePartData).toByteArray(), QByteArray());
0512         QTest::qWait(15);
0513         cClient(t.mk("UID FETCH 333 (BINARY.PEEK[3])\r\n"));
0514         cServer(t.last("NO [UNKNOWN-CTE] some items failed to fetch\r\n"));
0515         QCOMPARE(dataChangedSpy.size(), 0);
0516         QVERIFY(!part3.data(RoleIsFetched).toBool());
0517         QVERIFY(!part3.data(RoleIsUnavailable).toBool());
0518         QTest::qWait(15);
0519         cClient(t.mk("UID FETCH 333 (BODY.PEEK[3])\r\n"));
0520         cServer(t.last("NO [UNKNOWN-CTE] pwned\r\n"));
0521         QCOMPARE(dataChangedSpy.size(), 1);
0522         CHECK_DATACHANGED(0, part3);
0523         QVERIFY(!part3.data(RoleIsFetched).toBool());
0524         QVERIFY(part3.data(RoleIsUnavailable).toBool());
0525         QVERIFY(model->cache()->messagePart("b", 333, "3").isNull());
0526         dataChangedSpy.clear();
0527         QTest::qWait(15);
0528         cEmpty();
0529     }
0530 
0531     {
0532         // A retry of a failed BINARY fails with a regular NO, but without any [UNKNOWN-CTE] this time.
0533         auto part4 = rootMultipart.model()->index(3, 0, rootMultipart);
0534         QCOMPARE(part4.data(RolePartId).toString(), QString("4"));
0535         QCOMPARE(part4.data(RolePartData).toByteArray(), QByteArray());
0536         QTest::qWait(15);
0537         cClient(t.mk("UID FETCH 333 (BINARY.PEEK[4])\r\n"));
0538         cServer(t.last("NO [UNKNOWN-CTE] some items failed to fetch\r\n"));
0539         QCOMPARE(dataChangedSpy.size(), 0);
0540         QVERIFY(!part4.data(RoleIsFetched).toBool());
0541         QVERIFY(!part4.data(RoleIsUnavailable).toBool());
0542         QTest::qWait(15);
0543         cClient(t.mk("UID FETCH 333 (BODY.PEEK[4])\r\n"));
0544         cServer(t.last("NO just go away\r\n"));
0545         QCOMPARE(dataChangedSpy.size(), 1);
0546         CHECK_DATACHANGED(0, part4);
0547         QVERIFY(!part4.data(RoleIsFetched).toBool());
0548         QVERIFY(part4.data(RoleIsUnavailable).toBool());
0549         QVERIFY(model->cache()->messagePart("b", 333, "4").isNull());
0550         dataChangedSpy.clear();
0551         QTest::qWait(15);
0552         cEmpty();
0553     }
0554 }
0555 
0556 QTEST_GUILESS_MAIN(BodyPartsTest)