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"