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 "ObtainSynchronizedMailboxTask.h"
24 #include <algorithm>
25 #include <sstream>
26 #include <QTimer>
27 #include "Common/InvokeMethod.h"
28 #include "Imap/Model/ItemRoles.h"
29 #include "Imap/Model/MailboxTree.h"
30 #include "Imap/Model/Model.h"
31 #include "KeepMailboxOpenTask.h"
32 #include "UnSelectTask.h"
33 
34 namespace Imap
35 {
36 namespace Mailbox
37 {
38 
ObtainSynchronizedMailboxTask(Model * model,const QModelIndex & mailboxIndex,ImapTask * parentTask,KeepMailboxOpenTask * keepTask)39 ObtainSynchronizedMailboxTask::ObtainSynchronizedMailboxTask(Model *model, const QModelIndex &mailboxIndex, ImapTask *parentTask,
40         KeepMailboxOpenTask *keepTask):
41     ImapTask(model), conn(parentTask), mailboxIndex(mailboxIndex), status(STATE_WAIT_FOR_CONN), uidSyncingMode(UID_SYNC_ALL),
42     firstUnknownUidOffset(0), m_usingQresync(false), unSelectTask(0), keepTaskChild(keepTask)
43 {
44     // The Parser* is not provided by our parent task, but instead through the keepTaskChild.  The reason is simple, the parent
45     // task might not even exist, but there's always an KeepMailboxOpenTask in the game.
46     parser = keepTaskChild->parser;
47     Q_ASSERT(parser);
48     if (conn) {
49         conn->addDependentTask(this);
50     }
51     CHECK_TASK_TREE
52     addDependentTask(keepTaskChild);
53     CHECK_TASK_TREE
54     connect(this, &ImapTask::failed, this, &ObtainSynchronizedMailboxTask::signalSyncFailure);
55 }
56 
addDependentTask(ImapTask * task)57 void ObtainSynchronizedMailboxTask::addDependentTask(ImapTask *task)
58 {
59     if (!dependentTasks.isEmpty()) {
60         throw CantHappen("Attempted to add another dependent task to an ObtainSynchronizedMailboxTask");
61     }
62     ImapTask::addDependentTask(task);
63 }
64 
perform()65 void ObtainSynchronizedMailboxTask::perform()
66 {
67     CHECK_TASK_TREE
68     markAsActiveTask();
69 
70     if (_dead || _aborted) {
71         // We're at the very start, so let's try to abort in a sane way
72         _failed(tr("Asked to abort or die"));
73         die(tr("Mailbox syncing dead or aborted"));
74         return;
75     }
76 
77     if (! mailboxIndex.isValid()) {
78         // FIXME: proper error handling
79         log(QStringLiteral("The mailbox went missing, sorry"), Common::LOG_MAILBOX_SYNC);
80         _completed();
81         return;
82     }
83 
84     TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
85     Q_ASSERT(mailbox);
86     TreeItemMsgList *msgList = dynamic_cast<TreeItemMsgList *>(mailbox->m_children[0]);
87     Q_ASSERT(msgList);
88 
89     msgList->setFetchStatus(TreeItem::LOADING);
90 
91     Q_ASSERT(model->m_parsers.contains(parser));
92 
93     oldSyncState = model->cache()->mailboxSyncState(mailbox->mailbox());
94     bool hasQresync = model->accessParser(parser).capabilities.contains(QStringLiteral("QRESYNC"));
95     if (hasQresync && oldSyncState.isUsableForCondstore()) {
96         m_usingQresync = true;
97         auto oldUidMap = model->cache()->uidMapping(mailbox->mailbox());
98         if (oldUidMap.isEmpty()) {
99             selectCmd = parser->selectQresync(mailbox->mailbox(), oldSyncState.uidValidity(),
100                                               oldSyncState.highestModSeq());
101         } else {
102             Sequence knownSeq, knownUid;
103             int i = oldUidMap.size() / 2;
104             while (i < oldUidMap.size()) {
105                 // Message sequence number is one-based, our indexes are zero-based
106                 knownSeq.add(i + 1);
107                 knownUid.add(oldUidMap[i]);
108                 i += (oldUidMap.size() - i) / 2 + 1;
109             }
110             // We absolutely want to maintain a complete UID->seq mapping at all times, which is why the known-uids shall remain
111             // empty to indicate "anything".
112             selectCmd = parser->selectQresync(mailbox->mailbox(), oldSyncState.uidValidity(),
113                                               oldSyncState.highestModSeq(), Sequence(), knownSeq, knownUid);
114         }
115     } else if (model->accessParser(parser).capabilities.contains(QStringLiteral("CONDSTORE"))) {
116         selectCmd = parser->select(mailbox->mailbox(), QList<QByteArray>() << "CONDSTORE");
117     } else {
118         selectCmd = parser->select(mailbox->mailbox());
119     }
120     if (hasQresync && model->accessParser(parser).connState > CONN_STATE_AUTHENTICATED) {
121         // The CLOSED response code is defined in RFC 5162. It should be sent out even if the client does not actually use
122         // the QRESYNC extension (such as when syncing a mailbox for the first time).
123         // There will, however, be no CLOSED if no mailbox was selected previously, of course.
124         model->changeConnectionState(parser, CONN_STATE_SELECTING_WAIT_FOR_CLOSE);
125     } else {
126         model->changeConnectionState(parser, CONN_STATE_SELECTING);
127     }
128     mailbox->syncState = SyncState();
129     status = STATE_SELECTING;
130     log(QStringLiteral("Synchronizing mailbox"), Common::LOG_MAILBOX_SYNC);
131     emit model->mailboxSyncingProgress(mailboxIndex, status);
132 }
133 
handleStateHelper(const Imap::Responses::State * const resp)134 bool ObtainSynchronizedMailboxTask::handleStateHelper(const Imap::Responses::State *const resp)
135 {
136     if (dieIfInvalidMailbox())
137         return true;
138 
139     if (handleResponseCodeInsideState(resp))
140         return true;
141 
142     if (resp->tag.isEmpty())
143         return false;
144 
145     if (_dead) {
146         _failed(tr("Asked to die"));
147         return true;
148     }
149     // We absolutely have to ignore the abort() request
150 
151     if (resp->tag == selectCmd) {
152 
153         if (resp->kind == Responses::OK) {
154             Q_ASSERT(status == STATE_SELECTING);
155             switch (model->accessParser(parser).connState) {
156             case CONN_STATE_SELECTING_WAIT_FOR_CLOSE:
157                 throw UnexpectedResponseReceived("Server did not send the CLOSED response code to notify us that "
158                                                  "the previous mailbox was successfully closed. Stopping the sync to prevent data loss.");
159             case CONN_STATE_SELECTING:
160                 // this is what we want
161                 break;
162             default:
163                 throw UnexpectedResponseReceived("Wrong connection state -- how come that a mailbox was opened in this moment?");
164             }
165             finalizeSelect();
166         } else {
167             _failed(QLatin1String("SELECT failed: ") + resp->message);
168             model->changeConnectionState(parser, CONN_STATE_AUTHENTICATED);
169         }
170         return true;
171     } else if (resp->tag == uidSyncingCmd) {
172 
173         if (resp->kind == Responses::OK) {
174             // FIXME: move the finalizeSearch() here to support working with split SEARCH reposnes -- but beware of
175             // arrivals/expunges which happen while the UID SEARCH is in progres...
176             log(QStringLiteral("UIDs synchronized"), Common::LOG_MAILBOX_SYNC);
177             Q_ASSERT(status == STATE_SYNCING_FLAGS);
178             Q_ASSERT(mailboxIndex.isValid());   // FIXME
179             TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
180             Q_ASSERT(mailbox);
181             syncFlags(mailbox);
182         } else {
183             _failed(QLatin1String("UID syncing failed: ") + resp->message);
184             // FIXME: UNSELECT?
185         }
186         return true;
187     } else if (resp->tag == flagsCmd) {
188 
189         if (resp->kind == Responses::OK) {
190             //qDebug() << "received OK for flagsCmd";
191             Q_ASSERT(status == STATE_SYNCING_FLAGS);
192             Q_ASSERT(mailboxIndex.isValid());
193             TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
194             Q_ASSERT(mailbox);
195             status = STATE_DONE;
196             log(QStringLiteral("Flags synchronized"), Common::LOG_MAILBOX_SYNC);
197             notifyInterestingMessages(mailbox);
198             flagsCmd.clear();
199 
200             if (newArrivalsFetch.isEmpty()) {
201                 mailbox->saveSyncStateAndUids(model);
202                 model->changeConnectionState(parser, CONN_STATE_SELECTED);
203                 _completed();
204             } else {
205                 log(QStringLiteral("Pending new arrival fetching, not terminating yet"), Common::LOG_MAILBOX_SYNC);
206             }
207         } else {
208             status = STATE_DONE;
209             _failed(QLatin1String("Flags synchronization failed: ") + resp->message);
210             // FIXME: UNSELECT?
211         }
212         emit model->mailboxSyncingProgress(mailboxIndex, status);
213         return true;
214     } else if (newArrivalsFetch.contains(resp->tag)) {
215 
216         if (resp->kind == Responses::OK) {
217             newArrivalsFetch.removeOne(resp->tag);
218 
219             if (newArrivalsFetch.isEmpty() && status == STATE_DONE && flagsCmd.isEmpty()) {
220                 Q_ASSERT(mailboxIndex.isValid());
221                 TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
222                 Q_ASSERT(mailbox);
223                 mailbox->saveSyncStateAndUids(model);
224                 model->changeConnectionState(parser, CONN_STATE_SELECTED);
225                 _completed();
226             }
227         } else {
228             _failed(QLatin1String("UID discovery of new arrivals after initial UID sync has failed: ") + resp->message);
229             // FIXME: UNSELECT?
230         }
231         return true;
232 
233     } else {
234         return false;
235     }
236 }
237 
finalizeSelect()238 void ObtainSynchronizedMailboxTask::finalizeSelect()
239 {
240     Q_ASSERT(mailboxIndex.isValid());
241     TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
242     Q_ASSERT(mailbox);
243     TreeItemMsgList *list = dynamic_cast<TreeItemMsgList *>(mailbox->m_children[ 0 ]);
244     Q_ASSERT(list);
245 
246     model->changeConnectionState(parser, CONN_STATE_SYNCING);
247     const SyncState &syncState = mailbox->syncState;
248     oldSyncState = model->cache()->mailboxSyncState(mailbox->mailbox());
249     list->m_totalMessageCount = syncState.exists();
250     // Note: syncState.unSeen() is the NUMBER of the first unseen message, not their count!
251 
252     uidMap = model->cache()->uidMapping(mailbox->mailbox());
253 
254     if (static_cast<uint>(uidMap.size()) != oldSyncState.exists()) {
255 
256         QString buf;
257         QDebug dbg(&buf);
258         dbg << "Inconsistent cache data, falling back to full sync (" << uidMap.size() << "in UID map," << oldSyncState.exists() <<
259             "EXIST before)";
260         log(buf, Common::LOG_MAILBOX_SYNC);
261         oldSyncState.setHighestModSeq(0);
262         fullMboxSync(mailbox, list);
263     } else {
264         if (syncState.isUsableForSyncing() && oldSyncState.isUsableForSyncing() && syncState.uidValidity() == oldSyncState.uidValidity()) {
265             // Perform a nice re-sync
266 
267             // Check the QRESYNC support and availability
268             if (m_usingQresync && oldSyncState.isUsableForCondstore() && syncState.isUsableForCondstore()) {
269                 // Looks like we can use QRESYNC for fast syncing
270                 if (oldSyncState.highestModSeq() > syncState.highestModSeq()) {
271                     // Looks like a corrupted cache or a server's bug
272                     log(QStringLiteral("Yuck, recycled HIGHESTMODSEQ when trying to use QRESYNC"), Common::LOG_MAILBOX_SYNC);
273                     mailbox->syncState.setHighestModSeq(0);
274                     model->cache()->clearAllMessages(mailbox->mailbox());
275                     m_usingQresync = false;
276                     fullMboxSync(mailbox, list);
277                 } else {
278                     if (oldSyncState.highestModSeq() == syncState.highestModSeq()) {
279                         if (oldSyncState.exists() != syncState.exists()) {
280                             log(QStringLiteral("Sync error: QRESYNC says no changes but EXISTS has changed"), Common::LOG_MAILBOX_SYNC);
281                             mailbox->syncState.setHighestModSeq(0);
282                             model->cache()->clearAllMessages(mailbox->mailbox());
283                             m_usingQresync = false;
284                             fullMboxSync(mailbox, list);
285                         } else if (oldSyncState.uidNext() != syncState.uidNext()) {
286                             log(QStringLiteral("Sync error: QRESYNC says no changes but UIDNEXT has changed"), Common::LOG_MAILBOX_SYNC);
287                             mailbox->syncState.setHighestModSeq(0);
288                             model->cache()->clearAllMessages(mailbox->mailbox());
289                             m_usingQresync = false;
290                             fullMboxSync(mailbox, list);
291                         } else if (syncState.exists() != static_cast<uint>(list->m_children.size())) {
292                             log(QString::fromUtf8("Sync error: constant HIGHESTMODSEQ, EXISTS says %1 messages but in fact "
293                                                    "there are %2 when finalizing SELECT")
294                                 .arg(QString::number(mailbox->syncState.exists()), QString::number(list->m_children.size())),
295                                 Common::LOG_MAILBOX_SYNC);
296                             mailbox->syncState.setHighestModSeq(0);
297                             model->cache()->clearAllMessages(mailbox->mailbox());
298                             m_usingQresync = false;
299                             fullMboxSync(mailbox, list);
300                         } else {
301                             // This should be enough
302                             list->setFetchStatus(TreeItem::DONE);
303                             notifyInterestingMessages(mailbox);
304                             mailbox->saveSyncStateAndUids(model);
305                             model->changeConnectionState(parser, CONN_STATE_SELECTED);
306                             _completed();
307                         }
308                         return;
309                     }
310 
311                     if (static_cast<uint>(list->m_children.size()) != mailbox->syncState.exists()) {
312                         log(QStringLiteral("Sync error: EXISTS says %1 messages, msgList has %2")
313                             .arg(QString::number(mailbox->syncState.exists()), QString::number(list->m_children.size())));
314                         mailbox->syncState.setHighestModSeq(0);
315                         model->cache()->clearAllMessages(mailbox->mailbox());
316                         m_usingQresync = false;
317                         fullMboxSync(mailbox, list);
318                         return;
319                     }
320 
321 
322                     if (oldSyncState.uidNext() < syncState.uidNext()) {
323                         list->setFetchStatus(TreeItem::DONE);
324                         int seqWithLowestUnknownUid = -1;
325                         for (int i = 0; i < list->m_children.size(); ++i) {
326                             TreeItemMessage *msg = static_cast<TreeItemMessage*>(list->m_children[i]);
327                             if (!msg->uid()) {
328                                 seqWithLowestUnknownUid = i;
329                                 break;
330                             }
331                         }
332                         if (seqWithLowestUnknownUid >= 0) {
333                             // We've got some new arrivals, but unfortunately QRESYNC won't report them just yet :(
334                             CommandHandle fetchCmd = parser->uidFetch(Sequence::startingAt(qMax(oldSyncState.uidNext(), 1u)),
335                                                                       QList<QByteArray>() << "FLAGS");
336                             newArrivalsFetch.append(fetchCmd);
337                             status = STATE_DONE;
338                         } else {
339                             // All UIDs are known at this point, including the new arrivals, yay
340                             notifyInterestingMessages(mailbox);
341                             mailbox->saveSyncStateAndUids(model);
342                             model->changeConnectionState(parser, CONN_STATE_SELECTED);
343                             _completed();
344                         }
345                     } else {
346                         // This should be enough, the server should've sent the data already
347                         list->setFetchStatus(TreeItem::DONE);
348                         notifyInterestingMessages(mailbox);
349                         mailbox->saveSyncStateAndUids(model);
350                         model->changeConnectionState(parser, CONN_STATE_SELECTED);
351                         _completed();
352                     }
353                 }
354                 return;
355             }
356 
357             if (syncState.exists() == 0) {
358                 // This is a special case, the mailbox doesn't contain any messages now.
359                 // Let's just save ourselves some work and reuse the "smart" code in the fullMboxSync() here, it will
360                 // do the right thing.
361                 fullMboxSync(mailbox, list);
362                 return;
363             }
364 
365             if (syncState.uidNext() == oldSyncState.uidNext()) {
366                 // No new messages
367 
368                 if (syncState.exists() == oldSyncState.exists()) {
369                     // No deletions, either, so we resync only flag changes
370                     syncNoNewNoDeletions(mailbox, list);
371                 } else {
372                     // Some messages got deleted, but there have been no additions
373                     syncGeneric(mailbox, list);
374                 }
375 
376             } else if (syncState.uidNext() > oldSyncState.uidNext()) {
377                 // Some new messages were delivered since we checked the last time.
378                 // There's no guarantee they are still present, though.
379 
380                 if (syncState.uidNext() - oldSyncState.uidNext() == syncState.exists() - oldSyncState.exists()) {
381                     // Only some new arrivals, no deletions
382                     syncOnlyAdditions(mailbox, list);
383                 } else {
384                     // Generic case; we don't know anything about which messages were deleted and which added
385                     syncGeneric(mailbox, list);
386                 }
387             } else {
388                 // The UIDNEXT has decreased while UIDVALIDITY remains the same. This is forbidden,
389                 // so either a server's bug, or a completely invalid cache.
390                 Q_ASSERT(syncState.uidNext() < oldSyncState.uidNext());
391                 Q_ASSERT(syncState.uidValidity() == oldSyncState.uidValidity());
392                 log(QStringLiteral("Yuck, UIDVALIDITY remains same but UIDNEXT decreased"), Common::LOG_MAILBOX_SYNC);
393                 model->cache()->clearAllMessages(mailbox->mailbox());
394                 fullMboxSync(mailbox, list);
395             }
396         } else if (oldSyncState.isUsableForSyncingWithoutUidNext() && syncState.isUsableForSyncingWithoutUidNext() && oldSyncState.uidValidity() == syncState.uidValidity()) {
397             log(QStringLiteral("Did not receive UIDNEXT, but UIDVALIDITY remains same -> trying non-destructive generic sync"));
398             syncGeneric(mailbox, list);
399         } else {
400             // Forget everything, do a dumb sync
401             model->cache()->clearAllMessages(mailbox->mailbox());
402             fullMboxSync(mailbox, list);
403         }
404     }
405 }
406 
fullMboxSync(TreeItemMailbox * mailbox,TreeItemMsgList * list)407 void ObtainSynchronizedMailboxTask::fullMboxSync(TreeItemMailbox *mailbox, TreeItemMsgList *list)
408 {
409     log(QStringLiteral("Full synchronization"), Common::LOG_MAILBOX_SYNC);
410 
411     QModelIndex parent = list->toIndex(model);
412     if (! list->m_children.isEmpty()) {
413         model->beginRemoveRows(parent, 0, list->m_children.size() - 1);
414         auto oldItems = list->m_children;
415         list->m_children.clear();
416         model->endRemoveRows();
417         qDeleteAll(oldItems);
418     }
419     if (mailbox->syncState.exists()) {
420         list->m_children.reserve(mailbox->syncState.exists());
421         model->beginInsertRows(parent, 0, mailbox->syncState.exists() - 1);
422         for (uint i = 0; i < mailbox->syncState.exists(); ++i) {
423             TreeItemMessage *msg = new TreeItemMessage(list);
424             msg->m_offset = i;
425             list->m_children << msg;
426         }
427         model->endInsertRows();
428 
429         syncUids(mailbox);
430         list->m_numberFetchingStatus = TreeItem::LOADING;
431         list->m_unreadMessageCount = 0;
432     } else {
433         // No messages, we're done here
434         list->m_totalMessageCount = 0;
435         list->m_unreadMessageCount = 0;
436         list->m_numberFetchingStatus = TreeItem::DONE;
437         list->setFetchStatus(TreeItem::DONE);
438 
439         // The remote mailbox is empty -> we're done now
440         model->changeConnectionState(parser, CONN_STATE_SELECTED);
441         status = STATE_DONE;
442         emit model->mailboxSyncingProgress(mailboxIndex, status);
443         notifyInterestingMessages(mailbox);
444         mailbox->saveSyncStateAndUids(model);
445         model->changeConnectionState(parser, CONN_STATE_SELECTED);
446         // Take care here: this call could invalidate our index (see test coverage)
447         _completed();
448     }
449     // Our mailbox might have actually been invalidated by various callbacks activated above
450     if (mailboxIndex.isValid()) {
451         Q_ASSERT(mailboxIndex.internalPointer() == mailbox);
452         model->emitMessageCountChanged(mailbox);
453     }
454 }
455 
syncNoNewNoDeletions(TreeItemMailbox * mailbox,TreeItemMsgList * list)456 void ObtainSynchronizedMailboxTask::syncNoNewNoDeletions(TreeItemMailbox *mailbox, TreeItemMsgList *list)
457 {
458     Q_ASSERT(mailbox->syncState.exists() == static_cast<uint>(uidMap.size()));
459     log(QStringLiteral("No arrivals or deletions since the last time"), Common::LOG_MAILBOX_SYNC);
460     if (mailbox->syncState.exists()) {
461         // Verify that we indeed have all UIDs and not need them anymore
462 #ifndef QT_NO_DEBUG
463         for (int i = 0; i < list->m_children.size(); ++i) {
464             // FIXME: This assert can fail if the mailbox contained messages with missing UIDs even before we opened it now.
465             Q_ASSERT(static_cast<TreeItemMessage *>(list->m_children[i])->uid());
466         }
467 #endif
468     } else {
469         list->m_unreadMessageCount = 0;
470         list->m_totalMessageCount = 0;
471         list->m_numberFetchingStatus = TreeItem::DONE;
472     }
473 
474     if (list->m_children.isEmpty()) {
475         TreeItemChildrenList messages;
476         list->m_children.reserve(mailbox->syncState.exists());
477         for (uint i = 0; i < mailbox->syncState.exists(); ++i) {
478             TreeItemMessage *msg = new TreeItemMessage(list);
479             msg->m_offset = i;
480             msg->m_uid = uidMap[ i ];
481             messages << msg;
482         }
483         list->setChildren(messages);
484 
485     } else {
486         if (mailbox->syncState.exists() != static_cast<uint>(list->m_children.size())) {
487             throw CantHappen("TreeItemMsgList has wrong number of "
488                              "children, even though no change of "
489                              "message count occurred");
490         }
491     }
492 
493     list->setFetchStatus(TreeItem::DONE);
494 
495     if (mailbox->syncState.exists()) {
496         syncFlags(mailbox);
497     } else {
498         status = STATE_DONE;
499         emit model->mailboxSyncingProgress(mailboxIndex, status);
500         notifyInterestingMessages(mailbox);
501 
502         if (newArrivalsFetch.isEmpty()) {
503             mailbox->saveSyncStateAndUids(model);
504             model->changeConnectionState(parser, CONN_STATE_SELECTED);
505             _completed();
506         }
507     }
508 }
509 
syncOnlyAdditions(TreeItemMailbox * mailbox,TreeItemMsgList * list)510 void ObtainSynchronizedMailboxTask::syncOnlyAdditions(TreeItemMailbox *mailbox, TreeItemMsgList *list)
511 {
512     log(QStringLiteral("Syncing new arrivals"), Common::LOG_MAILBOX_SYNC);
513 
514     // So, we know that messages only got added to the mailbox and that none were removed,
515     // neither those that we already know or those that got added while we weren't around.
516     // Therefore we ask only for UIDs of new messages
517 
518     firstUnknownUidOffset = oldSyncState.exists();
519     list->m_numberFetchingStatus = TreeItem::LOADING;
520     uidSyncingMode = UID_SYNC_ONLY_NEW;
521     syncUids(mailbox, oldSyncState.uidNext());
522 }
523 
syncGeneric(TreeItemMailbox * mailbox,TreeItemMsgList * list)524 void ObtainSynchronizedMailboxTask::syncGeneric(TreeItemMailbox *mailbox, TreeItemMsgList *list)
525 {
526     log(QStringLiteral("generic synchronization from previous state"), Common::LOG_MAILBOX_SYNC);
527 
528     list->m_numberFetchingStatus = TreeItem::LOADING;
529     list->m_unreadMessageCount = 0;
530     uidSyncingMode = UID_SYNC_ALL;
531     syncUids(mailbox);
532 }
533 
syncUids(TreeItemMailbox * mailbox,const uint lowestUidToQuery)534 void ObtainSynchronizedMailboxTask::syncUids(TreeItemMailbox *mailbox, const uint lowestUidToQuery)
535 {
536     status = STATE_SYNCING_UIDS;
537     log(QStringLiteral("Syncing UIDs"), Common::LOG_MAILBOX_SYNC);
538     QByteArray uidSpecification;
539     if (lowestUidToQuery == 0) {
540         uidSpecification = "ALL";
541     } else {
542         uidSpecification = QStringLiteral("UID %1:*").arg(QString::number(lowestUidToQuery)).toUtf8();
543     }
544     uidMap.clear();
545     if (model->accessParser(parser).capabilities.contains(QStringLiteral("ESEARCH"))) {
546         uidSyncingCmd = parser->uidESearchUid(uidSpecification);
547     } else {
548         uidSyncingCmd = parser->uidSearchUid(uidSpecification);
549     }
550     emit model->mailboxSyncingProgress(mailboxIndex, status);
551 }
552 
syncFlags(TreeItemMailbox * mailbox)553 void ObtainSynchronizedMailboxTask::syncFlags(TreeItemMailbox *mailbox)
554 {
555     status = STATE_SYNCING_FLAGS;
556     log(QStringLiteral("Syncing flags"), Common::LOG_MAILBOX_SYNC);
557     TreeItemMsgList *list = dynamic_cast<TreeItemMsgList *>(mailbox->m_children[ 0 ]);
558     Q_ASSERT(list);
559 
560     // 0 => don't use it; >0 => use that as the old value
561     quint64 useModSeq = 0;
562     if ((model->accessParser(parser).capabilities.contains(QStringLiteral("CONDSTORE")) ||
563          model->accessParser(parser).capabilities.contains(QStringLiteral("QRESYNC"))) &&
564             oldSyncState.highestModSeq() > 0 && mailbox->syncState.isUsableForCondstore() &&
565             oldSyncState.uidValidity() == mailbox->syncState.uidValidity()) {
566         // The CONDSTORE is available, UIDVALIDITY has not changed and the HIGHESTMODSEQ suggests that
567         // it will be useful
568         if (oldSyncState.highestModSeq() == mailbox->syncState.highestModSeq()) {
569             // Looks like there were no changes in flags -- that's cool, we're done here,
570             // but only after some sanity checks
571             if (oldSyncState.exists() > mailbox->syncState.exists()) {
572                 log(QStringLiteral("Some messages have arrived to the mailbox, but HIGHESTMODSEQ hasn't changed. "
573                     "That's a bug in the server implementation."), Common::LOG_MAILBOX_SYNC);
574                 // will issue the ordinary FETCH command for FLAGS
575             } else if (oldSyncState.uidNext() != mailbox->syncState.uidNext()) {
576                 log(QStringLiteral("UIDNEXT has changed, yet HIGHESTMODSEQ remained constant; that's server's bug"), Common::LOG_MAILBOX_SYNC);
577                 // and again, don't trust that HIGHESTMODSEQ
578             } else {
579                 // According to HIGHESTMODSEQ, there hasn't been any change. UIDNEXT and EXISTS do not contradict
580                 // this interpretation, so we can go and call stuff finished.
581                 if (newArrivalsFetch.isEmpty()) {
582                     // No pending activity -> let's call it a day
583                     status = STATE_DONE;
584                     mailbox->saveSyncStateAndUids(model);
585                     model->changeConnectionState(parser, CONN_STATE_SELECTED);
586                     _completed();
587                     return;
588                 } else {
589                     // ...but there's still some pending activity; let's wait for its termination
590                     status = STATE_DONE;
591                 }
592             }
593         } else if (oldSyncState.highestModSeq() > mailbox->syncState.highestModSeq()) {
594             // Clearly a bug
595             log(QStringLiteral("HIGHESTMODSEQ decreased, that's a bug in the IMAP server"), Common::LOG_MAILBOX_SYNC);
596             // won't use HIGHESTMODSEQ
597         } else {
598             // Will use FETCH CHANGEDSINCE
599             useModSeq = oldSyncState.highestModSeq();
600         }
601     }
602     if (useModSeq > 0) {
603         QMap<QByteArray, quint64> fetchModifier;
604         fetchModifier["CHANGEDSINCE"] = oldSyncState.highestModSeq();
605         flagsCmd = parser->fetch(Sequence(1, mailbox->syncState.exists()), QStringList() << QStringLiteral("FLAGS"), fetchModifier);
606     } else {
607         flagsCmd = parser->fetch(Sequence(1, mailbox->syncState.exists()), QStringList() << QStringLiteral("FLAGS"));
608     }
609     list->m_numberFetchingStatus = TreeItem::LOADING;
610     emit model->mailboxSyncingProgress(mailboxIndex, status);
611 }
612 
handleResponseCodeInsideState(const Imap::Responses::State * const resp)613 bool ObtainSynchronizedMailboxTask::handleResponseCodeInsideState(const Imap::Responses::State *const resp)
614 {
615     if (dieIfInvalidMailbox())
616         return resp->tag.isEmpty();
617 
618     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
619     Q_ASSERT(mailbox);
620     switch (resp->respCode) {
621     case Responses::UNSEEN:
622     {
623         const Responses::RespData<uint> *const num = dynamic_cast<const Responses::RespData<uint>* const>(resp->respCodeData.data());
624         if (num) {
625             mailbox->syncState.setUnSeenOffset(num->data);
626             return resp->tag.isEmpty();
627         } else {
628             throw CantHappen("State response has invalid UNSEEN respCodeData", *resp);
629         }
630     }
631     case Responses::PERMANENTFLAGS:
632     {
633         const Responses::RespData<QStringList> *const num = dynamic_cast<const Responses::RespData<QStringList>* const>(resp->respCodeData.data());
634         if (num) {
635             mailbox->syncState.setPermanentFlags(num->data);
636             return resp->tag.isEmpty();
637         } else {
638             throw CantHappen("State response has invalid PERMANENTFLAGS respCodeData", *resp);
639         }
640     }
641     case Responses::UIDNEXT:
642     {
643         const Responses::RespData<uint> *const num = dynamic_cast<const Responses::RespData<uint>* const>(resp->respCodeData.data());
644         if (num) {
645             mailbox->syncState.setUidNext(num->data);
646             return resp->tag.isEmpty();
647         } else {
648             throw CantHappen("State response has invalid UIDNEXT respCodeData", *resp);
649         }
650     }
651     case Responses::UIDVALIDITY:
652     {
653         const Responses::RespData<uint> *const num = dynamic_cast<const Responses::RespData<uint>* const>(resp->respCodeData.data());
654         if (num) {
655             mailbox->syncState.setUidValidity(num->data);
656             return resp->tag.isEmpty();
657         } else {
658             throw CantHappen("State response has invalid UIDVALIDITY respCodeData", *resp);
659         }
660     }
661     case Responses::NOMODSEQ:
662         // NOMODSEQ means that this mailbox doesn't support CONDSTORE or QRESYNC. We have to avoid sending any fancy commands like
663         // the FETCH CHANGEDSINCE etc.
664         mailbox->syncState.setHighestModSeq(0);
665         m_usingQresync = false;
666         return resp->tag.isEmpty();
667 
668     case Responses::HIGHESTMODSEQ:
669     {
670         const Responses::RespData<quint64> *const num = dynamic_cast<const Responses::RespData<quint64>* const>(resp->respCodeData.data());
671         Q_ASSERT(num);
672         mailbox->syncState.setHighestModSeq(num->data);
673         return resp->tag.isEmpty();
674     }
675     default:
676         break;
677     }
678     return false;
679 }
680 
updateHighestKnownUid(TreeItemMailbox * mailbox,const TreeItemMsgList * list) const681 void ObtainSynchronizedMailboxTask::updateHighestKnownUid(TreeItemMailbox *mailbox, const TreeItemMsgList *list) const
682 {
683     uint highestKnownUid = 0;
684     for (int i = list->m_children.size() - 1; ! highestKnownUid && i >= 0; --i) {
685         highestKnownUid = static_cast<const TreeItemMessage *>(list->m_children[i])->uid();
686     }
687     if (highestKnownUid) {
688         // If the UID walk return a usable number, remember that and use it for updating our idea of the UIDNEXT
689         mailbox->syncState.setUidNext(qMax(mailbox->syncState.uidNext(), highestKnownUid + 1));
690     }
691 }
692 
handleNumberResponse(const Imap::Responses::NumberResponse * const resp)693 bool ObtainSynchronizedMailboxTask::handleNumberResponse(const Imap::Responses::NumberResponse *const resp)
694 {
695     if (dieIfInvalidMailbox())
696         return true;
697 
698     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
699     Q_ASSERT(mailbox);
700     TreeItemMsgList *list = dynamic_cast<TreeItemMsgList *>(mailbox->m_children[0]);
701     Q_ASSERT(list);
702     switch (resp->kind) {
703     case Imap::Responses::EXISTS:
704         switch (status) {
705         case STATE_WAIT_FOR_CONN:
706             Q_ASSERT(false);
707             return false;
708 
709         case STATE_SELECTING:
710             if (m_usingQresync) {
711                 // Because QRESYNC won't tell us anything about the new UIDs, we have to resort to this kludgy way of working.
712                 // I really, really wonder why there's no such thing like the ARRIVED to accompany VANISHED. Oh well.
713                 mailbox->syncState.setExists(resp->number);
714                 int newArrivals = resp->number - list->m_children.size();
715                 if (newArrivals > 0) {
716                     // We have to add empty messages here
717                     QModelIndex parent = list->toIndex(model);
718                     int offset = list->m_children.size();
719                     list->m_children.reserve(resp->number);
720                     model->beginInsertRows(parent, offset, resp->number - 1);
721                     for (int i = 0; i < newArrivals; ++i) {
722                         TreeItemMessage *msg = new TreeItemMessage(list);
723                         msg->m_offset = i + offset;
724                         list->m_children << msg;
725                         // yes, we really have to add this message with UID 0 :(
726                     }
727                     model->endInsertRows();
728                     list->m_totalMessageCount = resp->number;
729                 }
730             } else {
731                 // It's perfectly acceptable for the server to start its responses with EXISTS instead of UIDVALIDITY & UIDNEXT, so
732                 // we really cannot do anything besides remembering this value for later.
733                 mailbox->syncState.setExists(resp->number);
734             }
735             return true;
736 
737         case STATE_SYNCING_UIDS:
738             mailbox->handleExists(model, *resp);
739             updateHighestKnownUid(mailbox, list);
740             return true;
741 
742         case STATE_SYNCING_FLAGS:
743         case STATE_DONE:
744             if (resp->number == static_cast<uint>(list->m_children.size())) {
745                 // no changes
746                 return true;
747             }
748             mailbox->handleExists(model, *resp);
749             Q_ASSERT(list->m_children.size());
750             updateHighestKnownUid(mailbox, list);
751             CommandHandle fetchCmd = parser->uidFetch(Sequence::startingAt(
752                                                     // prevent a possible invalid 0:*
753                                                     qMax(mailbox->syncState.uidNext(), 1u)
754                                                 ), QList<QByteArray>() << "FLAGS");
755             newArrivalsFetch.append(fetchCmd);
756             return true;
757         }
758         Q_ASSERT(false);
759         return false;
760 
761     case Imap::Responses::EXPUNGE:
762 
763         if (mailbox->syncState.exists() > 0) {
764             // Always update the number of expected messages
765             mailbox->syncState.setExists(mailbox->syncState.exists() - 1);
766         }
767 
768         switch (status) {
769         case STATE_SYNCING_FLAGS:
770             // The UID mapping has been already established, but we don't have enough information for
771             // an atomic state transition yet
772             mailbox->handleExpunge(model, *resp);
773             // The SyncState and the UID map will be saved later, along with the flags, when this task finishes
774             return true;
775 
776         case STATE_DONE:
777             // The UID mapping has been already established, so we just want to handle the EXPUNGE as usual
778             mailbox->handleExpunge(model, *resp);
779             mailbox->saveSyncStateAndUids(model);
780             return true;
781 
782         default:
783             // This is handled by the code below
784             break;
785         }
786 
787         // We shall track updates to the place where the unknown UIDs resign
788         if (resp->number < firstUnknownUidOffset + 1) {
789             // The message which we're deleting has UID which is already known, ie. it isn't among those whose UIDs got requested
790             // by an incremental UID SEARCH
791             Q_ASSERT(firstUnknownUidOffset > 0);
792             --firstUnknownUidOffset;
793 
794             // The deleted message has previously been present; that means that we shall immediately signal about its expunge
795             mailbox->handleExpunge(model, *resp);
796         }
797 
798         switch(status) {
799         case STATE_WAIT_FOR_CONN:
800             Q_ASSERT(false);
801             return false;
802 
803         case STATE_SELECTING:
804             // The actual change will be handled by the UID syncing code
805             return true;
806 
807         case STATE_SYNCING_UIDS:
808             // We shouldn't delete stuff at this point, it will be handled by the UID syncing.
809             // The response shall be consumed, though.
810             return true;
811 
812         case STATE_SYNCING_FLAGS:
813         case STATE_DONE:
814             // Already handled above
815             Q_ASSERT(false);
816             return false;
817 
818         }
819 
820         break;
821     case Imap::Responses::RECENT:
822         mailbox->syncState.setRecent(resp->number);
823         list->m_recentMessageCount = resp->number;
824         return true;
825         break;
826     default:
827         throw CantHappen("Got a NumberResponse of invalid kind. This is supposed to be handled in its constructor!", *resp);
828     }
829     return false;
830 }
831 
handleVanished(const Imap::Responses::Vanished * const resp)832 bool ObtainSynchronizedMailboxTask::handleVanished(const Imap::Responses::Vanished *const resp)
833 {
834     if (dieIfInvalidMailbox())
835         return true;
836 
837     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
838     Q_ASSERT(mailbox);
839 
840     switch (status) {
841     case STATE_WAIT_FOR_CONN:
842         Q_ASSERT(false);
843         return false;
844 
845     case STATE_SELECTING:
846     case STATE_SYNCING_UIDS:
847     case STATE_SYNCING_FLAGS:
848     case STATE_DONE:
849         mailbox->handleVanished(model, *resp);
850         return true;
851     }
852 
853     Q_ASSERT(false);
854     return false;
855 }
856 
handleFlags(const Imap::Responses::Flags * const resp)857 bool ObtainSynchronizedMailboxTask::handleFlags(const Imap::Responses::Flags *const resp)
858 {
859     if (dieIfInvalidMailbox())
860         return true;
861 
862     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
863     Q_ASSERT(mailbox);
864     mailbox->syncState.setFlags(resp->flags);
865     return true;
866 }
867 
handleSearch(const Imap::Responses::Search * const resp)868 bool ObtainSynchronizedMailboxTask::handleSearch(const Imap::Responses::Search *const resp)
869 {
870     if (dieIfInvalidMailbox())
871         return true;
872 
873     if (uidSyncingCmd.isEmpty())
874         return false;
875 
876     Q_ASSERT(Model::mailboxForSomeItem(mailboxIndex));
877 
878     uidMap += resp->items;
879 
880     finalizeSearch();
881     return true;
882 }
883 
handleESearch(const Imap::Responses::ESearch * const resp)884 bool ObtainSynchronizedMailboxTask::handleESearch(const Imap::Responses::ESearch *const resp)
885 {
886     if (dieIfInvalidMailbox())
887         return true;
888 
889     if (resp->tag.isEmpty() || resp->tag != uidSyncingCmd)
890         return false;
891 
892     if (resp->seqOrUids != Imap::Responses::ESearch::UIDS)
893         throw UnexpectedResponseReceived("ESEARCH response with matching tag uses sequence numbers instead of UIDs", *resp);
894 
895     // Yes, I just love templates.
896     Responses::ESearch::CompareListDataIdentifier<Responses::ESearch::ListData_t> allComparator("ALL");
897     Responses::ESearch::ListData_t::const_iterator listIterator =
898             std::find_if(resp->listData.constBegin(), resp->listData.constEnd(), allComparator);
899 
900     if (listIterator != resp->listData.constEnd()) {
901         uidMap = listIterator->second;
902         ++listIterator;
903         if (std::find_if(listIterator, resp->listData.constEnd(), allComparator) != resp->listData.constEnd())
904             throw UnexpectedResponseReceived("ESEARCH contains the ALL key too many times", *resp);
905     } else {
906         // If the ALL key is not present, the server is telling us that there are no messages matching the query
907         uidMap.clear();
908     }
909 
910     finalizeSearch();
911     return true;
912 }
913 
handleEnabled(const Responses::Enabled * const resp)914 bool ObtainSynchronizedMailboxTask::handleEnabled(const Responses::Enabled * const resp)
915 {
916     if (dieIfInvalidMailbox())
917         return false;
918 
919     // This function is needed to work around a bug in Kolab's version of Cyrus which sometimes sends out untagged ENABLED
920     // during the SELECT processing. RFC 5161 is pretty clear in saying that ENABLED shall be sent only in response to
921     // the ENABLE command; the log submitted at https://bugs.kde.org/show_bug.cgi?id=329204#c5 shows that Trojita receives
922     // an extra * ENABLED CONDSTORE QRESYNC even after we have issued the x ENABLE QRESYNC previously and the server already
923     // replied with * ENABLED QRESYNC to that.
924     if (resp->extensions.contains("CONDSTORE") || resp->extensions.contains("QRESYNC")) {
925         return true;
926     }
927 
928     // In addition, https://bugs.kde.org/show_bug.cgi?id=350006 reports that Cyrus occasionally sends empty * ENABLED reponse.
929     // At this point we don't have many options left, so let's just eat each and every ENABLED while we select mailboxes.
930     // https://bugzilla.cyrusimap.org/show_bug.cgi?id=3898
931     if (resp->extensions.isEmpty()) {
932         return true;
933     }
934 
935     return false;
936 }
937 
938 /** @short Process the result of UID SEARCH or UID ESEARCH commands */
finalizeSearch()939 void ObtainSynchronizedMailboxTask::finalizeSearch()
940 {
941     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
942     Q_ASSERT(mailbox);
943     TreeItemMsgList *list = dynamic_cast<TreeItemMsgList*>(mailbox->m_children[0]);
944     Q_ASSERT(list);
945 
946     switch (uidSyncingMode) {
947     case UID_SYNC_ALL:
948         if (static_cast<uint>(uidMap.size()) != mailbox->syncState.exists()) {
949             // The (possibly updated) EXISTS does not match what we received for UID (E)SEARCH ALL. Please note that
950             // it's the server's responsibility to feed us with valid data; scenarios like sending out-of-order responses
951             // would clearly break this contract.
952             std::ostringstream ss;
953             ss << "Error when synchronizing all messages: server said that there are " << mailbox->syncState.exists() <<
954                   " messages, but UID (E)SEARCH ALL response contains " << uidMap.size() << " entries" << std::endl;
955             ss.flush();
956             throw MailboxException(ss.str().c_str());
957         }
958         break;
959     case UID_SYNC_ONLY_NEW:
960     {
961         // Be sure there really are some new messages
962         const int newArrivals = mailbox->syncState.exists() - firstUnknownUidOffset;
963         Q_ASSERT(newArrivals >= 0);
964 
965         if (newArrivals != uidMap.size()) {
966             std::ostringstream ss;
967             ss << "Error when synchronizing new messages: server said that there are " << mailbox->syncState.exists() <<
968                   " messages in total (" << newArrivals << " new), but UID (E)SEARCH response contains " << uidMap.size() <<
969                   " entries" << std::endl;
970             ss.flush();
971             throw MailboxException(ss.str().c_str());
972         }
973         break;
974     }
975     }
976 
977     qSort(uidMap);
978     if (!uidMap.isEmpty() && uidMap.front() == 0) {
979         throw MailboxException("UID (E)SEARCH response contains invalid UID zero");
980     }
981     applyUids(mailbox);
982     uidMap.clear();
983     updateHighestKnownUid(mailbox, list);
984     status = STATE_SYNCING_FLAGS;
985 }
986 
handleFetch(const Imap::Responses::Fetch * const resp)987 bool ObtainSynchronizedMailboxTask::handleFetch(const Imap::Responses::Fetch *const resp)
988 {
989     if (dieIfInvalidMailbox())
990         return true;
991 
992     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
993     Q_ASSERT(mailbox);
994     QList<TreeItemPart *> changedParts;
995     TreeItemMessage *changedMessage = 0;
996     mailbox->handleFetchResponse(model, *resp, changedParts, changedMessage, m_usingQresync);
997     if (changedMessage) {
998         QModelIndex index = changedMessage->toIndex(model);
999         emit model->dataChanged(index, index);
1000         if (mailbox->syncState.uidNext() <= changedMessage->uid()) {
1001             mailbox->syncState.setUidNext(changedMessage->uid() + 1);
1002         }
1003         // On the other hand, this one will be emitted at the very end
1004         // model->emitMessageCountChanged(mailbox);
1005     }
1006     if (!changedParts.isEmpty() && !m_usingQresync) {
1007         // On the other hand, with QRESYNC our code is ready to receive extra data that changes body parts...
1008         qDebug() << "Weird, FETCH when syncing has changed some body parts. We aren't ready for that.";
1009         log(QStringLiteral("This response has changed some message parts. That should not have happened, as we're still syncing."));
1010     }
1011     return true;
1012 }
1013 
1014 /** @short Apply the received UID map to the messages in mailbox
1015 
1016 The @arg firstUnknownUidOffset corresponds to the offset of a message whose UID is specified by the first item in the UID map.
1017 */
applyUids(TreeItemMailbox * mailbox)1018 void ObtainSynchronizedMailboxTask::applyUids(TreeItemMailbox *mailbox)
1019 {
1020     TreeItemMsgList *list = dynamic_cast<TreeItemMsgList *>(mailbox->m_children[0]);
1021     Q_ASSERT(list);
1022     QModelIndex parent = list->toIndex(model);
1023     list->m_children.reserve(mailbox->syncState.exists());
1024 
1025     int i = firstUnknownUidOffset;
1026     while (i < uidMap.size() + static_cast<int>(firstUnknownUidOffset)) {
1027         // Index inside the uidMap in which the UID of a message at offset i in the list->m_children can be found
1028         int uidOffset = i - firstUnknownUidOffset;
1029         Q_ASSERT(uidOffset >= 0);
1030         Q_ASSERT(uidOffset < uidMap.size());
1031 
1032         // For each UID which is really supposed to be there...
1033 
1034         Q_ASSERT(i <= list->m_children.size());
1035         if (i == list->m_children.size()) {
1036             // now we're just adding new messages to the end of the list
1037             const int futureTotalMessages = mailbox->syncState.exists();
1038             model->beginInsertRows(parent, i, futureTotalMessages - 1);
1039             for (/*nothing*/; i < futureTotalMessages; ++i) {
1040                 // Add all messages in one go
1041                 TreeItemMessage *msg = new TreeItemMessage(list);
1042                 msg->m_offset = i;
1043                 // We're iterating with i, so we got to update the uidOffset
1044                 uidOffset = i - firstUnknownUidOffset;
1045                 Q_ASSERT(uidOffset >= 0);
1046                 Q_ASSERT(uidOffset < uidMap.size());
1047                 msg->m_uid = uidMap[uidOffset];
1048                 list->m_children << msg;
1049             }
1050             model->endInsertRows();
1051             Q_ASSERT(i == list->m_children.size());
1052             Q_ASSERT(i == futureTotalMessages);
1053         } else if (static_cast<TreeItemMessage *>(list->m_children[i])->m_uid == uidMap[uidOffset]) {
1054             // If the UID of the "current message" matches, we're okay
1055             static_cast<TreeItemMessage *>(list->m_children[i])->m_offset = i;
1056             ++i;
1057         } else if (static_cast<TreeItemMessage *>(list->m_children[i])->m_uid == 0) {
1058             // If the UID of the "current message" is zero, replace that with this message
1059             TreeItemMessage *msg = static_cast<TreeItemMessage*>(list->m_children[i]);
1060             msg->m_uid = uidMap[uidOffset];
1061             msg->m_offset = i;
1062             QModelIndex idx = model->createIndex(i, 0, msg);
1063             emit model->dataChanged(idx, idx);
1064             if (msg->accessFetchStatus() == TreeItem::LOADING) {
1065                 // We've got to ask for the message metadata once again; the first attempt happened when the UID was still zero,
1066                 // so this is our chance
1067                 model->askForMsgMetadata(msg, Model::PRELOAD_PER_POLICY);
1068             }
1069             ++i;
1070         } else {
1071             // We've got an UID mismatch
1072             int pos = i;
1073             while (pos < list->m_children.size()) {
1074                 // Remove any messages which have non-zero UID which is at the same time different than the UID we want to add
1075                 // The key idea here is that IMAP guarantees that each and every new message will have greater UID than any
1076                 // other message already in the mailbox. Just for the sake of completeness, should an evil server send us a
1077                 // malformed response, we wouldn't care (or notice at this point), we'd just "needlessly" delete many "innocent"
1078                 // messages due to that one out-of-place arrival -- but we'd still remain correct and not crash.
1079                 TreeItemMessage *otherMessage = static_cast<TreeItemMessage*>(list->m_children[pos]);
1080                 if (otherMessage->m_uid != 0 && otherMessage->m_uid != uidMap[uidOffset]) {
1081                     model->cache()->clearMessage(mailbox->mailbox(), otherMessage->uid());
1082                     ++pos;
1083                 } else {
1084                     break;
1085                 }
1086             }
1087             Q_ASSERT(pos > i);
1088             model->beginRemoveRows(parent, i, pos - 1);
1089             TreeItemChildrenList removedItems = list->m_children.mid(i, pos - i);
1090             list->m_children.erase(list->m_children.begin() + i, list->m_children.begin() + pos);
1091             model->endRemoveRows();
1092             // the m_offset of all subsequent messages will be updated later, at the time *they* are processed
1093             qDeleteAll(removedItems);
1094             if (i == list->m_children.size()) {
1095                 // We're asked to add messages to the end of the list. That's something that's already implemented above,
1096                 // so let's reuse that code. That's why we do *not* want to increment the counter here.
1097             } else {
1098                 Q_ASSERT(i < list->m_children.size());
1099                 // But this case is also already implemented above, so we won't touch the counter from here, either,
1100                 // and let the existing code do its job
1101             }
1102         }
1103     }
1104 
1105     if (i != list->m_children.size()) {
1106         // remove items at the end
1107         model->beginRemoveRows(parent, i, list->m_children.size() - 1);
1108         TreeItemChildrenList removedItems = list->m_children.mid(i);
1109         list->m_children.erase(list->m_children.begin() + i, list->m_children.end());
1110         model->endRemoveRows();
1111         qDeleteAll(removedItems);
1112     }
1113 
1114     uidMap.clear();
1115 
1116     list->m_totalMessageCount = list->m_children.size();
1117     list->setFetchStatus(TreeItem::DONE);
1118 
1119     model->emitMessageCountChanged(mailbox);
1120     model->changeConnectionState(parser, CONN_STATE_SELECTED);
1121 }
1122 
debugIdentification() const1123 QString ObtainSynchronizedMailboxTask::debugIdentification() const
1124 {
1125     if (! mailboxIndex.isValid())
1126         return QStringLiteral("[invalid mailboxIndex]");
1127 
1128     TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
1129     Q_ASSERT(mailbox);
1130 
1131     QString statusStr;
1132     switch (status) {
1133     case STATE_WAIT_FOR_CONN:
1134         statusStr = QStringLiteral("STATE_WAIT_FOR_CONN");
1135         break;
1136     case STATE_SELECTING:
1137         statusStr = QStringLiteral("STATE_SELECTING");
1138         break;
1139     case STATE_SYNCING_UIDS:
1140         statusStr = QStringLiteral("STATE_SYNCING_UIDS");
1141         break;
1142     case STATE_SYNCING_FLAGS:
1143         statusStr = QStringLiteral("STATE_SYNCING_FLAGS");
1144         break;
1145     case STATE_DONE:
1146         statusStr = QStringLiteral("STATE_DONE");
1147         break;
1148     }
1149     return QStringLiteral("%1 %2").arg(statusStr, mailbox->mailbox());
1150 }
1151 
notifyInterestingMessages(TreeItemMailbox * mailbox)1152 void ObtainSynchronizedMailboxTask::notifyInterestingMessages(TreeItemMailbox *mailbox)
1153 {
1154     Q_ASSERT(mailbox);
1155     TreeItemMsgList *list = dynamic_cast<Imap::Mailbox::TreeItemMsgList *>(mailbox->m_children[0]);
1156     Q_ASSERT(list);
1157     list->recalcVariousMessageCounts(model);
1158     QModelIndex listIndex = list->toIndex(model);
1159     Q_ASSERT(listIndex.isValid());
1160     QModelIndex firstInterestingMessage = model->index(
1161                 // remember, the offset has one-based indexing
1162                 mailbox->syncState.unSeenOffset() ? mailbox->syncState.unSeenOffset() - 1 : 0, 0, listIndex);
1163     if (!firstInterestingMessage.data(RoleMessageIsMarkedRecent).toBool() &&
1164             firstInterestingMessage.data(RoleMessageIsMarkedRead).toBool()) {
1165         // Clearly the reported value is utter nonsense. Let's just scroll to the end instead
1166         int offset = model->rowCount(listIndex) - 1;
1167         log(QStringLiteral("\"First interesting message\" doesn't look terribly interesting (%1), scrolling to the end at %2 instead")
1168             .arg(firstInterestingMessage.data(RoleMessageFlags).toStringList().join(QStringLiteral(", ")),
1169                  QString::number(offset)), Common::LOG_MAILBOX_SYNC);
1170         firstInterestingMessage = model->index(offset, 0, listIndex);
1171     } else {
1172         log(QStringLiteral("First interesting message at %1 (%2)")
1173             .arg(QString::number(mailbox->syncState.unSeenOffset()),
1174                  firstInterestingMessage.data(RoleMessageFlags).toStringList().join(QStringLiteral(", "))
1175                  ), Common::LOG_MAILBOX_SYNC);
1176     }
1177     emit model->mailboxFirstUnseenMessage(mailbox->toIndex(model), firstInterestingMessage);
1178 }
1179 
dieIfInvalidMailbox()1180 bool ObtainSynchronizedMailboxTask::dieIfInvalidMailbox()
1181 {
1182     if (mailboxIndex.isValid())
1183         return false;
1184 
1185     // OK, so we are in trouble -- our mailbox has disappeared, but the IMAP server will likely keep us busy with its
1186     // status updates. This is bad, so we have to get out as fast as possible. All hands, evasive maneuvers!
1187 
1188     log(QStringLiteral("Mailbox disappeared"), Common::LOG_MAILBOX_SYNC);
1189 
1190     if (!unSelectTask) {
1191         unSelectTask = model->m_taskFactory->createUnSelectTask(model, this);
1192         connect(unSelectTask, &ImapTask::completed, this, &ObtainSynchronizedMailboxTask::slotUnSelectCompleted);
1193         unSelectTask->perform();
1194     }
1195 
1196     return true;
1197 }
1198 
slotUnSelectCompleted()1199 void ObtainSynchronizedMailboxTask::slotUnSelectCompleted()
1200 {
1201     // Now, just finish and signal a failure
1202     _failed(tr("Escaped from mailbox"));
1203 }
1204 
taskData(const int role) const1205 QVariant ObtainSynchronizedMailboxTask::taskData(const int role) const
1206 {
1207     return role == RoleTaskCompactName ? QVariant(tr("Synchronizing mailbox")) : QVariant();
1208 }
1209 
1210 /** @short Let the model know that a mailbox synchronization has failed */
signalSyncFailure(const QString & message)1211 void ObtainSynchronizedMailboxTask::signalSyncFailure(const QString &message)
1212 {
1213     if (!mailboxIndex.isValid()) {
1214         // Well, that mailbox is no longer there; perhaps this is because the list of mailboxes got replaced.
1215         // Seems that there's nothing to report here.
1216         return;
1217     }
1218 
1219     EMIT_LATER(model, mailboxSyncFailed, Q_ARG(QString, mailboxIndex.data(RoleMailboxName).toString()), Q_ARG(QString, message));
1220 }
1221 
1222 }
1223 }
1224