File indexing completed on 2024-06-02 05:20:58

0001 /*
0002     SPDX-FileCopyrightText: 2015-2016 Krzysztof Nowicki <krissn@op.pl>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "ewsfetchfoldersjob.h"
0008 
0009 #include <Akonadi/CollectionFetchJob>
0010 #include <Akonadi/CollectionFetchScope>
0011 #include <Akonadi/CollectionMoveJob>
0012 #include <Akonadi/CollectionStatistics>
0013 #include <KCalendarCore/Event>
0014 #include <KCalendarCore/Todo>
0015 #include <KContacts/Addressee>
0016 #include <KContacts/ContactGroup>
0017 #include <KMime/Message>
0018 
0019 #include "ewsclient.h"
0020 #include "ewseffectiverights.h"
0021 #include "ewsgetfolderrequest.h"
0022 #include "ewsresource_debug.h"
0023 #include "ewssyncfolderhierarchyrequest.h"
0024 
0025 using namespace Akonadi;
0026 
0027 static const EwsPropertyField propPidTagContainerClass(0x3613, EwsPropTypeString);
0028 
0029 static constexpr int fetchBatchSize = 50;
0030 
0031 class EwsFetchFoldersJobPrivate : public QObject
0032 {
0033 public:
0034     EwsFetchFoldersJobPrivate(EwsFetchFoldersJob *parent, EwsClient &client, const Collection &rootCollection);
0035     ~EwsFetchFoldersJobPrivate() override;
0036 
0037     void processRemoteFolders();
0038     Collection createFolderCollection(const EwsFolder &folder);
0039 
0040     void buildCollectionList();
0041     void buildChildCollectionList(const Collection &col);
0042 public Q_SLOTS:
0043     void remoteFolderFullFetchDone(KJob *job);
0044     void remoteFolderIdFullFetchDone(KJob *job);
0045     void remoteFolderDetailFetchDone(KJob *job);
0046 
0047 public:
0048     EwsClient &mClient;
0049     int mPendingFetchJobs;
0050     int mPendingMoveJobs;
0051     EwsId::List mRemoteFolderIds;
0052 
0053     const Collection &mRootCollection;
0054 
0055     EwsFolder::List mRemoteChangedFolders; // Contains details of folders that need update
0056                                            // (either created or changed)
0057     QHash<QString, Akonadi::Collection> mCollectionMap;
0058     QMultiHash<QString, QString> mParentMap;
0059 
0060     EwsFetchFoldersJob *q_ptr;
0061     Q_DECLARE_PUBLIC(EwsFetchFoldersJob)
0062 };
0063 
0064 EwsFetchFoldersJobPrivate::EwsFetchFoldersJobPrivate(EwsFetchFoldersJob *parent, EwsClient &client, const Collection &rootCollection)
0065     : QObject(parent)
0066     , mClient(client)
0067     , mRootCollection(rootCollection)
0068     , q_ptr(parent)
0069 {
0070     mPendingFetchJobs = 0;
0071     mPendingMoveJobs = 0;
0072 }
0073 
0074 EwsFetchFoldersJobPrivate::~EwsFetchFoldersJobPrivate() = default;
0075 
0076 void EwsFetchFoldersJobPrivate::remoteFolderFullFetchDone(KJob *job)
0077 {
0078     Q_Q(EwsFetchFoldersJob);
0079 
0080     auto req = qobject_cast<EwsSyncFolderHierarchyRequest *>(job);
0081     if (!req) {
0082         qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsSyncFolderHierarchyRequest job object");
0083         q->setErrorMsg(QStringLiteral("Invalid EwsSyncFolderHierarchyRequest job object"));
0084         q->emitResult();
0085         return;
0086     }
0087 
0088     if (req->error()) {
0089         /* It has been reported that the SyncFolderHierarchyRequest can fail with Internal Server
0090          * Error (possibly because of a large number of folders). In order to work around this
0091          * try to fallback to fetching just the folder identifiers and retrieve the details later. */
0092         qCDebug(EWSRES_LOG) << QStringLiteral("Full fetch failed. Trying to fetch ids only.");
0093 
0094         auto syncFoldersReq = new EwsSyncFolderHierarchyRequest(mClient, this);
0095         syncFoldersReq->setFolderId(EwsId(EwsDIdMsgFolderRoot));
0096         EwsFolderShape shape(EwsShapeIdOnly);
0097         syncFoldersReq->setFolderShape(shape);
0098         connect(syncFoldersReq, &EwsSyncFolderHierarchyRequest::result, this, &EwsFetchFoldersJobPrivate::remoteFolderIdFullFetchDone);
0099         q->addSubjob(syncFoldersReq);
0100         syncFoldersReq->start();
0101 
0102         return;
0103     }
0104 
0105     const auto reqChanges{req->changes()};
0106     for (const EwsSyncFolderHierarchyRequest::Change &ch : reqChanges) {
0107         if (ch.type() == EwsSyncFolderHierarchyRequest::Create) {
0108             mRemoteChangedFolders.append(ch.folder());
0109         } else {
0110             q->setErrorMsg(QStringLiteral("Got non-create change for full sync."));
0111             q->emitResult();
0112             return;
0113         }
0114     }
0115 
0116     if (req->includesLastItem()) {
0117         processRemoteFolders();
0118 
0119         buildCollectionList();
0120 
0121         q->mSyncState = req->syncState();
0122 
0123         q->emitResult();
0124     } else {
0125         auto syncFoldersReq = new EwsSyncFolderHierarchyRequest(mClient, this);
0126         syncFoldersReq->setFolderId(EwsId(EwsDIdMsgFolderRoot));
0127         EwsFolderShape shape;
0128         shape << propPidTagContainerClass;
0129         shape << EwsPropertyField(QStringLiteral("folder:EffectiveRights"));
0130         shape << EwsPropertyField(QStringLiteral("folder:ParentFolderId"));
0131         syncFoldersReq->setFolderShape(shape);
0132         syncFoldersReq->setSyncState(req->syncState());
0133         connect(syncFoldersReq, &EwsSyncFolderHierarchyRequest::result, this, &EwsFetchFoldersJobPrivate::remoteFolderFullFetchDone);
0134         syncFoldersReq->start();
0135     }
0136 }
0137 
0138 void EwsFetchFoldersJobPrivate::remoteFolderIdFullFetchDone(KJob *job)
0139 {
0140     Q_Q(EwsFetchFoldersJob);
0141 
0142     auto req = qobject_cast<EwsSyncFolderHierarchyRequest *>(job);
0143     if (!req) {
0144         qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsSyncFolderHierarchyRequest job object");
0145         q->setErrorMsg(QStringLiteral("Invalid EwsSyncFolderHierarchyRequest job object"));
0146         q->emitResult();
0147         return;
0148     }
0149 
0150     if (req->error()) {
0151         return;
0152     }
0153 
0154     const auto reqChanges{req->changes()};
0155     for (const EwsSyncFolderHierarchyRequest::Change &ch : reqChanges) {
0156         if (ch.type() == EwsSyncFolderHierarchyRequest::Create) {
0157             mRemoteFolderIds.append(ch.folder()[EwsFolderFieldFolderId].value<EwsId>());
0158         } else {
0159             q->setErrorMsg(QStringLiteral("Got non-create change for full sync."));
0160             q->emitResult();
0161             return;
0162         }
0163     }
0164 
0165     if (req->includesLastItem()) {
0166         EwsFolderShape shape(EwsShapeDefault);
0167         shape << propPidTagContainerClass;
0168         shape << EwsPropertyField(QStringLiteral("folder:EffectiveRights"));
0169         shape << EwsPropertyField(QStringLiteral("folder:ParentFolderId"));
0170         mPendingFetchJobs = 0;
0171 
0172         for (int i = 0, total = mRemoteFolderIds.size(); i < total; i += fetchBatchSize) {
0173             auto folderReq = new EwsGetFolderRequest(mClient, this);
0174             folderReq->setFolderIds(mRemoteFolderIds.mid(i, fetchBatchSize));
0175             folderReq->setFolderShape(shape);
0176             connect(folderReq, &EwsSyncFolderHierarchyRequest::result, this, &EwsFetchFoldersJobPrivate::remoteFolderDetailFetchDone);
0177             folderReq->start();
0178             q->addSubjob(folderReq);
0179             mPendingFetchJobs++;
0180         }
0181 
0182         qCDebugNC(EWSRES_LOG) << QStringLiteral("Starting %1 folder fetch jobs.").arg(mPendingFetchJobs);
0183 
0184         q->mSyncState = req->syncState();
0185     } else {
0186         auto syncFoldersReq = new EwsSyncFolderHierarchyRequest(mClient, this);
0187         syncFoldersReq->setFolderId(EwsId(EwsDIdMsgFolderRoot));
0188         EwsFolderShape shape(EwsShapeIdOnly);
0189         syncFoldersReq->setFolderShape(shape);
0190         syncFoldersReq->setSyncState(req->syncState());
0191         connect(syncFoldersReq, &EwsSyncFolderHierarchyRequest::result, this, &EwsFetchFoldersJobPrivate::remoteFolderIdFullFetchDone);
0192         q->addSubjob(syncFoldersReq);
0193         syncFoldersReq->start();
0194     }
0195 }
0196 
0197 void EwsFetchFoldersJobPrivate::remoteFolderDetailFetchDone(KJob *job)
0198 {
0199     Q_Q(EwsFetchFoldersJob);
0200 
0201     auto req = qobject_cast<EwsGetFolderRequest *>(job);
0202     if (!req) {
0203         qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsGetFolderRequest job object");
0204         q->setErrorMsg(QStringLiteral("Invalid EwsGetFolderRequest job object"));
0205         q->emitResult();
0206         return;
0207     }
0208 
0209     if (req->error()) {
0210         return;
0211     }
0212 
0213     const auto reqResponses{req->responses()};
0214     for (const EwsGetFolderRequest::Response &resp : reqResponses) {
0215         if (resp.isSuccess()) {
0216             mRemoteChangedFolders.append(resp.folder());
0217         } else {
0218             qCWarningNC(EWSRES_LOG) << QStringLiteral("Failed to fetch folder details.");
0219         }
0220     }
0221 
0222     mPendingFetchJobs--;
0223     qCDebugNC(EWSRES_LOG) << QStringLiteral("%1 folder fetch jobs pending").arg(mPendingFetchJobs);
0224 
0225     if (mPendingFetchJobs == 0) {
0226         qCDebugNC(EWSRES_LOG) << QStringLiteral("All folder fetch jobs complete");
0227 
0228         processRemoteFolders();
0229 
0230         buildCollectionList();
0231 
0232         q->emitResult();
0233     }
0234 }
0235 
0236 void EwsFetchFoldersJobPrivate::processRemoteFolders()
0237 {
0238     /* mCollectionMap contains the global collection list keyed by the EWS ID. */
0239     /* mParentMap contains the parent->child map for each collection. */
0240 
0241     /* Iterate over all changed folders. */
0242     for (const EwsFolder &folder : std::as_const(mRemoteChangedFolders)) {
0243         /* Create a collection for each folder. */
0244         Collection c = createFolderCollection(folder);
0245 
0246         /* Insert it into the global collection list. */
0247         mCollectionMap.insert(c.remoteId(), c);
0248 
0249         /* Determine the parent and insert a parent->child relationship.
0250          * Don't use Collection::setParentCollection() yet as the collection object will be updated
0251          * which will cause the parent->child relationship to be broken. This happens because the
0252          * collection object holds a copy of the parent collection object. An update to that
0253          * object in the list will not be visible in the copy inside of the child object. */
0254         auto parentId = folder[EwsFolderFieldParentFolderId].value<EwsId>();
0255         mParentMap.insert(parentId.id(), c.remoteId());
0256     }
0257 }
0258 
0259 void EwsFetchFoldersJobPrivate::buildCollectionList()
0260 {
0261     Q_Q(EwsFetchFoldersJob);
0262 
0263     q->mFolders.append(mRootCollection);
0264     buildChildCollectionList(mRootCollection);
0265 
0266     if (!mCollectionMap.isEmpty()) {
0267         q->setErrorMsg(QStringLiteral("Found orphaned collections"));
0268     }
0269 }
0270 
0271 void EwsFetchFoldersJobPrivate::buildChildCollectionList(const Collection &col)
0272 {
0273     Q_Q(EwsFetchFoldersJob);
0274 
0275     const QStringList children = mParentMap.values(col.remoteId());
0276     for (const QString &childId : children) {
0277         Collection child(mCollectionMap.take(childId));
0278         child.setParentCollection(col);
0279         q->mFolders.append(child);
0280         buildChildCollectionList(child);
0281     }
0282 }
0283 
0284 Collection EwsFetchFoldersJobPrivate::createFolderCollection(const EwsFolder &folder)
0285 {
0286     Collection collection;
0287     collection.setName(folder[EwsFolderFieldDisplayName].toString());
0288     QStringList mimeTypes;
0289     QString contClass = folder[propPidTagContainerClass].toString();
0290     mimeTypes.append(Collection::mimeType());
0291     switch (folder.type()) {
0292     case EwsFolderTypeCalendar:
0293         mimeTypes.append(KCalendarCore::Event::eventMimeType());
0294         break;
0295     case EwsFolderTypeContacts:
0296         mimeTypes.append(KContacts::Addressee::mimeType());
0297         mimeTypes.append(KContacts::ContactGroup::mimeType());
0298         break;
0299     case EwsFolderTypeTasks:
0300         mimeTypes.append(KCalendarCore::Todo::todoMimeType());
0301         break;
0302     case EwsFolderTypeMail:
0303         if (contClass == QLatin1StringView("IPF.Note") || contClass.isEmpty()) {
0304             mimeTypes.append(KMime::Message::mimeType());
0305         }
0306         break;
0307     default:
0308         break;
0309     }
0310     collection.setContentMimeTypes(mimeTypes);
0311     Collection::Rights colRights;
0312     auto ewsRights = folder[EwsFolderFieldEffectiveRights].value<EwsEffectiveRights>();
0313     // FIXME: For now full read/write support is only implemented for e-mail. In order to avoid
0314     // potential problems block write access to all other folder types.
0315     if (folder.type() == EwsFolderTypeMail) {
0316         if (ewsRights.canDelete()) {
0317             colRights |= Collection::CanDeleteCollection | Collection::CanDeleteItem;
0318         }
0319         if (ewsRights.canModify()) {
0320             colRights |= Collection::CanChangeCollection | Collection::CanChangeItem;
0321         }
0322         if (ewsRights.canCreateContents()) {
0323             colRights |= Collection::CanCreateItem;
0324         }
0325         if (ewsRights.canCreateHierarchy()) {
0326             colRights |= Collection::CanCreateCollection;
0327         }
0328     }
0329     collection.setRights(colRights);
0330     auto id = folder[EwsFolderFieldFolderId].value<EwsId>();
0331     collection.setRemoteId(id.id());
0332     collection.setRemoteRevision(id.changeKey());
0333 
0334     return collection;
0335 }
0336 
0337 EwsFetchFoldersJob::EwsFetchFoldersJob(EwsClient &client, const Akonadi::Collection &rootCollection, QObject *parent)
0338     : EwsJob(parent)
0339     , d_ptr(new EwsFetchFoldersJobPrivate(this, client, rootCollection))
0340 {
0341     qRegisterMetaType<EwsId::List>();
0342 }
0343 
0344 EwsFetchFoldersJob::~EwsFetchFoldersJob() = default;
0345 
0346 void EwsFetchFoldersJob::start()
0347 {
0348     Q_D(const EwsFetchFoldersJob);
0349 
0350     auto syncFoldersReq = new EwsSyncFolderHierarchyRequest(d->mClient, this);
0351     syncFoldersReq->setFolderId(EwsId(EwsDIdMsgFolderRoot));
0352     EwsFolderShape shape;
0353     shape << propPidTagContainerClass;
0354     shape << EwsPropertyField(QStringLiteral("folder:EffectiveRights"));
0355     shape << EwsPropertyField(QStringLiteral("folder:ParentFolderId"));
0356     syncFoldersReq->setFolderShape(shape);
0357     if (!mSyncState.isNull()) {
0358         syncFoldersReq->setSyncState(mSyncState);
0359     }
0360     connect(syncFoldersReq, &EwsSyncFolderHierarchyRequest::result, d, &EwsFetchFoldersJobPrivate::remoteFolderFullFetchDone);
0361     // Don't add this as a subjob as the error is handled in its own way rather than throwing an
0362     // error code to the parent.
0363 
0364     syncFoldersReq->start();
0365 }
0366 
0367 #include "moc_ewsfetchfoldersjob.cpp"