diff --git a/agents/unifiedmailboxagent/unifiedmailbox.cpp b/agents/unifiedmailboxagent/unifiedmailbox.cpp index 260954a81..16e7bfc3c 100644 --- a/agents/unifiedmailboxagent/unifiedmailbox.cpp +++ b/agents/unifiedmailboxagent/unifiedmailbox.cpp @@ -1,121 +1,140 @@ /* 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" -UnifiedMailbox::UnifiedMailbox(UnifiedMailboxManager *manager) - : mManager(manager) +#include + + +void UnifiedMailbox::load(const KConfigGroup &group) +{ + mId = group.name(); + mName = group.readEntry("name"); + mIcon = group.readEntry("icon", QStringLiteral("folder-mail")); + mSources = listToSet(group.readEntry("sources", QList{})); + // This is not authoritative, we will do collection discovery anyway + mCollectionId = group.readEntry("collectionId", -1ll); +} + +void UnifiedMailbox::save(KConfigGroup& group) const { + group.writeEntry("name", name()); + group.writeEntry("icon", icon()); + group.writeEntry("sources", setToList(sourceCollections())); + // just for cacheing, we will do collection discovery on next start anyway + group.writeEntry("collectionId", collectionId()); } + bool UnifiedMailbox::isSpecial() const { return mId == QLatin1String("inbox") || mId == QLatin1String("sent-mail") || mId == QLatin1String("drafts"); } void UnifiedMailbox::setCollectionId(qint64 id) { mCollectionId = id; } qint64 UnifiedMailbox::collectionId() const { return mCollectionId; } void UnifiedMailbox::setId(const QString &id) { mId = id; } QString UnifiedMailbox::id() const { return mId; } void UnifiedMailbox::setName(const QString &name) { mName = name; } QString UnifiedMailbox::name() const { return mName; } void UnifiedMailbox::setIcon(const QString &icon) { mIcon = icon; } QString UnifiedMailbox::icon() const { return mIcon; } void UnifiedMailbox::addSourceCollection(qint64 source) { mSources.insert(source); if (mManager) { mManager->mMonitor.setCollectionMonitored(Akonadi::Collection{source}); } } void UnifiedMailbox::removeSourceCollection(qint64 source) { mSources.remove(source); if (mManager) { mManager->mMonitor.setCollectionMonitored(Akonadi::Collection{source}, false); } } void UnifiedMailbox::setSourceCollections(const QSet &sources) { const auto updateMonitor = [this](bool monitor) { if (mManager) { for (const auto source : mSources) { mManager->mMonitor.setCollectionMonitored(Akonadi::Collection{source}, monitor); } } }; updateMonitor(false); mSources = sources; updateMonitor(true); } QSet UnifiedMailbox::sourceCollections() const { return mSources; } void UnifiedMailbox::attachManager(UnifiedMailboxManager *manager) { Q_ASSERT(manager); if (mManager != manager) { mManager = manager; // Force that we start monitoring all the collections setSourceCollections(mSources); } } diff --git a/agents/unifiedmailboxagent/unifiedmailbox.h b/agents/unifiedmailboxagent/unifiedmailbox.h index 229091f7b..8bf6f5847 100644 --- a/agents/unifiedmailboxagent/unifiedmailbox.h +++ b/agents/unifiedmailboxagent/unifiedmailbox.h @@ -1,74 +1,77 @@ /* Copyright (C) 2018 Daniel Vrátil This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef UNIFIEDMAILBOX_H #define UNIFIEDMAILBOX_H #include #include #include +class KConfigGroup; class UnifiedMailboxManager; + class UnifiedMailbox { friend class UnifiedMailboxManager; public: UnifiedMailbox() = default; UnifiedMailbox(UnifiedMailbox &&) = default; UnifiedMailbox &operator=(UnifiedMailbox &&) = default; UnifiedMailbox(const UnifiedMailbox &) = delete; UnifiedMailbox &operator=(const UnifiedMailbox &) = delete; + void save(KConfigGroup &group) const; + void load(const KConfigGroup &group); + bool isSpecial() const; qint64 collectionId() const; void setCollectionId(qint64 id); QString id() const; void setId(const QString &id); QString name() const; void setName(const QString &name); QString icon() const; void setIcon(const QString &icon); void addSourceCollection(qint64 source); void removeSourceCollection(qint64 source); void setSourceCollections(const QSet &sources); QSet sourceCollections() const; private: - UnifiedMailbox(UnifiedMailboxManager *manager); - void attachManager(UnifiedMailboxManager *manager); qint64 mCollectionId = -1; QString mId; QString mName; QString mIcon; QSet mSources; UnifiedMailboxManager *mManager = nullptr; }; Q_DECLARE_METATYPE(UnifiedMailbox*) #endif diff --git a/agents/unifiedmailboxagent/unifiedmailboxmanager.cpp b/agents/unifiedmailboxagent/unifiedmailboxmanager.cpp index 97fce85c0..7f7d7947c 100644 --- a/agents/unifiedmailboxagent/unifiedmailboxmanager.cpp +++ b/agents/unifiedmailboxagent/unifiedmailboxmanager.cpp @@ -1,413 +1,406 @@ /* 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 #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) { return col.resource() == QLatin1String("akonadi_unifiedmailbox_agent"); } UnifiedMailboxManager::UnifiedMailboxManager(KSharedConfigPtr config, QObject* parent) : QObject(parent) , mConfig(std::move(config)) { mMonitor.setObjectName(QStringLiteral("UnifiedMailboxChangeRecorder")); mMonitor.setConfig(&mMonitorSettings); mMonitor.setChangeRecordingEnabled(true); mMonitor.setTypeMonitored(Akonadi::Monitor::Items); mMonitor.setTypeMonitored(Akonadi::Monitor::Collections); mMonitor.itemFetchScope().setCacheOnly(true); mMonitor.itemFetchScope().setFetchRemoteIdentification(false); mMonitor.itemFetchScope().setFetchModificationTime(false); mMonitor.collectionFetchScope().fetchAttribute(); connect(&mMonitor, &Akonadi::Monitor::itemAdded, this, [this](const Akonadi::Item &item, const Akonadi::Collection &collection) { ReplayNextOnExit replayNext(mMonitor); qCDebug(agent_log) << "Item" << item.id() << "added to collection" << collection.id(); const auto box = unifiedMailboxForSource(collection.id()); if (!box) { qCWarning(agent_log) << "Failed to find unified mailbox for source collection " << collection.id(); return; } if (box->collectionId() <= -1) { qCWarning(agent_log) << "Missing box->collection mapping for unified mailbox" << box->id(); return; } new Akonadi::LinkJob(Akonadi::Collection{box->collectionId()}, {item}, this); }); connect(&mMonitor, &Akonadi::Monitor::itemsRemoved, this, [this](const Akonadi::Item::List &items) { ReplayNextOnExit replayNext(mMonitor); // Monitor did the heavy lifting for us and already figured out that // we only monitor the source collection of the Items and translated // it into REMOVE change. // This relies on Akonadi never mixing Items from different sources or // destination during batch-moves. const auto parentId = items.first().parentCollection().id(); const auto box = unifiedMailboxForSource(parentId); if (!box) { qCWarning(agent_log) << "Received Remove notification for Items belonging to" << parentId << "which we don't monitor"; return; } if (box->collectionId() <= -1) { qCWarning(agent_log) << "Missing box->collection mapping for unified mailbox" << box->id(); } new Akonadi::UnlinkJob(Akonadi::Collection{box->collectionId()}, items, this); }); connect(&mMonitor, &Akonadi::Monitor::itemsMoved, this, [this](const Akonadi::Item::List &items, const Akonadi::Collection &srcCollection, const Akonadi::Collection &dstCollection) { ReplayNextOnExit replayNext(mMonitor); if (const auto srcBox = unifiedMailboxForSource(srcCollection.id())) { // Move source collection was our source, unlink the Item from a box new Akonadi::UnlinkJob(Akonadi::Collection{srcBox->collectionId()}, items, this); } if (const auto dstBox = unifiedMailboxForSource(dstCollection.id())) { // Move destination collection is our source, link the Item into a box new Akonadi::LinkJob(Akonadi::Collection{dstBox->collectionId()}, items, this); } //TODO Settings::self()->setLastSeenEvent(std::chrono::steady_clock::now().time_since_epoch().count()); }); connect(&mMonitor, &Akonadi::Monitor::collectionRemoved, this, [this](const Akonadi::Collection &col) { ReplayNextOnExit replayNext(mMonitor); if (auto box = unifiedMailboxForSource(col.id())) { box->removeSourceCollection(col.id()); mMonitor.setCollectionMonitored(col, false); if (box->sourceCollections().isEmpty()) { removeBox(box->id()); } saveBoxes(); // No need to resync the box collection, the linked Items got removed by Akonadi } else { qCWarning(agent_log) << "Received notification about removal of Collection" << col.id() << "which we don't monitor"; } }); connect(&mMonitor, QOverload &>::of(&Akonadi::Monitor::collectionChanged), this, [this](const Akonadi::Collection &col, const QSet &parts) { ReplayNextOnExit replayNext(mMonitor); qCDebug(agent_log) << "Collection changed:" << parts; if (!parts.contains(Akonadi::SpecialCollectionAttribute().type())) { return; } if (col.hasAttribute()) { const auto srcBox = unregisterSpecialSourceCollection(col.id()); const auto dstBox = registerSpecialSourceCollection(col); if (srcBox == dstBox) { return; } saveBoxes(); if (srcBox) { Q_EMIT updateBox(srcBox); } if (dstBox) { Q_EMIT updateBox(dstBox); } } else { if (const auto box = unregisterSpecialSourceCollection(col.id())) { saveBoxes(); Q_EMIT updateBox(box); } } }); } UnifiedMailboxManager::~UnifiedMailboxManager() { } void UnifiedMailboxManager::loadBoxes(LoadCallback &&cb) { 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->setId(boxGroupName); - box->setName(boxGroup.readEntry("name")); - box->setIcon(boxGroup.readEntry("icon", QStringLiteral("folder-mail"))); - QList sources = boxGroup.readEntry("sources", QList{}); - box->setSourceCollections(listToSet(std::move(sources))); + box->load(boxGroup); insertBox(std::move(box)); } if (mMailboxes.empty()) { createDefaultBoxes(std::move(cb)); } else { discoverBoxCollections([this, cb = std::move(cb)]() { // Only now start processing changes from change recorder connect(&mMonitor, &Akonadi::ChangeRecorder::changesAdded, &mMonitor, &Akonadi::ChangeRecorder::replayNext, Qt::QueuedConnection); // And start replaying any potentially pending notification mMonitor.replayNext(); if (cb) { 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) { - const auto &box = boxIt.second; - auto boxGroup = group.group(box->id()); - boxGroup.writeEntry("name", box->name()); - boxGroup.writeEntry("icon", box->icon()); - boxGroup.writeEntry("sources", setToList(box->sourceCollections())); + auto boxGroup = group.group(boxIt.second->id()); + boxIt.second->save(boxGroup); } mConfig->sync(); } void UnifiedMailboxManager::insertBox(std::unique_ptr box) { auto it = mMailboxes.emplace(std::make_pair(box->id(), std::move(box))); it.first->second->attachManager(this); } void UnifiedMailboxManager::removeBox(const QString &id) { auto box = std::find_if(mMailboxes.begin(), mMailboxes.end(), [&id](const std::pair> &box) { return box.second->id() == id; }); if (box == mMailboxes.end()) { return; } for (const auto srcCol : box->second->sourceCollections()) { mMonitor.setCollectionMonitored(Akonadi::Collection{srcCol}, false); } for (auto it = mSourceToBoxMap.begin(), end = mSourceToBoxMap.end(); it != end; ) { if (it->second == box->second.get()) { it = mSourceToBoxMap.erase(it); } else { ++it; } } 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(LoadCallback &&cb) { // First build empty boxes auto inbox = std::make_unique(); inbox->attachManager(this); inbox->setId(QStringLiteral("inbox")); inbox->setName(i18n("Inbox")); inbox->setIcon(QStringLiteral("mail-folder-inbox")); insertBox(std::move(inbox)); auto sent = std::make_unique(); sent->attachManager(this); sent->setId(QStringLiteral("sent-mail")); sent->setName(i18n("Sent")); sent->setIcon(QStringLiteral("mail-folder-sent")); insertBox(std::move(sent)); auto drafts = std::make_unique(); drafts->attachManager(this); drafts->setId(QStringLiteral("drafts")); 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")}); 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(QStringLiteral("inbox"))->addSourceCollection(col.id()); break; case Akonadi::SpecialMailCollections::SentMail: mMailboxes.at(QStringLiteral("sent-mail"))->addSourceCollection(col.id()); break; case Akonadi::SpecialMailCollections::Drafts: mMailboxes.at(QStringLiteral("drafts"))->addSourceCollection(col.id()); break; default: continue; } } catch (const std::out_of_range &) { qCWarning(agent_log) << "Failed to find a special unified mailbox for source collection" << col.id(); continue; } } }); connect(list, &Akonadi::CollectionFetchJob::finished, this, [this, cb = std::move(cb)]() { saveBoxes(); if (cb) { cb(); } }); } void UnifiedMailboxManager::discoverBoxCollections(LoadCallback &&cb) { auto list = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::Recursive, this); list->fetchScope().setResource(QStringLiteral("akonadi_unifiedmailbox_agent")); connect(list, &Akonadi::CollectionFetchJob::collectionsReceived, this, [this](const Akonadi::Collection::List &list) { for (const auto &col : list) { if (col.name() == QLatin1String("akonadi_unifiedmailbox_agent")) { continue; } mMailboxes.at(col.name())->setCollectionId(col.id()); } }); connect(list, &Akonadi::CollectionFetchJob::finished, this, [cb = std::move(cb)]() { if (cb) { cb(); } }); } 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() == "inbox") { box = mMailboxes.find(QStringLiteral("inbox")); } else if (attr->collectionType() == "sent-mail") { box = mMailboxes.find(QStringLiteral("sent-mail")); } else if (attr->collectionType() == "drafts") { box = mMailboxes.find(QStringLiteral("drafts")); } if (box == mMailboxes.end()) { return {}; } box->second->addSourceCollection(col.id()); mMonitor.setCollectionMonitored(col); return box->second.get(); } const UnifiedMailbox *UnifiedMailboxManager::unregisterSpecialSourceCollection(qint64 colId) { auto box = unifiedMailboxForSource(colId); if (!box->isSpecial()) { return {}; } box->removeSourceCollection(colId); mMonitor.setCollectionMonitored(Akonadi::Collection{colId}, false); return box; }