diff --git a/resources/imap/autotests/testexpungecollectiontask.cpp b/resources/imap/autotests/testexpungecollectiontask.cpp index ae0ac2138..3fa8dbdba 100644 --- a/resources/imap/autotests/testexpungecollectiontask.cpp +++ b/resources/imap/autotests/testexpungecollectiontask.cpp @@ -1,125 +1,137 @@ /* Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or ( at your option ) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "imaptestbase.h" #include "expungecollectiontask.h" class TestExpungeCollectionTask : public ImapTestBase { Q_OBJECT private Q_SLOTS: void shouldDeleteMailBox_data() { QTest::addColumn("collection"); QTest::addColumn< QList >("scenario"); QTest::addColumn("callNames"); Akonadi::Collection collection; QList scenario; QStringList callNames; collection = createCollectionChain(QStringLiteral("/INBOX/Foo")); scenario.clear(); scenario << defaultPoolConnectionScenario() << "C: A000003 SELECT \"INBOX/Foo\"" << "S: A000003 OK select done" << "C: A000004 EXPUNGE" << "S: A000004 OK expunge done"; callNames.clear(); callNames << QStringLiteral("taskDone"); QTest::newRow("normal case") << collection << scenario << callNames; // We keep the same collection scenario.clear(); scenario << defaultPoolConnectionScenario() << "C: A000003 SELECT \"INBOX/Foo\"" << "S: A000003 NO select failed"; callNames.clear(); callNames << QStringLiteral("cancelTask"); QTest::newRow("select failed") << collection << scenario << callNames; // We keep the same collection scenario.clear(); scenario << defaultPoolConnectionScenario() << "C: A000003 SELECT \"INBOX/Foo\"" << "S: A000003 OK select done" << "C: A000004 EXPUNGE" << "S: A000004 NO expunge failed"; callNames.clear(); callNames << QStringLiteral("cancelTask"); QTest::newRow("expunge failed") << collection << scenario << callNames; + + // We keep the same collection + + scenario.clear(); + scenario << defaultPoolConnectionScenario() + << "C: A000003 SELECT \"INBOX/Foo\"" + << "S: A000003 OK [READ-ONLY] select done"; + + callNames.clear(); + callNames << QStringLiteral("taskDone"); + + QTest::newRow("read-only mailbox") << collection << scenario << callNames; } void shouldDeleteMailBox() { QFETCH(Akonadi::Collection, collection); QFETCH(QList, scenario); QFETCH(QStringList, callNames); FakeServer server; server.setScenario(scenario); server.startAndWait(); SessionPool pool(1); pool.setPasswordRequester(createDefaultRequester()); QVERIFY(pool.connect(createDefaultAccount())); QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString)))); DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState); state->setCollection(collection); ExpungeCollectionTask *task = new ExpungeCollectionTask(state); task->start(&pool); QTRY_COMPARE(state->calls().count(), callNames.size()); for (int i = 0; i < callNames.size(); i++) { QString command = QString::fromUtf8(state->calls().at(i).first); QVariant parameter = state->calls().at(i).second; if (command == QLatin1String("cancelTask") && callNames[i] != QLatin1String("cancelTask")) { qDebug() << "Got a cancel:" << parameter.toString(); } QCOMPARE(command, callNames[i]); if (command == QLatin1String("cancelTask")) { QVERIFY(!parameter.toString().isEmpty()); } } QVERIFY(server.isAllScenarioDone()); server.quit(); } }; QTEST_GUILESS_MAIN(TestExpungeCollectionTask) #include "testexpungecollectiontask.moc" diff --git a/resources/imap/autotests/testretrieveitemstask.cpp b/resources/imap/autotests/testretrieveitemstask.cpp index 2c2f01d5b..1727df9b7 100644 --- a/resources/imap/autotests/testretrieveitemstask.cpp +++ b/resources/imap/autotests/testretrieveitemstask.cpp @@ -1,614 +1,643 @@ /* Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Kevin Ottens This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or ( at your option ) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "imaptestbase.h" #include "retrieveitemstask.h" #include "uidnextattribute.h" #include #include #include #include #include class TestRetrieveItemsTask : public ImapTestBase { Q_OBJECT private Q_SLOTS: void shouldIntrospectCollection_data() { QTest::addColumn("collection"); QTest::addColumn< QList >("scenario"); QTest::addColumn("callNames"); Akonadi::Collection collection; QList scenario; QStringList callNames; collection = createCollectionChain(QStringLiteral("/INBOX/Foo")); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135); scenario.clear(); scenario << defaultPoolConnectionScenario() << "C: A000003 SELECT \"INBOX/Foo\"" << "S: A000003 OK select done" << "C: A000004 EXPUNGE" << "S: A000004 OK expunge done" << "C: A000005 SELECT \"INBOX/Foo\"" << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)" << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]" << "S: * 1 EXISTS" << "S: * 0 RECENT" << "S: * OK [ UIDVALIDITY 1149151135 ]" << "S: * OK [ UIDNEXT 9 ]" << "S: A000005 OK select done" << "C: A000006 UID SEARCH UID 1:9" << "S: * SEARCH 1 2 3 4 5 6 7 8 9" << "S: A000006 OK search done" << "C: A000007 UID FETCH 1:9 (RFC822.SIZE INTERNALDATE " "BODY.PEEK[HEADER] " "FLAGS UID)" << "S: * 1 FETCH ( FLAGS (\\Seen) UID 7 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" " "RFC822.SIZE 75 BODY[HEADER] {69}\r\n" "From: Foo \r\n" "To: Bar \r\n" "Subject: Test Mail\r\n" "\r\n" " )" << "S: A000007 OK fetch done"; callNames.clear(); callNames << QStringLiteral("itemsRetrieved") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievalDone"); QTest::newRow("first listing, connected IMAP") << collection << scenario << callNames; + scenario.clear(); + scenario << defaultPoolConnectionScenario() + << "C: A000003 SELECT \"INBOX/Foo\"" + << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)" + << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]" + << "S: * 1 EXISTS" + << "S: * 0 RECENT" + << "S: * OK [ UIDVALIDITY 1149151135 ]" + << "S: * OK [ UIDNEXT 9 ]" + << "S: A000003 OK [READ-ONLY] select done" + << "C: A000004 UID SEARCH UID 1:9" + << "S: * SEARCH 1 2 3 4 5 6 7 8 9" + << "S: A000004 OK search done" + << "C: A000005 UID FETCH 1:9 (RFC822.SIZE INTERNALDATE " + "BODY.PEEK[HEADER] " + "FLAGS UID)" + << "S: * 1 FETCH ( FLAGS (\\Seen) UID 7 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" " + "RFC822.SIZE 75 BODY[HEADER] {69}\r\n" + "From: Foo \r\n" + "To: Bar \r\n" + "Subject: Test Mail\r\n" + "\r\n" + " )" + << "S: A000005 OK fetch done"; + callNames.clear(); + callNames << QStringLiteral("itemsRetrieved") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievalDone"); + + QTest::newRow("retrieval from read-only mailbox (no expunge)") << collection << scenario << callNames; + Akonadi::CachePolicy policy; policy.setLocalParts(QStringList() << Akonadi::MessagePart::Envelope << Akonadi::MessagePart::Header << Akonadi::MessagePart::Body); collection = createCollectionChain(QStringLiteral("/INBOX/Foo")); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135); collection.setCachePolicy(policy); scenario.clear(); scenario << defaultPoolConnectionScenario() << "C: A000003 SELECT \"INBOX/Foo\"" << "S: A000003 OK select done" << "C: A000004 EXPUNGE" << "S: A000004 OK expunge done" << "C: A000005 SELECT \"INBOX/Foo\"" << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)" << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]" << "S: * 1 EXISTS" << "S: * 0 RECENT" << "S: * OK [ UIDVALIDITY 1149151135 ]" << "S: * OK [ UIDNEXT 9 ]" << "S: A000005 OK select done" << "C: A000006 UID SEARCH UID 1:9" << "S: * SEARCH 1 2 3 4 5 6 7 8 9" << "S: A000006 OK search done" << "C: A000007 UID FETCH 1:9 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)" << "S: * 1 FETCH ( FLAGS (\\Seen) UID 7 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" " "RFC822.SIZE 75 BODY[] {75}\r\n" "From: Foo \r\n" "To: Bar \r\n" "Subject: Test Mail\r\n" "\r\n" "Test\r\n" " )" << "S: A000007 OK fetch done"; callNames.clear(); callNames << QStringLiteral("itemsRetrieved") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievalDone"); QTest::newRow("first listing, disconnected IMAP") << collection << scenario << callNames; Akonadi::CollectionStatistics stats; stats.setCount(1); collection = createCollectionChain(QStringLiteral("/INBOX/Foo")); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidNext(9); collection.setCachePolicy(policy); collection.setStatistics(stats); scenario.clear(); scenario << defaultPoolConnectionScenario() << "C: A000003 SELECT \"INBOX/Foo\"" << "S: A000003 OK select done" << "C: A000004 EXPUNGE" << "S: A000004 OK expunge done" << "C: A000005 SELECT \"INBOX/Foo\"" << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)" << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]" << "S: * 1 EXISTS" << "S: * 0 RECENT" << "S: * OK [ UIDVALIDITY 1149151135 ]" << "S: * OK [ UIDNEXT 9 ]" << "S: A000005 OK select done" << "C: A000006 UID SEARCH UID 1:9" << "S: * SEARCH 1 2 3 4 5 6 7 8 9" << "S: A000006 OK search done" << "C: A000007 UID FETCH 1:9 (FLAGS UID)" << "S: * 1 FETCH ( FLAGS (\\Seen) UID 7 )" << "S: A000007 OK fetch done"; callNames.clear(); callNames << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievalDone"); //Disabled test since the flag sync is disabled if CONDSTORE is not supported // QTest::newRow( "second listing, checking for flag changes" ) << collection << scenario << callNames; collection = createCollectionChain(QStringLiteral("/INBOX/Foo")); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135); collection.setCachePolicy(policy); stats.setCount(1); collection.setStatistics(stats); scenario.clear(); scenario << defaultPoolConnectionScenario() << "C: A000003 SELECT \"INBOX/Foo\"" << "S: A000003 OK select done" << "C: A000004 EXPUNGE" << "S: A000004 OK expunge done" << "C: A000005 SELECT \"INBOX/Foo\"" << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)" << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]" << "S: * 0 EXISTS" << "S: * 0 RECENT" << "S: * OK [ UIDVALIDITY 1149151135 ]" << "S: * OK [ UIDNEXT 9 ]" << "S: A000005 OK select done"; callNames.clear(); callNames << QStringLiteral("itemsRetrieved") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievalDone"); QTest::newRow("third listing, full sync, empty folder") << collection << scenario << callNames; collection.attribute(Akonadi::Collection::AddIfMissing)->setUidNext(8); stats.setCount(4); collection.setStatistics(stats); collection.attribute(Akonadi::Collection::AddIfMissing)->setHighestModSeq(123456788); scenario.clear(); scenario << defaultPoolConnectionScenario() << "C: A000003 SELECT \"INBOX/Foo\"" << "S: A000003 OK select done" << "C: A000004 EXPUNGE" << "S: A000004 OK expunge done" << "C: A000005 SELECT \"INBOX/Foo\"" << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)" << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]" << "S: * 5 EXISTS" << "S: * 0 RECENT" << "S: * OK [ UIDVALIDITY 1149151135 ]" << "S: * OK [ UIDNEXT 9 ]" << "S: * OK [ HIGHESTMODSEQ 123456789 ]" << "S: A000005 OK select done" << "C: A000006 UID SEARCH UID 8:9" << "S: * SEARCH 8 9" << "S: A000006 OK search done" << "C: A000007 UID FETCH 8:9 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)" << "S: * 5 FETCH ( FLAGS (\\Seen) UID 9 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" " "RFC822.SIZE 75 BODY[] {75}\r\n" "From: Foo \r\n" "To: Bar \r\n" "Subject: Test Mail\r\n" "\r\n" "Test\r\n" " )" << "S: A000007 OK fetch done" << "C: A000008 UID SEARCH UID 1:7" << "S: * SEARCH 1 2 3 4 5 6 7" << "S: A000008 OK search done" << "C: A000009 UID FETCH 1:7 (FLAGS UID)" << "S: * 1 FETCH" << "S: * 2 FETCH" << "S: * 3 FETCH" << "S: * 4 FETCH" << "S: A000009 OK fetch done"; callNames.clear(); callNames << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievalDone"); //We know no messages have been removed, so we can do an incremental update QTest::newRow("uidnext changed, fetch new messages incrementally") << collection << scenario << callNames; collection.attribute(Akonadi::Collection::AddIfMissing)->setUidNext(8); stats.setCount(5); collection.setStatistics(stats); scenario.clear(); scenario << defaultPoolConnectionScenario() << "C: A000003 SELECT \"INBOX/Foo\"" << "S: A000003 OK select done" << "C: A000004 EXPUNGE" << "S: A000004 OK expunge done" << "C: A000005 SELECT \"INBOX/Foo\"" << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)" << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]" << "S: * 5 EXISTS" << "S: * 0 RECENT" << "S: * OK [ UIDVALIDITY 1149151135 ]" << "S: * OK [ UIDNEXT 9 ]" << "S: * OK [ HIGHESTMODSEQ 123456789 ]" << "S: A000005 OK select done" << "C: A000006 UID SEARCH UID 8:9" << "S: * SEARCH 8 9" << "S: A000006 OK search done" << "C: A000007 UID FETCH 8:9 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)" << "S: * 4 FETCH ( FLAGS (\\Seen) UID 8 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" " "RFC822.SIZE 75 BODY[] {75}\r\n" "From: Foo \r\n" "To: Bar \r\n" "Subject: Test Mail\r\n" "\r\n" "Test\r\n" " )" << "S: * 5 FETCH ( FLAGS (\\Seen) UID 9 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" " "RFC822.SIZE 75 BODY[] {75}\r\n" "From: Foo \r\n" "To: Bar \r\n" "Subject: Test Mail\r\n" "\r\n" "Test\r\n" " )" << "S: A000007 OK fetch done" << "C: A000008 UID SEARCH UID 1:7" << "S: * SEARCH 1 2 3 4 5 6 7" << "S: A000008 OK search done" << "C: A000009 UID FETCH 1:7 (FLAGS UID)" << "S: * 1 FETCH" << "S: * 2 FETCH" << "S: * 3 FETCH" << "S: A000009 OK fetch done"; callNames.clear(); callNames << QStringLiteral("itemsRetrieved") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievalDone"); //A new message has been added and an old one removed, we can't do an incremental update QTest::newRow("uidnext changed, fetch new messages and list flags") << collection << scenario << callNames; collection = createCollectionChain(QStringLiteral("/INBOX/Foo")); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135); collection.setCachePolicy(policy); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidNext(9); collection.attribute(Akonadi::Collection::AddIfMissing)->setHighestModSeq(123456789); stats.setCount(5); collection.setStatistics(stats); scenario.clear(); scenario << defaultPoolConnectionScenario(QList() << "CONDSTORE") << "C: A000003 SELECT \"INBOX/Foo\" (CONDSTORE)" << "S: A000003 OK select done" << "C: A000004 EXPUNGE" << "S: A000004 OK expunge DONE" << "C: A000005 SELECT \"INBOX/Foo\" (CONDSTORE)" << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)" << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]" << "S: * 5 EXISTS" << "S: * 0 RECENT" << "S: * OK [ UIDVALIDITY 1149151135 ]" << "S: * OK [ UIDNEXT 9 ]" << "S: * OK [ HIGHESTMODSEQ 123456789 ]" << "S: A000005 OK select done"; callNames.clear(); callNames << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievalDone"); //No flags have changed QTest::newRow("highestmodseq test") << collection << scenario << callNames; collection = createCollectionChain(QStringLiteral("/INBOX/Foo")); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135); collection.setCachePolicy(policy); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidNext(9); collection.attribute(Akonadi::Collection::AddIfMissing)->setHighestModSeq(123456788); stats.setCount(5); collection.setStatistics(stats); scenario.clear(); scenario << defaultPoolConnectionScenario(QList() << "CONDSTORE") << "C: A000003 SELECT \"INBOX/Foo\" (CONDSTORE)" << "S: A000003 OK select done" << "C: A000004 EXPUNGE" << "S: A000004 OK expunge DONE" << "C: A000005 SELECT \"INBOX/Foo\" (CONDSTORE)" << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)" << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]" << "S: * 5 EXISTS" << "S: * 0 RECENT" << "S: * OK [ UIDVALIDITY 1149151135 ]" << "S: * OK [ UIDNEXT 9 ]" << "S: * OK [ HIGHESTMODSEQ 123456789 ]" << "S: A000005 OK select done" << "C: A000006 UID FETCH 1:9 (FLAGS UID) (CHANGEDSINCE 123456788)" << "S: * 5 FETCH ( UID 8 FLAGS () )" << "S: A000006 OK fetch done"; callNames.clear(); callNames << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievalDone"); //fetch only changed flags QTest::newRow("changedsince test") << collection << scenario << callNames; collection = createCollectionChain(QStringLiteral("/INBOX/Foo")); collection.setCachePolicy(policy); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidNext(9); collection.attribute(Akonadi::Collection::AddIfMissing)->setHighestModSeq(123456788); stats.setCount(5); collection.setStatistics(stats); scenario.clear(); scenario << defaultPoolConnectionScenario(QList() << "XYMHIGHESTMODSEQ") << "C: A000003 SELECT \"INBOX/Foo\"" << "S: A000003 OK select done" << "C: A000004 EXPUNGE" << "S: A000004 OK expunge DONE" << "C: A000005 SELECT \"INBOX/Foo\"" << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)" << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]" << "S: * 5 EXISTS" << "S: * 0 RECENT" << "S: * OK [ UIDVALIDITY 1149151135 ]" << "S: * OK [ UIDNEXT 9 ]" << "S: * OK [ HIGHESTMODSEQ 123456789 ]" << "S: A000005 OK select done"; //Disabled since the flag sync is disabled if CONDSTORE is not supported // << "C: A000006 UID SEARCH UID 1:9" // << "S: * SEARCH 1 2 3 4 5 6 7 8 9" // << "S: A000006 OK search done" // << "C: A000007 UID FETCH 1:9 (FLAGS UID)" // << "S: * 5 FETCH ( UID 8 FLAGS () )" // << "S: A000007 OK fetch done"; callNames.clear(); //Disabled since the flag sync is disabled if CONDSTORE is not supported callNames << /*"itemsRetrievedIncremental" << */ QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievalDone"); //Don't rely on yahoos highestmodseq implementation QTest::newRow("yahoo highestmodseq test") << collection << scenario << callNames; collection = createCollectionChain(QStringLiteral("/INBOX/Foo")); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidNext(9); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidValidity(3); collection.setCachePolicy(policy); stats.setCount(1); collection.setStatistics(stats); scenario.clear(); scenario << defaultPoolConnectionScenario() << "C: A000003 SELECT \"INBOX/Foo\"" << "S: A000003 OK select done" << "C: A000004 EXPUNGE" << "S: A000004 OK expunge done" << "C: A000005 SELECT \"INBOX/Foo\"" << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)" << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]" << "S: * 1 EXISTS" << "S: * 0 RECENT" << "S: * OK [ UIDVALIDITY 1149151135 ]" << "S: * OK [ UIDNEXT 9 ]" << "S: A000005 OK select done" << "C: A000006 UID SEARCH UID 1:9" << "S: * SEARCH 1 2 3 4 5 6 7 8 9" << "S: A000006 OK search done" << "C: A000007 UID FETCH 1:9 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)" << "S: * 1 FETCH ( FLAGS (\\Seen) UID 2321 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" " "RFC822.SIZE 75 BODY[] {75}\r\n" "From: Foo \r\n" "To: Bar \r\n" "Subject: Test Mail\r\n" "\r\n" "Test\r\n" " )" << "S: A000007 OK fetch done"; callNames.clear(); callNames << QStringLiteral("itemsRetrieved") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievalDone"); QTest::newRow("uidvalidity changed") << collection << scenario << callNames; collection = createCollectionChain(QStringLiteral("/INBOX/Foo")); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidNext(105); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135); collection.setCachePolicy(policy); stats.setCount(104); collection.setStatistics(stats); scenario.clear(); scenario << defaultPoolConnectionScenario() << "C: A000003 SELECT \"INBOX/Foo\"" << "S: A000003 OK select done" << "C: A000004 EXPUNGE" << "S: A000004 OK expunge done" << "C: A000005 SELECT \"INBOX/Foo\"" << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)" << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]" << "S: * 119 EXISTS" << "S: * 0 RECENT" << "S: * OK [ UIDVALIDITY 1149151135 ]" << "S: * OK [ UIDNEXT 120 ]" << "S: A000005 OK select done" << "C: A000006 UID SEARCH UID 105:120" //We asked for until 120 but only 119 is available (120 is uidnext) << "S: * SEARCH 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119" << "S: A000006 OK search done" << "C: A000007 UID FETCH 105:114 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)" << "S: * 1 FETCH ( FLAGS (\\Seen) UID 105 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" " "RFC822.SIZE 75 BODY[] {75}\r\n" "From: Foo \r\n" "To: Bar \r\n" "Subject: Test Mail\r\n" "\r\n" "Test\r\n" " )" //9 more would follow but are excluded for clarity << "S: A000007 OK fetch done" << "C: A000008 UID FETCH 115:119 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)" << "S: * 1 FETCH ( FLAGS (\\Seen) UID 115 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" " "RFC822.SIZE 75 BODY[] {75}\r\n" "From: Foo \r\n" "To: Bar \r\n" "Subject: Test Mail\r\n" "\r\n" "Test\r\n" " )" //4 more would follow but are excluded for clarity << "S: A000008 OK fetch done" << "C: A000009 UID SEARCH UID 1:104" << "S: * SEARCH 1 2 99 100" << "S: A000009 OK search done" << "C: A000010 UID FETCH 1:2,99:100 (FLAGS UID)" << "S: * 1 FETCH ( FLAGS (\\Seen) UID 1 )" //3 more would follow but are excluded for clarity << "S: A000010 OK fetch done"; callNames.clear(); callNames << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral( "applyCollectionChanges") << QStringLiteral("itemsRetrievedIncremental") << QStringLiteral("itemsRetrievalDone"); QTest::newRow("test batch processing") << collection << scenario << callNames; collection = createCollectionChain(QStringLiteral("/INBOX/Foo")); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135); collection.setCachePolicy(policy); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidNext(9); collection.attribute(Akonadi::Collection::AddIfMissing)->setHighestModSeq(123456789); stats.setCount(5); collection.setStatistics(stats); scenario.clear(); scenario << defaultPoolConnectionScenario(QList() << "CONDSTORE") << "C: A000003 SELECT \"INBOX/Foo\" (CONDSTORE)" << "S: A000003 OK select done" << "C: A000004 EXPUNGE" << "S: A000004 OK expunge DONE" << "C: A000005 SELECT \"INBOX/Foo\" (CONDSTORE)" << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)" << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]" << "S: * 4 EXISTS" << "S: * 0 RECENT" << "S: * OK [ UIDVALIDITY 1149151135 ]" << "S: * OK [ UIDNEXT 9 ]" << "S: * OK [ HIGHESTMODSEQ 123456789 ]" << "S: A000005 OK select done" << "C: A000006 UID SEARCH UID 1:9" << "S: * SEARCH 1 2 3 4" << "S: A000006 OK search done" << "C: A000007 UID FETCH 1:4 (FLAGS UID)" << "S: * 1 FETCH ( FLAGS (\\Seen) UID 1 )" << "S: * 2 FETCH ( FLAGS (\\Seen) UID 2 )" << "S: * 3 FETCH ( FLAGS (\\Seen) UID 3 )" << "S: * 4 FETCH ( FLAGS (\\Seen) UID 4 )" << "S: A000007 OK fetch done"; callNames.clear(); callNames << QStringLiteral("itemsRetrieved") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievalDone"); //fetch only changed flags QTest::newRow("remote message deleted") << collection << scenario << callNames; collection = createCollectionChain(QStringLiteral("/INBOX/Foo")); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidValidity(1149151135); collection.setCachePolicy(policy); collection.attribute(Akonadi::Collection::AddIfMissing)->setUidNext(-1); collection.attribute(Akonadi::Collection::AddIfMissing)->setHighestModSeq(123456789); stats.setCount(0); collection.setStatistics(stats); scenario.clear(); scenario << defaultPoolConnectionScenario() << "C: A000003 SELECT \"INBOX/Foo\"" << "S: A000003 OK select done" << "C: A000004 EXPUNGE" << "S: A000004 OK expunge done" << "C: A000005 SELECT \"INBOX/Foo\"" << "S: * FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)" << "S: * OK [ PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen) ]" << "S: * 1 EXISTS" << "S: * 0 RECENT" << "S: * OK [ UIDVALIDITY 1149151135 ]" << "S: A000005 OK select done" << "C: A000006 STATUS \"INBOX/Foo\" (UIDNEXT)" << "S: * STATUS \"INBOX/Foo\" (UIDNEXT 10)" << "S: A000006 OK status done" << "C: A000007 UID SEARCH UID 1:10" << "S: * SEARCH 1 2 3 4 5 6 7 8 9" << "S: A000007 OK search done" << "C: A000008 UID FETCH 1:9 (RFC822.SIZE INTERNALDATE BODY.PEEK[] FLAGS UID)" << "S: * 1 FETCH ( FLAGS (\\Seen) UID 2321 INTERNALDATE \"29-Jun-2010 15:26:42 +0200\" " "RFC822.SIZE 75 BODY[] {75}\r\n" "From: Foo \r\n" "To: Bar \r\n" "Subject: Test Mail\r\n" "\r\n" "Test\r\n" " )" << "S: A000008 OK fetch done"; callNames.clear(); callNames << QStringLiteral("itemsRetrieved") << QStringLiteral("applyCollectionChanges") << QStringLiteral("itemsRetrievalDone"); QTest::newRow("missing uidnext") << collection << scenario << callNames; } void shouldIntrospectCollection() { QFETCH(Akonadi::Collection, collection); QFETCH(QList, scenario); QFETCH(QStringList, callNames); FakeServer server; server.setScenario(scenario); server.startAndWait(); SessionPool pool(1); pool.setPasswordRequester(createDefaultRequester()); QVERIFY(pool.connect(createDefaultAccount())); QVERIFY(waitForSignal(&pool, SIGNAL(connectDone(int,QString)))); DummyResourceState::Ptr state = DummyResourceState::Ptr(new DummyResourceState); state->setServerCapabilities(pool.serverCapabilities()); state->setCollection(collection); RetrieveItemsTask *task = new RetrieveItemsTask(state); task->setFetchMissingItemBodies(false); task->start(&pool); QTRY_COMPARE(state->calls().count(), callNames.size()); qDebug() << state->calls(); for (int i = 0; i < callNames.size(); i++) { QString command = QString::fromUtf8(state->calls().at(i).first); QVariant parameter = state->calls().at(i).second; if (command == QLatin1String("cancelTask") && callNames[i] != QLatin1String("cancelTask")) { qDebug() << "Got a cancel:" << parameter.toString(); } QCOMPARE(command, callNames[i]); if (command == QLatin1String("cancelTask")) { QVERIFY(!parameter.toString().isEmpty()); } } QVERIFY(server.isAllScenarioDone()); server.quit(); } }; QTEST_GUILESS_MAIN(TestRetrieveItemsTask) #include "testretrieveitemstask.moc" diff --git a/resources/imap/expungecollectiontask.cpp b/resources/imap/expungecollectiontask.cpp index 09b186c8c..5b8a64531 100644 --- a/resources/imap/expungecollectiontask.cpp +++ b/resources/imap/expungecollectiontask.cpp @@ -1,93 +1,98 @@ /* Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Kevin Ottens This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "expungecollectiontask.h" #include "imapresource_debug.h" #include #include #include #include "noselectattribute.h" ExpungeCollectionTask::ExpungeCollectionTask(const ResourceStateInterface::Ptr &resource, QObject *parent) : ResourceTask(CancelIfNoSession, resource, parent) { } ExpungeCollectionTask::~ExpungeCollectionTask() { } void ExpungeCollectionTask::doStart(KIMAP::Session *session) { // Prevent expunging items from noselect folders. if (collection().hasAttribute("noselect")) { NoSelectAttribute *noselect = static_cast(collection().attribute("noselect")); if (noselect->noSelect()) { qCDebug(IMAPRESOURCE_LOG) << "No Select folder"; taskDone(); return; } } const QString mailBox = mailBoxForCollection(collection()); if (session->selectedMailBox() != mailBox) { KIMAP::SelectJob *select = new KIMAP::SelectJob(session); select->setMailBox(mailBox); connect(select, &KIMAP::SelectJob::result, this, &ExpungeCollectionTask::onSelectDone); select->start(); } else { triggerExpungeJob(session); } } void ExpungeCollectionTask::onSelectDone(KJob *job) { if (job->error()) { cancelTask(job->errorString()); } else { KIMAP::SelectJob *select = static_cast(job); - triggerExpungeJob(select->session()); + if (select->isOpenReadOnly()) { + qCDebug(IMAPRESOURCE_LOG) << "Mailbox is opened readonly, not expunging"; + taskDone(); + } else { + triggerExpungeJob(select->session()); + } } } void ExpungeCollectionTask::triggerExpungeJob(KIMAP::Session *session) { KIMAP::ExpungeJob *expunge = new KIMAP::ExpungeJob(session); connect(expunge, &KIMAP::ExpungeJob::result, this, &ExpungeCollectionTask::onExpungeDone); expunge->start(); } void ExpungeCollectionTask::onExpungeDone(KJob *job) { if (job->error()) { cancelTask(job->errorString()); } else { taskDone(); } } diff --git a/resources/imap/retrieveitemstask.cpp b/resources/imap/retrieveitemstask.cpp index acae5f5e7..4a40950cf 100644 --- a/resources/imap/retrieveitemstask.cpp +++ b/resources/imap/retrieveitemstask.cpp @@ -1,639 +1,645 @@ /* Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Kevin Ottens Copyright (c) 2014 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "retrieveitemstask.h" #include "collectionflagsattribute.h" #include "noselectattribute.h" #include "uidvalidityattribute.h" #include "uidnextattribute.h" #include "highestmodseqattribute.h" #include "messagehelper.h" #include "batchfetcher.h" #include #include #include #include #include #include #include #include "imapresource_debug.h" #include #include #include #include #include #include #include RetrieveItemsTask::RetrieveItemsTask(const ResourceStateInterface::Ptr &resource, QObject *parent) : ResourceTask(CancelIfNoSession, resource, parent) , m_session(nullptr) , m_fetchedMissingBodies(-1) , m_fetchMissingBodies(false) , m_incremental(true) , m_localHighestModSeq(-1) , m_batchFetcher(nullptr) , m_uidBasedFetch(true) , m_flagsChanged(false) , m_messageCount(-1) , m_uidValidity(-1) , m_nextUid(-1) , m_highestModSeq(-1) { } RetrieveItemsTask::~RetrieveItemsTask() { } void RetrieveItemsTask::setFetchMissingItemBodies(bool enabled) { m_fetchMissingBodies = enabled; } void RetrieveItemsTask::doStart(KIMAP::Session *session) { emitPercent(0); // Prevent fetching items from noselect folders. if (collection().hasAttribute("noselect")) { NoSelectAttribute *noselect = static_cast(collection().attribute("noselect")); if (noselect->noSelect()) { qCDebug(IMAPRESOURCE_LOG) << "No Select folder"; itemsRetrievalDone(); return; } } m_session = session; const Akonadi::Collection col = collection(); // Only with emails we can be sure that RID is persistent and thus we can use // it for merging. For other potential content types (like Kolab events etc.) // use GID instead. QStringList cts = col.contentMimeTypes(); cts.removeOne(Akonadi::Collection::mimeType()); cts.removeOne(KMime::Message::mimeType()); if (!cts.isEmpty()) { setItemMergingMode(Akonadi::ItemSync::GIDMerge); } else { setItemMergingMode(Akonadi::ItemSync::RIDMerge); } if (m_fetchMissingBodies && col.cachePolicy() .localParts().contains(QLatin1String(Akonadi::MessagePart::Body))) { //disconnected mode, make sure we really have the body cached Akonadi::Session *session = new Akonadi::Session(resourceName().toLatin1() + "_body_checker", this); Akonadi::ItemFetchJob *fetchJob = new Akonadi::ItemFetchJob(col, session); fetchJob->fetchScope().setCheckForCachedPayloadPartsOnly(); fetchJob->fetchScope().fetchPayloadPart(Akonadi::MessagePart::Body); fetchJob->fetchScope().setFetchModificationTime(false); connect(fetchJob, &Akonadi::ItemFetchJob::result, this, &RetrieveItemsTask::fetchItemsWithoutBodiesDone); connect(fetchJob, &Akonadi::ItemFetchJob::result, session, &Akonadi::Session::deleteLater); } else { startRetrievalTasks(); } } BatchFetcher *RetrieveItemsTask::createBatchFetcher(MessageHelper::Ptr messageHelper, const KIMAP::ImapSet &set, const KIMAP::FetchJob::FetchScope &scope, int batchSize, KIMAP::Session *session) { return new BatchFetcher(messageHelper, set, scope, batchSize, session); } void RetrieveItemsTask::fetchItemsWithoutBodiesDone(KJob *job) { QVector uids; if (job->error()) { qCWarning(IMAPRESOURCE_LOG) << job->errorString(); cancelTask(job->errorString()); return; } else { int i = 0; Akonadi::ItemFetchJob *fetch = static_cast(job); const Akonadi::Item::List lstItems = fetch->items(); for (const Akonadi::Item &item : lstItems) { if (!item.cachedPayloadParts().contains(Akonadi::MessagePart::Body)) { qCWarning(IMAPRESOURCE_LOG) << "Item " << item.id() << " is missing the payload! Cached payloads: " << item.cachedPayloadParts(); uids.append(item.remoteId().toInt()); i++; } } if (i > 0) { qCWarning(IMAPRESOURCE_LOG) << "Number of items missing the body: " << i; } } onFetchItemsWithoutBodiesDone(uids); } void RetrieveItemsTask::onFetchItemsWithoutBodiesDone(const QVector &items) { m_messageUidsMissingBody = items; startRetrievalTasks(); } void RetrieveItemsTask::startRetrievalTasks() { const QString mailBox = mailBoxForCollection(collection()); qCDebug(IMAPRESOURCE_LOG) << "Starting retrieval for " << mailBox; m_time.start(); // Now is the right time to expunge the messages marked \\Deleted from this mailbox. const bool hasACL = serverCapabilities().contains(QLatin1String("ACL")); const KIMAP::Acl::Rights rights = myRights(collection()); if (isAutomaticExpungeEnabled() && (!hasACL || (rights &KIMAP::Acl::Expunge) || (rights & KIMAP::Acl::Delete))) { if (m_session->selectedMailBox() != mailBox) { triggerPreExpungeSelect(mailBox); } else { triggerExpunge(mailBox); } } else { // Always select to get the stats updated triggerFinalSelect(mailBox); } } void RetrieveItemsTask::triggerPreExpungeSelect(const QString &mailBox) { KIMAP::SelectJob *select = new KIMAP::SelectJob(m_session); select->setMailBox(mailBox); select->setCondstoreEnabled(serverSupportsCondstore()); connect(select, &KJob::result, this, &RetrieveItemsTask::onPreExpungeSelectDone); select->start(); } void RetrieveItemsTask::onPreExpungeSelectDone(KJob *job) { if (job->error()) { qCWarning(IMAPRESOURCE_LOG) << job->errorString(); cancelTask(job->errorString()); } else { KIMAP::SelectJob *select = static_cast(job); - triggerExpunge(select->mailBox()); + if (select->isOpenReadOnly()) { + qCDebug(IMAPRESOURCE_LOG) << "Mailbox is opened readonly, not expunging"; + // Treat this SELECT as if it was triggerFinalSelect() + onFinalSelectDone(job); + } else { + triggerExpunge(select->mailBox()); + } } } void RetrieveItemsTask::triggerExpunge(const QString &mailBox) { Q_UNUSED(mailBox); KIMAP::ExpungeJob *expunge = new KIMAP::ExpungeJob(m_session); connect(expunge, &KJob::result, this, &RetrieveItemsTask::onExpungeDone); expunge->start(); } void RetrieveItemsTask::onExpungeDone(KJob *job) { // We can ignore the error, we just had a wrong expunge so some old messages will just reappear. // TODO we should probably hide messages that are marked as deleted (skipping will not work because we rely on the message count) if (job->error()) { qCWarning(IMAPRESOURCE_LOG) << "Expunge failed: " << job->errorString(); } // Except for network errors. if (job->error() && m_session->state() == KIMAP::Session::Disconnected) { cancelTask(job->errorString()); return; } // We have to re-select the mailbox to update all the stats after the expunge // (the EXPUNGE command doesn't return enough for our needs) triggerFinalSelect(m_session->selectedMailBox()); } void RetrieveItemsTask::triggerFinalSelect(const QString &mailBox) { KIMAP::SelectJob *select = new KIMAP::SelectJob(m_session); select->setMailBox(mailBox); select->setCondstoreEnabled(serverSupportsCondstore()); connect(select, &KJob::result, this, &RetrieveItemsTask::onFinalSelectDone); select->start(); } void RetrieveItemsTask::onFinalSelectDone(KJob *job) { KIMAP::SelectJob *select = qobject_cast(job); if (job->error()) { qCWarning(IMAPRESOURCE_LOG) << select->mailBox() << ":" << job->errorString(); cancelTask(select->mailBox() + QStringLiteral(" : ") + job->errorString()); return; } m_mailBox = select->mailBox(); m_messageCount = select->messageCount(); m_uidValidity = select->uidValidity(); m_nextUid = select->nextUid(); m_highestModSeq = select->highestModSequence(); m_flags = select->permanentFlags(); //This is known to happen with Courier IMAP. if (m_nextUid < 0) { KIMAP::StatusJob *status = new KIMAP::StatusJob(m_session); status->setMailBox(m_mailBox); status->setDataItems({ "UIDNEXT" }); connect(status, &KJob::result, this, &RetrieveItemsTask::onStatusDone); status->start(); } else { prepareRetrieval(); } } void RetrieveItemsTask::onStatusDone(KJob *job) { if (job->error()) { qCWarning(IMAPRESOURCE_LOG) << job->errorString(); cancelTask(job->errorString()); return; } KIMAP::StatusJob *status = qobject_cast(job); const QList > results = status->status(); for (const auto &val : results) { if (val.first == "UIDNEXT") { m_nextUid = val.second; break; } } prepareRetrieval(); } void RetrieveItemsTask::prepareRetrieval() { // Handle invalid UIDNEXT in case even STATUS is not able to retrieve it if (m_nextUid < 0) { qCWarning(IMAPRESOURCE_LOG) << "Server bug: Your IMAP Server delivered an invalid UIDNEXT value."; m_nextUid = 0; } //The select job retrieves highestmodseq whenever it's available, but in case of no CONDSTORE support we ignore it if (!serverSupportsCondstore()) { m_localHighestModSeq = 0; } Akonadi::Collection col = collection(); bool modifyNeeded = false; // Get the current uid validity value and store it int oldUidValidity = 0; if (!col.hasAttribute("uidvalidity")) { UidValidityAttribute *currentUidValidity = new UidValidityAttribute(m_uidValidity); col.addAttribute(currentUidValidity); modifyNeeded = true; } else { UidValidityAttribute *currentUidValidity = static_cast(col.attribute("uidvalidity")); oldUidValidity = currentUidValidity->uidValidity(); if (oldUidValidity != m_uidValidity) { currentUidValidity->setUidValidity(m_uidValidity); modifyNeeded = true; } } // Get the current uid next value and store it int oldNextUid = 0; if (m_nextUid > 0) { //this can fail with faulty servers that don't deliver uidnext if (UidNextAttribute *currentNextUid = col.attribute()) { oldNextUid = currentNextUid->uidNext(); if (oldNextUid != m_nextUid) { currentNextUid->setUidNext(m_nextUid); modifyNeeded = true; } } else { col.attribute(Akonadi::Collection::AddIfMissing)->setUidNext(m_nextUid); modifyNeeded = true; } } // Store the mailbox flags if (!col.hasAttribute("collectionflags")) { Akonadi::CollectionFlagsAttribute *flagsAttribute = new Akonadi::CollectionFlagsAttribute(m_flags); col.addAttribute(flagsAttribute); modifyNeeded = true; } else { Akonadi::CollectionFlagsAttribute *flagsAttribute = static_cast(col.attribute("collectionflags")); const QList oldFlags = flagsAttribute->flags(); if (oldFlags != m_flags) { flagsAttribute->setFlags(m_flags); modifyNeeded = true; } } qint64 oldHighestModSeq = 0; if (serverSupportsCondstore() && m_highestModSeq > 0) { if (!col.hasAttribute("highestmodseq")) { HighestModSeqAttribute *attr = new HighestModSeqAttribute(m_highestModSeq); col.addAttribute(attr); modifyNeeded = true; } else { HighestModSeqAttribute *attr = col.attribute(); if (attr->highestModSequence() < m_highestModSeq) { oldHighestModSeq = attr->highestModSequence(); attr->setHighestModSeq(m_highestModSeq); modifyNeeded = true; } else if (attr->highestModSequence() == m_highestModSeq) { oldHighestModSeq = attr->highestModSequence(); } else if (attr->highestModSequence() > m_highestModSeq) { // This situation should not happen. If it does, update the highestModSeq // attribute, but rather do a full sync attr->setHighestModSeq(m_highestModSeq); modifyNeeded = true; } } } m_localHighestModSeq = oldHighestModSeq; if (modifyNeeded) { m_modifiedCollection = col; } KIMAP::FetchJob::FetchScope scope; scope.parts.clear(); scope.mode = KIMAP::FetchJob::FetchScope::FullHeaders; if (col.cachePolicy() .localParts().contains(QLatin1String(Akonadi::MessagePart::Body))) { scope.mode = KIMAP::FetchJob::FetchScope::Full; } const qint64 realMessageCount = col.statistics().count(); qCDebug(IMAPRESOURCE_LOG) << "Starting message retrieval. Elapsed(ms): " << m_time.elapsed(); qCDebug(IMAPRESOURCE_LOG) << "MessageCount: " << m_messageCount << "Local message count: " << realMessageCount; qCDebug(IMAPRESOURCE_LOG) << "UidNext: " << m_nextUid << "Local UidNext: " << oldNextUid; qCDebug(IMAPRESOURCE_LOG) << "HighestModSeq: " << m_highestModSeq << "Local HighestModSeq: " << oldHighestModSeq; /* * A synchronization has 3 mandatory steps: * * If uidvalidity changed the local cache must be invalidated * * New messages can be fetched usin uidNext and the last known fetched uid * * flag changes and removals can be detected by listing all messages that weren't part of the previous step * * Everything else is optimizations. * * TODO: Note that the local message count can be larger than the remote message count although no messages * have been deleted remotely, if we locally have messages that were not yet uploaded. * We cannot differentiate that from remotely removed messages, so we have to do a full flag * listing in that case. This can be optimized once we support QRESYNC and therefore have a way * to determine whether messages have been removed. */ if (m_messageCount == 0) { //Shortcut: //If no messages are present on the server, clear local cash and finish m_incremental = false; if (realMessageCount > 0) { qCDebug(IMAPRESOURCE_LOG) << "No messages present so we are done, deleting local messages."; itemsRetrieved(Akonadi::Item::List()); } else { qCDebug(IMAPRESOURCE_LOG) << "No messages present so we are done"; } taskComplete(); } else if (oldUidValidity != m_uidValidity || m_nextUid <= 0) { //If uidvalidity has changed our local cache is worthless and has to be refetched completely if (oldUidValidity != 0 && oldUidValidity != m_uidValidity) { qCDebug(IMAPRESOURCE_LOG) << "UIDVALIDITY check failed (" << oldUidValidity << "|" << m_uidValidity << ")"; } if (m_nextUid <= 0) { qCDebug(IMAPRESOURCE_LOG) << "Invalid UIDNEXT"; } qCDebug(IMAPRESOURCE_LOG) << "Fetching complete mailbox " << m_mailBox; setTotalItems(m_messageCount); retrieveItems(KIMAP::ImapSet(1, m_nextUid), scope, false, true); } else if (m_nextUid <= 0) { //This is a compatibility codepath for Courier IMAP. It probably introduces problems, but at least it syncs. //Since we don't have uidnext available, we simply use the messagecount. This will miss simultaneously added/removed messages. //qCDebug(IMAPRESOURCE_LOG) << "Running courier imap compatibility codepath"; if (m_messageCount > realMessageCount) { //Get new messages retrieveItems(KIMAP::ImapSet(realMessageCount + 1, m_messageCount), scope, false, false); } else if (m_messageCount == realMessageCount) { m_uidBasedFetch = false; m_incremental = true; setTotalItems(m_messageCount); listFlagsForImapSet(KIMAP::ImapSet(1, m_messageCount)); } else { m_uidBasedFetch = false; m_incremental = false; setTotalItems(m_messageCount); listFlagsForImapSet(KIMAP::ImapSet(1, m_messageCount)); } } else if (!m_messageUidsMissingBody.isEmpty()) { //fetch missing uids m_fetchedMissingBodies = 0; setTotalItems(m_messageUidsMissingBody.size()); KIMAP::ImapSet imapSet; imapSet.add(m_messageUidsMissingBody); retrieveItems(imapSet, scope, true, true); } else if (m_nextUid > oldNextUid && ((realMessageCount + m_nextUid - oldNextUid) == m_messageCount) && realMessageCount > 0) { //Optimization: //New messages are available, but we know no messages have been removed. //Fetch new messages, and then check for changed flags and removed messages //We can make an incremental update and use modseq. qCDebug(IMAPRESOURCE_LOG) << "Incrementally fetching new messages: UidNext: " << m_nextUid << " Old UidNext: " << oldNextUid << " message count " << m_messageCount << realMessageCount; setTotalItems(qMax(1ll, m_messageCount - realMessageCount)); m_flagsChanged = !(m_highestModSeq == oldHighestModSeq); retrieveItems(KIMAP::ImapSet(qMax(1, oldNextUid), m_nextUid), scope, true, true); } else if (m_nextUid > oldNextUid && m_messageCount > (realMessageCount + m_nextUid - oldNextUid) && realMessageCount > 0) { //Error recovery: //New messages are available, but not enough to justify the difference between the local and remote message count. //This can be triggered if we i.e. clear the local cache, but the keep the annotations. //If we didn't catch this case, we end up inserting flags only for every missing message. qCWarning(IMAPRESOURCE_LOG) << "Detected inconsistency in local cache, we're missing some messages. Server: " << m_messageCount << " Local: " << realMessageCount; qCWarning(IMAPRESOURCE_LOG) << "Refetching complete mailbox."; setTotalItems(m_messageCount); retrieveItems(KIMAP::ImapSet(1, m_nextUid), scope, false, true); } else if (m_nextUid > oldNextUid) { //New messages are available. Fetch new messages, and then check for changed flags and removed messages qCDebug(IMAPRESOURCE_LOG) << "Fetching new messages: UidNext: " << m_nextUid << " Old UidNext: " << oldNextUid; setTotalItems(m_messageCount); retrieveItems(KIMAP::ImapSet(qMax(1, oldNextUid), m_nextUid), scope, false, true); } else if (m_messageCount == realMessageCount && oldNextUid == m_nextUid) { //Optimization: //We know no messages were added or removed (if the message count and uidnext is still the same) //We only check the flags incrementally and can make use of modseq m_uidBasedFetch = true; m_incremental = true; m_flagsChanged = !(m_highestModSeq == oldHighestModSeq); //Workaround: If the server doesn't support CONDSTORE we would end up syncing all flags during every sync. //Instead we only sync flags when new messages are available or removed and skip this step. //WARNING: This sacrifices consistency as we will not detect flag changes until a new message enters the mailbox. if (m_incremental && !serverSupportsCondstore()) { qCDebug(IMAPRESOURCE_LOG) << "Avoiding flag sync due to missing CONDSTORE support"; taskComplete(); return; } setTotalItems(m_messageCount); listFlagsForImapSet(KIMAP::ImapSet(1, m_nextUid)); } else if (m_messageCount > realMessageCount) { //Error recovery: //We didn't detect any new messages based on the uid, but according to the message count there are new ones. //Our local cache is invalid and has to be refetched. qCWarning(IMAPRESOURCE_LOG) << "Detected inconsistency in local cache, we're missing some messages. Server: " << m_messageCount << " Local: " << realMessageCount; qCWarning(IMAPRESOURCE_LOG) << "Refetching complete mailbox."; setTotalItems(m_messageCount); retrieveItems(KIMAP::ImapSet(1, m_nextUid), scope, false, true); } else { //Shortcut: //No new messages are available. Directly check for changed flags and removed messages. m_uidBasedFetch = true; m_incremental = false; setTotalItems(m_messageCount); listFlagsForImapSet(KIMAP::ImapSet(1, m_nextUid)); } } void RetrieveItemsTask::retrieveItems(const KIMAP::ImapSet &set, const KIMAP::FetchJob::FetchScope &scope, bool incremental, bool uidBased) { Q_ASSERT(set.intervals().size() == 1); m_incremental = incremental; m_uidBasedFetch = uidBased; m_batchFetcher = createBatchFetcher(resourceState()->messageHelper(), set, scope, batchSize(), m_session); m_batchFetcher->setUidBased(m_uidBasedFetch); if (m_uidBasedFetch && set.intervals().size() == 1) { m_batchFetcher->setSearchUids(set.intervals().front()); } m_batchFetcher->setProperty("alreadyFetched", set.intervals().at(0).begin()); connect(m_batchFetcher, &BatchFetcher::itemsRetrieved, this, &RetrieveItemsTask::onItemsRetrieved); connect(m_batchFetcher, &KJob::result, this, &RetrieveItemsTask::onRetrievalDone); m_batchFetcher->start(); } void RetrieveItemsTask::onReadyForNextBatch(int size) { Q_UNUSED(size); if (m_batchFetcher) { m_batchFetcher->fetchNextBatch(); } } void RetrieveItemsTask::onItemsRetrieved(const Akonadi::Item::List &addedItems) { if (m_incremental) { itemsRetrievedIncremental(addedItems, Akonadi::Item::List()); } else { itemsRetrieved(addedItems); } //m_fetchedMissingBodies is -1 if we fetch for other reason, but missing bodies if (m_fetchedMissingBodies != -1) { const QString mailBox = mailBoxForCollection(collection()); m_fetchedMissingBodies += addedItems.count(); Q_EMIT status(Akonadi::AgentBase::Running, i18nc("@info:status", "Fetching missing mail bodies in %3: %1/%2", m_fetchedMissingBodies, m_messageUidsMissingBody.count(), mailBox)); } } void RetrieveItemsTask::onRetrievalDone(KJob *job) { m_batchFetcher = nullptr; if (job->error()) { qCWarning(IMAPRESOURCE_LOG) << job->errorString(); cancelTask(job->errorString()); m_fetchedMissingBodies = -1; return; } //This is the lowest sequence number that we just fetched. const KIMAP::ImapSet::Id alreadyFetchedBegin = job->property("alreadyFetched").value(); // If this is the first fetch of a folder, skip getting flags, we // already have them all from the previous full fetch. This is not // just an optimization, as incremental retrieval assumes nothing // will be listed twice. if (m_fetchedMissingBodies != -1 || alreadyFetchedBegin <= 1) { taskComplete(); return; } // Fetch flags of all items that were not fetched by the fetchJob. After // that /all/ items in the folder are synced. listFlagsForImapSet(KIMAP::ImapSet(1, alreadyFetchedBegin - 1)); } void RetrieveItemsTask::listFlagsForImapSet(const KIMAP::ImapSet &set) { qCDebug(IMAPRESOURCE_LOG) << "Listing flags " << set.intervals().at(0).begin() << set.intervals().at(0).end(); qCDebug(IMAPRESOURCE_LOG) << "Starting flag retrieval. Elapsed(ms): " << m_time.elapsed(); KIMAP::FetchJob::FetchScope scope; scope.parts.clear(); scope.mode = KIMAP::FetchJob::FetchScope::Flags; // Only use changeSince when doing incremental listings, // otherwise we would overwrite our local data with an incomplete dataset if (m_incremental && serverSupportsCondstore()) { scope.changedSince = m_localHighestModSeq; if (!m_flagsChanged) { qCDebug(IMAPRESOURCE_LOG) << "No flag changes."; taskComplete(); return; } } m_batchFetcher = createBatchFetcher(resourceState()->messageHelper(), set, scope, 10 * batchSize(), m_session); m_batchFetcher->setUidBased(m_uidBasedFetch); if (m_uidBasedFetch && scope.changedSince == 0 && set.intervals().size() == 1) { m_batchFetcher->setSearchUids(set.intervals().front()); } connect(m_batchFetcher, &BatchFetcher::itemsRetrieved, this, &RetrieveItemsTask::onItemsRetrieved); connect(m_batchFetcher, &KJob::result, this, &RetrieveItemsTask::onFlagsFetchDone); m_batchFetcher->start(); } void RetrieveItemsTask::onFlagsFetchDone(KJob *job) { m_batchFetcher = nullptr; if (job->error()) { qCWarning(IMAPRESOURCE_LOG) << job->errorString(); cancelTask(job->errorString()); } else { taskComplete(); } } void RetrieveItemsTask::taskComplete() { if (m_modifiedCollection.isValid()) { qCDebug(IMAPRESOURCE_LOG) << "Applying collection changes"; applyCollectionChanges(m_modifiedCollection); } if (m_incremental) { // Calling itemsRetrievalDone() before previous call to itemsRetrievedIncremental() // behaves like if we called itemsRetrieved(Items::List()), so make sure // Akonadi knows we did incremental fetch that came up with no changes itemsRetrievedIncremental(Akonadi::Item::List(), Akonadi::Item::List()); } qCDebug(IMAPRESOURCE_LOG) << "Retrieval complete. Elapsed(ms): " << m_time.elapsed(); itemsRetrievalDone(); }