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