File indexing completed on 2024-11-17 04:44:57

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 "ewsfetchfoldersincrjob.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 "ewsresource_debug.h"
0022 #include "ewssyncfolderhierarchyrequest.h"
0023 
0024 using namespace Akonadi;
0025 
0026 /*
0027  * Performing an incremental folder tree update relies on the SyncFolderHierarchy EWS request,
0028  * which returns a list of changes to the folder tree. Each of the change can be a folder creation,
0029  * modification or removal notification.
0030  *
0031  * The EwsFetchFoldersIncrJob class starts by executing the SyncFolderHierarchy request in order to
0032  * retrieve the remote changes.
0033  *
0034  * Once that is completed the remoteFolderIncrFetchDone() method processes the changes. For each
0035  * one a folder descriptor (FolderDescr) is created and inserted into the folder hash (mFolderHash)
0036  * keyed by the folder EWS identifier. The folder hash contains all collections that are being
0037  * processed during the update. The flags member is used to determine the type of each collection.
0038  * For each change the corresponding local collection needs to be retrieved in order to correctly
0039  * pass the change list to Akonadi. The following rules apply:
0040  *
0041  *  * For created folders the parent collection is retrieved. This is necessary to put a valid parent
0042  *    collection to the newly created one. In order to handle cascaded folder creations
0043  *    (i.e. two folders are created, one child of the other) the parent collection is only retrieved
0044  *    for the topmost created folder.
0045  *  * For updated/modified folders both the current (corresponding to the EWS updated folder) and
0046  *    parent collections are retrieved. The current collection is retrieved to update only the
0047  *    changed information in the collection. The parent collection is retrieved in order to detect
0048  *    and handle collection moves as in such case the Akonadi-side parent will not be the same as
0049  *    the EWS-side parent (the parent retrieved is the EWS-side parent and the Akonadi-side parent
0050  *    will be known as part of the current collection once retrieved from Akonadi).
0051  *  * For deleted folders the current (corresponding to the EWS deleted folder) is retrieved.
0052  *
0053  * After the local Akonadi collections are retrieved the objects are put into their corresponding
0054  * folder descriptors in the folder hash.
0055  *
0056  * Having information about all remote changed folders and their local Akonadi collections the main
0057  * part of the synchronization process can be started.
0058  *
0059  * In the first pass the processRemoteFolders() method looks at all folders in the hash table.
0060  * Setting the parent-child relationships is performed at this stage only when the relevant parent
0061  * collection has already been processed (the FolderDescr::Processed flag is set). This ensures
0062  * that the parent-child relationships are set in the down-the-tree order. In case this condition
0063  * is not met for a collection the need for an extra reparenting pass is flagged and the parent
0064  * collection setting is not performed.
0065  *
0066  * Two types of folders are of main interest:
0067  *
0068  *  * For created folders the Akonadi collection object is created and populated with data obtained
0069  *    from Exchange. If the parent collection has already been processed in this pass the parent is
0070  *    set on the newly created collection.
0071  *  * For modified folders the Akonadi collection object that was retrieved previously is updated
0072  *    with data obtained from Exchange. If the folder was moved (the Akonadi parent differs from the
0073  *    Exchange parent) a collection move is attempted. This needs to be done explicitly using a
0074  *    CollectionMoveJob as Akonadi is unable to detect collection moves in the sync code. Similar
0075  *    to the created folder case the move is only performed in case the new parent is flagged as
0076  *    processed. Additionally the code checks if the new parent is a newly created folder. In such
0077  *    case the whole incremental sync is aborted as handling this rare corner case would introduce
0078  *    extra complexity. In case of incremental sync failure the resource will fallback to a full
0079  *    sync that will handle the case.
0080  *
0081  * Regardless of collection type the first pass also builds a list of top-level collections
0082  * (i.e. ones for which the parent is not in the folder hash) and a hash containing the parent-child
0083  * relationship. Both lists will be needed in case a reparenting pass is needed.
0084  *
0085  * The optional reparenting pass follows the first pass. It is performed if processing of at least
0086  * one collection failed due to an unprocessed parent. The reparenting pass focuses on the top-level
0087  * folders and starting from each recursively goes into its children setting their parent to itself.
0088  * The pass also processes any delayed collection moves in case executing them was impossible in the
0089  * first pass.
0090  *
0091  * The final stage of the synchronization process builds a list of changed and deleted collections
0092  * for Akonadi. At this stage all collections must be processed, otherwise an error is raised. If
0093  * no collection moves have been executed the job is completed. Otherwise the completion is
0094  * singalled once all moves are done.
0095  */
0096 
0097 static const EwsPropertyField propPidTagContainerClass(0x3613, EwsPropTypeString);
0098 
0099 class FolderDescr
0100 {
0101 public:
0102     using Flag = enum { RemoteCreated = 0x0001, RemoteUpdated = 0x0002, RemoteDeleted = 0x0004, Processed = 0x0008 };
0103     Q_DECLARE_FLAGS(Flags, Flag)
0104 
0105     FolderDescr() = default;
0106 
0107     Akonadi::Collection collection;
0108     Flags flags;
0109     EwsFolder ewsFolder;
0110 
0111     [[nodiscard]] bool isCreated() const
0112     {
0113         return flags & RemoteCreated;
0114     }
0115 
0116     [[nodiscard]] bool isModified() const
0117     {
0118         return flags & RemoteUpdated;
0119     }
0120 
0121     [[nodiscard]] bool isRemoved() const
0122     {
0123         return flags & RemoteDeleted;
0124     }
0125 
0126     [[nodiscard]] bool isProcessed() const
0127     {
0128         return flags & Processed;
0129     }
0130 
0131     [[nodiscard]] QString parent() const
0132     {
0133         return ewsFolder.isValid() ? ewsFolder[EwsFolderFieldParentFolderId].value<EwsId>().id() : QString();
0134     }
0135 };
0136 
0137 Q_DECLARE_OPERATORS_FOR_FLAGS(FolderDescr::Flags)
0138 
0139 class EwsFetchFoldersIncrJobPrivate : public QObject
0140 {
0141 public:
0142     EwsFetchFoldersIncrJobPrivate(EwsFetchFoldersIncrJob *parent, EwsClient &client, const Collection &rootCollection);
0143     ~EwsFetchFoldersIncrJobPrivate() override;
0144 
0145     bool processRemoteFolders();
0146     void updateFolderCollection(Collection &collection, const EwsFolder &folder);
0147 
0148     void reparentRemoteFolder(const QString &id);
0149     void moveCollection(const FolderDescr &fd);
0150 public Q_SLOTS:
0151     void remoteFolderIncrFetchDone(KJob *job);
0152     void localFolderFetchDone(KJob *job);
0153     void localFolderMoveDone(KJob *job);
0154 
0155 public:
0156     EwsClient &mClient;
0157     int mPendingMoveJobs;
0158     EwsId::List mRemoteFolderIds;
0159 
0160     const Collection &mRootCollection;
0161 
0162     QMultiHash<QString, QString> mParentMap;
0163 
0164     QHash<QString, FolderDescr> mFolderHash;
0165 
0166     EwsFetchFoldersIncrJob *q_ptr;
0167     Q_DECLARE_PUBLIC(EwsFetchFoldersIncrJob)
0168 };
0169 
0170 EwsFetchFoldersIncrJobPrivate::EwsFetchFoldersIncrJobPrivate(EwsFetchFoldersIncrJob *parent, EwsClient &client, const Collection &rootCollection)
0171     : QObject(parent)
0172     , mClient(client)
0173     , mRootCollection(rootCollection)
0174     , q_ptr(parent)
0175 {
0176     mPendingMoveJobs = 0;
0177 }
0178 
0179 EwsFetchFoldersIncrJobPrivate::~EwsFetchFoldersIncrJobPrivate() = default;
0180 
0181 void EwsFetchFoldersIncrJobPrivate::remoteFolderIncrFetchDone(KJob *job)
0182 {
0183     Q_Q(EwsFetchFoldersIncrJob);
0184 
0185     auto req = qobject_cast<EwsSyncFolderHierarchyRequest *>(job);
0186     if (!req) {
0187         qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsSyncFolderHierarchyRequestjob object");
0188         q->setErrorMsg(QStringLiteral("Invalid EwsSyncFolderHierarchyRequest job object"));
0189         q->emitResult();
0190         return;
0191     }
0192 
0193     if (req->error()) {
0194         return;
0195     }
0196 
0197     if (req->changes().isEmpty()) {
0198         /* Nothing to do. */
0199         q->emitResult();
0200         return;
0201     }
0202 
0203     /* Build a list of local collections to fetch in response to the remote changes.
0204      * Use a hash to auto-eliminate duplicates. */
0205     QHash<QString, Collection> localFetchHash;
0206 
0207     const auto reqChanges{req->changes()};
0208     for (const EwsSyncFolderHierarchyRequest::Change &ch : reqChanges) {
0209         FolderDescr fd;
0210         Collection c;
0211 
0212         switch (ch.type()) {
0213         case EwsSyncFolderHierarchyRequest::Update: {
0214             fd.ewsFolder = ch.folder();
0215             fd.flags |= FolderDescr::RemoteUpdated;
0216             auto id = fd.ewsFolder[EwsFolderFieldFolderId].value<EwsId>();
0217             mFolderHash.insert(id.id(), fd);
0218 
0219             /* For updated folders fetch the collection corresponding to that folder and its parent
0220              * (the parent will be needed in case of a collection move) */
0221             Collection c2;
0222             c2.setRemoteId(fd.parent());
0223             localFetchHash.insert(c2.remoteId(), c2);
0224 
0225             c.setRemoteId(id.id());
0226             localFetchHash.insert(c.remoteId(), c);
0227             break;
0228         }
0229         case EwsSyncFolderHierarchyRequest::Create: {
0230             fd.ewsFolder = ch.folder();
0231             fd.flags |= FolderDescr::RemoteCreated;
0232             auto id = fd.ewsFolder[EwsFolderFieldFolderId].value<EwsId>();
0233             mFolderHash.insert(id.id(), fd);
0234 
0235             c.setRemoteId(fd.parent());
0236             /* For created folders fetch the parent collection on Exchange side. Don't do this
0237              * when the parent collection has also been created as it would fail. */
0238             if (!mFolderHash.value(fd.parent()).isCreated()) {
0239                 localFetchHash.insert(c.remoteId(), c);
0240             }
0241             break;
0242         }
0243         case EwsSyncFolderHierarchyRequest::Delete:
0244             fd.flags |= FolderDescr::RemoteDeleted;
0245             mFolderHash.insert(ch.folderId().id(), fd);
0246 
0247             /* For deleted folders fetch the collection corresponding to the deleted folder. */
0248             c.setRemoteId(ch.folderId().id());
0249             localFetchHash.insert(c.remoteId(), c);
0250             break;
0251         default:
0252             break;
0253         }
0254     }
0255 
0256     if (localFetchHash.isEmpty()) {
0257         /* In either case at least one folder is expected to be queued for fetching. */
0258         q->setErrorMsg(QStringLiteral("Expected at least one local folder to fetch."));
0259         q->emitResult();
0260         return;
0261     }
0262 
0263     q->mSyncState = req->syncState();
0264 
0265     auto fetchJob = new CollectionFetchJob(localFetchHash.values().toVector(), CollectionFetchJob::Base);
0266     CollectionFetchScope scope;
0267     scope.setAncestorRetrieval(CollectionFetchScope::All);
0268     fetchJob->setFetchScope(scope);
0269     connect(fetchJob, &CollectionFetchJob::result, this, &EwsFetchFoldersIncrJobPrivate::localFolderFetchDone);
0270     q->addSubjob(fetchJob);
0271 }
0272 
0273 void EwsFetchFoldersIncrJobPrivate::localFolderFetchDone(KJob *job)
0274 {
0275     Q_Q(EwsFetchFoldersIncrJob);
0276 
0277     if (job->error()) {
0278         q->setErrorMsg(QStringLiteral("Failed to fetch local collections."));
0279         q->emitResult();
0280         return;
0281     }
0282 
0283     auto fetchJob = qobject_cast<CollectionFetchJob *>(job);
0284     Q_ASSERT(fetchJob);
0285 
0286     const auto collections{fetchJob->collections()};
0287     for (const Collection &col : collections) {
0288         /* Retrieve the folder descriptor for this collection. Note that a new descriptor will be
0289          * created if it does not yet exist. */
0290         FolderDescr &fd = mFolderHash[col.remoteId()];
0291         fd.collection = col;
0292         if (!fd.flags) {
0293             /* This collection has just been created and this means that it's a parent collection
0294              * added in response to a created folder. Since the collection is here just for reference
0295              * it will not be processed by processRemoteFolders() and can be marked accordingly. */
0296             fd.flags |= FolderDescr::Processed;
0297         }
0298     }
0299 
0300     if (!processRemoteFolders()) {
0301         q->setErrorMsg(QStringLiteral("Failed to process remote folder list."));
0302         q->emitResult();
0303     }
0304 
0305     if (!mPendingMoveJobs) {
0306         q->emitResult();
0307     }
0308     /* Otherwise wait for the move requests to finish. */
0309 }
0310 
0311 bool EwsFetchFoldersIncrJobPrivate::processRemoteFolders()
0312 {
0313     Q_Q(EwsFetchFoldersIncrJob);
0314 
0315     /* The list of top-level collections. It contains identifiers of collections for which the
0316      * parent collection is not in the folder hash. This list is used at a later stage when
0317      * setting collections parents. Building a top-level list is necessary as those updates can
0318      * only be safely performed down the tree. */
0319     QStringList topLevelList;
0320 
0321     bool reparentPassNeeded = false;
0322 
0323     /* Iterate over all changed folders. */
0324     for (auto it = mFolderHash.begin(), end = mFolderHash.end(); it != end; ++it) {
0325         qCDebugNC(EWSRES_LOG) << QStringLiteral("Processing: ") << it.key();
0326 
0327         if (it->isModified()) {
0328             qCDebugNC(EWSRES_LOG) << QStringLiteral("Collection was modified");
0329             updateFolderCollection(it->collection, it->ewsFolder);
0330 
0331             if (it->parent() != it->collection.parentCollection().remoteId()) {
0332                 /* This collection has been moved. Since Akonadi currently cannot handle collection
0333                  * moves the resource needs to manually move it. */
0334                 qCDebugNC(EWSRES_LOG) << QStringLiteral("Collection was moved");
0335 
0336                 /* Before moving check if the parent exists and has been processed. */
0337                 auto parentIt = mFolderHash.find(it->parent());
0338                 if (parentIt == mFolderHash.end()) {
0339                     q->setErrorMsg(QStringLiteral("Found moved collection without new parent."));
0340                     return false;
0341                 }
0342 
0343                 if (parentIt->isCreated()) {
0344                     /* Further workarounds could be done here to ensure that the parent is manually
0345                      * created before triggering a move but this would just unnecessarily complicate
0346                      * matters. Instead just surrender and retry with a full sync. */
0347                     q->setErrorMsg(QStringLiteral("Found moved collection to a just created parent."));
0348                     return false;
0349                 }
0350 
0351                 if (!parentIt->isProcessed()) {
0352                     qCDebugNC(EWSRES_LOG) << QStringLiteral("Parent not yet processed - delaying");
0353                     /* The new parent collection is not yet processed - defer the move to make
0354                      * sure all the operations are done in down-the-tree order. */
0355                     reparentPassNeeded = true;
0356                 } else {
0357                     moveCollection(*it);
0358                     it->collection.setParentCollection(parentIt->collection);
0359                     it->flags |= FolderDescr::Processed;
0360                 }
0361             } else {
0362                 /* No collection move happening so nothing else to for this one. */
0363                 it->flags |= FolderDescr::Processed;
0364             }
0365         } else if (it->isCreated()) {
0366             qCDebugNC(EWSRES_LOG) << QStringLiteral("Collection was created");
0367             it->collection.setRemoteId(it.key());
0368             updateFolderCollection(it->collection, it->ewsFolder);
0369 
0370             auto parentIt = mFolderHash.find(it->parent());
0371             if (parentIt == mFolderHash.end()) {
0372                 q->setErrorMsg(QStringLiteral("Found created collection without parent."));
0373                 return false;
0374             }
0375 
0376             /* Check if the parent has already been processed. If yes, set the parent of this
0377              * collection and mark this one as done. Otherwise a second pass will be needed later. */
0378             if (parentIt->isProcessed()) {
0379                 qCDebugNC(EWSRES_LOG) << QStringLiteral("Processing");
0380                 it->collection.setParentCollection(parentIt->collection);
0381                 it->flags |= FolderDescr::Processed;
0382             } else {
0383                 qCDebugNC(EWSRES_LOG) << QStringLiteral("Parent not yet processed - delaying");
0384                 reparentPassNeeded = true;
0385             }
0386         } else {
0387             qCDebugNC(EWSRES_LOG) << QStringLiteral("Collection is not remotely changed");
0388             /* This is either a deleted folder or a parent to an added collection. No processing
0389              * needed for either of those. */
0390             it->flags |= FolderDescr::Processed;
0391         }
0392 
0393         /* Check if this collection is a top-level collection. */
0394         if (!mFolderHash.contains(it->parent())) {
0395             qCDebugNC(EWSRES_LOG) << QStringLiteral("Collection is top level");
0396             topLevelList.append(it.key());
0397         }
0398 
0399         /* Put the collection into the parent map. This will help running the reparent pass. */
0400         if (!it->parent().isNull()) {
0401             mParentMap.insert(it->parent(), it.key());
0402         }
0403     }
0404 
0405     if (reparentPassNeeded) {
0406         qCDebugNC(EWSRES_LOG) << QStringLiteral("Executing reparent pass") << topLevelList;
0407         for (const QString &id : std::as_const(topLevelList)) {
0408             reparentRemoteFolder(id);
0409         }
0410     }
0411 
0412     /* Build the resulting collection list. */
0413     for (auto it = mFolderHash.cbegin(), end = mFolderHash.cend(); it != end; ++it) {
0414         if (it->isRemoved()) {
0415             q->mDeletedFolders.append(it->collection);
0416         } else if (it->isProcessed()) {
0417             q->mChangedFolders.append(it->collection);
0418         } else {
0419             qCWarningNC(EWSRES_LOG) << QStringLiteral("Found unprocessed collection %1").arg(it.key());
0420             return false;
0421         }
0422     }
0423 
0424     return true;
0425 }
0426 
0427 void EwsFetchFoldersIncrJobPrivate::reparentRemoteFolder(const QString &id)
0428 {
0429     qCDebugNC(EWSRES_LOG) << QStringLiteral("Reparenting") << id;
0430     const QStringList children = mParentMap.values(id);
0431     FolderDescr &fd = mFolderHash[id];
0432     for (const QString &childId : children) {
0433         FolderDescr &childFd = mFolderHash[childId];
0434         if (!childFd.isProcessed() && childFd.isModified() && childFd.parent() != childFd.collection.parentCollection().remoteId()) {
0435             qCDebugNC(EWSRES_LOG) << QStringLiteral("Found moved collection");
0436             /* Found unprocessed collection move. */
0437             moveCollection(childFd);
0438         }
0439 
0440         childFd.collection.setParentCollection(fd.collection);
0441         reparentRemoteFolder(childId);
0442     }
0443     fd.flags |= FolderDescr::Processed;
0444 }
0445 
0446 void EwsFetchFoldersIncrJobPrivate::moveCollection(const FolderDescr &fd)
0447 {
0448     qCDebugNC(EWSRES_LOG) << QStringLiteral("Moving collection") << fd.collection.remoteId() << QStringLiteral("from")
0449                           << fd.collection.parentCollection().remoteId() << QStringLiteral("to") << fd.parent();
0450     auto job = new CollectionMoveJob(fd.collection, mFolderHash[fd.parent()].collection);
0451     connect(job, &CollectionMoveJob::result, this, &EwsFetchFoldersIncrJobPrivate::localFolderMoveDone);
0452     mPendingMoveJobs++;
0453     job->start();
0454 }
0455 
0456 void EwsFetchFoldersIncrJobPrivate::localFolderMoveDone(KJob *job)
0457 {
0458     Q_Q(EwsFetchFoldersIncrJob);
0459 
0460     if (job->error()) {
0461         q->setErrorMsg(QStringLiteral("Failed to move collection."));
0462         q->emitResult();
0463         return;
0464     }
0465 
0466     if (--mPendingMoveJobs == 0) {
0467         q->emitResult();
0468     }
0469 }
0470 
0471 void EwsFetchFoldersIncrJobPrivate::updateFolderCollection(Collection &collection, const EwsFolder &folder)
0472 {
0473     collection.setName(folder[EwsFolderFieldDisplayName].toString());
0474     QStringList mimeTypes;
0475     QString contClass = folder[propPidTagContainerClass].toString();
0476     mimeTypes.append(Collection::mimeType());
0477     switch (folder.type()) {
0478     case EwsFolderTypeCalendar:
0479         mimeTypes.append(KCalendarCore::Event::eventMimeType());
0480         break;
0481     case EwsFolderTypeContacts:
0482         mimeTypes.append(KContacts::Addressee::mimeType());
0483         mimeTypes.append(KContacts::ContactGroup::mimeType());
0484         break;
0485     case EwsFolderTypeTasks:
0486         mimeTypes.append(KCalendarCore::Todo::todoMimeType());
0487         break;
0488     case EwsFolderTypeMail:
0489         if (contClass == QLatin1StringView("IPF.Note") || contClass.isEmpty()) {
0490             mimeTypes.append(KMime::Message::mimeType());
0491         }
0492         break;
0493     default:
0494         break;
0495     }
0496     collection.setContentMimeTypes(mimeTypes);
0497     Collection::Rights colRights;
0498     auto ewsRights = folder[EwsFolderFieldEffectiveRights].value<EwsEffectiveRights>();
0499     // FIXME: For now full read/write support is only implemented for e-mail. In order to avoid
0500     // potential problems block write access to all other folder types.
0501     if (folder.type() == EwsFolderTypeMail) {
0502         if (ewsRights.canDelete()) {
0503             colRights |= Collection::CanDeleteCollection | Collection::CanDeleteItem;
0504         }
0505         if (ewsRights.canModify()) {
0506             colRights |= Collection::CanChangeCollection | Collection::CanChangeItem;
0507         }
0508         if (ewsRights.canCreateContents()) {
0509             colRights |= Collection::CanCreateItem;
0510         }
0511         if (ewsRights.canCreateHierarchy()) {
0512             colRights |= Collection::CanCreateCollection;
0513         }
0514     }
0515     collection.setRights(colRights);
0516     auto id = folder[EwsFolderFieldFolderId].value<EwsId>();
0517     collection.setRemoteRevision(id.changeKey());
0518 }
0519 
0520 EwsFetchFoldersIncrJob::EwsFetchFoldersIncrJob(EwsClient &client, const QString &syncState, const Akonadi::Collection &rootCollection, QObject *parent)
0521     : EwsJob(parent)
0522     , mSyncState(syncState)
0523     , d_ptr(new EwsFetchFoldersIncrJobPrivate(this, client, rootCollection))
0524 {
0525     qRegisterMetaType<EwsId::List>();
0526 }
0527 
0528 EwsFetchFoldersIncrJob::~EwsFetchFoldersIncrJob() = default;
0529 
0530 void EwsFetchFoldersIncrJob::start()
0531 {
0532     Q_D(const EwsFetchFoldersIncrJob);
0533 
0534     auto syncFoldersReq = new EwsSyncFolderHierarchyRequest(d->mClient, this);
0535     syncFoldersReq->setFolderId(EwsId(EwsDIdMsgFolderRoot));
0536     EwsFolderShape shape;
0537     shape << propPidTagContainerClass;
0538     shape << EwsPropertyField(QStringLiteral("folder:EffectiveRights"));
0539     shape << EwsPropertyField(QStringLiteral("folder:ParentFolderId"));
0540     syncFoldersReq->setFolderShape(shape);
0541     if (!mSyncState.isNull()) {
0542         syncFoldersReq->setSyncState(mSyncState);
0543     }
0544     connect(syncFoldersReq, &EwsSyncFolderHierarchyRequest::result, d, &EwsFetchFoldersIncrJobPrivate::remoteFolderIncrFetchDone);
0545     addSubjob(syncFoldersReq);
0546 
0547     syncFoldersReq->start();
0548 }
0549 
0550 QDebug operator<<(QDebug debug, const FolderDescr &fd)
0551 {
0552     QDebugStateSaver saver(debug);
0553     QDebug d = debug.nospace().noquote();
0554     d << QStringLiteral("FolderDescr(");
0555 
0556     d << fd.collection;
0557     d << fd.flags;
0558 
0559     d << ')';
0560     return debug;
0561 }
0562 
0563 #include "moc_ewsfetchfoldersincrjob.cpp"