diff --git a/agents/unifiedmailboxagent/CMakeLists.txt b/agents/unifiedmailboxagent/CMakeLists.txt index 0cede6572..8a21518b9 100644 --- a/agents/unifiedmailboxagent/CMakeLists.txt +++ b/agents/unifiedmailboxagent/CMakeLists.txt @@ -1,50 +1,49 @@ add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_unifiedmailbox_agent\") set(CMAKE_CXX_STANDARD 14) if(BUILD_TESTING) - #add_subdirectory(tests) - #add_subdirectory(autotests) + add_subdirectory(autotests) endif() set(unifiedmailbox_agent_SRCS unifiedmailbox.cpp unifiedmailboxagent.cpp unifiedmailboxmanager.cpp unifiedmailboxeditor.cpp settingsdialog.cpp mailkernel.cpp ) ecm_qt_declare_logging_category(unifiedmailbox_agent_SRCS HEADER unifiedmailboxagent_debug.h IDENTIFIER agent_log CATEGORY_NAME org.kde.pim.unifiedmailboxagent) kconfig_add_kcfg_files(unifiedmailbox_agent_SRCS settings.kcfgc ) add_executable(akonadi_unifiedmailbox_agent ${unifiedmailbox_agent_SRCS}) target_link_libraries(akonadi_unifiedmailbox_agent KF5::AkonadiAgentBase KF5::AkonadiMime KF5::AkonadiWidgets KF5::Mime KF5::I18n KF5::IdentityManagement KF5::WidgetsAddons KF5::IconThemes KF5::ItemModels KF5::MailCommon ) if( APPLE ) set_target_properties(akonadi_unifiedmailbox_agent PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${kmail_SOURCE_DIR}/agents/Info.plist.template) set_target_properties(akonadi_unifiedmailbox_agent PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "org.kde.Akonadi.KF5::UnifiedMailbox") set_target_properties(akonadi_unifiedmailbox_agent PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "KDE PIM Unified Mailbox") endif () install(TARGETS akonadi_unifiedmailbox_agent ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} ) install(FILES unifiedmailboxagent.desktop DESTINATION "${KDE_INSTALL_DATAROOTDIR}/akonadi/agents") diff --git a/agents/unifiedmailboxagent/autotests/CMakeLists.txt b/agents/unifiedmailboxagent/autotests/CMakeLists.txt new file mode 100644 index 000000000..10eb7721c --- /dev/null +++ b/agents/unifiedmailboxagent/autotests/CMakeLists.txt @@ -0,0 +1,11 @@ +set(common_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/../unifiedmailboxagent_debug.cpp +) +include_directories(${CMAKE_CURRENT_BINARY_DIR}/..) + +add_definitions(-DUNIT_TESTS) + +add_akonadi_isolated_test(SOURCE unifiedmailboxmanagertest.cpp + ADDITIONAL_SOURCES ${common_SRCS} ../unifiedmailboxmanager.cpp ../unifiedmailbox.cpp + LINK_LIBRARIES KF5::I18n KF5::AkonadiMime +) diff --git a/agents/unifiedmailboxagent/autotests/unifiedmailboxmanagertest.cpp b/agents/unifiedmailboxagent/autotests/unifiedmailboxmanagertest.cpp new file mode 100644 index 000000000..96be40c22 --- /dev/null +++ b/agents/unifiedmailboxagent/autotests/unifiedmailboxmanagertest.cpp @@ -0,0 +1,664 @@ +/* + Copyright (C) 2018 Daniel Vrátil + + 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; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "../unifiedmailboxmanager.h" +#include "../unifiedmailbox.h" +#include "../common.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +using namespace std::chrono; +using namespace std::chrono_literals; + +namespace { + +#define AKVERIFY_RET(statement, ret) \ +do {\ + if (!QTest::qVerify(static_cast(statement), #statement, "", __FILE__, __LINE__))\ + return ret;\ +} while (false) + +#define AKCOMPARE_RET(actual, expected, ret) \ +do {\ + if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__))\ + return ret;\ +} while (false) + + + +Akonadi::Collection collectionForId(qint64 id) +{ + auto fetch = new Akonadi::CollectionFetchJob(Akonadi::Collection(id), Akonadi::CollectionFetchJob::Base); + fetch->fetchScope().fetchAttribute(); + AKVERIFY_RET(fetch->exec(), {}); + const auto cols = fetch->collections(); + AKCOMPARE_RET(cols.count(), 1, {}); + return cols.first(); +} + +Akonadi::Collection collectionForRid(const QString &rid) +{ + auto fetch = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::Recursive); + fetch->fetchScope().fetchAttribute(); + fetch->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All); + AKVERIFY_RET(fetch->exec(), {}); + const auto cols = fetch->collections(); + auto colIt = std::find_if(cols.cbegin(), cols.cend(), [&rid](const Akonadi::Collection &col) { + return col.remoteId() == rid; + }); + AKVERIFY_RET(colIt != cols.cend(), {}); + return *colIt; +} + +// A kingdom and a horse for std::optional! +std::unique_ptr createUnifiedMailbox(const QString &id, const QString &name, const QStringList &sourceRids) +{ + auto mailbox = std::make_unique(); + mailbox->setId(id); + mailbox->setName(name); + mailbox->setIcon(QStringLiteral("dummy-icon")); + for (const auto &srcRid : sourceRids) { + const auto srcCol = collectionForRid(srcRid); + AKVERIFY_RET(srcCol.isValid(), {}); + mailbox->addSourceCollection(srcCol.id()); + } + return mailbox; +} + +class EntityDeleter +{ +public: + ~EntityDeleter() + { + while (!cols.isEmpty()) { + if (!(new Akonadi::CollectionDeleteJob(cols.takeFirst()))->exec()) { + QFAIL("Failed to cleanup collection!"); + } + } + while (!items.isEmpty()) { + if (!(new Akonadi::ItemDeleteJob(items.takeFirst()))->exec()) { + QFAIL("Failed to cleanup Item"); + } + } + } + + EntityDeleter &operator<<(const Akonadi::Collection &col) + { + cols.push_back(col); + return *this; + } + + EntityDeleter &operator<<(const Akonadi::Item &item) + { + items.push_back(item); + return *this; + } +private: + Akonadi::Collection::List cols; + Akonadi::Item::List items; +}; + +Akonadi::Collection createCollection(const QString &name, const Akonadi::Collection &parent, EntityDeleter &deleter) +{ + Akonadi::Collection col; + col.setName(name); + col.setParentCollection(parent); + col.setVirtual(true); + auto createCol = new Akonadi::CollectionCreateJob(col); + AKVERIFY_RET(createCol->exec(), {}); + col = createCol->collection(); + if (col.isValid()) { + deleter << col; + } + return col; +} + + +} // namespace + + +class UnifiedMailboxManagerTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + + void initTestCase() + { + AkonadiTest::checkTestIsIsolated(); + } + + void testCreateDefaultBoxes() + { + // Setup + auto kcfg = KSharedConfig::openConfig(QString::fromUtf8(QTest::currentTestFunction())); + const auto boxesGroup = kcfg->group("UnifiedMailboxes"); + UnifiedMailboxManager manager(kcfg); + + // Make sure the config is empty + QVERIFY(boxesGroup.groupList().empty()); + + // Call loadBoxes and wait for it to finish + bool loadingDone = false; + manager.loadBoxes([&loadingDone]() { loadingDone = true; }); + QTRY_VERIFY_WITH_TIMEOUT(loadingDone, milliseconds(10s).count()); + + // Check all the three boxes were created + bool success; + const auto verifyBox = [&manager, &success](const QString &id, int numSources) { + success = false; + auto boxIt = std::find_if(manager.begin(), manager.end(), + [&id](const UnifiedMailboxManager::Entry &e) { + return e.second->id() == id; + }); + QVERIFY(boxIt != manager.end()); + const auto &box = boxIt->second; + const auto sourceCollections = box->sourceCollections(); + QCOMPARE(sourceCollections.size(), numSources); + for (auto source : sourceCollections) { + auto col = collectionForId(source); + QVERIFY(col.isValid()); + QVERIFY(col.hasAttribute()); + QCOMPARE(col.attribute()->collectionType(), id.toLatin1()); + } + success = true; + }; + verifyBox(Common::InboxBoxId, 2); + QVERIFY(success); + verifyBox(Common::SentBoxId, 2); + QVERIFY(success); + verifyBox(Common::DraftsBoxId, 1); + QVERIFY(success); + + // Check boxes were written to config - we don't check the contents of + // the group, testing UnifiedMailbox serialization is done in other tests + QVERIFY(boxesGroup.groupList().size() == 3); + QVERIFY(boxesGroup.hasGroup(Common::InboxBoxId)); + QVERIFY(boxesGroup.hasGroup(Common::SentBoxId)); + QVERIFY(boxesGroup.hasGroup(Common::DraftsBoxId)); + } + + void testAddingNewMailbox() + { + // Setup + auto kcfg = KSharedConfig::openConfig(QString::fromUtf8(QTest::currentTestFunction())); + const auto boxesGroup = kcfg->group("UnifiedMailboxes"); + UnifiedMailboxManager manager(kcfg); + Akonadi::ChangeRecorder &recorder = manager.changeRecorder(); + + // Nothing should be monitored as of now + QVERIFY(recorder.collectionsMonitored().isEmpty()); + + // Create a new unified mailbox and passit to the manager + auto mailbox = createUnifiedMailbox(QStringLiteral("Test1"), QStringLiteral("Test 1"), + { QStringLiteral("res1_inbox") }); + QVERIFY(mailbox); + const auto sourceCol = mailbox->sourceCollections().toList().first(); + manager.insertBox(std::move(mailbox)); + + // Now manager should have one unified mailbox and monitor all of its + // source collections + QCOMPARE(std::distance(manager.begin(), manager.end()), 1); + QCOMPARE(recorder.collectionsMonitored().size(), 1); + QCOMPARE(recorder.collectionsMonitored().at(0).id(), sourceCol); + QVERIFY(manager.unifiedMailboxForSource(sourceCol) != nullptr); + + // But nothing should bne written in the config yet + QVERIFY(!boxesGroup.groupList().contains(QLatin1String("Test1"))); + + // Now write to the config file and check it's actually there - we don't test + // the contents of the group, UnifiedMailbox serialization has its own test + manager.saveBoxes(); + QVERIFY(boxesGroup.hasGroup(QLatin1String("Test1"))); + } + + void testRemoveMailbox() + { + // Setup + auto kcfg = KSharedConfig::openConfig(QString::fromUtf8(QTest::currentTestFunction())); + auto boxesGroup = kcfg->group("UnifiedMailboxes"); + auto mailbox = createUnifiedMailbox(QStringLiteral("Test1"), QStringLiteral("Test 1"), + { QStringLiteral("res1_foo"), QStringLiteral("res2_foo") }); + QVERIFY(mailbox); + auto group = boxesGroup.group(mailbox->id()); + mailbox->save(group); + + UnifiedMailboxManager manager(kcfg); + Akonadi::ChangeRecorder &recorder = manager.changeRecorder(); + + // Nothing should be monitored right now + QVERIFY(recorder.collectionsMonitored().isEmpty()); + + // Load the config + bool loadingDone = false; + manager.loadBoxes([&loadingDone]() { loadingDone = true; }); + QTRY_VERIFY_WITH_TIMEOUT(loadingDone, milliseconds(10s).count()); + + // Now the box should be loaded and its source collections monitored + QCOMPARE(std::distance(manager.begin(), manager.end()), 1); + QCOMPARE(recorder.collectionsMonitored().count(), 2); + const auto srcCols = mailbox->sourceCollections().toList(); + QCOMPARE(srcCols.count(), 2); + QVERIFY(recorder.collectionsMonitored().contains(Akonadi::Collection(srcCols[0]))); + QVERIFY(recorder.collectionsMonitored().contains(Akonadi::Collection(srcCols[1]))); + + // Now remove the box + manager.removeBox(mailbox->id()); + + // Manager should have no boxes and no source collections should be monitored + QVERIFY(manager.begin() == manager.end()); + QVERIFY(recorder.collectionsMonitored().isEmpty()); + + // But the box still exists in the config + QVERIFY(boxesGroup.hasGroup(mailbox->id())); + + // Save the new state + manager.saveBoxes(); + + // And now it should be gone from the config file as well + QVERIFY(!boxesGroup.hasGroup(mailbox->id())); + } + + void testDiscoverBoxCollections() + { + // Setup + auto kcfg = KSharedConfig::openConfig(QString::fromUtf8(QTest::currentTestFunction())); + auto boxesGroup = kcfg->group("UnifiedMailboxes"); + UnifiedMailboxManager manager(kcfg); + EntityDeleter deleter; + const auto inbox = createUnifiedMailbox(Common::InboxBoxId, QStringLiteral("Inbox"), + { QStringLiteral("res1_inbox"), QStringLiteral("res2_inbox") }); + auto boxGroup = boxesGroup.group(inbox->id()); + inbox->save(boxGroup); + const auto sentBox = createUnifiedMailbox(Common::SentBoxId, QStringLiteral("Sent"), + { QStringLiteral("res1_sent"), QStringLiteral("res2_sent") }); + boxGroup = boxesGroup.group(sentBox->id()); + sentBox->save(boxGroup); + + const Akonadi::Collection parentCol = collectionForRid(Common::AgentIdentifier); + QVERIFY(parentCol.isValid()); + + const auto inboxBoxCol = createCollection(Common::InboxBoxId, parentCol, deleter); + QVERIFY(inboxBoxCol.isValid()); + + const auto sentBoxCol = createCollection(Common::SentBoxId, parentCol, deleter); + QVERIFY(sentBoxCol.isValid()); + + // Load from config + bool loadingDone = false; + manager.loadBoxes([&loadingDone]() { loadingDone = true; }); + QTRY_VERIFY_WITH_TIMEOUT(loadingDone, milliseconds(10s).count()); + + // Now the boxes should be loaded and we should be able to access them + // by IDs of collections that represent them. The collections should also + // be set for each box. + auto box = manager.unifiedMailboxFromCollection(inboxBoxCol); + QVERIFY(box != nullptr); + QCOMPARE(box->collectionId(), inboxBoxCol.id()); + + box = manager.unifiedMailboxFromCollection(sentBoxCol); + QVERIFY(box != nullptr); + QCOMPARE(box->collectionId(), sentBoxCol.id()); + } + + void testItemAddedToSourceCollection() + { + // Setup + auto kcfg = KSharedConfig::openConfig(QString::fromUtf8(QTest::currentTestFunction())); + UnifiedMailboxManager manager(kcfg); + EntityDeleter deleter; + + const auto parentCol = collectionForRid(Common::AgentIdentifier); + QVERIFY(parentCol.isValid()); + + const auto inboxBoxCol = createCollection(Common::InboxBoxId, parentCol, deleter); + QVERIFY(inboxBoxCol.isValid()); + + // Load boxes - config is empty so this will create the default Boxes and + // assign the Inboxes from Knuts to it + bool loadingDone = true; + manager.loadBoxes([&loadingDone]() { loadingDone = true; }); + QTRY_VERIFY_WITH_TIMEOUT(loadingDone, milliseconds(10s).count()); + + // Now discover collections for the created boxes + loadingDone = false; + manager.discoverBoxCollections([&loadingDone]() { loadingDone = true; }); + QTRY_VERIFY_WITH_TIMEOUT(loadingDone, milliseconds(10s).count()); + + // Get one of the source collections for Inbox + const auto inboxSourceCol = collectionForRid(QStringLiteral("res1_inbox")); + QVERIFY(inboxSourceCol.isValid()); + + // Setup up a monitor to to be notified when an item gets linked into + // the unified mailbox collection + Akonadi::Monitor monitor; + monitor.setCollectionMonitored(inboxBoxCol); + QSignalSpy itemLinkedSignalSpy(&monitor, &Akonadi::Monitor::itemsLinked); + QVERIFY(QSignalSpy(&monitor, &Akonadi::Monitor::monitorReady).wait()); + + // Add a new Item into the source collection + Akonadi::Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setParentCollection(inboxSourceCol); + item.setPayload(QByteArray{"Hello world!"}); + auto createItem = new Akonadi::ItemCreateJob(item, inboxSourceCol, this); + AKVERIFYEXEC(createItem); + item = createItem->item(); + deleter << item; + + // Then wait for ItemLinked notification as the Manager has linked the new Item + // to the dest collection + QTRY_COMPARE(itemLinkedSignalSpy.size(), 1); + const auto linkedItems = itemLinkedSignalSpy.at(0).at(0).value(); + QCOMPARE(linkedItems.size(), 1); + QCOMPARE(linkedItems.at(0), item); + const auto linkedCol = itemLinkedSignalSpy.at(0).at(1).value(); + QCOMPARE(linkedCol, inboxBoxCol); + } + + void testItemMovedFromSourceCollection() + { + // Setup + auto kcfg = KSharedConfig::openConfig(QString::fromUtf8(QTest::currentTestFunction())); + UnifiedMailboxManager manager(kcfg); + EntityDeleter deleter; + + const auto parentCol = collectionForRid(Common::AgentIdentifier); + QVERIFY(parentCol.isValid()); + + const auto inboxBoxCol = createCollection(Common::InboxBoxId, parentCol, deleter); + QVERIFY(inboxBoxCol.isValid()); + + // Load boxes - config is empty so this will create the default Boxes and + // assign the Inboxes from Knuts to it + bool loadingDone = true; + manager.loadBoxes([&loadingDone]() { loadingDone = true; }); + QTRY_VERIFY_WITH_TIMEOUT(loadingDone, milliseconds(10s).count()); + + // Now discover collections for the created boxes + loadingDone = false; + manager.discoverBoxCollections([&loadingDone]() { loadingDone = true; }); + QTRY_VERIFY_WITH_TIMEOUT(loadingDone, milliseconds(10s).count()); + + // Get one of the source collections for Inbox + const auto inboxSourceCol = collectionForRid(QStringLiteral("res1_inbox")); + QVERIFY(inboxSourceCol.isValid()); + + // Setup up a monitor to to be notified when an item gets linked into + // the unified mailbox collection + Akonadi::Monitor monitor; + monitor.setCollectionMonitored(inboxBoxCol); + QSignalSpy itemLinkedSignalSpy(&monitor, &Akonadi::Monitor::itemsLinked); + QSignalSpy itemUnlinkedSignalSpy(&monitor, &Akonadi::Monitor::itemsUnlinked); + QVERIFY(QSignalSpy(&monitor, &Akonadi::Monitor::monitorReady).wait()); + + // Add a new Item into the source collection + Akonadi::Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setParentCollection(inboxSourceCol); + item.setPayload(QByteArray{"Hello world!"}); + auto createItem = new Akonadi::ItemCreateJob(item, inboxSourceCol, this); + AKVERIFYEXEC(createItem); + item = createItem->item(); + deleter << item; + + // Waity for the item to be linked + QTRY_COMPARE(itemLinkedSignalSpy.size(), 1); + + const auto destinationCol = collectionForRid(QStringLiteral("res1_foo")); + QVERIFY(destinationCol.isValid()); + + // Now move the Item to an unmonitored collection + auto move = new Akonadi::ItemMoveJob(item, destinationCol, this); + AKVERIFYEXEC(move); + + QTRY_COMPARE(itemUnlinkedSignalSpy.size(), 1); + const auto unlinkedItems = itemUnlinkedSignalSpy.at(0).at(0).value(); + QCOMPARE(unlinkedItems.size(), 1); + QCOMPARE(unlinkedItems.first(), item); + const auto unlinkedCol = itemUnlinkedSignalSpy.at(0).at(1).value(); + QCOMPARE(unlinkedCol, inboxBoxCol); + } + + void testItemMovedBetweenSourceCollections() + { + // Setup + auto kcfg = KSharedConfig::openConfig(QString::fromUtf8(QTest::currentTestFunction())); + UnifiedMailboxManager manager(kcfg); + EntityDeleter deleter; + + const auto parentCol = collectionForRid(Common::AgentIdentifier); + QVERIFY(parentCol.isValid()); + + const auto inboxBoxCol = createCollection(Common::InboxBoxId, parentCol, deleter); + QVERIFY(inboxBoxCol.isValid()); + + const auto draftsBoxCol = createCollection(Common::DraftsBoxId, parentCol, deleter); + QVERIFY(draftsBoxCol.isValid()); + + // Load boxes - config is empty so this will create the default Boxes and + // assign the Inboxes from Knuts to it + bool loadingDone = true; + manager.loadBoxes([&loadingDone]() { loadingDone = true; }); + QTRY_VERIFY_WITH_TIMEOUT(loadingDone, milliseconds(10s).count()); + + // Now discover collections for the created boxes + loadingDone = false; + manager.discoverBoxCollections([&loadingDone]() { loadingDone = true; }); + QTRY_VERIFY_WITH_TIMEOUT(loadingDone, milliseconds(10s).count()); + + // Get one of the source collections for Inbox and Drafts + const auto inboxSourceCol = collectionForRid(QStringLiteral("res1_inbox")); + QVERIFY(inboxSourceCol.isValid()); + const auto draftsSourceCol = collectionForRid(QStringLiteral("res1_drafts")); + QVERIFY(draftsSourceCol.isValid()); + + + // Setup up a monitor to to be notified when an item gets linked into + // the unified mailbox collection + Akonadi::Monitor monitor; + monitor.setCollectionMonitored(inboxBoxCol); + monitor.setCollectionMonitored(draftsBoxCol); + QSignalSpy itemLinkedSignalSpy(&monitor, &Akonadi::Monitor::itemsLinked); + QSignalSpy itemUnlinkedSignalSpy(&monitor, &Akonadi::Monitor::itemsUnlinked); + QVERIFY(QSignalSpy(&monitor, &Akonadi::Monitor::monitorReady).wait()); + + // Add a new Item into the source Inbox collection + Akonadi::Item item; + item.setMimeType(QStringLiteral("application/octet-stream")); + item.setParentCollection(inboxSourceCol); + item.setPayload(QByteArray{"Hello world!"}); + auto createItem = new Akonadi::ItemCreateJob(item, inboxSourceCol, this); + AKVERIFYEXEC(createItem); + item = createItem->item(); + deleter << item; + + // Waity for the item to be linked + QTRY_COMPARE(itemLinkedSignalSpy.size(), 1); + itemLinkedSignalSpy.clear(); + + // Now move the Item to another Unified mailbox's source collection + auto move = new Akonadi::ItemMoveJob(item, draftsSourceCol, this); + AKVERIFYEXEC(move); + + QTRY_COMPARE(itemUnlinkedSignalSpy.size(), 1); + const auto unlinkedItems = itemUnlinkedSignalSpy.at(0).at(0).value(); + QCOMPARE(unlinkedItems.size(), 1); + QCOMPARE(unlinkedItems.first(), item); + const auto unlinkedCol = itemUnlinkedSignalSpy.at(0).at(1).value(); + QCOMPARE(unlinkedCol, inboxBoxCol); + + QTRY_COMPARE(itemLinkedSignalSpy.size(), 1); + const auto linkedItems = itemLinkedSignalSpy.at(0).at(0).value(); + QCOMPARE(linkedItems.size(), 1); + QCOMPARE(linkedItems.first(), item); + const auto linkedCol = itemLinkedSignalSpy.at(0).at(1).value(); + QCOMPARE(linkedCol, draftsBoxCol); + } + + void testSourceCollectionRemoved() + { + // Setup + auto kcfg = KSharedConfig::openConfig(QString::fromUtf8(QTest::currentTestFunction())); + UnifiedMailboxManager manager(kcfg); + auto &changeRecorder = manager.changeRecorder(); + QSignalSpy crRemovedSpy(&changeRecorder, &Akonadi::Monitor::collectionRemoved); + EntityDeleter deleter; + + const auto parentCol = collectionForRid(Common::AgentIdentifier); + QVERIFY(parentCol.isValid()); + + const auto inboxBoxCol = createCollection(Common::InboxBoxId, parentCol, deleter); + QVERIFY(inboxBoxCol.isValid()); + + // Load boxes - config is empty so this will create the default Boxes and + // assign the Inboxes from Knuts to it + bool loadingDone = true; + manager.loadBoxes([&loadingDone]() { loadingDone = true; }); + QTRY_VERIFY_WITH_TIMEOUT(loadingDone, milliseconds(10s).count()); + + // Now discover collections for the created boxes + loadingDone = false; + manager.discoverBoxCollections([&loadingDone]() { loadingDone = true; }); + QTRY_VERIFY_WITH_TIMEOUT(loadingDone, milliseconds(10s).count()); + + auto inboxSourceCol = collectionForRid(QStringLiteral("res1_inbox")); + QVERIFY(inboxSourceCol.isValid()); + auto delJob = new Akonadi::CollectionDeleteJob(inboxSourceCol, this); + AKVERIFYEXEC(delJob); + + // Wait for the change recorder to be notified + QVERIFY(crRemovedSpy.wait()); + crRemovedSpy.clear(); + // and then wait a little bit more to give the Manager time to process the event + QTest::qWait(0); + + auto inboxBox = manager.unifiedMailboxFromCollection(inboxBoxCol); + QVERIFY(inboxBox); + QVERIFY(!inboxBox->sourceCollections().contains(inboxSourceCol.id())); + QVERIFY(!changeRecorder.collectionsMonitored().contains(inboxSourceCol)); + QVERIFY(!manager.unifiedMailboxForSource(inboxSourceCol.id())); + + // Lets removed the other source collection now, that should remove the unified box completely + inboxSourceCol = collectionForRid(QStringLiteral("res2_inbox")); + QVERIFY(inboxSourceCol.isValid()); + delJob = new Akonadi::CollectionDeleteJob(inboxSourceCol, this); + AKVERIFYEXEC(delJob); + + // Wait for the change recorder once again + QVERIFY(crRemovedSpy.wait()); + QTest::qWait(0); + + QVERIFY(!manager.unifiedMailboxFromCollection(inboxBoxCol)); + QVERIFY(!changeRecorder.collectionsMonitored().contains(inboxSourceCol)); + QVERIFY(!manager.unifiedMailboxForSource(inboxSourceCol.id())); + } + + void testSpecialSourceCollectionCreated() + { + // TODO: this does not work yet: we only monitor collections that we are + // intersted in, we don't monitor other collections + } + + void testSpecialSourceCollectionDemoted() + { + // Setup + auto kcfg = KSharedConfig::openConfig(QString::fromUtf8(QTest::currentTestFunction())); + UnifiedMailboxManager manager(kcfg); + auto &changeRecorder = manager.changeRecorder(); + QSignalSpy crChangedSpy(&changeRecorder, qOverload &>(&Akonadi::Monitor::collectionChanged)); + EntityDeleter deleter; + + const auto parentCol = collectionForRid(Common::AgentIdentifier); + QVERIFY(parentCol.isValid()); + + const auto sentBoxCol = createCollection(Common::SentBoxId, parentCol, deleter); + QVERIFY(sentBoxCol.isValid()); + + // Load boxes - config is empty so this will create the default Boxes and + // assign the Inboxes from Knuts to it + bool loadingDone = true; + manager.loadBoxes([&loadingDone]() { loadingDone = true; }); + QTRY_VERIFY_WITH_TIMEOUT(loadingDone, milliseconds(10s).count()); + + // Now discover collections for the created boxes + loadingDone = false; + manager.discoverBoxCollections([&loadingDone]() { loadingDone = true; }); + QTRY_VERIFY_WITH_TIMEOUT(loadingDone, milliseconds(10s).count()); + + auto sentSourceCol = collectionForRid(QStringLiteral("res1_sent")); + QVERIFY(sentSourceCol.isValid()); + sentSourceCol.removeAttribute(); + auto modify = new Akonadi::CollectionModifyJob(sentSourceCol, this); + AKVERIFYEXEC(modify); + + // Wait for the change recorder to be notified + QVERIFY(crChangedSpy.wait()); + crChangedSpy.clear(); + // and then wait a little bit more to give the Manager time to process the event + QTest::qWait(0); + + auto sourceBox = manager.unifiedMailboxFromCollection(sentBoxCol); + QVERIFY(sourceBox); + QVERIFY(!sourceBox->sourceCollections().contains(sentSourceCol.id())); + QVERIFY(!changeRecorder.collectionsMonitored().contains(sentSourceCol)); + QVERIFY(!manager.unifiedMailboxForSource(sentSourceCol.id())); + + // Lets demote the other source collection now, that should remove the unified box completely + sentSourceCol = collectionForRid(QStringLiteral("res2_sent")); + QVERIFY(sentSourceCol.isValid()); + sentSourceCol.attribute()->setCollectionType("drafts"); + modify = new Akonadi::CollectionModifyJob(sentSourceCol, this); + AKVERIFYEXEC(modify); + + // Wait for the change recorder once again + QVERIFY(crChangedSpy.wait()); + QTest::qWait(0); + + // There's no more Sent unified box + QVERIFY(!manager.unifiedMailboxFromCollection(sentBoxCol)); + + // The collection is still monitored: it belongs to the Drafts special box now! + QVERIFY(changeRecorder.collectionsMonitored().contains(sentSourceCol)); + QVERIFY(manager.unifiedMailboxForSource(sentSourceCol.id())); + } + +}; + +QTEST_AKONADIMAIN(UnifiedMailboxManagerTest) + +#include "unifiedmailboxmanagertest.moc" diff --git a/agents/unifiedmailboxagent/autotests/unittestenv/config.xml b/agents/unifiedmailboxagent/autotests/unittestenv/config.xml new file mode 100644 index 000000000..2b5dd0b87 --- /dev/null +++ b/agents/unifiedmailboxagent/autotests/unittestenv/config.xml @@ -0,0 +1,7 @@ + + xdglocal + akonadi_knut_resource + akonadi_knut_resource + akonadi_knut_resource + true + diff --git a/agents/unifiedmailboxagent/autotests/unittestenv/xdgconfig/akonadi-firstrunrc b/agents/unifiedmailboxagent/autotests/unittestenv/xdgconfig/akonadi-firstrunrc new file mode 100644 index 000000000..c5e90d841 --- /dev/null +++ b/agents/unifiedmailboxagent/autotests/unittestenv/xdgconfig/akonadi-firstrunrc @@ -0,0 +1,4 @@ +[ProcessedDefaults] +defaultaddressbook=done +defaultcalendar=done +defaultnotebook=done diff --git a/agents/unifiedmailboxagent/autotests/unittestenv/xdgconfig/akonadi_knut_resource_0rc b/agents/unifiedmailboxagent/autotests/unittestenv/xdgconfig/akonadi_knut_resource_0rc new file mode 100644 index 000000000..0d9e3cfe5 --- /dev/null +++ b/agents/unifiedmailboxagent/autotests/unittestenv/xdgconfig/akonadi_knut_resource_0rc @@ -0,0 +1,4 @@ +[General] +DataFile[$e]=$XDG_DATA_HOME/testdata-res1.xml +FileWatchingEnabled=false + diff --git a/agents/unifiedmailboxagent/autotests/unittestenv/xdgconfig/akonadi_knut_resource_1rc b/agents/unifiedmailboxagent/autotests/unittestenv/xdgconfig/akonadi_knut_resource_1rc new file mode 100644 index 000000000..d39b82877 --- /dev/null +++ b/agents/unifiedmailboxagent/autotests/unittestenv/xdgconfig/akonadi_knut_resource_1rc @@ -0,0 +1,5 @@ +[General] +DataFile[$e]=$XDG_DATA_HOME/testdata-res2.xml +FileWatchingEnabled=false + + diff --git a/agents/unifiedmailboxagent/autotests/unittestenv/xdgconfig/akonadi_knut_resource_2rc b/agents/unifiedmailboxagent/autotests/unittestenv/xdgconfig/akonadi_knut_resource_2rc new file mode 100644 index 000000000..274fbfc7e --- /dev/null +++ b/agents/unifiedmailboxagent/autotests/unittestenv/xdgconfig/akonadi_knut_resource_2rc @@ -0,0 +1,3 @@ +[General] +DataFile[$e]=$XDG_DATA_HOME/testdata-res3.xml +FileWatchingEnabled=false diff --git a/agents/unifiedmailboxagent/autotests/unittestenv/xdglocal/testdata-res1.xml b/agents/unifiedmailboxagent/autotests/unittestenv/xdglocal/testdata-res1.xml new file mode 100644 index 000000000..e0aae9fb2 --- /dev/null +++ b/agents/unifiedmailboxagent/autotests/unittestenv/xdglocal/testdata-res1.xml @@ -0,0 +1,53 @@ + + + + inbox + + testmailbody + From: <test@user.tst> + \SEEN + + + testmailbody1 + From: <test1@user.tst> + \FLAGGED + + + testmailbody2 + From: <test2@user.tst> + + + + sent-mail + + testmailbody3 + From: <test3@user.tst> + + + testmailbody4 + From: <test4@user.tst> + + + + drafts + + testmailbody5 + From: <test5@user.tst> + + + + + testmailbody6 + From: <test6@user.tst> + + + testmailbody7 + From: <test7@user.tst> + + + testmailbody8 + From: <test8@user.tst> + + + + diff --git a/agents/unifiedmailboxagent/autotests/unittestenv/xdglocal/testdata-res2.xml b/agents/unifiedmailboxagent/autotests/unittestenv/xdglocal/testdata-res2.xml new file mode 100644 index 000000000..4deeb07f9 --- /dev/null +++ b/agents/unifiedmailboxagent/autotests/unittestenv/xdglocal/testdata-res2.xml @@ -0,0 +1,46 @@ + + + + inbox + + testmailbody + From: <test@user.tst> + \SEEN + + + testmailbody1 + From: <test1@user.tst> + \FLAGGED + + + + sent-mail + + testmailbody3 + From: <test3@user.tst> + + + + templates + + testmailbody5 + From: <test5@user.tst> + + + + + testmailbody6 + From: <test6@user.tst> + + + testmailbody7 + From: <test7@user.tst> + + + testmailbody8 + From: <test8@user.tst> + + + + + diff --git a/agents/unifiedmailboxagent/autotests/unittestenv/xdglocal/testdata-res3.xml b/agents/unifiedmailboxagent/autotests/unittestenv/xdglocal/testdata-res3.xml new file mode 100644 index 000000000..937d98488 --- /dev/null +++ b/agents/unifiedmailboxagent/autotests/unittestenv/xdglocal/testdata-res3.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/agents/unifiedmailboxagent/unifiedmailbox.cpp b/agents/unifiedmailboxagent/unifiedmailbox.cpp index 16e7bfc3c..ca24df5d6 100644 --- a/agents/unifiedmailboxagent/unifiedmailbox.cpp +++ b/agents/unifiedmailboxagent/unifiedmailbox.cpp @@ -1,140 +1,151 @@ /* Copyright (C) 2018 Daniel Vrátil 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "unifiedmailbox.h" #include "unifiedmailboxmanager.h" #include "utils.h" +#include "common.h" #include +bool UnifiedMailbox::operator==(const UnifiedMailbox &other) const +{ + return mId == other.mId; +} + void UnifiedMailbox::load(const KConfigGroup &group) { mId = group.name(); mName = group.readEntry("name"); mIcon = group.readEntry("icon", QStringLiteral("folder-mail")); mSources = listToSet(group.readEntry("sources", QList{})); // This is not authoritative, we will do collection discovery anyway mCollectionId = group.readEntry("collectionId", -1ll); } void UnifiedMailbox::save(KConfigGroup& group) const { group.writeEntry("name", name()); group.writeEntry("icon", icon()); group.writeEntry("sources", setToList(sourceCollections())); // just for cacheing, we will do collection discovery on next start anyway group.writeEntry("collectionId", collectionId()); } - bool UnifiedMailbox::isSpecial() const { - return mId == QLatin1String("inbox") - || mId == QLatin1String("sent-mail") - || mId == QLatin1String("drafts"); + return mId == Common::InboxBoxId + || mId == Common::SentBoxId + || mId == Common::DraftsBoxId; } void UnifiedMailbox::setCollectionId(qint64 id) { mCollectionId = id; } qint64 UnifiedMailbox::collectionId() const { return mCollectionId; } void UnifiedMailbox::setId(const QString &id) { mId = id; } QString UnifiedMailbox::id() const { return mId; } void UnifiedMailbox::setName(const QString &name) { mName = name; } QString UnifiedMailbox::name() const { return mName; } void UnifiedMailbox::setIcon(const QString &icon) { mIcon = icon; } QString UnifiedMailbox::icon() const { return mIcon; } void UnifiedMailbox::addSourceCollection(qint64 source) { mSources.insert(source); if (mManager) { mManager->mMonitor.setCollectionMonitored(Akonadi::Collection{source}); + mManager->mSourceToBoxMap.insert({ source, this }); } } void UnifiedMailbox::removeSourceCollection(qint64 source) { mSources.remove(source); if (mManager) { mManager->mMonitor.setCollectionMonitored(Akonadi::Collection{source}, false); + mManager->mSourceToBoxMap.erase(source); } } void UnifiedMailbox::setSourceCollections(const QSet &sources) { - const auto updateMonitor = [this](bool monitor) { - if (mManager) { - for (const auto source : mSources) { - mManager->mMonitor.setCollectionMonitored(Akonadi::Collection{source}, monitor); - } - } - }; - - updateMonitor(false); - mSources = sources; - updateMonitor(true); + while (!mSources.empty()) { + removeSourceCollection(*mSources.begin()); + } + for (auto source : sources) { + addSourceCollection(source); + } } QSet UnifiedMailbox::sourceCollections() const { return mSources; } void UnifiedMailbox::attachManager(UnifiedMailboxManager *manager) { - Q_ASSERT(manager); if (mManager != manager) { + if (manager) { + // Force that we start monitoring all the collections + for (auto source : mSources) { + manager->mMonitor.setCollectionMonitored(Akonadi::Collection{source}); + manager->mSourceToBoxMap.insert({ source, this }); + } + } else { + for (auto source : mSources) { + mManager->mMonitor.setCollectionMonitored(Akonadi::Collection{source}, false); + mManager->mSourceToBoxMap.erase(source); + } + } mManager = manager; - // Force that we start monitoring all the collections - setSourceCollections(mSources); } } diff --git a/agents/unifiedmailboxagent/unifiedmailbox.h b/agents/unifiedmailboxagent/unifiedmailbox.h index 8bf6f5847..c8266674f 100644 --- a/agents/unifiedmailboxagent/unifiedmailbox.h +++ b/agents/unifiedmailboxagent/unifiedmailbox.h @@ -1,77 +1,80 @@ /* Copyright (C) 2018 Daniel Vrátil 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef UNIFIEDMAILBOX_H #define UNIFIEDMAILBOX_H #include #include #include class KConfigGroup; class UnifiedMailboxManager; class UnifiedMailbox { friend class UnifiedMailboxManager; public: UnifiedMailbox() = default; UnifiedMailbox(UnifiedMailbox &&) = default; UnifiedMailbox &operator=(UnifiedMailbox &&) = default; UnifiedMailbox(const UnifiedMailbox &) = delete; UnifiedMailbox &operator=(const UnifiedMailbox &) = delete; + /** Compares two boxes by their ID **/ + bool operator==(const UnifiedMailbox &other) const; + void save(KConfigGroup &group) const; void load(const KConfigGroup &group); bool isSpecial() const; qint64 collectionId() const; void setCollectionId(qint64 id); QString id() const; void setId(const QString &id); QString name() const; void setName(const QString &name); QString icon() const; void setIcon(const QString &icon); void addSourceCollection(qint64 source); void removeSourceCollection(qint64 source); void setSourceCollections(const QSet &sources); QSet sourceCollections() const; private: void attachManager(UnifiedMailboxManager *manager); qint64 mCollectionId = -1; QString mId; QString mName; QString mIcon; QSet mSources; UnifiedMailboxManager *mManager = nullptr; }; Q_DECLARE_METATYPE(UnifiedMailbox*) #endif diff --git a/agents/unifiedmailboxagent/unifiedmailboxagent.cpp b/agents/unifiedmailboxagent/unifiedmailboxagent.cpp index bdd3a184e..31a92a997 100644 --- a/agents/unifiedmailboxagent/unifiedmailboxagent.cpp +++ b/agents/unifiedmailboxagent/unifiedmailboxagent.cpp @@ -1,236 +1,239 @@ /* Copyright (C) 2018 Daniel Vrátil 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "unifiedmailboxagent.h" #include "unifiedmailbox.h" #include "unifiedmailboxagent_debug.h" #include "settingsdialog.h" #include "settings.h" #include "common.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include UnifiedMailboxAgent::UnifiedMailboxAgent(const QString &id) : Akonadi::ResourceBase(id) , mBoxManager(config()) { setAgentName(i18n("Unified Mailboxes")); connect(&mBoxManager, &UnifiedMailboxManager::updateBox, this, [this](const UnifiedMailbox *box) { if (box->collectionId() <= -1) { qCWarning(agent_log) << "MailboxManager wants us to update Box but does not have its CollectionId!?"; return; } // Schedule collection sync for the box synchronizeCollection(box->collectionId()); }); auto &ifs = changeRecorder()->itemFetchScope(); ifs.setAncestorRetrieval(Akonadi::ItemFetchScope::None); ifs.setCacheOnly(true); ifs.fetchFullPayload(false); QTimer::singleShot(0, this, [this]() { qCDebug(agent_log) << "delayed init"; fixSpecialCollections(); mBoxManager.loadBoxes([this]() { // boxes loaded, let's sync up synchronize(); }); }); } void UnifiedMailboxAgent::configure(WId windowId) { QPointer agent(this); if (SettingsDialog(config(), mBoxManager, windowId).exec() && agent) { mBoxManager.saveBoxes(); synchronize(); Q_EMIT configurationDialogAccepted(); } else { mBoxManager.loadBoxes(); } } void UnifiedMailboxAgent::retrieveCollections() { Akonadi::Collection::List collections; Akonadi::Collection topLevel; topLevel.setName(identifier()); topLevel.setRemoteId(identifier()); topLevel.setParentCollection(Akonadi::Collection::root()); topLevel.setContentMimeTypes({Akonadi::Collection::mimeType()}); topLevel.setRights(Akonadi::Collection::ReadOnly); auto displayAttr = topLevel.attribute(Akonadi::Collection::AddIfMissing); displayAttr->setDisplayName(i18n("Unified Mailboxes")); displayAttr->setActiveIconName(QStringLiteral("globe")); collections.push_back(topLevel); for (const auto &boxIt : mBoxManager) { const auto &box = boxIt.second; Akonadi::Collection col; col.setName(box->id()); col.setRemoteId(box->id()); col.setParentCollection(topLevel); col.setContentMimeTypes({Common::MailMimeType}); col.setRights(Akonadi::Collection::CanChangeItem | Akonadi::Collection::CanDeleteItem); col.setVirtual(true); auto displayAttr = col.attribute(Akonadi::Collection::AddIfMissing); displayAttr->setDisplayName(box->name()); displayAttr->setIconName(box->icon()); collections.push_back(std::move(col)); } collectionsRetrieved(std::move(collections)); + + // Add mapping between boxes and collections + mBoxManager.discoverBoxCollections(); } void UnifiedMailboxAgent::retrieveItems(const Akonadi::Collection &c) { // First check that we have all Items from all source collections Q_EMIT status(Running, i18n("Synchronizing unified mailbox %1", c.displayName())); const auto unifiedBox = mBoxManager.unifiedMailboxFromCollection(c); if (!unifiedBox) { qCWarning(agent_log) << "Failed to retrieve box ID for collection " << c.id(); itemsRetrievedIncremental({}, {}); // fake incremental retrieval return; } const auto lastSeenEvent = QDateTime::fromSecsSinceEpoch(c.remoteRevision().toLongLong()); const auto sources = unifiedBox->sourceCollections(); for (auto source : sources) { auto fetch = new Akonadi::ItemFetchJob(Akonadi::Collection(source), this); fetch->setDeliveryOption(Akonadi::ItemFetchJob::EmitItemsInBatches); fetch->fetchScope().setFetchVirtualReferences(true); fetch->fetchScope().setCacheOnly(true); connect(fetch, &Akonadi::ItemFetchJob::itemsReceived, this, [this, c](const Akonadi::Item::List &items) { Akonadi::Item::List toLink; std::copy_if(items.cbegin(), items.cend(), std::back_inserter(toLink), [&c](const Akonadi::Item &item) { return !item.virtualReferences().contains(c); }); if (!toLink.isEmpty()) { new Akonadi::LinkJob(c, toLink, this); } }); } auto fetch = new Akonadi::ItemFetchJob(c, this); fetch->setDeliveryOption(Akonadi::ItemFetchJob::EmitItemsInBatches); fetch->fetchScope().setCacheOnly(true); fetch->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent); connect(fetch, &Akonadi::ItemFetchJob::itemsReceived, this, [this, unifiedBox, c](const Akonadi::Item::List &items) { Akonadi::Item::List toUnlink; std::copy_if(items.cbegin(), items.cend(), std::back_inserter(toUnlink), [&unifiedBox](const Akonadi::Item &item) { return !unifiedBox->sourceCollections().contains(item.storageCollectionId()); }); if (!toUnlink.isEmpty()) { new Akonadi::UnlinkJob(c, toUnlink, this); } }); connect(fetch, &Akonadi::ItemFetchJob::result, this, [this]() { itemsRetrievedIncremental({}, {}); // fake incremental retrieval }); } bool UnifiedMailboxAgent::retrieveItem(const Akonadi::Item &item, const QSet &parts) { // This method should never be called by Akonadi Q_UNUSED(parts); qCWarning(agent_log) << "retrieveItem() for item" << item.id() << "called but we can't own any items! This is a bug in Akonadi"; return false; } void UnifiedMailboxAgent::fixSpecialCollection(const QString &colId, Akonadi::SpecialMailCollections::Type type) { if (colId.isEmpty()) { return; } const auto id = colId.toLongLong(); // SpecialMailCollection requires the Collection to have a Resource set as well, so // we have to retrieve it first. connect(new Akonadi::CollectionFetchJob(Akonadi::Collection(id), Akonadi::CollectionFetchJob::Base, this), &Akonadi::CollectionFetchJob::collectionsReceived, this, [type](const Akonadi::Collection::List &cols) { if (cols.count() != 1) { qCWarning(agent_log) << "Identity special collection retrieval did not find a valid collection"; return; } Akonadi::SpecialMailCollections::self()->registerCollection(type, cols.first()); }); } void UnifiedMailboxAgent::fixSpecialCollections() { // This is a tiny hack to assign proper SpecialCollectionAttribute to special collections // assigned trough Identities. This should happen automatically in KMail when user changes // the special collections on the identity page, but until recent master (2018-07-24) this // wasn't the case and there's no automatic migration, so we need to fix up manually here. if (Settings::self()->fixedSpecialCollections()) { return; } qCDebug(agent_log) << "Fixing special collections assigned from Identities"; for (const auto &identity : *KIdentityManagement::IdentityManager::self()) { if (!identity.disabledFcc()) { fixSpecialCollection(identity.fcc(), Akonadi::SpecialMailCollections::SentMail); } fixSpecialCollection(identity.drafts(), Akonadi::SpecialMailCollections::Drafts); fixSpecialCollection(identity.templates(), Akonadi::SpecialMailCollections::Templates); } Settings::self()->setFixedSpecialCollections(true); } AKONADI_RESOURCE_MAIN(UnifiedMailboxAgent) diff --git a/agents/unifiedmailboxagent/unifiedmailboxmanager.cpp b/agents/unifiedmailboxagent/unifiedmailboxmanager.cpp index 16fedb2e8..e3af4df91 100644 --- a/agents/unifiedmailboxagent/unifiedmailboxmanager.cpp +++ b/agents/unifiedmailboxagent/unifiedmailboxmanager.cpp @@ -1,405 +1,423 @@ /* Copyright (C) 2018 Daniel Vrátil 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "unifiedmailboxmanager.h" #include "unifiedmailbox.h" #include "unifiedmailboxagent_debug.h" #include "utils.h" #include "common.h" #include #include #include #include #include #include #include #include #include #include #include namespace { /** * A little RAII helper to make sure changeProcessed() and replayNext() gets * called on the ChangeRecorder whenever we are done with handling a change. */ class ReplayNextOnExit { public: ReplayNextOnExit(Akonadi::ChangeRecorder &recorder) : mRecorder(recorder) {} ~ReplayNextOnExit() { mRecorder.changeProcessed(); mRecorder.replayNext(); } private: Akonadi::ChangeRecorder &mRecorder; }; } // static bool UnifiedMailboxManager::isUnifiedMailbox(const Akonadi::Collection &col) { +#ifdef UNIT_TESTS + return col.parentCollection().name() == Common::AgentIdentifier; +#else return col.resource() == Common::AgentIdentifier; +#endif } UnifiedMailboxManager::UnifiedMailboxManager(KSharedConfigPtr config, QObject* parent) : QObject(parent) , mConfig(std::move(config)) { mMonitor.setObjectName(QStringLiteral("UnifiedMailboxChangeRecorder")); mMonitor.setConfig(&mMonitorSettings); mMonitor.setChangeRecordingEnabled(true); mMonitor.setTypeMonitored(Akonadi::Monitor::Items); mMonitor.setTypeMonitored(Akonadi::Monitor::Collections); mMonitor.itemFetchScope().setCacheOnly(true); mMonitor.itemFetchScope().setFetchRemoteIdentification(false); mMonitor.itemFetchScope().setFetchModificationTime(false); mMonitor.collectionFetchScope().fetchAttribute(); connect(&mMonitor, &Akonadi::Monitor::itemAdded, this, [this](const Akonadi::Item &item, const Akonadi::Collection &collection) { ReplayNextOnExit replayNext(mMonitor); qCDebug(agent_log) << "Item" << item.id() << "added to collection" << collection.id(); const auto box = unifiedMailboxForSource(collection.id()); if (!box) { qCWarning(agent_log) << "Failed to find unified mailbox for source collection " << collection.id(); return; } if (box->collectionId() <= -1) { qCWarning(agent_log) << "Missing box->collection mapping for unified mailbox" << box->id(); return; } new Akonadi::LinkJob(Akonadi::Collection{box->collectionId()}, {item}, this); }); connect(&mMonitor, &Akonadi::Monitor::itemsRemoved, this, [this](const Akonadi::Item::List &items) { ReplayNextOnExit replayNext(mMonitor); // Monitor did the heavy lifting for us and already figured out that // we only monitor the source collection of the Items and translated // it into REMOVE change. // This relies on Akonadi never mixing Items from different sources or // destination during batch-moves. const auto parentId = items.first().parentCollection().id(); const auto box = unifiedMailboxForSource(parentId); if (!box) { qCWarning(agent_log) << "Received Remove notification for Items belonging to" << parentId << "which we don't monitor"; return; } if (box->collectionId() <= -1) { qCWarning(agent_log) << "Missing box->collection mapping for unified mailbox" << box->id(); } new Akonadi::UnlinkJob(Akonadi::Collection{box->collectionId()}, items, this); }); connect(&mMonitor, &Akonadi::Monitor::itemsMoved, this, [this](const Akonadi::Item::List &items, const Akonadi::Collection &srcCollection, const Akonadi::Collection &dstCollection) { ReplayNextOnExit replayNext(mMonitor); if (const auto srcBox = unifiedMailboxForSource(srcCollection.id())) { // Move source collection was our source, unlink the Item from a box new Akonadi::UnlinkJob(Akonadi::Collection{srcBox->collectionId()}, items, this); } if (const auto dstBox = unifiedMailboxForSource(dstCollection.id())) { // Move destination collection is our source, link the Item into a box new Akonadi::LinkJob(Akonadi::Collection{dstBox->collectionId()}, items, this); } }); connect(&mMonitor, &Akonadi::Monitor::collectionRemoved, this, [this](const Akonadi::Collection &col) { ReplayNextOnExit replayNext(mMonitor); if (auto box = unifiedMailboxForSource(col.id())) { box->removeSourceCollection(col.id()); mMonitor.setCollectionMonitored(col, false); if (box->sourceCollections().isEmpty()) { removeBox(box->id()); } saveBoxes(); // No need to resync the box collection, the linked Items got removed by Akonadi } else { qCWarning(agent_log) << "Received notification about removal of Collection" << col.id() << "which we don't monitor"; } }); connect(&mMonitor, QOverload &>::of(&Akonadi::Monitor::collectionChanged), this, [this](const Akonadi::Collection &col, const QSet &parts) { ReplayNextOnExit replayNext(mMonitor); qCDebug(agent_log) << "Collection changed:" << parts; if (!parts.contains(Akonadi::SpecialCollectionAttribute().type())) { return; } if (col.hasAttribute()) { const auto srcBox = unregisterSpecialSourceCollection(col.id()); const auto dstBox = registerSpecialSourceCollection(col); if (srcBox == dstBox) { return; } saveBoxes(); + + if (srcBox && srcBox->sourceCollections().isEmpty()) { + removeBox(srcBox->id()); + return; + } + if (srcBox) { Q_EMIT updateBox(srcBox); } if (dstBox) { Q_EMIT updateBox(dstBox); } } else { if (const auto box = unregisterSpecialSourceCollection(col.id())) { saveBoxes(); - Q_EMIT updateBox(box); + if (box->sourceCollections().isEmpty()) { + removeBox(box->id()); + } else { + Q_EMIT updateBox(box); + } } } }); } UnifiedMailboxManager::~UnifiedMailboxManager() { } +Akonadi::ChangeRecorder &UnifiedMailboxManager::changeRecorder() +{ + return mMonitor; +} + void UnifiedMailboxManager::loadBoxes(FinishedCallback &&finishedCb) { const auto group = mConfig->group("UnifiedMailboxes"); const auto boxGroups = group.groupList(); for (const auto &boxGroupName : boxGroups) { const auto boxGroup = group.group(boxGroupName); auto box = std::make_unique(); box->load(boxGroup); insertBox(std::move(box)); } + const auto cb = [this, finishedCb = std::move(finishedCb)]() { + // Only now start processing changes from change recorder + connect(&mMonitor, &Akonadi::ChangeRecorder::changesAdded, &mMonitor, &Akonadi::ChangeRecorder::replayNext, Qt::QueuedConnection); + // And start replaying any potentially pending notification + mMonitor.replayNext(); + + if (finishedCb) { + finishedCb(); + } + }; + if (mMailboxes.empty()) { - createDefaultBoxes(std::move(finishedCb)); + createDefaultBoxes(std::move(cb)); } else { - discoverBoxCollections([this, finishedCb = std::move(finishedCb)]() { - // Only now start processing changes from change recorder - connect(&mMonitor, &Akonadi::ChangeRecorder::changesAdded, &mMonitor, &Akonadi::ChangeRecorder::replayNext, Qt::QueuedConnection); - // And start replaying any potentially pending notification - mMonitor.replayNext(); - - if (finishedCb) { - finishedCb(); - } - }); + discoverBoxCollections(std::move(cb)); } } void UnifiedMailboxManager::saveBoxes() { auto group = mConfig->group("UnifiedMailboxes"); const auto currentGroups = group.groupList(); for (const auto &groupName : currentGroups) { group.deleteGroup(groupName); } for (const auto &boxIt : mMailboxes) { auto boxGroup = group.group(boxIt.second->id()); boxIt.second->save(boxGroup); } mConfig->sync(); } void UnifiedMailboxManager::insertBox(std::unique_ptr box) { auto it = mMailboxes.emplace(std::make_pair(box->id(), std::move(box))); it.first->second->attachManager(this); } void UnifiedMailboxManager::removeBox(const QString &id) { auto box = std::find_if(mMailboxes.begin(), mMailboxes.end(), [&id](const std::pair> &box) { return box.second->id() == id; }); if (box == mMailboxes.end()) { return; } - for (const auto srcCol : box->second->sourceCollections()) { - mMonitor.setCollectionMonitored(Akonadi::Collection{srcCol}, false); - } - for (auto it = mSourceToBoxMap.begin(), end = mSourceToBoxMap.end(); it != end; ) { - if (it->second == box->second.get()) { - it = mSourceToBoxMap.erase(it); - } else { - ++it; - } - } + box->second->attachManager(nullptr); mMailboxes.erase(box); } UnifiedMailbox *UnifiedMailboxManager::unifiedMailboxForSource(qint64 source) const { const auto box = mSourceToBoxMap.find(source); if (box == mSourceToBoxMap.cend()) { return {}; } return box->second; } UnifiedMailbox * UnifiedMailboxManager::unifiedMailboxFromCollection(const Akonadi::Collection &col) const { if (!isUnifiedMailbox(col)) { return nullptr; } const auto box = mMailboxes.find(col.name()); if (box == mMailboxes.cend()) { return {}; } return box->second.get(); } void UnifiedMailboxManager::createDefaultBoxes(FinishedCallback &&finishedCb) { // First build empty boxes auto inbox = std::make_unique(); inbox->attachManager(this); inbox->setId(Common::InboxBoxId); inbox->setName(i18n("Inbox")); inbox->setIcon(QStringLiteral("mail-folder-inbox")); insertBox(std::move(inbox)); auto sent = std::make_unique(); sent->attachManager(this); sent->setId(Common::SentBoxId); sent->setName(i18n("Sent")); sent->setIcon(QStringLiteral("mail-folder-sent")); insertBox(std::move(sent)); auto drafts = std::make_unique(); drafts->attachManager(this); drafts->setId(Common::DraftsBoxId); drafts->setName(i18n("Drafts")); drafts->setIcon(QStringLiteral("document-properties")); insertBox(std::move(drafts)); auto list = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::Recursive, this); list->fetchScope().fetchAttribute(); list->fetchScope().setContentMimeTypes({QStringLiteral("message/rfc822")}); +#ifdef UNIT_TESTS + list->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::Parent); +#else + list->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::None); +#endif connect(list, &Akonadi::CollectionFetchJob::collectionsReceived, this, [this](const Akonadi::Collection::List &list) { for (const auto &col : list) { if (isUnifiedMailbox(col)) { continue; } try { switch (Akonadi::SpecialMailCollections::self()->specialCollectionType(col)) { case Akonadi::SpecialMailCollections::Inbox: mMailboxes.at(Common::InboxBoxId)->addSourceCollection(col.id()); break; case Akonadi::SpecialMailCollections::SentMail: mMailboxes.at(Common::SentBoxId)->addSourceCollection(col.id()); break; case Akonadi::SpecialMailCollections::Drafts: mMailboxes.at(Common::DraftsBoxId)->addSourceCollection(col.id()); break; default: continue; } } catch (const std::out_of_range &) { qCWarning(agent_log) << "Failed to find a special unified mailbox for source collection" << col.id(); continue; } } }); connect(list, &Akonadi::CollectionFetchJob::finished, this, [this, finishedCb = std::move(finishedCb)]() { saveBoxes(); if (finishedCb) { finishedCb(); } }); } void UnifiedMailboxManager::discoverBoxCollections(FinishedCallback &&finishedCb) { auto list = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::Recursive, this); +#ifdef UNIT_TESTS + list->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::Parent); +#else list->fetchScope().setResource(Common::AgentIdentifier); +#endif connect(list, &Akonadi::CollectionFetchJob::collectionsReceived, this, [this](const Akonadi::Collection::List &list) { for (const auto &col : list) { - if (isUnifiedMailbox(col)) { + if (!isUnifiedMailbox(col)) { continue; } mMailboxes.at(col.name())->setCollectionId(col.id()); } }); - connect(list, &Akonadi::CollectionFetchJob::finished, - this, [finishedCb = std::move(finishedCb)]() { - if (finishedCb) { - finishedCb(); - } - }); + if (finishedCb) { + connect(list, &Akonadi::CollectionFetchJob::finished, + this, finishedCb); + } } const UnifiedMailbox *UnifiedMailboxManager::registerSpecialSourceCollection(const Akonadi::Collection& col) { // This is slightly awkward, wold be better if we could use SpecialMailCollections, // but it also relies on Monitor internally, so there's a possible race condition // between our ChangeRecorder and SpecialMailCollections' Monitor auto attr = col.attribute(); Q_ASSERT(attr); if (!attr) { return {}; } decltype(mMailboxes)::iterator box; if (attr->collectionType() == Common::SpecialCollectionInbox) { box = mMailboxes.find(Common::InboxBoxId); } else if (attr->collectionType() == Common::SpecialCollectionSentMail) { box = mMailboxes.find(Common::SentBoxId); } else if (attr->collectionType() == Common::SpecialCollectionDrafts) { box = mMailboxes.find(Common::DraftsBoxId); } if (box == mMailboxes.end()) { return {}; } box->second->addSourceCollection(col.id()); - mMonitor.setCollectionMonitored(col); return box->second.get(); } const UnifiedMailbox *UnifiedMailboxManager::unregisterSpecialSourceCollection(qint64 colId) { auto box = unifiedMailboxForSource(colId); if (!box->isSpecial()) { + qDebug() << colId << "does not belong to a special unified box" << box->id(); return {}; } box->removeSourceCollection(colId); - mMonitor.setCollectionMonitored(Akonadi::Collection{colId}, false); return box; } diff --git a/agents/unifiedmailboxagent/unifiedmailboxmanager.h b/agents/unifiedmailboxagent/unifiedmailboxmanager.h index d0d2ad612..544977ccc 100644 --- a/agents/unifiedmailboxagent/unifiedmailboxmanager.h +++ b/agents/unifiedmailboxagent/unifiedmailboxmanager.h @@ -1,88 +1,91 @@ /* Copyright (C) 2018 Daniel Vrátil 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef UNIFIEDBOXMANAGER_H #define UNIFIEDBOXMANAGER_H #include "utils.h" #include #include #include #include #include #include #include #include class UnifiedMailbox; class UnifiedMailboxManager : public QObject { Q_OBJECT friend class UnifiedMailbox; public: using FinishedCallback = std::function; + using Entry = std::pair>; explicit UnifiedMailboxManager(KSharedConfigPtr config, QObject *parent = nullptr); ~UnifiedMailboxManager() override; void loadBoxes(FinishedCallback &&cb = {}); void saveBoxes(); + void discoverBoxCollections(FinishedCallback &&cb = {}); void insertBox(std::unique_ptr box); void removeBox(const QString &id); UnifiedMailbox *unifiedMailboxForSource(qint64 source) const; UnifiedMailbox *unifiedMailboxFromCollection(const Akonadi::Collection &col) const; inline auto begin() const { return mMailboxes.begin(); } inline auto end() const { return mMailboxes.end(); } - void discoverBoxCollections(FinishedCallback &&cb); - static bool isUnifiedMailbox(const Akonadi::Collection &col); + // Internal change recorder, for unittests + Akonadi::ChangeRecorder &changeRecorder(); Q_SIGNALS: void updateBox(const UnifiedMailbox *box); private: void createDefaultBoxes(FinishedCallback &&cb); + const UnifiedMailbox *unregisterSpecialSourceCollection(qint64 colId); const UnifiedMailbox *registerSpecialSourceCollection(const Akonadi::Collection &col); // Using unordered_map because Qt containers do not support movable-only types std::unordered_map> mMailboxes; std::unordered_map mSourceToBoxMap; Akonadi::ChangeRecorder mMonitor; QSettings mMonitorSettings; KSharedConfigPtr mConfig; }; #endif