You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
429 lines
15 KiB
429 lines
15 KiB
/* |
|
SPDX-FileCopyrightText: 2018 Daniel Vrátil <dvratil@kde.org> |
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later |
|
*/ |
|
|
|
#include "unifiedmailboxmanager.h" |
|
#include "unifiedmailbox.h" |
|
#include "unifiedmailboxagent_debug.h" |
|
#include "common.h" |
|
#include "settings.h" |
|
|
|
#include <KLocalizedString> |
|
|
|
#include <AkonadiCore/SpecialCollectionAttribute> |
|
#include <AkonadiCore/CollectionFetchJob> |
|
#include <AkonadiCore/CollectionFetchScope> |
|
#include <AkonadiCore/LinkJob> |
|
#include <AkonadiCore/UnlinkJob> |
|
#include <AkonadiCore/ItemFetchScope> |
|
#include <Akonadi/KMime/SpecialMailCollections> |
|
|
|
#include <QTimer> |
|
|
|
#include <stdexcept> // for std::out_of_range |
|
|
|
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<Akonadi::SpecialCollectionAttribute>(); |
|
connect(&mMonitor, &Akonadi::Monitor::itemAdded, |
|
this, [this](const Akonadi::Item &item, const Akonadi::Collection &collection) { |
|
ReplayNextOnExit replayNext(mMonitor); |
|
|
|
qCDebug(UNIFIEDMAILBOXAGENT_LOG) << "Item" << item.id() << "added to collection" << collection.id(); |
|
const auto box = unifiedMailboxForSource(collection.id()); |
|
if (!box) { |
|
qCWarning(UNIFIEDMAILBOXAGENT_LOG) << "Failed to find unified mailbox for source collection " << collection.id(); |
|
return; |
|
} |
|
|
|
if (box->collectionId() <= -1) { |
|
qCWarning(UNIFIEDMAILBOXAGENT_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(UNIFIEDMAILBOXAGENT_LOG) << "Received Remove notification for Items belonging to" << parentId << "which we don't monitor"; |
|
return; |
|
} |
|
if (box->collectionId() <= -1) { |
|
qCWarning(UNIFIEDMAILBOXAGENT_LOG) << "Missing box->collection mapping for unified mailbox" << box->id(); |
|
return; |
|
} |
|
|
|
new Akonadi::UnlinkJob(Akonadi::Collection {box->collectionId()}, items, this); |
|
}); |
|
connect(&mMonitor, &Akonadi::Monitor::itemsMoved, |
|
this, [this](const Akonadi::Item::List &items, const Akonadi::Collection &srcCollection, |
|
const Akonadi::Collection &dstCollection) { |
|
ReplayNextOnExit replayNext(mMonitor); |
|
|
|
if (const auto srcBox = unifiedMailboxForSource(srcCollection.id())) { |
|
// Move source collection was our source, unlink the Item from a box |
|
new Akonadi::UnlinkJob(Akonadi::Collection {srcBox->collectionId()}, items, this); |
|
} |
|
if (const auto dstBox = unifiedMailboxForSource(dstCollection.id())) { |
|
// Move destination collection is our source, link the Item into a box |
|
new Akonadi::LinkJob(Akonadi::Collection {dstBox->collectionId()}, items, this); |
|
} |
|
}); |
|
|
|
connect(&mMonitor, &Akonadi::Monitor::collectionRemoved, |
|
this, [this](const Akonadi::Collection &col) { |
|
ReplayNextOnExit replayNext(mMonitor); |
|
|
|
if (auto box = unifiedMailboxForSource(col.id())) { |
|
box->removeSourceCollection(col.id()); |
|
mMonitor.setCollectionMonitored(col, false); |
|
if (box->sourceCollections().isEmpty()) { |
|
removeBox(box->id()); |
|
} |
|
saveBoxes(); |
|
// No need to resync the box collection, the linked Items got removed by Akonadi |
|
} else { |
|
qCWarning(UNIFIEDMAILBOXAGENT_LOG) << "Received notification about removal of Collection" << col.id() << "which we don't monitor"; |
|
} |
|
}); |
|
connect(&mMonitor, qOverload<const Akonadi::Collection &, const QSet<QByteArray> &>(&Akonadi::Monitor::collectionChanged), |
|
this, [this](const Akonadi::Collection &col, const QSet<QByteArray> &parts) { |
|
ReplayNextOnExit replayNext(mMonitor); |
|
|
|
qCDebug(UNIFIEDMAILBOXAGENT_LOG) << "Collection changed:" << parts; |
|
if (!parts.contains(Akonadi::SpecialCollectionAttribute().type())) { |
|
return; |
|
} |
|
|
|
if (col.hasAttribute<Akonadi::SpecialCollectionAttribute>()) { |
|
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() |
|
= default; |
|
|
|
Akonadi::ChangeRecorder &UnifiedMailboxManager::changeRecorder() |
|
{ |
|
return mMonitor; |
|
} |
|
|
|
void UnifiedMailboxManager::loadBoxes(FinishedCallback &&finishedCb) |
|
{ |
|
qCDebug(UNIFIEDMAILBOXAGENT_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<UnifiedMailbox>(); |
|
box->load(boxGroup); |
|
insertBox(std::move(box)); |
|
} |
|
|
|
const auto cb = [this, finishedCb = std::move(finishedCb)]() { |
|
qCDebug(UNIFIEDMAILBOXAGENT_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(UNIFIEDMAILBOXAGENT_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(); |
|
mConfig->reparseConfiguration(); |
|
} |
|
|
|
void UnifiedMailboxManager::insertBox(std::unique_ptr<UnifiedMailbox> 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<const QString, std::unique_ptr<UnifiedMailbox> > &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) |
|
{ |
|
if (!Settings::self()->createDefaultBoxes()) { |
|
return; |
|
} |
|
// First build empty boxes |
|
auto inbox = std::make_unique<UnifiedMailbox>(); |
|
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<UnifiedMailbox>(); |
|
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<UnifiedMailbox>(); |
|
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<Akonadi::SpecialCollectionAttribute>(); |
|
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(UNIFIEDMAILBOXAGENT_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(); |
|
} |
|
}); |
|
#ifndef UNIT_TESTS |
|
Settings::self()->setCreateDefaultBoxes(false); |
|
Settings::self()->save(); |
|
#endif |
|
} |
|
|
|
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; |
|
} |
|
const auto it = mMailboxes.find(col.name()); |
|
if (it == mMailboxes.end()) { |
|
qCWarning(UNIFIEDMAILBOXAGENT_LOG) << "Failed to find an unified mailbox for source collection" << col.id(); |
|
} else { |
|
it->second->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<Akonadi::SpecialCollectionAttribute>(); |
|
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(UNIFIEDMAILBOXAGENT_LOG) << colId << "does not belong to a special unified box" << box->id(); |
|
return {}; |
|
} |
|
|
|
box->removeSourceCollection(colId); |
|
return box; |
|
}
|
|
|