diff --git a/agents/CMakeLists.txt b/agents/CMakeLists.txt --- a/agents/CMakeLists.txt +++ b/agents/CMakeLists.txt @@ -6,3 +6,4 @@ add_subdirectory(archivemailagent) add_subdirectory(mailfilteragent) add_subdirectory(followupreminderagent) +add_subdirectory(unifiedmailboxagent) diff --git a/agents/unifiedmailboxagent/CMakeLists.txt b/agents/unifiedmailboxagent/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/CMakeLists.txt @@ -0,0 +1,51 @@ +add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_unifiedmailbox_agent\") + +set(CMAKE_CXX_STANDARD 14) + +if(BUILD_TESTING) + 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 +) + +qt5_add_dbus_adaptor(unifiedmailbox_agent_SRCS org.freedesktop.Akonadi.UnifiedMailboxAgent.xml unifiedmailboxagent.h UnifiedMailboxAgent) + +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 + KF5::DBusAddons +) + +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/Messages.sh b/agents/unifiedmailboxagent/Messages.sh new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/Messages.sh @@ -0,0 +1,2 @@ +#! /bin/sh +$XGETTEXT `find . -name '*.h' -o -name '*.cpp' | grep -v '/autotests/'` -o $podir/akonadi_unifiedmailbox_agent.pot diff --git a/agents/unifiedmailboxagent/autotests/CMakeLists.txt b/agents/unifiedmailboxagent/autotests/CMakeLists.txt new file mode 100644 --- /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 --- /dev/null +++ b/agents/unifiedmailboxagent/autotests/unifiedmailboxmanagertest.cpp @@ -0,0 +1,666 @@ +/* + 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 "../utils.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) + + + +stdx::optional collectionForId(qint64 id) +{ + auto fetch = new Akonadi::CollectionFetchJob(Akonadi::Collection(id), Akonadi::CollectionFetchJob::Base); + fetch->fetchScope().fetchAttribute(); + AKVERIFY_RET(fetch->exec(), stdx::nullopt); + const auto cols = fetch->collections(); + AKCOMPARE_RET(cols.count(), 1, stdx::nullopt); + AKVERIFY_RET(cols.first().isValid(), stdx::nullopt); + return cols.first(); +} + +stdx::optional 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(), stdx::nullopt); + 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(), stdx::nullopt); + return *colIt; +} + +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, {}); + 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; +}; + +stdx::optional 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(), stdx::nullopt); + col = createCol->collection(); + if (col.isValid()) { + deleter << col; + return col; + } + return stdx::nullopt; +} + + +} // 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); + 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 auto parentCol = collectionForRid(Common::AgentIdentifier); + QVERIFY(parentCol); + + const auto inboxBoxCol = createCollection(Common::InboxBoxId, parentCol.value(), deleter); + QVERIFY(inboxBoxCol); + + const auto sentBoxCol = createCollection(Common::SentBoxId, parentCol.value(), deleter); + QVERIFY(sentBoxCol); + + // 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.value()); + QVERIFY(box != nullptr); + QCOMPARE(box->collectionId(), inboxBoxCol->id()); + + box = manager.unifiedMailboxFromCollection(sentBoxCol.value()); + 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); + + const auto inboxBoxCol = createCollection(Common::InboxBoxId, parentCol.value(), deleter); + QVERIFY(inboxBoxCol); + + // 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); + + // Setup up a monitor to to be notified when an item gets linked into + // the unified mailbox collection + Akonadi::Monitor monitor; + monitor.setCollectionMonitored(inboxBoxCol.value()); + 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.value()); + item.setPayload(QByteArray{"Hello world!"}); + auto createItem = new Akonadi::ItemCreateJob(item, inboxSourceCol.value(), 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); + + const auto inboxBoxCol = createCollection(Common::InboxBoxId, parentCol.value(), deleter); + QVERIFY(inboxBoxCol); + + // 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); + + // Setup up a monitor to to be notified when an item gets linked into + // the unified mailbox collection + Akonadi::Monitor monitor; + monitor.setCollectionMonitored(inboxBoxCol.value()); + 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.value()); + item.setPayload(QByteArray{"Hello world!"}); + auto createItem = new Akonadi::ItemCreateJob(item, inboxSourceCol.value(), 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); + + // Now move the Item to an unmonitored collection + auto move = new Akonadi::ItemMoveJob(item, destinationCol.value(), 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); + + const auto inboxBoxCol = createCollection(Common::InboxBoxId, parentCol.value(), deleter); + QVERIFY(inboxBoxCol); + + const auto draftsBoxCol = createCollection(Common::DraftsBoxId, parentCol.value(), deleter); + QVERIFY(draftsBoxCol); + + // 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); + const auto draftsSourceCol = collectionForRid(QStringLiteral("res1_drafts")); + QVERIFY(draftsSourceCol); + + + // Setup up a monitor to to be notified when an item gets linked into + // the unified mailbox collection + Akonadi::Monitor monitor; + monitor.setCollectionMonitored(inboxBoxCol.value()); + monitor.setCollectionMonitored(draftsBoxCol.value()); + 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.value()); + item.setPayload(QByteArray{"Hello world!"}); + auto createItem = new Akonadi::ItemCreateJob(item, inboxSourceCol.value(), 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.value(), 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); + + const auto inboxBoxCol = createCollection(Common::InboxBoxId, parentCol.value(), deleter); + QVERIFY(inboxBoxCol); + + // 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); + auto delJob = new Akonadi::CollectionDeleteJob(inboxSourceCol.value(), 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.value()); + QVERIFY(inboxBox); + QVERIFY(!inboxBox->sourceCollections().contains(inboxSourceCol->id())); + QVERIFY(!changeRecorder.collectionsMonitored().contains(inboxSourceCol.value())); + 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); + delJob = new Akonadi::CollectionDeleteJob(inboxSourceCol.value(), this); + AKVERIFYEXEC(delJob); + + // Wait for the change recorder once again + QVERIFY(crRemovedSpy.wait()); + QTest::qWait(0); + + QVERIFY(!manager.unifiedMailboxFromCollection(inboxBoxCol.value())); + QVERIFY(!changeRecorder.collectionsMonitored().contains(inboxSourceCol.value())); + 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); + + const auto sentBoxCol = createCollection(Common::SentBoxId, parentCol.value(), deleter); + QVERIFY(sentBoxCol); + + // 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); + sentSourceCol->removeAttribute(); + auto modify = new Akonadi::CollectionModifyJob(sentSourceCol.value(), 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.value()); + QVERIFY(sourceBox); + QVERIFY(!sourceBox->sourceCollections().contains(sentSourceCol->id())); + QVERIFY(!changeRecorder.collectionsMonitored().contains(sentSourceCol.value())); + 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); + sentSourceCol->attribute()->setCollectionType("drafts"); + modify = new Akonadi::CollectionModifyJob(sentSourceCol.value(), 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.value())); + + // The collection is still monitored: it belongs to the Drafts special box now! + QVERIFY(changeRecorder.collectionsMonitored().contains(sentSourceCol.value())); + 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 --- /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 --- /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 --- /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 --- /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 --- /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 --- /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 --- /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 --- /dev/null +++ b/agents/unifiedmailboxagent/autotests/unittestenv/xdglocal/testdata-res3.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/agents/unifiedmailboxagent/common.h b/agents/unifiedmailboxagent/common.h new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/common.h @@ -0,0 +1,41 @@ +/* + 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 COMMON_H +#define COMMON_H + +#include + +namespace Common { + +static const auto MailMimeType = QStringLiteral("message/rfc822"); + +static const auto InboxBoxId = QStringLiteral("inbox"); +static const auto SentBoxId = QStringLiteral("sent-mail"); +static const auto DraftsBoxId = QStringLiteral("drafts"); + +static constexpr auto SpecialCollectionInbox = "inbox"; +static constexpr auto SpecialCollectionSentMail = "send-mail"; +static constexpr auto SpecialCollectionDrafts = "drafts"; + +static const auto AgentIdentifier = QStringLiteral("akonadi_unifiedmailbox_agent"); + +} + +#endif diff --git a/agents/unifiedmailboxagent/mailkernel.h b/agents/unifiedmailboxagent/mailkernel.h new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/mailkernel.h @@ -0,0 +1,74 @@ +/* + 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 MAILKERNEL_H +#define MAILKERNEL_H + +#include +#include + +namespace Akonadi { +class EntityTreeModel; +class EntityMimeTypeFilterModel; +} + +namespace MailCommon { +class FolderCollectionMonitor; +} + +class MailKernel : public QObject + , public MailCommon::IKernel + , public MailCommon::ISettings +{ + Q_OBJECT +public: + explicit MailKernel(KSharedConfigPtr config, QObject *parent = nullptr); + ~MailKernel() override; + + KIdentityManagement::IdentityManager *identityManager() override; + MessageComposer::MessageSender *msgSender() override; + + Akonadi::EntityMimeTypeFilterModel *collectionModel() const override; + KSharedConfig::Ptr config() override; + void syncConfig() override; + MailCommon::JobScheduler *jobScheduler() const override; + Akonadi::ChangeRecorder *folderCollectionMonitor() const override; + void updateSystemTray() override; + + qreal closeToQuotaThreshold() override; + bool excludeImportantMailFromExpiry() override; + QStringList customTemplates() override; + Akonadi::Collection::Id lastSelectedFolder() override; + void setLastSelectedFolder(Akonadi::Collection::Id col) override; + bool showPopupAfterDnD() override; + void expunge(Akonadi::Collection::Id id, bool sync) override; + +private: + Q_DISABLE_COPY(MailKernel) + + KSharedConfigPtr mConfig; + KIdentityManagement::IdentityManager *mIdentityManager = nullptr; + MessageComposer::MessageSender *mMessageSender = nullptr; + MailCommon::FolderCollectionMonitor *mFolderCollectionMonitor = nullptr; + Akonadi::EntityTreeModel *mEntityTreeModel = nullptr; + Akonadi::EntityMimeTypeFilterModel *mCollectionModel = nullptr; +}; + +#endif + diff --git a/agents/unifiedmailboxagent/mailkernel.cpp b/agents/unifiedmailboxagent/mailkernel.cpp new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/mailkernel.cpp @@ -0,0 +1,144 @@ +/* + 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 "mailkernel.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +MailKernel::MailKernel(KSharedConfigPtr config, QObject *parent) + : QObject(parent) + , mConfig(config) +{ + mMessageSender = new MessageComposer::AkonadiSender(this); + mIdentityManager = new KIdentityManagement::IdentityManager(true, this); + Akonadi::Session *session = new Akonadi::Session("UnifiedMailbox Kernel ETM", this); + + mFolderCollectionMonitor = new MailCommon::FolderCollectionMonitor(session, this); + + mEntityTreeModel = new Akonadi::EntityTreeModel(folderCollectionMonitor(), this); + mEntityTreeModel->setListFilter(Akonadi::CollectionFetchScope::Enabled); + mEntityTreeModel->setItemPopulationStrategy(Akonadi::EntityTreeModel::LazyPopulation); + + mCollectionModel = new Akonadi::EntityMimeTypeFilterModel(this); + mCollectionModel->setSourceModel(mEntityTreeModel); + mCollectionModel->addMimeTypeInclusionFilter(Akonadi::Collection::mimeType()); + mCollectionModel->setHeaderGroup(Akonadi::EntityTreeModel::CollectionTreeHeaders); + mCollectionModel->setDynamicSortFilter(true); + mCollectionModel->setSortCaseSensitivity(Qt::CaseInsensitive); + + CommonKernel->registerKernelIf(this); + CommonKernel->registerSettingsIf(this); +} + +MailKernel::~MailKernel() +{ + CommonKernel->registerKernelIf(nullptr); + CommonKernel->registerSettingsIf(nullptr); +} + +KIdentityManagement::IdentityManager *MailKernel::identityManager() +{ + return mIdentityManager; +} + +MessageComposer::MessageSender *MailKernel::msgSender() +{ + return mMessageSender; +} + +Akonadi::EntityMimeTypeFilterModel *MailKernel::collectionModel() const +{ + return mCollectionModel; +} + +KSharedConfig::Ptr MailKernel::config() +{ + return mConfig; +} + +void MailKernel::syncConfig() +{ + Q_ASSERT(false); +} + +MailCommon::JobScheduler *MailKernel::jobScheduler() const +{ + Q_ASSERT(false); + return nullptr; +} + +Akonadi::ChangeRecorder *MailKernel::folderCollectionMonitor() const +{ + return mFolderCollectionMonitor->monitor(); +} + +void MailKernel::updateSystemTray() +{ + Q_ASSERT(false); +} + +bool MailKernel::showPopupAfterDnD() +{ + return false; +} + +qreal MailKernel::closeToQuotaThreshold() +{ + return 80; +} + +QStringList MailKernel::customTemplates() +{ + Q_ASSERT(false); + return QStringList(); +} + +bool MailKernel::excludeImportantMailFromExpiry() +{ + Q_ASSERT(false); + return true; +} + +Akonadi::Collection::Id MailKernel::lastSelectedFolder() +{ + Q_ASSERT(false); + return Akonadi::Collection::Id(); +} + +void MailKernel::setLastSelectedFolder(Akonadi::Collection::Id col) +{ + Q_UNUSED(col); +} + +void MailKernel::expunge(Akonadi::Collection::Id id, bool sync) +{ + Akonadi::Collection col(id); + if (col.isValid()) { + mFolderCollectionMonitor->expunge(Akonadi::Collection(col), sync); + } +} + diff --git a/agents/unifiedmailboxagent/org.freedesktop.Akonadi.UnifiedMailboxAgent.xml b/agents/unifiedmailboxagent/org.freedesktop.Akonadi.UnifiedMailboxAgent.xml new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/org.freedesktop.Akonadi.UnifiedMailboxAgent.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/agents/unifiedmailboxagent/settings.kcfg b/agents/unifiedmailboxagent/settings.kcfg new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/settings.kcfg @@ -0,0 +1,15 @@ + + + + + false + + + false + + + + diff --git a/agents/unifiedmailboxagent/settings.kcfgc b/agents/unifiedmailboxagent/settings.kcfgc new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/settings.kcfgc @@ -0,0 +1,5 @@ +# Code generation options for kconfig_compiler +File=settings.kcfg +ClassName=Settings +Singleton=true +Mutators=true diff --git a/agents/unifiedmailboxagent/settingsdialog.h b/agents/unifiedmailboxagent/settingsdialog.h new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/settingsdialog.h @@ -0,0 +1,56 @@ +/* + 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 SETTINGSDIALOG_H_ +#define SETTINGSDIALOG_H_ + +#include + +#include + +#include "unifiedmailboxmanager.h" + +class QStandardItemModel; +class MailKernel; + +class SettingsDialog : public QDialog +{ + Q_OBJECT +public: + explicit SettingsDialog(KSharedConfigPtr config, UnifiedMailboxManager &manager, + WId windowId, QWidget *parent = nullptr); + ~SettingsDialog() override; + +public Q_SLOTS: + void accept() override; + +private: + void loadBoxes(); + void addBox(UnifiedMailbox *box); + +private: + QStandardItemModel *mBoxModel = nullptr; + UnifiedMailboxManager &mBoxManager; + MailKernel *mKernel = nullptr; + KSharedConfigPtr mConfig; +}; + + + +#endif diff --git a/agents/unifiedmailboxagent/settingsdialog.cpp b/agents/unifiedmailboxagent/settingsdialog.cpp new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/settingsdialog.cpp @@ -0,0 +1,162 @@ +/* + 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 "settingsdialog.h" +#include "unifiedmailboxmanager.h" +#include "unifiedmailboxeditor.h" +#include "unifiedmailbox.h" +#include "mailkernel.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace { + +static constexpr const char *DialogGroup = "__Dialog"; + +} + +SettingsDialog::SettingsDialog(KSharedConfigPtr config, UnifiedMailboxManager &boxManager, WId, QWidget *parent) + : QDialog(parent) + , mBoxManager(boxManager) + , mKernel(new MailKernel(config, this)) + , mConfig(config) +{ + auto l = new QVBoxLayout; + setLayout(l); + + auto h = new QHBoxLayout; + l->addLayout(h); + mBoxModel = new QStandardItemModel(this); + auto view = new QListView(this); + view->setEditTriggers(QListView::NoEditTriggers); + view->setModel(mBoxModel); + h->addWidget(view); + + auto v = new QVBoxLayout; + h->addLayout(v); + auto addButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-add-symbolic")), i18n("Add")); + v->addWidget(addButton); + connect(addButton, &QPushButton::clicked, + this, [this]() { + auto mailbox = std::make_unique(); + auto editor = new UnifiedMailboxEditor(mailbox.get(), mConfig, this); + if (editor->exec()) { + mailbox->setId(mailbox->name()); // assign ID + addBox(mailbox.get()); + mBoxManager.insertBox(std::move(mailbox)); + } + }); + auto editButton = new QPushButton(QIcon::fromTheme(QStringLiteral("entry-edit")), i18n("Modify")); + editButton->setEnabled(false); + v->addWidget(editButton); + connect(editButton, &QPushButton::clicked, + this, [this, view]() { + const auto indexes = view->selectionModel()->selectedIndexes(); + if (!indexes.isEmpty()) { + auto item = mBoxModel->itemFromIndex(indexes[0]); + auto mailbox = item->data().value(); + auto editor = new UnifiedMailboxEditor(mailbox, mConfig, this); + if (editor->exec()) { + item->setText(mailbox->name()); + item->setIcon(QIcon::fromTheme(mailbox->icon())); + } + } + }); + auto removeButton = new QPushButton(QIcon::fromTheme(QStringLiteral("list-remove-symbolic")), i18n("Remove")); + removeButton->setEnabled(false); + v->addWidget(removeButton); + connect(removeButton, &QPushButton::clicked, + this, [this, view]() { + const auto indexes = view->selectionModel()->selectedIndexes(); + if (!indexes.isEmpty()) { + auto item = mBoxModel->itemFromIndex(indexes[0]); + const auto mailbox = item->data().value(); + if (KMessageBox::warningYesNo( + this, i18n("Do you really want to remove unified mailbox %1?", mailbox->name()), + i18n("Really Remove?"), KStandardGuiItem::remove(), KStandardGuiItem::cancel()) == KMessageBox::Yes) + { + mBoxModel->removeRow(item->row()); + mBoxManager.removeBox(mailbox->id()); + } + } + }); + v->addStretch(1); + + connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, + this, [view, editButton, removeButton]() { + const bool hasSelection = view->selectionModel()->hasSelection(); + editButton->setEnabled(hasSelection); + removeButton->setEnabled(hasSelection); + }); + + auto box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(box, &QDialogButtonBox::accepted, this, &SettingsDialog::accept); + connect(box, &QDialogButtonBox::rejected, this, &SettingsDialog::reject); + l->addWidget(box); + + loadBoxes(); + + const auto dlgGroup = config->group(DialogGroup); + if (dlgGroup.hasKey("geometry")) { + restoreGeometry(dlgGroup.readEntry("geometry", QByteArray())); + } else { + resize(500, 500); + } + +} + +SettingsDialog::~SettingsDialog() +{ + auto dlgGroup = mConfig->group(DialogGroup); + dlgGroup.writeEntry("geometry", saveGeometry()); +} + +void SettingsDialog::accept() +{ + mBoxManager.saveBoxes(); + + QDialog::accept(); +} + +void SettingsDialog::loadBoxes() +{ + mBoxModel->clear(); + for (const auto &mailboxIt : mBoxManager) { + addBox(mailboxIt.second.get()); + } +} + +void SettingsDialog::addBox(UnifiedMailbox *box) +{ + auto item = new QStandardItem(QIcon::fromTheme(box->icon()), box->name()); + item->setData(QVariant::fromValue(box)); + mBoxModel->appendRow(item); +} diff --git a/agents/unifiedmailboxagent/unifiedmailbox.h b/agents/unifiedmailboxagent/unifiedmailbox.h new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/unifiedmailbox.h @@ -0,0 +1,83 @@ +/* + 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 + +#include "utils.h" + +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; + + stdx::optional 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); + + stdx::optional mCollectionId; + QString mId; + QString mName; + QString mIcon; + QSet mSources; + + UnifiedMailboxManager *mManager = nullptr; +}; + +Q_DECLARE_METATYPE(UnifiedMailbox*) + + +#endif diff --git a/agents/unifiedmailboxagent/unifiedmailbox.cpp b/agents/unifiedmailboxagent/unifiedmailbox.cpp new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/unifiedmailbox.cpp @@ -0,0 +1,155 @@ +/* + 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", mName); + group.writeEntry("icon", mIcon); + group.writeEntry("sources", setToList(mSources)); + // just for cacheing, we will do collection discovery on next start anyway + if (mCollectionId) { + group.writeEntry("collectionId", *mCollectionId); + } else { + group.deleteEntry("collectionId"); + } +} + +bool UnifiedMailbox::isSpecial() const +{ + return mId == Common::InboxBoxId + || mId == Common::SentBoxId + || mId == Common::DraftsBoxId; +} + +void UnifiedMailbox::setCollectionId(qint64 id) +{ + mCollectionId = id; +} + +stdx::optional 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) +{ + while (!mSources.empty()) { + removeSourceCollection(*mSources.begin()); + } + for (auto source : sources) { + addSourceCollection(source); + } +} + +QSet UnifiedMailbox::sourceCollections() const +{ + return mSources; +} + +void UnifiedMailbox::attachManager(UnifiedMailboxManager *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; + } +} + + diff --git a/agents/unifiedmailboxagent/unifiedmailboxagent.h b/agents/unifiedmailboxagent/unifiedmailboxagent.h new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/unifiedmailboxagent.h @@ -0,0 +1,63 @@ +/* + 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 UNIFIEDMAILBOXAGENT_H +#define UNIFIEDMAILBOXAGENT_H + +#include +#include + +#include "unifiedmailboxmanager.h" + +#include +#include + +/* Despite its name, this is actually a Resource, but it acts as an Agent: it + * listens to notifications about Items that belong to other resources and acts + * on them. + * The only reason this agent has to implement ResourceBase is to be able to own + * the virtual unified collections into which content of other collections is + * linked. + */ +class UnifiedMailboxAgent : public Akonadi::ResourceBase +{ + Q_OBJECT + +public: + explicit UnifiedMailboxAgent(const QString &id); + ~UnifiedMailboxAgent() override = default; + + void configure(WId windowId) override; + + void setEnableAgent(bool enable); + bool enabledAgent() const; + + void retrieveCollections() override; + void retrieveItems(const Akonadi::Collection &collection) override; + bool retrieveItem(const Akonadi::Item &item, const QSet &parts) override; +private: + void delayedInit(); + + void fixSpecialCollections(); + void fixSpecialCollection(const QString &colId, Akonadi::SpecialMailCollections::Type type); + + UnifiedMailboxManager mBoxManager; +}; + +#endif diff --git a/agents/unifiedmailboxagent/unifiedmailboxagent.cpp b/agents/unifiedmailboxagent/unifiedmailboxagent.cpp new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/unifiedmailboxagent.cpp @@ -0,0 +1,292 @@ +/* + 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 "unifiedmailboxagentadaptor.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 + +#include +#include +#include + + +UnifiedMailboxAgent::UnifiedMailboxAgent(const QString &id) + : Akonadi::ResourceBase(id) + , mBoxManager(config()) +{ + setAgentName(i18n("Unified Mailboxes")); + + new UnifiedMailboxAgentAdaptor(this); + KDBusConnectionPool::threadConnection().registerObject(QStringLiteral("/UnifiedMailboxAgent"), this, QDBusConnection::ExportAdaptors); + const auto service = Akonadi::ServerManager::agentServiceName(Akonadi::ServerManager::Resource, identifier()); + KDBusConnectionPool::threadConnection().registerService(service); + + connect(&mBoxManager, &UnifiedMailboxManager::updateBox, + this, [this](const UnifiedMailbox *box) { + if (!box->collectionId()) { + 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().value()); + }); + + auto &ifs = changeRecorder()->itemFetchScope(); + ifs.setAncestorRetrieval(Akonadi::ItemFetchScope::None); + ifs.setCacheOnly(true); + ifs.fetchFullPayload(false); + + if (Settings::self()->enabled()) { + QTimer::singleShot(0, this, &UnifiedMailboxAgent::delayedInit); + } +} + +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::delayedInit() +{ + qCDebug(agent_log) << "delayed init"; + + fixSpecialCollections(); + mBoxManager.loadBoxes([this]() { + // boxes loaded, let's sync up + synchronize(); + }); +} + + +bool UnifiedMailboxAgent::enabledAgent() const +{ + return Settings::self()->enabled(); +} + +void UnifiedMailboxAgent::setEnableAgent(bool enabled) +{ + if (enabled != Settings::self()->enabled()) { + Settings::self()->setEnabled(enabled); + Settings::self()->save(); + if (!enabled) { + setOnline(false); + auto fetch = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::Recursive, this); + fetch->fetchScope().setResource(identifier()); + connect(fetch, &Akonadi::CollectionFetchJob::collectionsReceived, + this, [this](const Akonadi::Collection::List &cols) { + for (const auto &col : cols) { + new Akonadi::CollectionDeleteJob(col, this); + } + }); + } else { + setOnline(true); + delayedInit(); + } + } +} + + +void UnifiedMailboxAgent::retrieveCollections() +{ + if (!Settings::self()->enabled()) { + collectionsRetrieved({}); + return; + } + + 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) +{ + if (!Settings::self()->enabled()) { + itemsRetrieved({}); + return; + } + + // 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/unifiedmailboxagent.desktop b/agents/unifiedmailboxagent/unifiedmailboxagent.desktop new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/unifiedmailboxagent.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=Unified Mailbox Agent +Comment=Unified Mailbox Agent +Type=AkonadiAgent +Exec=akonadi_unifiedmailbox_agent +Icon=folder-mail +X-Akonadi-MimeTypes=message/rfc822 +X-Akonadi-Capabilities=Resource,Unique,Autostart +X-Akonadi-Identifier=akonadi_unifiedmailbox_agent diff --git a/agents/unifiedmailboxagent/unifiedmailboxeditor.h b/agents/unifiedmailboxagent/unifiedmailboxeditor.h new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/unifiedmailboxeditor.h @@ -0,0 +1,37 @@ +/* + 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 + +#include "unifiedmailboxmanager.h" + +class UnifiedMailbox; +class UnifiedMailboxEditor : public QDialog +{ + Q_OBJECT +public: + explicit UnifiedMailboxEditor(const KSharedConfigPtr &config, QWidget *parent = nullptr); + explicit UnifiedMailboxEditor(UnifiedMailbox *mailbox, const KSharedConfigPtr &config, + QWidget *parent = nullptr); + + ~UnifiedMailboxEditor() override; +private: + UnifiedMailbox *mMailbox = nullptr; + KSharedConfigPtr mConfig; +}; diff --git a/agents/unifiedmailboxagent/unifiedmailboxeditor.cpp b/agents/unifiedmailboxagent/unifiedmailboxeditor.cpp new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/unifiedmailboxeditor.cpp @@ -0,0 +1,184 @@ +/* + 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 "unifiedmailboxeditor.h" +#include "unifiedmailbox.h" +#include "mailkernel.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace { + +static constexpr const char *EditorGroup = "__Editor"; + +class SelfFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + explicit SelfFilterProxyModel(QObject *parent = nullptr) + : QSortFilterProxyModel(parent) + {} + + QVariant data(const QModelIndex &index, int role) const override + { + if (role == Qt::CheckStateRole) { + // Make top-level collections uncheckable + const Akonadi::Collection col = data(index, Akonadi::EntityTreeModel::CollectionRole).value(); + if (col.parentCollection() == Akonadi::Collection::root()) { + return {}; + } + } + + return QSortFilterProxyModel::data(index, role); + } + + Qt::ItemFlags flags(const QModelIndex &index) const override + { + // Make top-level collections uncheckable + const Akonadi::Collection col = data(index, Akonadi::EntityTreeModel::CollectionRole).value(); + if (col.parentCollection() == Akonadi::Collection::root()) { + return QSortFilterProxyModel::flags(index) & ~Qt::ItemIsUserCheckable; + } else { + return QSortFilterProxyModel::flags(index); + } + } + + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override + { + // Hide ourselves + const auto sourceIndex = sourceModel()->index(source_row, 0, source_parent); + const Akonadi::Collection col = sourceModel()->data(sourceIndex, Akonadi::EntityTreeModel::CollectionRole).value(); + return !UnifiedMailboxManager::isUnifiedMailbox(col); + } +}; + +} + +UnifiedMailboxEditor::UnifiedMailboxEditor(const KSharedConfigPtr &config, QWidget* parent) + : UnifiedMailboxEditor({}, config, parent) +{ +} + +UnifiedMailboxEditor::UnifiedMailboxEditor(UnifiedMailbox *mailbox, const KSharedConfigPtr &config, QWidget *parent) + : QDialog(parent) + , mMailbox(mailbox) + , mConfig(config) +{ + auto l = new QVBoxLayout; + setLayout(l); + + auto f = new QFormLayout; + l->addLayout(f); + auto nameEdit = new QLineEdit(mMailbox->name()); + f->addRow(i18n("Name:"), nameEdit); + connect(nameEdit, &QLineEdit::textChanged, + this, [this](const QString &name) { + mMailbox->setName(name); + }); + + auto iconButton = new QPushButton(QIcon::fromTheme(mMailbox->icon(), QIcon::fromTheme(QStringLiteral("folder-mail"))), + i18n("Pick icon...")); + f->addRow(i18n("Icon:"), iconButton); + connect(iconButton, &QPushButton::clicked, + this, [iconButton, this]() { + const auto iconName = KIconDialog::getIcon(); + if (!iconName.isEmpty()) { + mMailbox->setIcon(iconName); + iconButton->setIcon(QIcon::fromTheme(iconName)); + } + }); + mMailbox->setIcon(iconButton->icon().name()); + + l->addSpacing(10); + + auto ftw = new MailCommon::FolderTreeWidget(nullptr, nullptr, + MailCommon::FolderTreeWidget::TreeViewOptions(MailCommon::FolderTreeWidget::UseDistinctSelectionModel | + MailCommon::FolderTreeWidget::HideStatistics)); + l->addWidget(ftw); + + auto ftv = ftw->folderTreeView(); + auto sourceModel = ftv->model(); + auto selectionModel = ftw->selectionModel(); + + auto checkable = new KCheckableProxyModel(this); + checkable->setSourceModel(sourceModel); + checkable->setSelectionModel(selectionModel); + const auto sources = mMailbox->sourceCollections(); + for (const auto source : sources) { + const auto index = Akonadi::EntityTreeModel::modelIndexForCollection(selectionModel->model(), Akonadi::Collection(source)); + selectionModel->select(index, QItemSelectionModel::Select); + } + connect(checkable->selectionModel(), &QItemSelectionModel::selectionChanged, + this, [this](const QItemSelection &selected, const QItemSelection &deselected) { + auto indexes = selected.indexes(); + for (const auto &index : indexes) { + mMailbox->addSourceCollection(index.data(Akonadi::EntityTreeModel::CollectionIdRole).toLongLong()); + } + indexes = deselected.indexes(); + for (const auto &index : indexes) { + mMailbox->removeSourceCollection(index.data(Akonadi::EntityTreeModel::CollectionIdRole).toLongLong()); + } + }); + + auto selfFilter = new SelfFilterProxyModel(this); + selfFilter->setSourceModel(checkable); + + ftv->setModel(selfFilter); + ftv->expandAll(); + + auto box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(nameEdit, &QLineEdit::textChanged, + box, [box](const QString &name) { + box->button(QDialogButtonBox::Ok)->setEnabled(!name.isEmpty()); + }); + box->button(QDialogButtonBox::Ok)->setEnabled(!nameEdit->text().isEmpty()); + l->addWidget(box); + + const auto editorGroup = config->group(EditorGroup); + if (editorGroup.hasKey("geometry")) { + restoreGeometry(editorGroup.readEntry("geometry", QByteArray())); + } else { + resize(500, 900); + } +} + +UnifiedMailboxEditor::~UnifiedMailboxEditor() +{ + auto editorGrp = mConfig->group(EditorGroup); + editorGrp.writeEntry("geometry", saveGeometry()); +} + + +#include "unifiedmailboxeditor.moc" diff --git a/agents/unifiedmailboxagent/unifiedmailboxmanager.h b/agents/unifiedmailboxagent/unifiedmailboxmanager.h new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/unifiedmailboxmanager.h @@ -0,0 +1,92 @@ +/* + 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(const 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(); + } + + 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 std::unique_ptr because QScopedPointer is not movable + // Using std::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 diff --git a/agents/unifiedmailboxagent/unifiedmailboxmanager.cpp b/agents/unifiedmailboxagent/unifiedmailboxmanager.cpp new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/unifiedmailboxmanager.cpp @@ -0,0 +1,433 @@ +/* + 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 +#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(const KSharedConfigPtr &config, QObject* parent) + : QObject(parent) + , mConfig(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()) { + qCWarning(agent_log) << "Missing box->collection mapping for unified mailbox" << box->id(); + return; + } + + new Akonadi::LinkJob(Akonadi::Collection{box->collectionId().value()}, {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()) { + qCWarning(agent_log) << "Missing box->collection mapping for unified mailbox" << box->id(); + return; + } + + new Akonadi::UnlinkJob(Akonadi::Collection{box->collectionId().value()}, 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().value()}, 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().value()}, 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 &>(&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(); + 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) +{ + qCDebug(agent_log) << "loading boxes"; + 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)]() { + qCDebug(agent_log) << "Finished callback: enabling change recorder"; + // 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 + QTimer::singleShot(0, &mMonitor, &Akonadi::ChangeRecorder::replayNext); + + if (finishedCb) { + finishedCb(); + } + }; + + qCDebug(agent_log) << "Loaded" << mMailboxes.size() << "boxes from config"; + + if (mMailboxes.empty()) { + createDefaultBoxes(std::move(cb)); + } else { + 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; + } + + 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::result, + 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) || col.parentCollection() == Akonadi::Collection::root()) { + continue; + } + + mMailboxes.at(col.name())->setCollectionId(col.id()); + } + }); + if (finishedCb) { + connect(list, &Akonadi::CollectionFetchJob::result, 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()); + return box->second.get(); +} + +const UnifiedMailbox *UnifiedMailboxManager::unregisterSpecialSourceCollection(qint64 colId) +{ + auto box = unifiedMailboxForSource(colId); + if (!box) { + return {}; + } + + if (!box->isSpecial()) { + qCDebug(agent_log) << colId << "does not belong to a special unified box" << box->id(); + return {}; + } + + box->removeSourceCollection(colId); + return box; +} diff --git a/agents/unifiedmailboxagent/utils.h b/agents/unifiedmailboxagent/utils.h new file mode 100644 --- /dev/null +++ b/agents/unifiedmailboxagent/utils.h @@ -0,0 +1,72 @@ +/* + 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 UTILS_H_ +#define UTILS_H_ + +#include +#include +#include + +#include +namespace stdx { + // Injects content of std::experimental namespace into "exp" namespace. + // C++ is magical. + using namespace std::experimental; +} + +template +inline QList setToList(QSet &&set) +{ + QList rv; + rv.reserve(set.size()); + std::copy(set.cbegin(), set.cend(), std::back_inserter(rv)); + return rv; +} + +template +inline QList setToList(const QSet &set) +{ + QList rv; + rv.reserve(set.size()); + std::copy(set.cbegin(), set.cend(), std::back_inserter(rv)); + return rv; +} + +template +inline QSet listToSet(QList &&list) +{ + QSet rv; + rv.reserve(list.size()); + for (auto t : list) { + rv.insert(std::move(t)); + } + return rv; +} + +namespace std { + template<> + struct hash { + inline size_t operator()(const QString &str) const { + return qHash(str); + } + }; +} + +#endif diff --git a/kmail.categories b/kmail.categories --- a/kmail.categories +++ b/kmail.categories @@ -4,4 +4,4 @@ org.kde.pim.followupreminderagent kmail (followupreminderagent) org.kde.pim.kmail_plugin kmail (kmail kontact plugins) org.kde.pim.mailfilteragent kmail (mailfilter agent) - +org.kde.pim.unifiedmailboxagent kmail (unifiedmailboxagent) diff --git a/src/configuredialog/configureaccountpage.cpp b/src/configuredialog/configureaccountpage.cpp --- a/src/configuredialog/configureaccountpage.cpp +++ b/src/configuredialog/configureaccountpage.cpp @@ -195,7 +195,7 @@ mAccountsReceiving.mAccountsReceiving->setMimeTypeFilter(QStringList() << KMime::Message::mimeType()); mAccountsReceiving.mAccountsReceiving->setCapabilityFilter(QStringList() << QStringLiteral("Resource")); - mAccountsReceiving.mAccountsReceiving->setExcludeCapabilities(QStringList() << QStringLiteral("MailTransport") << QStringLiteral("Notes")); + mAccountsReceiving.mAccountsReceiving->setExcludeCapabilities(QStringList() << QStringLiteral("MailTransport") << QStringLiteral("Notes") << QStringLiteral("Autostart")); KConfig specialMailCollection(QStringLiteral("specialmailcollectionsrc")); if (specialMailCollection.hasGroup(QStringLiteral("SpecialCollections"))) { diff --git a/src/configuredialog/configureplugins/configurepluginslistwidget.cpp b/src/configuredialog/configureplugins/configurepluginslistwidget.cpp --- a/src/configuredialog/configureplugins/configurepluginslistwidget.cpp +++ b/src/configuredialog/configureplugins/configurepluginslistwidget.cpp @@ -44,6 +44,7 @@ #include #include #include +#include namespace { QString pluginEditorGroupName() @@ -242,6 +243,7 @@ mPluginUtilDataList << createAgentPluginData(QStringLiteral("akonadi_archivemail_agent"), QStringLiteral("/ArchiveMailAgent")); mPluginUtilDataList << createAgentPluginData(QStringLiteral("akonadi_newmailnotifier_agent"), QStringLiteral("/NewMailNotifierAgent")); mPluginUtilDataList << createAgentPluginData(QStringLiteral("akonadi_followupreminder_agent"), QStringLiteral("/FollowUpReminder")); + mPluginUtilDataList << createAgentPluginData(QStringLiteral("akonadi_unifiedmailbox_agent"), QStringLiteral("/UnifiedMailboxAgent")); PimCommon::ConfigurePluginsListWidget::fillTopItems(mPluginUtilDataList, i18n("Akonadi Agents"), diff --git a/src/kmkernel.h b/src/kmkernel.h --- a/src/kmkernel.h +++ b/src/kmkernel.h @@ -443,7 +443,6 @@ void slotRunBackgroundTasks(); void slotConfigChanged(); - Q_SIGNALS: void configChanged(); void onlineStatusChanged(KMailSettings::EnumNetworkState::type); @@ -452,6 +451,7 @@ void startCheckMail(); void endCheckMail(); + void incomingAccountsChanged(); private Q_SLOTS: /** Updates identities when a transport has been deleted. */ void transportRemoved(int id, const QString &name); @@ -467,6 +467,7 @@ void slotCollectionRemoved(const Akonadi::Collection &col); void slotDeleteIdentity(uint identity); void slotInstanceRemoved(const Akonadi::AgentInstance &); + void slotInstanceAdded(const Akonadi::AgentInstance &); void slotSystemNetworkStatusChanged(bool isOnline); void slotCollectionChanged(const Akonadi::Collection &, const QSet &set); diff --git a/src/kmkernel.cpp b/src/kmkernel.cpp --- a/src/kmkernel.cpp +++ b/src/kmkernel.cpp @@ -208,6 +208,8 @@ connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceRemoved, this, &KMKernel::slotInstanceRemoved); + connect(Akonadi::AgentManager::self(), &Akonadi::AgentManager::instanceAdded, this, &KMKernel::slotInstanceAdded); + connect(PimCommon::NetworkManager::self()->networkConfigureManager(), &QNetworkConfigurationManager::onlineStateChanged, this, &KMKernel::slotSystemNetworkStatusChanged); @@ -1809,6 +1811,17 @@ mResourceCryptoSettingCache.remove(identifier); } mFolderArchiveManager->slotInstanceRemoved(instance); + + if (MailCommon::Util::isMailAgent(instance)) { + Q_EMIT incomingAccountsChanged(); + } +} + +void KMKernel::slotInstanceAdded(const Akonadi::AgentInstance &instance) +{ + if (MailCommon::Util::isMailAgent(instance)) { + Q_EMIT incomingAccountsChanged(); + } } void KMKernel::savePaneSelection() diff --git a/src/kmmainwidget.h b/src/kmmainwidget.h --- a/src/kmmainwidget.h +++ b/src/kmmainwidget.h @@ -513,6 +513,7 @@ void slotPageIsScrolledToBottom(bool isAtBottom); void printCurrentMessage(bool preview); void replyCurrentMessageCommand(MessageComposer::ReplyStrategy strategy); + void setupUnifiedMailboxChecker(); QAction *filterToAction(MailCommon::MailFilter *filter); Akonadi::Collection::List applyFilterOnCollection(bool recursive); diff --git a/src/kmmainwidget.cpp b/src/kmmainwidget.cpp --- a/src/kmmainwidget.cpp +++ b/src/kmmainwidget.cpp @@ -185,6 +185,9 @@ // System includes #include #include +#include +#include +#include #include "PimCommonAkonadi/ManageServerSideSubscriptionJob" #include @@ -323,6 +326,8 @@ mCheckMailTimer.setInterval(3 * 1000); mCheckMailTimer.setSingleShot(true); connect(&mCheckMailTimer, &QTimer::timeout, this, &KMMainWidget::slotUpdateActionsAfterMailChecking); + + setupUnifiedMailboxChecker(); } void KMMainWidget::restoreCollectionFolderViewConfig() @@ -4700,3 +4705,51 @@ messageView()->viewer()->setFocus(); } } + +void KMMainWidget::setupUnifiedMailboxChecker() +{ + if (!KMailSettings::self()->askEnableUnifiedMailboxes()) { + return; + } + + const auto ask = [this]() { + if (!KMailSettings::self()->askEnableUnifiedMailboxes()) { + return; + } + + if (kmkernel->accounts().count() <= 1) { + return; + } + + const auto service = Akonadi::ServerManager::self()->agentServiceName(Akonadi::ServerManager::Agent, QStringLiteral("akonadi_unifiedmailbox_agent")); + QDBusInterface iface(service, QStringLiteral("/"), QStringLiteral("org.freedesktop.Akonadi.UnifiedMailboxAgent"), + QDBusConnection::sessionBus(), this); + if (!iface.isValid()) { + return; + } + + QDBusReply reply = iface.call(QStringLiteral("enabledAgent")); + if (!reply.isValid() || bool(reply)) { + return; + } + + const auto answer = KMessageBox::questionYesNo( + this, i18n("You have more than one email account set up. Do you want to enable the Unified Mailbox feature to " + "show unified content of your inbox, sent and drafts folders?\n" + "You can configure unified mailboxes, create custom ones or disable the feature completely in KMail's Plugin settings."), + i18n("Enable Unified Mailboxes?"), + KGuiItem(i18n("Enable Unified Mailboxes"), QStringLiteral("dialog-ok")), + KGuiItem(i18n("Cancel"), QStringLiteral("dialog-cancel"))); + if (answer == KMessageBox::Yes) { + iface.call(QStringLiteral("setEnableAgent"), true); + } + + KMailSettings::self()->setAskEnableUnifiedMailboxes(false); + }; + + connect(kmkernel, &KMKernel::incomingAccountsChanged, this, ask); + + // Wait for a bit before asking so we at least have the window on screen + QTimer::singleShot(500, this, ask); +} + diff --git a/src/settings/kmail.kcfg.cmake b/src/settings/kmail.kcfg.cmake --- a/src/settings/kmail.kcfg.cmake +++ b/src/settings/kmail.kcfg.cmake @@ -134,7 +134,10 @@ - + + true + +