controller/ServerSearchSession.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "ServerSearchSession.h" | ||
| 2 | |||
| 3 | #include "service/ImapService.h" | ||
| 4 | #include <QLoggingCategory> | ||
| 5 | #include <algorithm> | ||
| 6 | #include <utility> | ||
| 7 | |||
| 8 | // Same category as MailController (defined there): server-search logs keep | ||
| 9 | // their name and filtering (docs/LOGGING.md). | ||
| 10 | Q_DECLARE_LOGGING_CATEGORY(lcController) | ||
| 11 | |||
| 12 | 1051 | ServerSearchSession::ServerSearchSession(ImapService *const &searchImap, | |
| 13 | const QStringList &subscribedFolders, | ||
| 14 | 1051 | Deps deps, QObject *parent) | |
| 15 | 1051 | : QObject(parent), m_subscribedFolders(subscribedFolders), m_deps(std::move(deps)), | |
| 16 |
8/8✓ Branch 6 → 7 taken 1051 times.
✓ Branch 7 → 8 taken 1051 times.
✓ Branch 8 → 9 taken 1051 times.
✓ Branch 9 → 10 taken 1051 times.
✓ Branch 10 → 11 taken 1051 times.
✓ Branch 11 → 12 taken 1051 times.
✓ Branch 12 → 13 taken 1051 times.
✓ Branch 13 → 14 taken 1051 times.
|
2102 | m_searchImap(searchImap) {} |
| 17 | |||
| 18 | // Sprint 59 (S1): translate the local SearchFilter + free text into the | ||
| 19 | // server-mappable IMAP criteria. has:attachment has no standard SEARCH key, so | ||
| 20 | // it is intentionally dropped here and left to the local FTS/cache. | ||
| 21 | static ImapService::SearchCriteria | ||
| 22 | 15 | toImapCriteria(const QString &freeText, | |
| 23 | const MailCache::SearchFilter &filter) { | ||
| 24 | using FTri = MailCache::SearchFilter::Tri; | ||
| 25 | using ITri = ImapService::SearchTri; | ||
| 26 | 45 | auto tri = [](FTri t) { | |
| 27 |
2/4✓ Branch 2 → 3 taken 45 times.
✗ Branch 2 → 7 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 45 times.
|
45 | return t == FTri::Yes ? ITri::Yes : t == FTri::No ? ITri::No : ITri::Any; |
| 28 | }; | ||
| 29 | |||
| 30 | 15 | ImapService::SearchCriteria c; | |
| 31 | 15 | c.text = freeText; | |
| 32 | 15 | c.from = filter.fromFilter; | |
| 33 | 15 | c.to = filter.toFilter; | |
| 34 | 15 | c.listId = filter.listFilter; | |
| 35 | 15 | c.subject = filter.subjectFilter; | |
| 36 |
2/3✓ Branch 7 → 8 taken 15 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 15 times.
|
15 | if (filter.dateFrom.isValid()) |
| 37 | ✗ | c.since = filter.dateFrom.date(); | |
| 38 |
2/3✓ Branch 10 → 11 taken 15 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 15 taken 15 times.
|
15 | if (filter.dateTo.isValid()) |
| 39 | ✗ | c.before = filter.dateTo.date().addDays(1); // inclusive UI → exclusive BEFORE | |
| 40 | 15 | c.unread = tri(filter.unread); | |
| 41 | 15 | c.flagged = tri(filter.flagged); | |
| 42 | 15 | c.answered = tri(filter.answered); | |
| 43 | 15 | c.keywords = filter.tags; | |
| 44 | // Sprint 101 D-14: negations map to NOT …; excluded folders are skipped | ||
| 45 | // when the folder queue is built (serverSearch). | ||
| 46 | 15 | c.notText = filter.excludedTerms; | |
| 47 | 15 | c.notFrom = filter.excludedFrom; | |
| 48 | 15 | c.notTo = filter.excludedTo; | |
| 49 | 15 | c.notListId = filter.excludedLists; | |
| 50 | 15 | c.notSubject = filter.excludedSubjects; | |
| 51 | 15 | c.notKeywords = filter.excludedTags; | |
| 52 | 15 | return c; | |
| 53 | ✗ | } | |
| 54 | |||
| 55 | 1 | void ServerSearchSession::setServerSearchExcludedFolders( | |
| 56 | const QStringList &folders) { | ||
| 57 | 1 | m_serverSearchExcludedFolders.clear(); | |
| 58 |
2/2✓ Branch 18 → 5 taken 2 times.
✓ Branch 18 → 19 taken 1 time.
|
3 | for (const QString &folder : folders) { |
| 59 |
1/1✓ Branch 6 → 7 taken 2 times.
|
2 | const QString path = folder.trimmed(); |
| 60 |
2/4✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 12 not taken.
✓ Branch 13 → 14 taken 2 times.
✗ Branch 13 → 15 not taken.
|
4 | if (!path.isEmpty() && |
| 61 |
1/2✓ Branch 10 → 11 taken 2 times.
✗ Branch 10 → 12 not taken.
|
2 | !m_serverSearchExcludedFolders.contains(path, Qt::CaseInsensitive)) |
| 62 |
1/1✓ Branch 14 → 15 taken 2 times.
|
2 | m_serverSearchExcludedFolders.append(path); |
| 63 | 2 | } | |
| 64 | 1 | } | |
| 65 | |||
| 66 | 20 | void ServerSearchSession::serverSearch(const QString &freeText, | |
| 67 | const MailCache::SearchFilter &filter, const QString &owner) { | ||
| 68 | 20 | m_deps.ensureConnection(); | |
| 69 | 20 | m_owner = owner; | |
| 70 | 20 | m_paused = false; | |
| 71 | 20 | m_totalFolders = 0; | |
| 72 | |||
| 73 | // T-195: Disconnect any previous one-shot connections (re-entry safety). | ||
| 74 | // S106-188: that includes the previous scan's header fetch — its late | ||
| 75 | // headerFetchComplete would otherwise store the old folder's headers under | ||
| 76 | // the new scan's folder id and report them as the new scan's hits. | ||
| 77 | 20 | QObject::disconnect(m_searchStateConn); | |
| 78 | 20 | QObject::disconnect(m_searchFolderConn); | |
| 79 | 20 | QObject::disconnect(m_searchFailConn); | |
| 80 | 20 | QObject::disconnect(m_searchResultConn); | |
| 81 | 20 | QObject::disconnect(m_searchRefusedConn); | |
| 82 | 20 | dropHeaderFetch(); | |
| 83 | |||
| 84 |
6/6✓ Branch 11 → 12 taken 17 times.
✓ Branch 11 → 15 taken 3 times.
✓ Branch 13 → 14 taken 13 times.
✓ Branch 13 → 15 taken 4 times.
✓ Branch 16 → 17 taken 13 times.
✓ Branch 16 → 37 taken 7 times.
|
37 | if (m_searchImap->state() != ImapService::State::Authenticated && |
| 85 | 17 | m_searchImap->state() != ImapService::State::Selected) { | |
| 86 | // Not ready yet — wait for authentication, then retry | ||
| 87 |
5/5✓ Branch 17 → 18 taken 13 times.
✓ Branch 21 → 22 taken 13 times.
✓ Branch 22 → 23 taken 13 times.
✓ Branch 29 → 19 taken 13 times.
✓ Branch 29 → 30 taken 13 times.
|
26 | qCInfo(lcController) << "Search IMAP not ready (state:" |
| 88 |
1/1✓ Branch 24 → 25 taken 13 times.
|
13 | << static_cast<int>(m_searchImap->state()) |
| 89 |
1/1✓ Branch 25 → 26 taken 13 times.
|
13 | << ") — waiting for Authenticated"; |
| 90 | // S106-193: the scan starts from here, so a pause() requested while | ||
| 91 | // waiting holds it (serverSearch() would reset the pause). | ||
| 92 | 13 | m_searchStateConn = connect( | |
| 93 | 13 | m_searchImap, &ImapService::stateChanged, this, | |
| 94 |
1/3✓ Branch 32 → 33 taken 13 times.
✗ Branch 46 → 47 not taken.
✗ Branch 46 → 48 not taken.
|
26 | [this, freeText, filter](ImapService::State s) { |
| 95 |
3/4✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 1 time.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 2 times.
|
3 | if (s == ImapService::State::Authenticated || |
| 96 | s == ImapService::State::Selected) { | ||
| 97 | 1 | QObject::disconnect(m_searchStateConn); | |
| 98 | 1 | beginScan(freeText, filter); | |
| 99 | } | ||
| 100 | 13 | }); | |
| 101 | 13 | return; | |
| 102 | } | ||
| 103 | 7 | beginScan(freeText, filter); | |
| 104 | } | ||
| 105 | |||
| 106 | 8 | void ServerSearchSession::beginScan(const QString &freeText, const MailCache::SearchFilter &filter) { | |
| 107 | 8 | m_searchQuery = freeText; | |
| 108 | 8 | m_searchFilter = filter; | |
| 109 | |||
| 110 | // If nothing maps to a server SEARCH key (e.g. a has:attachment-only search), | ||
| 111 | // skip the server scan entirely — the local FTS/cache already has the answer. | ||
| 112 | // Sprint 103 D-21: about:<name> names mails linked locally, which are in | ||
| 113 | // the cache; IMAP has no key for it. | ||
| 114 |
8/12✓ Branch 5 → 6 taken 8 times.
✗ Branch 5 → 9 not taken.
✓ Branch 6 → 7 taken 8 times.
✓ Branch 7 → 8 taken 8 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 7 times.
✓ Branch 11 → 12 taken 8 times.
✗ Branch 11 → 13 not taken.
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 26 taken 7 times.
✗ Branch 108 → 109 not taken.
✗ Branch 108 → 110 not taken.
|
8 | if (!filter.aboutFilter.isEmpty() || toImapCriteria(freeText, filter).isEmpty()) { |
| 115 |
4/4✓ Branch 14 → 15 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 23 → 16 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
|
2 | qCInfo(lcController) << "Server search: no server-mappable criteria — " |
| 116 |
1/1✓ Branch 19 → 20 taken 1 time.
|
1 | "serving from local cache only"; |
| 117 |
1/1✓ Branch 24 → 25 taken 1 time.
|
1 | emit serverSearchComplete(); |
| 118 | 1 | return; | |
| 119 | } | ||
| 120 | |||
| 121 | // Build folder search queue: all subscribed folders, skip special ones. | ||
| 122 | // Sprint 60 (S1): when folder filter(s) are given (from "folder:" prefixes), | ||
| 123 | // only folders whose path contains ANY of the patterns are searched (OR). | ||
| 124 | 7 | QStringList folderFilters; | |
| 125 |
2/2✓ Branch 36 → 28 taken 3 times.
✓ Branch 36 → 37 taken 7 times.
|
10 | for (const QString &p : filter.folderPatterns) |
| 126 |
2/3✓ Branch 29 → 30 taken 3 times.
✓ Branch 32 → 33 taken 3 times.
✗ Branch 32 → 34 not taken.
|
3 | if (!p.trimmed().isEmpty()) |
| 127 |
1/1✓ Branch 33 → 34 taken 3 times.
|
3 | folderFilters.append(p); |
| 128 | 7 | const bool hasFolderFilter = !folderFilters.isEmpty(); | |
| 129 |
1/1✓ Branch 38 → 39 taken 7 times.
|
7 | m_searchPendingFolders.clear(); |
| 130 |
2/2✓ Branch 88 → 41 taken 18 times.
✓ Branch 88 → 89 taken 7 times.
|
25 | for (const QString &folder : m_subscribedFolders) { |
| 131 |
2/2✓ Branch 42 → 43 taken 8 times.
✓ Branch 42 → 55 taken 10 times.
|
18 | if (hasFolderFilter) { |
| 132 | 8 | bool matchesAny = false; | |
| 133 |
4/4✓ Branch 43 → 44 taken 8 times.
✓ Branch 44 → 45 taken 8 times.
✓ Branch 52 → 46 taken 8 times.
✓ Branch 52 → 53 taken 5 times.
|
13 | for (const QString &f : folderFilters) { |
| 134 |
3/3✓ Branch 47 → 48 taken 8 times.
✓ Branch 48 → 49 taken 3 times.
✓ Branch 48 → 50 taken 5 times.
|
8 | if (folder.contains(f, Qt::CaseInsensitive)) { |
| 135 | 3 | matchesAny = true; | |
| 136 | 3 | break; | |
| 137 | } | ||
| 138 | } | ||
| 139 |
2/2✓ Branch 53 → 54 taken 5 times.
✓ Branch 53 → 55 taken 3 times.
|
8 | if (!matchesAny) |
| 140 | 5 | continue; | |
| 141 | } | ||
| 142 | 13 | bool skip = false; | |
| 143 | // Sprint 101 D-14: "-folder:x" never scans a matching folder. | ||
| 144 |
1/2✗ Branch 71 → 57 not taken.
✓ Branch 71 → 72 taken 13 times.
|
13 | for (const QString &excluded : filter.excludedFolders) |
| 145 | ✗ | if (!excluded.trimmed().isEmpty() && folder.contains(excluded, Qt::CaseInsensitive)) skip = true; | |
| 146 | // A folder explicitly targeted via folder: is searched even if it is one of | ||
| 147 | // the normally-skipped special folders (the user asked for it). | ||
| 148 |
2/2✓ Branch 72 → 73 taken 10 times.
✓ Branch 72 → 84 taken 3 times.
|
13 | if (!hasFolderFilter) { |
| 149 |
4/4✓ Branch 73 → 74 taken 10 times.
✓ Branch 74 → 75 taken 10 times.
✓ Branch 82 → 76 taken 7 times.
✓ Branch 82 → 83 taken 8 times.
|
15 | for (const QString &excluded : m_serverSearchExcludedFolders) { |
| 150 |
2/2✓ Branch 78 → 79 taken 2 times.
✓ Branch 78 → 80 taken 5 times.
|
7 | if (folder.compare(excluded, Qt::CaseInsensitive) == 0) { |
| 151 | 2 | skip = true; | |
| 152 | 2 | break; | |
| 153 | } | ||
| 154 | } | ||
| 155 | } | ||
| 156 |
2/2✓ Branch 84 → 85 taken 11 times.
✓ Branch 84 → 86 taken 2 times.
|
13 | if (!skip) |
| 157 |
1/1✓ Branch 85 → 86 taken 11 times.
|
11 | m_searchPendingFolders.append(folder); |
| 158 | } | ||
| 159 | |||
| 160 | // Privacy (Sprint 101 rule 11, Sprint 98 G18): the query text is never logged. | ||
| 161 |
7/7✓ Branch 89 → 90 taken 7 times.
✓ Branch 93 → 94 taken 7 times.
✓ Branch 94 → 95 taken 7 times.
✓ Branch 96 → 97 taken 7 times.
✓ Branch 97 → 98 taken 7 times.
✓ Branch 101 → 91 taken 7 times.
✓ Branch 101 → 102 taken 7 times.
|
14 | qCInfo(lcController) << "Server search: queued" << m_searchPendingFolders.size() << "folders"; |
| 162 | 7 | m_totalFolders = int(m_searchPendingFolders.size()); | |
| 163 |
1/1✓ Branch 103 → 104 taken 7 times.
|
7 | emit serverSearchProgress(0, m_totalFolders); |
| 164 |
1/1✓ Branch 104 → 105 taken 7 times.
|
7 | searchNextFolder(); |
| 165 | 7 | } | |
| 166 | |||
| 167 | 5 | int ServerSearchSession::doneFolders() const { | |
| 168 |
1/2✓ Branch 4 → 5 taken 5 times.
✗ Branch 4 → 6 not taken.
|
5 | return qMax(0, m_totalFolders - int(m_searchPendingFolders.size()) - (m_searchCurrentFolder.isEmpty() ? 0 : 1)); |
| 169 | } | ||
| 170 | |||
| 171 | // S101.1.7 (D-12): switching away from the owning search tab stops the scan | ||
| 172 | // after the folder in flight; resume() continues with the remaining queue. | ||
| 173 | 1 | void ServerSearchSession::pause() { | |
| 174 |
3/6✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 7 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1 time.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
|
1 | if (!running() && !awaitingConnection()) return; |
| 175 | 1 | m_paused = true; | |
| 176 |
7/7✓ Branch 10 → 11 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 17 → 18 taken 1 time.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 22 → 12 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
|
2 | qCInfo(lcController) << "Server search: paused (" << m_searchPendingFolders.size() << "folders remaining)"; |
| 177 | } | ||
| 178 | |||
| 179 | 1 | void ServerSearchSession::resume() { | |
| 180 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
|
1 | if (!m_paused) return; |
| 181 | 1 | m_paused = false; | |
| 182 |
7/7✓ Branch 4 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 16 → 6 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
|
2 | qCInfo(lcController) << "Server search: resumed (" << m_searchPendingFolders.size() << "folders remaining)"; |
| 183 |
1/2✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 1 time.
|
1 | if (awaitingConnection()) return; // the scan starts once the connection is ready |
| 184 |
1/2✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 23 not taken.
|
1 | if (m_searchCurrentFolder.isEmpty()) searchNextFolder(); |
| 185 | } | ||
| 186 | |||
| 187 | 16 | void ServerSearchSession::searchNextFolder() { | |
| 188 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 5 taken 15 times.
|
16 | if (m_paused) { |
| 189 | 1 | m_searchCurrentFolder.clear(); | |
| 190 | 1 | m_searchCurrentFolderId = -1; | |
| 191 | 1 | return; | |
| 192 | } | ||
| 193 |
2/2✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 20 taken 12 times.
|
15 | if (m_searchPendingFolders.isEmpty()) { |
| 194 |
5/5✓ Branch 7 → 8 taken 3 times.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 12 → 13 taken 3 times.
✓ Branch 16 → 9 taken 3 times.
✓ Branch 16 → 17 taken 3 times.
|
6 | qCInfo(lcController) << "Server search: all folders searched"; |
| 195 | 3 | m_searchCurrentFolder.clear(); | |
| 196 | 3 | m_searchCurrentFolderId = -1; | |
| 197 | 3 | emit serverSearchComplete(); | |
| 198 | 3 | return; | |
| 199 | } | ||
| 200 | |||
| 201 | // Disconnect previous one-shot connections | ||
| 202 | 12 | QObject::disconnect(m_searchFolderConn); | |
| 203 | 12 | QObject::disconnect(m_searchFailConn); | |
| 204 | 12 | QObject::disconnect(m_searchResultConn); | |
| 205 | 12 | QObject::disconnect(m_searchRefusedConn); | |
| 206 | |||
| 207 |
1/1✓ Branch 24 → 25 taken 12 times.
|
12 | m_searchCurrentFolder = m_searchPendingFolders.takeFirst(); |
| 208 | 12 | m_searchCurrentFolderId = m_deps.resolveFolderId(m_searchCurrentFolder); | |
| 209 | 12 | m_searchCurrentUidValidity = 0; | |
| 210 | |||
| 211 | // Sprint 101 G11: folder paths only at debug level. | ||
| 212 |
7/7✓ Branch 28 → 29 taken 12 times.
✓ Branch 32 → 33 taken 12 times.
✓ Branch 33 → 34 taken 12 times.
✓ Branch 35 → 36 taken 12 times.
✓ Branch 36 → 37 taken 12 times.
✓ Branch 40 → 30 taken 12 times.
✓ Branch 40 → 41 taken 12 times.
|
24 | qCInfo(lcController) << "Server search: SELECT next folder (" << m_searchPendingFolders.size() << "remaining)"; |
| 213 |
6/6✓ Branch 41 → 42 taken 12 times.
✓ Branch 45 → 46 taken 12 times.
✓ Branch 46 → 47 taken 12 times.
✓ Branch 47 → 48 taken 12 times.
✓ Branch 51 → 43 taken 12 times.
✓ Branch 51 → 52 taken 12 times.
|
24 | qCDebug(lcController) << "Server search: SELECT" << m_searchCurrentFolder; |
| 214 | |||
| 215 | // Step 1: SELECT folder. We must react to BOTH outcomes: | ||
| 216 | // - folderSelected → folder is ready, send SEARCH | ||
| 217 | // - folderSelectFailed → folder cannot be selected (e.g. \Noselect | ||
| 218 | // container), skip it and continue. Without this the | ||
| 219 | // whole search would stall forever on that folder. | ||
| 220 | // NOTE: not Qt::SingleShotConnection — a mismatching path must NOT consume | ||
| 221 | // the connection, otherwise the real folderSelected would be missed. | ||
| 222 | 12 | m_searchFolderConn = connect( | |
| 223 | 12 | m_searchImap, &ImapService::folderSelected, this, | |
| 224 |
1/1✓ Branch 52 → 53 taken 12 times.
|
12 | [this](const QString &path, int, quint32 uidValidity, quint64) { |
| 225 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 7 times.
|
7 | if (path != m_searchCurrentFolder) return; |
| 226 | 7 | QObject::disconnect(m_searchFolderConn); | |
| 227 | 7 | QObject::disconnect(m_searchFailConn); | |
| 228 | 7 | m_searchCurrentUidValidity = uidValidity; | |
| 229 | |||
| 230 | // Step 2: Connect result handler BEFORE sending SEARCH | ||
| 231 | 7 | m_currentFolderRefused = false; | |
| 232 | 7 | m_searchRefusedConn = connect(m_searchImap, &ImapService::searchFailed, this, | |
| 233 |
1/1✓ Branch 7 → 8 taken 7 times.
|
7 | [this](const QString &) { m_currentFolderRefused = true; }); |
| 234 | 7 | m_searchResultConn = connect( | |
| 235 | 7 | m_searchImap, &ImapService::searchResultReceived, this, | |
| 236 |
1/1✓ Branch 10 → 11 taken 7 times.
|
7 | [this](const QList<qint64> &uids) { |
| 237 | 6 | QObject::disconnect(m_searchResultConn); | |
| 238 | 6 | QObject::disconnect(m_searchRefusedConn); | |
| 239 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 17 taken 5 times.
|
6 | if (std::exchange(m_currentFolderRefused, false)) { |
| 240 |
5/5✓ Branch 6 → 7 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 15 → 8 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
|
2 | qCWarning(lcController) << "Server search: the server refused the search in a folder"; |
| 241 | 1 | emit serverSearchFolderFailed(m_searchCurrentFolder); | |
| 242 | } | ||
| 243 |
7/7✓ Branch 17 → 18 taken 6 times.
✓ Branch 21 → 22 taken 6 times.
✓ Branch 22 → 23 taken 6 times.
✓ Branch 24 → 25 taken 6 times.
✓ Branch 25 → 26 taken 6 times.
✓ Branch 29 → 19 taken 6 times.
✓ Branch 29 → 30 taken 6 times.
|
12 | qCInfo(lcController) << "Server search:" << uids.size() << "results in the folder"; |
| 244 |
6/6✓ Branch 30 → 31 taken 6 times.
✓ Branch 34 → 35 taken 6 times.
✓ Branch 35 → 36 taken 6 times.
✓ Branch 36 → 37 taken 6 times.
✓ Branch 40 → 32 taken 6 times.
✓ Branch 40 → 41 taken 6 times.
|
12 | qCDebug(lcController) << "Server search: results in" << m_searchCurrentFolder; |
| 245 | 6 | fetchUncachedThenFinish(uids); | |
| 246 | 13 | }); | |
| 247 | |||
| 248 | // Step 3: Send the composite SEARCH built from free text + facets. | ||
| 249 |
2/2✓ Branch 13 → 14 taken 7 times.
✓ Branch 14 → 15 taken 7 times.
|
7 | m_searchImap->search(toImapCriteria(m_searchQuery, m_searchFilter)); |
| 250 | 12 | }); | |
| 251 | |||
| 252 | 12 | m_searchFailConn = connect( | |
| 253 | 12 | m_searchImap, &ImapService::folderSelectFailed, this, | |
| 254 |
1/1✓ Branch 55 → 56 taken 12 times.
|
12 | [this](const QString &path) { |
| 255 | ✗ | if (path != m_searchCurrentFolder) return; | |
| 256 | ✗ | QObject::disconnect(m_searchFolderConn); | |
| 257 | ✗ | QObject::disconnect(m_searchFailConn); | |
| 258 | ✗ | qCWarning(lcController) << "Server search: SELECT failed — skipping the folder"; | |
| 259 | ✗ | qCDebug(lcController) << "Server search: SELECT failed for" << path; | |
| 260 | ✗ | m_searchCurrentFolder.clear(); | |
| 261 | ✗ | emit serverSearchProgress(doneFolders(), m_totalFolders); | |
| 262 | ✗ | searchNextFolder(); | |
| 263 | 12 | }); | |
| 264 | |||
| 265 | 12 | m_searchImap->selectFolder(m_searchCurrentFolder); | |
| 266 | } | ||
| 267 | |||
| 268 | 6 | void ServerSearchSession::fetchUncachedThenFinish(const QList<qint64> &uids) { | |
| 269 |
3/4✓ Branch 4 → 5 taken 6 times.
✗ Branch 4 → 9 not taken.
✓ Branch 6 → 7 taken 4 times.
✓ Branch 6 → 9 taken 2 times.
|
12 | QList<qint64> missing = m_deps.uncachedUids && m_searchCurrentFolderId >= 0 && !uids.isEmpty() |
| 270 |
2/3✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 9 not taken.
✓ Branch 7 → 8 taken 4 times.
|
12 | ? m_deps.uncachedUids(m_searchCurrentFolderId, uids) : QList<qint64>{}; |
| 271 |
6/8✓ Branch 11 → 12 taken 4 times.
✓ Branch 11 → 15 taken 2 times.
✓ Branch 13 → 14 taken 4 times.
✗ Branch 13 → 15 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 4 times.
✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 20 taken 4 times.
|
6 | if (missing.isEmpty() || !m_deps.storeHeaders || !m_searchImap) { |
| 272 |
1/1✓ Branch 18 → 19 taken 2 times.
|
2 | finishFolder(uids); |
| 273 | 2 | return; | |
| 274 | } | ||
| 275 |
3/3✓ Branch 20 → 21 taken 4 times.
✓ Branch 21 → 22 taken 4 times.
✓ Branch 22 → 23 taken 4 times.
|
4 | std::sort(missing.begin(), missing.end(), std::greater<>()); // the newest first |
| 276 |
1/2✗ Branch 24 → 25 not taken.
✓ Branch 24 → 39 taken 4 times.
|
4 | if (missing.size() > MaximumHeaderFetch) { |
| 277 | ✗ | qCInfo(lcController) << "Server search:" << missing.size() - MaximumHeaderFetch | |
| 278 | ✗ | << "uncached hits beyond the fetch limit"; | |
| 279 | ✗ | missing.resize(MaximumHeaderFetch); | |
| 280 | } | ||
| 281 |
7/7✓ Branch 39 → 40 taken 4 times.
✓ Branch 43 → 44 taken 4 times.
✓ Branch 44 → 45 taken 4 times.
✓ Branch 46 → 47 taken 4 times.
✓ Branch 47 → 48 taken 4 times.
✓ Branch 51 → 41 taken 4 times.
✓ Branch 51 → 52 taken 4 times.
|
8 | qCInfo(lcController) << "Server search: fetching" << missing.size() << "uncached headers"; |
| 282 |
1/1✓ Branch 52 → 53 taken 4 times.
|
4 | m_fetchedHeaders.clear(); |
| 283 | 4 | m_headerConn = connect(m_searchImap, &ImapService::headersReceived, this, | |
| 284 |
1/1✓ Branch 53 → 54 taken 4 times.
|
6 | [this](const QList<MailHeader> &headers) { m_fetchedHeaders += headers; }); |
| 285 |
1/1✓ Branch 57 → 58 taken 4 times.
|
8 | m_headerDoneConn = connect(m_searchImap, &ImapService::headerFetchComplete, this, [this, uids] { |
| 286 | 2 | const QList<MailHeader> headers = m_fetchedHeaders; | |
| 287 |
1/1✓ Branch 3 → 4 taken 2 times.
|
2 | dropHeaderFetch(); |
| 288 |
1/2✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 7 not taken.
|
2 | if (!headers.isEmpty()) |
| 289 |
1/1✓ Branch 6 → 7 taken 2 times.
|
2 | m_deps.storeHeaders(m_searchCurrentFolderId, m_searchCurrentUidValidity, headers); |
| 290 |
1/1✓ Branch 7 → 8 taken 2 times.
|
2 | finishFolder(uids); |
| 291 | 6 | }); | |
| 292 | // A failed fetch still reports the folder's cached hits. | ||
| 293 |
1/1✓ Branch 62 → 63 taken 4 times.
|
8 | m_headerFailConn = connect(m_searchImap, &ImapService::errorOccurred, this, [this, uids](const QString &error) { |
| 294 |
6/6✓ Branch 2 → 3 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 12 → 4 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
|
2 | qCWarning(lcController) << "Server search: header fetch failed:" << error; |
| 295 | 1 | dropHeaderFetch(); | |
| 296 | 1 | finishFolder(uids); | |
| 297 | 5 | }); | |
| 298 |
1/1✓ Branch 66 → 67 taken 4 times.
|
4 | m_searchImap->fetchHeadersByUids(missing); |
| 299 | 6 | } | |
| 300 | |||
| 301 | 5 | void ServerSearchSession::finishFolder(const QList<qint64> &uids) { | |
| 302 |
2/2✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 2 times.
|
5 | if (!uids.isEmpty()) emit serverSearchResultReceived(uids, m_searchCurrentFolderId, m_searchCurrentFolder); |
| 303 | 5 | m_searchCurrentFolder.clear(); | |
| 304 | 5 | emit serverSearchProgress(doneFolders(), m_totalFolders); | |
| 305 | 5 | searchNextFolder(); | |
| 306 | 5 | } | |
| 307 | |||
| 308 | 31 | void ServerSearchSession::dropHeaderFetch() { | |
| 309 | 31 | QObject::disconnect(m_headerConn); | |
| 310 | 31 | QObject::disconnect(m_headerDoneConn); | |
| 311 | 31 | QObject::disconnect(m_headerFailConn); | |
| 312 | 31 | m_fetchedHeaders.clear(); | |
| 313 | 31 | } | |
| 314 | |||
| 315 | 6 | void ServerSearchSession::cancelServerSearch() { | |
| 316 |
5/5✓ Branch 2 → 3 taken 6 times.
✓ Branch 6 → 7 taken 6 times.
✓ Branch 7 → 8 taken 6 times.
✓ Branch 14 → 4 taken 6 times.
✓ Branch 14 → 15 taken 6 times.
|
12 | qCInfo(lcController) << "Server search: cancelled (" |
| 317 |
2/2✓ Branch 9 → 10 taken 6 times.
✓ Branch 10 → 11 taken 6 times.
|
6 | << m_searchPendingFolders.size() << "folders remaining)"; |
| 318 | 6 | m_searchPendingFolders.clear(); | |
| 319 | 6 | m_searchCurrentFolder.clear(); | |
| 320 | 6 | m_searchCurrentFolderId = -1; | |
| 321 | 6 | m_searchQuery.clear(); | |
| 322 | 6 | m_searchFilter = {}; | |
| 323 | 6 | m_owner.clear(); | |
| 324 | 6 | m_paused = false; | |
| 325 | 6 | m_totalFolders = 0; | |
| 326 | 6 | QObject::disconnect(m_searchStateConn); | |
| 327 | 6 | QObject::disconnect(m_searchFolderConn); | |
| 328 | 6 | QObject::disconnect(m_searchFailConn); | |
| 329 | 6 | QObject::disconnect(m_searchResultConn); | |
| 330 | 6 | QObject::disconnect(m_searchRefusedConn); | |
| 331 | 6 | dropHeaderFetch(); | |
| 332 | 12 | } | |
| 333 | |||
| 334 | 1043 | void ServerSearchSession::abandon() { | |
| 335 |
5/6✓ Branch 3 → 4 taken 1042 times.
✓ Branch 3 → 7 taken 1 time.
✓ Branch 5 → 6 taken 1042 times.
✗ Branch 5 → 7 not taken.
✓ Branch 8 → 9 taken 1042 times.
✓ Branch 8 → 10 taken 1 time.
|
1043 | if (!running() && !awaitingConnection()) |
| 336 | 1042 | return; | |
| 337 |
5/5✓ Branch 10 → 11 taken 1 time.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 19 → 12 taken 1 time.
✓ Branch 19 → 20 taken 1 time.
|
2 | qCInfo(lcController) << "Server search: ended by an account switch"; |
| 338 | 1 | emit serverSearchComplete(); // still owned: the starting tab leaves "running" | |
| 339 | 1 | cancelServerSearch(); | |
| 340 | } | ||
| 341 | |||
| 342 | // Formerly part of MailController::ensureSearchConnection()'s stateChanged | ||
| 343 | // handler (Authenticated branch); the controller calls it right after its | ||
| 344 | // "connection ready" log. | ||
| 345 | 6 | void ServerSearchSession::resumeAfterReconnect() { | |
| 346 | // T-720: Active server-search recovery. If the connection | ||
| 347 | // died mid-search (m_searchCurrentFolder / | ||
| 348 | // m_searchPendingFolders non-empty), the one-shot SELECT/ | ||
| 349 | // SEARCH handlers were disconnected by Error/Disconnected. | ||
| 350 | // Resume the search here so the user's query completes | ||
| 351 | // instead of hanging forever (the stall the sprint plan | ||
| 352 | // calls out at MailController.cpp:1295-1358). | ||
| 353 |
4/4✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 6 taken 3 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 27 taken 3 times.
|
9 | if (!m_searchCurrentFolder.isEmpty() || |
| 354 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 3 times.
|
3 | !m_searchPendingFolders.isEmpty()) { |
| 355 |
5/5✓ Branch 9 → 10 taken 3 times.
✓ Branch 13 → 14 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
✓ Branch 21 → 11 taken 3 times.
✓ Branch 21 → 22 taken 3 times.
|
6 | qCInfo(lcController) << "T-720: Resuming server search after search-connection reconnect (pending=" |
| 356 |
2/2✓ Branch 16 → 17 taken 3 times.
✓ Branch 17 → 18 taken 3 times.
|
3 | << m_searchPendingFolders.size() << ")"; |
| 357 | // Put the current folder back at the head of the queue so | ||
| 358 | // searchNextFolder() retries it, then drains the rest. | ||
| 359 |
1/2✓ Branch 23 → 24 taken 3 times.
✗ Branch 23 → 25 not taken.
|
3 | if (!m_searchCurrentFolder.isEmpty()) |
| 360 | 3 | m_searchPendingFolders.prepend(m_searchCurrentFolder); | |
| 361 | 3 | m_searchCurrentFolder.clear(); | |
| 362 | 3 | searchNextFolder(); | |
| 363 | } | ||
| 364 | 6 | } | |
| 365 | |||
| 366 | // Formerly the Error/Disconnected branch of the same handler, called after | ||
| 367 | // the controller's warning log. | ||
| 368 | 2 | void ServerSearchSession::dropPendingHandlers() { | |
| 369 | // T-720: Disconnect the one-shot SELECT/SEARCH handlers so | ||
| 370 | // they cannot fire against the wrong socket state after | ||
| 371 | // reconnect. searchNextFolder() reconnects them. | ||
| 372 | 2 | QObject::disconnect(m_searchFolderConn); | |
| 373 | 2 | QObject::disconnect(m_searchFailConn); | |
| 374 | 2 | QObject::disconnect(m_searchResultConn); | |
| 375 | 2 | QObject::disconnect(m_searchRefusedConn); | |
| 376 | 2 | dropHeaderFetch(); // the folder is searched again after the reconnect | |
| 377 | 2 | } | |
| 378 |