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

0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
0002 
0003    This file is part of the Trojita Qt IMAP e-mail client,
0004    http://trojita.flaska.net/
0005 
0006    This program is free software; you can redistribute it and/or
0007    modify it under the terms of the GNU General Public License as
0008    published by the Free Software Foundation; either version 2 of
0009    the License or (at your option) version 3 or any later version
0010    accepted by the membership of KDE e.V. (or its successor approved
0011    by the membership of KDE e.V.), which shall act as a proxy
0012    defined in Section 14 of version 3 of the license.
0013 
0014    This program is distributed in the hope that it will be useful,
0015    but WITHOUT ANY WARRANTY; without even the implied warranty of
0016    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0017    GNU General Public License for more details.
0018 
0019    You should have received a copy of the GNU General Public License
0020    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0021 */
0022 
0023 #include "SortTask.h"
0024 #include <algorithm>
0025 #include "Imap/Model/ItemRoles.h"
0026 #include "Imap/Model/Model.h"
0027 #include "Imap/Model/MailboxTree.h"
0028 #include "Imap/Model/TaskPresentationModel.h"
0029 #include "KeepMailboxOpenTask.h"
0030 
0031 namespace Imap
0032 {
0033 namespace Mailbox
0034 {
0035 
0036 
0037 SortTask::SortTask(Model *model, const QModelIndex &mailbox, const QStringList &searchConditions, const QStringList &sortCriteria):
0038     ImapTask(model), mailboxIndex(mailbox), searchConditions(searchConditions), sortCriteria(sortCriteria),
0039     m_persistentSearch(false), m_firstUntaggedReceived(false), m_firstCommandCompleted(false)
0040 {
0041     conn = model->findTaskResponsibleFor(mailbox);
0042     conn->addDependentTask(this);
0043     if (searchConditions.isEmpty())
0044         this->searchConditions << QStringLiteral("ALL");
0045 }
0046 
0047 void SortTask::perform()
0048 {
0049     parser = conn->parser;
0050     markAsActiveTask();
0051 
0052     IMAP_TASK_CHECK_ABORT_DIE;
0053 
0054     if (! mailboxIndex.isValid()) {
0055         _failed(tr("Mailbox vanished before we could ask for threading info"));
0056         return;
0057     }
0058 
0059     // We can be killed when appropriate
0060     KeepMailboxOpenTask *keepTask = dynamic_cast<KeepMailboxOpenTask*>(conn);
0061     Q_ASSERT(keepTask);
0062     keepTask->feelFreeToAbortCaller(this);
0063 
0064     if (sortCriteria.isEmpty()) {
0065         if (model->accessParser(parser).capabilitiesFresh &&
0066                 model->accessParser(parser).capabilities.contains(QStringLiteral("ESEARCH"))) {
0067             // We always prefer ESEARCH over SEARCH, if only for its embedded reference to the command tag
0068             if (model->accessParser(parser).capabilities.contains(QStringLiteral("CONTEXT=SEARCH"))) {
0069                 // Hurray, this IMAP server supports incremental ESEARCH updates
0070                 m_persistentSearch = true;
0071                 sortTag = parser->uidESearch("utf-8", searchConditions,
0072                                              QStringList() << QStringLiteral("ALL") << QStringLiteral("UPDATE"));
0073             } else {
0074                 // ESORT without CONTEXT is still worth the effort, if only for the tag reference
0075                 sortTag = parser->uidESearch("utf-8", searchConditions, QStringList() << QStringLiteral("ALL"));
0076             }
0077         } else {
0078             // Plain "old" SORT
0079             sortTag = parser->uidSearch(searchConditions,
0080                                         // It looks that Exchange 2003 does not support the UTF-8 charset in searches.
0081                                         // That is, of course, insane, and only illustrates how useless its support of IMAP really is.
0082                                         model->m_capabilitiesBlacklist.contains(QStringLiteral("X-NO-UTF8-SEARCH")) ? QByteArray() : "utf-8"
0083                                         );
0084         }
0085     } else {
0086         // SEARCH and SORT combined
0087         if (model->accessParser(parser).capabilitiesFresh &&
0088                 model->accessParser(parser).capabilities.contains(QStringLiteral("ESORT"))) {
0089             // ESORT's better than regular SORT, if only for its embedded reference to the command tag
0090             if (model->accessParser(parser).capabilities.contains(QStringLiteral("CONTEXT=SORT"))) {
0091                 // Hurray, this IMAP server supports incremental SORT updates
0092                 m_persistentSearch = true;
0093                 sortTag = parser->uidESort(sortCriteria, "utf-8", searchConditions,
0094                                        QStringList() << QStringLiteral("ALL") << QStringLiteral("UPDATE"));
0095             } else {
0096                 // ESORT without CONTEXT is still worth the effort, if only for the tag reference
0097                 sortTag = parser->uidESort(sortCriteria, "utf-8", searchConditions, QStringList() << QStringLiteral("ALL"));
0098             }
0099         } else {
0100             // Plain "old" SORT
0101             sortTag = parser->uidSort(sortCriteria, "utf-8", searchConditions);
0102         }
0103     }
0104 }
0105 
0106 bool SortTask::handleStateHelper(const Imap::Responses::State *const resp)
0107 {
0108     if (resp->tag.isEmpty()) {
0109         if (resp->kind == Responses::NO && resp->respCode == Responses::NOUPDATE) {
0110             // * NO [NOUPDATE "tag"] means that the server won't be providing further updates for our SEARCH/SORT criteria
0111             const Responses::RespData<QString> *const untaggedTag = dynamic_cast<const Responses::RespData<QString>* const>(
0112                         resp->respCodeData.data());
0113             Q_ASSERT(untaggedTag);
0114             if (untaggedTag->data.toUtf8() == sortTag) {
0115                 m_persistentSearch = false;
0116                 model->m_taskModel->slotTaskMighHaveChanged(this);
0117 
0118                 if (m_firstCommandCompleted) {
0119                     // The server decided that it will no longer inform us about the updated SORT order, and the original
0120                     // response has been already received and processed. That means that we're done here and shall declare
0121                     // ourselves as completed.
0122                     _completed();
0123                 }
0124                 // We actually support even more benevolent mode of operation where the server can tell us at any time that
0125                 // this context updating is no longer supported. Yay for that; let's hope that it's reasonably bug-free now.
0126                 return true;
0127             }
0128         }
0129         return false;
0130     }
0131 
0132     if (resp->tag == sortTag) {
0133         m_firstCommandCompleted = true;
0134         if (resp->kind == Responses::OK) {
0135             emit sortingAvailable(sortResult);
0136             if (!m_persistentSearch || _aborted) {
0137                 // This is a one-shot operation, we shall not remain as an active task, listening for further updates
0138                 _completed();
0139             } else {
0140                 // got to prod the TaskPresentationModel
0141                 model->m_taskModel->slotTaskMighHaveChanged(this);
0142 
0143                 // Even though we aren't "finished" at this point, the KeepMailboxOpenTask is now free to issue its IDLE thing,
0144                 // as that won't interfere with our mode of operation. Let's kick it around.
0145                 KeepMailboxOpenTask *keepTask = dynamic_cast<KeepMailboxOpenTask*>(conn);
0146                 Q_ASSERT(keepTask);
0147                 keepTask->activateTasks();
0148             }
0149         } else {
0150             _failed(tr("Sorting command has failed"));
0151         }
0152         return true;
0153     } else if (resp->tag == cancelUpdateTag) {
0154         m_persistentSearch = false;
0155         model->m_taskModel->slotTaskMighHaveChanged(this);
0156         _completed();
0157         return true;
0158     } else {
0159         return false;
0160     }
0161 }
0162 
0163 bool SortTask::handleSort(const Imap::Responses::Sort *const resp)
0164 {
0165     sortResult = resp->numbers;
0166     return true;
0167 }
0168 
0169 bool SortTask::handleSearch(const Imap::Responses::Search *const resp)
0170 {
0171     if (searchConditions == QStringList() << QStringLiteral("ALL")) {
0172         // We're really a SORT task, so we shouldn't process this stuff
0173         return false;
0174     }
0175 
0176     // The actual data for the SEARCH response can be split into several responses.
0177     // Possible performance optimization might be to call sort & unique only after receiving the tagged OK,
0178     // but that'd also mean that one has to keep track of whether we're doing a SORT or SEARCH from there.
0179     // That just doesn't look like worth it.
0180 
0181     sortResult += resp->items;
0182     std::sort(sortResult.begin(), sortResult.end());
0183     sortResult.erase(std::unique(sortResult.begin(), sortResult.end()), sortResult.end());
0184     return true;
0185 }
0186 
0187 bool SortTask::handleESearch(const Responses::ESearch *const resp)
0188 {
0189     if (resp->tag != sortTag)
0190         return false;
0191 
0192     auto allComparator = [](const auto &listData) { return listData.first == "ALL"; };
0193     auto allIterator = std::find_if(resp->listData.constBegin(), resp->listData.constEnd(), allComparator);
0194 
0195     if (allIterator != resp->listData.constEnd()) {
0196         m_firstUntaggedReceived = true;
0197         sortResult = allIterator->second;
0198 
0199         if (!sortResult.isEmpty()) {
0200             // Only check when the result set is non-empty, https://bugs.kde.org/show_bug.cgi?id=350698
0201             if (resp->seqOrUids != Imap::Responses::ESearch::UIDS) {
0202                 throw UnexpectedResponseReceived("ESEARCH response to a UID SEARCH / UID SORT command with matching tag uses "
0203                                                  "sequence numbers instead of UIDs", *resp);
0204             }
0205         }
0206 
0207         ++allIterator;
0208         if (std::find_if(allIterator, resp->listData.constEnd(), allComparator) != resp->listData.constEnd())
0209             throw UnexpectedResponseReceived("ESEARCH contains the ALL key too many times", *resp);
0210 
0211         if (!resp->incrementalContextData.isEmpty())
0212             throw UnexpectedResponseReceived("ESEARCH contains both ALL result set and some incremental updates", *resp);
0213 
0214         return true;
0215     }
0216 
0217     Q_ASSERT(allIterator == resp->listData.constEnd());
0218 
0219     if (resp->incrementalContextData.isEmpty()) {
0220         sortResult.clear();
0221         // This means that there have been no matches
0222         // FIXME: cover this in the test suite!
0223         return true;
0224     } else {
0225         if (resp->seqOrUids != Imap::Responses::ESearch::UIDS) {
0226             throw UnexpectedResponseReceived("ESEARCH response to a UID SEARCH / UID SORT command with matching tag uses "
0227                                              "sequence numbers instead of UIDs", *resp);
0228         }
0229     }
0230 
0231     Q_ASSERT(!resp->incrementalContextData.isEmpty());
0232 
0233     if (!m_persistentSearch)
0234         throw UnexpectedResponseReceived("ESEARCH contains incremental responses even though we haven't requested that", *resp);
0235 
0236     emit incrementalSortUpdate(resp->incrementalContextData);
0237 
0238     return true;
0239 }
0240 
0241 QVariant SortTask::taskData(const int role) const
0242 {
0243     return role == RoleTaskCompactName ? QVariant(tr("Sorting mailbox")) : QVariant();
0244 }
0245 
0246 void SortTask::_failed(const QString &errorMessage)
0247 {
0248     // FIXME: show this in the GUI
0249     emit sortingFailed();
0250     ImapTask::_failed(errorMessage);
0251 }
0252 
0253 bool SortTask::isPersistent() const
0254 {
0255     return m_persistentSearch;
0256 }
0257 
0258 /** @short Return true if this task has already done its job and is now merely listening for further updates */
0259 bool SortTask::isJustUpdatingNow() const
0260 {
0261     return isPersistent() && m_firstCommandCompleted && !_aborted;
0262 }
0263 
0264 void SortTask::cancelSortingUpdates()
0265 {
0266     Q_ASSERT(m_persistentSearch);
0267     Q_ASSERT(!sortTag.isEmpty());
0268     KeepMailboxOpenTask *keepTask = dynamic_cast<KeepMailboxOpenTask*>(conn);
0269     Q_ASSERT(keepTask);
0270     keepTask->breakOrCancelPossibleIdle();
0271     cancelUpdateTag = parser->cancelUpdate(sortTag);
0272 }
0273 
0274 void SortTask::abort()
0275 {
0276     if (cancelUpdateTag.isEmpty() && isJustUpdatingNow()) {
0277         cancelSortingUpdates();
0278     }
0279     ImapTask::abort();
0280 }
0281 
0282 }
0283 }