File indexing completed on 2025-02-16 04:50:10

0001 /*
0002     SPDX-FileCopyrightText: 2015-2020 Krzysztof Nowicki <krissn@op.pl>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "ewsresource.h"
0008 
0009 #include <QDebug>
0010 
0011 #include <Akonadi/AttributeFactory>
0012 #include <Akonadi/ChangeRecorder>
0013 #include <Akonadi/CollectionFetchJob>
0014 #include <Akonadi/CollectionFetchScope>
0015 #include <Akonadi/CollectionModifyJob>
0016 #include <Akonadi/EntityDisplayAttribute>
0017 #include <Akonadi/ItemCreateJob>
0018 #include <Akonadi/ItemDeleteJob>
0019 #include <Akonadi/ItemFetchScope>
0020 #include <Akonadi/ItemModifyJob>
0021 #include <Akonadi/SpecialMailCollections>
0022 #include <KMime/Message>
0023 #include <KNotification>
0024 
0025 #include <KLocalizedString>
0026 
0027 #include "auth/ewsabstractauth.h"
0028 #include "ewsconfigdialog.h"
0029 #include "ewscreatefolderrequest.h"
0030 #include "ewscreateitemjob.h"
0031 #if HAVE_SEPARATE_MTA_RESOURCE
0032 #include "ewscreateitemrequest.h"
0033 #endif
0034 #include "ewsdeletefolderrequest.h"
0035 #include "ewsdeleteitemrequest.h"
0036 #include "ewsfetchfoldersincrjob.h"
0037 #include "ewsfetchfoldersjob.h"
0038 #include "ewsfetchitempayloadjob.h"
0039 #include "ewsgetfolderrequest.h"
0040 #include "ewsgetitemrequest.h"
0041 #include "ewsitemhandler.h"
0042 #include "ewsmodifyitemflagsjob.h"
0043 #include "ewsmodifyitemjob.h"
0044 #include "ewsmovefolderrequest.h"
0045 #include "ewsmoveitemrequest.h"
0046 #include "ewsresource_debug.h"
0047 #include "ewssettings.h"
0048 #include "ewssubscriptionmanager.h"
0049 #include "ewssyncstateattribute.h"
0050 #include "ewsupdatefolderrequest.h"
0051 #include "tags/ewsglobaltagsreadjob.h"
0052 #include "tags/ewsglobaltagswritejob.h"
0053 #include "tags/ewstagstore.h"
0054 #include "tags/ewsupdateitemstagsjob.h"
0055 
0056 #include "ewsresourceadaptor.h"
0057 #include "ewssettingsadaptor.h"
0058 #include "ewswalletadaptor.h"
0059 
0060 using namespace Akonadi;
0061 
0062 struct SpecialFolders {
0063     EwsDistinguishedId did;
0064     SpecialMailCollections::Type type;
0065     QString iconName;
0066 };
0067 
0068 static const QList<SpecialFolders> specialFolderList = {{EwsDIdInbox, SpecialMailCollections::Inbox, QStringLiteral("mail-folder-inbox")},
0069                                                         {EwsDIdOutbox, SpecialMailCollections::Outbox, QStringLiteral("mail-folder-outbox")},
0070                                                         {EwsDIdSentItems, SpecialMailCollections::SentMail, QStringLiteral("mail-folder-sent")},
0071                                                         {EwsDIdDeletedItems, SpecialMailCollections::Trash, QStringLiteral("user-trash")},
0072                                                         {EwsDIdDrafts, SpecialMailCollections::Drafts, QStringLiteral("document-properties")}};
0073 
0074 const QString EwsResource::akonadiEwsPropsetUuid = QStringLiteral("9bf757ae-69b5-4d8a-bf1d-2dd0c0871a28");
0075 
0076 const EwsPropertyField EwsResource::globalTagsProperty(EwsResource::akonadiEwsPropsetUuid, QStringLiteral("GlobalTags"), EwsPropTypeStringArray);
0077 const EwsPropertyField EwsResource::globalTagsVersionProperty(EwsResource::akonadiEwsPropsetUuid, QStringLiteral("GlobalTagsVersion"), EwsPropTypeInteger);
0078 const EwsPropertyField EwsResource::tagsProperty(EwsResource::akonadiEwsPropsetUuid, QStringLiteral("Tags"), EwsPropTypeStringArray);
0079 const EwsPropertyField EwsResource::flagsProperty(EwsResource::akonadiEwsPropsetUuid, QStringLiteral("Flags"), EwsPropTypeStringArray);
0080 
0081 static constexpr int InitialReconnectTimeout = 15;
0082 static constexpr int MaxReconnectTimeout = 300;
0083 
0084 EwsResource::EwsResource(const QString &id)
0085     : Akonadi::ResourceBase(id)
0086     , mAuthStage(AuthIdle)
0087     , mTagsRetrieved(false)
0088     , mReconnectTimeout(InitialReconnectTimeout)
0089     , mInitialReconnectTimeout(InitialReconnectTimeout)
0090     , mSettings(new EwsSettings(winIdForDialogs()))
0091 {
0092     AttributeFactory::registerAttribute<EwsSyncStateAttribute>();
0093 
0094     mEwsClient.setUserAgent(mSettings->userAgent());
0095     mEwsClient.setEnableNTLMv2(mSettings->enableNTLMv2());
0096 
0097     changeRecorder()->fetchCollection(true);
0098     changeRecorder()->collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::Parent);
0099     changeRecorder()->collectionFetchScope().fetchAttribute<EwsSyncStateAttribute>();
0100     changeRecorder()->itemFetchScope().fetchFullPayload(true);
0101     changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::Parent);
0102     changeRecorder()->itemFetchScope().setFetchModificationTime(false);
0103     changeRecorder()->itemFetchScope().setFetchTags(true);
0104 
0105     mRootCollection.setParentCollection(Collection::root());
0106     mRootCollection.setName(name());
0107     mRootCollection.setContentMimeTypes(QStringList() << Collection::mimeType() << KMime::Message::mimeType());
0108     mRootCollection.setRights(Collection::ReadOnly);
0109 
0110     setScheduleAttributeSyncBeforeItemSync(true);
0111 
0112     // Load the sync state
0113     QByteArray data = QByteArray::fromBase64(mSettings->folderSyncState().toLatin1());
0114     if (!data.isEmpty()) {
0115         data = qUncompress(data);
0116         if (!data.isEmpty()) {
0117             mFolderSyncState = QString::fromLatin1(data);
0118         }
0119     }
0120 
0121     setHierarchicalRemoteIdentifiersEnabled(true);
0122 
0123     mTagStore = new EwsTagStore(this);
0124 
0125     QMetaObject::invokeMethod(this, &EwsResource::delayedInit, Qt::QueuedConnection);
0126 
0127     connect(this, &AgentBase::reloadConfiguration, this, &EwsResource::reloadConfig);
0128     connect(this, &ResourceBase::nameChanged, this, &EwsResource::adjustRootCollectionName);
0129 }
0130 
0131 EwsResource::~EwsResource() = default;
0132 
0133 void EwsResource::delayedInit()
0134 {
0135     new EwsResourceAdaptor(this);
0136     new EwsSettingsAdaptor(mSettings.data());
0137     new EwsWalletAdaptor(mSettings.data());
0138     QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), mSettings.data(), QDBusConnection::ExportAdaptors);
0139 }
0140 
0141 void EwsResource::resetUrl()
0142 {
0143     Q_EMIT status(Running, i18nc("@info:status", "Connecting to Exchange server"));
0144 
0145     auto req = new EwsGetFolderRequest(mEwsClient, this);
0146     const EwsId::List folders{EwsId(EwsDIdMsgFolderRoot), EwsId(EwsDIdInbox)};
0147     req->setFolderIds(folders);
0148     EwsFolderShape shape(EwsShapeIdOnly);
0149     shape << EwsPropertyField(QStringLiteral("folder:DisplayName"));
0150     // Use the opportunity of reading the root folder to read the tag data.
0151     shape << globalTagsProperty << globalTagsVersionProperty;
0152     req->setFolderShape(shape);
0153     connect(req, &EwsRequest::result, this, &EwsResource::rootFolderFetchFinished);
0154     req->start();
0155 }
0156 
0157 void EwsResource::rootFolderFetchFinished(KJob *job)
0158 {
0159     auto req = qobject_cast<EwsGetFolderRequest *>(job);
0160     if (!req) {
0161         Q_EMIT status(Idle, i18nc("@info:status", "Unable to connect to Exchange server"));
0162         setTemporaryOffline(reconnectTimeout());
0163         qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsGetFolderRequest job object");
0164         return;
0165     }
0166 
0167     if (req->error()) {
0168         Q_EMIT status(Idle, i18nc("@info:status", "Unable to connect to Exchange server"));
0169         setTemporaryOffline(reconnectTimeout());
0170         qWarning() << "ERROR" << req->errorString();
0171         return;
0172     }
0173 
0174     if (req->responses().size() != 2) {
0175         Q_EMIT status(Idle, i18nc("@info:status", "Unable to connect to Exchange server"));
0176         setTemporaryOffline(reconnectTimeout());
0177         qCWarning(EWSRES_LOG) << QStringLiteral("Invalid number of responses received");
0178         return;
0179     }
0180 
0181     EwsFolder folder = req->responses()[1].folder();
0182     auto id = folder[EwsFolderFieldFolderId].value<EwsId>();
0183     if (id.type() == EwsId::Real) {
0184         /* Since KDE PIM is heavily based on IMAP philosophy it would only consider for filtering
0185          * folders with the remote identifier set to "INBOX". While this is true for IMAP/POP3, Exchange
0186          * uses Base64-encoded strings with data private to the server. In order for mail filtering to work
0187          * the EWS resource has pretended that the inbox folder's remote name is "INBOX". Since KDE Applications
0188          * 17.12 this workaround is no longer needed, however in order to clean-up after old Akonadi EWS
0189          * installations the code below sets the correct Exchange id for the Inbox folder.
0190          *
0191          * At some day in the future this part of code can be removed too. */
0192         Collection c;
0193         c.setRemoteId(QStringLiteral("INBOX"));
0194         auto job = new CollectionFetchJob(c, CollectionFetchJob::Base, this);
0195         job->setFetchScope(changeRecorder()->collectionFetchScope());
0196         job->fetchScope().setResource(identifier());
0197         job->fetchScope().setListFilter(CollectionFetchScope::Sync);
0198         job->setProperty("inboxId", id.id());
0199         connect(job, &CollectionFetchJob::result, this, &EwsResource::adjustInboxRemoteIdFetchFinished);
0200 
0201         int inboxIdx = mSettings->serverSubscriptionList().indexOf(QLatin1StringView("INBOX"));
0202         if (inboxIdx >= 0) {
0203             QStringList subList = mSettings->serverSubscriptionList();
0204             subList[inboxIdx] = id.id();
0205             mSettings->setServerSubscriptionList(subList);
0206         }
0207     }
0208 
0209     folder = req->responses().first().folder();
0210     id = folder[EwsFolderFieldFolderId].value<EwsId>();
0211     if (id.type() == EwsId::Real) {
0212         mRootCollection.setRemoteId(id.id());
0213         mRootCollection.setRemoteRevision(id.changeKey());
0214         qCDebug(EWSRES_LOG) << "Root folder is " << id;
0215         emitReadyStatus();
0216         mReconnectTimeout = mInitialReconnectTimeout;
0217 
0218         if (mSettings->serverSubscription()) {
0219             mSubManager.reset(new EwsSubscriptionManager(mEwsClient, id, mSettings.data(), this));
0220             connect(mSubManager.data(), &EwsSubscriptionManager::foldersModified, this, &EwsResource::foldersModifiedEvent);
0221             connect(mSubManager.data(), &EwsSubscriptionManager::folderTreeModified, this, &EwsResource::folderTreeModifiedEvent);
0222             connect(mSubManager.data(), &EwsSubscriptionManager::fullSyncRequested, this, &EwsResource::fullSyncRequestedEvent);
0223 
0224             /* Use a queued connection here as the connectionError() method will actually destroy the subscription manager. If this
0225              * was done with a direct connection this would have ended up with destroying the caller object followed by a crash. */
0226             connect(mSubManager.data(), &EwsSubscriptionManager::connectionError, this, &EwsResource::connectionError, Qt::QueuedConnection);
0227             mSubManager->start();
0228         }
0229 
0230         synchronizeCollectionTree();
0231 
0232         mTagStore->readTags(folder[globalTagsProperty].toStringList(), folder[globalTagsVersionProperty].toInt());
0233     }
0234 }
0235 
0236 void EwsResource::adjustInboxRemoteIdFetchFinished(KJob *job)
0237 {
0238     if (!job->error()) {
0239         auto fetchJob = qobject_cast<CollectionFetchJob *>(job);
0240         Q_ASSERT(fetchJob);
0241         if (!fetchJob->collections().isEmpty()) {
0242             Collection c = fetchJob->collections()[0];
0243             c.setRemoteId(fetchJob->property("inboxId").toString());
0244             auto modifyJob = new CollectionModifyJob(c, this);
0245             modifyJob->start();
0246         }
0247     }
0248 }
0249 
0250 void EwsResource::retrieveCollections()
0251 {
0252     if (mRootCollection.remoteId().isNull()) {
0253         cancelTask(i18nc("@info:status", "Root folder id not known."));
0254         return;
0255     }
0256 
0257     Q_EMIT status(Running, i18nc("@info:status", "Retrieving collection tree"));
0258 
0259     if (!mFolderSyncState.isEmpty() && !mRootCollection.isValid()) {
0260         /* When doing an incremental sync the real Akonadi identifier of the root collection must
0261          * be known, because the retrieved list of changes needs to include all parent folders up
0262          * to the root. None of the child collections are required to be valid, but the root must
0263          * be, as it needs to be the anchor point.
0264          */
0265         auto fetchJob = new CollectionFetchJob(mRootCollection, CollectionFetchJob::Base);
0266         connect(fetchJob, &CollectionFetchJob::result, this, &EwsResource::rootCollectionFetched);
0267         fetchJob->start();
0268     } else {
0269         doRetrieveCollections();
0270     }
0271     synchronizeTags();
0272 }
0273 
0274 void EwsResource::rootCollectionFetched(KJob *job)
0275 {
0276     if (job->error()) {
0277         qCWarning(EWSRES_LOG) << "ERROR" << job->errorString();
0278     } else {
0279         auto fetchJob = qobject_cast<CollectionFetchJob *>(job);
0280         if (fetchJob && !fetchJob->collections().isEmpty()) {
0281             mRootCollection = fetchJob->collections().at(0);
0282             adjustRootCollectionName(name());
0283             qCDebugNC(EWSRES_LOG) << QStringLiteral("Root collection fetched: ") << mRootCollection;
0284         }
0285     }
0286 
0287     /* If the fetch failed for whatever reason force a full sync, which doesn't require the root
0288      * collection to be valid. */
0289     if (!mRootCollection.isValid()) {
0290         mFolderSyncState.clear();
0291     }
0292 
0293     doRetrieveCollections();
0294 }
0295 
0296 void EwsResource::doRetrieveCollections()
0297 {
0298     if (mFolderSyncState.isEmpty()) {
0299         auto job = new EwsFetchFoldersJob(mEwsClient, mRootCollection, this);
0300         connect(job, &EwsFetchFoldersJob::result, this, &EwsResource::fetchFoldersJobFinished);
0301         connectStatusSignals(job);
0302         job->start();
0303     } else {
0304         auto job = new EwsFetchFoldersIncrJob(mEwsClient, mFolderSyncState, mRootCollection, this);
0305         connect(job, &EwsFetchFoldersIncrJob::result, this, &EwsResource::fetchFoldersIncrJobFinished);
0306         connectStatusSignals(job);
0307         job->start();
0308     }
0309 }
0310 
0311 void EwsResource::connectionError()
0312 {
0313     Q_EMIT status(Broken, i18nc("@info:status", "Unable to connect to Exchange server"));
0314     setTemporaryOffline(reconnectTimeout());
0315 }
0316 
0317 void EwsResource::retrieveItems(const Collection &collection)
0318 {
0319     queueFetchItemsJob(collection, RetrieveItems, [this](EwsFetchItemsJob *fetchJob) {
0320         auto col = fetchJob->collection();
0321         if (fetchJob->error()) {
0322             qCWarningNC(EWSRES_LOG) << QStringLiteral("Item fetch error:") << fetchJob->errorString() << fetchJob->error() << fetchJob->ewsResponseCode();
0323             if (!isEwsResponseCodeTemporaryError(fetchJob->ewsResponseCode())) {
0324                 const auto syncState = getCollectionSyncState(fetchJob->collection());
0325                 if (!syncState.isEmpty()) {
0326                     qCDebugNC(EWSRES_LOG) << QStringLiteral("Retrying with empty state.");
0327                     // Retry with a clear sync state.
0328                     saveCollectionSyncState(col, QString());
0329                     retrieveItems(col);
0330                 } else {
0331                     qCDebugNC(EWSRES_LOG) << QStringLiteral("Clean sync failed.");
0332                     // No more hope
0333                     cancelTask(i18nc("@info:status", "Failed to retrieve items"));
0334                     return;
0335                 }
0336             } else {
0337                 qCDebugNC(EWSRES_LOG) << QStringLiteral("Sync failed due to temporary error - not clearing state");
0338                 cancelTask(i18nc("@info:status", "Failed to retrieve items"));
0339                 setTemporaryOffline(reconnectTimeout());
0340                 return;
0341             }
0342         } else {
0343             saveCollectionSyncState(col, fetchJob->syncState());
0344             itemsRetrievedIncremental(fetchJob->newItems() + fetchJob->changedItems(), fetchJob->deletedItems());
0345         }
0346         saveState();
0347         mItemsToCheck.remove(fetchJob->collection().remoteId());
0348         emitReadyStatus();
0349     });
0350 }
0351 
0352 void EwsResource::queueFetchItemsJob(const Akonadi::Collection &col, QueuedFetchItemsJobType type, const std::function<void(EwsFetchItemsJob *)> &startFn)
0353 {
0354     qCDebugNC(EWSRES_LOG) << QStringLiteral("Enqueuing sync for collection ") << col << col.id();
0355 
0356     const auto queueEmpty = mFetchItemsJobQueue.empty();
0357     if (mFetchItemsJobQueue.count() > 1) {
0358         // Don't enqueue the same collection id, type pair twice, except for the first element,
0359         // which belongs to the collection being synced right now.
0360         for (const auto &item : std::as_const(mFetchItemsJobQueue).mid(1)) {
0361             if ((item.col == col) && (item.type == type)) {
0362                 qCDebugNC(EWSRES_LOG) << QStringLiteral("Sync already queued - skipping");
0363                 return;
0364             }
0365         }
0366     }
0367 
0368     mFetchItemsJobQueue.enqueue({col, type, startFn});
0369 
0370     qCDebugNC(EWSRES_LOG) << QStringLiteral("Sync queue state: ") << dumpResourceToString().replace(QLatin1Char('\n'), QLatin1Char(' '));
0371 
0372     if (queueEmpty) {
0373         startFetchItemsJob(col, startFn);
0374     }
0375 }
0376 
0377 void EwsResource::dequeueFetchItemsJob()
0378 {
0379     qCDebugNC(EWSRES_LOG) << QStringLiteral("Finished queued sync ") << mFetchItemsJobQueue.head().col << mFetchItemsJobQueue.head().col.id();
0380 
0381     mFetchItemsJobQueue.dequeue();
0382 
0383     if (!mFetchItemsJobQueue.empty()) {
0384         const auto &head = mFetchItemsJobQueue.head();
0385         startFetchItemsJob(head.col, head.startFn);
0386     }
0387 }
0388 
0389 void EwsResource::startFetchItemsJob(const Akonadi::Collection &col, std::function<void(EwsFetchItemsJob *)> startFn)
0390 {
0391     qCDebugNC(EWSRES_LOG) << QStringLiteral("Starting queued sync for collection ") << col;
0392 
0393     auto fetchJob = new EwsFetchItemsJob(col, mEwsClient, getCollectionSyncState(col), mItemsToCheck.value(col.remoteId()), mTagStore, this);
0394     connect(fetchJob, &EwsFetchItemsJob::result, this, [this, startFn, fetchJob](KJob *) {
0395         startFn(fetchJob);
0396         dequeueFetchItemsJob();
0397     });
0398     connectStatusSignals(fetchJob);
0399     fetchJob->start();
0400 }
0401 
0402 bool EwsResource::retrieveItems(const Item::List &items, const QSet<QByteArray> &parts)
0403 {
0404     qCDebugNC(EWSRES_AGENTIF_LOG) << "retrieveItems: start " << items << parts;
0405 
0406     Q_EMIT status(Running, i18nc("@info:status", "Retrieving items"));
0407 
0408     auto job = new EwsFetchItemPayloadJob(mEwsClient, this, items);
0409     connect(job, &EwsGetItemRequest::result, this, &EwsResource::getItemsRequestFinished);
0410     connectStatusSignals(job);
0411     job->start();
0412 
0413     return true;
0414 }
0415 
0416 void EwsResource::getItemsRequestFinished(KJob *job)
0417 {
0418     emitReadyStatus();
0419 
0420     if (job->error()) {
0421         cancelTask(job->errorText());
0422         return;
0423     }
0424 
0425     auto *fetchJob = qobject_cast<EwsFetchItemPayloadJob *>(job);
0426     if (!fetchJob) {
0427         qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsFetchItemPayloadJob job object");
0428         cancelTask(i18nc("@info:status", "Failed to retrieve items - internal error"));
0429         return;
0430     }
0431 
0432     qCDebugNC(EWSRES_AGENTIF_LOG) << "retrieveItems: done";
0433     itemsRetrieved(fetchJob->items());
0434 }
0435 
0436 void EwsResource::reloadConfig()
0437 {
0438     mSubManager.reset(nullptr);
0439     mEwsClient.setUrl(mSettings->baseUrl());
0440     setUpAuth();
0441     mEwsClient.setAuth(mAuth.data());
0442 }
0443 
0444 void EwsResource::configure(WId windowId)
0445 {
0446     QPointer<EwsConfigDialog> dlg = new EwsConfigDialog(this, mEwsClient, windowId, mSettings.data());
0447     if (dlg->exec()) {
0448         reloadConfig();
0449         Q_EMIT configurationDialogAccepted();
0450     } else {
0451         Q_EMIT configurationDialogRejected();
0452     }
0453     delete dlg;
0454 }
0455 
0456 void EwsResource::fetchFoldersJobFinished(KJob *job)
0457 {
0458     emitReadyStatus();
0459     auto req = qobject_cast<EwsFetchFoldersJob *>(job);
0460     if (!req) {
0461         qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsFetchFoldersJob job object");
0462         cancelTask(i18nc("@info:status", "Failed to retrieve folders - internal error"));
0463         return;
0464     }
0465 
0466     if (req->error()) {
0467         qWarning() << "ERROR" << req->errorString();
0468         cancelTask(i18nc("@info:status", "Failed to process folders retrieval request"));
0469         return;
0470     }
0471 
0472     mFolderSyncState = req->syncState();
0473     saveState();
0474     collectionsRetrieved(req->folders());
0475 
0476     fetchSpecialFolders();
0477 }
0478 
0479 void EwsResource::fetchFoldersIncrJobFinished(KJob *job)
0480 {
0481     emitReadyStatus();
0482     auto req = qobject_cast<EwsFetchFoldersIncrJob *>(job);
0483     if (!req) {
0484         qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsFetchFoldersIncrJob job object");
0485         cancelTask(i18nc("@info:status", "Invalid incremental folders retrieval request job object"));
0486         return;
0487     }
0488 
0489     if (req->error()) {
0490         qCWarningNC(EWSRES_LOG) << QStringLiteral("ERROR") << req->errorString();
0491 
0492         /* Retry with a full sync. */
0493         qCWarningNC(EWSRES_LOG) << QStringLiteral("Retrying with a full sync.");
0494         mFolderSyncState.clear();
0495         doRetrieveCollections();
0496         return;
0497     }
0498 
0499     mFolderSyncState = req->syncState();
0500     saveState();
0501     collectionsRetrievedIncremental(req->changedFolders(), req->deletedFolders());
0502 
0503     if (!req->changedFolders().isEmpty() || !req->deletedFolders().isEmpty()) {
0504         fetchSpecialFolders();
0505     }
0506 }
0507 
0508 void EwsResource::itemFetchJobFinished(KJob *job)
0509 {
0510     auto fetchJob = qobject_cast<EwsFetchItemsJob *>(job);
0511 
0512     if (!fetchJob) {
0513         qCWarningNC(EWSRES_LOG) << QStringLiteral("Invalid EwsFetchItemsJobjob object");
0514         cancelTask(i18nc("@info:status", "Failed to retrieve items - internal error"));
0515         return;
0516     }
0517     auto col = fetchJob->collection();
0518     if (job->error()) {
0519         qCWarningNC(EWSRES_LOG) << QStringLiteral("Item fetch error:") << job->errorString();
0520         const auto syncState = getCollectionSyncState(fetchJob->collection());
0521         if (!syncState.isEmpty()) {
0522             qCDebugNC(EWSRES_LOG) << QStringLiteral("Retrying with empty state.");
0523             // Retry with a clear sync state.
0524             saveCollectionSyncState(col, QString());
0525             retrieveItems(col);
0526         } else {
0527             qCDebugNC(EWSRES_LOG) << QStringLiteral("Clean sync failed.");
0528             // No more hope
0529             cancelTask(i18nc("@info:status", "Failed to retrieve items"));
0530             return;
0531         }
0532     } else {
0533         saveCollectionSyncState(col, fetchJob->syncState());
0534         itemsRetrievedIncremental(fetchJob->newItems() + fetchJob->changedItems(), fetchJob->deletedItems());
0535     }
0536     saveState();
0537     mItemsToCheck.remove(fetchJob->collection().remoteId());
0538     emitReadyStatus();
0539 }
0540 
0541 void EwsResource::itemChanged(const Akonadi::Item &item, const QSet<QByteArray> &partIdentifiers)
0542 {
0543     qCDebugNC(EWSRES_AGENTIF_LOG) << "itemChanged: start " << item << partIdentifiers;
0544 
0545     EwsItemType type = EwsItemHandler::mimeToItemType(item.mimeType());
0546     if (isEwsMessageItemType(type)) {
0547         qCWarningNC(EWSRES_AGENTIF_LOG) << "itemChanged: Item type not supported for changing";
0548         cancelTask(i18nc("@info:status", "Item type not supported for changing"));
0549     } else {
0550         EwsModifyItemJob *job = EwsItemHandler::itemHandler(type)->modifyItemJob(mEwsClient, Item::List() << item, partIdentifiers, this);
0551         connect(job, &KJob::result, this, &EwsResource::itemChangeRequestFinished);
0552         connectStatusSignals(job);
0553         job->start();
0554     }
0555 }
0556 
0557 void EwsResource::itemsFlagsChanged(const Akonadi::Item::List &items, const QSet<QByteArray> &addedFlags, const QSet<QByteArray> &removedFlags)
0558 {
0559     qCDebug(EWSRES_AGENTIF_LOG) << "itemsFlagsChanged: start" << items << addedFlags << removedFlags;
0560 
0561     Q_EMIT status(Running, i18nc("@info:status", "Updating item flags"));
0562 
0563     auto job = new EwsModifyItemFlagsJob(mEwsClient, this, items, addedFlags, removedFlags);
0564     connect(job, &EwsModifyItemFlagsJob::result, this, &EwsResource::itemModifyFlagsRequestFinished);
0565     connectStatusSignals(job);
0566     job->start();
0567 }
0568 
0569 void EwsResource::itemModifyFlagsRequestFinished(KJob *job)
0570 {
0571     if (job->error()) {
0572         qCWarning(EWSRES_AGENTIF_LOG) << "itemsFlagsChanged:" << job->errorString();
0573         cancelTask(i18nc("@info:status", "Failed to process item flags update request"));
0574         return;
0575     }
0576 
0577     auto req = qobject_cast<EwsModifyItemFlagsJob *>(job);
0578     if (!req) {
0579         qCWarning(EWSRES_AGENTIF_LOG) << "itemsFlagsChanged: Invalid EwsModifyItemFlagsJob job object";
0580         cancelTask(i18nc("@info:status", "Failed to update item flags - internal error"));
0581         return;
0582     }
0583 
0584     emitReadyStatus();
0585 
0586     qCDebug(EWSRES_AGENTIF_LOG) << "itemsFlagsChanged: done";
0587     changesCommitted(req->items());
0588 }
0589 
0590 void EwsResource::itemChangeRequestFinished(KJob *job)
0591 {
0592     if (job->error()) {
0593         qCWarningNC(EWSRES_AGENTIF_LOG) << "itemChanged: " << job->errorString();
0594         cancelTask(i18nc("@info:status", "Failed to process item update request"));
0595         return;
0596     }
0597 
0598     auto req = qobject_cast<EwsModifyItemJob *>(job);
0599     if (!req) {
0600         qCWarningNC(EWSRES_AGENTIF_LOG) << "itemChanged: Invalid EwsModifyItemJob job object";
0601         cancelTask(i18nc("@info:status", "Failed to update item - internal error"));
0602         return;
0603     }
0604 
0605     qCDebugNC(EWSRES_AGENTIF_LOG) << "itemChanged: done";
0606     changesCommitted(req->items());
0607 }
0608 
0609 void EwsResource::itemsMoved(const Item::List &items, const Collection &sourceCollection, const Collection &destinationCollection)
0610 {
0611     qCDebug(EWSRES_AGENTIF_LOG) << "itemsMoved: start" << items << sourceCollection << destinationCollection;
0612 
0613     EwsId::List ids;
0614 
0615     ids.reserve(items.count());
0616     for (const Item &item : items) {
0617         EwsId id(item.remoteId(), item.remoteRevision());
0618         ids.append(id);
0619     }
0620 
0621     auto req = new EwsMoveItemRequest(mEwsClient, this);
0622     req->setItemIds(ids);
0623     EwsId destId(destinationCollection.remoteId(), QString());
0624     req->setDestinationFolderId(destId);
0625     req->setProperty("items", QVariant::fromValue<Item::List>(items));
0626     req->setProperty("sourceCollection", QVariant::fromValue<Collection>(sourceCollection));
0627     req->setProperty("destinationCollection", QVariant::fromValue<Collection>(destinationCollection));
0628     connect(req, &KJob::result, this, &EwsResource::itemMoveRequestFinished);
0629     req->start();
0630 }
0631 
0632 void EwsResource::itemMoveRequestFinished(KJob *job)
0633 {
0634     if (job->error()) {
0635         qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsMoved: " << job->errorString();
0636         cancelTask(i18nc("@info:status", "Failed to process item move request"));
0637         return;
0638     }
0639 
0640     auto req = qobject_cast<EwsMoveItemRequest *>(job);
0641     if (!req) {
0642         qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsMoved: Invalid EwsMoveItemRequest job object";
0643         cancelTask(i18nc("@info:status", "Failed to move item - internal error"));
0644         return;
0645     }
0646     auto items = job->property("items").value<Item::List>();
0647 
0648     if (items.count() != req->responses().count()) {
0649         qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsMoved: Invalid number of responses received from server";
0650         cancelTask(i18nc("@info:status", "Failed to move item - invalid number of responses received from server"));
0651         return;
0652     }
0653 
0654     /* When moving a batch of items it is possible that the operation will fail for some of them.
0655      * Unfortunately Akonadi doesn't provide a way to report such partial success/failure. In order
0656      * to work around this in case of partial failure the source and destination folders will be
0657      * resynchronised. In order to avoid doing a full sync a hint will be provided in order to
0658      * indicate the item(s) to check.
0659      */
0660 
0661     Item::List movedItems;
0662     EwsId::List failedIds;
0663 
0664     auto srcCol = req->property("sourceCollection").value<Collection>();
0665     auto dstCol = req->property("destinationCollection").value<Collection>();
0666     Item::List::iterator it = items.begin();
0667     const auto reqResponses{req->responses()};
0668     for (const EwsMoveItemRequest::Response &resp : reqResponses) {
0669         Item &item = *it;
0670         if (resp.isSuccess()) {
0671             qCDebugNC(EWSRES_AGENTIF_LOG)
0672                 << QStringLiteral("itemsMoved: succeeded for item %1 (new id: %2)").arg(ewsHash(item.remoteId()), ewsHash(resp.itemId().id()));
0673             if (item.isValid()) {
0674                 item.setRemoteId(resp.itemId().id());
0675                 item.setRemoteRevision(resp.itemId().changeKey());
0676                 movedItems.append(item);
0677             }
0678         } else {
0679             Q_EMIT warning(QStringLiteral("Move failed for item %1").arg(item.remoteId()));
0680             qCDebugNC(EWSRES_AGENTIF_LOG) << QStringLiteral("itemsMoved: failed for item %1").arg(ewsHash(item.remoteId()));
0681             failedIds.append(EwsId(item.remoteId(), QString()));
0682         }
0683         ++it;
0684     }
0685 
0686     if (!failedIds.isEmpty()) {
0687         qCWarningNC(EWSRES_LOG) << QStringLiteral("Failed to move %1 items. Forcing src & dst folder sync.").arg(failedIds.size());
0688         mItemsToCheck[srcCol.remoteId()] += failedIds;
0689         foldersModifiedEvent(EwsId::List({EwsId(srcCol.remoteId(), QString())}));
0690         mItemsToCheck[dstCol.remoteId()] += failedIds;
0691         foldersModifiedEvent(EwsId::List({EwsId(dstCol.remoteId(), QString())}));
0692     }
0693 
0694     qCDebugNC(EWSRES_AGENTIF_LOG) << "itemsMoved: done";
0695     changesCommitted(movedItems);
0696 }
0697 
0698 void EwsResource::itemsRemoved(const Item::List &items)
0699 {
0700     qCDebugNC(EWSRES_AGENTIF_LOG) << "itemsRemoved: start" << items;
0701     if (items.isEmpty())
0702         return;
0703 
0704     EwsDeleteItemRequest *lastReq = nullptr;
0705     EwsId::List ids;
0706     ids.reserve(100);
0707     for (const Item &item : items) {
0708         EwsId id(item.remoteId(), item.remoteRevision());
0709         ids.append(id);
0710         if (ids.count() >= 100) {
0711             auto *req = new EwsDeleteItemRequest(mEwsClient, this);
0712             req->setItemIds(ids);
0713             req->setProperty("items", QVariant::fromValue<Item::List>(items));
0714             connect(req, &EwsDeleteItemRequest::result, [this,lastReq](KJob *job) {
0715                 itemDeleteRequestFinished(job);
0716                 if (lastReq && !job->error())
0717                     lastReq->start();
0718             });
0719             lastReq = req;
0720             ids.clear();
0721             ids.reserve(100);
0722         }
0723     }
0724     if (!ids.isEmpty()) {
0725         auto *req = new EwsDeleteItemRequest(mEwsClient, this);
0726         req->setItemIds(ids);
0727         req->setProperty("items", QVariant::fromValue<Item::List>(items));
0728         connect(req, &EwsDeleteItemRequest::result, [this,lastReq](KJob *job) {
0729             itemDeleteRequestFinished(job);
0730             if (lastReq && !job->error())
0731                 lastReq->start();
0732         });
0733         lastReq = req;
0734     }
0735     if (lastReq)
0736         lastReq->start();
0737 }
0738 
0739 void EwsResource::itemDeleteRequestFinished(KJob *job)
0740 {
0741     if (job->error()) {
0742         qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsRemoved: " << job->errorString();
0743         cancelTask(i18nc("@info:status", "Failed to process item delete request"));
0744         return;
0745     }
0746 
0747     auto req = qobject_cast<EwsDeleteItemRequest *>(job);
0748     if (!req) {
0749         qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsRemoved: Invalid EwsDeleteItemRequest job object";
0750         cancelTask(i18nc("@info:status", "Failed to delete item - internal error"));
0751         return;
0752     }
0753     auto items = job->property("items").value<Item::List>();
0754 
0755     if (items.count() != req->responses().count()) {
0756         qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsRemoved: Invalid number of responses received from server";
0757         cancelTask(i18nc("@info:status", "Failed to delete item - invalid number of responses received from server"));
0758         return;
0759     }
0760 
0761     /* When removing a batch of items it is possible that the operation will fail for some of them.
0762      * Unfortunately Akonadi doesn't provide a way to report such partial success/failure. In order
0763      * to work around this in case of partial failure the original folder(s) will be resynchronised.
0764      * In order to avoid doing a full sync a hint will be provided in order to indicate the item(s)
0765      * to check.
0766      */
0767 
0768     EwsId::List foldersToSync;
0769 
0770     Item::List::iterator it = items.begin();
0771 
0772     const auto reqResponses{req->responses()};
0773     for (const EwsDeleteItemRequest::Response &resp : reqResponses) {
0774         Item &item = *it;
0775         if (resp.isSuccess()) {
0776             qCDebugNC(EWSRES_AGENTIF_LOG) << QStringLiteral("itemsRemoved: succeeded for item %1").arg(ewsHash(item.remoteId()));
0777         } else {
0778             Q_EMIT warning(QStringLiteral("Delete failed for item %1").arg(item.remoteId()));
0779             qCWarningNC(EWSRES_AGENTIF_LOG) << QStringLiteral("itemsRemoved: failed for item %1").arg(ewsHash(item.remoteId()));
0780             EwsId colId = EwsId(item.parentCollection().remoteId(), QString());
0781             mItemsToCheck[colId.id()].append(EwsId(item.remoteId(), QString()));
0782             if (!foldersToSync.contains(colId)) {
0783                 foldersToSync.append(colId);
0784             }
0785         }
0786         ++it;
0787     }
0788 
0789     if (!foldersToSync.isEmpty()) {
0790         qCWarningNC(EWSRES_LOG) << QStringLiteral("Need to force sync for %1 folders.").arg(foldersToSync.size());
0791         foldersModifiedEvent(foldersToSync);
0792     }
0793 
0794     qCDebug(EWSRES_AGENTIF_LOG) << "itemsRemoved: done";
0795     changeProcessed();
0796 }
0797 
0798 void EwsResource::itemAdded(const Item &item, const Collection &collection)
0799 {
0800     EwsItemType type = EwsItemHandler::mimeToItemType(item.mimeType());
0801     if (isEwsMessageItemType(type)) {
0802         cancelTask(i18nc("@info:status", "Item type not supported for creation"));
0803     } else {
0804         EwsCreateItemJob *job = EwsItemHandler::itemHandler(type)->createItemJob(mEwsClient, item, collection, mTagStore, this);
0805         connect(job, &EwsCreateItemJob::result, this, &EwsResource::itemCreateRequestFinished);
0806         job->start();
0807     }
0808 }
0809 
0810 void EwsResource::itemCreateRequestFinished(KJob *job)
0811 {
0812     if (job->error()) {
0813         cancelTask(i18nc("@info:status", "Failed to process item create request"));
0814         return;
0815     }
0816 
0817     auto req = qobject_cast<EwsCreateItemJob *>(job);
0818     if (!req) {
0819         cancelTask(i18nc("@info:status", "Failed to create item - internal error"));
0820         return;
0821     }
0822 
0823     changeCommitted(req->item());
0824 }
0825 
0826 void EwsResource::collectionAdded(const Collection &collection, const Collection &parent)
0827 {
0828     EwsFolderType type;
0829     QStringList mimeTypes = collection.contentMimeTypes();
0830     if (mimeTypes.contains(EwsItemHandler::itemHandler(EwsItemTypeCalendarItem)->mimeType())) {
0831         type = EwsFolderTypeCalendar;
0832     } else if (mimeTypes.contains(EwsItemHandler::itemHandler(EwsItemTypeContact)->mimeType())) {
0833         type = EwsFolderTypeContacts;
0834     } else if (mimeTypes.contains(EwsItemHandler::itemHandler(EwsItemTypeTask)->mimeType())) {
0835         type = EwsFolderTypeTasks;
0836     } else if (mimeTypes.contains(EwsItemHandler::itemHandler(EwsItemTypeMessage)->mimeType())) {
0837         type = EwsFolderTypeMail;
0838     } else {
0839         qCWarningNC(EWSRES_LOG) << QStringLiteral("Cannot determine EWS folder type.");
0840         cancelTask(i18nc("@info:status", "Failed to add collection - cannot determine EWS folder type"));
0841         return;
0842     }
0843 
0844     EwsFolder folder;
0845     folder.setType(type);
0846     folder.setField(EwsFolderFieldDisplayName, collection.name());
0847 
0848     auto req = new EwsCreateFolderRequest(mEwsClient, this);
0849     req->setParentFolderId(EwsId(parent.remoteId()));
0850     req->setFolders(EwsFolder::List() << folder);
0851     req->setProperty("collection", QVariant::fromValue<Collection>(collection));
0852     connect(req, &EwsCreateFolderRequest::result, this, &EwsResource::folderCreateRequestFinished);
0853     req->start();
0854 }
0855 
0856 void EwsResource::folderCreateRequestFinished(KJob *job)
0857 {
0858     if (job->error()) {
0859         cancelTask(i18nc("@info:status", "Failed to process folder create request"));
0860         return;
0861     }
0862 
0863     auto req = qobject_cast<EwsCreateFolderRequest *>(job);
0864     if (!req) {
0865         cancelTask(i18nc("@info:status", "Failed to create folder - internal error"));
0866         return;
0867     }
0868     auto col = job->property("collection").value<Collection>();
0869 
0870     EwsCreateFolderRequest::Response resp = req->responses().first();
0871     if (resp.isSuccess()) {
0872         const EwsId &id = resp.folderId();
0873         col.setRemoteId(id.id());
0874         col.setRemoteRevision(id.changeKey());
0875         changeCommitted(col);
0876     } else {
0877         cancelTask(i18nc("@info:status", "Failed to create folder"));
0878     }
0879 }
0880 
0881 void EwsResource::collectionMoved(const Collection &collection, const Collection &collectionSource, const Collection &collectionDestination)
0882 {
0883     Q_UNUSED(collectionSource)
0884 
0885     EwsId::List ids;
0886     ids.append(EwsId(collection.remoteId(), collection.remoteRevision()));
0887 
0888     auto req = new EwsMoveFolderRequest(mEwsClient, this);
0889     req->setFolderIds(ids);
0890     EwsId destId(collectionDestination.remoteId());
0891     req->setDestinationFolderId(destId);
0892     req->setProperty("collection", QVariant::fromValue<Collection>(collection));
0893     connect(req, &EwsMoveFolderRequest::result, this, &EwsResource::folderMoveRequestFinished);
0894     req->start();
0895 }
0896 
0897 void EwsResource::folderMoveRequestFinished(KJob *job)
0898 {
0899     if (job->error()) {
0900         cancelTask(i18nc("@info:status", "Failed to process folder move request"));
0901         return;
0902     }
0903 
0904     auto req = qobject_cast<EwsMoveFolderRequest *>(job);
0905     if (!req) {
0906         cancelTask(i18nc("@info:status", "Failed to move folder - internal error"));
0907         return;
0908     }
0909     auto col = job->property("collection").value<Collection>();
0910 
0911     if (req->responses().count() != 1) {
0912         cancelTask(i18nc("@info:status", "Failed to move folder - invalid number of responses received from server"));
0913         return;
0914     }
0915 
0916     EwsMoveFolderRequest::Response resp = req->responses().first();
0917     if (resp.isSuccess()) {
0918         const EwsId &id = resp.folderId();
0919         col.setRemoteId(id.id());
0920         col.setRemoteRevision(id.changeKey());
0921         changeCommitted(col);
0922     } else {
0923         cancelTask(i18nc("@info:status", "Failed to move folder"));
0924     }
0925 }
0926 
0927 void EwsResource::collectionChanged(const Collection &collection, const QSet<QByteArray> &changedAttributes)
0928 {
0929     if (changedAttributes.contains("NAME")) {
0930         auto req = new EwsUpdateFolderRequest(mEwsClient, this);
0931         EwsUpdateFolderRequest::FolderChange fc(EwsId(collection.remoteId(), collection.remoteRevision()), EwsFolderTypeMail);
0932         EwsUpdateFolderRequest::Update *upd = new EwsUpdateFolderRequest::SetUpdate(EwsPropertyField(QStringLiteral("folder:DisplayName")), collection.name());
0933         fc.addUpdate(upd);
0934         req->addFolderChange(fc);
0935         req->setProperty("collection", QVariant::fromValue<Collection>(collection));
0936         connect(req, &EwsUpdateFolderRequest::finished, this, &EwsResource::folderUpdateRequestFinished);
0937         req->start();
0938     } else {
0939         changeCommitted(collection);
0940     }
0941 }
0942 
0943 void EwsResource::collectionChanged(const Akonadi::Collection &collection)
0944 {
0945     Q_UNUSED(collection)
0946 }
0947 
0948 void EwsResource::folderUpdateRequestFinished(KJob *job)
0949 {
0950     if (job->error()) {
0951         cancelTask(i18nc("@info:status", "Failed to process folder update request"));
0952         return;
0953     }
0954 
0955     auto req = qobject_cast<EwsUpdateFolderRequest *>(job);
0956     if (!req) {
0957         cancelTask(i18nc("@info:status", "Failed to update folder - internal error"));
0958         return;
0959     }
0960     auto col = job->property("collection").value<Collection>();
0961 
0962     if (req->responses().count() != 1) {
0963         cancelTask(i18nc("@info:status", "Failed to update folder - invalid number of responses received from server"));
0964         return;
0965     }
0966 
0967     EwsUpdateFolderRequest::Response resp = req->responses().first();
0968     if (resp.isSuccess()) {
0969         const EwsId &id = resp.folderId();
0970         col.setRemoteId(id.id());
0971         col.setRemoteRevision(id.changeKey());
0972         changeCommitted(col);
0973     } else {
0974         cancelTask(i18nc("@info:status", "Failed to update folder"));
0975     }
0976 }
0977 
0978 void EwsResource::collectionRemoved(const Collection &collection)
0979 {
0980     auto req = new EwsDeleteFolderRequest(mEwsClient, this);
0981     EwsId::List ids;
0982     ids.append(EwsId(collection.remoteId(), collection.remoteRevision()));
0983     req->setFolderIds(ids);
0984     connect(req, &EwsDeleteFolderRequest::result, this, &EwsResource::folderDeleteRequestFinished);
0985     req->start();
0986 }
0987 
0988 void EwsResource::folderDeleteRequestFinished(KJob *job)
0989 {
0990     if (job->error()) {
0991         cancelTask(i18nc("@info:status", "Failed to process folder delete request"));
0992         return;
0993     }
0994 
0995     auto req = qobject_cast<EwsDeleteFolderRequest *>(job);
0996     if (!req) {
0997         cancelTask(i18nc("@info:status", "Failed to delete folder - internal error"));
0998         return;
0999     }
1000 
1001     EwsDeleteFolderRequest::Response resp = req->responses().first();
1002     if (resp.isSuccess()) {
1003         changeProcessed();
1004     } else {
1005         cancelTask(i18nc("@info:status", "Failed to delete folder"));
1006         mFolderSyncState.clear();
1007         synchronizeCollectionTree();
1008     }
1009 }
1010 
1011 void EwsResource::sendItem(const Akonadi::Item &item)
1012 {
1013     EwsItemType type = EwsItemHandler::mimeToItemType(item.mimeType());
1014     if (isEwsMessageItemType(type)) {
1015         itemSent(item, TransportFailed, i18nc("@info:status", "Item type not supported for creation"));
1016     } else {
1017         EwsCreateItemJob *job = EwsItemHandler::itemHandler(type)->createItemJob(mEwsClient, item, Collection(), mTagStore, this);
1018         job->setSend(true);
1019         job->setProperty("item", QVariant::fromValue<Item>(item));
1020         connect(job, &EwsCreateItemJob::result, this, &EwsResource::itemSendRequestFinished);
1021         job->start();
1022     }
1023 }
1024 
1025 void EwsResource::itemSendRequestFinished(KJob *job)
1026 {
1027     Item item = job->property("item").value<Item>();
1028     if (job->error()) {
1029         itemSent(item, TransportFailed, i18nc("@info:status", "Failed to process item send request"));
1030         return;
1031     }
1032 
1033     auto req = qobject_cast<EwsCreateItemJob *>(job);
1034     if (!req) {
1035         itemSent(item, TransportFailed, i18nc("@info:status", "Failed to send item - internal error"));
1036         return;
1037     }
1038 
1039     itemSent(item, TransportSucceeded);
1040 }
1041 
1042 void EwsResource::sendMessage(const QString &id, const QByteArray &content)
1043 {
1044 #if HAVE_SEPARATE_MTA_RESOURCE
1045     auto req = new EwsCreateItemRequest(mEwsClient, this);
1046 
1047     EwsItem item;
1048     item.setType(EwsItemTypeMessage);
1049     item.setField(EwsItemFieldMimeContent, content);
1050     req->setItems(EwsItem::List() << item);
1051     req->setMessageDisposition(EwsDispSendOnly);
1052     req->setProperty("requestId", id);
1053     connect(req, &EwsCreateItemRequest::finished, this, &EwsResource::messageSendRequestFinished);
1054     req->start();
1055 #endif
1056 }
1057 
1058 #if HAVE_SEPARATE_MTA_RESOURCE
1059 void EwsResource::messageSendRequestFinished(KJob *job)
1060 {
1061     QString id = job->property("requestId").toString();
1062     if (job->error()) {
1063         Q_EMIT messageSent(id, i18nc("@info:status", "Failed to process item send request"));
1064         return;
1065     }
1066 
1067     auto req = qobject_cast<EwsCreateItemRequest *>(job);
1068     if (!req) {
1069         Q_EMIT messageSent(id, i18nc("@info:status", "Failed to send item - internal error"));
1070         return;
1071     }
1072 
1073     if (req->responses().count() != 1) {
1074         Q_EMIT messageSent(id, i18nc("@info:status", "Invalid number of responses received from server"));
1075         return;
1076     }
1077 
1078     EwsCreateItemRequest::Response resp = req->responses().first();
1079     if (resp.isSuccess()) {
1080         Q_EMIT messageSent(id, QString());
1081     } else {
1082         Q_EMIT messageSent(id, resp.responseMessage());
1083     }
1084 }
1085 
1086 #endif
1087 
1088 void EwsResource::foldersModifiedEvent(const EwsId::List &folders)
1089 {
1090     for (const EwsId &id : folders) {
1091         Collection c;
1092         c.setRemoteId(id.id());
1093         auto job = new CollectionFetchJob(c, CollectionFetchJob::Base);
1094         job->setFetchScope(changeRecorder()->collectionFetchScope());
1095         job->fetchScope().setResource(identifier());
1096         job->fetchScope().setListFilter(CollectionFetchScope::Sync);
1097         connect(job, &KJob::result, this, &EwsResource::foldersModifiedCollectionSyncFinished);
1098     }
1099 }
1100 
1101 void EwsResource::foldersModifiedCollectionSyncFinished(KJob *job)
1102 {
1103     if (job->error()) {
1104         qCDebug(EWSRES_LOG) << QStringLiteral("Failed to fetch collection tree for sync.");
1105         return;
1106     }
1107 
1108     auto fetchColJob = qobject_cast<CollectionFetchJob *>(job);
1109     const auto collection = fetchColJob->collections().at(0);
1110     queueFetchItemsJob(collection, SubscriptionSync, [this](EwsFetchItemsJob *fetchJob) {
1111         auto collection = fetchJob->collection();
1112         if (fetchJob->error()) {
1113             qCWarningNC(EWSRES_LOG) << QStringLiteral("Item fetch error:") << fetchJob->errorString() << fetchJob->error();
1114             synchronizeCollection(collection.id());
1115         } else {
1116             const auto newItems = fetchJob->newItems();
1117             for (const auto &newItem : newItems) {
1118                 new ItemCreateJob(newItem, collection, this);
1119             }
1120             if (!fetchJob->changedItems().isEmpty()) {
1121                 new ItemModifyJob(fetchJob->changedItems());
1122             }
1123             if (!fetchJob->deletedItems().isEmpty()) {
1124                 new ItemDeleteJob(fetchJob->deletedItems());
1125             }
1126             saveCollectionSyncState(collection, fetchJob->syncState());
1127             emitReadyStatus();
1128         }
1129     });
1130 }
1131 
1132 void EwsResource::folderTreeModifiedEvent()
1133 {
1134     synchronizeCollectionTree();
1135 }
1136 
1137 void EwsResource::fullSyncRequestedEvent()
1138 {
1139     synchronize();
1140 }
1141 
1142 void EwsResource::clearCollectionSyncState(int collectionId)
1143 {
1144     Collection col(collectionId);
1145     auto attr = col.attribute<EwsSyncStateAttribute>();
1146     col.addAttribute(attr);
1147     auto job = new CollectionModifyJob(col);
1148     job->start();
1149 }
1150 
1151 void EwsResource::clearFolderTreeSyncState()
1152 {
1153     mFolderSyncState.clear();
1154     saveState();
1155 }
1156 
1157 void EwsResource::fetchSpecialFolders()
1158 {
1159     auto job = new CollectionFetchJob(mRootCollection, CollectionFetchJob::Recursive, this);
1160     connect(job, &CollectionFetchJob::collectionsReceived, this, &EwsResource::specialFoldersCollectionsRetrieved);
1161     connect(job, &CollectionFetchJob::result, this, [](KJob *job) {
1162         if (job->error()) {
1163             qCWarningNC(EWSRES_LOG) << "Special folders fetch failed:" << job->errorString();
1164         }
1165     });
1166     job->start();
1167 }
1168 
1169 void EwsResource::specialFoldersCollectionsRetrieved(const Collection::List &folders)
1170 {
1171     EwsId::List queryItems;
1172 
1173     queryItems.reserve(specialFolderList.count());
1174     for (const SpecialFolders &sf : std::as_const(specialFolderList)) {
1175         queryItems.append(EwsId(sf.did));
1176     }
1177 
1178     if (!queryItems.isEmpty()) {
1179         auto req = new EwsGetFolderRequest(mEwsClient, this);
1180         req->setFolderShape(EwsFolderShape(EwsShapeIdOnly));
1181         req->setFolderIds(queryItems);
1182         req->setProperty("collections", QVariant::fromValue<Collection::List>(folders));
1183         connect(req, &EwsGetFolderRequest::finished, this, &EwsResource::specialFoldersFetchFinished);
1184         req->start();
1185     }
1186 }
1187 
1188 void EwsResource::specialFoldersFetchFinished(KJob *job)
1189 {
1190     if (job->error()) {
1191         qCWarningNC(EWSRES_LOG) << QStringLiteral("Special collection fetch failed:") << job->errorString();
1192         return;
1193     }
1194 
1195     auto req = qobject_cast<EwsGetFolderRequest *>(job);
1196     if (!req) {
1197         qCWarningNC(EWSRES_LOG) << QStringLiteral("Special collection fetch failed:") << QStringLiteral("Invalid EwsGetFolderRequest job object");
1198         return;
1199     }
1200 
1201     const auto collections = req->property("collections").value<Collection::List>();
1202 
1203     if (req->responses().size() != specialFolderList.size()) {
1204         qCWarningNC(EWSRES_LOG) << QStringLiteral("Special collection fetch failed:") << QStringLiteral("Invalid number of responses received");
1205         return;
1206     }
1207 
1208     QMap<QString, Collection> map;
1209     for (const Collection &col : collections) {
1210         map.insert(col.remoteId(), col);
1211     }
1212 
1213     auto it = specialFolderList.cbegin();
1214     const auto responses{req->responses()};
1215     for (const EwsGetFolderRequest::Response &resp : responses) {
1216         if (resp.isSuccess()) {
1217             auto fid = resp.folder()[EwsFolderFieldFolderId].value<EwsId>();
1218             QMap<QString, Collection>::iterator mapIt = map.find(fid.id());
1219             if (mapIt != map.end()) {
1220                 qCDebugNC(EWSRES_LOG)
1221                     << QStringLiteral("Registering folder %1(%2) as special collection %3").arg(ewsHash(mapIt->remoteId())).arg(mapIt->id()).arg(it->type);
1222                 SpecialMailCollections::self()->registerCollection(it->type, *mapIt);
1223                 if (!mapIt->hasAttribute<EntityDisplayAttribute>()) {
1224                     auto attr = mapIt->attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
1225                     attr->setIconName(it->iconName);
1226                     auto modJob = new CollectionModifyJob(*mapIt, this);
1227                     modJob->start();
1228                 }
1229             }
1230         }
1231         it++;
1232     }
1233 }
1234 
1235 void EwsResource::saveState()
1236 {
1237     QByteArray str;
1238     QDataStream dataStream(&str, QIODevice::WriteOnly);
1239     mSettings->setFolderSyncState(QString::fromLatin1(qCompress(mFolderSyncState.toLatin1(), 9).toBase64()));
1240     mSettings->save();
1241 }
1242 
1243 void EwsResource::doSetOnline(bool online)
1244 {
1245     if (online) {
1246         reloadConfig();
1247     } else {
1248         mSubManager.reset(nullptr);
1249     }
1250 }
1251 
1252 int EwsResource::reconnectTimeout()
1253 {
1254     int timeout = mReconnectTimeout;
1255     if (mReconnectTimeout < MaxReconnectTimeout) {
1256         mReconnectTimeout *= 2;
1257     }
1258     return timeout;
1259 }
1260 
1261 void EwsResource::itemsTagsChanged(const Item::List &items, const QSet<Tag> &addedTags, const QSet<Tag> &removedTags)
1262 {
1263     Q_UNUSED(addedTags)
1264     Q_UNUSED(removedTags)
1265 
1266     Q_EMIT status(Running, i18nc("@info:status", "Updating item tags"));
1267 
1268     auto job = new EwsUpdateItemsTagsJob(items, mTagStore, mEwsClient, this);
1269     connect(job, &EwsUpdateItemsTagsJob::result, this, &EwsResource::itemsTagChangeFinished);
1270     connectStatusSignals(job);
1271     job->start();
1272 }
1273 
1274 void EwsResource::itemsTagChangeFinished(KJob *job)
1275 {
1276     emitReadyStatus();
1277 
1278     if (job->error()) {
1279         cancelTask(i18nc("@info:status", "Failed to process item tags update request"));
1280         return;
1281     }
1282 
1283     auto updJob = qobject_cast<EwsUpdateItemsTagsJob *>(job);
1284     if (!updJob) {
1285         cancelTask(i18nc("@info:status", "Failed to update item tags - internal error"));
1286         return;
1287     }
1288 
1289     changesCommitted(updJob->items());
1290 }
1291 
1292 void EwsResource::tagAdded(const Tag &tag)
1293 {
1294     mTagStore->addTag(tag);
1295 
1296     auto job = new EwsGlobalTagsWriteJob(mTagStore, mEwsClient, mRootCollection, this);
1297     connect(job, &EwsGlobalTagsWriteJob::result, this, &EwsResource::globalTagChangeFinished);
1298     job->start();
1299 }
1300 
1301 void EwsResource::tagChanged(const Tag &tag)
1302 {
1303     mTagStore->addTag(tag);
1304 
1305     auto job = new EwsGlobalTagsWriteJob(mTagStore, mEwsClient, mRootCollection, this);
1306     connect(job, &EwsGlobalTagsWriteJob::result, this, &EwsResource::globalTagChangeFinished);
1307     job->start();
1308 }
1309 
1310 void EwsResource::tagRemoved(const Tag &tag)
1311 {
1312     mTagStore->removeTag(tag);
1313 
1314     auto job = new EwsGlobalTagsWriteJob(mTagStore, mEwsClient, mRootCollection, this);
1315     connect(job, &EwsGlobalTagsWriteJob::result, this, &EwsResource::globalTagChangeFinished);
1316     job->start();
1317 }
1318 
1319 void EwsResource::globalTagChangeFinished(KJob *job)
1320 {
1321     if (job->error()) {
1322         cancelTask(i18nc("@info:status", "Failed to process global tag update request"));
1323     } else {
1324         changeProcessed();
1325     }
1326 }
1327 
1328 void EwsResource::retrieveTags()
1329 {
1330     auto job = new EwsGlobalTagsReadJob(mTagStore, mEwsClient, mRootCollection, this);
1331     connect(job, &EwsGlobalTagsReadJob::result, this, &EwsResource::globalTagsRetrievalFinished);
1332     job->start();
1333 }
1334 
1335 void EwsResource::globalTagsRetrievalFinished(KJob *job)
1336 {
1337     if (job->error()) {
1338         cancelTask(i18nc("@info:status", "Failed to process global tags retrieval request"));
1339     } else {
1340         auto readJob = qobject_cast<EwsGlobalTagsReadJob *>(job);
1341         Q_ASSERT(readJob);
1342         tagsRetrieved(readJob->tags(), QHash<QString, Item::List>());
1343     }
1344 }
1345 
1346 void EwsResource::setUpAuth()
1347 {
1348     EwsAbstractAuth *auth = mSettings->loadAuth(this);
1349 
1350     /* Use queued connections here to avoid stack overflow when the reauthentication proceeds through all stages. */
1351     connect(auth, &EwsAbstractAuth::authSucceeded, this, &EwsResource::authSucceeded, Qt::QueuedConnection);
1352     connect(auth, &EwsAbstractAuth::authFailed, this, &EwsResource::authFailed, Qt::QueuedConnection);
1353     connect(auth, &EwsAbstractAuth::requestAuthFailed, this, &EwsResource::requestAuthFailed, Qt::QueuedConnection);
1354 
1355     qCDebugNC(EWSRES_LOG) << QStringLiteral("Initializing authentication");
1356 
1357     mAuth.reset(auth);
1358 
1359     auth->init();
1360 }
1361 
1362 void EwsResource::authSucceeded()
1363 {
1364     if (mAuthStage != AuthIdle) {
1365         setOnline(true);
1366     }
1367 
1368     mAuthStage = AuthIdle;
1369 
1370     resetUrl();
1371 }
1372 
1373 void EwsResource::reauthNotificationDismissed(bool accepted)
1374 {
1375     if (mReauthNotification) {
1376         mReauthNotification.clear();
1377         if (accepted) {
1378             mAuth->authenticate(true);
1379         } else {
1380             authFailed(QStringLiteral("Interactive authentication request denied"));
1381         }
1382     }
1383 }
1384 
1385 void EwsResource::authFailed(const QString &error)
1386 {
1387     qCWarningNC(EWSRES_LOG) << "Authentication failed: " << error;
1388 
1389     reauthenticate();
1390 }
1391 
1392 void EwsResource::reauthenticate()
1393 {
1394     switch (mAuthStage) {
1395     case AuthIdle:
1396         mAuthStage = AuthRefreshToken;
1397         qCWarningNC(EWSRES_LOG) << "reauthenticate: trying to refresh";
1398         if (mAuth->authenticate(false)) {
1399             break;
1400         }
1401     /* fall through */
1402     case AuthRefreshToken: {
1403         mAuthStage = AuthAccessToken;
1404         const auto reauthPrompt = mAuth->reauthPrompt();
1405         if (!reauthPrompt.isNull()) {
1406             mReauthNotification = new KNotification(QStringLiteral("auth-expired"), KNotification::Persistent, this);
1407 
1408             mReauthNotification->setText(reauthPrompt.arg(name()));
1409             mReauthNotification->setComponentName(QStringLiteral("akonadi_ews_resource"));
1410             auto acceptedFn = std::bind(&EwsResource::reauthNotificationDismissed, this, true);
1411             auto rejectedFn = std::bind(&EwsResource::reauthNotificationDismissed, this, false);
1412             connect(mReauthNotification.data(), &KNotification::closed, this, rejectedFn);
1413             connect(mReauthNotification.data(), &KNotification::ignored, this, rejectedFn);
1414 
1415             auto authenticateAction = mReauthNotification->addAction(i18nc("@action:button", "Authenticate"));
1416             connect(authenticateAction, &KNotificationAction::activated, this, acceptedFn);
1417 
1418             mReauthNotification->sendEvent();
1419             break;
1420         }
1421     }
1422     /* fall through */
1423     case AuthAccessToken:
1424         mAuthStage = AuthFailure;
1425         Q_EMIT status(Broken, i18nc("@info:status", "Authentication failed"));
1426         break;
1427     case AuthFailure:
1428         break;
1429     }
1430 }
1431 
1432 void EwsResource::requestAuthFailed()
1433 {
1434     qCWarningNC(EWSRES_LOG) << "requestAuthFailed - going offline";
1435 
1436     if (mAuthStage == AuthIdle) {
1437         QTimer::singleShot(0, this, [&]() {
1438             setTemporaryOffline(reconnectTimeout());
1439         });
1440         Q_EMIT status(Broken, i18nc("@info:status", "Authentication failed"));
1441 
1442         reauthenticate();
1443     }
1444 }
1445 
1446 void EwsResource::emitReadyStatus()
1447 {
1448     Q_EMIT status(Idle, i18nc("@info:status Resource is ready", "Ready"));
1449     Q_EMIT percent(0);
1450 }
1451 
1452 void EwsResource::adjustRootCollectionName(const QString &newName)
1453 {
1454     if (mRootCollection.isValid()) {
1455         auto attr = mRootCollection.attribute<Akonadi::EntityDisplayAttribute>(Akonadi::Collection::AddIfMissing);
1456         if (attr->displayName() != newName) {
1457             attr->setDisplayName(newName);
1458             new CollectionModifyJob(mRootCollection);
1459         }
1460     }
1461 }
1462 
1463 void EwsResource::setInitialReconnectTimeout(int timeout)
1464 {
1465     mInitialReconnectTimeout = mReconnectTimeout = timeout;
1466 }
1467 
1468 template<class Job>
1469 void EwsResource::connectStatusSignals(Job *job)
1470 {
1471     connect(job, &Job::reportStatus, this, [this](int s, const QString &message) {
1472         Q_EMIT status(s, message);
1473     });
1474     connect(job, &Job::reportPercent, this, [this](int p) {
1475         Q_EMIT percent(p);
1476     });
1477 }
1478 
1479 QString EwsResource::getCollectionSyncState(const Akonadi::Collection &col)
1480 {
1481     auto attr = col.attribute<EwsSyncStateAttribute>();
1482     return attr ? attr->syncState() : QString();
1483 }
1484 
1485 void EwsResource::saveCollectionSyncState(Akonadi::Collection &col, const QString &state)
1486 {
1487     col.addAttribute(new EwsSyncStateAttribute(state));
1488     auto job = new CollectionModifyJob(col);
1489     job->start();
1490 }
1491 
1492 QString EwsResource::dumpResourceToString() const
1493 {
1494     QString dump = QStringLiteral("item sync queue (%1):\n").arg(mFetchItemsJobQueue.count());
1495 
1496     for (const auto &item : std::as_const(mFetchItemsJobQueue)) {
1497         dump += QStringLiteral(" %1:%2\n").arg(item.col.id()).arg(item.type);
1498     }
1499 
1500     return dump;
1501 }
1502 
1503 AKONADI_RESOURCE_MAIN(EwsResource)
1504 
1505 #include "moc_ewsresource.cpp"