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