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"