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)