File indexing completed on 2024-12-22 05:00:56

0001 /*
0002    SPDX-FileCopyrightText: 2018 Daniel Vrátil <dvratil@kde.org>
0003 
0004    SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "unifiedmailboxmanager.h"
0008 #include "common.h"
0009 #include "settings.h"
0010 #include "unifiedmailbox.h"
0011 #include "unifiedmailboxagent_debug.h"
0012 
0013 #include <KLocalizedString>
0014 
0015 #include <Akonadi/CollectionFetchJob>
0016 #include <Akonadi/CollectionFetchScope>
0017 #include <Akonadi/ItemFetchScope>
0018 #include <Akonadi/LinkJob>
0019 #include <Akonadi/SpecialCollectionAttribute>
0020 #include <Akonadi/SpecialMailCollections>
0021 #include <Akonadi/UnlinkJob>
0022 
0023 #include <QTimer>
0024 
0025 #include <stdexcept> // for std::out_of_range
0026 
0027 namespace
0028 {
0029 /**
0030  * A little RAII helper to make sure changeProcessed() and replayNext() gets
0031  * called on the ChangeRecorder whenever we are done with handling a change.
0032  */
0033 class ReplayNextOnExit
0034 {
0035 public:
0036     ReplayNextOnExit(Akonadi::ChangeRecorder &recorder)
0037         : mRecorder(recorder)
0038     {
0039     }
0040 
0041     ~ReplayNextOnExit()
0042     {
0043         mRecorder.changeProcessed();
0044         mRecorder.replayNext();
0045     }
0046 
0047 private:
0048     Akonadi::ChangeRecorder &mRecorder;
0049 };
0050 }
0051 
0052 // static
0053 bool UnifiedMailboxManager::isUnifiedMailbox(const Akonadi::Collection &col)
0054 {
0055 #ifdef UNIT_TESTS
0056     return col.parentCollection().name() == Common::AgentIdentifier;
0057 #else
0058     return col.resource() == Common::AgentIdentifier;
0059 #endif
0060 }
0061 
0062 UnifiedMailboxManager::UnifiedMailboxManager(const KSharedConfigPtr &config, QObject *parent)
0063     : QObject(parent)
0064     , mConfig(config)
0065 {
0066     mMonitor.setObjectName(QLatin1StringView("UnifiedMailboxChangeRecorder"));
0067     mMonitor.setConfig(&mMonitorSettings);
0068     mMonitor.setChangeRecordingEnabled(true);
0069     mMonitor.setTypeMonitored(Akonadi::Monitor::Items);
0070     mMonitor.setTypeMonitored(Akonadi::Monitor::Collections);
0071     mMonitor.itemFetchScope().setCacheOnly(true);
0072     mMonitor.itemFetchScope().setFetchRemoteIdentification(false);
0073     mMonitor.itemFetchScope().setFetchModificationTime(false);
0074     mMonitor.collectionFetchScope().fetchAttribute<Akonadi::SpecialCollectionAttribute>();
0075     connect(&mMonitor, &Akonadi::Monitor::itemAdded, this, [this](const Akonadi::Item &item, const Akonadi::Collection &collection) {
0076         ReplayNextOnExit replayNext(mMonitor);
0077 
0078         qCDebug(UNIFIEDMAILBOXAGENT_LOG) << "Item" << item.id() << "added to collection" << collection.id();
0079         const auto box = unifiedMailboxForSource(collection.id());
0080         if (!box) {
0081             qCWarning(UNIFIEDMAILBOXAGENT_LOG) << "Failed to find unified mailbox for source collection " << collection.id();
0082             return;
0083         }
0084 
0085         if (box->collectionId() <= -1) {
0086             qCWarning(UNIFIEDMAILBOXAGENT_LOG) << "Missing box->collection mapping for unified mailbox" << box->id();
0087             return;
0088         }
0089 
0090         new Akonadi::LinkJob(Akonadi::Collection{box->collectionId()}, {item}, this);
0091     });
0092     connect(&mMonitor, &Akonadi::Monitor::itemsRemoved, this, [this](const Akonadi::Item::List &items) {
0093         ReplayNextOnExit replayNext(mMonitor);
0094 
0095         // Monitor did the heavy lifting for us and already figured out that
0096         // we only monitor the source collection of the Items and translated
0097         // it into REMOVE change.
0098 
0099         // This relies on Akonadi never mixing Items from different sources or
0100         // destination during batch-moves.
0101         const auto parentId = items.first().parentCollection().id();
0102         const auto box = unifiedMailboxForSource(parentId);
0103         if (!box) {
0104             qCWarning(UNIFIEDMAILBOXAGENT_LOG) << "Received Remove notification for Items belonging to" << parentId << "which we don't monitor";
0105             return;
0106         }
0107         if (box->collectionId() <= -1) {
0108             qCWarning(UNIFIEDMAILBOXAGENT_LOG) << "Missing box->collection mapping for unified mailbox" << box->id();
0109             return;
0110         }
0111 
0112         new Akonadi::UnlinkJob(Akonadi::Collection{box->collectionId()}, items, this);
0113     });
0114     connect(&mMonitor,
0115             &Akonadi::Monitor::itemsMoved,
0116             this,
0117             [this](const Akonadi::Item::List &items, const Akonadi::Collection &srcCollection, const Akonadi::Collection &dstCollection) {
0118                 ReplayNextOnExit replayNext(mMonitor);
0119 
0120                 if (const auto srcBox = unifiedMailboxForSource(srcCollection.id())) {
0121                     // Move source collection was our source, unlink the Item from a box
0122                     new Akonadi::UnlinkJob(Akonadi::Collection{srcBox->collectionId()}, items, this);
0123                 }
0124                 if (const auto dstBox = unifiedMailboxForSource(dstCollection.id())) {
0125                     // Move destination collection is our source, link the Item into a box
0126                     new Akonadi::LinkJob(Akonadi::Collection{dstBox->collectionId()}, items, this);
0127                 }
0128             });
0129 
0130     connect(&mMonitor, &Akonadi::Monitor::collectionRemoved, this, [this](const Akonadi::Collection &col) {
0131         ReplayNextOnExit replayNext(mMonitor);
0132 
0133         if (auto box = unifiedMailboxForSource(col.id())) {
0134             box->removeSourceCollection(col.id());
0135             mMonitor.setCollectionMonitored(col, false);
0136             if (box->sourceCollections().isEmpty()) {
0137                 removeBox(box->id());
0138             }
0139             saveBoxes();
0140             // No need to resync the box collection, the linked Items got removed by Akonadi
0141         } else {
0142             qCWarning(UNIFIEDMAILBOXAGENT_LOG) << "Received notification about removal of Collection" << col.id() << "which we don't monitor";
0143         }
0144     });
0145     connect(&mMonitor,
0146             qOverload<const Akonadi::Collection &, const QSet<QByteArray> &>(&Akonadi::Monitor::collectionChanged),
0147             this,
0148             [this](const Akonadi::Collection &col, const QSet<QByteArray> &parts) {
0149                 ReplayNextOnExit replayNext(mMonitor);
0150 
0151                 qCDebug(UNIFIEDMAILBOXAGENT_LOG) << "Collection changed:" << parts;
0152                 if (!parts.contains(Akonadi::SpecialCollectionAttribute().type())) {
0153                     return;
0154                 }
0155 
0156                 if (col.hasAttribute<Akonadi::SpecialCollectionAttribute>()) {
0157                     const auto srcBox = unregisterSpecialSourceCollection(col.id());
0158                     const auto dstBox = registerSpecialSourceCollection(col);
0159                     if (srcBox == dstBox) {
0160                         return;
0161                     }
0162 
0163                     saveBoxes();
0164 
0165                     if (srcBox && srcBox->sourceCollections().isEmpty()) {
0166                         removeBox(srcBox->id());
0167                         return;
0168                     }
0169 
0170                     if (srcBox) {
0171                         Q_EMIT updateBox(srcBox);
0172                     }
0173                     if (dstBox) {
0174                         Q_EMIT updateBox(dstBox);
0175                     }
0176                 } else {
0177                     if (const auto box = unregisterSpecialSourceCollection(col.id())) {
0178                         saveBoxes();
0179                         if (box->sourceCollections().isEmpty()) {
0180                             removeBox(box->id());
0181                         } else {
0182                             Q_EMIT updateBox(box);
0183                         }
0184                     }
0185                 }
0186             });
0187 }
0188 
0189 UnifiedMailboxManager::~UnifiedMailboxManager() = default;
0190 
0191 Akonadi::ChangeRecorder &UnifiedMailboxManager::changeRecorder()
0192 {
0193     return mMonitor;
0194 }
0195 
0196 void UnifiedMailboxManager::loadBoxes(FinishedCallback &&finishedCb)
0197 {
0198     qCDebug(UNIFIEDMAILBOXAGENT_LOG) << "loading boxes";
0199     const auto group = mConfig->group(QStringLiteral("UnifiedMailboxes"));
0200     const auto boxGroups = group.groupList();
0201     for (const auto &boxGroupName : boxGroups) {
0202         const auto boxGroup = group.group(boxGroupName);
0203         auto box = std::make_unique<UnifiedMailbox>();
0204         box->load(boxGroup);
0205         insertBox(std::move(box));
0206     }
0207 
0208     const auto cb = [this, finishedCb = std::move(finishedCb)]() {
0209         qCDebug(UNIFIEDMAILBOXAGENT_LOG) << "Finished callback: enabling change recorder";
0210         // Only now start processing changes from change recorder
0211         connect(&mMonitor, &Akonadi::ChangeRecorder::changesAdded, &mMonitor, &Akonadi::ChangeRecorder::replayNext, Qt::QueuedConnection);
0212         // And start replaying any potentially pending notification
0213         QTimer::singleShot(0, &mMonitor, &Akonadi::ChangeRecorder::replayNext);
0214 
0215         if (finishedCb) {
0216             finishedCb();
0217         }
0218     };
0219 
0220     qCDebug(UNIFIEDMAILBOXAGENT_LOG) << "Loaded" << mMailboxes.size() << "boxes from config";
0221 
0222     if (mMailboxes.empty()) {
0223         createDefaultBoxes(std::move(cb));
0224     } else {
0225         discoverBoxCollections(std::move(cb));
0226     }
0227 }
0228 
0229 void UnifiedMailboxManager::saveBoxes()
0230 {
0231     auto group = mConfig->group(QStringLiteral("UnifiedMailboxes"));
0232     const auto currentGroups = group.groupList();
0233     for (const auto &groupName : currentGroups) {
0234         group.deleteGroup(groupName);
0235     }
0236     for (const auto &boxIt : mMailboxes) {
0237         auto boxGroup = group.group(boxIt.second->id());
0238         boxIt.second->save(boxGroup);
0239     }
0240     mConfig->sync();
0241     mConfig->reparseConfiguration();
0242 }
0243 
0244 void UnifiedMailboxManager::insertBox(std::unique_ptr<UnifiedMailbox> box)
0245 {
0246     auto it = mMailboxes.emplace(std::make_pair(box->id(), std::move(box)));
0247     it.first->second->attachManager(this);
0248 }
0249 
0250 void UnifiedMailboxManager::removeBox(const QString &id)
0251 {
0252     auto box = std::find_if(mMailboxes.begin(), mMailboxes.end(), [&id](const std::pair<const QString, std::unique_ptr<UnifiedMailbox>> &box) {
0253         return box.second->id() == id;
0254     });
0255     if (box == mMailboxes.end()) {
0256         return;
0257     }
0258 
0259     box->second->attachManager(nullptr);
0260     mMailboxes.erase(box);
0261 }
0262 
0263 UnifiedMailbox *UnifiedMailboxManager::unifiedMailboxForSource(qint64 source) const
0264 {
0265     const auto box = mSourceToBoxMap.find(source);
0266     if (box == mSourceToBoxMap.cend()) {
0267         return {};
0268     }
0269     return box->second;
0270 }
0271 
0272 UnifiedMailbox *UnifiedMailboxManager::unifiedMailboxFromCollection(const Akonadi::Collection &col) const
0273 {
0274     if (!isUnifiedMailbox(col)) {
0275         return nullptr;
0276     }
0277 
0278     const auto box = mMailboxes.find(col.name());
0279     if (box == mMailboxes.cend()) {
0280         return {};
0281     }
0282     return box->second.get();
0283 }
0284 
0285 void UnifiedMailboxManager::createDefaultBoxes(FinishedCallback &&finishedCb)
0286 {
0287     if (!Settings::self()->createDefaultBoxes()) {
0288         return;
0289     }
0290     // First build empty boxes
0291     auto inbox = std::make_unique<UnifiedMailbox>();
0292     inbox->attachManager(this);
0293     inbox->setId(Common::InboxBoxId);
0294     inbox->setName(i18n("Inbox"));
0295     inbox->setIcon(QStringLiteral("mail-folder-inbox"));
0296     insertBox(std::move(inbox));
0297 
0298     auto sent = std::make_unique<UnifiedMailbox>();
0299     sent->attachManager(this);
0300     sent->setId(Common::SentBoxId);
0301     sent->setName(i18n("Sent"));
0302     sent->setIcon(QStringLiteral("mail-folder-sent"));
0303     insertBox(std::move(sent));
0304 
0305     auto drafts = std::make_unique<UnifiedMailbox>();
0306     drafts->attachManager(this);
0307     drafts->setId(Common::DraftsBoxId);
0308     drafts->setName(i18n("Drafts"));
0309     drafts->setIcon(QStringLiteral("document-properties"));
0310     insertBox(std::move(drafts));
0311 
0312     auto list = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::Recursive, this);
0313     list->fetchScope().fetchAttribute<Akonadi::SpecialCollectionAttribute>();
0314     list->fetchScope().setContentMimeTypes({QStringLiteral("message/rfc822")});
0315 #ifdef UNIT_TESTS
0316     list->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::Parent);
0317 #else
0318     list->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::None);
0319 #endif
0320     connect(list, &Akonadi::CollectionFetchJob::collectionsReceived, this, [this](const Akonadi::Collection::List &list) {
0321         for (const auto &col : list) {
0322             if (isUnifiedMailbox(col)) {
0323                 continue;
0324             }
0325 
0326             try {
0327                 switch (Akonadi::SpecialMailCollections::self()->specialCollectionType(col)) {
0328                 case Akonadi::SpecialMailCollections::Inbox:
0329                     mMailboxes.at(Common::InboxBoxId)->addSourceCollection(col.id());
0330                     break;
0331                 case Akonadi::SpecialMailCollections::SentMail:
0332                     mMailboxes.at(Common::SentBoxId)->addSourceCollection(col.id());
0333                     break;
0334                 case Akonadi::SpecialMailCollections::Drafts:
0335                     mMailboxes.at(Common::DraftsBoxId)->addSourceCollection(col.id());
0336                     break;
0337                 default:
0338                     continue;
0339                 }
0340             } catch (const std::out_of_range &) {
0341                 qCWarning(UNIFIEDMAILBOXAGENT_LOG) << "Failed to find a special unified mailbox for source collection" << col.id();
0342                 continue;
0343             }
0344         }
0345     });
0346     connect(list, &Akonadi::CollectionFetchJob::result, this, [this, finishedCb = std::move(finishedCb)]() {
0347         saveBoxes();
0348         if (finishedCb) {
0349             finishedCb();
0350         }
0351     });
0352 #ifndef UNIT_TESTS
0353     Settings::self()->setCreateDefaultBoxes(false);
0354     Settings::self()->save();
0355 #endif
0356 }
0357 
0358 void UnifiedMailboxManager::discoverBoxCollections(FinishedCallback &&finishedCb)
0359 {
0360     auto list = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::Recursive, this);
0361 #ifdef UNIT_TESTS
0362     list->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::Parent);
0363 #else
0364     list->fetchScope().setResource(Common::AgentIdentifier);
0365 #endif
0366     connect(list, &Akonadi::CollectionFetchJob::collectionsReceived, this, [this](const Akonadi::Collection::List &list) {
0367         for (const auto &col : list) {
0368             if (!isUnifiedMailbox(col) || col.parentCollection() == Akonadi::Collection::root()) {
0369                 continue;
0370             }
0371             const auto it = mMailboxes.find(col.name());
0372             if (it == mMailboxes.end()) {
0373                 qCWarning(UNIFIEDMAILBOXAGENT_LOG) << "Failed to find an unified mailbox for source collection" << col.id();
0374             } else {
0375                 it->second->setCollectionId(col.id());
0376             }
0377         }
0378     });
0379     if (finishedCb) {
0380         connect(list, &Akonadi::CollectionFetchJob::result, this, finishedCb);
0381     }
0382 }
0383 
0384 const UnifiedMailbox *UnifiedMailboxManager::registerSpecialSourceCollection(const Akonadi::Collection &col)
0385 {
0386     // This is slightly awkward, wold be better if we could use SpecialMailCollections,
0387     // but it also relies on Monitor internally, so there's a possible race condition
0388     // between our ChangeRecorder and SpecialMailCollections' Monitor
0389     auto attr = col.attribute<Akonadi::SpecialCollectionAttribute>();
0390     Q_ASSERT(attr);
0391     if (!attr) {
0392         return {};
0393     }
0394 
0395     decltype(mMailboxes)::iterator box;
0396     if (attr->collectionType() == Common::SpecialCollectionInbox) {
0397         box = mMailboxes.find(Common::InboxBoxId);
0398     } else if (attr->collectionType() == Common::SpecialCollectionSentMail) {
0399         box = mMailboxes.find(Common::SentBoxId);
0400     } else if (attr->collectionType() == Common::SpecialCollectionDrafts) {
0401         box = mMailboxes.find(Common::DraftsBoxId);
0402     }
0403     if (box == mMailboxes.end()) {
0404         return {};
0405     }
0406 
0407     box->second->addSourceCollection(col.id());
0408     return box->second.get();
0409 }
0410 
0411 const UnifiedMailbox *UnifiedMailboxManager::unregisterSpecialSourceCollection(qint64 colId)
0412 {
0413     auto box = unifiedMailboxForSource(colId);
0414     if (!box) {
0415         return {};
0416     }
0417 
0418     if (!box->isSpecial()) {
0419         qCDebug(UNIFIEDMAILBOXAGENT_LOG) << colId << "does not belong to a special unified box" << box->id();
0420         return {};
0421     }
0422 
0423     box->removeSourceCollection(colId);
0424     return box;
0425 }
0426 
0427 #include "moc_unifiedmailboxmanager.cpp"