File indexing completed on 2024-05-12 05:17:10

0001 /*
0002    SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
0003    SPDX-FileContributor: Kevin Ottens <kevin@kdab.com>
0004 
0005    SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include <QTest>
0009 
0010 #include "kimap/fetchjob.h"
0011 #include "kimap/session.h"
0012 #include "kimaptest/fakeserver.h"
0013 
0014 #include <QSignalSpy>
0015 #include <QTest>
0016 
0017 Q_DECLARE_METATYPE(KIMAP::FetchJob::FetchScope)
0018 
0019 class FetchJobTest : public QObject
0020 {
0021     Q_OBJECT
0022 
0023 public:
0024     FetchJobTest()
0025     {
0026         qRegisterMetaType<KIMAP::ImapSet>();
0027     }
0028 
0029 private:
0030     QStringList m_signals;
0031 
0032     QMap<qint64, qint64> m_uids;
0033     QMap<qint64, qint64> m_sizes;
0034     QMap<qint64, KIMAP::MessageFlags> m_flags;
0035     QMap<qint64, KIMAP::MessagePtr> m_messages;
0036     QMap<qint64, KIMAP::MessageParts> m_parts;
0037     QMap<qint64, KIMAP::MessageAttribute> m_attrs;
0038     QMap<qint64, KIMAP::Message> m_msgs;
0039 
0040 public Q_SLOTS:
0041     void onHeadersReceived(const QString & /*mailBox*/,
0042                            const QMap<qint64, qint64> &uids,
0043                            const QMap<qint64, qint64> &sizes,
0044                            const QMap<qint64, KIMAP::MessageAttribute> &attrs,
0045                            const QMap<qint64, KIMAP::MessageFlags> &flags,
0046                            const QMap<qint64, KIMAP::MessagePtr> &messages)
0047     {
0048         m_signals << QStringLiteral("headersReceived");
0049         m_uids.insert(uids);
0050         m_sizes.insert(sizes);
0051         m_flags.insert(flags);
0052         m_messages.insert(messages);
0053         m_attrs.insert(attrs);
0054     }
0055 
0056     void onMessagesReceived(const QString & /*mailbox*/,
0057                             const QMap<qint64, qint64> &uids,
0058                             const QMap<qint64, KIMAP::MessageAttribute> &attrs,
0059                             const QMap<qint64, KIMAP::MessagePtr> &messages)
0060     {
0061         m_signals << QStringLiteral("messagesReceived");
0062         m_uids.insert(uids);
0063         m_messages.insert(messages);
0064         m_attrs.insert(attrs);
0065     }
0066 
0067     void onPartsReceived(const QString & /*mailbox*/,
0068                          const QMap<qint64, qint64> & /*uids*/,
0069                          const QMap<qint64, KIMAP::MessageAttribute> &attrs,
0070                          const QMap<qint64, KIMAP::MessageParts> &parts)
0071     {
0072         m_signals << QStringLiteral("partsReceived");
0073         m_attrs.insert(attrs);
0074         m_parts.insert(parts);
0075     }
0076 
0077     void onMessagesAvailable(const QMap<qint64, KIMAP::Message> &messages)
0078     {
0079         m_signals << QStringLiteral("messagesAvailable");
0080         m_msgs.insert(messages);
0081     }
0082 
0083 private Q_SLOTS:
0084 
0085     void testFetch_data()
0086     {
0087         qRegisterMetaType<KIMAP::FetchJob::FetchScope>();
0088 
0089         QTest::addColumn<bool>("uidBased");
0090         QTest::addColumn<KIMAP::ImapSet>("set");
0091         QTest::addColumn<int>("expectedMessageCount");
0092         QTest::addColumn<QList<QByteArray>>("scenario");
0093         QTest::addColumn<KIMAP::FetchJob::FetchScope>("scope");
0094         QTest::addColumn<KIMAP::ImapSet>("expectedVanished");
0095 
0096         KIMAP::FetchJob::FetchScope scope;
0097         scope.mode = KIMAP::FetchJob::FetchScope::Flags;
0098         scope.changedSince = 123456789;
0099 
0100         QList<QByteArray> scenario;
0101         scenario << FakeServer::preauth() << "C: A000001 FETCH 1:4 (FLAGS UID) (CHANGEDSINCE 123456789)"
0102                  << "S: * 1 FETCH ( FLAGS () UID 1 )"
0103                  << "S: * 2 FETCH ( FLAGS () UID 2 )"
0104                  << "S: * 3 FETCH ( FLAGS () UID 3 )"
0105                  << "S: * 4 FETCH ( FLAGS () UID 4 )"
0106                  << "S: A000001 OK fetch done";
0107 
0108         QTest::newRow("messages have empty flags (with changedsince)") << false << KIMAP::ImapSet(1, 4) << 4 << scenario << scope << KIMAP::ImapSet{};
0109 
0110         scenario.clear();
0111         scope.changedSince = 0;
0112         scenario << FakeServer::preauth() << "C: A000001 FETCH 1:4 (FLAGS UID)"
0113                  << "S: * 1 FETCH ( FLAGS () UID 1 )"
0114                  << "S: * 2 FETCH ( FLAGS () UID 2 )"
0115                  << "S: * 3 FETCH ( FLAGS () UID 3 )"
0116                  << "S: * 4 FETCH ( FLAGS () UID 4 )"
0117                  << "S: A000001 OK fetch done";
0118 
0119         QTest::newRow("messages have empty flags") << false << KIMAP::ImapSet(1, 4) << 4 << scenario << scope << KIMAP::ImapSet{};
0120 
0121         scenario.clear();
0122         // kill the connection part-way through a list, with carriage returns at end
0123         // BUG 253619
0124         // this should fail, but it shouldn't crash
0125         scenario << FakeServer::preauth()
0126                  << "C: A000001 FETCH 11 (RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)] FLAGS UID)"
0127                  << "S: * 11 FETCH (RFC822.SIZE 770 INTERNALDATE \"11-Oct-2010 03:33:50 +0100\" BODY[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO "
0128                     "SUBJECT DATE)] {246}"
0129                  << "S: From: John Smith <jonathanr.smith@foobarbaz.com>\r\nTo: "
0130                     "\"amagicemailaddress@foobarbazbarfoo.com\"\r\n\t<amagicemailaddress@foobarbazbarfoo.com>\r\nDate: Mon, 11 Oct 2010 03:34:48 "
0131                     "+0100\r\nSubject: unsubscribe\r\nMessage-ID: <ASDFFDSASDFFDS@foobarbaz.com>\r\n\r\n"
0132                  << "X";
0133         scope.mode = KIMAP::FetchJob::FetchScope::Headers;
0134         QTest::newRow("connection drop") << false << KIMAP::ImapSet(11, 11) << 1 << scenario << scope << KIMAP::ImapSet{};
0135 
0136         scenario.clear();
0137         // Important bit here if "([127.0.0.1])" which used to crash the stream parser
0138         scenario << FakeServer::preauth()
0139                  << "C: A000001 FETCH 11 (RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)] FLAGS UID)"
0140                  << "S: * 11 FETCH (RFC822.SIZE 770 INTERNALDATE \"11-Oct-2010 03:33:50 +0100\" BODY[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO "
0141                     "SUBJECT DATE)] {246}"
0142                  << "S: ([127.0.0.1])\r\nDate: Mon, 11 Oct 2010 03:34:48 +0100\r\nSubject: unsubscribe\r\nMessage-ID: <ASDFFDSASDFFDS@foobarbaz.com>\r\n\r\n"
0143                  << "X";
0144         scope.mode = KIMAP::FetchJob::FetchScope::Headers;
0145         QTest::newRow("buffer overwrite") << false << KIMAP::ImapSet(11, 11) << 1 << scenario << scope << KIMAP::ImapSet{};
0146 
0147         scenario.clear();
0148         // We're assuming a buffer overwrite here which made us miss the opening parenthesis
0149         // for the properties list
0150         scenario << FakeServer::preauth()
0151                  << "C: A000001 FETCH 11 (RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)] FLAGS UID)"
0152                  << "S: * 11 FETCH {10}doh!\r\n\r\n\r\n)\r\n"
0153                  << "X";
0154         scope.mode = KIMAP::FetchJob::FetchScope::Headers;
0155         QTest::newRow("buffer overwrite 2") << false << KIMAP::ImapSet(11, 11) << 1 << scenario << scope << KIMAP::ImapSet{};
0156 
0157         scenario.clear();
0158         scenario << FakeServer::preauth() << "C: A000001 FETCH 11 (RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER] FLAGS UID) (CHANGEDSINCE 123456789)"
0159                  << "S: * 11 FETCH (UID 123 RFC822.SIZE 770 INTERNALDATE \"11-Oct-2010 03:33:50 +0100\" BODY[HEADER] {245}"
0160                  << "S: From: John Smith <jonathanr.smith@foobarbaz.com>\r\nTo: "
0161                     "\"amagicemailaddress@foobarbazbarfoo.com\"\r\n\t<amagicemailaddress@foobarbazbarfoo.com>\r\nDate: Mon, 11 Oct 2010 03:34:48 "
0162                     "+0100\r\nSubject: unsubscribe\r\nMessage-ID: <ASDFFDSASDFFDS@foobarbaz.com>\r\n\r\n  FLAGS ())"
0163                  << "S: A000001 OK fetch done";
0164         scope.mode = KIMAP::FetchJob::FetchScope::FullHeaders;
0165         scope.changedSince = 123456789;
0166         QTest::newRow("fetch full headers") << false << KIMAP::ImapSet(11, 11) << 1 << scenario << scope << KIMAP::ImapSet{};
0167 
0168         scenario.clear();
0169         scenario << FakeServer::preauth() << "C: A000001 UID FETCH 300:500 (FLAGS UID) (CHANGEDSINCE 12345 VANISHED)"
0170                  << "S: * VANISHED (EARLIER) 300:310,405,411"
0171                  << "S: * 1 FETCH (UID 404 MODSEQ (65402) FLAGS (\\Seen))"
0172                  << "S: * 2 FETCH (UID 406 MODSEQ (75403) FLAGS (\\Deleted))"
0173                  << "S: * 4 FETCH (UID 408 MODSEQ (29738) FLAGS ($Nojunk $AutoJunk $MDNSent))"
0174                  << "S: A000001 OK Fetch completed";
0175         scope.mode = KIMAP::FetchJob::FetchScope::Flags;
0176         scope.changedSince = 12345;
0177         scope.qresync = true;
0178         KIMAP::ImapSet vanished;
0179         vanished.add(KIMAP::ImapInterval{300, 310});
0180         vanished.add(QList<qint64>{405, 411});
0181         QTest::newRow("qresync") << true << KIMAP::ImapSet(300, 500) << 3 << scenario << scope << vanished;
0182     }
0183 
0184     void testFetch()
0185     {
0186         QFETCH(bool, uidBased);
0187         QFETCH(KIMAP::ImapSet, set);
0188         QFETCH(int, expectedMessageCount);
0189         QFETCH(QList<QByteArray>, scenario);
0190         QFETCH(KIMAP::FetchJob::FetchScope, scope);
0191         QFETCH(KIMAP::ImapSet, expectedVanished);
0192 
0193         FakeServer fakeServer;
0194         fakeServer.setScenario(scenario);
0195         fakeServer.startAndWait();
0196 
0197         KIMAP::Session session(QStringLiteral("127.0.0.1"), 5989);
0198 
0199         auto job = new KIMAP::FetchJob(&session);
0200         job->setUidBased(uidBased);
0201         job->setSequenceSet(set);
0202         job->setScope(scope);
0203 
0204         connect(job,
0205                 SIGNAL(headersReceived(QString,
0206                                        QMap<qint64, qint64>,
0207                                        QMap<qint64, qint64>,
0208                                        QMap<qint64, KIMAP::MessageAttribute>,
0209                                        QMap<qint64, KIMAP::MessageFlags>,
0210                                        QMap<qint64, KIMAP::MessagePtr>)),
0211                 this,
0212                 SLOT(onHeadersReceived(QString,
0213                                        QMap<qint64, qint64>,
0214                                        QMap<qint64, qint64>,
0215                                        QMap<qint64, KIMAP::MessageAttribute>,
0216                                        QMap<qint64, KIMAP::MessageFlags>,
0217                                        QMap<qint64, KIMAP::MessagePtr>)));
0218         connect(job, &KIMAP::FetchJob::messagesAvailable, this, &FetchJobTest::onMessagesAvailable);
0219 
0220         QSignalSpy vanishedSpy(job, &KIMAP::FetchJob::messagesVanished);
0221         QVERIFY(vanishedSpy.isValid());
0222 
0223         bool result = job->exec();
0224         QEXPECT_FAIL("connection drop", "Expected failure on connection drop", Continue);
0225         QEXPECT_FAIL("buffer overwrite", "Expected failure on confused list", Continue);
0226         QEXPECT_FAIL("buffer overwrite 2", "Expected beginning of message missing", Continue);
0227         QVERIFY(result);
0228         if (result) {
0229             QVERIFY(m_signals.count() > 0);
0230             QCOMPARE(m_uids.count(), expectedMessageCount);
0231             QCOMPARE(m_msgs.count(), expectedMessageCount);
0232             if (scope.qresync) {
0233                 QCOMPARE(vanishedSpy.size(), 1);
0234                 QCOMPARE(vanishedSpy.at(0).at(0).value<KIMAP::ImapSet>(), expectedVanished);
0235             }
0236         }
0237 
0238         QVERIFY(fakeServer.isAllScenarioDone());
0239         fakeServer.quit();
0240 
0241         m_signals.clear();
0242         m_uids.clear();
0243         m_sizes.clear();
0244         m_flags.clear();
0245         m_messages.clear();
0246         m_parts.clear();
0247         m_attrs.clear();
0248         m_msgs.clear();
0249     }
0250 
0251     void testFetchStructure()
0252     {
0253         QList<QByteArray> scenario;
0254         scenario
0255             << FakeServer::preauth() << "C: A000001 FETCH 1:2 (BODYSTRUCTURE UID)"
0256             << R"(S: * 1 FETCH (UID 10 BODYSTRUCTURE ("TEXT" "PLAIN" ("CHARSET" "ISO-8859-1") NIL NIL "7BIT" 5 1 NIL NIL NIL)))"
0257             << R"(S: * 2 FETCH (UID 20 BODYSTRUCTURE (((("TEXT" "PLAIN" ("CHARSET" "ISO-8859-1") NIL NIL "7BIT" 72 4 NIL NIL NIL)("TEXT" "HTML" ("CHARSET" "ISO-8859-1") NIL NIL "QUOTED-PRINTABLE" 281 5 NIL NIL NIL) "ALTERNATIVE" ("BOUNDARY" "0001") NIL NIL)("IMAGE" "GIF" ("NAME" "B56.gif") "<B56@goomoji.gmail>" NIL "BASE64" 528 NIL NIL NIL) "RELATED" ("BOUNDARY" "0002") NIL NIL)("IMAGE" "JPEG" ("NAME" "photo.jpg") NIL NIL "BASE64" 53338 NIL ("ATTACHMENT" ("FILENAME" "photo.jpg")) NIL) "MIXED" ("BOUNDARY" "0003") NIL NIL)))"
0258             << "S: A000001 OK fetch done";
0259 
0260         KIMAP::FetchJob::FetchScope scope;
0261         scope.mode = KIMAP::FetchJob::FetchScope::Structure;
0262 
0263         FakeServer fakeServer;
0264         fakeServer.setScenario(scenario);
0265         fakeServer.startAndWait();
0266 
0267         KIMAP::Session session(QStringLiteral("127.0.0.1"), 5989);
0268 
0269         auto job = new KIMAP::FetchJob(&session);
0270         job->setUidBased(false);
0271         job->setSequenceSet(KIMAP::ImapSet(1, 2));
0272         job->setScope(scope);
0273 
0274         connect(job,
0275                 SIGNAL(messagesReceived(QString, QMap<qint64, qint64>, QMap<qint64, KIMAP::MessageAttribute>, QMap<qint64, KIMAP::MessagePtr>)),
0276                 this,
0277                 SLOT(onMessagesReceived(QString, QMap<qint64, qint64>, QMap<qint64, KIMAP::MessageAttribute>, QMap<qint64, KIMAP::MessagePtr>)));
0278         connect(job, &KIMAP::FetchJob::messagesAvailable, this, &FetchJobTest::onMessagesAvailable);
0279 
0280         bool result = job->exec();
0281         QVERIFY(result);
0282         QVERIFY(m_signals.count() > 0);
0283         QCOMPARE(m_uids.count(), 2);
0284         QCOMPARE(m_messages[1]->attachments().count(), 0);
0285         QCOMPARE(m_messages[2]->attachments().count(), 1);
0286         QCOMPARE(m_messages[2]->contents().size(), 2);
0287         QCOMPARE(m_messages[2]->contents()[0]->contents().size(), 2);
0288         QCOMPARE(m_messages[2]->attachments().at(0)->contentDisposition()->filename(), QStringLiteral("photo.jpg"));
0289         QCOMPARE(m_msgs.count(), 2);
0290         QCOMPARE(m_msgs[1].message->attachments().count(), 0);
0291         QCOMPARE(m_msgs[2].message->attachments().count(), 1);
0292         QCOMPARE(m_msgs[2].message->contents().size(), 2);
0293         QCOMPARE(m_msgs[2].message->contents()[0]->contents().size(), 2);
0294         QCOMPARE(m_msgs[2].message->attachments().at(0)->contentDisposition()->filename(), QStringLiteral("photo.jpg"));
0295 
0296         fakeServer.quit();
0297 
0298         m_signals.clear();
0299         m_uids.clear();
0300         m_sizes.clear();
0301         m_flags.clear();
0302         m_messages.clear();
0303         m_parts.clear();
0304         m_attrs.clear();
0305         m_msgs.clear();
0306     }
0307 
0308     void testFetchParts()
0309     {
0310         QList<QByteArray> scenario;
0311         scenario << FakeServer::preauth()
0312                  << "C: A000001 FETCH 2 (BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)] BODY.PEEK[1.1.1.MIME] "
0313                     "BODY.PEEK[1.1.1] FLAGS UID)"
0314                  << "S: * 2 FETCH (UID 20 FLAGS (\\Seen) BODY[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)] {154}\r\nFrom: Joe Smith "
0315                     "<smith@example.com>\r\nDate: Wed, 2 Mar 2011 11:33:24 +0700\r\nMessage-ID: <1234@example.com>\r\nSubject: hello\r\nTo: Jane "
0316                     "<jane@example.com>\r\n\r\n BODY[1.1.1] {28}\r\nHi Jane, nice to meet you!\r\n BODY[1.1.1.MIME] {48}\r\nContent-Type: text/plain; "
0317                     "charset=ISO-8859-1\r\n\r\n)\r\n"
0318                  << "S: A000001 OK fetch done";
0319 
0320         KIMAP::FetchJob::FetchScope scope;
0321         scope.mode = KIMAP::FetchJob::FetchScope::HeaderAndContent;
0322         scope.parts.clear();
0323         scope.parts.append("1.1.1");
0324 
0325         FakeServer fakeServer;
0326         fakeServer.setScenario(scenario);
0327         fakeServer.startAndWait();
0328 
0329         KIMAP::Session session(QStringLiteral("127.0.0.1"), 5989);
0330 
0331         auto job = new KIMAP::FetchJob(&session);
0332         job->setUidBased(false);
0333         job->setSequenceSet(KIMAP::ImapSet(2, 2));
0334         job->setScope(scope);
0335 
0336         connect(job,
0337                 SIGNAL(headersReceived(QString,
0338                                        QMap<qint64, qint64>,
0339                                        QMap<qint64, qint64>,
0340                                        QMap<qint64, KIMAP::MessageAttribute>,
0341                                        QMap<qint64, KIMAP::MessageFlags>,
0342                                        QMap<qint64, KIMAP::MessagePtr>)),
0343                 this,
0344                 SLOT(onHeadersReceived(QString,
0345                                        QMap<qint64, qint64>,
0346                                        QMap<qint64, qint64>,
0347                                        QMap<qint64, KIMAP::MessageAttribute>,
0348                                        QMap<qint64, KIMAP::MessageFlags>,
0349                                        QMap<qint64, KIMAP::MessagePtr>)));
0350         connect(job,
0351                 SIGNAL(partsReceived(QString, QMap<qint64, qint64>, QMap<qint64, KIMAP::MessageAttribute>, QMap<qint64, KIMAP::MessageParts>)),
0352                 this,
0353                 SLOT(onPartsReceived(QString, QMap<qint64, qint64>, QMap<qint64, KIMAP::MessageAttribute>, QMap<qint64, KIMAP::MessageParts>)));
0354         connect(job, &KIMAP::FetchJob::messagesAvailable, this, &FetchJobTest::onMessagesAvailable);
0355         bool result = job->exec();
0356 
0357         QVERIFY(result);
0358         QVERIFY(m_signals.count() > 0);
0359         QCOMPARE(m_uids.count(), 1);
0360         QCOMPARE(m_parts.count(), 1);
0361         QCOMPARE(m_attrs.count(), 0);
0362         QCOMPARE(m_msgs.count(), 1);
0363 
0364         // Check that we received the message header
0365         QCOMPARE(m_messages[2]->messageID()->identifier(), QByteArray("1234@example.com"));
0366         QCOMPARE(m_msgs[2].message->messageID()->identifier(), QByteArray("1234@example.com"));
0367 
0368         // Check that we received the flags
0369         QMap<qint64, KIMAP::MessageFlags> expectedFlags;
0370         expectedFlags.insert(2, KIMAP::MessageFlags() << "\\Seen");
0371         QCOMPARE(m_flags, expectedFlags);
0372         QCOMPARE(m_msgs[2].flags, expectedFlags[2]);
0373 
0374         // Check that we didn't received the full message body, since we only requested a specific part
0375         QCOMPARE(m_messages[2]->decodedText().length(), 0);
0376         QCOMPARE(m_messages[2]->attachments().count(), 0);
0377         QCOMPARE(m_msgs[2].message->decodedText().length(), 0);
0378         QCOMPARE(m_msgs[2].message->attachments().count(), 0);
0379 
0380         // Check that we received the part we requested
0381         QByteArray partId = m_parts[2].keys().first();
0382         QString text = m_parts[2].value(partId)->decodedText(true, true);
0383         QCOMPARE(partId, QByteArray("1.1.1"));
0384         QCOMPARE(text, QStringLiteral("Hi Jane, nice to meet you!"));
0385 
0386         QCOMPARE(m_msgs[2].parts.keys().first(), QByteArray("1.1.1"));
0387         QCOMPARE(m_msgs[2].parts.value(partId)->decodedText(true, true), QStringLiteral("Hi Jane, nice to meet you!"));
0388 
0389         fakeServer.quit();
0390 
0391         m_signals.clear();
0392         m_uids.clear();
0393         m_sizes.clear();
0394         m_flags.clear();
0395         m_messages.clear();
0396         m_parts.clear();
0397         m_attrs.clear();
0398         m_msgs.clear();
0399     }
0400 };
0401 
0402 QTEST_GUILESS_MAIN(FetchJobTest)
0403 
0404 #include "fetchjobtest.moc"