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