diff --git a/src/Imap/Model/MailboxTree.cpp b/src/Imap/Model/MailboxTree.cpp index ae920487..38aa553c 100644 --- a/src/Imap/Model/MailboxTree.cpp +++ b/src/Imap/Model/MailboxTree.cpp @@ -1,2032 +1,2037 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include "Common/FindWithUnknown.h" #include "Common/InvokeMethod.h" #include "Common/MetaTypes.h" #include "Imap/Encoders.h" #include "Imap/Parser/Rfc5322HeaderParser.h" #include "Imap/Tasks/KeepMailboxOpenTask.h" #include "UiUtils/Formatting.h" #include "ItemRoles.h" #include "MailboxTree.h" #include "Model.h" #include "SpecialFlagNames.h" #include namespace Imap { namespace Mailbox { TreeItem::TreeItem(TreeItem *parent): m_parent(parent) { // These just have to be present in the context of TreeItem, otherwise they couldn't access the protected members static_assert(static_cast(alignof(TreeItem&)) > TreeItem::TagMask, "class TreeItem must be aligned at at least four bytes due to the FetchingState optimization"); static_assert(DONE <= TagMask, "Invalid masking for pointer tag access"); } TreeItem::~TreeItem() { qDeleteAll(m_children); } unsigned int TreeItem::childrenCount(Model *const model) { fetch(model); return m_children.size(); } TreeItem *TreeItem::child(int offset, Model *const model) { fetch(model); if (offset >= 0 && offset < m_children.size()) return m_children[ offset ]; else return 0; } int TreeItem::row() const { return parent() ? parent()->m_children.indexOf(const_cast(this)) : 0; } TreeItemChildrenList TreeItem::setChildren(const TreeItemChildrenList &items) { auto res = m_children; m_children = items; setFetchStatus(DONE); return res; } bool TreeItem::isUnavailable() const { return accessFetchStatus() == UNAVAILABLE; } unsigned int TreeItem::columnCount() { return 1; } TreeItem *TreeItem::specialColumnPtr(int row, int column) const { Q_UNUSED(row); Q_UNUSED(column); return 0; } QModelIndex TreeItem::toIndex(Model *const model) const { Q_ASSERT(model); if (this == model->m_mailboxes) return QModelIndex(); // void* != const void*, but I believe that it's safe in this context return model->createIndex(row(), 0, const_cast(this)); } TreeItemMailbox::TreeItemMailbox(TreeItem *parent): TreeItem(parent), maintainingTask(0) { m_children.prepend(new TreeItemMsgList(this)); } TreeItemMailbox::TreeItemMailbox(TreeItem *parent, Responses::List response): TreeItem(parent), m_metadata(response.mailbox, response.separator, QStringList()), maintainingTask(0) { for (QStringList::const_iterator it = response.flags.constBegin(); it != response.flags.constEnd(); ++it) m_metadata.flags.append(it->toUpper()); m_children.prepend(new TreeItemMsgList(this)); } TreeItemMailbox::~TreeItemMailbox() { if (maintainingTask) { maintainingTask->dieIfInvalidMailbox(); } } TreeItemMailbox *TreeItemMailbox::fromMetadata(TreeItem *parent, const MailboxMetadata &metadata) { TreeItemMailbox *res = new TreeItemMailbox(parent); res->m_metadata = metadata; return res; } void TreeItemMailbox::fetch(Model *const model) { fetchWithCacheControl(model, false); } void TreeItemMailbox::fetchWithCacheControl(Model *const model, bool forceReload) { if (fetched() || isUnavailable()) return; if (hasNoChildMailboxesAlreadyKnown()) { setFetchStatus(DONE); return; } if (! loading()) { setFetchStatus(LOADING); QModelIndex mailboxIndex = toIndex(model); CALL_LATER(model, askForChildrenOfMailbox, Q_ARG(QModelIndex, mailboxIndex), Q_ARG(Imap::Mailbox::CacheLoadingMode, forceReload ? LOAD_FORCE_RELOAD : LOAD_CACHED_IS_OK)); } } void TreeItemMailbox::rescanForChildMailboxes(Model *const model) { if (accessFetchStatus() != LOADING) { setFetchStatus(NONE); fetchWithCacheControl(model, true); } } unsigned int TreeItemMailbox::rowCount(Model *const model) { fetch(model); return m_children.size(); } QVariant TreeItemMailbox::data(Model *const model, int role) { switch (role) { case RoleIsFetched: return fetched(); case RoleIsUnavailable: return isUnavailable(); }; if (!parent()) return QVariant(); TreeItemMsgList *list = dynamic_cast(m_children[0]); Q_ASSERT(list); switch (role) { case Qt::DisplayRole: { // this one is used only for a dumb view attached to the Model QString res = separator().isEmpty() ? mailbox() : mailbox().split(separator(), QString::SkipEmptyParts).last(); return loading() ? res + QLatin1String(" [loading]") : res; } case RoleShortMailboxName: return separator().isEmpty() ? mailbox() : mailbox().split(separator(), QString::SkipEmptyParts).last(); case RoleMailboxName: return mailbox(); case RoleMailboxSeparator: return separator(); case RoleMailboxHasChildMailboxes: return hasChildMailboxes(model); case RoleMailboxIsINBOX: return mailbox().toUpper() == QLatin1String("INBOX"); case RoleMailboxIsSelectable: return isSelectable(); case RoleMailboxNumbersFetched: return list->numbersFetched(); case RoleTotalMessageCount: { if (! isSelectable()) return QVariant(); // At first, register that request for count int res = list->totalMessageCount(model); // ...and now that it's been sent, display a number if it's available, or if it was available before. // It's better to return a value which is maybe already obsolete than to hide a value which might // very well be still correct. return list->numbersFetched() || res != -1 ? QVariant(res) : QVariant(); } case RoleUnreadMessageCount: { if (! isSelectable()) return QVariant(); // This one is similar to the case of RoleTotalMessageCount int res = list->unreadMessageCount(model); return list->numbersFetched() || res != -1 ? QVariant(res): QVariant(); } case RoleRecentMessageCount: { if (! isSelectable()) return QVariant(); // see above int res = list->recentMessageCount(model); return list->numbersFetched() || res != -1 ? QVariant(res): QVariant(); } case RoleMailboxItemsAreLoading: return list->loading() || (isSelectable() && ! list->numbersFetched()); case RoleMailboxUidValidity: { list->fetch(model); return list->fetched() ? QVariant(syncState.uidValidity()) : QVariant(); } case RoleMailboxIsSubscribed: return QVariant::fromValue(m_metadata.flags.contains(QStringLiteral("\\SUBSCRIBED"))); default: return QVariant(); } } bool TreeItemMailbox::hasChildren(Model *const model) { Q_UNUSED(model); return true; // we have that "messages" thing built in } QLatin1String TreeItemMailbox::flagNoInferiors("\\NOINFERIORS"); QLatin1String TreeItemMailbox::flagHasNoChildren("\\HASNOCHILDREN"); QLatin1String TreeItemMailbox::flagHasChildren("\\HASCHILDREN"); bool TreeItemMailbox::hasNoChildMailboxesAlreadyKnown() { if (m_metadata.flags.contains(flagNoInferiors) || (m_metadata.flags.contains(flagHasNoChildren) && ! m_metadata.flags.contains(flagHasChildren))) return true; else return false; } bool TreeItemMailbox::hasChildMailboxes(Model *const model) { if (fetched() || isUnavailable()) { return m_children.size() > 1; } else if (hasNoChildMailboxesAlreadyKnown()) { return false; } else if (m_metadata.flags.contains(flagHasChildren) && ! m_metadata.flags.contains(flagHasNoChildren)) { return true; } else { fetch(model); return m_children.size() > 1; } } TreeItem *TreeItemMailbox::child(const int offset, Model *const model) { // accessing TreeItemMsgList doesn't need fetch() if (offset == 0) return m_children[ 0 ]; return TreeItem::child(offset, model); } TreeItemChildrenList TreeItemMailbox::setChildren(const TreeItemChildrenList &items) { // This function has to be special because we want to preserve m_children[0] TreeItemMsgList *msgList = dynamic_cast(m_children[0]); Q_ASSERT(msgList); m_children.erase(m_children.begin()); auto list = TreeItem::setChildren(items); // this also adjusts m_loading and m_fetched m_children.prepend(msgList); return list; } void TreeItemMailbox::handleFetchResponse(Model *const model, const Responses::Fetch &response, QList &changedParts, TreeItemMessage *&changedMessage, bool usingQresync) { TreeItemMsgList *list = static_cast(m_children[0]); Responses::Fetch::dataType::const_iterator uidRecord = response.data.find("UID"); // Previously, we would ignore any FETCH responses until we are fully synced. This is rather hard do to "properly", // though. // What we want to achieve is to never store data into a "wrong" message. Theoretically, we are prone to just this // in case the server sends us unsolicited data before we are fully synced. When this happens for flags, it's a pretty // harmless operation as we're going to re-fetch the flags for the concerned part of mailbox anyway (even with CONDSTORE, // and this is never an issue with QRESYNC). // It's worse when the data refer to some immutable piece of information like the bodystructure or body parts. // If that happens, then we have to actively prevent the data from being stored because we cannot know whether we would // be putting it into a correct bucket^Hmessage. bool ignoreImmutableData = !list->fetched() && uidRecord == response.data.constEnd(); int number = response.number - 1; if (number < 0 || number >= list->m_children.size()) throw UnknownMessageIndex(QStringLiteral("Got FETCH that is out of bounds -- got %1 messages").arg( QString::number(list->m_children.size())).toUtf8().constData(), response); TreeItemMessage *message = static_cast(list->child(number, model)); // At first, have a look at the response and check the UID of the message if (uidRecord != response.data.constEnd()) { uint receivedUid = static_cast&>(*(uidRecord.value())).data; if (receivedUid == 0) { throw MailboxException(QStringLiteral("Server claims that message #%1 has UID 0") .arg(QString::number(response.number)).toUtf8().constData(), response); } else if (message->uid() == receivedUid) { // That's what we expect -> do nothing } else if (message->uid() == 0) { // This is the first time we see the UID, so let's take a note message->m_uid = receivedUid; changedMessage = message; if (message->loading()) { // The Model tried to ask for data for this message. That couldn't succeeded because the UID // wasn't known at that point, so let's ask now // // FIXME: tweak this to keep a high watermark of "highest UID we requested an ENVELOPE for", // issue bulk fetches in the same manner as we do the UID FETCH (FLAGS) when discovering UIDs, // and at this place in code, only ask for the metadata when the UID is higher than the watermark. // Optionally, simply ask for the ENVELOPE etc along with the FLAGS upon new message arrivals, maybe // with some limit on the number of pending fetches. And make that dapandent on online/expensive modes. message->setFetchStatus(NONE); message->fetch(model); } if (syncState.uidNext() <= receivedUid) { // Try to guess the UIDNEXT. We have to take an educated guess here, and I believe that this approach // at least is not wrong. The server won't tell us the UIDNEXT (well, it could, but it doesn't have to), // the only way of asking for it is via STATUS which is not allowed to reference the current mailbox and // even if it was, it wouldn't be atomic. So, what could the UIDNEXT possibly be? It can't be smaller // than the UID_of_highest_message, and it can't be the same, either, so it really has to be higher. // Let's just increment it by one, this is our lower bound. // Not guessing the UIDNEXT correctly would result at decreased performance at the next sync, and we // can't really do better -> let's just set it now, along with the UID mapping. syncState.setUidNext(receivedUid + 1); list->setFetchStatus(LOADING); } } else { throw MailboxException(QStringLiteral("FETCH response: UID consistency error for message #%1 -- expected UID %2, got UID %3").arg( QString::number(response.number), QString::number(message->uid()), QString::number(receivedUid) ).toUtf8().constData(), response); } } else if (! message->uid()) { qDebug() << "FETCH: received a FETCH response for message #" << response.number << "whose UID is not yet known. This sucks."; QList uidsInMailbox; Q_FOREACH(TreeItem *node, list->m_children) { uidsInMailbox << static_cast(node)->uid(); } qDebug() << "UIDs in the mailbox now: " << uidsInMailbox; } bool updatedFlags = false; for (Responses::Fetch::dataType::const_iterator it = response.data.begin(); it != response.data.end(); ++ it) { if (it.key() == "UID") { // established above Q_ASSERT(static_cast&>(*(it.value())).data == message->uid()); } else if (it.key() == "FLAGS") { // Only emit signals when the flags have actually changed QStringList newFlags = model->normalizeFlags(static_cast&>(*(it.value())).data); bool forceChange = !message->m_flagsHandled || (message->m_flags != newFlags); message->setFlags(list, newFlags); if (forceChange) { updatedFlags = true; changedMessage = message; } } else if (it.key() == "MODSEQ") { quint64 num = static_cast&>(*(it.value())).data; if (num > syncState.highestModSeq()) { syncState.setHighestModSeq(num); if (list->accessFetchStatus() == DONE) { // This means that everything is known already, so we are by definition OK to save stuff to disk. // We can also skip rebuilding the UID map and save just the HIGHESTMODSEQ, i.e. the SyncState. model->cache()->setMailboxSyncState(mailbox(), syncState); } else { // it's already marked as dirty -> nothing to do here } } } else if (ignoreImmutableData) { QByteArray buf; QTextStream ss(&buf); ss << response; ss.flush(); qDebug() << "Ignoring FETCH response to a mailbox that isn't synced yet:" << buf; continue; } else if (it.key() == "ENVELOPE") { message->data()->setEnvelope(static_cast&>(*(it.value())).data); changedMessage = message; } else if (it.key() == "BODYSTRUCTURE") { if (message->data()->gotRemeberedBodyStructure() || message->fetched()) { // The message structure is already known, so we are free to ignore it } else { // We had no idea about the structure of the message // At first, save the bodystructure. This is needed so that our overridden rowCount() works properly. // (The rowCount() gets called through QAIM::beginInsertRows(), for example.) auto xtbIt = response.data.constFind("x-trojita-bodystructure"); Q_ASSERT(xtbIt != response.data.constEnd()); message->data()->setRememberedBodyStructure( static_cast&>(*(xtbIt.value())).data); // Now insert the children. We're of course assuming that the TreeItemMessage is now empty. auto newChildren = static_cast(*(it.value())).createTreeItems(message); Q_ASSERT(!newChildren.isEmpty()); Q_ASSERT(message->m_children.isEmpty()); QModelIndex messageIdx = message->toIndex(model); model->beginInsertRows(messageIdx, 0, newChildren.size() - 1); message->setChildren(newChildren); model->endInsertRows(); } } else if (it.key() == "x-trojita-bodystructure") { // do nothing here, it's been already taken care of from the BODYSTRUCTURE handler } else if (it.key() == "RFC822.SIZE") { message->data()->setSize(static_cast&>(*(it.value())).data); } else if (it.key().startsWith("BODY[HEADER.FIELDS (")) { // Process any headers found in any such response bit const QByteArray &rawHeaders = static_cast&>(*(it.value())).data; message->processAdditionalHeaders(model, rawHeaders); changedMessage = message; } else if (it.key().startsWith("BODY[") || it.key().startsWith("BINARY[")) { if (it.key()[ it.key().size() - 1 ] != ']') throw UnknownMessageIndex("Can't parse such BODY[]/BINARY[]", response); TreeItemPart *part = partIdToPtr(model, message, it.key()); if (! part) throw UnknownMessageIndex("Got BODY[]/BINARY[] fetch that did not resolve to any known part", response); const QByteArray &data = static_cast&>(*(it.value())).data; if (it.key().startsWith("BODY[")) { // Check whether we are supposed to be loading the raw, undecoded part as well. // The check has to be done via a direct pointer access to m_partRaw to make sure that it does not // get instantiated when not actually needed. if (part->m_partRaw && part->m_partRaw->loading()) { part->m_partRaw->m_data = data; part->m_partRaw->setFetchStatus(DONE); changedParts.append(part->m_partRaw); if (message->uid()) { model->cache()->forgetMessagePart(mailbox(), message->uid(), part->partId()); model->cache()->setMsgPart(mailbox(), message->uid(), part->partId() + ".X-RAW", data); } } // Do not overwrite the part data if we were not asked to fetch it. // One possibility is that it's already there because it was fetched before. The second option is that // we were in fact asked to only fetch the raw data and the user is not itnerested in the processed data at all. if (part->loading()) { // got to decode the part data by hand Imap::decodeContentTransferEncoding(data, part->transferEncoding(), part->dataPtr()); part->setFetchStatus(DONE); changedParts.append(part); if (message->uid() && model->cache()->messagePart(mailbox(), message->uid(), part->partId() + ".X-RAW").isNull()) { // Do not store the data into cache if the raw data are already there model->cache()->setMsgPart(mailbox(), message->uid(), part->partId(), part->m_data); } } } else { // A BINARY FETCH item is already decoded for us, yay part->m_data = data; part->setFetchStatus(DONE); changedParts.append(part); if (message->uid()) { model->cache()->setMsgPart(mailbox(), message->uid(), part->partId(), part->m_data); } } } else if (it.key() == "INTERNALDATE") { message->data()->setInternalDate(static_cast&>(*(it.value())).data); } else { qDebug() << "TreeItemMailbox::handleFetchResponse: unknown FETCH identifier" << it.key(); } } if (message->uid()) { if (message->data()->isComplete() && model->cache()->messageMetadata(mailbox(), message->uid()).uid == 0) { model->cache()->setMessageMetadata( mailbox(), message->uid(), Imap::Mailbox::AbstractCache::MessageDataBundle( message->uid(), message->data()->envelope(), message->data()->internalDate(), message->data()->size(), message->data()->rememberedBodyStructure(), message->data()->hdrReferences(), message->data()->hdrListPost(), message->data()->hdrListPostNo() )); message->setFetchStatus(DONE); } if (updatedFlags) { model->cache()->setMsgFlags(mailbox(), message->uid(), message->m_flags); } } } /** @short Save the sync state and the UID mapping into the cache Please note that FLAGS are still being updated "asynchronously", i.e. immediately when an update arrives. The motivation behind this is that both SyncState and UID mapping just absolutely have to be kept in sync due to the way they are used where our syncing code simply expects both to match each other. There cannot ever be any 0 UIDs in the saved UID mapping, and the number in EXISTS and the amount of cached UIDs is not supposed to differ or all bets are off. The flags, on the other hand, are not critical -- if a message gets saved with the correct flags "too early", i.e. before the corresponding SyncState and/or UIDs are saved, the wors case which could possibly happen are data which do not match the old state any longer. But the old state is not important anyway because it's already gone on the server. */ void TreeItemMailbox::saveSyncStateAndUids(Model * model) { TreeItemMsgList *list = dynamic_cast(m_children[0]); if (list->m_unreadMessageCount != -1) { syncState.setUnSeenCount(list->m_unreadMessageCount); } if (list->m_recentMessageCount != -1) { syncState.setRecent(list->m_recentMessageCount); } model->cache()->setMailboxSyncState(mailbox(), syncState); model->saveUidMap(list); list->setFetchStatus(DONE); } /** @short Process the EXPUNGE response when the UIDs are already synced */ void TreeItemMailbox::handleExpunge(Model *const model, const Responses::NumberResponse &resp) { Q_ASSERT(resp.kind == Responses::EXPUNGE); TreeItemMsgList *list = dynamic_cast(m_children[ 0 ]); Q_ASSERT(list); if (resp.number > static_cast(list->m_children.size()) || resp.number == 0) { throw UnknownMessageIndex("EXPUNGE references message number which is out-of-bounds"); } uint offset = resp.number - 1; model->beginRemoveRows(list->toIndex(model), offset, offset); auto it = list->m_children.begin() + offset; TreeItemMessage *message = static_cast(*it); list->m_children.erase(it); model->cache()->clearMessage(static_cast(list->parent())->mailbox(), message->uid()); for (int i = offset; i < list->m_children.size(); ++i) { --static_cast(list->m_children[i])->m_offset; } model->endRemoveRows(); --list->m_totalMessageCount; list->recalcVariousMessageCountsOnExpunge(const_cast(model), message); delete message; // The UID map is not synced at this time, though, and we defer a decision on when to do this to the context // of the task which invoked this method. The idea is that this task has a better insight for potentially // batching these changes to prevent useless hammering of the saveUidMap() etc. // Previously, the code would simetimes do this twice in a row, which is kinda suboptimal... } void TreeItemMailbox::handleVanished(Model *const model, const Responses::Vanished &resp) { TreeItemMsgList *list = dynamic_cast(m_children[ 0 ]); Q_ASSERT(list); QModelIndex listIndex = list->toIndex(model); auto uids = resp.uids; qSort(uids); // Remove duplicates -- even that garbage can be present in a perfectly valid VANISHED :( uids.erase(std::unique(uids.begin(), uids.end()), uids.end()); auto it = list->m_children.end(); while (!uids.isEmpty()) { // We have to process each UID separately because the UIDs in the mailbox are not necessarily present // in a continuous range; zeros might be present uint uid = uids.last(); uids.pop_back(); if (uid == 0) { qDebug() << "VANISHED informs about removal of UID zero..."; model->logTrace(listIndex.parent(), Common::LOG_MAILBOX_SYNC, QStringLiteral("TreeItemMailbox::handleVanished"), QStringLiteral("VANISHED contains UID zero for increased fun")); break; } if (list->m_children.isEmpty()) { // Well, it'd be cool to throw an exception here but VANISHED is free to contain references to UIDs which are not here // at all... qDebug() << "VANISHED attempted to remove too many messages"; model->logTrace(listIndex.parent(), Common::LOG_MAILBOX_SYNC, QStringLiteral("TreeItemMailbox::handleVanished"), QStringLiteral("VANISHED attempted to remove too many messages")); break; } // Find a highest message with UID zero such as no message with non-zero UID higher than the current UID exists // at a position after the target message it = model->findMessageOrNextOneByUid(list, uid); if (it == list->m_children.end()) { // this is a legitimate situation, the UID of the last message in the mailbox which is getting expunged right now // could very well be not know at this point --it; } // there's a special case above guarding against an empty list Q_ASSERT(it >= list->m_children.begin()); TreeItemMessage *msgCandidate = static_cast(*it); if (msgCandidate->uid() == uid) { // will be deleted } else if (resp.earlier == Responses::Vanished::EARLIER) { // We don't have any such UID in our UID mapping, so we can safely ignore this one continue; } else if (msgCandidate->uid() == 0) { // will be deleted } else { if (it != list->m_children.begin()) { --it; msgCandidate = static_cast(*it); if (msgCandidate->uid() == 0) { // will be deleted } else { // VANISHED is free to refer to a non-existing UID... QString str; QTextStream ss(&str); ss << "VANISHED refers to UID " << uid << " which wasn't found in the mailbox (found adjacent UIDs " << msgCandidate->uid() << " and " << static_cast(*(it + 1))->uid() << " with " << static_cast(*(list->m_children.end() - 1))->uid() << " at the end)"; ss.flush(); qDebug() << str.toUtf8().constData(); model->logTrace(listIndex.parent(), Common::LOG_MAILBOX_SYNC, QStringLiteral("TreeItemMailbox::handleVanished"), str); continue; } } else { // Again, VANISHED can refer to non-existing UIDs QString str; QTextStream ss(&str); ss << "VANISHED refers to UID " << uid << " which is too low (lowest UID is " << static_cast(list->m_children.front())->uid() << ")"; ss.flush(); qDebug() << str.toUtf8().constData(); model->logTrace(listIndex.parent(), Common::LOG_MAILBOX_SYNC, QStringLiteral("TreeItemMailbox::handleVanished"), str); continue; } } int row = msgCandidate->row(); Q_ASSERT(row == it - list->m_children.begin()); model->beginRemoveRows(listIndex, row, row); it = list->m_children.erase(it); for (auto furtherMessage = it; furtherMessage != list->m_children.end(); ++furtherMessage) { --static_cast(*furtherMessage)->m_offset; } model->endRemoveRows(); if (syncState.uidNext() <= uid) { // We're informed about a message being deleted; this means that that UID must have been in the mailbox for some // (possibly tiny) time and we can therefore use it to get an idea about the UIDNEXT syncState.setUidNext(uid + 1); } model->cache()->clearMessage(mailbox(), uid); delete msgCandidate; } if (resp.earlier == Responses::Vanished::EARLIER && static_cast(list->m_children.size()) < syncState.exists()) { // Okay, there were some new arrivals which we failed to take into account because we had processed EXISTS // before VANISHED (EARLIER). That means that we have to add some of that messages back right now. int newArrivals = syncState.exists() - list->m_children.size(); Q_ASSERT(newArrivals > 0); QModelIndex parent = list->toIndex(model); int offset = list->m_children.size(); model->beginInsertRows(parent, offset, syncState.exists() - 1); for (int i = 0; i < newArrivals; ++i) { TreeItemMessage *msg = new TreeItemMessage(list); msg->m_offset = i + offset; list->m_children << msg; // yes, we really have to add this message with UID 0 :( } model->endInsertRows(); } list->m_totalMessageCount = list->m_children.size(); syncState.setExists(list->m_totalMessageCount); list->recalcVariousMessageCounts(const_cast(model)); if (list->accessFetchStatus() == DONE) { // Previously, we were synced, so we got to save this update saveSyncStateAndUids(model); } } /** @short Process the EXISTS response This function assumes that the mailbox is already synced. */ void TreeItemMailbox::handleExists(Model *const model, const Responses::NumberResponse &resp) { Q_ASSERT(resp.kind == Responses::EXISTS); TreeItemMsgList *list = dynamic_cast(m_children[0]); Q_ASSERT(list); // This is a bit tricky -- unfortunately, we can't assume anything about the UID of new arrivals. On the other hand, // these messages can be referenced by (even unrequested) FETCH responses and deleted by EXPUNGE, so we really want // to add them to the tree. int newArrivals = resp.number - list->m_children.size(); if (newArrivals < 0) { throw UnexpectedResponseReceived("EXISTS response attempted to decrease number of messages", resp); } syncState.setExists(resp.number); if (newArrivals == 0) { // remains unchanged... return; } QModelIndex parent = list->toIndex(model); int offset = list->m_children.size(); model->beginInsertRows(parent, offset, resp.number - 1); for (int i = 0; i < newArrivals; ++i) { TreeItemMessage *msg = new TreeItemMessage(list); msg->m_offset = i + offset; list->m_children << msg; // yes, we really have to add this message with UID 0 :( } model->endInsertRows(); list->m_totalMessageCount = resp.number; list->setFetchStatus(LOADING); model->emitMessageCountChanged(this); } TreeItemPart *TreeItemMailbox::partIdToPtr(Model *const model, TreeItemMessage *message, const QByteArray &msgId) { QByteArray partIdentification; if (msgId.startsWith("BODY[")) { partIdentification = msgId.mid(5, msgId.size() - 6); } else if (msgId.startsWith("BODY.PEEK[")) { partIdentification = msgId.mid(10, msgId.size() - 11); } else if (msgId.startsWith("BINARY.PEEK[")) { partIdentification = msgId.mid(12, msgId.size() - 13); } else if (msgId.startsWith("BINARY[")) { partIdentification = msgId.mid(7, msgId.size() - 8); } else { throw UnknownMessageIndex(QByteArray("Fetch identifier doesn't start with reasonable prefix: " + msgId).constData()); } TreeItem *item = message; Q_ASSERT(item); QList separated = partIdentification.split('.'); for (QList::const_iterator it = separated.constBegin(); it != separated.constEnd(); ++it) { bool ok; uint number = it->toUInt(&ok); if (!ok) { // It isn't a number, so let's check for that special modifiers if (it + 1 != separated.constEnd()) { // If it isn't at the very end, it's an error throw UnknownMessageIndex(QByteArray("Part offset contains non-numeric identifiers in the middle: " + msgId).constData()); } // Recognize the valid modifiers if (*it == "HEADER") item = item->specialColumnPtr(0, OFFSET_HEADER); else if (*it == "TEXT") item = item->specialColumnPtr(0, OFFSET_TEXT); else if (*it == "MIME") item = item->specialColumnPtr(0, OFFSET_MIME); else throw UnknownMessageIndex(QByteArray("Can't translate received offset of the message part to a number: " + msgId).constData()); break; } // Normal path: descending down and finding the correct part TreeItemPart *part = dynamic_cast(item->child(0, model)); if (part && part->isTopLevelMultiPart()) item = part; item = item->child(number - 1, model); if (! item) { throw UnknownMessageIndex(QStringLiteral( "Offset of the message part not found: message %1 (UID %2), current number %3, full identification %4") .arg(QString::number(message->row()), QString::number(message->uid()), QString::number(number), QString::fromUtf8(msgId)).toUtf8().constData()); } } TreeItemPart *part = dynamic_cast(item); return part; } bool TreeItemMailbox::isSelectable() const { return !m_metadata.flags.contains(QStringLiteral("\\NOSELECT")) && !m_metadata.flags.contains(QStringLiteral("\\NONEXISTENT")); } TreeItemMsgList::TreeItemMsgList(TreeItem *parent): TreeItem(parent), m_numberFetchingStatus(NONE), m_totalMessageCount(-1), m_unreadMessageCount(-1), m_recentMessageCount(-1) { if (!parent->parent()) setFetchStatus(DONE); } void TreeItemMsgList::fetch(Model *const model) { if (fetched() || isUnavailable()) return; if (!loading()) { setFetchStatus(LOADING); // We can't ask right now, has to wait till the end of the event loop CALL_LATER(model, askForMessagesInMailbox, Q_ARG(QModelIndex, toIndex(model))); } } void TreeItemMsgList::fetchNumbers(Model *const model) { if (m_numberFetchingStatus == NONE) { m_numberFetchingStatus = LOADING; model->askForNumberOfMessages(this); } } unsigned int TreeItemMsgList::rowCount(Model *const model) { return childrenCount(model); } QVariant TreeItemMsgList::data(Model *const model, int role) { if (role == RoleIsFetched) return fetched(); if (role == RoleIsUnavailable) return isUnavailable(); if (role != Qt::DisplayRole) return QVariant(); if (!parent()) return QVariant(); if (loading()) return QLatin1String("[loading messages...]"); if (isUnavailable()) return QLatin1String("[offline]"); if (fetched()) return hasChildren(model) ? QStringLiteral("[%1 messages]").arg(childrenCount(model)) : QStringLiteral("[no messages]"); return QLatin1String("[messages?]"); } bool TreeItemMsgList::hasChildren(Model *const model) { Q_UNUSED(model); return true; // we can easily wait here } int TreeItemMsgList::totalMessageCount(Model *const model) { // Yes, the numbers can be accommodated by a full mailbox sync, but that's not really what we shall do from this context. // Because we want to allow the old-school polling for message numbers, we have to look just at the numberFetched() state. if (!numbersFetched()) fetchNumbers(model); return m_totalMessageCount; } int TreeItemMsgList::unreadMessageCount(Model *const model) { // See totalMessageCount() if (!numbersFetched()) fetchNumbers(model); return m_unreadMessageCount; } int TreeItemMsgList::recentMessageCount(Model *const model) { // See totalMessageCount() if (!numbersFetched()) fetchNumbers(model); return m_recentMessageCount; } void TreeItemMsgList::recalcVariousMessageCounts(Model *model) { m_unreadMessageCount = 0; m_recentMessageCount = 0; for (int i = 0; i < m_children.size(); ++i) { TreeItemMessage *message = static_cast(m_children[i]); bool isRead, isRecent; message->checkFlagsReadRecent(isRead, isRecent); if (!message->m_flagsHandled) message->m_wasUnread = ! isRead; message->m_flagsHandled = true; if (!isRead) ++m_unreadMessageCount; if (isRecent) ++m_recentMessageCount; } m_totalMessageCount = m_children.size(); m_numberFetchingStatus = DONE; model->emitMessageCountChanged(static_cast(parent())); } void TreeItemMsgList::recalcVariousMessageCountsOnExpunge(Model *model, TreeItemMessage *expungedMessage) { if (m_numberFetchingStatus != DONE) { // In case the counts weren't synced before, we cannot really rely on them now -> go to the slow path recalcVariousMessageCounts(model); return; } bool isRead, isRecent; expungedMessage->checkFlagsReadRecent(isRead, isRecent); if (expungedMessage->m_flagsHandled) { if (!isRead) --m_unreadMessageCount; if (isRecent) --m_recentMessageCount; } model->emitMessageCountChanged(static_cast(parent())); } void TreeItemMsgList::resetWasUnreadState() { for (int i = 0; i < m_children.size(); ++i) { TreeItemMessage *message = static_cast(m_children[i]); message->m_wasUnread = ! message->isMarkedAsRead(); } } bool TreeItemMsgList::numbersFetched() const { return m_numberFetchingStatus == DONE; } MessageDataPayload::MessageDataPayload() : m_size(0) , m_hdrListPostNo(false) , m_partHeader(nullptr) , m_partText(nullptr) , m_gotEnvelope(false) , m_gotInternalDate(false) , m_gotSize(false) , m_gotBodystructure(false) , m_gotHdrReferences(false) , m_gotHdrListPost(false) { } bool MessageDataPayload::isComplete() const { return m_gotEnvelope && m_gotInternalDate && m_gotSize && m_gotBodystructure; } bool MessageDataPayload::gotEnvelope() const { return m_gotEnvelope; } bool MessageDataPayload::gotInternalDate() const { return m_gotInternalDate; } bool MessageDataPayload::gotSize() const { return m_gotSize; } bool MessageDataPayload::gotHdrReferences() const { return m_gotHdrReferences; } bool MessageDataPayload::gotHdrListPost() const { return m_gotHdrListPost; } const Message::Envelope &MessageDataPayload::envelope() const { return m_envelope; } void MessageDataPayload::setEnvelope(const Message::Envelope &envelope) { m_envelope = envelope; m_gotEnvelope = true; } const QDateTime &MessageDataPayload::internalDate() const { return m_internalDate; } void MessageDataPayload::setInternalDate(const QDateTime &internalDate) { m_internalDate = internalDate; m_gotInternalDate = true; } quint64 MessageDataPayload::size() const { return m_size; } void MessageDataPayload::setSize(quint64 size) { m_size = size; m_gotSize = true; } const QList &MessageDataPayload::hdrReferences() const { return m_hdrReferences; } void MessageDataPayload::setHdrReferences(const QList &hdrReferences) { m_hdrReferences = hdrReferences; m_gotHdrReferences = true; } const QList &MessageDataPayload::hdrListPost() const { return m_hdrListPost; } bool MessageDataPayload::hdrListPostNo() const { return m_hdrListPostNo; } void MessageDataPayload::setHdrListPost(const QList &hdrListPost) { m_hdrListPost = hdrListPost; m_gotHdrListPost = true; } void MessageDataPayload::setHdrListPostNo(const bool hdrListPostNo) { m_hdrListPostNo = hdrListPostNo; m_gotHdrListPost = true; } const QByteArray &MessageDataPayload::rememberedBodyStructure() const { return m_rememberedBodyStructure; } void MessageDataPayload::setRememberedBodyStructure(const QByteArray &blob) { m_rememberedBodyStructure = blob; m_gotBodystructure = true; } bool MessageDataPayload::gotRemeberedBodyStructure() const { return m_gotBodystructure; } TreeItemPart *MessageDataPayload::partHeader() const { return m_partHeader.get(); } void MessageDataPayload::setPartHeader(std::unique_ptr part) { m_partHeader = std::move(part); } TreeItemPart *MessageDataPayload::partText() const { return m_partText.get(); } void MessageDataPayload::setPartText(std::unique_ptr part) { m_partText = std::move(part); } TreeItemMessage::TreeItemMessage(TreeItem *parent): TreeItem(parent), m_offset(-1), m_uid(0), m_data(0), m_flagsHandled(false), m_wasUnread(false) { } TreeItemMessage::~TreeItemMessage() { delete m_data; } void TreeItemMessage::fetch(Model *const model) { if (fetched() || loading() || isUnavailable()) return; if (m_uid) { // Message UID is already known, which means that we can request data for this message model->askForMsgMetadata(this, Model::PRELOAD_PER_POLICY); } else { // The UID is not known yet, so we can't initiate a UID FETCH at this point. However, we mark // this message as "loading", which has the side effect that it will get re-fetched as soon as // the UID arrives -- see TreeItemMailbox::handleFetchResponse(), the section which deals with // setting previously unknown UIDs, and the similar code in ObtainSynchronizedMailboxTask. // // Even though this breaks the message preload done in Model::_askForMsgmetadata, chances are that // the UIDs will arrive rather soon for all of the pending messages, and the request for metadata // will therefore get queued roughly at the same time. This gives the KeepMailboxOpenTask a chance // to group similar requests together. To reiterate: // - Messages are attempted to get *preloaded* (ie. requesting metadata even for messages that are not // yet shown) as usual; this could fail because the UIDs might not be known yet. // - The actual FETCH could be batched by the KeepMailboxOpenTask anyway // - Hence, this should be still pretty fast and efficient setFetchStatus(LOADING); } } unsigned int TreeItemMessage::rowCount(Model *const model) { if (!data()->gotRemeberedBodyStructure()) { fetch(model); } return m_children.size(); } TreeItemChildrenList TreeItemMessage::setChildren(const TreeItemChildrenList &items) { auto origStatus = accessFetchStatus(); auto res = TreeItem::setChildren(items); setFetchStatus(origStatus); return res; } unsigned int TreeItemMessage::columnCount() { static_assert(OFFSET_HEADER < OFFSET_TEXT && OFFSET_MIME == OFFSET_TEXT + 1, "We need column 0 for regular children and columns 1 and 2 for OFFSET_HEADER and OFFSET_TEXT."); // Oh, and std::max is not constexpr in C++11. return OFFSET_MIME; } TreeItem *TreeItemMessage::specialColumnPtr(int row, int column) const { // This is a nasty one -- we have an irregular shape... // No extra columns on other rows if (row != 0) return 0; switch (column) { case OFFSET_TEXT: if (!data()->partText()) { data()->setPartText(std::unique_ptr(new TreeItemModifiedPart(const_cast(this), OFFSET_TEXT))); } return data()->partText(); case OFFSET_HEADER: if (!data()->partHeader()) { data()->setPartHeader(std::unique_ptr(new TreeItemModifiedPart(const_cast(this), OFFSET_HEADER))); } return data()->partHeader(); default: return 0; } } int TreeItemMessage::row() const { Q_ASSERT(m_offset != -1); return m_offset; } QVariant TreeItemMessage::data(Model *const model, int role) { if (!parent()) return QVariant(); // Special item roles which should not trigger fetching of message metadata switch (role) { case RoleMessageUid: return m_uid ? QVariant(m_uid) : QVariant(); case RoleIsFetched: return fetched(); case RoleIsUnavailable: return isUnavailable(); case RoleMessageFlags: // The flags are already sorted by Model::normalizeFlags() return m_flags; case RoleMessageIsMarkedDeleted: return isMarkedAsDeleted(); case RoleMessageIsMarkedRead: return isMarkedAsRead(); case RoleMessageIsMarkedForwarded: return isMarkedAsForwarded(); case RoleMessageIsMarkedReplied: return isMarkedAsReplied(); case RoleMessageIsMarkedRecent: return isMarkedAsRecent(); case RoleMessageIsMarkedFlagged: return isMarkedAsFlagged(); case RoleMessageIsMarkedJunk: return isMarkedAsJunk(); case RoleMessageIsMarkedNotJunk: return isMarkedAsNotJunk(); case RoleMessageFuzzyDate: { // When the QML ListView is configured with its section.* properties, it will call the corresponding data() section *very* // often. The data are however only "needed" when the real items are visible, and when they are visible, the data() will // get called anyway and the correct stuff will ultimately arrive. This is why we don't call fetch() from here. // // FIXME: double-check the above once again! Maybe it was just a side effect of too fast updates of the currentIndex? if (!fetched()) { return QVariant(); } QDateTime timestamp = envelope(model).date; if (!timestamp.isValid()) return QString(); if (timestamp.date() == QDate::currentDate()) return Model::tr("Today"); int beforeDays = timestamp.date().daysTo(QDate::currentDate()); if (beforeDays >= 0 && beforeDays < 7) return Model::tr("Last Week"); return QDate(timestamp.date().year(), timestamp.date().month(), 1).toString( Model::tr("MMMM yyyy", "The format specifiers (yyyy, etc) must not be translated, " "but their order can be changed to follow the local conventions. " "For valid specifiers see http://doc.qt.io/qt-5/qdate.html#toString")); } case RoleMessageWasUnread: return m_wasUnread; case RoleThreadRootWithUnreadMessages: // This one doesn't really make much sense here, but we do want to catch it to prevent a fetch request from this context qDebug() << "Warning: asked for RoleThreadRootWithUnreadMessages on TreeItemMessage. This does not make sense."; return QVariant(); case RoleMailboxName: case RoleMailboxUidValidity: return parent()->parent()->data(model, role); case RolePartMimeType: return QByteArrayLiteral("message/rfc822"); case RoleIMAPRelativeUrl: if (m_uid) { return QByteArray("/" + QUrl::toPercentEncoding(data(model, RoleMailboxName).toString()) + ";UIDVALIDITY=" + data(model, RoleMailboxUidValidity).toByteArray() + "/;UID=" + QByteArray::number(m_uid)); } else { return QVariant(); } } // Any other roles will result in fetching the data; however, we won't exit if the data isn't available yet fetch(model); switch (role) { case Qt::DisplayRole: if (loading()) { return QStringLiteral("[loading UID %1...]").arg(QString::number(uid())); } else if (isUnavailable()) { return QStringLiteral("[offline UID %1]").arg(QString::number(uid())); } else { return QStringLiteral("UID %1: %2").arg(QString::number(uid()), data()->envelope().subject); } case Qt::ToolTipRole: if (fetched()) { QString buf; QTextStream stream(&buf); stream << data()->envelope(); return UiUtils::Formatting::htmlEscaped(buf); } else { return QVariant(); } case RoleMessageSize: return data()->gotSize() ? QVariant::fromValue(data()->size()) : QVariant(); case RoleMessageInternalDate: return data()->gotInternalDate() ? data()->internalDate() : QVariant(); case RoleMessageHeaderReferences: return data()->gotHdrReferences() ? QVariant::fromValue(data()->hdrReferences()) : QVariant(); case RoleMessageHeaderListPost: if (data()->gotHdrListPost()) { QVariantList res; Q_FOREACH(const QUrl &url, data()->hdrListPost()) res << url; return res; } else { return QVariant(); } case RoleMessageHeaderListPostNo: return data()->gotHdrListPost() ? QVariant(data()->hdrListPostNo()) : QVariant(); } if (data()->gotEnvelope()) { // If the envelope is already available, we might be able to deliver some bits even prior to full fetch being done. // // Only those values which need ENVELOPE go here! switch (role) { case RoleMessageSubject: return data()->gotEnvelope() ? QVariant(data()->envelope().subject) : QVariant(); case RoleMessageDate: return data()->gotEnvelope() ? envelope(model).date : QVariant(); case RoleMessageFrom: return addresListToQVariant(envelope(model).from); case RoleMessageTo: return addresListToQVariant(envelope(model).to); case RoleMessageCc: return addresListToQVariant(envelope(model).cc); case RoleMessageBcc: return addresListToQVariant(envelope(model).bcc); case RoleMessageSender: return addresListToQVariant(envelope(model).sender); case RoleMessageReplyTo: return addresListToQVariant(envelope(model).replyTo); case RoleMessageInReplyTo: return QVariant::fromValue(envelope(model).inReplyTo); case RoleMessageMessageId: return envelope(model).messageId; case RoleMessageEnvelope: return QVariant::fromValue(envelope(model)); case RoleMessageHasAttachments: return hasAttachments(model); } } return QVariant(); } QVariantList TreeItemMessage::addresListToQVariant(const QList &addressList) { QVariantList res; foreach(const Imap::Message::MailAddress& address, addressList) { res.append(QVariant(QStringList() << address.name << address.adl << address.mailbox << address.host)); } return res; } namespace { /** @short Find a string based on d-ptr equality This works because our flags always use implicit sharing. If they didn't use that, this method wouldn't work. */ bool containsStringByDPtr(const QStringList &haystack, const QString &needle) { const auto sentinel = const_cast(needle).data_ptr(); Q_FOREACH(const auto &item, haystack) { if (const_cast(item).data_ptr() == sentinel) return true; } return false; } } bool TreeItemMessage::isMarkedAsDeleted() const { return containsStringByDPtr(m_flags, FlagNames::deleted); } bool TreeItemMessage::isMarkedAsRead() const { return containsStringByDPtr(m_flags, FlagNames::seen); } +bool TreeItemMessage::usedToBeUnread() const +{ + return m_wasUnread; +} + bool TreeItemMessage::isMarkedAsReplied() const { return containsStringByDPtr(m_flags, FlagNames::answered); } bool TreeItemMessage::isMarkedAsForwarded() const { return containsStringByDPtr(m_flags, FlagNames::forwarded); } bool TreeItemMessage::isMarkedAsRecent() const { return containsStringByDPtr(m_flags, FlagNames::recent); } bool TreeItemMessage::isMarkedAsFlagged() const { return containsStringByDPtr(m_flags, FlagNames::flagged); } bool TreeItemMessage::isMarkedAsJunk() const { return containsStringByDPtr(m_flags, FlagNames::junk); } bool TreeItemMessage::isMarkedAsNotJunk() const { return containsStringByDPtr(m_flags, FlagNames::notjunk); } void TreeItemMessage::checkFlagsReadRecent(bool &isRead, bool &isRecent) const { const auto dRead = const_cast(FlagNames::seen).data_ptr(); const auto dRecent = const_cast(FlagNames::recent).data_ptr(); auto end = m_flags.end(); auto it = m_flags.begin(); isRead = isRecent = false; while (it != end && !(isRead && isRecent)) { isRead |= const_cast(*it).data_ptr() == dRead; isRecent |= const_cast(*it).data_ptr() == dRecent; ++it; } } uint TreeItemMessage::uid() const { return m_uid; } Message::Envelope TreeItemMessage::envelope(Model *const model) { fetch(model); return data()->envelope(); } QDateTime TreeItemMessage::internalDate(Model *const model) { fetch(model); return data()->internalDate(); } quint64 TreeItemMessage::size(Model *const model) { fetch(model); return data()->size(); } void TreeItemMessage::setFlags(TreeItemMsgList *list, const QStringList &flags) { // wasSeen is used to determine if the message was marked as read before this operation bool wasSeen = isMarkedAsRead(); m_flags = flags; if (list->m_numberFetchingStatus == DONE) { bool isSeen = isMarkedAsRead(); if (m_flagsHandled) { if (wasSeen && !isSeen) { ++list->m_unreadMessageCount; // leave the message as "was unread" so it persists in the view when read messages are hidden m_wasUnread = true; } else if (!wasSeen && isSeen) { --list->m_unreadMessageCount; } } else { // it's a new message m_flagsHandled = true; if (!isSeen) { ++list->m_unreadMessageCount; // mark the message as "was unread" so it shows up in the view when read messages are hidden m_wasUnread = true; } } } } /** @short Process the data found in the headers passed along and file in auxiliary metadata This function accepts a snippet containing some RFC5322 headers of a message, no matter what headers are actually present in the passed text. The headers are parsed and those recognized are used as a source of data to file the "auxiliary metadata" of this TreeItemMessage (basically anything not available in ENVELOPE, UID, FLAGS, INTERNALDATE etc). */ void TreeItemMessage::processAdditionalHeaders(Model *model, const QByteArray &rawHeaders) { Imap::LowLevelParser::Rfc5322HeaderParser parser; bool ok = parser.parse(rawHeaders); if (!ok) { model->logTrace(0, Common::LOG_OTHER, QStringLiteral("Rfc5322HeaderParser"), QStringLiteral("Unspecified error during RFC5322 header parsing")); } data()->setHdrReferences(parser.references); QList hdrListPost; if (!parser.listPost.isEmpty()) { Q_FOREACH(const QByteArray &item, parser.listPost) hdrListPost << QUrl(QString::fromUtf8(item)); data()->setHdrListPost(hdrListPost); } // That's right, this can only be set, not ever reset from this context. // This is because we absolutely want to support incremental header arrival. if (parser.listPostNo) data()->setHdrListPostNo(true); } bool TreeItemMessage::hasAttachments(Model *const model) { fetch(model); if (!fetched()) return false; if (m_children.isEmpty()) { // strange, but why not, I guess return false; } else if (m_children.size() > 1) { // Again, very strange -- the message should have had a single multipart as a root node, but let's cope with this as well return true; } else { return hasNestedAttachments(model, static_cast(m_children[0])); } } /** @short Walk the MIME tree starting at @arg part and check if there are any attachments below (or at there) */ bool TreeItemMessage::hasNestedAttachments(Model *const model, TreeItemPart *part) { while (true) { const QByteArray mimeType = part->mimeType(); if (mimeType == "multipart/signed" && part->childrenCount(model) == 2) { // "strip" the signature, look at what's inside part = static_cast(part->child(0, model)); } else if (mimeType.startsWith("multipart/")) { // Return false iff no children is/has an attachment. // Originally this code was like this only for multipart/alternative, but in the end Stephan Platz lobbied for // treating ML signatures the same (which means multipart/mixed) has to be included, and there's also a RFC // which says that unrecognized multiparts should be treated exactly like a multipart/mixed. // As a bonus, this makes it possible to get rid of an extra branch for single-childed multiparts. for (uint i = 0; i < part->childrenCount(model); ++i) { if (hasNestedAttachments(model, static_cast(part->child(i, model)))) { return true; } } return false; } else if (mimeType == "text/html" || mimeType == "text/plain") { // See AttachmentView for details behind this. const QByteArray contentDisposition = part->bodyDisposition().toLower(); const bool isInline = contentDisposition.isEmpty() || contentDisposition == "inline"; const bool looksLikeAttachment = !part->fileName().isEmpty(); return looksLikeAttachment || !isInline; } else { // anything else must surely be an attachment return true; } } } TreeItemPart::TreeItemPart(TreeItem *parent, const QByteArray &mimeType) : TreeItem(parent) , m_mimeType(mimeType.toLower()) , m_octets(0) , m_partMime(nullptr) , m_partRaw(nullptr) , m_binaryCTEFailed(false) { if (isTopLevelMultiPart()) { // Note that top-level multipart messages are special, their immediate contents // can't be fetched. That's why we have to update the status here. setFetchStatus(DONE); } } TreeItemPart::TreeItemPart(TreeItem *parent) : TreeItem(parent) , m_mimeType("text/plain") , m_octets(0) , m_partMime(nullptr) , m_partRaw(nullptr) , m_binaryCTEFailed(false) { } TreeItemPart::~TreeItemPart() { delete m_partMime; delete m_partRaw; } unsigned int TreeItemPart::childrenCount(Model *const model) { Q_UNUSED(model); return m_children.size(); } TreeItem *TreeItemPart::child(const int offset, Model *const model) { Q_UNUSED(model); if (offset >= 0 && offset < m_children.size()) return m_children[ offset ]; else return 0; } TreeItemChildrenList TreeItemPart::setChildren(const TreeItemChildrenList &items) { FetchingState fetchStatus = accessFetchStatus(); auto res = TreeItem::setChildren(items); setFetchStatus(fetchStatus); return res; } void TreeItemPart::fetch(Model *const model) { if (fetched() || loading() || isUnavailable()) return; setFetchStatus(LOADING); model->askForMsgPart(this); } void TreeItemPart::fetchFromCache(Model *const model) { if (fetched() || loading() || isUnavailable()) return; model->askForMsgPart(this, true); } unsigned int TreeItemPart::rowCount(Model *const model) { // no call to fetch() required Q_UNUSED(model); return m_children.size(); } QVariant TreeItemPart::data(Model *const model, int role) { if (!parent()) return QVariant(); // these data are available immediately switch (role) { case RoleIsFetched: return fetched(); case RoleIsUnavailable: return isUnavailable(); case RolePartMimeType: return m_mimeType; case RolePartCharset: return m_charset; case RolePartContentFormat: return m_contentFormat; case RolePartContentDelSp: return m_delSp; case RolePartTransferEncoding: return m_transferEncoding; case RolePartBodyFldId: return m_bodyFldId; case RolePartBodyDisposition: return m_bodyDisposition; case RolePartFileName: return m_fileName; case RolePartOctets: return QVariant::fromValue(m_octets); case RolePartId: return partId(); case RolePartPathToPart: return pathToPart(); case RolePartMultipartRelatedMainCid: if (!multipartRelatedStartPart().isEmpty()) return multipartRelatedStartPart(); else return QVariant(); case RolePartMessageIndex: return QVariant::fromValue(message()->toIndex(model)); case RoleMailboxName: case RoleMailboxUidValidity: return message()->parent()->parent()->data(model, role); case RoleMessageUid: return message()->uid(); case RolePartIsTopLevelMultipart: return isTopLevelMultiPart(); case RolePartForceFetchFromCache: fetchFromCache(model); return QVariant(); case RolePartBufferPtr: return QVariant::fromValue(dataPtr()); case RolePartBodyFldParam: return QVariant::fromValue(m_bodyFldParam); case RoleIMAPRelativeUrl: if (message() && message()->uid()) { return QByteArray("/" + QUrl::toPercentEncoding(data(model, RoleMailboxName).toString()) + ";UIDVALIDITY=" + data(model, RoleMailboxUidValidity).toByteArray() + "/;UID=" + QByteArray::number(message()->uid()) + "/;SECTION=" + partId()); } else { return QVariant(); } } fetch(model); if (loading()) { if (role == Qt::DisplayRole) { return isTopLevelMultiPart() ? Model::tr("[loading %1...]").arg(QString::fromUtf8(m_mimeType)) : Model::tr("[loading %1: %2...]").arg(QString::fromUtf8(partId()), QString::fromUtf8(m_mimeType)); } else { return QVariant(); } } switch (role) { case Qt::DisplayRole: return isTopLevelMultiPart() ? QString::fromUtf8(m_mimeType) : QStringLiteral("%1: %2").arg(QString::fromUtf8(partId()), QString::fromUtf8(m_mimeType)); case Qt::ToolTipRole: return QStringLiteral("%1 bytes of data").arg(m_data.size()); case RolePartData: return m_data; case RolePartUnicodeText: if (m_mimeType.startsWith("text/")) { return decodeByteArray(m_data, m_charset); } else { return QVariant(); } default: return QVariant(); } } bool TreeItemPart::hasChildren(Model *const model) { // no need to fetch() here Q_UNUSED(model); return ! m_children.isEmpty(); } /** @short Returns true if we're a multipart, top-level item in the body of a message */ bool TreeItemPart::isTopLevelMultiPart() const { TreeItemMessage *msg = dynamic_cast(parent()); TreeItemPart *part = dynamic_cast(parent()); return m_mimeType.startsWith("multipart/") && (msg || (part && part->m_mimeType.startsWith("message/"))); } QByteArray TreeItemPart::partId() const { if (isTopLevelMultiPart()) { return QByteArray(); } else if (dynamic_cast(parent())) { return QByteArray::number(row() + 1); } else { QByteArray parentId; TreeItemPart *parentPart = dynamic_cast(parent()); Q_ASSERT(parentPart); if (parentPart->isTopLevelMultiPart()) { if (TreeItemPart *parentOfParent = dynamic_cast(parentPart->parent())) { Q_ASSERT(!parentOfParent->isTopLevelMultiPart()); // grand parent: message/rfc822 with a part-id, parent: top-level multipart parentId = parentOfParent->partId(); } else { // grand parent: TreeItemMessage, parent: some multipart, me: some part return QByteArray::number(row() + 1); } } else { parentId = parentPart->partId(); } Q_ASSERT(!parentId.isEmpty()); return parentId + '.' + QByteArray::number(row() + 1); } } QByteArray TreeItemPart::partIdForFetch(const PartFetchingMode mode) const { return QByteArray(mode == FETCH_PART_BINARY ? "BINARY" : "BODY") + ".PEEK[" + partId() + "]"; } QByteArray TreeItemPart::pathToPart() const { TreeItemPart *part = dynamic_cast(parent()); TreeItemMessage *msg = dynamic_cast(parent()); if (part) return part->pathToPart() + '/' + QByteArray::number(row()); else if (msg) return '/' + QByteArray::number(row()); else { Q_ASSERT(false); return QByteArray(); } } TreeItemMessage *TreeItemPart::message() const { const TreeItemPart *part = this; while (part) { TreeItemMessage *message = dynamic_cast(part->parent()); if (message) return message; part = dynamic_cast(part->parent()); } return 0; } QByteArray *TreeItemPart::dataPtr() { return &m_data; } unsigned int TreeItemPart::columnCount() { if (isTopLevelMultiPart()) { // Because a top-level multipart doesn't have its own part number, one cannot really fetch from it return 1; } // This one includes the OFFSET_MIME and OFFSET_RAW_CONTENTS, unlike the TreeItemMessage static_assert(OFFSET_MIME < OFFSET_RAW_CONTENTS, "The OFFSET_RAW_CONTENTS shall be the biggest one for tree invariants to work"); return OFFSET_RAW_CONTENTS + 1; } TreeItem *TreeItemPart::specialColumnPtr(int row, int column) const { if (row == 0 && !isTopLevelMultiPart()) { switch (column) { case OFFSET_MIME: if (!m_partMime) { m_partMime = new TreeItemModifiedPart(const_cast(this), OFFSET_MIME); } return m_partMime; case OFFSET_RAW_CONTENTS: if (!m_partRaw) { m_partRaw = new TreeItemModifiedPart(const_cast(this), OFFSET_RAW_CONTENTS); } return m_partRaw; } } return 0; } void TreeItemPart::silentlyReleaseMemoryRecursive() { Q_FOREACH(TreeItem *item, m_children) { TreeItemPart *part = dynamic_cast(item); Q_ASSERT(part); part->silentlyReleaseMemoryRecursive(); } if (m_partMime) { m_partMime->silentlyReleaseMemoryRecursive(); delete m_partMime; m_partMime = 0; } if (m_partRaw) { m_partRaw->silentlyReleaseMemoryRecursive(); delete m_partRaw; m_partRaw = 0; } m_data.clear(); setFetchStatus(NONE); qDeleteAll(m_children); m_children.clear(); } TreeItemModifiedPart::TreeItemModifiedPart(TreeItem *parent, const PartModifier kind): TreeItemPart(parent), m_modifier(kind) { } int TreeItemModifiedPart::row() const { // we're always at the very top return 0; } TreeItem *TreeItemModifiedPart::specialColumnPtr(int row, int column) const { Q_UNUSED(row); Q_UNUSED(column); // no special children below the current special one return 0; } bool TreeItemModifiedPart::isTopLevelMultiPart() const { // we're special enough not to ever act like a "top-level multipart" return false; } unsigned int TreeItemModifiedPart::columnCount() { // no child items, either return 0; } QByteArray TreeItemModifiedPart::partId() const { if (m_modifier == OFFSET_RAW_CONTENTS) { // This item is not directly fetcheable, so it does *not* make sense to ask for it. // We cannot really assert at this point, though, because this function is published via the MVC interface. return "application-bug-dont-fetch-this"; } else if (TreeItemPart *part = dynamic_cast(parent())) { // The TreeItemPart is supposed to prevent creation of any special subparts if it's a top-level multipart Q_ASSERT(!part->isTopLevelMultiPart()); return part->partId() + '.' + modifierToByteArray(); } else { // Our parent is a message/rfc822, and it's definitely not nested -> no need for parent id here // Cannot assert() on a dynamic_cast at this point because the part is already nullptr at this time Q_ASSERT(dynamic_cast(parent())); return modifierToByteArray(); } } TreeItem::PartModifier TreeItemModifiedPart::kind() const { return m_modifier; } QByteArray TreeItemModifiedPart::modifierToByteArray() const { switch (m_modifier) { case OFFSET_HEADER: return "HEADER"; case OFFSET_TEXT: return "TEXT"; case OFFSET_MIME: return "MIME"; case OFFSET_RAW_CONTENTS: Q_ASSERT(!"Cannot get the fetch modifier for an OFFSET_RAW_CONTENTS item"); // fall through default: Q_ASSERT(false); return QByteArray(); } } QByteArray TreeItemModifiedPart::pathToPart() const { if (TreeItemPart *parentPart = dynamic_cast(parent())) { return parentPart->pathToPart() + "/" + modifierToByteArray(); } else { Q_ASSERT(dynamic_cast(parent())); return "/" + modifierToByteArray(); } } QModelIndex TreeItemModifiedPart::toIndex(Model *const model) const { Q_ASSERT(model); // see TreeItem::toIndex() for the const_cast explanation return model->createIndex(row(), static_cast(kind()), const_cast(this)); } QByteArray TreeItemModifiedPart::partIdForFetch(const PartFetchingMode mode) const { Q_UNUSED(mode); // Don't try to use BINARY for special message parts, it's forbidden. One can only use that for the "regular" MIME parts return TreeItemPart::partIdForFetch(FETCH_PART_IMAP); } TreeItemPartMultipartMessage::TreeItemPartMultipartMessage(TreeItem *parent, const Message::Envelope &envelope): TreeItemPart(parent, "message/rfc822"), m_envelope(envelope) { } TreeItemPartMultipartMessage::~TreeItemPartMultipartMessage() { } /** @short Overridden from TreeItemPart::data with added support for RoleMessageEnvelope */ QVariant TreeItemPartMultipartMessage::data(Model * const model, int role) { switch (role) { case RoleMessageEnvelope: return QVariant::fromValue(m_envelope); case RoleMessageHeaderReferences: case RoleMessageHeaderListPost: case RoleMessageHeaderListPostNo: // FIXME: implement me; TreeItemPart has no path for this return QVariant(); default: return TreeItemPart::data(model, role); } } TreeItem *TreeItemPartMultipartMessage::specialColumnPtr(int row, int column) const { if (row != 0) return 0; switch (column) { case OFFSET_HEADER: if (!m_partHeader) { m_partHeader.reset(new TreeItemModifiedPart(const_cast(this), OFFSET_HEADER)); } return m_partHeader.get(); case OFFSET_TEXT: if (!m_partText) { m_partText.reset(new TreeItemModifiedPart(const_cast(this), OFFSET_TEXT)); } return m_partText.get(); default: return TreeItemPart::specialColumnPtr(row, column); } } void TreeItemPartMultipartMessage::silentlyReleaseMemoryRecursive() { TreeItemPart::silentlyReleaseMemoryRecursive(); if (m_partHeader) { m_partHeader->silentlyReleaseMemoryRecursive(); m_partHeader = nullptr; } if (m_partText) { m_partText->silentlyReleaseMemoryRecursive(); m_partText = nullptr; } } } } diff --git a/src/Imap/Model/MailboxTree.h b/src/Imap/Model/MailboxTree.h index a72d4bbb..4fdb1170 100644 --- a/src/Imap/Model/MailboxTree.h +++ b/src/Imap/Model/MailboxTree.h @@ -1,473 +1,474 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef IMAP_MAILBOXTREE_H #define IMAP_MAILBOXTREE_H #include #include #include #include #include #include "../Parser/Response.h" #include "../Parser/Message.h" #include "MailboxMetadata.h" namespace Imap { namespace Mailbox { class Model; class MailboxModel; class KeepMailboxOpenTask; class ListChildMailboxesTask; class TreeItem { friend class Model; // for m_loading and m_fetched TreeItem(const TreeItem &); // don't implement void operator=(const TreeItem &); // don't implement friend class DeleteMailboxTask; // for direct access to m_children friend class ObtainSynchronizedMailboxTask; friend class KeepMailboxOpenTask; // for direct access to m_children friend class ListChildMailboxesTask; // setStatus() in case of failure friend class MsgListModel; // for direct access to m_children friend class ThreadingMsgListModel; // for direct access to m_children friend class UpdateFlagsOfAllMessagesTask; // for direct access to m_children friend class FetchMsgPartTask; // for direct access to m_children protected: /** @short Availability of an item */ enum FetchingState { NONE, /**< @short No attempt to download an item has been made yet */ UNAVAILABLE, /**< @short Item isn't cached and remote requests are disabled */ LOADING, /**< @short Download of an item is already scheduled */ DONE /**< @short Item is available right now */ }; public: typedef enum { /** @short Full body of an e-mail stored on the IMAP server This one really makes sense on a TreeItemMessage and TreeItemPart, and are used */ /** @short The HEADER fetch modifier for the current item */ OFFSET_HEADER=1, /** @short The TEXT fetch modifier for the current item */ OFFSET_TEXT=2, /** @short The MIME fetch modifier for individual message parts In constrast to OFFSET_HEADER and OFFSET_TEXT, this one applies only to TreeItemPart, simply because using the MIME modifier on a top-level message is not allowed as per RFC 3501. */ OFFSET_MIME=3, /** @short Obtain the raw data without any kind of Content-Transfer-Encoding decoding */ OFFSET_RAW_CONTENTS = 4 } PartModifier; protected: static const intptr_t TagMask = 0x3; static const intptr_t PointerMask = ~TagMask; union { TreeItem *m_parent; intptr_t m_parentAsBits; }; TreeItemChildrenList m_children; FetchingState accessFetchStatus() const { return static_cast(m_parentAsBits & TagMask); } void setFetchStatus(const FetchingState fetchStatus) { m_parentAsBits = reinterpret_cast(parent()) | fetchStatus; } public: explicit TreeItem(TreeItem *parent); TreeItem *parent() const { return reinterpret_cast(m_parentAsBits & PointerMask); } virtual int row() const; virtual ~TreeItem(); virtual unsigned int childrenCount(Model *const model); virtual TreeItem *child(const int offset, Model *const model); virtual TreeItemChildrenList setChildren(const TreeItemChildrenList &items); virtual void fetch(Model *const model) = 0; virtual unsigned int rowCount(Model *const model) = 0; virtual unsigned int columnCount(); virtual QVariant data(Model *const model, int role) = 0; virtual bool hasChildren(Model *const model) = 0; virtual bool fetched() const { return accessFetchStatus() == DONE; } virtual bool loading() const { return accessFetchStatus() == LOADING; } virtual bool isUnavailable() const; virtual TreeItem *specialColumnPtr(int row, int column) const; virtual QModelIndex toIndex(Model *const model) const; }; class TreeItemPart; class TreeItemMessage; class TreeItemMailbox: public TreeItem { void operator=(const TreeItem &); // don't implement MailboxMetadata m_metadata; friend class Model; // needs access to maintianingTask friend class MailboxModel; friend class DeleteMailboxTask; // for direct access to maintainingTask friend class KeepMailboxOpenTask; // needs access to maintainingTask friend class SubscribeUnsubscribeTask; // needs access to m_metadata.flags friend class FetchMsgPartTask; // needs access to partIdToPtr() static QLatin1String flagNoInferiors; static QLatin1String flagHasNoChildren; static QLatin1String flagHasChildren; public: explicit TreeItemMailbox(TreeItem *parent); TreeItemMailbox(TreeItem *parent, Responses::List); ~TreeItemMailbox(); static TreeItemMailbox *fromMetadata(TreeItem *parent, const MailboxMetadata &metadata); virtual TreeItemChildrenList setChildren(const TreeItemChildrenList &items); virtual void fetch(Model *const model); virtual void fetchWithCacheControl(Model *const model, bool forceReload); virtual unsigned int rowCount(Model *const model); virtual QVariant data(Model *const model, int role); virtual bool hasChildren(Model *const model); virtual TreeItem *child(const int offset, Model *const model); SyncState syncState; /** @short Returns true if this mailbox has child mailboxes This function might access the network if the answer can't be decided, for example on basis of mailbox flags. */ bool hasChildMailboxes(Model *const model); /** @short Return true if the mailbox is already known to not have any child mailboxes No network activity will be caused. If the answer is not known for sure, we return false (meaning "don't know"). */ bool hasNoChildMailboxesAlreadyKnown(); QString mailbox() const { return m_metadata.mailbox; } QString separator() const { return m_metadata.separator; } const MailboxMetadata &mailboxMetadata() const { return m_metadata; } /** @short Update internal tree with the results of a FETCH response If \a changedPart is not null, it will be updated to point to the message part whose content got fetched. */ void handleFetchResponse(Model *const model, const Responses::Fetch &response, QList &changedParts, TreeItemMessage *&changedMessage, bool usingQresync); void rescanForChildMailboxes(Model *const model); void handleExpunge(Model *const model, const Responses::NumberResponse &resp); void handleExists(Model *const model, const Responses::NumberResponse &resp); void handleVanished(Model *const model, const Responses::Vanished &resp); bool isSelectable() const; void saveSyncStateAndUids(Model *model); private: TreeItemPart *partIdToPtr(Model *model, TreeItemMessage *message, const QByteArray &msgId); /** @short ImapTask which is currently responsible for well-being of this mailbox */ QPointer maintainingTask; }; class TreeItemMsgList: public TreeItem { void operator=(const TreeItem &); // don't implement friend class TreeItemMailbox; friend class TreeItemMessage; // for maintaining the m_unreadMessageCount friend class Model; friend class ObtainSynchronizedMailboxTask; friend class KeepMailboxOpenTask; FetchingState m_numberFetchingStatus; int m_totalMessageCount; int m_unreadMessageCount; int m_recentMessageCount; public: explicit TreeItemMsgList(TreeItem *parent); virtual void fetch(Model *const model); virtual unsigned int rowCount(Model *const model); virtual QVariant data(Model *const model, int role); virtual bool hasChildren(Model *const model); int totalMessageCount(Model *const model); int unreadMessageCount(Model *const model); int recentMessageCount(Model *const model); void fetchNumbers(Model *const model); void recalcVariousMessageCounts(Model *model); void recalcVariousMessageCountsOnExpunge(Model *model, TreeItemMessage *expungedMessage); void resetWasUnreadState(); bool numbersFetched() const; }; class MessageDataPayload { public: MessageDataPayload(); const Message::Envelope &envelope() const; void setEnvelope(const Message::Envelope &envelope); const QDateTime &internalDate() const; void setInternalDate(const QDateTime &internalDate); quint64 size() const; void setSize(const quint64 size); const QList &hdrReferences() const; void setHdrReferences(const QList &hdrReferences); const QList &hdrListPost() const; void setHdrListPost(const QList &hdrListPost); bool hdrListPostNo() const; void setHdrListPostNo(const bool hdrListPostNo); const QByteArray &rememberedBodyStructure() const; void setRememberedBodyStructure(const QByteArray &blob); TreeItemPart *partHeader() const; void setPartHeader(std::unique_ptr part); TreeItemPart *partText() const; void setPartText(std::unique_ptr part); bool isComplete() const; bool gotEnvelope() const; bool gotInternalDate() const; bool gotSize() const; bool gotHdrReferences() const; bool gotHdrListPost() const; bool gotRemeberedBodyStructure() const; private: Message::Envelope m_envelope; QDateTime m_internalDate; quint64 m_size; QList m_hdrReferences; QList m_hdrListPost; QByteArray m_rememberedBodyStructure; bool m_hdrListPostNo; std::unique_ptr m_partHeader; std::unique_ptr m_partText; bool m_gotEnvelope : 1; bool m_gotInternalDate : 1; bool m_gotSize : 1; bool m_gotBodystructure : 1; bool m_gotHdrReferences : 1; bool m_gotHdrListPost : 1; }; class TreeItemMessage: public TreeItem { void operator=(const TreeItem &); // don't implement friend class TreeItemMailbox; friend class TreeItemMsgList; friend class Model; friend class ObtainSynchronizedMailboxTask; // needs access to m_offset friend class KeepMailboxOpenTask; // needs access to m_offset friend class ThreadingMsgListModel; // needs access to m_flags friend class UpdateFlagsTask; // needs access to m_flags friend class UpdateFlagsOfAllMessagesTask; // needs access to m_flags int m_offset; uint m_uid; mutable MessageDataPayload *m_data; QStringList m_flags; bool m_flagsHandled; bool m_wasUnread; /** @short Set FLAGS and maintain the unread message counter */ void setFlags(TreeItemMsgList *list, const QStringList &flags); void processAdditionalHeaders(Model *model, const QByteArray &rawHeaders); static bool hasNestedAttachments(Model *const model, TreeItemPart *part); MessageDataPayload *data() const { return m_data ? m_data : (m_data = new MessageDataPayload()); } public: explicit TreeItemMessage(TreeItem *parent); ~TreeItemMessage(); virtual int row() const; virtual void fetch(Model *const model); virtual unsigned int rowCount(Model *const model); virtual unsigned int columnCount(); virtual QVariant data(Model *const model, int role); virtual bool hasChildren(Model *const model) { Q_UNUSED(model); return true; } virtual TreeItemChildrenList setChildren(const TreeItemChildrenList &items); Message::Envelope envelope(Model *const model); QDateTime internalDate(Model *const model); quint64 size(Model *const model); bool isMarkedAsDeleted() const; bool isMarkedAsRead() const; + bool usedToBeUnread() const; bool isMarkedAsReplied() const; bool isMarkedAsForwarded() const; bool isMarkedAsRecent() const; bool isMarkedAsFlagged() const; bool isMarkedAsJunk() const; bool isMarkedAsNotJunk() const; void checkFlagsReadRecent(bool &isRead, bool &isRecent) const; uint uid() const; virtual TreeItem *specialColumnPtr(int row, int column) const; bool hasAttachments(Model *const model); static QVariantList addresListToQVariant(const QList &addressList); }; class TreeItemPart: public TreeItem { void operator=(const TreeItem &); // don't implement friend class TreeItemMailbox; // needs access to m_data friend class Model; // dtto friend class FetchMsgPartTask; // needs m_binaryCTEFailed QByteArray m_mimeType; QByteArray m_charset; QByteArray m_contentFormat; QByteArray m_delSp; QByteArray m_transferEncoding; QByteArray m_data; QByteArray m_bodyFldId; QByteArray m_bodyDisposition; QString m_fileName; quint64 m_octets; QByteArray m_multipartRelatedStartPart; Imap::Message::AbstractMessage::bodyFldParam_t m_bodyFldParam; mutable TreeItemPart *m_partMime; mutable TreeItemPart *m_partRaw; bool m_binaryCTEFailed; public: TreeItemPart(TreeItem *parent, const QByteArray &mimeType); ~TreeItemPart(); virtual unsigned int childrenCount(Model *const model); virtual TreeItem *child(const int offset, Model *const model); virtual TreeItemChildrenList setChildren(const TreeItemChildrenList &items); virtual void fetchFromCache(Model *const model); virtual void fetch(Model *const model); virtual unsigned int rowCount(Model *const model); virtual unsigned int columnCount(); virtual QVariant data(Model *const model, int role); virtual bool hasChildren(Model *const model); virtual QByteArray partId() const; /** @short Shall we use RFC3516 BINARY for fetching message parts or not */ typedef enum { /** @short Use the baseline IMAP feature, the BODY[...], from RFC 3501 */ FETCH_PART_IMAP, /** @short Fetch via the RFC3516's BINARY extension */ FETCH_PART_BINARY } PartFetchingMode; virtual QByteArray partIdForFetch(const PartFetchingMode fetchingMode) const; virtual QByteArray pathToPart() const; TreeItemMessage *message() const; /** @short Provide access to the internal buffer holding data It is safe to access the obtained pointer as long as this object is not deleted. This function violates the classic concept of object encapsulation, but is really useful for the implementation of Imap::Network::MsgPartNetworkReply. */ QByteArray *dataPtr(); QByteArray mimeType() const { return m_mimeType; } QByteArray charset() const { return m_charset; } void setCharset(const QByteArray &ch) { m_charset = ch; } void setContentFormat(const QByteArray &format) { m_contentFormat = format; } void setContentDelSp(const QByteArray &delSp) { m_delSp = delSp; } void setTransferEncoding(const QByteArray &transferEncoding) { m_transferEncoding = transferEncoding; } QByteArray transferEncoding() const { return m_transferEncoding; } void setBodyFldId(const QByteArray &id) { m_bodyFldId = id; } QByteArray bodyFldId() const { return m_bodyFldId; } void setBodyDisposition(const QByteArray &disposition) { m_bodyDisposition = disposition; } QByteArray bodyDisposition() const { return m_bodyDisposition; } void setFileName(const QString &name) { m_fileName = name; } QString fileName() const { return m_fileName; } void setOctets(const quint64 size) { m_octets = size; } /** @short Return the downloadable size of the message part */ quint64 octets() const { return m_octets; } QByteArray multipartRelatedStartPart() const { return m_multipartRelatedStartPart; } void setMultipartRelatedStartPart(const QByteArray &start) { m_multipartRelatedStartPart = start; } void setBodyFldParam(const Imap::Message::AbstractMessage::bodyFldParam_t &bodyFldParam) { m_bodyFldParam = bodyFldParam; } Imap::Message::AbstractMessage::bodyFldParam_t bodyFldParam() const { return m_bodyFldParam; } virtual TreeItem *specialColumnPtr(int row, int column) const; virtual bool isTopLevelMultiPart() const; virtual void silentlyReleaseMemoryRecursive(); protected: TreeItemPart(TreeItem *parent); }; /** @short A message part with a modifier This item hanldes fetching of message parts with an attached modifier (like TEXT, HEADER or MIME). */ class TreeItemModifiedPart: public TreeItemPart { PartModifier m_modifier; public: TreeItemModifiedPart(TreeItem *parent, const PartModifier kind); virtual int row() const; virtual unsigned int columnCount(); virtual QByteArray partId() const; virtual QByteArray pathToPart() const; virtual TreeItem *specialColumnPtr(int row, int column) const; PartModifier kind() const; virtual QModelIndex toIndex(Model *const model) const; virtual QByteArray partIdForFetch(const PartFetchingMode fetchingMode) const; protected: virtual bool isTopLevelMultiPart() const; private: QByteArray modifierToByteArray() const; }; /** @short Specialization of TreeItemPart for parts holding a multipart/message */ class TreeItemPartMultipartMessage: public TreeItemPart { Message::Envelope m_envelope; mutable std::unique_ptr m_partHeader; mutable std::unique_ptr m_partText; public: TreeItemPartMultipartMessage(TreeItem *parent, const Message::Envelope &envelope); virtual ~TreeItemPartMultipartMessage(); virtual QVariant data(Model * const model, int role); virtual TreeItem *specialColumnPtr(int row, int column) const; virtual void silentlyReleaseMemoryRecursive(); }; } } Q_DECLARE_METATYPE(QByteArray*) #endif // IMAP_MAILBOXTREE_H diff --git a/src/Imap/Model/ThreadingMsgListModel.cpp b/src/Imap/Model/ThreadingMsgListModel.cpp index f89961a6..d0b18a76 100644 --- a/src/Imap/Model/ThreadingMsgListModel.cpp +++ b/src/Imap/Model/ThreadingMsgListModel.cpp @@ -1,1535 +1,1535 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "ThreadingMsgListModel.h" #include #include #include #include "Imap/Tasks/SortTask.h" #include "Imap/Tasks/ThreadTask.h" #include "ItemRoles.h" #include "MailboxTree.h" #include "MsgListModel.h" namespace { /** @short Preallocate a bit more space in the hashmaps for future new arrivals */ const int headroomForNewmessages = 1000; } namespace { using Imap::Mailbox::ThreadNodeInfo; #if 0 QByteArray dumpThreadNodeInfo(const QHash &mapping, const uint nodeId, const uint offset) { QByteArray res; QByteArray prefix(offset, ' '); QTextStream ss(&res); Q_ASSERT(mapping.contains(nodeId)); const ThreadNodeInfo &node = mapping[nodeId]; ss << prefix << "ThreadNodeInfo intId " << node.internalId << " UID " << node.uid << " ptr " << node.ptr << " parentIntId " << node.parent << "\n"; Q_FOREACH(const uint childId, node.children) { ss << dumpThreadNodeInfo(mapping, childId, offset + 1); } return res; } #endif #if 0 void dumpThreading(const QVector& thr, const uint offset) { QByteArray prefix(offset, ' '); for (const auto& x: thr) { std::cerr << prefix.data() << "ThreadingNode " << x.num << "\n"; dumpThreading(x.children, offset + 1); } } #endif } namespace Imap { namespace Mailbox { ThreadingMsgListModel::ThreadingMsgListModel(QObject *parent): QAbstractProxyModel(parent), threadingHelperLastId(0), modelResetInProgress(false), threadingInFlight(false), m_shallBeThreading(false), m_filteredBySearch(false), m_sortTask(0), m_sortReverse(false), m_currentSortingCriteria(SORT_NONE), m_searchValidity(RESULT_INVALIDATED) { m_delayedPrune = new QTimer(this); m_delayedPrune->setSingleShot(true); m_delayedPrune->setInterval(0); connect(m_delayedPrune, &QTimer::timeout, this, &ThreadingMsgListModel::delayedPrune); } void ThreadingMsgListModel::setSourceModel(QAbstractItemModel *sourceModel) { beginResetModel(); threading.clear(); ptrToInternal.clear(); unknownUids.clear(); threadedRootIds.clear(); m_currentSortResult.clear(); m_searchValidity = RESULT_INVALIDATED; if (this->sourceModel()) { // there's already something, so take care to disconnect all signals this->sourceModel()->disconnect(this); } endResetModel(); if (!sourceModel) return; Imap::Mailbox::MsgListModel *msgList = qobject_cast(sourceModel); Q_ASSERT(msgList); QAbstractProxyModel::setSourceModel(msgList); // FIXME: will need to be expanded when Model supports more signals... connect(sourceModel, &QAbstractItemModel::modelReset, this, &ThreadingMsgListModel::resetMe); connect(sourceModel, &QAbstractItemModel::layoutAboutToBeChanged, this, &QAbstractItemModel::layoutAboutToBeChanged); connect(sourceModel, &QAbstractItemModel::layoutChanged, this, &QAbstractItemModel::layoutChanged); connect(sourceModel, &QAbstractItemModel::dataChanged, this, &ThreadingMsgListModel::handleDataChanged); connect(sourceModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ThreadingMsgListModel::handleRowsAboutToBeRemoved); connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, &ThreadingMsgListModel::handleRowsRemoved); connect(sourceModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &ThreadingMsgListModel::handleRowsAboutToBeInserted); connect(sourceModel, &QAbstractItemModel::rowsInserted, this, &ThreadingMsgListModel::handleRowsInserted); resetMe(); } QVariant ThreadingMsgListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (sourceModel()) { return sourceModel()->headerData(section, orientation, role); } else { return QVariant(); } } void ThreadingMsgListModel::handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) { // We don't support updates which concern multiple rows at this time. // Doing that would very likely require a completely different codepath due to threading... Q_ASSERT(topLeft.parent() == bottomRight.parent()); Q_ASSERT(topLeft.row() == bottomRight.row()); QModelIndex translated = mapFromSource(topLeft); emit dataChanged(translated, translated.sibling(translated.row(), bottomRight.column())); // We provide funny data like "does this thread contain unread messages?". Now the original signal might mean that flags of a // nested message have changed. In order to always be consistent, we have to find the thread root and emit dataChanged() on that // as well. QModelIndex rootCandidate = translated; while (rootCandidate.parent().isValid()) { rootCandidate = rootCandidate.parent(); } if (rootCandidate != translated) { // We're really an embedded message emit dataChanged(rootCandidate, rootCandidate.sibling(rootCandidate.row(), bottomRight.column())); } auto message = dynamic_cast(static_cast(topLeft.internalPointer())); Q_ASSERT(message); if (message->uid() == 0) { // UID is not yet known. // This is a legal situation, for example when an unsolicited FETCH FLAGS arrives and there's no UID in there. return; } QSet::iterator persistent = unknownUids.find(message); if (persistent != unknownUids.end()) { // The message wasn't fully synced before, and now it is persistent = unknownUids.erase(persistent); if (unknownUids.isEmpty()) { wantThreading(); } } } QModelIndex ThreadingMsgListModel::index(int row, int column, const QModelIndex &parent) const { Q_ASSERT(!parent.isValid() || parent.model() == this); if (threading.isEmpty()) { // mapping not available yet return QModelIndex(); } if (row < 0 || column < 0 || column >= MsgListModel::COLUMN_COUNT) return QModelIndex(); if (parent.isValid() && parent.column() != 0) { // only the first column should have children return QModelIndex(); } uint parentId = parent.isValid() ? parent.internalId() : 0; QHash::const_iterator it = threading.constFind(parentId); Q_ASSERT(it != threading.constEnd()); if (it->children.size() <= row) return QModelIndex(); return createIndex(row, column, it->children[row]); } QModelIndex ThreadingMsgListModel::parent(const QModelIndex &index) const { if (! index.isValid() || index.model() != this) return QModelIndex(); if (threading.isEmpty()) return QModelIndex(); if (index.row() < 0 || index.column() < 0 || index.column() >= MsgListModel::COLUMN_COUNT) return QModelIndex(); QHash::const_iterator node = threading.constFind(index.internalId()); if (node == threading.constEnd()) return QModelIndex(); QHash::const_iterator parentNode = threading.constFind(node->parent); Q_ASSERT(parentNode != threading.constEnd()); Q_ASSERT(parentNode->internalId == node->parent); if (parentNode->internalId == 0) return QModelIndex(); return createIndex(parentNode->offset, 0, parentNode->internalId); } bool ThreadingMsgListModel::hasChildren(const QModelIndex &parent) const { if (parent.isValid() && parent.column() != 0) return false; return ! threading.isEmpty() && ! threading.value(parent.internalId()).children.isEmpty(); } int ThreadingMsgListModel::rowCount(const QModelIndex &parent) const { if (threading.isEmpty()) return 0; if (parent.isValid() && parent.column() != 0) return 0; return threading.value(parent.internalId()).children.size(); } int ThreadingMsgListModel::columnCount(const QModelIndex &parent) const { if (parent.isValid() && parent.column() != 0) return 0; return MsgListModel::COLUMN_COUNT; } QModelIndex ThreadingMsgListModel::mapToSource(const QModelIndex &proxyIndex) const { if (!proxyIndex.isValid() || !proxyIndex.internalId()) return QModelIndex(); if (threading.isEmpty()) return QModelIndex(); Imap::Mailbox::MsgListModel *msgList = qobject_cast(sourceModel()); Q_ASSERT(msgList); QHash::const_iterator node = threading.constFind(proxyIndex.internalId()); if (node == threading.constEnd()) return QModelIndex(); if (node->ptr) { return msgList->createIndex(node->ptr->row(), proxyIndex.column(), node->ptr); } else { // it's a fake message return QModelIndex(); } } QModelIndex ThreadingMsgListModel::mapFromSource(const QModelIndex &sourceIndex) const { if (!sourceIndex.isValid()) return QModelIndex(); Q_ASSERT(sourceIndex.model() == sourceModel()); QHash::const_iterator it = ptrToInternal.constFind(sourceIndex.internalPointer()); if (it == ptrToInternal.constEnd()) return QModelIndex(); const uint internalId = *it; QHash::const_iterator node = threading.constFind(internalId); if (node == threading.constEnd()) { // The filtering criteria say that this index shall not be visible return QModelIndex(); } Q_ASSERT(node != threading.constEnd()); return createIndex(node->offset, sourceIndex.column(), internalId); } QVariant ThreadingMsgListModel::data(const QModelIndex &proxyIndex, int role) const { if (! proxyIndex.isValid() || proxyIndex.model() != this) return QVariant(); QHash::const_iterator it = threading.constFind(proxyIndex.internalId()); Q_ASSERT(it != threading.constEnd()); if (it->ptr) { // It's a real item which exists in the underlying model switch (role) { case RoleThreadRootWithUnreadMessages: if (proxyIndex.parent().isValid()) { // We don't support this kind of questions for other messages than the roots of the threads. // Other components, like the QML bindings, are however happy to request that, so let's just return // a reasonable result instead of whinning about callers requesting useless stuff. return false; } else { - return threadContainsUnreadMessages(it->internalId); + return threadContainedUnreadMessages(it->internalId); } case RoleThreadAggregatedFlags: return threadAggregatedFlags(it->internalId); default: return QAbstractProxyModel::data(proxyIndex, role); } } switch (role) { case Qt::DisplayRole: if (proxyIndex.column() == 0) return tr("[Message is missing]"); break; case Qt::ToolTipRole: return tr("This thread refers to an extra message, but that message is not present in the " "selected mailbox, or is missing from the current search context."); } return QVariant(); } Qt::ItemFlags ThreadingMsgListModel::flags(const QModelIndex &index) const { if (! index.isValid() || index.model() != this) return Qt::NoItemFlags; QHash::const_iterator it = threading.constFind(index.internalId()); Q_ASSERT(it != threading.constEnd()); if (it->ptr && it->uid) return Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled; return Qt::NoItemFlags; } void ThreadingMsgListModel::handleRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) { Q_ASSERT(!parent.isValid()); for (int i = start; i <= end; ++i) { QModelIndex index = sourceModel()->index(i, 0, parent); Q_ASSERT(index.isValid()); QModelIndex translated = mapFromSource(index); unknownUids.remove(static_cast(index.internalPointer())); if (!translated.isValid()) { // The index being removed wasn't visible in our mapping anyway continue; } Q_ASSERT(translated.isValid()); QHash::iterator it = threading.find(translated.internalId()); Q_ASSERT(it != threading.end()); it->uid = 0; it->ptr = 0; } } void ThreadingMsgListModel::handleRowsRemoved(const QModelIndex &parent, int start, int end) { Q_ASSERT(!parent.isValid()); Q_UNUSED(start); Q_UNUSED(end); if (!m_delayedPrune->isActive()) m_delayedPrune->start(); } void ThreadingMsgListModel::delayedPrune() { emit layoutAboutToBeChanged(); updatePersistentIndexesPhase1(); pruneTree(); updatePersistentIndexesPhase2(); emit layoutChanged(); } void ThreadingMsgListModel::handleRowsAboutToBeInserted(const QModelIndex &parent, int start, int end) { Q_ASSERT(!parent.isValid()); int myStart = threading[0].children.size(); int myEnd = myStart + (end - start); beginInsertRows(QModelIndex(), myStart, myEnd); } void ThreadingMsgListModel::handleRowsInserted(const QModelIndex &parent, int start, int end) { Q_ASSERT(!parent.isValid()); for (int i = start; i <= end; ++i) { QModelIndex index = sourceModel()->index(i, 0); uint uid = index.data(RoleMessageUid).toUInt(); ThreadNodeInfo node; node.internalId = ++threadingHelperLastId; node.uid = uid; node.ptr = static_cast(index.internalPointer()); node.offset = threading[0].children.size(); threading[node.internalId] = node; threading[0].children << node.internalId; ptrToInternal[node.ptr] = node.internalId; if (!node.uid) { unknownUids << static_cast(index.internalPointer()); } else { threadedRootIds.append(node.internalId); } } endInsertRows(); if (!m_sortTask || !m_sortTask->isPersistent()) { m_currentSortResult.clear(); if (m_searchValidity == RESULT_FRESH) m_searchValidity = RESULT_INVALIDATED; } if (m_shallBeThreading) wantThreading(); } void ThreadingMsgListModel::resetMe() { // Prevent possible recursion here if (modelResetInProgress) return; beginResetModel(); modelResetInProgress = true; threading.clear(); ptrToInternal.clear(); unknownUids.clear(); threadedRootIds.clear(); m_currentSortResult.clear(); m_searchValidity = RESULT_INVALIDATED; endResetModel(); updateNoThreading(); modelResetInProgress = false; // Refresh the sorting/searching/threading preferences. // This is important, otherwise we won't track threading and/or the sort direction after e.g. changing a mailbox. wantThreading(); } void ThreadingMsgListModel::updateNoThreading() { threadingHelperLastId = 0; if (!sourceModel()) { // Maybe we got reset because the parent model is no longer here... if (! threading.isEmpty()) { beginRemoveRows(QModelIndex(), 0, rowCount() - 1); threading.clear(); ptrToInternal.clear(); endRemoveRows(); } unknownUids.clear(); return; } emit layoutAboutToBeChanged(); updatePersistentIndexesPhase1(); threading.clear(); ptrToInternal.clear(); unknownUids.clear(); threadedRootIds.clear(); int upstreamMessages = sourceModel()->rowCount(); QList allIds; QHash newThreading; QHash newPtrToInternal; if (upstreamMessages) { // Prefer the direct pointer access instead of going through the MVC API -- similar to how applyThreading() works. // This improves the speed of the testSortingPerformance benchmark by 18%. QModelIndex firstMessageIndex = sourceModel()->index(0, 0); Q_ASSERT(firstMessageIndex.isValid()); const Model *realModel = 0; TreeItem *firstMessagePtr = Model::realTreeItem(firstMessageIndex, &realModel); Q_ASSERT(firstMessagePtr); // If the next asserts fails, it means that the implementation of MsgListModel has changed and uses its own pointers Q_ASSERT(firstMessagePtr == firstMessageIndex.internalPointer()); TreeItemMsgList *list = dynamic_cast(firstMessagePtr->parent()); Q_ASSERT(list); newThreading.reserve(upstreamMessages + headroomForNewmessages); newPtrToInternal.reserve(upstreamMessages + headroomForNewmessages); for (int i = 0; i < upstreamMessages; ++i) { TreeItemMessage *ptr = static_cast(list->m_children[i]); Q_ASSERT(ptr); ThreadNodeInfo node; node.internalId = i + 1; node.uid = ptr->uid(); node.ptr = ptr; node.offset = i; newThreading[node.internalId] = node; allIds.append(node.internalId); newPtrToInternal[node.ptr] = node.internalId; if (!node.uid) { unknownUids << ptr; } } } if (newThreading.size()) { threading = newThreading; ptrToInternal = newPtrToInternal; threading[ 0 ].children = allIds; threading[ 0 ].ptr = 0; threadingHelperLastId = newThreading.size(); threadedRootIds = threading[0].children; } updatePersistentIndexesPhase2(); emit layoutChanged(); } void ThreadingMsgListModel::wantThreading(const SkipSortSearch skipSortSearch) { if (!sourceModel() || !sourceModel()->rowCount() || !m_shallBeThreading) { updateNoThreading(); if (skipSortSearch == AUTO_SORT_SEARCH) { searchSortPreferenceImplementation(m_currentSearchConditions, m_currentSortingCriteria, m_sortReverse ? Qt::DescendingOrder : Qt::AscendingOrder); } return; } if (threadingInFlight) { // Imagine the following scenario: // <<< "* 3 EXISTS" // Message 2 has unknown UID // >>> "y4 UID FETCH 66:* (FLAGS)" // >>> "y5 UID THREAD REFS utf-8 ALL" // <<< "* 3 FETCH (UID 66 FLAGS ())" // Got UID for seq# 3 // ThreadingMsgListModel::wantThreading: THREAD contains info about UID 1 (or higher), mailbox has 66 // *** this is the interesting part *** // <<< "y4 OK fetch" // <<< "* THREAD (1)(2)(66)" // <<< "y5 OK thread" // >>> "y6 UID THREAD REFS utf-8 ALL" // // See, at the indicated (***) place, we already have an in-flight THREAD request and receive UID for newly arrived // message. We certainly don't want to ask for threading once again; it's better to wait a bit and only ask when the // to-be-received THREAD does not contain all required UIDs. if (skipSortSearch == AUTO_SORT_SEARCH) { searchSortPreferenceImplementation(m_currentSearchConditions, m_currentSortingCriteria, m_sortReverse ? Qt::DescendingOrder : Qt::AscendingOrder); } return; } const Imap::Mailbox::Model *realModel; QModelIndex someMessage = sourceModel()->index(0,0); QModelIndex realIndex; Imap::Mailbox::Model::realTreeItem(someMessage, &realModel, &realIndex); QModelIndex mailbox = realIndex.parent().parent(); TreeItemMsgList *list = dynamic_cast(static_cast(realIndex.parent().internalPointer())); Q_ASSERT(list); // Something has happened and we want to process the THREAD response QVector mapping = realModel->cache()->messageThreading(mailbox.data(RoleMailboxName).toString()); // Find the UID of the last message in the mailbox QString scope; uint highestUidInMailbox; if (m_filteredBySearch) { scope = QLatin1String("search"); if (m_searchValidity != RESULT_FRESH) { if (m_searchValidity != RESULT_ASKED) { if (skipSortSearch == AUTO_SORT_SEARCH) { searchSortPreferenceImplementation(m_currentSearchConditions, m_currentSortingCriteria, m_sortReverse ? Qt::DescendingOrder : Qt::AscendingOrder); } } else { logTrace(QStringLiteral("Seems like the server answered in reverse order to our SEARCH/THREAD question. It's okay, threading should be tried later.")); } return; } // Required due to incremental updates highestUidInMailbox = *std::max_element(m_currentSortResult.constBegin(), m_currentSortResult.constEnd()); } else { scope = QLatin1String("mailbox"); highestUidInMailbox = findHighestUidInMailbox(list); } uint highestUidInThreadingLowerBound = findHighEnoughNumber(mapping, highestUidInMailbox); logTrace(QStringLiteral("ThreadingMsgListModel::wantThreading: THREAD contains info about UID %1 (or higher), %2 has %3") .arg(QString::number(highestUidInThreadingLowerBound), scope, QString::number(highestUidInMailbox))); if (highestUidInThreadingLowerBound >= highestUidInMailbox) { // There's no point asking for data at this point, we shall just apply threading applyThreading(mapping); } else { // There's apparently at least one known UID whose threading info we do not know; that means that we have to ask the // server here. auto roughlyLastKnown = const_cast(realModel)->findMessageOrNextOneByUid(list, highestUidInThreadingLowerBound); if (list->m_children.end() - roughlyLastKnown >= 50 || roughlyLastKnown == list->m_children.begin()) { askForThreading(); } else { askForThreading(static_cast(*roughlyLastKnown)->uid() + 1); } } } uint ThreadingMsgListModel::findHighestUidInMailbox(TreeItemMsgList *list) { uint highestUidInMailbox = 0; for (int i = sourceModel()->rowCount() - 1; i > -1 && !highestUidInMailbox; --i) { highestUidInMailbox = dynamic_cast(list->m_children[i])->uid(); } return highestUidInMailbox; } uint ThreadingMsgListModel::findHighEnoughNumber(const QVector &mapping, uint marker) { if (mapping.isEmpty()) return 0; // Find the highest UID for which we have the threading info uint highestUidInThreadingLowerBound = 0; // If the threading already contains everything we need, we could have a higher chance of finding the high enough UID at the // end of the list. On the other hand, in case when the cached THREAD response does not cintain everything we need, we're out // of luck, we have absolutely no guarantee about relative greatness of parent/child/siblings in the tree. // Searching backward could lead to faster lookups, but we cannot avoid a full lookup in the bad case. for (int i = mapping.size() - 1; i >= 0; --i) { if (highestUidInThreadingLowerBound < mapping[i].num) { highestUidInThreadingLowerBound = mapping[i].num; if (highestUidInThreadingLowerBound >= marker) { // There's no point going further, we already know that we shall ask for threading return highestUidInThreadingLowerBound; } } // OK, we have to consult our children highestUidInThreadingLowerBound = qMax(highestUidInThreadingLowerBound, findHighEnoughNumber(mapping[i].children, marker)); if (highestUidInThreadingLowerBound >= marker) { return highestUidInThreadingLowerBound; } } return highestUidInThreadingLowerBound; } void ThreadingMsgListModel::askForThreading(const uint firstUnknownUid) { Q_ASSERT(m_shallBeThreading); Q_ASSERT(sourceModel()); Q_ASSERT(sourceModel()->rowCount()); const Imap::Mailbox::Model *realModel = nullptr; QModelIndex someMessage = sourceModel()->index(0,0); QModelIndex realIndex; Imap::Mailbox::Model::realTreeItem(someMessage, &realModel, &realIndex); Q_ASSERT(realModel); QModelIndex mailboxIndex = realIndex.parent().parent(); if (realModel->capabilities().contains(QStringLiteral("THREAD=REFS"))) { requestedAlgorithm = "REFS"; } else if (realModel->capabilities().contains(QStringLiteral("THREAD=REFERENCES"))) { requestedAlgorithm = "REFERENCES"; } else if (realModel->capabilities().contains(QStringLiteral("THREAD=ORDEREDSUBJECT"))) { requestedAlgorithm = "ORDEREDSUBJECT"; } if (! requestedAlgorithm.isEmpty()) { threadingInFlight = true; if (firstUnknownUid && realModel->capabilities().contains(QStringLiteral("INCTHREAD"))) { auto threadTask = realModel->m_taskFactory-> createIncrementalThreadTask(const_cast(realModel), mailboxIndex, requestedAlgorithm, QStringList() << QStringLiteral("INTHREAD") << QString::fromUtf8(requestedAlgorithm) << QStringLiteral("UID") << QString::fromUtf8(Sequence::startingAt(firstUnknownUid).toByteArray())); connect(threadTask, &ThreadTask::incrementalThreadingAvailable, this, &ThreadingMsgListModel::slotIncrementalThreadingAvailable); connect(threadTask, &ImapTask::failed, this, &ThreadingMsgListModel::slotIncrementalThreadingFailed); } else { auto searchConditions = m_filteredBySearch ? m_currentSearchConditions : QStringList() << QStringLiteral("ALL"); realModel->m_taskFactory->createThreadTask(const_cast(realModel), mailboxIndex, requestedAlgorithm, searchConditions); connect(realModel, &Model::threadingAvailable, this, &ThreadingMsgListModel::slotThreadingAvailable); connect(realModel, &Model::threadingFailed, this, &ThreadingMsgListModel::slotThreadingFailed); } } } /** @short Gather all UIDs present in the mapping and push them into the "uids" vector */ static void gatherAllUidsFromThreadNode(Imap::Uids &uids, const QVector &list) { for (QVector::const_iterator it = list.constBegin(); it != list.constEnd(); ++it) { uids.push_back(it->num); gatherAllUidsFromThreadNode(uids, it->children); } } void ThreadingMsgListModel::slotIncrementalThreadingAvailable(const Responses::ESearch::IncrementalThreadingData_t &data) { // Preparation: get through to the real model const Imap::Mailbox::Model *realModel; QModelIndex someMessage = sourceModel()->index(0,0); Q_ASSERT(someMessage.isValid()); QModelIndex realIndex; Imap::Mailbox::Model::realTreeItem(someMessage, &realModel, &realIndex); QModelIndex mailboxIndex = realIndex.parent().parent(); Q_ASSERT(mailboxIndex.isValid()); // First phase: remove all messages mentioned in the incremental responses from their original placement Imap::Uids affectedUids; for (Responses::ESearch::IncrementalThreadingData_t::const_iterator it = data.constBegin(); it != data.constEnd(); ++it) { gatherAllUidsFromThreadNode(affectedUids, it->thread); } qSort(affectedUids); QList affectedMessages = const_cast(realModel)-> findMessagesByUids(static_cast(mailboxIndex.internalPointer()), affectedUids); QHash uidToPtrCache; emit layoutAboutToBeChanged(); updatePersistentIndexesPhase1(); for (QList::const_iterator it = affectedMessages.constBegin(); it != affectedMessages.constEnd(); ++it) { QHash::const_iterator ptrMappingIt = ptrToInternal.constFind(*it); Q_ASSERT(ptrMappingIt != ptrToInternal.constEnd()); QHash::iterator threadIt = threading.find(*ptrMappingIt); Q_ASSERT(threadIt != threading.end()); uidToPtrCache[(*it)->uid()] = threadIt->ptr; threadIt->ptr = 0; } pruneTree(); updatePersistentIndexesPhase2(); emit layoutChanged(); // Second phase: for each message whose UID is returned by the server, update the threading data QSet usedNodes; emit layoutAboutToBeChanged(); updatePersistentIndexesPhase1(); for (Responses::ESearch::IncrementalThreadingData_t::const_iterator it = data.constBegin(); it != data.constEnd(); ++it) { registerThreading(it->thread, 0, uidToPtrCache, usedNodes); int actualOffset = threading[0].children.size() - 1; int expectedOffsetOfPrevious = threading[0].children.indexOf(it->previousThreadRoot); if (actualOffset == expectedOffsetOfPrevious + 1) { // it's on the correct position, yay! } else { // move the new subthread to a correct place threading[0].children.insert(expectedOffsetOfPrevious + 1, threading[0].children.takeLast()); // push the rest (including the new arrival) forward for (int i = expectedOffsetOfPrevious + 1; i < threading[0].children.size(); ++i) { threading[threading[0].children[i]].offset = i; } } } updatePersistentIndexesPhase2(); emit layoutChanged(); } void ThreadingMsgListModel::slotIncrementalThreadingFailed() { } bool ThreadingMsgListModel::shouldIgnoreThisThreadingResponse(const QModelIndex &mailbox, const QByteArray &algorithm, const QStringList &searchCriteria, const Model **realModel) { QModelIndex someMessage = sourceModel()->index(0,0); if (!someMessage.isValid()) return true; const Model *model; QModelIndex realIndex; Imap::Mailbox::Model::realTreeItem(someMessage, &model, &realIndex); QModelIndex mailboxIndex = realIndex.parent().parent(); if (mailboxIndex != mailbox) { // this is for another mailbox return true; } if (algorithm != requestedAlgorithm) { logTrace(QStringLiteral("Weird, asked for threading via %1 but got %2 instead -- ignoring") .arg(QString::fromUtf8(requestedAlgorithm), QString::fromUtf8(algorithm))); return true; } if (realModel) *realModel = model; return false; } void ThreadingMsgListModel::slotThreadingFailed(const QModelIndex &mailbox, const QByteArray &algorithm, const QStringList &searchCriteria) { // Better safe than sorry -- prevent infinite waiting to the maximal possible extent threadingInFlight = false; if (shouldIgnoreThisThreadingResponse(mailbox, algorithm, searchCriteria)) return; auto model = qobject_cast(sender()); Q_ASSERT(model); disconnect(model, &Model::threadingAvailable, this, &ThreadingMsgListModel::slotThreadingAvailable); disconnect(model, &Model::threadingFailed, this, &ThreadingMsgListModel::slotThreadingFailed); updateNoThreading(); } void ThreadingMsgListModel::slotThreadingAvailable(const QModelIndex &mailbox, const QByteArray &algorithm, const QStringList &searchCriteria, const QVector &mapping) { // Better safe than sorry -- prevent infinite waiting to the maximal possible extent threadingInFlight = false; const Model *model = 0; if (shouldIgnoreThisThreadingResponse(mailbox, algorithm, searchCriteria, &model)) return; Q_ASSERT(model); disconnect(model, &Model::threadingAvailable, this, &ThreadingMsgListModel::slotThreadingAvailable); disconnect(model, &Model::threadingFailed, this, &ThreadingMsgListModel::slotThreadingFailed); model->cache()->setMessageThreading(mailbox.data(RoleMailboxName).toString(), mapping); // Indirect processing here -- the wantThreading() will check that the received response really contains everything we need // and if it does, simply applyThreading() that. If there's something missing, it will ask for the threading again. if (m_shallBeThreading) wantThreading(); } void ThreadingMsgListModel::slotSortingAvailable(const Imap::Uids &uids) { if (!m_sortTask->isPersistent()) { disconnect(m_sortTask.data(), &SortTask::sortingAvailable, this, &ThreadingMsgListModel::slotSortingAvailable); disconnect(m_sortTask.data(), &SortTask::sortingFailed, this, &ThreadingMsgListModel::slotSortingFailed); disconnect(m_sortTask.data(), &SortTask::incrementalSortUpdate, this, &ThreadingMsgListModel::slotSortingIncrementalUpdate); m_sortTask = 0; } m_currentSortResult = uids; if (m_searchValidity == RESULT_ASKED) m_searchValidity = RESULT_FRESH; wantThreading(); } void ThreadingMsgListModel::slotSortingFailed() { disconnect(m_sortTask.data(), &SortTask::sortingAvailable, this, &ThreadingMsgListModel::slotSortingAvailable); disconnect(m_sortTask.data(), &SortTask::sortingFailed, this, &ThreadingMsgListModel::slotSortingFailed); disconnect(m_sortTask.data(), &SortTask::incrementalSortUpdate, this, &ThreadingMsgListModel::slotSortingIncrementalUpdate); m_sortTask = 0; m_sortReverse = false; calculateNullSort(); applySort(); emit sortingFailed(); } void ThreadingMsgListModel::slotSortingIncrementalUpdate(const Responses::ESearch::IncrementalContextData_t &updates) { for (Responses::ESearch::IncrementalContextData_t::const_iterator it = updates.constBegin(); it != updates.constEnd(); ++it) { switch (it->modification) { case Responses::ESearch::ContextIncrementalItem::ADDTO: for (int i = 0; i < it->uids.size(); ++i) { int offset; if (it->offset == 0) { // FIXME: use mailbox order later on offset = 0; } else { // IMAP uses one-based indexing, we use zero-based offsets offset = it->offset + i - 1; } if (offset < 0 || offset > m_currentSortResult.size()) { throw MailboxException("ESEARCH: ADDTO out of bounds"); } m_currentSortResult.insert(offset, it->uids[i]); } break; case Responses::ESearch::ContextIncrementalItem::REMOVEFROM: for (int i = 0; i < it->uids.size(); ++i) { if (it->offset == 0) { // When the offset is not given, we have to find it ourselves auto item = std::find(m_currentSortResult.begin(), m_currentSortResult.end(), it->uids[i]); if (item == m_currentSortResult.end()) { throw MailboxException("ESEARCH: there's no such UID"); } m_currentSortResult.erase(item); } else { // We're given an offset, so let's make sure it is a correct one int offset = it->offset + i - 1; if (offset < 0 || offset >= m_currentSortResult.size()) { throw MailboxException("ESEARCH: REMOVEFROM out of bounds"); } if (m_currentSortResult[offset] != it->uids[i]) { throw MailboxException("ESEARCH: REMOVEFROM UID mismatch"); } m_currentSortResult.remove(offset); } } break; } } m_searchValidity = RESULT_FRESH; wantThreading(); } /** @short Store UIDs of the thread roots as the "current search order" */ void ThreadingMsgListModel::calculateNullSort() { m_currentSortResult.clear(); m_currentSortResult.reserve(threadedRootIds.size() + headroomForNewmessages); Q_FOREACH(const uint internalId, threadedRootIds) { QHash::const_iterator it = threading.constFind(internalId); if (it == threading.constEnd()) continue; if (it->uid) m_currentSortResult.append(it->uid); } m_searchValidity = RESULT_FRESH; } void ThreadingMsgListModel::applyThreading(const QVector &mapping) { if (! unknownUids.isEmpty()) { // Some messages have UID zero, which means that they weren't loaded yet. Too bad. logTrace(QStringLiteral("%1 messages have 0 UID").arg(unknownUids.size())); return; } emit layoutAboutToBeChanged(); updatePersistentIndexesPhase1(); threading.clear(); ptrToInternal.clear(); // Default-construct the root node threading[ 0 ].ptr = 0; // At first, initialize threading nodes for all messages which are right now available in the mailbox. // We risk that we will have to delete some of them later on, but this is likely better than doing a lookup // for each UID individually (remember, the THREAD response might contain UIDs in crazy order). int upstreamMessages = sourceModel()->rowCount(); QHash uidToPtrCache; QSet usedNodes; uidToPtrCache.reserve(upstreamMessages + headroomForNewmessages); threading.reserve(upstreamMessages + headroomForNewmessages); ptrToInternal.reserve(upstreamMessages + headroomForNewmessages); if (upstreamMessages) { // Work with pointers instead going through the MVC API for performance. // This matters (at least that's what by benchmarks said). QModelIndex firstMessageIndex = sourceModel()->index(0, 0); Q_ASSERT(firstMessageIndex.isValid()); const Model *realModel = 0; TreeItem *firstMessagePtr = Model::realTreeItem(firstMessageIndex, &realModel); Q_ASSERT(firstMessagePtr); // If the next asserts fails, it means that the implementation of MsgListModel has changed and uses its own pointers Q_ASSERT(firstMessagePtr == firstMessageIndex.internalPointer()); TreeItemMsgList *list = dynamic_cast(firstMessagePtr->parent()); Q_ASSERT(list); for (int i = 0; i < upstreamMessages; ++i) { ThreadNodeInfo node; node.uid = dynamic_cast(list->m_children[i])->uid(); if (! node.uid) { throw UnknownMessageIndex("Encountered a message with zero UID when threading. This is a bug in Trojita, sorry."); } node.internalId = i + 1; node.ptr = list->m_children[i]; uidToPtrCache[node.uid] = node.ptr; threadingHelperLastId = node.internalId; // We're creating a new node here Q_ASSERT(!threading.contains(node.internalId)); threading[ node.internalId ] = node; ptrToInternal[ node.ptr ] = node.internalId; } } // Mark the root node as always present usedNodes.insert(0); // Set up parents and find the list of all used nodes registerThreading(mapping, 0, uidToPtrCache, usedNodes); // Now remove all messages which were not referenced in the THREAD response from our mapping QHash::iterator it = threading.begin(); while (it != threading.end()) { if (usedNodes.contains(it.key())) { // this message should be shown ++it; } else { // this message is not included in the list of messages actually to be shown ptrToInternal.remove(it->ptr); it = threading.erase(it); } } pruneTree(); updatePersistentIndexesPhase2(); if (rowCount()) threadedRootIds = threading[0].children; emit layoutChanged(); // If the sorting was active before, we shall reactivate it now searchSortPreferenceImplementation(m_currentSearchConditions, m_currentSortingCriteria, m_sortReverse ? Qt::DescendingOrder : Qt::AscendingOrder); } void ThreadingMsgListModel::registerThreading(const QVector &mapping, uint parentId, const QHash &uidToPtr, QSet &usedNodes) { Q_FOREACH(const Imap::Responses::ThreadingNode &node, mapping) { uint nodeId; QHash::const_iterator ptrIt; if (node.num == 0 || (ptrIt = uidToPtr.find(node.num)) == uidToPtr.constEnd()) { // Either this is an empty node, or the THREAD response references a UID which is no longer in the mailbox. // This is a valid scenario; it can happen e.g. when reusing data from cache, or when a message got // expunged after the untagged THREAD was received, but before the tagged OK. // We cannot just ignore this node, though, because it might have some children which we would otherwise // simply hide. // The ptrIt which is initialized by the condition is used in the else branch. ThreadNodeInfo fake; fake.internalId = ++threadingHelperLastId; fake.parent = parentId; Q_ASSERT(threading.contains(parentId)); // The child will be registered to the list of parent's children after the if/else branch threading[ fake.internalId ] = fake; nodeId = fake.internalId; } else { QHash::const_iterator nodeIt = ptrToInternal.constFind(*ptrIt); // The following assert would fail if there was a node with a valid UID, but not in our ptrToInternal mapping. // That is however non-issue, as we pre-create nodes for all messages beforehand. Q_ASSERT(nodeIt != ptrToInternal.constEnd()); nodeId = *nodeIt; // This is needed for the incremental stuff threading[nodeId].ptr = static_cast(*ptrIt); } threading[nodeId].offset = threading[parentId].children.size(); threading[ parentId ].children.append(nodeId); threading[ nodeId ].parent = parentId; usedNodes.insert(nodeId); registerThreading(node.children, nodeId, uidToPtr, usedNodes); } } /** @short Gather a list of persistent indexes which we have to transform after out layout change */ void ThreadingMsgListModel::updatePersistentIndexesPhase1() { oldPersistentIndexes = persistentIndexList(); oldPtrs.clear(); Q_FOREACH(const QModelIndex &idx, oldPersistentIndexes) { // the index could get invalidated by the pruneTree() or something else manipulating our threading bool isOk = idx.isValid() && threading.contains(idx.internalId()); if (!isOk) { oldPtrs << 0; continue; } QModelIndex translated = mapToSource(idx); if (!translated.isValid()) { // another stale item oldPtrs << 0; continue; } oldPtrs << translated.internalPointer(); } } /** @short Update the gathered persistent indexes after our change in the layout */ void ThreadingMsgListModel::updatePersistentIndexesPhase2() { Q_ASSERT(oldPersistentIndexes.size() == oldPtrs.size()); QList updatedIndexes; for (int i = 0; i < oldPersistentIndexes.size(); ++i) { QHash::const_iterator ptrIt = ptrToInternal.constFind(oldPtrs[i]); if (ptrIt == ptrToInternal.constEnd()) { // That message is no longer there updatedIndexes.append(QModelIndex()); continue; } QHash::const_iterator it = threading.constFind(*ptrIt); if (it == threading.constEnd()) { // Filtering doesn't accept this index, let's declare it dead updatedIndexes.append(QModelIndex()); } else { updatedIndexes.append(createIndex(it->offset, oldPersistentIndexes[i].column(), it->internalId)); } } Q_ASSERT(oldPersistentIndexes.size() == updatedIndexes.size()); changePersistentIndexList(oldPersistentIndexes, updatedIndexes); oldPersistentIndexes.clear(); oldPtrs.clear(); } void ThreadingMsgListModel::pruneTree() { // Our mapping (threading) is completely unsorted, which means that we simply don't have any way of walking the tree from // the top. Instead, we got to work with a random walk, processing nodes in an unspecified order. If we iterated on the QHash // directly, we'd hit an issue with iterator ordering (basically, we want to be able to say "hey, I don't care at which point // of the iteration I'm right now, the next node to process should be that one, and then we should resume with the rest"). QList pending = threading.keys(); // These are the parents whose children will have to be renumbered later on QSet parentsForRenumbering; for (QList::iterator id = pending.begin(); id != pending.end(); /* nothing */) { // Convert to the hashmap // The "it" iterator point to the current node in the threading mapping QHash::iterator it = threading.find(*id); if (it == threading.end()) { // We've already seen this node, that's due to promoting ++id; continue; } if (it->internalId == 0) { // A special root item; we should not delete that one :) ++id; continue; } if (it->ptr) { // regular and valid message -> skip ++id; } else { // a fake one // each node has a parent QHash::iterator parent = threading.find(it->parent); Q_ASSERT(parent != threading.end()); // and the node itself has to be found in its parent's children QList::iterator childIt = qFind(parent->children.begin(), parent->children.end(), it->internalId); Q_ASSERT(childIt != parent->children.end()); // The offset of this child might no longer be correct, though -- we're postponing the actual deletion until later if (it->children.isEmpty()) { // This is a leaf node, so we can just remove it childIt = parent->children.erase(childIt); // We do not perform the renumbering immediately, that would lead to an O(n^2) performance when deleting nodes. parentsForRenumbering.insert(it->parent); parentsForRenumbering.remove(it->internalId); if (it->parent == 0) { threadedRootIds.removeOne(it->internalId); } threading.erase(it); ++id; } else { // This node has some children, so we can't just delete it. Instead of that, we promote its first child // to replace this node. QHash::iterator replaceWith = threading.find(it->children.first()); Q_ASSERT(replaceWith != threading.end()); // The offsets will, again, be updated later on parentsForRenumbering.insert(it->parent); parentsForRenumbering.insert(replaceWith.key()); parentsForRenumbering.remove(it->internalId); // Replace the node *childIt = it->children.first(); replaceWith->parent = parent->internalId; // Now merge the lists of children it->children.removeFirst(); replaceWith->children = replaceWith->children + it->children; // Fix parent information of all children of the replacement node for (int i = 0; i < replaceWith->children.size(); ++i) { QHash::iterator sibling = threading.find(replaceWith->children[i]); Q_ASSERT(sibling != threading.end()); sibling->parent = replaceWith.key(); } if (parent->internalId == 0) { // Update the list of all thread roots QList::iterator rootIt = qFind(threadedRootIds.begin(), threadedRootIds.end(), it->internalId); if (rootIt != threadedRootIds.end()) *rootIt = replaceWith->internalId; } // Now that all references are gone, remove the original node threading.erase(it); if (!replaceWith->ptr) { // If the just-promoted item is also a fake one, we'll have to visit it as well. This assignment is safe, // because we've already processed the current item and are completely done with it. The worst which can // happen is that we'll visit the same node twice, which is reasonably acceptable. *id = replaceWith.key(); } } } } // Now fix the sequential numbering of all siblings of deleted children Q_FOREACH(const auto parentId, parentsForRenumbering) { auto parentIt = threading.constFind(parentId); Q_ASSERT(parentIt != threading.constEnd()); int offset = 0; for (auto childNumber = parentIt->children.constBegin(); childNumber != parentIt->children.constEnd(); ++childNumber, ++offset) { auto childIt = threading.find(*childNumber); Q_ASSERT(childIt != threading.end()); childIt->offset = offset; } } } QStringList ThreadingMsgListModel::supportedCapabilities() { return QStringList() << QStringLiteral("THREAD=REFS") << QStringLiteral("THREAD=REFERENCES") << QStringLiteral("THREAD=ORDEREDSUBJECT"); } QStringList ThreadingMsgListModel::mimeTypes() const { return sourceModel() ? sourceModel()->mimeTypes() : QStringList(); } QMimeData *ThreadingMsgListModel::mimeData(const QModelIndexList &indexes) const { if (! sourceModel()) return 0; QModelIndexList translated; Q_FOREACH(const QModelIndex &idx, indexes) { translated << mapToSource(idx); } return sourceModel()->mimeData(translated); } template bool threadForeachCallback(std::function callback, const TreeItemMessage &message) { callback(message); return false; } template<> bool threadForeachCallback(std::function callback, const TreeItemMessage &message) { return callback(message); } /** @short Execute the provided function once for each message Returns immediately if the provided function returns `true`. */ template void ThreadingMsgListModel::threadForeach(const uint &root, std::function callback) const { QList queue; queue.append(root); while (! queue.isEmpty()) { uint current = queue.takeFirst(); QHash::const_iterator it = threading.constFind(current); Q_ASSERT(it != threading.constEnd()); if (it->ptr) { // Because of the delayed delete via pruneTree, we can hit a null pointer here TreeItemMessage *message = dynamic_cast(it->ptr); Q_ASSERT(message); if (threadForeachCallback(callback, *message)) return; } queue.append(it->children); } } -bool ThreadingMsgListModel::threadContainsUnreadMessages(const uint root) const +bool ThreadingMsgListModel::threadContainedUnreadMessages(const uint root) const { // FIXME: cache the value somewhere... - bool containsUnreadMessages = false; - threadForeach(root, [&containsUnreadMessages](const TreeItemMessage &message) -> const bool { - return containsUnreadMessages = ! message.isMarkedAsRead(); + bool containedUnreadMessages = false; + threadForeach(root, [&containedUnreadMessages](const TreeItemMessage &message) -> const bool { + return containedUnreadMessages = !message.isMarkedAsRead() || message.usedToBeUnread(); }); - return containsUnreadMessages; + return containedUnreadMessages; } QStringList ThreadingMsgListModel::threadAggregatedFlags(const uint root) const { // FIXME: cache the value somewhere... QStringList aggregatedFlags; threadForeach(root, [&aggregatedFlags](const TreeItemMessage &message) { aggregatedFlags += message.m_flags; }); aggregatedFlags.removeDuplicates(); return aggregatedFlags; } /** @short Pass a debugging message to the real Model, if possible If we don't know what the real model is, just dump it through the qDebug(); that's better than nothing. */ void ThreadingMsgListModel::logTrace(const QString &message) { if (!sourceModel()) { qDebug() << message; return; } QModelIndex idx = sourceModel()->index(0, 0); if (!idx.isValid()) { qDebug() << message; return; } // Got to find out the real model and also translate the index to one belonging to a real Model Q_ASSERT(idx.model()); const Model *realModel; QModelIndex realIndex; Model::realTreeItem(idx, &realModel, &realIndex); Q_ASSERT(realModel); QModelIndex mailboxIndex = const_cast(realModel)->findMailboxForItems(QModelIndexList() << realIndex); const_cast(realModel)->logTrace(mailboxIndex, Common::LOG_OTHER, QStringLiteral("ThreadingMsgListModel for %1").arg(mailboxIndex.data(RoleMailboxName).toString()), message); } void ThreadingMsgListModel::setUserWantsThreading(bool enable) { m_shallBeThreading = enable; if (m_shallBeThreading) { wantThreading(); } else { updateNoThreading(); } } bool ThreadingMsgListModel::setUserSearchingSortingPreference(const QStringList &searchConditions, const SortCriterium criterium, const Qt::SortOrder order) { auto changedSearch = (searchConditions != m_currentSearchConditions); if (!m_shallBeThreading) { updateNoThreading(); } auto succeed = searchSortPreferenceImplementation(searchConditions, criterium, order); if (m_shallBeThreading && changedSearch) { // Asking for threading regardless of `threadingInFlight` because // THREAD-ing had to be redone in the context of this search logTrace(QStringLiteral("Current threading invalidated by changed search")); askForThreading(0); } return succeed; } /** @short The workhorse behind setUserSearchingSortingPreference() */ bool ThreadingMsgListModel::searchSortPreferenceImplementation(const QStringList &searchConditions, const SortCriterium criterium, const Qt::SortOrder order) { Q_ASSERT(sourceModel()); m_sortReverse = order == Qt::DescendingOrder; if (!sourceModel()->rowCount()) { return false; } const Model *realModel; QModelIndex someMessage = sourceModel()->index(0,0); QModelIndex realIndex; Model::realTreeItem(someMessage, &realModel, &realIndex); QModelIndex mailboxIndex = realIndex.parent().parent(); bool hasDisplaySort = false; bool hasSort = false; if (realModel->capabilities().contains(QStringLiteral("SORT=DISPLAY"))) { hasDisplaySort = true; hasSort = true; } else if (realModel->capabilities().contains(QStringLiteral("SORT"))) { // just the regular sort hasSort = true; } QStringList sortOptions; switch (criterium) { case SORT_ARRIVAL: sortOptions << QStringLiteral("ARRIVAL"); break; case SORT_CC: sortOptions << QStringLiteral("CC"); break; case SORT_DATE: sortOptions << QStringLiteral("DATE"); break; case SORT_FROM: sortOptions << (hasDisplaySort ? QStringLiteral("DISPLAYFROM") : QStringLiteral("FROM")); break; case SORT_SIZE: sortOptions << QStringLiteral("SIZE"); break; case SORT_SUBJECT: sortOptions << QStringLiteral("SUBJECT"); break; case SORT_TO: sortOptions << (hasDisplaySort ? QStringLiteral("DISPLAYTO") : QStringLiteral("TO")); break; case SORT_NONE: if (m_sortTask && m_sortTask->isPersistent() && (m_currentSearchConditions != searchConditions || m_currentSortingCriteria != criterium)) { // Any change shall result in us killing that sort task m_sortTask->cancelSortingUpdates(); } m_currentSortingCriteria = criterium; if (searchConditions.isEmpty()) { // This operation is special, it will immediately restore the original shape of the mailbox m_currentSearchConditions = searchConditions; m_filteredBySearch = false; calculateNullSort(); applySort(); return true; } else if (searchConditions != m_currentSearchConditions || m_searchValidity != RESULT_FRESH) { // We have to update our search conditions m_sortTask = realModel->m_taskFactory->createSortTask(const_cast(realModel), mailboxIndex, searchConditions, QStringList()); connect(m_sortTask.data(), &SortTask::sortingAvailable, this, &ThreadingMsgListModel::slotSortingAvailable); connect(m_sortTask.data(), &SortTask::sortingFailed, this, &ThreadingMsgListModel::slotSortingFailed); connect(m_sortTask.data(), &SortTask::incrementalSortUpdate, this, &ThreadingMsgListModel::slotSortingIncrementalUpdate); m_currentSearchConditions = searchConditions; m_filteredBySearch = true; m_searchValidity = RESULT_ASKED; } else { // A result of SEARCH has just arrived Q_ASSERT(m_searchValidity == RESULT_FRESH); applySort(); } return true; } if (!hasSort) { // sorting is completely unsupported return false; } Q_ASSERT(!sortOptions.isEmpty()); if (m_currentSortingCriteria == criterium && m_currentSearchConditions == searchConditions && m_searchValidity != RESULT_INVALIDATED) { applySort(); } else { m_currentSearchConditions = searchConditions; m_filteredBySearch = ! searchConditions.isEmpty(); m_currentSortingCriteria = criterium; calculateNullSort(); applySort(); if (m_sortTask && m_sortTask->isPersistent()) m_sortTask->cancelSortingUpdates(); m_sortTask = realModel->m_taskFactory->createSortTask(const_cast(realModel), mailboxIndex, searchConditions, sortOptions); connect(m_sortTask.data(), &SortTask::sortingAvailable, this, &ThreadingMsgListModel::slotSortingAvailable); connect(m_sortTask.data(), &SortTask::sortingFailed, this, &ThreadingMsgListModel::slotSortingFailed); connect(m_sortTask.data(), &SortTask::incrementalSortUpdate, this, &ThreadingMsgListModel::slotSortingIncrementalUpdate); m_searchValidity = RESULT_ASKED; } return true; } void ThreadingMsgListModel::applySort() { if (!sourceModel()->rowCount()) { // empty mailbox is a corner case and it's already sorted anyway return; } const Imap::Mailbox::Model *realModel; QModelIndex someMessage = sourceModel()->index(0,0); QModelIndex realIndex; Model::realTreeItem(someMessage, &realModel, &realIndex); TreeItemMailbox *mailbox = dynamic_cast(static_cast(realIndex.parent().parent().internalPointer())); Q_ASSERT(mailbox); emit layoutAboutToBeChanged(); updatePersistentIndexesPhase1(); QSet newlyUnreachable(threading[0].children.toSet()); threading[0].children.clear(); threading[0].children.reserve(m_currentSortResult.size() + headroomForNewmessages); QSet allRootIds(threadedRootIds.toSet()); for (int i = 0; i < m_currentSortResult.size(); ++i) { int offset = m_sortReverse ? m_currentSortResult.size() - 1 - i : i; QList messages = const_cast(realModel) ->findMessagesByUids(mailbox, Imap::Uids() << m_currentSortResult[offset]); if (messages.isEmpty()) { // wrong UID, weird continue; } Q_ASSERT(messages.size() == 1); QHash::const_iterator it = ptrToInternal.constFind(messages.front()); // else applyThreading() taking care of it if (!threadingInFlight) Q_ASSERT(it != ptrToInternal.constEnd()); if (!allRootIds.contains(*it)) { // not a thread root, so don't show it continue; } threading[*it].offset = threading[0].children.size(); threading[0].children.append(*it); } // Now remove everything which is no longer reachable from the root of the thread mapping // Start working on the top-level orphans Q_FOREACH(const uint uid, threading[0].children) { newlyUnreachable.remove(uid); } std::vector queue(newlyUnreachable.constBegin(), newlyUnreachable.constEnd()); for (std::vector::size_type i = 0; i < queue.size(); ++i) { QHash::iterator threadingIt = threading.find(queue[i]); Q_ASSERT(threadingIt != threading.end()); queue.insert(queue.end(), threadingIt->children.constBegin(), threadingIt->children.constEnd()); threading.erase(threadingIt); } updatePersistentIndexesPhase2(); emit layoutChanged(); } QStringList ThreadingMsgListModel::currentSearchCondition() const { return m_currentSearchConditions; } ThreadingMsgListModel::SortCriterium ThreadingMsgListModel::currentSortCriterium() const { return m_currentSortingCriteria; } Qt::SortOrder ThreadingMsgListModel::currentSortOrder() const { return m_sortReverse ? Qt::DescendingOrder : Qt::AscendingOrder; } QModelIndex ThreadingMsgListModel::sibling(int row, int column, const QModelIndex &idx) const { return index(row, column, idx.parent()); } } } diff --git a/src/Imap/Model/ThreadingMsgListModel.h b/src/Imap/Model/ThreadingMsgListModel.h index 8da24f46..58dbbcd9 100644 --- a/src/Imap/Model/ThreadingMsgListModel.h +++ b/src/Imap/Model/ThreadingMsgListModel.h @@ -1,330 +1,334 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef IMAP_THREADINGMSGLISTMODEL_H #define IMAP_THREADINGMSGLISTMODEL_H #include #include #include #include #include "MailboxTree.h" #include "Imap/Parser/Response.h" class QTimer; class ImapModelThreadingTest; /** @short Namespace for IMAP interaction */ namespace Imap { /** @short Classes for handling of mailboxes and connections */ namespace Mailbox { class SortTask; class TreeItem; class TreeItemMsgList; /** @short A node in tree structure used for threading representation */ struct ThreadNodeInfo { /** @short Internal unique identifier used for model indexes */ uint internalId; /** @short A UID of the message in a mailbox */ uint uid; /** @short internalId of a parent of this message */ uint parent; /** @short List of children of current node */ QList children; /** @short Pointer to the TreeItemMessage* of the corresponding message */ TreeItem *ptr; /** @short Position among our parent's children */ int offset; ThreadNodeInfo(): internalId(0), uid(0), parent(0), ptr(0), offset(0) {} }; QDebug operator<<(QDebug debug, const ThreadNodeInfo &node); /** @short A model implementing view of the whole IMAP server The problem with threading is that due to the extremely asynchronous nature of the IMAP Model, we often get informed about indexes to messages which "just arrived", and therefore do not have even their UID available. That sucks, because we have to somehow handle them. Situation gets a bit more complicated by the initial syncing -- this ThreadingMsgListModel can't tell whether the rowsInserted() signals mean that the underlying model is getting populated, or whether it's a sign of a just-arrived message. On a plus side, the Model guarantees that the only occurrence when a message could have UID 0 is when the mailbox has been synced previously, and the message is a new arrival. In all other contexts (that is, during the mailbox re-synchronization), there is a hard guarantee that the UID of any message available via the MVC API will always be non-zero. The model should also refrain from sending extra THREAD commands to the server, and cache the responses locally. This is pretty easy for message deletions, as it should be only a matter of replacing some node in the threading info with a fake ThreadNodeInfo node and running the pruneTree() method, except that we might not know the UID of the message in question, and hence can't know what to delete. */ class ThreadingMsgListModel: public QAbstractProxyModel { Q_OBJECT Q_ENUMS(SortCriterium) public: /** @short On which column to sort The possible columns are described in RFC 5256, section 3. No support for multiple columns is present. Trojitá will automatically upgrade to the display-based search criteria from RFC 5957 if support for that RFC is indicated by the server. */ typedef enum { /** @short Don't do any explicit sorting If threading is not active, the order of messages represnets the order in which they appear in the IMAP mailbox. In case the display is threaded already, the order depends on the threading algorithm. */ SORT_NONE, /** @short RFC5256's ARRIVAL key, ie. the INTERNALDATE */ SORT_ARRIVAL, /** @short The Cc field from the IMAP ENVELOPE */ SORT_CC, /** @short Timestamp when the message was created, if available */ SORT_DATE, /** @short Either the display name or the mailbox of the "sender" of a message from the "From" header */ SORT_FROM, /** @short Size of the message */ SORT_SIZE, /** @short The subject of the e-mail */ SORT_SUBJECT, /** @short Recipient of the message, either their mailbox or their display name */ SORT_TO } SortCriterium; explicit ThreadingMsgListModel(QObject *parent); virtual void setSourceModel(QAbstractItemModel *sourceModel); virtual QModelIndex index(int row, int column, const QModelIndex &parent=QModelIndex()) const; virtual QModelIndex parent(const QModelIndex &index) const; virtual int rowCount(const QModelIndex &parent=QModelIndex()) const; virtual int columnCount(const QModelIndex &parent=QModelIndex()) const; virtual QModelIndex mapToSource(const QModelIndex &proxyIndex) const; virtual QModelIndex mapFromSource(const QModelIndex &sourceIndex) const; virtual bool hasChildren(const QModelIndex &parent=QModelIndex()) const; virtual QVariant data(const QModelIndex &proxyIndex, int role) const; virtual Qt::ItemFlags flags(const QModelIndex &index) const; QVariant headerData(int section, Qt::Orientation orientation, int role) const; // Qt5 reimplements sibling() within the proxy models, and the default implementation constitutes // a behavior change compared to Qt4. virtual QModelIndex sibling(int row, int column, const QModelIndex &idx) const; virtual QStringList mimeTypes() const; virtual QMimeData *mimeData(const QModelIndexList &indexes) const; /** @short List of capabilities which could be used for threading If any of them are present in server's capabilities, at least some level of threading will be possible. */ static QStringList supportedCapabilities(); QStringList currentSearchCondition() const; SortCriterium currentSortCriterium() const; Q_INVOKABLE Qt::SortOrder currentSortOrder() const; public slots: void resetMe(); void handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); void handleRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); void handleRowsRemoved(const QModelIndex &parent, int start, int end); void handleRowsAboutToBeInserted(const QModelIndex &parent, int start, int end); void handleRowsInserted(const QModelIndex &parent, int start, int end); /** @short Feed this with the data from a THREAD response */ void slotThreadingAvailable(const QModelIndex &mailbox, const QByteArray &algorithm, const QStringList &searchCriteria, const QVector &mapping); void slotThreadingFailed(const QModelIndex &mailbox, const QByteArray &algorithm, const QStringList &searchCriteria); /** @short Really apply threading to this model */ void applyThreading(const QVector &mapping); /** @short SORT response has arrived */ void slotSortingAvailable(const Imap::Uids &uids); /** @short SORT has failed */ void slotSortingFailed(); /** @short Dynamic update to the current SORT order */ void slotSortingIncrementalUpdate(const Imap::Responses::ESearch::IncrementalContextData_t &updates); void applySort(); /** @short Enable or disable threading */ void setUserWantsThreading(bool enable); Q_INVOKABLE bool setUserSearchingSortingPreference(const QStringList &searchConditions, const SortCriterium criterium, const Qt::SortOrder order = Qt::AscendingOrder); void slotIncrementalThreadingAvailable(const Responses::ESearch::IncrementalThreadingData_t &data); void slotIncrementalThreadingFailed(); void delayedPrune(); signals: void sortingFailed(); private: /** @short Display messages without any threading at all, as a liner list */ void updateNoThreading(); /** @short Ask the model for a THREAD response If the firstUnknownUid is different than zero, an incremental response is requested. */ void askForThreading(const uint firstUnknownUid = 0); void updatePersistentIndexesPhase1(); void updatePersistentIndexesPhase2(); /** @short Shall we ask for SORT/SEARCH automatically? */ typedef enum { AUTO_SORT_SEARCH, SKIP_SORT_SEARCH } SkipSortSearch; /** @short Apply cached THREAD response or ask for threading again */ void wantThreading(const SkipSortSearch skipSortSearch = AUTO_SORT_SEARCH); /** @short Convert the threading from a THREAD response and apply that threading to this model */ void registerThreading(const QVector &mapping, uint parentId, const QHash &uidToPtr, QSet &usedNodes); bool searchSortPreferenceImplementation(const QStringList &searchConditions, const SortCriterium criterium, const Qt::SortOrder order = Qt::AscendingOrder); /** @short Remove fake messages from the threading tree */ void pruneTree(); /** @short Execute the provided function once for each message */ template void threadForeach(const uint &root, std::function callback) const; - /** @short Check current thread for "unread messages" */ - bool threadContainsUnreadMessages(const uint root) const; + /** @short Check current thread for "unread messages" + + We're also including those messages which were marked as unread "recently", otherwise the "hide read" + filtering becomes inconsistent with threads. + */ + bool threadContainedUnreadMessages(const uint root) const; /** @short Return aggregated flags from the thread */ QStringList threadAggregatedFlags(const uint root) const; /** @short Is this someone else's THREAD response? */ bool shouldIgnoreThisThreadingResponse(const QModelIndex &mailbox, const QByteArray &algorithm, const QStringList &searchCriteria, const Model **realModel=0); /** @short Return some number from the thread mapping @arg mapping which is either the highest among them, or at least as high as the marker*/ static uint findHighEnoughNumber(const QVector &mapping, uint marker); void calculateNullSort(); uint findHighestUidInMailbox(TreeItemMsgList *list); void logTrace(const QString &message); ThreadingMsgListModel &operator=(const ThreadingMsgListModel &); // don't implement ThreadingMsgListModel(const ThreadingMsgListModel &); // don't implement /** @short Mapping from the upstream model's internalId to ThreadingMsgListModel's internal IDs */ QHash ptrToInternal; /** @short Tree for the threading This tree is indexed by our internal ID. */ QHash threading; /** @short Last assigned internal ID */ uint threadingHelperLastId; /** @short Messages with unknown UIDs */ QSet unknownUids; /** @short Threading algorithm we're using for this request */ QByteArray requestedAlgorithm; /** @short Recursion guard for "is the model currently being reset?" We can't be sure what happens when we call rowCount() from updateNoThreading(). It is possible that the rowCount() would propagate to Model's askForMessagesInMailbox(), which could in turn call beginInsertRows, leading to a possible recursion. */ bool modelResetInProgress; QModelIndexList oldPersistentIndexes; QList oldPtrs; /** @short There's a pending THREAD command for which we haven't received data yet */ bool threadingInFlight; /** @short Is threading enabled, or shall we just use other features like sorting and filtering? */ bool m_shallBeThreading; /** @short Are we filtering the mailbox by search? */ bool m_filteredBySearch; /** @short Task handling the SORT command */ QPointer m_sortTask; /** @short Shall we sort in a reversed order? */ bool m_sortReverse; /** @short IDs of all thread roots when no sorting or filtering is applied */ QList threadedRootIds; /** @short Sorting criteria of the current copy of the sort result */ SortCriterium m_currentSortingCriteria; /** @short Search criteria of the current copy of the search/sort result */ QStringList m_currentSearchConditions; /** @short The current result of the SORT operation This variable holds the UIDs of all messages in this mailbox, sorted according to the current sorting criteria. */ Imap::Uids m_currentSortResult; /** @short Is the cached result of SEARCH/SORT fresh enough? */ typedef enum { RESULT_ASKED, /**< We've asked for the data */ RESULT_FRESH, /**< The response has just arrived and didn't get invalidated since then */ RESULT_INVALIDATED /**< A new message has arrived, rendering our copy invalid */ } ResultValidity; ResultValidity m_searchValidity; QTimer *m_delayedPrune; friend class ::ImapModelThreadingTest; // needs access to wantThreading(); }; } } #endif /* IMAP_THREADINGMSGLISTMODEL_H */