File indexing completed on 2024-06-23 05:21:15

0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
0002 
0003    This file is part of the Trojita Qt IMAP e-mail client,
0004    http://trojita.flaska.net/
0005 
0006    This program is free software; you can redistribute it and/or
0007    modify it under the terms of the GNU General Public License as
0008    published by the Free Software Foundation; either version 2 of
0009    the License or (at your option) version 3 or any later version
0010    accepted by the membership of KDE e.V. (or its successor approved
0011    by the membership of KDE e.V.), which shall act as a proxy
0012    defined in Section 14 of version 3 of the license.
0013 
0014    This program is distributed in the hope that it will be useful,
0015    but WITHOUT ANY WARRANTY; without even the implied warranty of
0016    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0017    GNU General Public License for more details.
0018 
0019    You should have received a copy of the GNU General Public License
0020    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0021 */
0022 
0023 #include "ObtainSynchronizedMailboxTask.h"
0024 #include <algorithm>
0025 #include <sstream>
0026 #include <QTimer>
0027 #include "Common/InvokeMethod.h"
0028 #include "Imap/Model/ItemRoles.h"
0029 #include "Imap/Model/MailboxTree.h"
0030 #include "Imap/Model/Model.h"
0031 #include "KeepMailboxOpenTask.h"
0032 #include "UnSelectTask.h"
0033 
0034 namespace Imap
0035 {
0036 namespace Mailbox
0037 {
0038 
0039 ObtainSynchronizedMailboxTask::ObtainSynchronizedMailboxTask(Model *model, const QModelIndex &mailboxIndex, ImapTask *parentTask,
0040         KeepMailboxOpenTask *keepTask):
0041     ImapTask(model), conn(parentTask), mailboxIndex(mailboxIndex), status(STATE_WAIT_FOR_CONN), uidSyncingMode(UID_SYNC_ALL),
0042     firstUnknownUidOffset(0), m_usingQresync(false), unSelectTask(0), keepTaskChild(keepTask)
0043 {
0044     // The Parser* is not provided by our parent task, but instead through the keepTaskChild.  The reason is simple, the parent
0045     // task might not even exist, but there's always an KeepMailboxOpenTask in the game.
0046     parser = keepTaskChild->parser;
0047     Q_ASSERT(parser);
0048     if (conn) {
0049         conn->addDependentTask(this);
0050     }
0051     CHECK_TASK_TREE
0052     addDependentTask(keepTaskChild);
0053     CHECK_TASK_TREE
0054     connect(this, &ImapTask::failed, this, &ObtainSynchronizedMailboxTask::signalSyncFailure);
0055 }
0056 
0057 void ObtainSynchronizedMailboxTask::addDependentTask(ImapTask *task)
0058 {
0059     if (!dependentTasks.isEmpty()) {
0060         throw CantHappen("Attempted to add another dependent task to an ObtainSynchronizedMailboxTask");
0061     }
0062     ImapTask::addDependentTask(task);
0063 }
0064 
0065 void ObtainSynchronizedMailboxTask::perform()
0066 {
0067     CHECK_TASK_TREE
0068     markAsActiveTask();
0069 
0070     if (_dead || _aborted) {
0071         // We're at the very start, so let's try to abort in a sane way
0072         _failed(tr("Asked to abort or die"));
0073         die(tr("Mailbox syncing dead or aborted"));
0074         return;
0075     }
0076 
0077     if (! mailboxIndex.isValid()) {
0078         // FIXME: proper error handling
0079         log(QStringLiteral("The mailbox went missing, sorry"), Common::LOG_MAILBOX_SYNC);
0080         _completed();
0081         return;
0082     }
0083 
0084     TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
0085     Q_ASSERT(mailbox);
0086     TreeItemMsgList *msgList = dynamic_cast<TreeItemMsgList *>(mailbox->m_children[0]);
0087     Q_ASSERT(msgList);
0088 
0089     msgList->setFetchStatus(TreeItem::LOADING);
0090 
0091     Q_ASSERT(model->m_parsers.contains(parser));
0092 
0093     oldSyncState = model->cache()->mailboxSyncState(mailbox->mailbox());
0094     bool hasQresync = model->accessParser(parser).capabilities.contains(QStringLiteral("QRESYNC"));
0095     if (hasQresync && oldSyncState.isUsableForCondstore()) {
0096         m_usingQresync = true;
0097         auto oldUidMap = model->cache()->uidMapping(mailbox->mailbox());
0098         if (oldUidMap.isEmpty()) {
0099             selectCmd = parser->selectQresync(mailbox->mailbox(), oldSyncState.uidValidity(),
0100                                               oldSyncState.highestModSeq());
0101         } else {
0102             Sequence knownSeq, knownUid;
0103             int i = oldUidMap.size() / 2;
0104             while (i < oldUidMap.size()) {
0105                 // Message sequence number is one-based, our indexes are zero-based
0106                 knownSeq.add(i + 1);
0107                 knownUid.add(oldUidMap[i]);
0108                 i += (oldUidMap.size() - i) / 2 + 1;
0109             }
0110             // We absolutely want to maintain a complete UID->seq mapping at all times, which is why the known-uids shall remain
0111             // empty to indicate "anything".
0112             selectCmd = parser->selectQresync(mailbox->mailbox(), oldSyncState.uidValidity(),
0113                                               oldSyncState.highestModSeq(), Sequence(), knownSeq, knownUid);
0114         }
0115     } else if (model->accessParser(parser).capabilities.contains(QStringLiteral("CONDSTORE"))) {
0116         selectCmd = parser->select(mailbox->mailbox(), QList<QByteArray>() << "CONDSTORE");
0117     } else {
0118         selectCmd = parser->select(mailbox->mailbox());
0119     }
0120     if (hasQresync && model->accessParser(parser).connState > CONN_STATE_AUTHENTICATED) {
0121         // The CLOSED response code is defined in RFC 5162. It should be sent out even if the client does not actually use
0122         // the QRESYNC extension (such as when syncing a mailbox for the first time).
0123         // There will, however, be no CLOSED if no mailbox was selected previously, of course.
0124         model->changeConnectionState(parser, CONN_STATE_SELECTING_WAIT_FOR_CLOSE);
0125     } else {
0126         model->changeConnectionState(parser, CONN_STATE_SELECTING);
0127     }
0128     mailbox->syncState = SyncState();
0129     status = STATE_SELECTING;
0130     log(QStringLiteral("Synchronizing mailbox"), Common::LOG_MAILBOX_SYNC);
0131     emit model->mailboxSyncingProgress(mailboxIndex, status);
0132 }
0133 
0134 bool ObtainSynchronizedMailboxTask::handleStateHelper(const Imap::Responses::State *const resp)
0135 {
0136     if (dieIfInvalidMailbox())
0137         return true;
0138 
0139     if (handleResponseCodeInsideState(resp))
0140         return true;
0141 
0142     if (resp->tag.isEmpty())
0143         return false;
0144 
0145     if (_dead) {
0146         _failed(tr("Asked to die"));
0147         return true;
0148     }
0149     // We absolutely have to ignore the abort() request
0150 
0151     if (resp->tag == selectCmd) {
0152 
0153         if (resp->kind == Responses::OK) {
0154             Q_ASSERT(status == STATE_SELECTING);
0155             switch (model->accessParser(parser).connState) {
0156             case CONN_STATE_SELECTING_WAIT_FOR_CLOSE:
0157                 throw UnexpectedResponseReceived("Server did not send the CLOSED response code to notify us that "
0158                                                  "the previous mailbox was successfully closed. Stopping the sync to prevent data loss.");
0159             case CONN_STATE_SELECTING:
0160                 // this is what we want
0161                 break;
0162             default:
0163                 throw UnexpectedResponseReceived("Wrong connection state -- how come that a mailbox was opened in this moment?");
0164             }
0165             finalizeSelect();
0166         } else {
0167             _failed(QLatin1String("SELECT failed: ") + resp->message);
0168             model->changeConnectionState(parser, CONN_STATE_AUTHENTICATED);
0169         }
0170         return true;
0171     } else if (resp->tag == uidSyncingCmd) {
0172 
0173         if (resp->kind == Responses::OK) {
0174             // FIXME: move the finalizeSearch() here to support working with split SEARCH reposnes -- but beware of
0175             // arrivals/expunges which happen while the UID SEARCH is in progres...
0176             log(QStringLiteral("UIDs synchronized"), Common::LOG_MAILBOX_SYNC);
0177             Q_ASSERT(status == STATE_SYNCING_FLAGS);
0178             Q_ASSERT(mailboxIndex.isValid());   // FIXME
0179             TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
0180             Q_ASSERT(mailbox);
0181             syncFlags(mailbox);
0182         } else {
0183             _failed(QLatin1String("UID syncing failed: ") + resp->message);
0184             // FIXME: UNSELECT?
0185         }
0186         return true;
0187     } else if (resp->tag == flagsCmd) {
0188 
0189         if (resp->kind == Responses::OK) {
0190             //qDebug() << "received OK for flagsCmd";
0191             Q_ASSERT(status == STATE_SYNCING_FLAGS);
0192             Q_ASSERT(mailboxIndex.isValid());
0193             TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
0194             Q_ASSERT(mailbox);
0195             status = STATE_DONE;
0196             log(QStringLiteral("Flags synchronized"), Common::LOG_MAILBOX_SYNC);
0197             notifyInterestingMessages(mailbox);
0198             flagsCmd.clear();
0199 
0200             if (newArrivalsFetch.isEmpty()) {
0201                 mailbox->saveSyncStateAndUids(model);
0202                 model->changeConnectionState(parser, CONN_STATE_SELECTED);
0203                 _completed();
0204             } else {
0205                 log(QStringLiteral("Pending new arrival fetching, not terminating yet"), Common::LOG_MAILBOX_SYNC);
0206             }
0207         } else {
0208             status = STATE_DONE;
0209             _failed(QLatin1String("Flags synchronization failed: ") + resp->message);
0210             // FIXME: UNSELECT?
0211         }
0212         emit model->mailboxSyncingProgress(mailboxIndex, status);
0213         return true;
0214     } else if (newArrivalsFetch.contains(resp->tag)) {
0215 
0216         if (resp->kind == Responses::OK) {
0217             newArrivalsFetch.removeOne(resp->tag);
0218 
0219             if (newArrivalsFetch.isEmpty() && status == STATE_DONE && flagsCmd.isEmpty()) {
0220                 Q_ASSERT(mailboxIndex.isValid());
0221                 TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
0222                 Q_ASSERT(mailbox);
0223                 mailbox->saveSyncStateAndUids(model);
0224                 model->changeConnectionState(parser, CONN_STATE_SELECTED);
0225                 _completed();
0226             }
0227         } else {
0228             _failed(QLatin1String("UID discovery of new arrivals after initial UID sync has failed: ") + resp->message);
0229             // FIXME: UNSELECT?
0230         }
0231         return true;
0232 
0233     } else {
0234         return false;
0235     }
0236 }
0237 
0238 void ObtainSynchronizedMailboxTask::finalizeSelect()
0239 {
0240     Q_ASSERT(mailboxIndex.isValid());
0241     TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
0242     Q_ASSERT(mailbox);
0243     TreeItemMsgList *list = dynamic_cast<TreeItemMsgList *>(mailbox->m_children[ 0 ]);
0244     Q_ASSERT(list);
0245 
0246     model->changeConnectionState(parser, CONN_STATE_SYNCING);
0247     const SyncState &syncState = mailbox->syncState;
0248     oldSyncState = model->cache()->mailboxSyncState(mailbox->mailbox());
0249     list->m_totalMessageCount = syncState.exists();
0250     // Note: syncState.unSeen() is the NUMBER of the first unseen message, not their count!
0251 
0252     uidMap = model->cache()->uidMapping(mailbox->mailbox());
0253 
0254     if (static_cast<uint>(uidMap.size()) != oldSyncState.exists()) {
0255 
0256         QString buf;
0257         QDebug dbg(&buf);
0258         dbg << "Inconsistent cache data, falling back to full sync (" << uidMap.size() << "in UID map," << oldSyncState.exists() <<
0259             "EXIST before)";
0260         log(buf, Common::LOG_MAILBOX_SYNC);
0261         oldSyncState.setHighestModSeq(0);
0262         fullMboxSync(mailbox, list);
0263     } else {
0264         if (syncState.isUsableForSyncing() && oldSyncState.isUsableForSyncing() && syncState.uidValidity() == oldSyncState.uidValidity()) {
0265             // Perform a nice re-sync
0266 
0267             // Check the QRESYNC support and availability
0268             if (m_usingQresync && oldSyncState.isUsableForCondstore() && syncState.isUsableForCondstore()) {
0269                 // Looks like we can use QRESYNC for fast syncing
0270                 if (oldSyncState.highestModSeq() > syncState.highestModSeq()) {
0271                     // Looks like a corrupted cache or a server's bug
0272                     log(QStringLiteral("Yuck, recycled HIGHESTMODSEQ when trying to use QRESYNC"), Common::LOG_MAILBOX_SYNC);
0273                     mailbox->syncState.setHighestModSeq(0);
0274                     model->cache()->clearAllMessages(mailbox->mailbox());
0275                     m_usingQresync = false;
0276                     fullMboxSync(mailbox, list);
0277                 } else {
0278                     if (oldSyncState.highestModSeq() == syncState.highestModSeq()) {
0279                         if (oldSyncState.exists() != syncState.exists()) {
0280                             log(QStringLiteral("Sync error: QRESYNC says no changes but EXISTS has changed"), Common::LOG_MAILBOX_SYNC);
0281                             mailbox->syncState.setHighestModSeq(0);
0282                             model->cache()->clearAllMessages(mailbox->mailbox());
0283                             m_usingQresync = false;
0284                             fullMboxSync(mailbox, list);
0285                         } else if (oldSyncState.uidNext() != syncState.uidNext()) {
0286                             log(QStringLiteral("Sync error: QRESYNC says no changes but UIDNEXT has changed"), Common::LOG_MAILBOX_SYNC);
0287                             mailbox->syncState.setHighestModSeq(0);
0288                             model->cache()->clearAllMessages(mailbox->mailbox());
0289                             m_usingQresync = false;
0290                             fullMboxSync(mailbox, list);
0291                         } else if (syncState.exists() != static_cast<uint>(list->m_children.size())) {
0292                             log(QString::fromUtf8("Sync error: constant HIGHESTMODSEQ, EXISTS says %1 messages but in fact "
0293                                                    "there are %2 when finalizing SELECT")
0294                                 .arg(QString::number(mailbox->syncState.exists()), QString::number(list->m_children.size())),
0295                                 Common::LOG_MAILBOX_SYNC);
0296                             mailbox->syncState.setHighestModSeq(0);
0297                             model->cache()->clearAllMessages(mailbox->mailbox());
0298                             m_usingQresync = false;
0299                             fullMboxSync(mailbox, list);
0300                         } else {
0301                             // This should be enough
0302                             list->setFetchStatus(TreeItem::DONE);
0303                             notifyInterestingMessages(mailbox);
0304                             mailbox->saveSyncStateAndUids(model);
0305                             model->changeConnectionState(parser, CONN_STATE_SELECTED);
0306                             _completed();
0307                         }
0308                         return;
0309                     }
0310 
0311                     if (static_cast<uint>(list->m_children.size()) != mailbox->syncState.exists()) {
0312                         log(QStringLiteral("Sync error: EXISTS says %1 messages, msgList has %2")
0313                             .arg(QString::number(mailbox->syncState.exists()), QString::number(list->m_children.size())));
0314                         mailbox->syncState.setHighestModSeq(0);
0315                         model->cache()->clearAllMessages(mailbox->mailbox());
0316                         m_usingQresync = false;
0317                         fullMboxSync(mailbox, list);
0318                         return;
0319                     }
0320 
0321 
0322                     if (oldSyncState.uidNext() < syncState.uidNext()) {
0323                         list->setFetchStatus(TreeItem::DONE);
0324                         int seqWithLowestUnknownUid = -1;
0325                         for (int i = 0; i < list->m_children.size(); ++i) {
0326                             TreeItemMessage *msg = static_cast<TreeItemMessage*>(list->m_children[i]);
0327                             if (!msg->uid()) {
0328                                 seqWithLowestUnknownUid = i;
0329                                 break;
0330                             }
0331                         }
0332                         if (seqWithLowestUnknownUid >= 0) {
0333                             // We've got some new arrivals, but unfortunately QRESYNC won't report them just yet :(
0334                             CommandHandle fetchCmd = parser->uidFetch(Sequence::startingAt(qMax(oldSyncState.uidNext(), 1u)),
0335                                                                       QList<QByteArray>() << "FLAGS");
0336                             newArrivalsFetch.append(fetchCmd);
0337                             status = STATE_DONE;
0338                         } else {
0339                             // All UIDs are known at this point, including the new arrivals, yay
0340                             notifyInterestingMessages(mailbox);
0341                             mailbox->saveSyncStateAndUids(model);
0342                             model->changeConnectionState(parser, CONN_STATE_SELECTED);
0343                             _completed();
0344                         }
0345                     } else {
0346                         // This should be enough, the server should've sent the data already
0347                         list->setFetchStatus(TreeItem::DONE);
0348                         notifyInterestingMessages(mailbox);
0349                         mailbox->saveSyncStateAndUids(model);
0350                         model->changeConnectionState(parser, CONN_STATE_SELECTED);
0351                         _completed();
0352                     }
0353                 }
0354                 return;
0355             }
0356 
0357             if (syncState.exists() == 0) {
0358                 // This is a special case, the mailbox doesn't contain any messages now.
0359                 // Let's just save ourselves some work and reuse the "smart" code in the fullMboxSync() here, it will
0360                 // do the right thing.
0361                 fullMboxSync(mailbox, list);
0362                 return;
0363             }
0364 
0365             if (syncState.uidNext() == oldSyncState.uidNext()) {
0366                 // No new messages
0367 
0368                 if (syncState.exists() == oldSyncState.exists()) {
0369                     // No deletions, either, so we resync only flag changes
0370                     syncNoNewNoDeletions(mailbox, list);
0371                 } else {
0372                     // Some messages got deleted, but there have been no additions
0373                     syncGeneric(mailbox, list);
0374                 }
0375 
0376             } else if (syncState.uidNext() > oldSyncState.uidNext()) {
0377                 // Some new messages were delivered since we checked the last time.
0378                 // There's no guarantee they are still present, though.
0379 
0380                 if (syncState.uidNext() - oldSyncState.uidNext() == syncState.exists() - oldSyncState.exists()) {
0381                     // Only some new arrivals, no deletions
0382                     syncOnlyAdditions(mailbox, list);
0383                 } else {
0384                     // Generic case; we don't know anything about which messages were deleted and which added
0385                     syncGeneric(mailbox, list);
0386                 }
0387             } else {
0388                 // The UIDNEXT has decreased while UIDVALIDITY remains the same. This is forbidden,
0389                 // so either a server's bug, or a completely invalid cache.
0390                 Q_ASSERT(syncState.uidNext() < oldSyncState.uidNext());
0391                 Q_ASSERT(syncState.uidValidity() == oldSyncState.uidValidity());
0392                 log(QStringLiteral("Yuck, UIDVALIDITY remains same but UIDNEXT decreased"), Common::LOG_MAILBOX_SYNC);
0393                 model->cache()->clearAllMessages(mailbox->mailbox());
0394                 fullMboxSync(mailbox, list);
0395             }
0396         } else if (oldSyncState.isUsableForSyncingWithoutUidNext() && syncState.isUsableForSyncingWithoutUidNext() && oldSyncState.uidValidity() == syncState.uidValidity()) {
0397             log(QStringLiteral("Did not receive UIDNEXT, but UIDVALIDITY remains same -> trying non-destructive generic sync"));
0398             syncGeneric(mailbox, list);
0399         } else {
0400             // Forget everything, do a dumb sync
0401             model->cache()->clearAllMessages(mailbox->mailbox());
0402             fullMboxSync(mailbox, list);
0403         }
0404     }
0405 }
0406 
0407 void ObtainSynchronizedMailboxTask::fullMboxSync(TreeItemMailbox *mailbox, TreeItemMsgList *list)
0408 {
0409     log(QStringLiteral("Full synchronization"), Common::LOG_MAILBOX_SYNC);
0410 
0411     QModelIndex parent = list->toIndex(model);
0412     if (! list->m_children.isEmpty()) {
0413         model->beginRemoveRows(parent, 0, list->m_children.size() - 1);
0414         auto oldItems = list->m_children;
0415         list->m_children.clear();
0416         model->endRemoveRows();
0417         qDeleteAll(oldItems);
0418     }
0419     if (mailbox->syncState.exists()) {
0420         list->m_children.reserve(mailbox->syncState.exists());
0421         model->beginInsertRows(parent, 0, mailbox->syncState.exists() - 1);
0422         for (uint i = 0; i < mailbox->syncState.exists(); ++i) {
0423             TreeItemMessage *msg = new TreeItemMessage(list);
0424             msg->m_offset = i;
0425             list->m_children << msg;
0426         }
0427         model->endInsertRows();
0428 
0429         syncUids(mailbox);
0430         list->m_numberFetchingStatus = TreeItem::LOADING;
0431         list->m_unreadMessageCount = 0;
0432     } else {
0433         // No messages, we're done here
0434         list->m_totalMessageCount = 0;
0435         list->m_unreadMessageCount = 0;
0436         list->m_numberFetchingStatus = TreeItem::DONE;
0437         list->setFetchStatus(TreeItem::DONE);
0438 
0439         // The remote mailbox is empty -> we're done now
0440         model->changeConnectionState(parser, CONN_STATE_SELECTED);
0441         status = STATE_DONE;
0442         emit model->mailboxSyncingProgress(mailboxIndex, status);
0443         notifyInterestingMessages(mailbox);
0444         mailbox->saveSyncStateAndUids(model);
0445         model->changeConnectionState(parser, CONN_STATE_SELECTED);
0446         // Take care here: this call could invalidate our index (see test coverage)
0447         _completed();
0448     }
0449     // Our mailbox might have actually been invalidated by various callbacks activated above
0450     if (mailboxIndex.isValid()) {
0451         Q_ASSERT(mailboxIndex.internalPointer() == mailbox);
0452         model->emitMessageCountChanged(mailbox);
0453     }
0454 }
0455 
0456 void ObtainSynchronizedMailboxTask::syncNoNewNoDeletions(TreeItemMailbox *mailbox, TreeItemMsgList *list)
0457 {
0458     Q_ASSERT(mailbox->syncState.exists() == static_cast<uint>(uidMap.size()));
0459     log(QStringLiteral("No arrivals or deletions since the last time"), Common::LOG_MAILBOX_SYNC);
0460     if (mailbox->syncState.exists()) {
0461         // Verify that we indeed have all UIDs and not need them anymore
0462 #ifndef QT_NO_DEBUG
0463         for (int i = 0; i < list->m_children.size(); ++i) {
0464             // FIXME: This assert can fail if the mailbox contained messages with missing UIDs even before we opened it now.
0465             Q_ASSERT(static_cast<TreeItemMessage *>(list->m_children[i])->uid());
0466         }
0467 #endif
0468     } else {
0469         list->m_unreadMessageCount = 0;
0470         list->m_totalMessageCount = 0;
0471         list->m_numberFetchingStatus = TreeItem::DONE;
0472     }
0473 
0474     if (list->m_children.isEmpty()) {
0475         TreeItemChildrenList messages;
0476         list->m_children.reserve(mailbox->syncState.exists());
0477         for (uint i = 0; i < mailbox->syncState.exists(); ++i) {
0478             TreeItemMessage *msg = new TreeItemMessage(list);
0479             msg->m_offset = i;
0480             msg->m_uid = uidMap[ i ];
0481             messages << msg;
0482         }
0483         list->setChildren(messages);
0484 
0485     } else {
0486         if (mailbox->syncState.exists() != static_cast<uint>(list->m_children.size())) {
0487             throw CantHappen("TreeItemMsgList has wrong number of "
0488                              "children, even though no change of "
0489                              "message count occurred");
0490         }
0491     }
0492 
0493     list->setFetchStatus(TreeItem::DONE);
0494 
0495     if (mailbox->syncState.exists()) {
0496         syncFlags(mailbox);
0497     } else {
0498         status = STATE_DONE;
0499         emit model->mailboxSyncingProgress(mailboxIndex, status);
0500         notifyInterestingMessages(mailbox);
0501 
0502         if (newArrivalsFetch.isEmpty()) {
0503             mailbox->saveSyncStateAndUids(model);
0504             model->changeConnectionState(parser, CONN_STATE_SELECTED);
0505             _completed();
0506         }
0507     }
0508 }
0509 
0510 void ObtainSynchronizedMailboxTask::syncOnlyAdditions(TreeItemMailbox *mailbox, TreeItemMsgList *list)
0511 {
0512     log(QStringLiteral("Syncing new arrivals"), Common::LOG_MAILBOX_SYNC);
0513 
0514     // So, we know that messages only got added to the mailbox and that none were removed,
0515     // neither those that we already know or those that got added while we weren't around.
0516     // Therefore we ask only for UIDs of new messages
0517 
0518     firstUnknownUidOffset = oldSyncState.exists();
0519     list->m_numberFetchingStatus = TreeItem::LOADING;
0520     uidSyncingMode = UID_SYNC_ONLY_NEW;
0521     syncUids(mailbox, oldSyncState.uidNext());
0522 }
0523 
0524 void ObtainSynchronizedMailboxTask::syncGeneric(TreeItemMailbox *mailbox, TreeItemMsgList *list)
0525 {
0526     log(QStringLiteral("generic synchronization from previous state"), Common::LOG_MAILBOX_SYNC);
0527 
0528     list->m_numberFetchingStatus = TreeItem::LOADING;
0529     list->m_unreadMessageCount = 0;
0530     uidSyncingMode = UID_SYNC_ALL;
0531     syncUids(mailbox);
0532 }
0533 
0534 void ObtainSynchronizedMailboxTask::syncUids(TreeItemMailbox *mailbox, const uint lowestUidToQuery)
0535 {
0536     status = STATE_SYNCING_UIDS;
0537     log(QStringLiteral("Syncing UIDs"), Common::LOG_MAILBOX_SYNC);
0538     QByteArray uidSpecification;
0539     if (lowestUidToQuery == 0) {
0540         uidSpecification = "ALL";
0541     } else {
0542         uidSpecification = QStringLiteral("UID %1:*").arg(QString::number(lowestUidToQuery)).toUtf8();
0543     }
0544     uidMap.clear();
0545     if (model->accessParser(parser).capabilities.contains(QStringLiteral("ESEARCH"))) {
0546         uidSyncingCmd = parser->uidESearchUid(uidSpecification);
0547     } else {
0548         uidSyncingCmd = parser->uidSearchUid(uidSpecification);
0549     }
0550     emit model->mailboxSyncingProgress(mailboxIndex, status);
0551 }
0552 
0553 void ObtainSynchronizedMailboxTask::syncFlags(TreeItemMailbox *mailbox)
0554 {
0555     status = STATE_SYNCING_FLAGS;
0556     log(QStringLiteral("Syncing flags"), Common::LOG_MAILBOX_SYNC);
0557     TreeItemMsgList *list = dynamic_cast<TreeItemMsgList *>(mailbox->m_children[ 0 ]);
0558     Q_ASSERT(list);
0559 
0560     // 0 => don't use it; >0 => use that as the old value
0561     quint64 useModSeq = 0;
0562     if ((model->accessParser(parser).capabilities.contains(QStringLiteral("CONDSTORE")) ||
0563          model->accessParser(parser).capabilities.contains(QStringLiteral("QRESYNC"))) &&
0564             oldSyncState.highestModSeq() > 0 && mailbox->syncState.isUsableForCondstore() &&
0565             oldSyncState.uidValidity() == mailbox->syncState.uidValidity()) {
0566         // The CONDSTORE is available, UIDVALIDITY has not changed and the HIGHESTMODSEQ suggests that
0567         // it will be useful
0568         if (oldSyncState.highestModSeq() == mailbox->syncState.highestModSeq()) {
0569             // Looks like there were no changes in flags -- that's cool, we're done here,
0570             // but only after some sanity checks
0571             if (oldSyncState.exists() > mailbox->syncState.exists()) {
0572                 log(QStringLiteral("Some messages have arrived to the mailbox, but HIGHESTMODSEQ hasn't changed. "
0573                     "That's a bug in the server implementation."), Common::LOG_MAILBOX_SYNC);
0574                 // will issue the ordinary FETCH command for FLAGS
0575             } else if (oldSyncState.uidNext() != mailbox->syncState.uidNext()) {
0576                 log(QStringLiteral("UIDNEXT has changed, yet HIGHESTMODSEQ remained constant; that's server's bug"), Common::LOG_MAILBOX_SYNC);
0577                 // and again, don't trust that HIGHESTMODSEQ
0578             } else {
0579                 // According to HIGHESTMODSEQ, there hasn't been any change. UIDNEXT and EXISTS do not contradict
0580                 // this interpretation, so we can go and call stuff finished.
0581                 if (newArrivalsFetch.isEmpty()) {
0582                     // No pending activity -> let's call it a day
0583                     status = STATE_DONE;
0584                     mailbox->saveSyncStateAndUids(model);
0585                     model->changeConnectionState(parser, CONN_STATE_SELECTED);
0586                     _completed();
0587                     return;
0588                 } else {
0589                     // ...but there's still some pending activity; let's wait for its termination
0590                     status = STATE_DONE;
0591                 }
0592             }
0593         } else if (oldSyncState.highestModSeq() > mailbox->syncState.highestModSeq()) {
0594             // Clearly a bug
0595             log(QStringLiteral("HIGHESTMODSEQ decreased, that's a bug in the IMAP server"), Common::LOG_MAILBOX_SYNC);
0596             // won't use HIGHESTMODSEQ
0597         } else {
0598             // Will use FETCH CHANGEDSINCE
0599             useModSeq = oldSyncState.highestModSeq();
0600         }
0601     }
0602     if (useModSeq > 0) {
0603         QMap<QByteArray, quint64> fetchModifier;
0604         fetchModifier["CHANGEDSINCE"] = oldSyncState.highestModSeq();
0605         flagsCmd = parser->fetch(Sequence(1, mailbox->syncState.exists()), QStringList() << QStringLiteral("FLAGS"), fetchModifier);
0606     } else {
0607         flagsCmd = parser->fetch(Sequence(1, mailbox->syncState.exists()), QStringList() << QStringLiteral("FLAGS"));
0608     }
0609     list->m_numberFetchingStatus = TreeItem::LOADING;
0610     emit model->mailboxSyncingProgress(mailboxIndex, status);
0611 }
0612 
0613 bool ObtainSynchronizedMailboxTask::handleResponseCodeInsideState(const Imap::Responses::State *const resp)
0614 {
0615     if (dieIfInvalidMailbox())
0616         return resp->tag.isEmpty();
0617 
0618     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
0619     Q_ASSERT(mailbox);
0620     switch (resp->respCode) {
0621     case Responses::UNSEEN:
0622     {
0623         const Responses::RespData<uint> *const num = dynamic_cast<const Responses::RespData<uint>* const>(resp->respCodeData.data());
0624         if (num) {
0625             mailbox->syncState.setUnSeenOffset(num->data);
0626             return resp->tag.isEmpty();
0627         } else {
0628             throw CantHappen("State response has invalid UNSEEN respCodeData", *resp);
0629         }
0630     }
0631     case Responses::PERMANENTFLAGS:
0632     {
0633         const Responses::RespData<QStringList> *const num = dynamic_cast<const Responses::RespData<QStringList>* const>(resp->respCodeData.data());
0634         if (num) {
0635             mailbox->syncState.setPermanentFlags(num->data);
0636             return resp->tag.isEmpty();
0637         } else {
0638             throw CantHappen("State response has invalid PERMANENTFLAGS respCodeData", *resp);
0639         }
0640     }
0641     case Responses::UIDNEXT:
0642     {
0643         const Responses::RespData<uint> *const num = dynamic_cast<const Responses::RespData<uint>* const>(resp->respCodeData.data());
0644         if (num) {
0645             mailbox->syncState.setUidNext(num->data);
0646             return resp->tag.isEmpty();
0647         } else {
0648             throw CantHappen("State response has invalid UIDNEXT respCodeData", *resp);
0649         }
0650     }
0651     case Responses::UIDVALIDITY:
0652     {
0653         const Responses::RespData<uint> *const num = dynamic_cast<const Responses::RespData<uint>* const>(resp->respCodeData.data());
0654         if (num) {
0655             mailbox->syncState.setUidValidity(num->data);
0656             return resp->tag.isEmpty();
0657         } else {
0658             throw CantHappen("State response has invalid UIDVALIDITY respCodeData", *resp);
0659         }
0660     }
0661     case Responses::NOMODSEQ:
0662         // NOMODSEQ means that this mailbox doesn't support CONDSTORE or QRESYNC. We have to avoid sending any fancy commands like
0663         // the FETCH CHANGEDSINCE etc.
0664         mailbox->syncState.setHighestModSeq(0);
0665         m_usingQresync = false;
0666         return resp->tag.isEmpty();
0667 
0668     case Responses::HIGHESTMODSEQ:
0669     {
0670         const Responses::RespData<quint64> *const num = dynamic_cast<const Responses::RespData<quint64>* const>(resp->respCodeData.data());
0671         Q_ASSERT(num);
0672         mailbox->syncState.setHighestModSeq(num->data);
0673         return resp->tag.isEmpty();
0674     }
0675     default:
0676         break;
0677     }
0678     return false;
0679 }
0680 
0681 void ObtainSynchronizedMailboxTask::updateHighestKnownUid(TreeItemMailbox *mailbox, const TreeItemMsgList *list) const
0682 {
0683     uint highestKnownUid = 0;
0684     for (int i = list->m_children.size() - 1; ! highestKnownUid && i >= 0; --i) {
0685         highestKnownUid = static_cast<const TreeItemMessage *>(list->m_children[i])->uid();
0686     }
0687     if (highestKnownUid) {
0688         // If the UID walk return a usable number, remember that and use it for updating our idea of the UIDNEXT
0689         mailbox->syncState.setUidNext(qMax(mailbox->syncState.uidNext(), highestKnownUid + 1));
0690     }
0691 }
0692 
0693 bool ObtainSynchronizedMailboxTask::handleNumberResponse(const Imap::Responses::NumberResponse *const resp)
0694 {
0695     if (dieIfInvalidMailbox())
0696         return true;
0697 
0698     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
0699     Q_ASSERT(mailbox);
0700     TreeItemMsgList *list = dynamic_cast<TreeItemMsgList *>(mailbox->m_children[0]);
0701     Q_ASSERT(list);
0702     switch (resp->kind) {
0703     case Imap::Responses::EXISTS:
0704         switch (status) {
0705         case STATE_WAIT_FOR_CONN:
0706             Q_ASSERT(false);
0707             return false;
0708 
0709         case STATE_SELECTING:
0710             if (m_usingQresync) {
0711                 // Because QRESYNC won't tell us anything about the new UIDs, we have to resort to this kludgy way of working.
0712                 // I really, really wonder why there's no such thing like the ARRIVED to accompany VANISHED. Oh well.
0713                 mailbox->syncState.setExists(resp->number);
0714                 int newArrivals = resp->number - list->m_children.size();
0715                 if (newArrivals > 0) {
0716                     // We have to add empty messages here
0717                     QModelIndex parent = list->toIndex(model);
0718                     int offset = list->m_children.size();
0719                     list->m_children.reserve(resp->number);
0720                     model->beginInsertRows(parent, offset, resp->number - 1);
0721                     for (int i = 0; i < newArrivals; ++i) {
0722                         TreeItemMessage *msg = new TreeItemMessage(list);
0723                         msg->m_offset = i + offset;
0724                         list->m_children << msg;
0725                         // yes, we really have to add this message with UID 0 :(
0726                     }
0727                     model->endInsertRows();
0728                     list->m_totalMessageCount = resp->number;
0729                 }
0730             } else {
0731                 // It's perfectly acceptable for the server to start its responses with EXISTS instead of UIDVALIDITY & UIDNEXT, so
0732                 // we really cannot do anything besides remembering this value for later.
0733                 mailbox->syncState.setExists(resp->number);
0734             }
0735             return true;
0736 
0737         case STATE_SYNCING_UIDS:
0738             mailbox->handleExists(model, *resp);
0739             updateHighestKnownUid(mailbox, list);
0740             return true;
0741 
0742         case STATE_SYNCING_FLAGS:
0743         case STATE_DONE:
0744             if (resp->number == static_cast<uint>(list->m_children.size())) {
0745                 // no changes
0746                 return true;
0747             }
0748             mailbox->handleExists(model, *resp);
0749             Q_ASSERT(list->m_children.size());
0750             updateHighestKnownUid(mailbox, list);
0751             CommandHandle fetchCmd = parser->uidFetch(Sequence::startingAt(
0752                                                     // prevent a possible invalid 0:*
0753                                                     qMax(mailbox->syncState.uidNext(), 1u)
0754                                                 ), QList<QByteArray>() << "FLAGS");
0755             newArrivalsFetch.append(fetchCmd);
0756             return true;
0757         }
0758         Q_ASSERT(false);
0759         return false;
0760 
0761     case Imap::Responses::EXPUNGE:
0762 
0763         if (mailbox->syncState.exists() > 0) {
0764             // Always update the number of expected messages
0765             mailbox->syncState.setExists(mailbox->syncState.exists() - 1);
0766         }
0767 
0768         switch (status) {
0769         case STATE_SYNCING_FLAGS:
0770             // The UID mapping has been already established, but we don't have enough information for
0771             // an atomic state transition yet
0772             mailbox->handleExpunge(model, *resp);
0773             // The SyncState and the UID map will be saved later, along with the flags, when this task finishes
0774             return true;
0775 
0776         case STATE_DONE:
0777             // The UID mapping has been already established, so we just want to handle the EXPUNGE as usual
0778             mailbox->handleExpunge(model, *resp);
0779             mailbox->saveSyncStateAndUids(model);
0780             return true;
0781 
0782         default:
0783             // This is handled by the code below
0784             break;
0785         }
0786 
0787         // We shall track updates to the place where the unknown UIDs resign
0788         if (resp->number < firstUnknownUidOffset + 1) {
0789             // The message which we're deleting has UID which is already known, ie. it isn't among those whose UIDs got requested
0790             // by an incremental UID SEARCH
0791             Q_ASSERT(firstUnknownUidOffset > 0);
0792             --firstUnknownUidOffset;
0793 
0794             // The deleted message has previously been present; that means that we shall immediately signal about its expunge
0795             mailbox->handleExpunge(model, *resp);
0796         }
0797 
0798         switch(status) {
0799         case STATE_WAIT_FOR_CONN:
0800             Q_ASSERT(false);
0801             return false;
0802 
0803         case STATE_SELECTING:
0804             // The actual change will be handled by the UID syncing code
0805             return true;
0806 
0807         case STATE_SYNCING_UIDS:
0808             // We shouldn't delete stuff at this point, it will be handled by the UID syncing.
0809             // The response shall be consumed, though.
0810             return true;
0811 
0812         case STATE_SYNCING_FLAGS:
0813         case STATE_DONE:
0814             // Already handled above
0815             Q_ASSERT(false);
0816             return false;
0817 
0818         }
0819 
0820         break;
0821     case Imap::Responses::RECENT:
0822         mailbox->syncState.setRecent(resp->number);
0823         list->m_recentMessageCount = resp->number;
0824         return true;
0825         break;
0826     default:
0827         throw CantHappen("Got a NumberResponse of invalid kind. This is supposed to be handled in its constructor!", *resp);
0828     }
0829     return false;
0830 }
0831 
0832 bool ObtainSynchronizedMailboxTask::handleVanished(const Imap::Responses::Vanished *const resp)
0833 {
0834     if (dieIfInvalidMailbox())
0835         return true;
0836 
0837     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
0838     Q_ASSERT(mailbox);
0839 
0840     switch (status) {
0841     case STATE_WAIT_FOR_CONN:
0842         Q_ASSERT(false);
0843         return false;
0844 
0845     case STATE_SELECTING:
0846     case STATE_SYNCING_UIDS:
0847     case STATE_SYNCING_FLAGS:
0848     case STATE_DONE:
0849         mailbox->handleVanished(model, *resp);
0850         return true;
0851     }
0852 
0853     Q_ASSERT(false);
0854     return false;
0855 }
0856 
0857 bool ObtainSynchronizedMailboxTask::handleFlags(const Imap::Responses::Flags *const resp)
0858 {
0859     if (dieIfInvalidMailbox())
0860         return true;
0861 
0862     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
0863     Q_ASSERT(mailbox);
0864     mailbox->syncState.setFlags(resp->flags);
0865     return true;
0866 }
0867 
0868 bool ObtainSynchronizedMailboxTask::handleSearch(const Imap::Responses::Search *const resp)
0869 {
0870     if (dieIfInvalidMailbox())
0871         return true;
0872 
0873     if (uidSyncingCmd.isEmpty())
0874         return false;
0875 
0876     Q_ASSERT(Model::mailboxForSomeItem(mailboxIndex));
0877 
0878     uidMap += resp->items;
0879 
0880     finalizeSearch();
0881     return true;
0882 }
0883 
0884 bool ObtainSynchronizedMailboxTask::handleESearch(const Imap::Responses::ESearch *const resp)
0885 {
0886     if (dieIfInvalidMailbox())
0887         return true;
0888 
0889     if (resp->tag.isEmpty() || resp->tag != uidSyncingCmd)
0890         return false;
0891 
0892     if (resp->seqOrUids != Imap::Responses::ESearch::UIDS)
0893         throw UnexpectedResponseReceived("ESEARCH response with matching tag uses sequence numbers instead of UIDs", *resp);
0894 
0895     auto allComparator = [](const auto &listData) { return listData.first == "ALL"; };
0896     auto listIterator = std::find_if(resp->listData.constBegin(), resp->listData.constEnd(), allComparator);
0897 
0898     if (listIterator != resp->listData.constEnd()) {
0899         uidMap = listIterator->second;
0900         ++listIterator;
0901         if (std::find_if(listIterator, resp->listData.constEnd(), allComparator) != resp->listData.constEnd())
0902             throw UnexpectedResponseReceived("ESEARCH contains the ALL key too many times", *resp);
0903     } else {
0904         // If the ALL key is not present, the server is telling us that there are no messages matching the query
0905         uidMap.clear();
0906     }
0907 
0908     finalizeSearch();
0909     return true;
0910 }
0911 
0912 bool ObtainSynchronizedMailboxTask::handleEnabled(const Responses::Enabled * const resp)
0913 {
0914     if (dieIfInvalidMailbox())
0915         return false;
0916 
0917     // This function is needed to work around a bug in Kolab's version of Cyrus which sometimes sends out untagged ENABLED
0918     // during the SELECT processing. RFC 5161 is pretty clear in saying that ENABLED shall be sent only in response to
0919     // the ENABLE command; the log submitted at https://bugs.kde.org/show_bug.cgi?id=329204#c5 shows that Trojita receives
0920     // an extra * ENABLED CONDSTORE QRESYNC even after we have issued the x ENABLE QRESYNC previously and the server already
0921     // replied with * ENABLED QRESYNC to that.
0922     if (resp->extensions.contains("CONDSTORE") || resp->extensions.contains("QRESYNC")) {
0923         return true;
0924     }
0925 
0926     // In addition, https://bugs.kde.org/show_bug.cgi?id=350006 reports that Cyrus occasionally sends empty * ENABLED reponse.
0927     // At this point we don't have many options left, so let's just eat each and every ENABLED while we select mailboxes.
0928     // https://bugzilla.cyrusimap.org/show_bug.cgi?id=3898
0929     if (resp->extensions.isEmpty()) {
0930         return true;
0931     }
0932 
0933     return false;
0934 }
0935 
0936 /** @short Process the result of UID SEARCH or UID ESEARCH commands */
0937 void ObtainSynchronizedMailboxTask::finalizeSearch()
0938 {
0939     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
0940     Q_ASSERT(mailbox);
0941     TreeItemMsgList *list = dynamic_cast<TreeItemMsgList*>(mailbox->m_children[0]);
0942     Q_ASSERT(list);
0943 
0944     switch (uidSyncingMode) {
0945     case UID_SYNC_ALL:
0946         if (static_cast<uint>(uidMap.size()) != mailbox->syncState.exists()) {
0947             // The (possibly updated) EXISTS does not match what we received for UID (E)SEARCH ALL. Please note that
0948             // it's the server's responsibility to feed us with valid data; scenarios like sending out-of-order responses
0949             // would clearly break this contract.
0950             std::ostringstream ss;
0951             ss << "Error when synchronizing all messages: server said that there are " << mailbox->syncState.exists() <<
0952                   " messages, but UID (E)SEARCH ALL response contains " << uidMap.size() << " entries" << std::endl;
0953             ss.flush();
0954             throw MailboxException(ss.str().c_str());
0955         }
0956         break;
0957     case UID_SYNC_ONLY_NEW:
0958     {
0959         // Be sure there really are some new messages
0960         const int newArrivals = mailbox->syncState.exists() - firstUnknownUidOffset;
0961         Q_ASSERT(newArrivals >= 0);
0962 
0963         if (newArrivals != uidMap.size()) {
0964             std::ostringstream ss;
0965             ss << "Error when synchronizing new messages: server said that there are " << mailbox->syncState.exists() <<
0966                   " messages in total (" << newArrivals << " new), but UID (E)SEARCH response contains " << uidMap.size() <<
0967                   " entries" << std::endl;
0968             ss.flush();
0969             throw MailboxException(ss.str().c_str());
0970         }
0971         break;
0972     }
0973     }
0974 
0975     std::sort(uidMap.begin(), uidMap.end());
0976     if (!uidMap.isEmpty() && uidMap.front() == 0) {
0977         throw MailboxException("UID (E)SEARCH response contains invalid UID zero");
0978     }
0979     applyUids(mailbox);
0980     uidMap.clear();
0981     updateHighestKnownUid(mailbox, list);
0982     status = STATE_SYNCING_FLAGS;
0983 }
0984 
0985 bool ObtainSynchronizedMailboxTask::handleFetch(const Imap::Responses::Fetch *const resp)
0986 {
0987     if (dieIfInvalidMailbox())
0988         return true;
0989 
0990     TreeItemMailbox *mailbox = Model::mailboxForSomeItem(mailboxIndex);
0991     Q_ASSERT(mailbox);
0992     QList<TreeItemPart *> changedParts;
0993     TreeItemMessage *changedMessage = 0;
0994     mailbox->handleFetchResponse(model, *resp, changedParts, changedMessage, m_usingQresync);
0995     if (changedMessage) {
0996         QModelIndex index = changedMessage->toIndex(model);
0997         emit model->dataChanged(index, index);
0998         if (mailbox->syncState.uidNext() <= changedMessage->uid()) {
0999             mailbox->syncState.setUidNext(changedMessage->uid() + 1);
1000         }
1001         // On the other hand, this one will be emitted at the very end
1002         // model->emitMessageCountChanged(mailbox);
1003     }
1004     if (!changedParts.isEmpty() && !m_usingQresync) {
1005         // On the other hand, with QRESYNC our code is ready to receive extra data that changes body parts...
1006         qDebug() << "Weird, FETCH when syncing has changed some body parts. We aren't ready for that.";
1007         log(QStringLiteral("This response has changed some message parts. That should not have happened, as we're still syncing."));
1008     }
1009     return true;
1010 }
1011 
1012 /** @short Apply the received UID map to the messages in mailbox
1013 
1014 The @arg firstUnknownUidOffset corresponds to the offset of a message whose UID is specified by the first item in the UID map.
1015 */
1016 void ObtainSynchronizedMailboxTask::applyUids(TreeItemMailbox *mailbox)
1017 {
1018     TreeItemMsgList *list = dynamic_cast<TreeItemMsgList *>(mailbox->m_children[0]);
1019     Q_ASSERT(list);
1020     QModelIndex parent = list->toIndex(model);
1021     list->m_children.reserve(mailbox->syncState.exists());
1022 
1023     int i = firstUnknownUidOffset;
1024     while (i < uidMap.size() + static_cast<int>(firstUnknownUidOffset)) {
1025         // Index inside the uidMap in which the UID of a message at offset i in the list->m_children can be found
1026         int uidOffset = i - firstUnknownUidOffset;
1027         Q_ASSERT(uidOffset >= 0);
1028         Q_ASSERT(uidOffset < uidMap.size());
1029 
1030         // For each UID which is really supposed to be there...
1031 
1032         Q_ASSERT(i <= list->m_children.size());
1033         if (i == list->m_children.size()) {
1034             // now we're just adding new messages to the end of the list
1035             const int futureTotalMessages = mailbox->syncState.exists();
1036             model->beginInsertRows(parent, i, futureTotalMessages - 1);
1037             for (/*nothing*/; i < futureTotalMessages; ++i) {
1038                 // Add all messages in one go
1039                 TreeItemMessage *msg = new TreeItemMessage(list);
1040                 msg->m_offset = i;
1041                 // We're iterating with i, so we got to update the uidOffset
1042                 uidOffset = i - firstUnknownUidOffset;
1043                 Q_ASSERT(uidOffset >= 0);
1044                 Q_ASSERT(uidOffset < uidMap.size());
1045                 msg->m_uid = uidMap[uidOffset];
1046                 list->m_children << msg;
1047             }
1048             model->endInsertRows();
1049             Q_ASSERT(i == list->m_children.size());
1050             Q_ASSERT(i == futureTotalMessages);
1051         } else if (static_cast<TreeItemMessage *>(list->m_children[i])->m_uid == uidMap[uidOffset]) {
1052             // If the UID of the "current message" matches, we're okay
1053             static_cast<TreeItemMessage *>(list->m_children[i])->m_offset = i;
1054             ++i;
1055         } else if (static_cast<TreeItemMessage *>(list->m_children[i])->m_uid == 0) {
1056             // If the UID of the "current message" is zero, replace that with this message
1057             TreeItemMessage *msg = static_cast<TreeItemMessage*>(list->m_children[i]);
1058             msg->m_uid = uidMap[uidOffset];
1059             msg->m_offset = i;
1060             QModelIndex idx = model->createIndex(i, 0, msg);
1061             emit model->dataChanged(idx, idx);
1062             if (msg->accessFetchStatus() == TreeItem::LOADING) {
1063                 // We've got to ask for the message metadata once again; the first attempt happened when the UID was still zero,
1064                 // so this is our chance
1065                 model->askForMsgMetadata(msg, Model::PRELOAD_PER_POLICY);
1066             }
1067             ++i;
1068         } else {
1069             // We've got an UID mismatch
1070             int pos = i;
1071             while (pos < list->m_children.size()) {
1072                 // Remove any messages which have non-zero UID which is at the same time different than the UID we want to add
1073                 // The key idea here is that IMAP guarantees that each and every new message will have greater UID than any
1074                 // other message already in the mailbox. Just for the sake of completeness, should an evil server send us a
1075                 // malformed response, we wouldn't care (or notice at this point), we'd just "needlessly" delete many "innocent"
1076                 // messages due to that one out-of-place arrival -- but we'd still remain correct and not crash.
1077                 TreeItemMessage *otherMessage = static_cast<TreeItemMessage*>(list->m_children[pos]);
1078                 if (otherMessage->m_uid != 0 && otherMessage->m_uid != uidMap[uidOffset]) {
1079                     model->cache()->clearMessage(mailbox->mailbox(), otherMessage->uid());
1080                     ++pos;
1081                 } else {
1082                     break;
1083                 }
1084             }
1085             Q_ASSERT(pos > i);
1086             model->beginRemoveRows(parent, i, pos - 1);
1087             TreeItemChildrenList removedItems = list->m_children.mid(i, pos - i);
1088             list->m_children.erase(list->m_children.begin() + i, list->m_children.begin() + pos);
1089             model->endRemoveRows();
1090             // the m_offset of all subsequent messages will be updated later, at the time *they* are processed
1091             qDeleteAll(removedItems);
1092             if (i == list->m_children.size()) {
1093                 // We're asked to add messages to the end of the list. That's something that's already implemented above,
1094                 // so let's reuse that code. That's why we do *not* want to increment the counter here.
1095             } else {
1096                 Q_ASSERT(i < list->m_children.size());
1097                 // But this case is also already implemented above, so we won't touch the counter from here, either,
1098                 // and let the existing code do its job
1099             }
1100         }
1101     }
1102 
1103     if (i != list->m_children.size()) {
1104         // remove items at the end
1105         model->beginRemoveRows(parent, i, list->m_children.size() - 1);
1106         TreeItemChildrenList removedItems = list->m_children.mid(i);
1107         list->m_children.erase(list->m_children.begin() + i, list->m_children.end());
1108         model->endRemoveRows();
1109         qDeleteAll(removedItems);
1110     }
1111 
1112     uidMap.clear();
1113 
1114     list->m_totalMessageCount = list->m_children.size();
1115     list->setFetchStatus(TreeItem::DONE);
1116 
1117     model->emitMessageCountChanged(mailbox);
1118     model->changeConnectionState(parser, CONN_STATE_SELECTED);
1119 }
1120 
1121 QString ObtainSynchronizedMailboxTask::debugIdentification() const
1122 {
1123     if (! mailboxIndex.isValid())
1124         return QStringLiteral("[invalid mailboxIndex]");
1125 
1126     TreeItemMailbox *mailbox = dynamic_cast<TreeItemMailbox *>(static_cast<TreeItem *>(mailboxIndex.internalPointer()));
1127     Q_ASSERT(mailbox);
1128 
1129     QString statusStr;
1130     switch (status) {
1131     case STATE_WAIT_FOR_CONN:
1132         statusStr = QStringLiteral("STATE_WAIT_FOR_CONN");
1133         break;
1134     case STATE_SELECTING:
1135         statusStr = QStringLiteral("STATE_SELECTING");
1136         break;
1137     case STATE_SYNCING_UIDS:
1138         statusStr = QStringLiteral("STATE_SYNCING_UIDS");
1139         break;
1140     case STATE_SYNCING_FLAGS:
1141         statusStr = QStringLiteral("STATE_SYNCING_FLAGS");
1142         break;
1143     case STATE_DONE:
1144         statusStr = QStringLiteral("STATE_DONE");
1145         break;
1146     }
1147     return QStringLiteral("%1 %2").arg(statusStr, mailbox->mailbox());
1148 }
1149 
1150 void ObtainSynchronizedMailboxTask::notifyInterestingMessages(TreeItemMailbox *mailbox)
1151 {
1152     Q_ASSERT(mailbox);
1153     TreeItemMsgList *list = dynamic_cast<Imap::Mailbox::TreeItemMsgList *>(mailbox->m_children[0]);
1154     Q_ASSERT(list);
1155     list->recalcVariousMessageCounts(model);
1156     QModelIndex listIndex = list->toIndex(model);
1157     Q_ASSERT(listIndex.isValid());
1158     QModelIndex firstInterestingMessage = model->index(
1159                 // remember, the offset has one-based indexing
1160                 mailbox->syncState.unSeenOffset() ? mailbox->syncState.unSeenOffset() - 1 : 0, 0, listIndex);
1161     if (!firstInterestingMessage.data(RoleMessageIsMarkedRecent).toBool() &&
1162             firstInterestingMessage.data(RoleMessageIsMarkedRead).toBool()) {
1163         // Clearly the reported value is utter nonsense. Let's just scroll to the end instead
1164         int offset = model->rowCount(listIndex) - 1;
1165         log(QStringLiteral("\"First interesting message\" doesn't look terribly interesting (%1), scrolling to the end at %2 instead")
1166             .arg(firstInterestingMessage.data(RoleMessageFlags).toStringList().join(QStringLiteral(", ")),
1167                  QString::number(offset)), Common::LOG_MAILBOX_SYNC);
1168         firstInterestingMessage = model->index(offset, 0, listIndex);
1169     } else {
1170         log(QStringLiteral("First interesting message at %1 (%2)")
1171             .arg(QString::number(mailbox->syncState.unSeenOffset()),
1172                  firstInterestingMessage.data(RoleMessageFlags).toStringList().join(QStringLiteral(", "))
1173                  ), Common::LOG_MAILBOX_SYNC);
1174     }
1175     emit model->mailboxFirstUnseenMessage(mailbox->toIndex(model), firstInterestingMessage);
1176 }
1177 
1178 bool ObtainSynchronizedMailboxTask::dieIfInvalidMailbox()
1179 {
1180     if (mailboxIndex.isValid())
1181         return false;
1182 
1183     // OK, so we are in trouble -- our mailbox has disappeared, but the IMAP server will likely keep us busy with its
1184     // status updates. This is bad, so we have to get out as fast as possible. All hands, evasive maneuvers!
1185 
1186     log(QStringLiteral("Mailbox disappeared"), Common::LOG_MAILBOX_SYNC);
1187 
1188     if (!unSelectTask) {
1189         unSelectTask = model->m_taskFactory->createUnSelectTask(model, this);
1190         connect(unSelectTask, &ImapTask::completed, this, &ObtainSynchronizedMailboxTask::slotUnSelectCompleted);
1191         unSelectTask->perform();
1192     }
1193 
1194     return true;
1195 }
1196 
1197 void ObtainSynchronizedMailboxTask::slotUnSelectCompleted()
1198 {
1199     // Now, just finish and signal a failure
1200     _failed(tr("Escaped from mailbox"));
1201 }
1202 
1203 QVariant ObtainSynchronizedMailboxTask::taskData(const int role) const
1204 {
1205     return role == RoleTaskCompactName ? QVariant(tr("Synchronizing mailbox")) : QVariant();
1206 }
1207 
1208 /** @short Let the model know that a mailbox synchronization has failed */
1209 void ObtainSynchronizedMailboxTask::signalSyncFailure(const QString &message)
1210 {
1211     if (!mailboxIndex.isValid()) {
1212         // Well, that mailbox is no longer there; perhaps this is because the list of mailboxes got replaced.
1213         // Seems that there's nothing to report here.
1214         return;
1215     }
1216 
1217     EMIT_LATER(model, mailboxSyncFailed, Q_ARG(QString, mailboxIndex.data(RoleMailboxName).toString()), Q_ARG(QString, message));
1218 }
1219 
1220 }
1221 }