1 /*
2     SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
3     SPDX-FileContributor: Kevin Ottens <kevin@kdab.com>
4 
5     SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "resourcetask.h"
9 
10 #include <Akonadi/KMime/MessageFlags>
11 
12 #include "imapresource_debug.h"
13 #include "imapresource_trace.h"
14 #include <KLocalizedString>
15 
16 #include "collectionflagsattribute.h"
17 #include "imapflags.h"
18 #include "sessionpool.h"
19 #include <imapaclattribute.h>
20 
ResourceTask(ActionIfNoSession action,ResourceStateInterface::Ptr resource,QObject * parent)21 ResourceTask::ResourceTask(ActionIfNoSession action, ResourceStateInterface::Ptr resource, QObject *parent)
22     : QObject(parent)
23     , m_pool(nullptr)
24     , m_sessionRequestId(0)
25     , m_session(nullptr)
26     , m_actionIfNoSession(action)
27     , m_resource(resource)
28     , mCancelled(false)
29 {
30 }
31 
~ResourceTask()32 ResourceTask::~ResourceTask()
33 {
34     if (m_pool) {
35         if (m_sessionRequestId) {
36             m_pool->cancelSessionRequest(m_sessionRequestId);
37         }
38         if (m_session) {
39             m_pool->releaseSession(m_session);
40         }
41     }
42 }
43 
start(SessionPool * pool)44 void ResourceTask::start(SessionPool *pool)
45 {
46     qCDebug(IMAPRESOURCE_TRACE) << metaObject()->className();
47     m_pool = pool;
48     connect(m_pool, &SessionPool::sessionRequestDone, this, &ResourceTask::onSessionRequested);
49 
50     m_sessionRequestId = m_pool->requestSession();
51 
52     if (m_sessionRequestId <= 0) {
53         m_sessionRequestId = 0;
54 
55         abortTask(QString());
56         // In this case we were likely disconnected, try to get the resource online
57         m_resource->scheduleConnectionAttempt();
58     }
59 }
60 
abortTask(const QString & errorString)61 void ResourceTask::abortTask(const QString &errorString)
62 {
63     if (!mCancelled) {
64         mCancelled = true;
65 
66         switch (m_actionIfNoSession) {
67         case CancelIfNoSession:
68             qCDebug(IMAPRESOURCE_LOG) << "Cancelling this request.";
69             m_resource->cancelTask(errorString.isEmpty() ? i18n("Unable to connect to the IMAP server.") : errorString);
70             break;
71 
72         case DeferIfNoSession:
73             qCDebug(IMAPRESOURCE_LOG) << "Deferring this request.";
74             m_resource->deferTask();
75             break;
76         }
77     }
78     deleteLater();
79 }
80 
onSessionRequested(qint64 requestId,KIMAP::Session * session,int errorCode,const QString & errorString)81 void ResourceTask::onSessionRequested(qint64 requestId, KIMAP::Session *session, int errorCode, const QString &errorString)
82 {
83     if (requestId != m_sessionRequestId) {
84         // Not for us, ignore
85         return;
86     }
87 
88     disconnect(m_pool, &SessionPool::sessionRequestDone, this, &ResourceTask::onSessionRequested);
89     m_sessionRequestId = 0;
90 
91     if (errorCode != SessionPool::NoError) {
92         abortTask(errorString);
93         return;
94     }
95 
96     m_session = session;
97 
98     connect(m_pool, &SessionPool::connectionLost, this, &ResourceTask::onConnectionLost);
99     connect(m_pool, &SessionPool::disconnectDone, this, &ResourceTask::onPoolDisconnect);
100 
101     qCDebug(IMAPRESOURCE_TRACE) << "starting: " << metaObject()->className();
102     doStart(m_session);
103 }
104 
onConnectionLost(KIMAP::Session * session)105 void ResourceTask::onConnectionLost(KIMAP::Session *session)
106 {
107     if (session == m_session) {
108         // Our session becomes invalid, so get rid of
109         // the pointer, we don't need to release it once the
110         // task is done
111         m_session = nullptr;
112         qCDebug(IMAPRESOURCE_TRACE) << metaObject()->className();
113         abortTask(i18n("Connection lost"));
114     }
115 }
116 
onPoolDisconnect()117 void ResourceTask::onPoolDisconnect()
118 {
119     // All the sessions in the pool we used changed,
120     // so get rid of the pointer, we don't need to
121     // release our session anymore
122     m_pool = nullptr;
123 
124     qCDebug(IMAPRESOURCE_TRACE) << metaObject()->className();
125     abortTask(i18n("Connection lost"));
126 }
127 
userName() const128 QString ResourceTask::userName() const
129 {
130     return m_resource->userName();
131 }
132 
resourceName() const133 QString ResourceTask::resourceName() const
134 {
135     return m_resource->resourceName();
136 }
137 
serverCapabilities() const138 QStringList ResourceTask::serverCapabilities() const
139 {
140     return m_resource->serverCapabilities();
141 }
142 
serverNamespaces() const143 QList<KIMAP::MailBoxDescriptor> ResourceTask::serverNamespaces() const
144 {
145     return m_resource->serverNamespaces();
146 }
147 
isAutomaticExpungeEnabled() const148 bool ResourceTask::isAutomaticExpungeEnabled() const
149 {
150     return m_resource->isAutomaticExpungeEnabled();
151 }
152 
isSubscriptionEnabled() const153 bool ResourceTask::isSubscriptionEnabled() const
154 {
155     return m_resource->isSubscriptionEnabled();
156 }
157 
isDisconnectedModeEnabled() const158 bool ResourceTask::isDisconnectedModeEnabled() const
159 {
160     return m_resource->isDisconnectedModeEnabled();
161 }
162 
intervalCheckTime() const163 int ResourceTask::intervalCheckTime() const
164 {
165     return m_resource->intervalCheckTime();
166 }
167 
detachCollection(const Akonadi::Collection & collection)168 static Akonadi::Collection detachCollection(const Akonadi::Collection &collection)
169 {
170     // HACK: Attributes are accessed via a const function, and the implicitly shared private pointer thus doesn't detach.
171     // We force a detach to avoid surprises. (RetrieveItemsTask used to write back the collection changes, even though the task was canceled)
172     // Once this is fixed this function can go away.
173     Akonadi::Collection col = collection;
174     col.setId(col.id());
175     return col;
176 }
177 
collection() const178 Akonadi::Collection ResourceTask::collection() const
179 {
180     return detachCollection(m_resource->collection());
181 }
182 
item() const183 Akonadi::Item ResourceTask::item() const
184 {
185     return m_resource->item();
186 }
187 
items() const188 Akonadi::Item::List ResourceTask::items() const
189 {
190     return m_resource->items();
191 }
192 
parentCollection() const193 Akonadi::Collection ResourceTask::parentCollection() const
194 {
195     return detachCollection(m_resource->parentCollection());
196 }
197 
sourceCollection() const198 Akonadi::Collection ResourceTask::sourceCollection() const
199 {
200     return detachCollection(m_resource->sourceCollection());
201 }
202 
targetCollection() const203 Akonadi::Collection ResourceTask::targetCollection() const
204 {
205     return detachCollection(m_resource->targetCollection());
206 }
207 
parts() const208 QSet<QByteArray> ResourceTask::parts() const
209 {
210     return m_resource->parts();
211 }
212 
addedFlags() const213 QSet<QByteArray> ResourceTask::addedFlags() const
214 {
215     return m_resource->addedFlags();
216 }
217 
removedFlags() const218 QSet<QByteArray> ResourceTask::removedFlags() const
219 {
220     return m_resource->removedFlags();
221 }
222 
rootRemoteId() const223 QString ResourceTask::rootRemoteId() const
224 {
225     return m_resource->rootRemoteId();
226 }
227 
mailBoxForCollection(const Akonadi::Collection & collection) const228 QString ResourceTask::mailBoxForCollection(const Akonadi::Collection &collection) const
229 {
230     return m_resource->mailBoxForCollection(collection);
231 }
232 
setIdleCollection(const Akonadi::Collection & collection)233 void ResourceTask::setIdleCollection(const Akonadi::Collection &collection)
234 {
235     if (!mCancelled) {
236         m_resource->setIdleCollection(collection);
237     }
238 }
239 
applyCollectionChanges(const Akonadi::Collection & collection)240 void ResourceTask::applyCollectionChanges(const Akonadi::Collection &collection)
241 {
242     if (!mCancelled) {
243         m_resource->applyCollectionChanges(collection);
244     }
245 }
246 
itemRetrieved(const Akonadi::Item & item)247 void ResourceTask::itemRetrieved(const Akonadi::Item &item)
248 {
249     if (!mCancelled) {
250         m_resource->itemRetrieved(item);
251         emitPercent(100);
252     }
253     deleteLater();
254 }
255 
itemsRetrieved(const Akonadi::Item::List & items)256 void ResourceTask::itemsRetrieved(const Akonadi::Item::List &items)
257 {
258     if (!mCancelled) {
259         m_resource->itemsRetrieved(items);
260     }
261 }
262 
itemsRetrievedIncremental(const Akonadi::Item::List & changed,const Akonadi::Item::List & removed)263 void ResourceTask::itemsRetrievedIncremental(const Akonadi::Item::List &changed, const Akonadi::Item::List &removed)
264 {
265     if (!mCancelled) {
266         m_resource->itemsRetrievedIncremental(changed, removed);
267     }
268 }
269 
itemsRetrievalDone()270 void ResourceTask::itemsRetrievalDone()
271 {
272     if (!mCancelled) {
273         m_resource->itemsRetrievalDone();
274     }
275     deleteLater();
276 }
277 
setTotalItems(int totalItems)278 void ResourceTask::setTotalItems(int totalItems)
279 {
280     if (!mCancelled) {
281         m_resource->setTotalItems(totalItems);
282     }
283 }
284 
changeCommitted(const Akonadi::Item & item)285 void ResourceTask::changeCommitted(const Akonadi::Item &item)
286 {
287     if (!mCancelled) {
288         m_resource->itemChangeCommitted(item);
289     }
290     deleteLater();
291 }
292 
changesCommitted(const Akonadi::Item::List & items)293 void ResourceTask::changesCommitted(const Akonadi::Item::List &items)
294 {
295     if (!mCancelled) {
296         m_resource->itemsChangesCommitted(items);
297     }
298     deleteLater();
299 }
300 
searchFinished(const QVector<qint64> & result,bool isRid)301 void ResourceTask::searchFinished(const QVector<qint64> &result, bool isRid)
302 {
303     if (!mCancelled) {
304         m_resource->searchFinished(result, isRid);
305     }
306     deleteLater();
307 }
308 
collectionsRetrieved(const Akonadi::Collection::List & collections)309 void ResourceTask::collectionsRetrieved(const Akonadi::Collection::List &collections)
310 {
311     if (!mCancelled) {
312         m_resource->collectionsRetrieved(collections);
313     }
314     deleteLater();
315 }
316 
collectionAttributesRetrieved(const Akonadi::Collection & col)317 void ResourceTask::collectionAttributesRetrieved(const Akonadi::Collection &col)
318 {
319     if (!mCancelled) {
320         m_resource->collectionAttributesRetrieved(col);
321     }
322     deleteLater();
323 }
324 
changeCommitted(const Akonadi::Collection & collection)325 void ResourceTask::changeCommitted(const Akonadi::Collection &collection)
326 {
327     if (!mCancelled) {
328         m_resource->collectionChangeCommitted(collection);
329     }
330     deleteLater();
331 }
332 
changeCommitted(const Akonadi::Tag & tag)333 void ResourceTask::changeCommitted(const Akonadi::Tag &tag)
334 {
335     if (!mCancelled) {
336         m_resource->tagChangeCommitted(tag);
337     }
338     deleteLater();
339 }
340 
changeProcessed()341 void ResourceTask::changeProcessed()
342 {
343     if (!mCancelled) {
344         m_resource->changeProcessed();
345     }
346     deleteLater();
347 }
348 
cancelTask(const QString & errorString)349 void ResourceTask::cancelTask(const QString &errorString)
350 {
351     qCDebug(IMAPRESOURCE_LOG) << "Cancel task: " << errorString;
352     if (!mCancelled) {
353         mCancelled = true;
354         m_resource->cancelTask(errorString);
355     }
356     deleteLater();
357 }
358 
deferTask()359 void ResourceTask::deferTask()
360 {
361     if (!mCancelled) {
362         mCancelled = true;
363         m_resource->deferTask();
364     }
365     deleteLater();
366 }
367 
restartItemRetrieval(Akonadi::Collection::Id col)368 void ResourceTask::restartItemRetrieval(Akonadi::Collection::Id col)
369 {
370     if (!mCancelled) {
371         m_resource->restartItemRetrieval(col);
372     }
373     deleteLater();
374 }
375 
taskDone()376 void ResourceTask::taskDone()
377 {
378     m_resource->taskDone();
379     deleteLater();
380 }
381 
emitPercent(int percent)382 void ResourceTask::emitPercent(int percent)
383 {
384     m_resource->emitPercent(percent);
385 }
386 
emitError(const QString & message)387 void ResourceTask::emitError(const QString &message)
388 {
389     m_resource->emitError(message);
390 }
391 
emitWarning(const QString & message)392 void ResourceTask::emitWarning(const QString &message)
393 {
394     m_resource->emitWarning(message);
395 }
396 
synchronizeCollectionTree()397 void ResourceTask::synchronizeCollectionTree()
398 {
399     m_resource->synchronizeCollectionTree();
400 }
401 
showInformationDialog(const QString & message,const QString & title,const QString & dontShowAgainName)402 void ResourceTask::showInformationDialog(const QString &message, const QString &title, const QString &dontShowAgainName)
403 {
404     m_resource->showInformationDialog(message, title, dontShowAgainName);
405 }
406 
fromAkonadiToSupportedImapFlags(const QList<QByteArray> & flags,const Akonadi::Collection & collection)407 QList<QByteArray> ResourceTask::fromAkonadiToSupportedImapFlags(const QList<QByteArray> &flags, const Akonadi::Collection &collection)
408 {
409     QList<QByteArray> imapFlags = fromAkonadiFlags(flags);
410 
411     const auto flagAttr = collection.attribute<Akonadi::CollectionFlagsAttribute>();
412     // the server does not support arbitrary flags, so filter out those it can't handle
413     if (flagAttr && !flagAttr->flags().isEmpty() && !flagAttr->flags().contains("\\*")) {
414         for (QList<QByteArray>::iterator it = imapFlags.begin(); it != imapFlags.end();) {
415             if (flagAttr->flags().contains(*it)) {
416                 ++it;
417             } else {
418                 qCDebug(IMAPRESOURCE_LOG) << "Server does not support flag" << *it;
419                 it = imapFlags.erase(it);
420             }
421         }
422     }
423 
424     return imapFlags;
425 }
426 
fromAkonadiFlags(const QList<QByteArray> & flags)427 QList<QByteArray> ResourceTask::fromAkonadiFlags(const QList<QByteArray> &flags)
428 {
429     QList<QByteArray> newFlags;
430 
431     for (const QByteArray &oldFlag : flags) {
432         if (oldFlag == Akonadi::MessageFlags::Seen) {
433             newFlags.append(ImapFlags::Seen);
434         } else if (oldFlag == Akonadi::MessageFlags::Deleted) {
435             newFlags.append(ImapFlags::Deleted);
436         } else if (oldFlag == Akonadi::MessageFlags::Answered || oldFlag == Akonadi::MessageFlags::Replied) {
437             newFlags.append(ImapFlags::Answered);
438         } else if (oldFlag == Akonadi::MessageFlags::Flagged) {
439             newFlags.append(ImapFlags::Flagged);
440         } else {
441             newFlags.append(oldFlag);
442         }
443     }
444 
445     return newFlags;
446 }
447 
toAkonadiFlags(const QList<QByteArray> & flags)448 QSet<QByteArray> ResourceTask::toAkonadiFlags(const QList<QByteArray> &flags)
449 {
450     QSet<QByteArray> newFlags;
451 
452     for (const QByteArray &oldFlag : flags) {
453         if (oldFlag == ImapFlags::Seen) {
454             newFlags.insert(Akonadi::MessageFlags::Seen);
455         } else if (oldFlag == ImapFlags::Deleted) {
456             newFlags.insert(Akonadi::MessageFlags::Deleted);
457         } else if (oldFlag == ImapFlags::Answered) {
458             newFlags.insert(Akonadi::MessageFlags::Answered);
459         } else if (oldFlag == ImapFlags::Flagged) {
460             newFlags.insert(Akonadi::MessageFlags::Flagged);
461         } else if (oldFlag.isEmpty()) {
462             // filter out empty flags, to avoid isNull/isEmpty confusions higher up
463             continue;
464         } else {
465             newFlags.insert(oldFlag);
466         }
467     }
468 
469     return newFlags;
470 }
471 
kill()472 void ResourceTask::kill()
473 {
474     qCDebug(IMAPRESOURCE_TRACE) << metaObject()->className();
475     abortTask(i18n("killed"));
476 }
477 
separatorCharacter() const478 const QChar ResourceTask::separatorCharacter() const
479 {
480     const QChar separator = m_resource->separatorCharacter();
481     if (!separator.isNull()) {
482         return separator;
483     } else {
484         // If we request the separator before first folder listing, then try to guess
485         // the separator:
486         // If we create a toplevel folder, assume the separator to be '/'. This is not perfect, but detecting the right
487         // IMAP separator is not straightforward for toplevel folders, and fixes bug 292418 and maybe other, where
488         // subfolders end up with remote id's starting with "i" (the first letter of imap:// ...)
489 
490         QString remoteId;
491         // We don't always have parent collection set (for example for CollectionChangeTask),
492         // in such cases however we can use current collection's remoteId to get the separator
493         const Akonadi::Collection parent = parentCollection();
494         if (parent.isValid()) {
495             remoteId = parent.remoteId();
496         } else {
497             remoteId = collection().remoteId();
498         }
499         return ((remoteId != rootRemoteId()) && !remoteId.isEmpty()) ? remoteId.at(0) : QLatin1Char('/');
500     }
501 }
502 
setSeparatorCharacter(QChar separator)503 void ResourceTask::setSeparatorCharacter(QChar separator)
504 {
505     m_resource->setSeparatorCharacter(separator);
506 }
507 
serverSupportsAnnotations() const508 bool ResourceTask::serverSupportsAnnotations() const
509 {
510     return serverCapabilities().contains(QLatin1String("METADATA")) || serverCapabilities().contains(QLatin1String("ANNOTATEMORE"));
511 }
512 
serverSupportsCondstore() const513 bool ResourceTask::serverSupportsCondstore() const
514 {
515     // Don't enable CONDSTORE for GMail (X-GM-EXT-1 is a GMail-specific capability)
516     // because it breaks changes synchronization when using labels.
517     return serverCapabilities().contains(QLatin1String("CONDSTORE")) && !serverCapabilities().contains(QLatin1String("X-GM-EXT-1"));
518 }
519 
batchSize() const520 int ResourceTask::batchSize() const
521 {
522     return m_resource->batchSize();
523 }
524 
resourceState()525 ResourceStateInterface::Ptr ResourceTask::resourceState()
526 {
527     return m_resource;
528 }
529 
myRights(const Akonadi::Collection & col)530 KIMAP::Acl::Rights ResourceTask::myRights(const Akonadi::Collection &col)
531 {
532     const auto aclAttribute = col.attribute<Akonadi::ImapAclAttribute>();
533     if (aclAttribute) {
534         // HACK, only return myrights if they are available
535         if (aclAttribute->myRights() != KIMAP::Acl::None) {
536             return aclAttribute->myRights();
537         } else {
538             // This should be removed after 4.14, and myrights should be always used.
539             return aclAttribute->rights().value(userName().toUtf8());
540         }
541     }
542     return KIMAP::Acl::None;
543 }
544 
setItemMergingMode(Akonadi::ItemSync::MergeMode mode)545 void ResourceTask::setItemMergingMode(Akonadi::ItemSync::MergeMode mode)
546 {
547     m_resource->setItemMergingMode(mode);
548 }
549