1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
2 
3    This file is part of the Trojita Qt IMAP e-mail client,
4    http://trojita.flaska.net/
5 
6    This program is free software; you can redistribute it and/or
7    modify it under the terms of the GNU General Public License as
8    published by the Free Software Foundation; either version 2 of
9    the License or (at your option) version 3 or any later version
10    accepted by the membership of KDE e.V. (or its successor approved
11    by the membership of KDE e.V.), which shall act as a proxy
12    defined in Section 14 of version 3 of the license.
13 
14    This program is distributed in the hope that it will be useful,
15    but WITHOUT ANY WARRANTY; without even the implied warranty of
16    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17    GNU General Public License for more details.
18 
19    You should have received a copy of the GNU General Public License
20    along with this program.  If not, see <http://www.gnu.org/licenses/>.
21 */
22 
23 #include <sstream>
24 #include "KeepMailboxOpenTask.h"
25 #include "Common/InvokeMethod.h"
26 #include "Imap/Model/ItemRoles.h"
27 #include "Imap/Model/MailboxTree.h"
28 #include "Imap/Model/Model.h"
29 #include "Imap/Model/TaskFactory.h"
30 #include "Imap/Model/TaskPresentationModel.h"
31 #include "DeleteMailboxTask.h"
32 #include "FetchMsgMetadataTask.h"
33 #include "FetchMsgPartTask.h"
34 #include "IdleLauncher.h"
35 #include "OpenConnectionTask.h"
36 #include "ObtainSynchronizedMailboxTask.h"
37 #include "OfflineConnectionTask.h"
38 #include "SortTask.h"
39 #include "NoopTask.h"
40 #include "UnSelectTask.h"
41 
42 namespace Imap
43 {
44 namespace Mailbox
45 {
46 
KeepMailboxOpenTask(Model * model,const QModelIndex & mailboxIndex,Parser * oldParser)47 KeepMailboxOpenTask::KeepMailboxOpenTask(Model *model, const QModelIndex &mailboxIndex, Parser *oldParser) :
48     ImapTask(model), mailboxIndex(mailboxIndex), synchronizeConn(0), shouldExit(false), isRunning(Running::NOT_YET),
49     shouldRunNoop(false), shouldRunIdle(false), idleLauncher(0), unSelectTask(0),
50     m_skippedStateSynces(0), m_performedStateSynces(0), m_syncingTimer(nullptr)
51 {
52     Q_ASSERT(mailboxIndex.isValid());
53     Q_ASSERT(mailboxIndex.model() == model);
54     TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
55     Q_ASSERT(mailbox);
56 
57     // Now make sure that we at least try to load data from the cache
58     Q_ASSERT(mailbox->m_children.size() > 0);
59     TreeItemMsgList *list = dynamic_cast<TreeItemMsgList*>(mailbox->m_children[0]);
60     Q_ASSERT(list);
61     list->fetch(model);
62 
63     // We're the latest KeepMailboxOpenTask, so it makes a lot of sense to add us as the active
64     // maintainingTask to the target mailbox
65     mailbox->maintainingTask = this;
66 
67     if (oldParser) {
68         // We're asked to re-use an existing connection. Let's see if there's something associated with it
69 
70         // We will use its parser, that's for sure already
71         parser = oldParser;
72 
73         // Find if there's a KeepMailboxOpenTask already associated; if it is, we have to register with it
74         if (model->accessParser(parser).maintainingTask) {
75             // The parser looks busy -- some task is associated with it and has a mailbox open, so
76             // let's just wait till we get a chance to play
77             synchronizeConn = model->m_taskFactory->
78                               createObtainSynchronizedMailboxTask(model, mailboxIndex, model->accessParser(oldParser).maintainingTask, this);
79         } else if (model->accessParser(parser).connState < CONN_STATE_AUTHENTICATED) {
80             // The parser is still in the process of being initialized, let's wait until it's completed
81             Q_ASSERT(!model->accessParser(oldParser).activeTasks.isEmpty());
82             ImapTask *task = model->accessParser(oldParser).activeTasks.front();
83             synchronizeConn = model->m_taskFactory->createObtainSynchronizedMailboxTask(model, mailboxIndex, task, this);
84         } else {
85             // The parser is free, or at least there's no KeepMailboxOpenTask associated with it.
86             // There's no mailbox besides us in the game, yet, so we can simply schedule us for immediate execution.
87             synchronizeConn = model->m_taskFactory->createObtainSynchronizedMailboxTask(model, mailboxIndex, 0, this);
88             // We'll also register with the model, so that all other KeepMailboxOpenTask which could get constructed in future
89             // know about us and don't step on our toes.  This means that further KeepMailboxOpenTask which could possibly want
90             // to use this connection will have to go through this task at first.
91             model->accessParser(parser).maintainingTask = this;
92             QTimer::singleShot(0, this, SLOT(slotPerformConnection()));
93         }
94 
95         // We shall catch destruction of any preexisting tasks so that we can properly launch IDLE etc in response to their termination
96         Q_FOREACH(ImapTask *task, model->accessParser(parser).activeTasks) {
97             connect(task, &QObject::destroyed, this, &KeepMailboxOpenTask::slotTaskDeleted);
98         }
99     } else {
100         ImapTask *conn = 0;
101         if (model->networkPolicy() == NETWORK_OFFLINE) {
102             // Well, except that we cannot really open a new connection now
103             conn = new OfflineConnectionTask(model);
104         } else {
105             conn = model->m_taskFactory->createOpenConnectionTask(model);
106         }
107         parser = conn->parser;
108         Q_ASSERT(parser);
109         model->accessParser(parser).maintainingTask = this;
110         synchronizeConn = model->m_taskFactory->createObtainSynchronizedMailboxTask(model, mailboxIndex, conn, this);
111     }
112 
113     Q_ASSERT(synchronizeConn);
114 
115     // Setup the timer for NOOPing. It won't get started at this time, though.
116     noopTimer = new QTimer(this);
117     connect(noopTimer, &QTimer::timeout, this, &KeepMailboxOpenTask::slotPerformNoop);
118     bool ok;
119     int timeout = model->property("trojita-imap-noop-period").toUInt(&ok);
120     if (! ok)
121         timeout = 2 * 60 * 1000; // once every two minutes
122     noopTimer->setInterval(timeout);
123     noopTimer->setSingleShot(true);
124 
125     fetchPartTimer = new QTimer(this);
126     connect(fetchPartTimer, &QTimer::timeout, this, &KeepMailboxOpenTask::slotFetchRequestedParts);
127     timeout = model->property("trojita-imap-delayed-fetch-part").toUInt(&ok);
128     if (! ok)
129         timeout = 50;
130     fetchPartTimer->setInterval(timeout);
131     fetchPartTimer->setSingleShot(true);
132 
133     fetchEnvelopeTimer = new QTimer(this);
134     connect(fetchEnvelopeTimer, &QTimer::timeout, this, &KeepMailboxOpenTask::slotFetchRequestedEnvelopes);
135     fetchEnvelopeTimer->setInterval(0); // message metadata is pretty important, hence an immediate fetch
136     fetchEnvelopeTimer->setSingleShot(true);
137 
138     limitBytesAtOnce = model->property("trojita-imap-limit-fetch-bytes-per-group").toUInt(&ok);
139     if (! ok)
140         limitBytesAtOnce = 1024 * 1024;
141 
142     limitMessagesAtOnce = model->property("trojita-imap-limit-fetch-messages-per-group").toInt(&ok);
143     if (! ok)
144         limitMessagesAtOnce = 300;
145 
146     limitParallelFetchTasks = model->property("trojita-imap-limit-parallel-fetch-tasks").toInt(&ok);
147     if (! ok)
148         limitParallelFetchTasks = 10;
149 
150     limitActiveTasks = model->property("trojita-imap-limit-active-tasks").toInt(&ok);
151     if (! ok)
152         limitActiveTasks = 100;
153 
154     CHECK_TASK_TREE
155     emit model->mailboxSyncingProgress(mailboxIndex, STATE_WAIT_FOR_CONN);
156 
157     /** @short How often to reset the time window (in ms) for determining mass-change mode */
158     const int throttlingWindowLength = 1000;
159 
160     m_syncingTimer = new QTimer(this);
161     m_syncingTimer->setSingleShot(true);
162     // This timeout specifies how long we're going to wait for an incoming reply which usually triggers state syncing.
163     // If no such response arrives during this time window, the changes are saved on disk; if, however, something does
164     // arrive, the rate of saving is only moderated based on the number of reponses which were already received,
165     // but which did not result in state saving yet.
166     m_syncingTimer->setInterval(throttlingWindowLength);
167     connect(m_syncingTimer, &QTimer::timeout, this, &KeepMailboxOpenTask::syncingTimeout);
168 }
169 
slotPerformConnection()170 void KeepMailboxOpenTask::slotPerformConnection()
171 {
172     CHECK_TASK_TREE
173     Q_ASSERT(synchronizeConn);
174     Q_ASSERT(!synchronizeConn->isFinished());
175     if (_dead) {
176         _failed(tr("Asked to die"));
177         synchronizeConn->die(QStringLiteral("KeepMailboxOpenTask died before the sync started"));
178         return;
179     }
180 
181     connect(synchronizeConn, &QObject::destroyed, this, &KeepMailboxOpenTask::slotTaskDeleted);
182     synchronizeConn->perform();
183 }
184 
addDependentTask(ImapTask * task)185 void KeepMailboxOpenTask::addDependentTask(ImapTask *task)
186 {
187     CHECK_TASK_TREE
188     Q_ASSERT(task);
189 
190     // FIXME: what about abort()/die() here?
191 
192     breakOrCancelPossibleIdle();
193 
194     DeleteMailboxTask *deleteTask = qobject_cast<DeleteMailboxTask*>(task);
195     if (!deleteTask || deleteTask->mailbox != mailboxIndex.data(RoleMailboxName).toString()) {
196         deleteTask = 0;
197     }
198 
199     if (ObtainSynchronizedMailboxTask *obtainTask = qobject_cast<ObtainSynchronizedMailboxTask *>(task)) {
200         // Another KeepMailboxOpenTask would like to replace us, so we shall die, eventually.
201         // This branch is reimplemented from ImapTask
202 
203         dependentTasks.append(task);
204         waitingObtainTasks.append(obtainTask);
205         shouldExit = true;
206         task->updateParentTask(this);
207 
208         // Before we can die, though, we have to accommodate fetch requests for all envelopes and parts queued so far.
209         slotFetchRequestedEnvelopes();
210         slotFetchRequestedParts();
211 
212         if (! hasPendingInternalActions() && (! synchronizeConn || synchronizeConn->isFinished())) {
213             QTimer::singleShot(0, this, SLOT(terminate()));
214         }
215 
216         Q_FOREACH(ImapTask *abortable, abortableTasks) {
217             abortable->abort();
218         }
219     } else if (deleteTask) {
220         // Got a request to delete the current mailbox. Fair enough, here we go!
221 
222         if (hasPendingInternalActions() || (synchronizeConn && !synchronizeConn->isFinished())) {
223             // Hmm, this is bad -- the caller has instructed us to delete the current mailbox, but we still have
224             // some pending actions (or have not even started yet). Better reject the request for deleting than lose some data.
225             // Alternatively, we might pretend that we're a performance-oriented library and "optimize out" the
226             // data transfer by deleting early :)
227             deleteTask->mailboxHasPendingActions();
228             return;
229         }
230 
231         m_deleteCurrentMailboxTask = deleteTask;
232         shouldExit = true;
233         connect(task, &QObject::destroyed, this, &KeepMailboxOpenTask::slotTaskDeleted);
234         ImapTask::addDependentTask(task);
235         QTimer::singleShot(0, this, SLOT(slotActivateTasks()));
236 
237     } else {
238         // This branch calls the inherited ImapTask::addDependentTask()
239         connect(task, &QObject::destroyed, this, &KeepMailboxOpenTask::slotTaskDeleted);
240         ImapTask::addDependentTask(task);
241         if (task->needsMailbox()) {
242             // it's a task which is tied to a particular mailbox
243             dependingTasksForThisMailbox.append(task);
244         } else {
245             dependingTasksNoMailbox.append(task);
246         }
247         QTimer::singleShot(0, this, SLOT(slotActivateTasks()));
248     }
249 }
250 
slotTaskDeleted(QObject * object)251 void KeepMailboxOpenTask::slotTaskDeleted(QObject *object)
252 {
253     if (_finished)
254         return;
255 
256     if (!model) {
257         // we're very likely hitting this during some rather unclean destruction -> ignore this and die ASAP
258         // See https://bugs.kde.org/show_bug.cgi?id=336090
259         return;
260     }
261 
262     if (!model->m_parsers.contains(parser)) {
263         // The parser is gone; we have to get out of here ASAP
264         _failed(tr("Parser is gone"));
265         die(tr("Parser is gone"));
266         return;
267     }
268     // FIXME: abort/die
269 
270     // Now, object is no longer an ImapTask*, as this gets emitted from inside QObject's destructor. However,
271     // we can't use the passed pointer directly, and therefore we have to perform the cast here. It is safe
272     // to do that here, as we're only interested in raw pointer value.
273     if (object) {
274         dependentTasks.removeOne(static_cast<ImapTask *>(object));
275         dependingTasksForThisMailbox.removeOne(static_cast<ImapTask *>(object));
276         dependingTasksNoMailbox.removeOne(static_cast<ImapTask *>(object));
277         runningTasksForThisMailbox.removeOne(static_cast<ImapTask *>(object));
278         fetchPartTasks.removeOne(static_cast<FetchMsgPartTask *>(object));
279         fetchMetadataTasks.removeOne(static_cast<FetchMsgMetadataTask *>(object));
280         abortableTasks.removeOne(static_cast<FetchMsgMetadataTask *>(object));
281     }
282 
283     if (isReadyToTerminate()) {
284         terminate();
285     } else if (shouldRunNoop) {
286         // A command just completed, and NOOPing is active, so let's schedule/postpone it again
287         noopTimer->start();
288     } else if (canRunIdleRightNow()) {
289         // A command just completed and IDLE is supported, so let's queue/schedule/postpone it
290         idleLauncher->enterIdleLater();
291     }
292     // It's possible that we can start more tasks at this time...
293     activateTasks();
294 }
295 
terminate()296 void KeepMailboxOpenTask::terminate()
297 {
298     if (_aborted) {
299         // We've already been there, so we *cannot* proceed towards activating our replacement tasks
300         return;
301     }
302     abort();
303 
304     m_syncingTimer->stop();
305     syncingTimeout();
306 
307     // FIXME: abort/die
308 
309     Q_ASSERT(dependingTasksForThisMailbox.isEmpty());
310     Q_ASSERT(dependingTasksNoMailbox.isEmpty());
311     Q_ASSERT(requestedParts.isEmpty());
312     Q_ASSERT(requestedEnvelopes.isEmpty());
313     Q_ASSERT(runningTasksForThisMailbox.isEmpty());
314     Q_ASSERT(abortableTasks.isEmpty());
315     Q_ASSERT(!m_syncingTimer->isActive());
316     Q_ASSERT(m_skippedStateSynces == 0);
317 
318     // Break periodic activities
319     shouldRunIdle = false;
320     shouldRunNoop = false;
321     isRunning = Running::NOT_ANYMORE;
322 
323     // Merge the lists of waiting tasks
324     if (!waitingObtainTasks.isEmpty()) {
325         ObtainSynchronizedMailboxTask *first = waitingObtainTasks.takeFirst();
326         dependentTasks.removeOne(first);
327         Q_ASSERT(first);
328         Q_ASSERT(first->keepTaskChild);
329         Q_ASSERT(first->keepTaskChild->synchronizeConn == first);
330 
331         CHECK_TASK_TREE
332         // Update the parent information for the moved tasks
333         Q_FOREACH(ObtainSynchronizedMailboxTask *movedObtainTask, waitingObtainTasks) {
334             Q_ASSERT(movedObtainTask->parentTask);
335             movedObtainTask->parentTask->dependentTasks.removeOne(movedObtainTask);
336             movedObtainTask->parentTask = first->keepTaskChild;
337             first->keepTaskChild->dependentTasks.append(movedObtainTask);
338         }
339         CHECK_TASK_TREE
340 
341         // And launch the replacement
342         first->keepTaskChild->waitingObtainTasks = waitingObtainTasks + first->keepTaskChild->waitingObtainTasks;
343         model->accessParser(parser).maintainingTask = first->keepTaskChild;
344         // make sure that if the SELECT dies uncleanly, such as with a missing [CLOSED], we get killed as well
345         connect(first->keepTaskChild, &ImapTask::failed, this, &KeepMailboxOpenTask::finalizeTermination);
346         first->keepTaskChild->slotPerformConnection();
347     } else {
348         Q_ASSERT(dependentTasks.isEmpty());
349     }
350     if (model->accessParser(parser).connState == CONN_STATE_SELECTING_WAIT_FOR_CLOSE) {
351         // we have to be kept busy, otherwise the responses which are still destined for *this* mailbox would
352         // get delivered to the new one
353     } else {
354         finalizeTermination();
355     }
356     CHECK_TASK_TREE
357 }
358 
perform()359 void KeepMailboxOpenTask::perform()
360 {
361     // FIXME: abort/die
362 
363     Q_ASSERT(synchronizeConn);
364     Q_ASSERT(synchronizeConn->isFinished());
365     parser = synchronizeConn->parser;
366     synchronizeConn = 0; // will get deleted by Model
367     markAsActiveTask();
368 
369     isRunning = Running::RUNNING;
370     fetchPartTimer->start();
371     fetchEnvelopeTimer->start();
372 
373     if (!waitingObtainTasks.isEmpty()) {
374         shouldExit = true;
375     }
376 
377     activateTasks();
378 
379     if (model->accessParser(parser).capabilitiesFresh && model->accessParser(parser).capabilities.contains(QStringLiteral("IDLE"))) {
380         shouldRunIdle = true;
381     } else {
382         shouldRunNoop = true;
383     }
384 
385     if (shouldRunNoop) {
386         noopTimer->start();
387     } else if (shouldRunIdle) {
388         idleLauncher = new IdleLauncher(this);
389         if (canRunIdleRightNow()) {
390             // There's no task yet, so we have to start IDLE now
391             idleLauncher->enterIdleLater();
392         }
393     }
394 }
395 
resynchronizeMailbox()396 void KeepMailboxOpenTask::resynchronizeMailbox()
397 {
398     // FIXME: abort/die
399 
400     if (isRunning != Running::NOT_YET) {
401         // Instead of wild magic with re-creating synchronizeConn, it's way easier to
402         // just have us replaced by another KeepMailboxOpenTask
403         model->m_taskFactory->createKeepMailboxOpenTask(model, mailboxIndex, parser);
404     } else {
405         // We aren't running yet, which means that the sync hadn't happened yet, and therefore
406         // we don't have to do it "once again" -- it will happen automatically later on.
407     }
408 }
409 
410 #define CHECK_IS_RUNNING \
411     switch (isRunning) { \
412     case Running::NOT_YET: \
413         return false; \
414     case Running::NOT_ANYMORE: \
415         /* OK, a lost reply -- we're already switching to another mailbox, and even though this might seem */ \
416         /* to be a safe change, we just cannot react to this right now :(. */ \
417         /* Also, don't eat further replies once we're dead :) */ \
418         return model->accessParser(parser).connState == CONN_STATE_SELECTING_WAIT_FOR_CLOSE; \
419     case Running::RUNNING: \
420         /* normal state -> handle this */ \
421         break; \
422     }
423 
handleNumberResponse(const Imap::Responses::NumberResponse * const resp)424 bool KeepMailboxOpenTask::handleNumberResponse(const Imap::Responses::NumberResponse *const resp)
425 {
426     if (_dead) {
427         _failed(tr("Asked to die"));
428         return true;
429     }
430 
431     if (dieIfInvalidMailbox())
432         return true;
433 
434     CHECK_IS_RUNNING
435 
436     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
437     Q_ASSERT(mailbox);
438     TreeItemMsgList *list = dynamic_cast<TreeItemMsgList *>(mailbox->m_children[0]);
439     Q_ASSERT(list);
440     // FIXME: tests!
441     if (resp->kind == Imap::Responses::EXPUNGE) {
442         mailbox->handleExpunge(model, *resp);
443         mailbox->syncState.setExists(mailbox->syncState.exists() - 1);
444         saveSyncStateNowOrLater(mailbox);
445         return true;
446     } else if (resp->kind == Imap::Responses::EXISTS) {
447 
448         if (resp->number == static_cast<uint>(list->m_children.size())) {
449             // no changes
450             return true;
451         }
452 
453         mailbox->handleExists(model, *resp);
454 
455         breakOrCancelPossibleIdle();
456 
457         Q_ASSERT(list->m_children.size());
458         uint highestKnownUid = 0;
459         for (int i = list->m_children.size() - 1; ! highestKnownUid && i >= 0; --i) {
460             highestKnownUid = static_cast<const TreeItemMessage *>(list->m_children[i])->uid();
461             //qDebug() << "UID disco: trying seq" << i << highestKnownUid;
462         }
463         breakOrCancelPossibleIdle();
464         newArrivalsFetch.append(parser->uidFetch(Sequence::startingAt(
465                                                 // Did the UID walk return a usable number?
466                                                 highestKnownUid ?
467                                                 // Yes, we've got at least one message with a UID known -> ask for higher
468                                                 // but don't forget to compensate for an pre-existing UIDNEXT value
469                                                 qMax(mailbox->syncState.uidNext(), highestKnownUid + 1)
470                                                 :
471                                                 // No messages, or no messages with valid UID -> use the UIDNEXT from the syncing state
472                                                 // but prevent a possible invalid 0:*
473                                                 qMax(mailbox->syncState.uidNext(), 1u)
474                                             ), QList<QByteArray>() << "FLAGS"));
475         model->m_taskModel->slotTaskMighHaveChanged(this);
476         return true;
477     } else if (resp->kind == Imap::Responses::RECENT) {
478         mailbox->syncState.setRecent(resp->number);
479         list->m_recentMessageCount = resp->number;
480         model->emitMessageCountChanged(mailbox);
481         saveSyncStateNowOrLater(mailbox);
482         return true;
483     } else {
484         return false;
485     }
486 }
487 
handleVanished(const Responses::Vanished * const resp)488 bool KeepMailboxOpenTask::handleVanished(const Responses::Vanished *const resp)
489 {
490     if (_dead) {
491         _failed(tr("Asked to die"));
492         return true;
493     }
494 
495     if (dieIfInvalidMailbox())
496         return true;
497 
498     CHECK_IS_RUNNING
499 
500     if (resp->earlier != Responses::Vanished::NOT_EARLIER)
501         return false;
502 
503     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
504     Q_ASSERT(mailbox);
505 
506     mailbox->handleVanished(model, *resp);
507     saveSyncStateNowOrLater(mailbox);
508     return true;
509 }
510 
handleFetch(const Imap::Responses::Fetch * const resp)511 bool KeepMailboxOpenTask::handleFetch(const Imap::Responses::Fetch *const resp)
512 {
513     if (dieIfInvalidMailbox())
514         return true;
515 
516     if (_dead) {
517         _failed(tr("Asked to die"));
518         return true;
519     }
520 
521     CHECK_IS_RUNNING
522 
523     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
524     Q_ASSERT(mailbox);
525     model->genericHandleFetch(mailbox, resp);
526     return true;
527 }
528 
slotPerformNoop()529 void KeepMailboxOpenTask::slotPerformNoop()
530 {
531     // FIXME: abort/die
532     model->m_taskFactory->createNoopTask(model, this);
533 }
534 
handleStateHelper(const Imap::Responses::State * const resp)535 bool KeepMailboxOpenTask::handleStateHelper(const Imap::Responses::State *const resp)
536 {
537     // FIXME: abort/die
538 
539     if (handleResponseCodeInsideState(resp))
540         return true;
541 
542     // FIXME: checks for shouldExit and proper boundaries?
543 
544     if (resp->respCode == Responses::CLOSED) {
545         switch (model->accessParser(parser).connState) {
546         case CONN_STATE_SELECTING:
547         case CONN_STATE_SELECTING_WAIT_FOR_CLOSE:
548             model->changeConnectionState(parser, CONN_STATE_SELECTING);
549             finalizeTermination();
550             break;
551         case CONN_STATE_LOGOUT:
552             finalizeTermination();
553             break;
554         default:
555             throw UnexpectedResponseReceived("No other mailbox is being selected, but got a [CLOSED] response", *resp);
556         }
557     }
558 
559     if (resp->tag.isEmpty())
560         return false;
561 
562     if (resp->tag == tagIdle) {
563 
564         Q_ASSERT(idleLauncher);
565         if (resp->kind == Responses::OK) {
566             // The IDLE got terminated for whatever reason, so we should schedule its restart
567             idleLauncher->idleCommandCompleted();
568             if (canRunIdleRightNow()) {
569                 idleLauncher->enterIdleLater();
570             }
571         } else {
572             // The IDLE command has failed. Let's assume it's a permanent error and don't request it in future.
573             log(QStringLiteral("The IDLE command has failed"));
574             shouldRunIdle = false;
575             idleLauncher->idleCommandFailed();
576             idleLauncher->deleteLater();
577             idleLauncher = 0;
578         }
579         tagIdle.clear();
580         // IDLE is special because it's not really a native Task. Therefore, we have to duplicate the check for its completion
581         // and possible termination request here.
582         // FIXME: maybe rewrite IDLE to be a native task and get all the benefits for free? Any drawbacks?
583         if (shouldExit && ! hasPendingInternalActions() && (! synchronizeConn || synchronizeConn->isFinished())) {
584             terminate();
585         }
586         return true;
587     } else if (newArrivalsFetch.contains(resp->tag)) {
588         newArrivalsFetch.removeOne(resp->tag);
589 
590         if (newArrivalsFetch.isEmpty() && mailboxIndex.isValid()) {
591             // No pending commands for fetches of the mailbox state -> we have a consistent and accurate, up-to-date view
592             // -> we should save this
593             TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
594             Q_ASSERT(mailbox);
595             mailbox->saveSyncStateAndUids(model);
596         }
597 
598         if (resp->kind != Responses::OK) {
599             _failed(QLatin1String("FETCH of new arrivals failed: ") + resp->message);
600         }
601         // Don't forget to resume IDLE, if desired; that's easiest by simply behaving as if a "task" has just finished
602         slotTaskDeleted(0);
603         model->m_taskModel->slotTaskMighHaveChanged(this);
604         return true;
605     } else if (resp->tag == tagClose) {
606         tagClose.clear();
607         model->changeConnectionState(parser, CONN_STATE_AUTHENTICATED);
608         if (m_deleteCurrentMailboxTask) {
609             m_deleteCurrentMailboxTask->perform();
610         }
611         if (resp->kind != Responses::OK) {
612             _failed(QLatin1String("CLOSE failed: ") + resp->message);
613         }
614         terminate();
615         return true;
616     } else {
617         return false;
618     }
619 }
620 
621 /** @short Reimplemented from ImapTask
622 
623 This function's semantics is slightly shifted from ImapTask::abort(). It gets called when the KeepMailboxOpenTask has decided to
624 terminate and its biggest goals are to:
625 
626 - Prevent any further activity from hitting this parser. We're here to "guard" access to it, and we're about to terminate, so the
627   tasks shall negotiate their access through some other KeepMailboxOpenTask.
628 - Terminate our internal code which might want to access the connection (NoopTask, IdleLauncher,...)
629 */
abort()630 void KeepMailboxOpenTask::abort()
631 {
632     if (noopTimer)
633         noopTimer->stop();
634     if (idleLauncher)
635         idleLauncher->die();
636 
637     detachFromMailbox();
638 
639     _aborted = true;
640     // We do not want to propagate the signal to the child tasks, though -- the KeepMailboxOpenTask::abort() is used in the course
641     // of the regular "hey, free this connection and pass it to another KeepMailboxOpenTask" situations.
642 }
643 
644 /** @short Stop working as a maintaining task */
detachFromMailbox()645 void KeepMailboxOpenTask::detachFromMailbox()
646 {
647     if (mailboxIndex.isValid()) {
648         // Mark current mailbox as "orphaned by the housekeeping task"
649         TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
650         Q_ASSERT(mailbox);
651 
652         // We're already obsolete -> don't pretend to accept new tasks
653         if (mailbox->maintainingTask == this)
654             mailbox->maintainingTask = 0;
655     }
656     if (model->m_parsers.contains(parser) && model->accessParser(parser).maintainingTask == this) {
657         model->accessParser(parser).maintainingTask = 0;
658     }
659 }
660 
661 /** @short Reimplemented from ImapTask
662 
663 We're aksed to die right now, so we better take any depending stuff with us. That poor tasks are not going to outlive me!
664 */
die(const QString & message)665 void KeepMailboxOpenTask::die(const QString &message)
666 {
667     if (shouldExit) {
668         // OK, we're done, and getting killed. This is fine; just don't emit failed()
669         // because we aren't actually failing.
670         // This is a speciality of the KeepMailboxOpenTask because it's the only task
671         // this has a very long life.
672         _finished = true;
673     }
674     ImapTask::die(message);
675     detachFromMailbox();
676 }
677 
678 /** @short Kill all pending tasks -- both the regular one and the replacement ObtainSynchronizedMailboxTask instances
679 
680 Reimplemented from the ImapTask.
681 */
killAllPendingTasks(const QString & message)682 void KeepMailboxOpenTask::killAllPendingTasks(const QString &message)
683 {
684     Q_FOREACH(ImapTask *task, dependingTasksForThisMailbox) {
685         task->die(message);
686     }
687     Q_FOREACH(ImapTask *task, dependingTasksNoMailbox) {
688         task->die(message);
689     }
690     Q_FOREACH(ImapTask *task, waitingObtainTasks) {
691         task->die(message);
692     }
693 }
694 
debugIdentification() const695 QString KeepMailboxOpenTask::debugIdentification() const
696 {
697     if (! mailboxIndex.isValid())
698         return QStringLiteral("[invalid mailboxIndex]");
699 
700     TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
701     Q_ASSERT(mailbox);
702     return QStringLiteral("attached to %1%2%3").arg(mailbox->mailbox(),
703             (synchronizeConn && ! synchronizeConn->isFinished()) ? QStringLiteral(" [syncConn unfinished]") : QString(),
704             shouldExit ? QStringLiteral(" [shouldExit]") : QString()
705                                                        );
706 }
707 
708 /** @short The user wants us to go offline */
stopForLogout()709 void KeepMailboxOpenTask::stopForLogout()
710 {
711     abort();
712     breakOrCancelPossibleIdle();
713     killAllPendingTasks(tr("Logging off..."));
714 
715     // We're supposed to go offline. Given that we're a long-running task, I do not consider this a "failure".
716     // In particular, if the initial SELECT has not finished yet, the ObtainSynchronizedMailboxTask would get
717     // killed as well, and hence the mailboxSyncFailed() signal will get emitted.
718     // The worst thing which can possibly happen is that we're in the middle of checking the new arrivals.
719     // That's bad, because we've got unknown UIDs in our in-memory map, which is going to hurt during the next sync
720     // -- but that's something which should be handled elsewhere, IMHO.
721     // Therefore, make sure a subsequent call to die() doesn't propagate a failure.
722     shouldExit = true;
723 }
724 
handleFlags(const Imap::Responses::Flags * const resp)725 bool KeepMailboxOpenTask::handleFlags(const Imap::Responses::Flags *const resp)
726 {
727     if (dieIfInvalidMailbox())
728         return true;
729 
730     // Well, there isn't much point in keeping track of these flags, but given that
731     // IMAP servers are happy to send these responses even after the initial sync, we
732     // better handle them explicitly here.
733     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
734     Q_ASSERT(mailbox);
735     mailbox->syncState.setFlags(resp->flags);
736     return true;
737 }
738 
activateTasks()739 void KeepMailboxOpenTask::activateTasks()
740 {
741     // FIXME: abort/die
742 
743     if (isRunning != Running::RUNNING)
744         return;
745 
746     breakOrCancelPossibleIdle();
747 
748     if (m_deleteCurrentMailboxTask) {
749         closeMailboxDestructively();
750         return;
751     }
752 
753     slotFetchRequestedEnvelopes();
754     slotFetchRequestedParts();
755 
756     while (!dependingTasksForThisMailbox.isEmpty() && model->accessParser(parser).activeTasks.size() < limitActiveTasks) {
757         breakOrCancelPossibleIdle();
758         ImapTask *task = dependingTasksForThisMailbox.takeFirst();
759         runningTasksForThisMailbox.append(task);
760         dependentTasks.removeOne(task);
761         task->perform();
762     }
763     while (!dependingTasksNoMailbox.isEmpty() && model->accessParser(parser).activeTasks.size() < limitActiveTasks) {
764         breakOrCancelPossibleIdle();
765         ImapTask *task = dependingTasksNoMailbox.takeFirst();
766         dependentTasks.removeOne(task);
767         task->perform();
768     }
769 
770     if (idleLauncher && canRunIdleRightNow())
771         idleLauncher->enterIdleLater();
772 }
773 
requestPartDownload(const uint uid,const QByteArray & partId,const uint estimatedSize)774 void KeepMailboxOpenTask::requestPartDownload(const uint uid, const QByteArray &partId, const uint estimatedSize)
775 {
776     requestedParts[uid].insert(partId);
777     requestedPartSizes[uid] += estimatedSize;
778     if (!fetchPartTimer->isActive()) {
779         fetchPartTimer->start();
780     }
781 }
782 
requestEnvelopeDownload(const uint uid)783 void KeepMailboxOpenTask::requestEnvelopeDownload(const uint uid)
784 {
785     requestedEnvelopes.append(uid);
786     if (!fetchEnvelopeTimer->isActive()) {
787         fetchEnvelopeTimer->start();
788     }
789 }
790 
slotFetchRequestedParts()791 void KeepMailboxOpenTask::slotFetchRequestedParts()
792 {
793     // FIXME: abort/die
794 
795     if (requestedParts.isEmpty())
796         return;
797 
798     breakOrCancelPossibleIdle();
799 
800     auto it = requestedParts.begin();
801     auto parts = *it;
802 
803     // When asked to exit, do as much as possible and die
804     while (shouldExit || fetchPartTasks.size() < limitParallelFetchTasks) {
805         Imap::Uids uids;
806         uint totalSize = 0;
807         while (uids.size() < limitMessagesAtOnce && it != requestedParts.end() && totalSize < limitBytesAtOnce) {
808             if (parts != *it)
809                 break;
810             parts = *it;
811             uids << it.key();
812             totalSize += requestedPartSizes.take(it.key());
813             it = requestedParts.erase(it);
814         }
815         if (uids.isEmpty())
816             return;
817 
818         fetchPartTasks << model->m_taskFactory->createFetchMsgPartTask(model, mailboxIndex, uids, parts.toList());
819     }
820 }
821 
slotFetchRequestedEnvelopes()822 void KeepMailboxOpenTask::slotFetchRequestedEnvelopes()
823 {
824     // FIXME: abort/die
825 
826     if (requestedEnvelopes.isEmpty())
827         return;
828 
829     breakOrCancelPossibleIdle();
830 
831     Imap::Uids fetchNow;
832     if (shouldExit) {
833         fetchNow = requestedEnvelopes;
834         requestedEnvelopes.clear();
835     } else {
836         const int amount = qMin(requestedEnvelopes.size(), limitMessagesAtOnce); // FIXME: add an extra limit?
837         fetchNow = requestedEnvelopes.mid(0, amount);
838         requestedEnvelopes.erase(requestedEnvelopes.begin(), requestedEnvelopes.begin() + amount);
839     }
840     fetchMetadataTasks << model->m_taskFactory->createFetchMsgMetadataTask(model, mailboxIndex, fetchNow);
841 }
842 
breakOrCancelPossibleIdle()843 void KeepMailboxOpenTask::breakOrCancelPossibleIdle()
844 {
845     if (idleLauncher) {
846         idleLauncher->finishIdle();
847     }
848 }
849 
handleResponseCodeInsideState(const Imap::Responses::State * const resp)850 bool KeepMailboxOpenTask::handleResponseCodeInsideState(const Imap::Responses::State *const resp)
851 {
852     switch (resp->respCode) {
853     case Responses::UIDNEXT:
854     {
855         if (dieIfInvalidMailbox())
856             return resp->tag.isEmpty();
857 
858         TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
859         Q_ASSERT(mailbox);
860         const Responses::RespData<uint> *const num = dynamic_cast<const Responses::RespData<uint>* const>(resp->respCodeData.data());
861         if (num) {
862             mailbox->syncState.setUidNext(num->data);
863             saveSyncStateNowOrLater(mailbox);
864             // We shouldn't eat tagged responses from this context
865             return resp->tag.isEmpty();
866         } else {
867             throw CantHappen("State response has invalid UIDNEXT respCodeData", *resp);
868         }
869         break;
870     }
871     case Responses::PERMANENTFLAGS:
872         // Another useless one, but we want to consume it now to prevent a warning about
873         // an unhandled message
874     {
875         if (dieIfInvalidMailbox())
876             return resp->tag.isEmpty();
877 
878         TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
879         Q_ASSERT(mailbox);
880         const Responses::RespData<QStringList> *const num = dynamic_cast<const Responses::RespData<QStringList>* const>(resp->respCodeData.data());
881         if (num) {
882             mailbox->syncState.setPermanentFlags(num->data);
883             // We shouldn't eat tagged responses from this context
884             return resp->tag.isEmpty();
885         } else {
886             throw CantHappen("State response has invalid PERMANENTFLAGS respCodeData", *resp);
887         }
888         break;
889     }
890     case Responses::HIGHESTMODSEQ:
891     {
892         if (dieIfInvalidMailbox())
893             return resp->tag.isEmpty();
894 
895         TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
896         Q_ASSERT(mailbox);
897         const Responses::RespData<quint64> *const num = dynamic_cast<const Responses::RespData<quint64>* const>(resp->respCodeData.data());
898         Q_ASSERT(num);
899         mailbox->syncState.setHighestModSeq(num->data);
900         saveSyncStateNowOrLater(mailbox);
901         return resp->tag.isEmpty();
902     }
903     case Responses::UIDVALIDITY:
904     {
905         if (dieIfInvalidMailbox())
906             return resp->tag.isEmpty();
907 
908         TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
909         Q_ASSERT(mailbox);
910         const Responses::RespData<uint> *const num = dynamic_cast<const Responses::RespData<uint>* const>(resp->respCodeData.data());
911         Q_ASSERT(num);
912         if (mailbox->syncState.uidValidity() == num->data) {
913             // this is a harmless and useless message
914             return resp->tag.isEmpty();
915         } else {
916             // On the other hand, this a serious condition -- the server is telling us that the UIDVALIDITY has changed while
917             // a mailbox is open. There isn't much we could do here; having code for handling this gracefuly is just too much
918             // work for little to no benefit.
919             // The sane thing is to disconnect from this mailbox.
920             EMIT_LATER(model, imapError, Q_ARG(QString, tr("The UIDVALIDITY has changed while mailbox is open. Please reconnect.")));
921             model->setNetworkPolicy(NETWORK_OFFLINE);
922             return resp->tag.isEmpty();
923         }
924     }
925     default:
926         // Do nothing here
927         break;
928     }
929     return false;
930 }
931 
slotUnselected()932 void KeepMailboxOpenTask::slotUnselected()
933 {
934     switch (model->accessParser(parser).connState) {
935     case CONN_STATE_SYNCING:
936     case CONN_STATE_SELECTED:
937     case CONN_STATE_FETCHING_PART:
938     case CONN_STATE_FETCHING_MSG_METADATA:
939         model->changeConnectionState(parser, CONN_STATE_AUTHENTICATED);
940         break;
941     default:
942         // no need to do anything from here
943         break;
944     }
945     detachFromMailbox();
946     isRunning = Running::RUNNING; // WTF?
947     shouldExit = true;
948     _failed(tr("UNSELECTed"));
949 }
950 
dieIfInvalidMailbox()951 bool KeepMailboxOpenTask::dieIfInvalidMailbox()
952 {
953     if (mailboxIndex.isValid())
954         return false;
955 
956     if (m_deleteCurrentMailboxTask) {
957         // The current mailbox was supposed to be deleted; don't try to UNSELECT from this context
958         return true;
959     }
960 
961     // See ObtainSynchronizedMailboxTask::dieIfInvalidMailbox() for details
962     if (!unSelectTask && isRunning == Running::RUNNING) {
963         unSelectTask = model->m_taskFactory->createUnSelectTask(model, this);
964         connect(unSelectTask, &ImapTask::completed, this, &KeepMailboxOpenTask::slotUnselected);
965         unSelectTask->perform();
966     }
967 
968     return true;
969 }
970 
hasPendingInternalActions() const971 bool KeepMailboxOpenTask::hasPendingInternalActions() const
972 {
973     bool hasToWaitForIdleTermination = idleLauncher ? idleLauncher->waitingForIdleTaggedTermination() : false;
974     return !(dependingTasksForThisMailbox.isEmpty() && dependingTasksNoMailbox.isEmpty() && runningTasksForThisMailbox.isEmpty() &&
975              requestedParts.isEmpty() && requestedEnvelopes.isEmpty() && newArrivalsFetch.isEmpty()) || hasToWaitForIdleTermination;
976 }
977 
978 /** @short Returns true if this task can be safely terminated
979 
980 FIXME: document me
981 */
isReadyToTerminate() const982 bool KeepMailboxOpenTask::isReadyToTerminate() const
983 {
984     return shouldExit && !hasPendingInternalActions() && (!synchronizeConn || synchronizeConn->isFinished());
985 }
986 
987 /** @short Return true if we're configured to run IDLE and if there's no ongoing activity */
canRunIdleRightNow() const988 bool KeepMailboxOpenTask::canRunIdleRightNow() const
989 {
990     bool res = shouldRunIdle && dependingTasksForThisMailbox.isEmpty() &&
991             dependingTasksNoMailbox.isEmpty() && newArrivalsFetch.isEmpty();
992 
993     // If there's just one active tasks, it's the "this" one. If there are more of them, let's see if it's just one more
994     // and that one more thing is a SortTask which is in the "just updating" mode.
995     // If that is the case, we can still allow further IDLE, that task will abort idling when it needs to.
996     // Nifty, isn't it?
997     if (model->accessParser(parser).activeTasks.size() > 1) {
998         if (model->accessParser(parser).activeTasks.size() == 2 &&
999                 dynamic_cast<SortTask*>(model->accessParser(parser).activeTasks[1]) &&
1000                 dynamic_cast<SortTask*>(model->accessParser(parser).activeTasks[1])->isJustUpdatingNow()) {
1001             // This is OK, so no need to clear the "OK" flag
1002         } else {
1003             // Too bad, cannot IDLE
1004             res = false;
1005         }
1006     }
1007 
1008     if (!res)
1009         return false;
1010 
1011     Q_ASSERT(model->accessParser(parser).activeTasks.front() == this);
1012     return true;
1013 }
1014 
taskData(const int role) const1015 QVariant KeepMailboxOpenTask::taskData(const int role) const
1016 {
1017     // FIXME
1018     Q_UNUSED(role);
1019     return QVariant();
1020 }
1021 
1022 /** @short The specified task can be abort()ed when the mailbox shall be vacanted
1023 
1024 It's an error to call this on a task which we aren't tracking already.
1025 */
feelFreeToAbortCaller(ImapTask * task)1026 void KeepMailboxOpenTask::feelFreeToAbortCaller(ImapTask *task)
1027 {
1028     abortableTasks.append(task);
1029 }
1030 
1031 /** @short It's time to reset the counters and perform the sync */
syncingTimeout()1032 void KeepMailboxOpenTask::syncingTimeout()
1033 {
1034     if (!mailboxIndex.isValid()) {
1035         return;
1036     }
1037     if (m_skippedStateSynces == 0) {
1038         // This means that state syncing it not being throttled, i.e. we're just resetting the window
1039         // which determines the rate of requests which would normally trigger saving
1040         m_performedStateSynces = 0;
1041     } else {
1042         // there's been no fresh arrivals for our timeout period -> let's flush the pending events
1043         TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
1044         Q_ASSERT(mailbox);
1045         saveSyncStateIfPossible(mailbox);
1046     }
1047 }
1048 
saveSyncStateNowOrLater(Imap::Mailbox::TreeItemMailbox * mailbox)1049 void KeepMailboxOpenTask::saveSyncStateNowOrLater(Imap::Mailbox::TreeItemMailbox *mailbox)
1050 {
1051     bool saveImmediately = true;
1052 
1053     /** @short After processing so many responses immediately, switch to a delayed mode where the saving is deferred */
1054     const uint throttlingThreshold = 100;
1055 
1056     /** @short Flush the queue after postponing this number of messages.
1057 
1058     It's "ridiculously high", but it's still a number so that our integer newer wraps.
1059     */
1060     const uint maxDelayedResponses = 10000;
1061 
1062     if (m_skippedStateSynces > 0) {
1063         // we're actively throttling
1064         if (m_skippedStateSynces >= maxDelayedResponses) {
1065             // ...but we've been throttling too many responses, let's flush them now
1066             Q_ASSERT(m_syncingTimer->isActive());
1067             // do not go back to 0, otherwise there will be a lot of immediate events within each interval
1068             m_performedStateSynces = throttlingThreshold;
1069             saveImmediately = true;
1070         } else {
1071             ++m_skippedStateSynces;
1072             saveImmediately = false;
1073         }
1074     } else {
1075         // no throttling, cool
1076         if (m_performedStateSynces >= throttlingThreshold) {
1077             // ...but we should start throttling now
1078             ++m_skippedStateSynces;
1079             saveImmediately = false;
1080         } else {
1081             ++m_performedStateSynces;
1082             if (!m_syncingTimer->isActive()) {
1083                 // reset the sliding window
1084                 m_syncingTimer->start();
1085             }
1086             saveImmediately = true;
1087         }
1088     }
1089 
1090     if (saveImmediately) {
1091         saveSyncStateIfPossible(mailbox);
1092     } else {
1093         m_performedStateSynces = 0;
1094         m_syncingTimer->start();
1095     }
1096 }
1097 
saveSyncStateIfPossible(TreeItemMailbox * mailbox)1098 void KeepMailboxOpenTask::saveSyncStateIfPossible(TreeItemMailbox *mailbox)
1099 {
1100     m_skippedStateSynces = 0;
1101     TreeItemMsgList *list = static_cast<TreeItemMsgList*>(mailbox->m_children[0]);
1102     if (list->fetched()) {
1103         mailbox->saveSyncStateAndUids(model);
1104     } else {
1105         list->setFetchStatus(Imap::Mailbox::TreeItem::LOADING);
1106     }
1107 }
1108 
closeMailboxDestructively()1109 void KeepMailboxOpenTask::closeMailboxDestructively()
1110 {
1111     tagClose = parser->close();
1112 }
1113 
1114 /** @short Is this task on its own keeping the connection busy?
1115 
1116 Right now, only fetching of new arrivals is being done in the context of this KeepMailboxOpenTask task.
1117 */
hasItsOwnActivity() const1118 bool KeepMailboxOpenTask::hasItsOwnActivity() const
1119 {
1120     return !newArrivalsFetch.isEmpty();
1121 }
1122 
1123 /** @short Signal the final termination of this task */
finalizeTermination()1124 void KeepMailboxOpenTask::finalizeTermination()
1125 {
1126     if (!_finished) {
1127         _finished = true;
1128         emit completed(this);
1129     }
1130     CHECK_TASK_TREE;
1131 }
1132 
1133 }
1134 }
1135