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"