1 /*
2    SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
3    SPDX-FileContributor: Kevin Ottens <kevin@kdab.com>
4 
5    SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 
8 #include <QTest>
9 
10 #include "kimap/fetchjob.h"
11 #include "kimap/session.h"
12 #include "kimaptest/fakeserver.h"
13 
14 #include <QSignalSpy>
15 #include <QTest>
16 
17 Q_DECLARE_METATYPE(KIMAP::FetchJob::FetchScope)
18 
19 class FetchJobTest : public QObject
20 {
21     Q_OBJECT
22 
23 public:
FetchJobTest()24     FetchJobTest()
25     {
26         qRegisterMetaType<KIMAP::ImapSet>();
27     }
28 
29 private:
30     QStringList m_signals;
31 
32     QMap<qint64, qint64> m_uids;
33     QMap<qint64, qint64> m_sizes;
34     QMap<qint64, KIMAP::MessageFlags> m_flags;
35     QMap<qint64, KIMAP::MessagePtr> m_messages;
36     QMap<qint64, KIMAP::MessageParts> m_parts;
37     QMap<qint64, KIMAP::MessageAttribute> m_attrs;
38     QMap<qint64, KIMAP::Message> m_msgs;
39 
40 public Q_SLOTS:
onHeadersReceived(const QString &,const QMap<qint64,qint64> & uids,const QMap<qint64,qint64> & sizes,const QMap<qint64,KIMAP::MessageAttribute> & attrs,const QMap<qint64,KIMAP::MessageFlags> & flags,const QMap<qint64,KIMAP::MessagePtr> & messages)41     void onHeadersReceived(const QString & /*mailBox*/,
42                            const QMap<qint64, qint64> &uids,
43                            const QMap<qint64, qint64> &sizes,
44                            const QMap<qint64, KIMAP::MessageAttribute> &attrs,
45                            const QMap<qint64, KIMAP::MessageFlags> &flags,
46                            const QMap<qint64, KIMAP::MessagePtr> &messages)
47     {
48         m_signals << QStringLiteral("headersReceived");
49         m_uids.unite(uids);
50         m_sizes.unite(sizes);
51         m_flags.unite(flags);
52         m_messages.unite(messages);
53         m_attrs.unite(attrs);
54     }
55 
onMessagesReceived(const QString &,const QMap<qint64,qint64> & uids,const QMap<qint64,KIMAP::MessageAttribute> & attrs,const QMap<qint64,KIMAP::MessagePtr> & messages)56     void onMessagesReceived(const QString & /*mailbox*/,
57                             const QMap<qint64, qint64> &uids,
58                             const QMap<qint64, KIMAP::MessageAttribute> &attrs,
59                             const QMap<qint64, KIMAP::MessagePtr> &messages)
60     {
61         m_signals << QStringLiteral("messagesReceived");
62         m_uids.unite(uids);
63         m_messages.unite(messages);
64         m_attrs.unite(attrs);
65     }
66 
onPartsReceived(const QString &,const QMap<qint64,qint64> &,const QMap<qint64,KIMAP::MessageAttribute> & attrs,const QMap<qint64,KIMAP::MessageParts> & parts)67     void onPartsReceived(const QString & /*mailbox*/,
68                          const QMap<qint64, qint64> & /*uids*/,
69                          const QMap<qint64, KIMAP::MessageAttribute> &attrs,
70                          const QMap<qint64, KIMAP::MessageParts> &parts)
71     {
72         m_signals << QStringLiteral("partsReceived");
73         m_attrs.unite(attrs);
74         m_parts.unite(parts);
75     }
76 
onMessagesAvailable(const QMap<qint64,KIMAP::Message> & messages)77     void onMessagesAvailable(const QMap<qint64, KIMAP::Message> &messages)
78     {
79         m_signals << QStringLiteral("messagesAvailable");
80         m_msgs.unite(messages);
81     }
82 
83 private Q_SLOTS:
84 
testFetch_data()85     void testFetch_data()
86     {
87         qRegisterMetaType<KIMAP::FetchJob::FetchScope>();
88 
89         QTest::addColumn<bool>("uidBased");
90         QTest::addColumn<KIMAP::ImapSet>("set");
91         QTest::addColumn<int>("expectedMessageCount");
92         QTest::addColumn<QList<QByteArray>>("scenario");
93         QTest::addColumn<KIMAP::FetchJob::FetchScope>("scope");
94         QTest::addColumn<KIMAP::ImapSet>("expectedVanished");
95 
96         KIMAP::FetchJob::FetchScope scope;
97         scope.mode = KIMAP::FetchJob::FetchScope::Flags;
98         scope.changedSince = 123456789;
99 
100         QList<QByteArray> scenario;
101         scenario << FakeServer::preauth() << "C: A000001 FETCH 1:4 (FLAGS UID) (CHANGEDSINCE 123456789)"
102                  << "S: * 1 FETCH ( FLAGS () UID 1 )"
103                  << "S: * 2 FETCH ( FLAGS () UID 2 )"
104                  << "S: * 3 FETCH ( FLAGS () UID 3 )"
105                  << "S: * 4 FETCH ( FLAGS () UID 4 )"
106                  << "S: A000001 OK fetch done";
107 
108         QTest::newRow("messages have empty flags (with changedsince)") << false << KIMAP::ImapSet(1, 4) << 4 << scenario << scope << KIMAP::ImapSet{};
109 
110         scenario.clear();
111         scope.changedSince = 0;
112         scenario << FakeServer::preauth() << "C: A000001 FETCH 1:4 (FLAGS UID)"
113                  << "S: * 1 FETCH ( FLAGS () UID 1 )"
114                  << "S: * 2 FETCH ( FLAGS () UID 2 )"
115                  << "S: * 3 FETCH ( FLAGS () UID 3 )"
116                  << "S: * 4 FETCH ( FLAGS () UID 4 )"
117                  << "S: A000001 OK fetch done";
118 
119         QTest::newRow("messages have empty flags") << false << KIMAP::ImapSet(1, 4) << 4 << scenario << scope << KIMAP::ImapSet{};
120 
121         scenario.clear();
122         // kill the connection part-way through a list, with carriage returns at end
123         // BUG 253619
124         // this should fail, but it shouldn't crash
125         scenario << FakeServer::preauth()
126                  << "C: A000001 FETCH 11 (RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)] FLAGS UID)"
127                  << "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 "
128                     "SUBJECT DATE)] {246}"
129                  << "S: From: John Smith <jonathanr.smith@foobarbaz.com>\r\nTo: "
130                     "\"amagicemailaddress@foobarbazbarfoo.com\"\r\n\t<amagicemailaddress@foobarbazbarfoo.com>\r\nDate: Mon, 11 Oct 2010 03:34:48 "
131                     "+0100\r\nSubject: unsubscribe\r\nMessage-ID: <ASDFFDSASDFFDS@foobarbaz.com>\r\n\r\n"
132                  << "X";
133         scope.mode = KIMAP::FetchJob::FetchScope::Headers;
134         QTest::newRow("connection drop") << false << KIMAP::ImapSet(11, 11) << 1 << scenario << scope << KIMAP::ImapSet{};
135 
136         scenario.clear();
137         // Important bit here if "([127.0.0.1])" which used to crash the stream parser
138         scenario << FakeServer::preauth()
139                  << "C: A000001 FETCH 11 (RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)] FLAGS UID)"
140                  << "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 "
141                     "SUBJECT DATE)] {246}"
142                  << "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"
143                  << "X";
144         scope.mode = KIMAP::FetchJob::FetchScope::Headers;
145         QTest::newRow("buffer overwrite") << false << KIMAP::ImapSet(11, 11) << 1 << scenario << scope << KIMAP::ImapSet{};
146 
147         scenario.clear();
148         // We're assuming a buffer overwrite here which made us miss the opening parenthesis
149         // for the properties list
150         scenario << FakeServer::preauth()
151                  << "C: A000001 FETCH 11 (RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)] FLAGS UID)"
152                  << "S: * 11 FETCH {10}doh!\r\n\r\n\r\n)\r\n"
153                  << "X";
154         scope.mode = KIMAP::FetchJob::FetchScope::Headers;
155         QTest::newRow("buffer overwrite 2") << false << KIMAP::ImapSet(11, 11) << 1 << scenario << scope << KIMAP::ImapSet{};
156 
157         scenario.clear();
158         scenario << FakeServer::preauth() << "C: A000001 FETCH 11 (RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER] FLAGS UID) (CHANGEDSINCE 123456789)"
159                  << "S: * 11 FETCH (UID 123 RFC822.SIZE 770 INTERNALDATE \"11-Oct-2010 03:33:50 +0100\" BODY[HEADER] {245}"
160                  << "S: From: John Smith <jonathanr.smith@foobarbaz.com>\r\nTo: "
161                     "\"amagicemailaddress@foobarbazbarfoo.com\"\r\n\t<amagicemailaddress@foobarbazbarfoo.com>\r\nDate: Mon, 11 Oct 2010 03:34:48 "
162                     "+0100\r\nSubject: unsubscribe\r\nMessage-ID: <ASDFFDSASDFFDS@foobarbaz.com>\r\n\r\n  FLAGS ())"
163                  << "S: A000001 OK fetch done";
164         scope.mode = KIMAP::FetchJob::FetchScope::FullHeaders;
165         scope.changedSince = 123456789;
166         QTest::newRow("fetch full headers") << false << KIMAP::ImapSet(11, 11) << 1 << scenario << scope << KIMAP::ImapSet{};
167 
168         scenario.clear();
169         scenario << FakeServer::preauth() << "C: A000001 UID FETCH 300:500 (FLAGS UID) (CHANGEDSINCE 12345 VANISHED)"
170                  << "S: * VANISHED (EARLIER) 300:310,405,411"
171                  << "S: * 1 FETCH (UID 404 MODSEQ (65402) FLAGS (\\Seen))"
172                  << "S: * 2 FETCH (UID 406 MODSEQ (75403) FLAGS (\\Deleted))"
173                  << "S: * 4 FETCH (UID 408 MODSEQ (29738) FLAGS ($Nojunk $AutoJunk $MDNSent))"
174                  << "S: A000001 OK Fetch completed";
175         scope.mode = KIMAP::FetchJob::FetchScope::Flags;
176         scope.changedSince = 12345;
177         scope.qresync = true;
178         KIMAP::ImapSet vanished;
179         vanished.add(KIMAP::ImapInterval{300, 310});
180         vanished.add(QVector<qint64>{405, 411});
181         QTest::newRow("qresync") << true << KIMAP::ImapSet(300, 500) << 3 << scenario << scope << vanished;
182     }
183 
testFetch()184     void testFetch()
185     {
186         QFETCH(bool, uidBased);
187         QFETCH(KIMAP::ImapSet, set);
188         QFETCH(int, expectedMessageCount);
189         QFETCH(QList<QByteArray>, scenario);
190         QFETCH(KIMAP::FetchJob::FetchScope, scope);
191         QFETCH(KIMAP::ImapSet, expectedVanished);
192 
193         FakeServer fakeServer;
194         fakeServer.setScenario(scenario);
195         fakeServer.startAndWait();
196 
197         KIMAP::Session session(QStringLiteral("127.0.0.1"), 5989);
198 
199         auto job = new KIMAP::FetchJob(&session);
200         job->setUidBased(uidBased);
201         job->setSequenceSet(set);
202         job->setScope(scope);
203 
204         connect(job,
205                 SIGNAL(headersReceived(QString,
206                                        QMap<qint64, qint64>,
207                                        QMap<qint64, qint64>,
208                                        QMap<qint64, KIMAP::MessageAttribute>,
209                                        QMap<qint64, KIMAP::MessageFlags>,
210                                        QMap<qint64, KIMAP::MessagePtr>)),
211                 this,
212                 SLOT(onHeadersReceived(QString,
213                                        QMap<qint64, qint64>,
214                                        QMap<qint64, qint64>,
215                                        QMap<qint64, KIMAP::MessageAttribute>,
216                                        QMap<qint64, KIMAP::MessageFlags>,
217                                        QMap<qint64, KIMAP::MessagePtr>)));
218         connect(job, &KIMAP::FetchJob::messagesAvailable, this, &FetchJobTest::onMessagesAvailable);
219 
220         QSignalSpy vanishedSpy(job, &KIMAP::FetchJob::messagesVanished);
221         QVERIFY(vanishedSpy.isValid());
222 
223         bool result = job->exec();
224         QEXPECT_FAIL("connection drop", "Expected failure on connection drop", Continue);
225         QEXPECT_FAIL("buffer overwrite", "Expected failure on confused list", Continue);
226         QEXPECT_FAIL("buffer overwrite 2", "Expected beginning of message missing", Continue);
227         QVERIFY(result);
228         if (result) {
229             QVERIFY(m_signals.count() > 0);
230             QCOMPARE(m_uids.count(), expectedMessageCount);
231             QCOMPARE(m_msgs.count(), expectedMessageCount);
232             if (scope.qresync) {
233                 QCOMPARE(vanishedSpy.size(), 1);
234                 QCOMPARE(vanishedSpy.at(0).at(0).value<KIMAP::ImapSet>(), expectedVanished);
235             }
236         }
237 
238         QVERIFY(fakeServer.isAllScenarioDone());
239         fakeServer.quit();
240 
241         m_signals.clear();
242         m_uids.clear();
243         m_sizes.clear();
244         m_flags.clear();
245         m_messages.clear();
246         m_parts.clear();
247         m_attrs.clear();
248         m_msgs.clear();
249     }
250 
testFetchStructure()251     void testFetchStructure()
252     {
253         QList<QByteArray> scenario;
254         scenario
255             << FakeServer::preauth() << "C: A000001 FETCH 1:2 (BODYSTRUCTURE UID)"
256             << R"(S: * 1 FETCH (UID 10 BODYSTRUCTURE ("TEXT" "PLAIN" ("CHARSET" "ISO-8859-1") NIL NIL "7BIT" 5 1 NIL NIL NIL)))"
257             << 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)))"
258             << "S: A000001 OK fetch done";
259 
260         KIMAP::FetchJob::FetchScope scope;
261         scope.mode = KIMAP::FetchJob::FetchScope::Structure;
262 
263         FakeServer fakeServer;
264         fakeServer.setScenario(scenario);
265         fakeServer.startAndWait();
266 
267         KIMAP::Session session(QStringLiteral("127.0.0.1"), 5989);
268 
269         auto job = new KIMAP::FetchJob(&session);
270         job->setUidBased(false);
271         job->setSequenceSet(KIMAP::ImapSet(1, 2));
272         job->setScope(scope);
273 
274         connect(job,
275                 SIGNAL(messagesReceived(QString, QMap<qint64, qint64>, QMap<qint64, KIMAP::MessageAttribute>, QMap<qint64, KIMAP::MessagePtr>)),
276                 this,
277                 SLOT(onMessagesReceived(QString, QMap<qint64, qint64>, QMap<qint64, KIMAP::MessageAttribute>, QMap<qint64, KIMAP::MessagePtr>)));
278         connect(job, &KIMAP::FetchJob::messagesAvailable, this, &FetchJobTest::onMessagesAvailable);
279 
280         bool result = job->exec();
281         QVERIFY(result);
282         QVERIFY(m_signals.count() > 0);
283         QCOMPARE(m_uids.count(), 2);
284         QCOMPARE(m_messages[1]->attachments().count(), 0);
285         QCOMPARE(m_messages[2]->attachments().count(), 1);
286         QCOMPARE(m_messages[2]->contents().size(), 2);
287         QCOMPARE(m_messages[2]->contents()[0]->contents().size(), 2);
288         QCOMPARE(m_messages[2]->attachments().at(0)->contentDisposition()->filename(), QStringLiteral("photo.jpg"));
289         QCOMPARE(m_msgs.count(), 2);
290         QCOMPARE(m_msgs[1].message->attachments().count(), 0);
291         QCOMPARE(m_msgs[2].message->attachments().count(), 1);
292         QCOMPARE(m_msgs[2].message->contents().size(), 2);
293         QCOMPARE(m_msgs[2].message->contents()[0]->contents().size(), 2);
294         QCOMPARE(m_msgs[2].message->attachments().at(0)->contentDisposition()->filename(), QStringLiteral("photo.jpg"));
295 
296         fakeServer.quit();
297 
298         m_signals.clear();
299         m_uids.clear();
300         m_sizes.clear();
301         m_flags.clear();
302         m_messages.clear();
303         m_parts.clear();
304         m_attrs.clear();
305         m_msgs.clear();
306     }
307 
testFetchParts()308     void testFetchParts()
309     {
310         QList<QByteArray> scenario;
311         scenario << FakeServer::preauth()
312                  << "C: A000001 FETCH 2 (BODY.PEEK[HEADER.FIELDS (TO FROM MESSAGE-ID REFERENCES IN-REPLY-TO SUBJECT DATE)] BODY.PEEK[1.1.1.MIME] "
313                     "BODY.PEEK[1.1.1] FLAGS UID)"
314                  << "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 "
315                     "<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 "
316                     "<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; "
317                     "charset=ISO-8859-1\r\n\r\n)\r\n"
318                  << "S: A000001 OK fetch done";
319 
320         KIMAP::FetchJob::FetchScope scope;
321         scope.mode = KIMAP::FetchJob::FetchScope::HeaderAndContent;
322         scope.parts.clear();
323         scope.parts.append("1.1.1");
324 
325         FakeServer fakeServer;
326         fakeServer.setScenario(scenario);
327         fakeServer.startAndWait();
328 
329         KIMAP::Session session(QStringLiteral("127.0.0.1"), 5989);
330 
331         auto job = new KIMAP::FetchJob(&session);
332         job->setUidBased(false);
333         job->setSequenceSet(KIMAP::ImapSet(2, 2));
334         job->setScope(scope);
335 
336         connect(job,
337                 SIGNAL(headersReceived(QString,
338                                        QMap<qint64, qint64>,
339                                        QMap<qint64, qint64>,
340                                        QMap<qint64, KIMAP::MessageAttribute>,
341                                        QMap<qint64, KIMAP::MessageFlags>,
342                                        QMap<qint64, KIMAP::MessagePtr>)),
343                 this,
344                 SLOT(onHeadersReceived(QString,
345                                        QMap<qint64, qint64>,
346                                        QMap<qint64, qint64>,
347                                        QMap<qint64, KIMAP::MessageAttribute>,
348                                        QMap<qint64, KIMAP::MessageFlags>,
349                                        QMap<qint64, KIMAP::MessagePtr>)));
350         connect(job,
351                 SIGNAL(partsReceived(QString, QMap<qint64, qint64>, QMap<qint64, KIMAP::MessageAttribute>, QMap<qint64, KIMAP::MessageParts>)),
352                 this,
353                 SLOT(onPartsReceived(QString, QMap<qint64, qint64>, QMap<qint64, KIMAP::MessageAttribute>, QMap<qint64, KIMAP::MessageParts>)));
354         connect(job, &KIMAP::FetchJob::messagesAvailable, this, &FetchJobTest::onMessagesAvailable);
355         bool result = job->exec();
356 
357         QVERIFY(result);
358         QVERIFY(m_signals.count() > 0);
359         QCOMPARE(m_uids.count(), 1);
360         QCOMPARE(m_parts.count(), 1);
361         QCOMPARE(m_attrs.count(), 0);
362         QCOMPARE(m_msgs.count(), 1);
363 
364         // Check that we received the message header
365         QCOMPARE(m_messages[2]->messageID()->identifier(), QByteArray("1234@example.com"));
366         QCOMPARE(m_msgs[2].message->messageID()->identifier(), QByteArray("1234@example.com"));
367 
368         // Check that we received the flags
369         QMap<qint64, KIMAP::MessageFlags> expectedFlags;
370         expectedFlags.insert(2, KIMAP::MessageFlags() << "\\Seen");
371         QCOMPARE(m_flags, expectedFlags);
372         QCOMPARE(m_msgs[2].flags, expectedFlags[2]);
373 
374         // Check that we didn't received the full message body, since we only requested a specific part
375         QCOMPARE(m_messages[2]->decodedText().length(), 0);
376         QCOMPARE(m_messages[2]->attachments().count(), 0);
377         QCOMPARE(m_msgs[2].message->decodedText().length(), 0);
378         QCOMPARE(m_msgs[2].message->attachments().count(), 0);
379 
380         // Check that we received the part we requested
381         QByteArray partId = m_parts[2].keys().first();
382         QString text = m_parts[2].value(partId)->decodedText(true, true);
383         QCOMPARE(partId, QByteArray("1.1.1"));
384         QCOMPARE(text, QStringLiteral("Hi Jane, nice to meet you!"));
385 
386         QCOMPARE(m_msgs[2].parts.keys().first(), QByteArray("1.1.1"));
387         QCOMPARE(m_msgs[2].parts.value(partId)->decodedText(true, true), QStringLiteral("Hi Jane, nice to meet you!"));
388 
389         fakeServer.quit();
390 
391         m_signals.clear();
392         m_uids.clear();
393         m_sizes.clear();
394         m_flags.clear();
395         m_messages.clear();
396         m_parts.clear();
397         m_attrs.clear();
398         m_msgs.clear();
399     }
400 };
401 
402 QTEST_GUILESS_MAIN(FetchJobTest)
403 
404 #include "fetchjobtest.moc"
405