MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 96.8% 61 / 0 / 63
Functions: 95.7% 44 / 0 / 46
Branches: 83.3% 10 / 0 / 12

controller/MailController.h
Line Branch Exec Source
1 #pragma once
2
3 #include "data/MailHeaderPage.h"
4 #include <QElapsedTimer>
5 #include <QHash>
6 #include <QList>
7 #include <QMap>
8 #include <QObject>
9 #include <QPair>
10 #include <QPointer>
11 #include <QSet>
12 #include <QString>
13 #include <QTimer>
14 #include <QThreadPool>
15 #include <atomic>
16 #include <memory>
17 #include <functional>
18 #include <optional>
19
20 #include "controller/BodyRequestRegistry.h"
21 #include "controller/FolderContext.h"
22 #include "controller/PendingFlagIntents.h"
23 #include "controller/FolderFlagSync.h"
24 #include "controller/MailBodyTransport.h"
25 #include "controller/MailMutationPipeline.h"
26 #include "controller/MailMoveCommands.h"
27 #include "controller/MailSelectionStateCommands.h"
28 #include "controller/ServerSearchSession.h"
29 #include "controller/MailDisplayPort.h"
30 #include "data/AccountConfig.h"
31 #include "data/MailCache.h" // Sprint 59 (S1): SearchFilter in serverSearch()
32 #include "data/MailDisplaySnapshot.h" // P90.8: preferred display projection
33 #include "data/Models.h"
34 #include "ui/MailListModel.h" // T-87.D1: cached page loader factory return type
35
36 struct MimeMessage;
37 struct MailBodyWriteResult;
38 class MailHeaderStateFence;
39 struct MailHeaderStreamIdentity;
40 class ImapService;
41 class MailCache;
42 class MailListModel;
43 class MailThreadModel;
44 #include "UndoManager.h"
45 class ConnectionHealthMonitor;
46
47 // MailController orchestrates the cache-first mail data flow with live updates:
48 //
49 // Folder change:
50 // 1. Load cached headers → display immediately (with correct badges)
51 // 2. executeAfterIdle → SELECT folder on IMAP
52 // 3. Check UIDVALIDITY → if changed, purge + full sync
53 // 4. Fetch headers since maxUid → append (streaming in 50er batches)
54 // 5. headerFetchComplete → fetchFlags → sync with server
55 // 6. Flag sync done → startIdle
56 //
57 // IDLE events (all use executeAfterIdle for safe timing):
58 // - New messages → fetch headers → append
59 // - Flag changes → update cache + model
60 // - Expunge → re-fetch flags to detect removals
61 //
62 // Polling (non-IDLE folders):
63 // - Timer-triggered, pauses IDLE via executeAfterIdle
64 // - Sequential STATUS for all subscribed folders
65 // - Re-starts IDLE after all STATUS complete
66 class MailController : public QObject {
67 1495 Q_OBJECT
68 public:
69 struct PublishedFolderSummary {
70 qint64 folderId = -1;
71 quint32 uidValidity = 0;
72 qint64 revision = -1;
73 int totalCount = -1;
74 int unreadCount = -1;
75 };
76 MailController(ImapService *imap, MailCache *cache, MailListModel *model,
77 MailDisplayPort *view, QObject *parent = nullptr);
78 ~MailController() override;
79
80 void setDisplayPort(MailDisplayPort *view);
81
82 void setAccount(const QString &accountId);
83 1614 QString accountId() const { return m_folder.accountId; }
84 799 qint64 currentFolderId() const { return m_folder.currentFolderId; }
85 // Read-only folder identity for the units composed into the controller.
86 const FolderContext &folderContext() const { return m_folder; }
87 qint64 resolveFolderId(const QString &folderPath);
88 // SF-04: the UIDs of `uids` in `folderId` without a cached header.
89 QList<qint64> uncachedUids(qint64 folderId, const QList<qint64> &uids) const;
90 883 QString currentFolder() const { return m_folder.currentFolder; }
91 // The first INBOX sync starts from an empty cache. Its headers are not new
92 // mail notifications; consumers may skip the whole batch before iterating.
93
2/4
✓ Branch 2 → 3 taken 4 times.
✗ Branch 2 → 5 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 4 times.
4 bool initialInboxLoad() const { return !m_inboxFirstSyncSignaled && m_inboxCacheWasEmpty; }
94 51 const QMap<QString, int> &lastPolledUnread() const { return m_mutations.intents().lastPolledUnreadCounts(); }
95 1015 void setThreadModel(MailThreadModel *model) { m_threadModel = model; }
96 // S91.2.6: IDLE pushes, flag-snapshot reconciliation, STATUS polling.
97 1638 FolderFlagSync *flagSync() { return &m_flagSync; }
98 // S91.4.1: body requests, preparation pipeline, prefetch, downloads.
99 1119 MailBodyTransport *bodies() { return &m_bodies; }
100 // S91.4.2: mutation journal dispatch, APPEND queue, verification.
101 288 MailMutationPipeline *mutations() { return &m_mutations; }
102 // Plan correction S91.4.4: visual/cross-folder moves, move results, undo.
103 87 MailMoveCommands *moves() { return &m_moves; }
104 // MAIL-SELECTION-ACTIONS: read/important/labels for a whole selection.
105 16 MailSelectionStateCommands *selectionStates() { return &m_selectionStates; }
106 // Quick's published indexed page supplies the header of a visible row
107 // (P90.17: no GUI SQLite read before the move's visual commit).
108 795 void setIndexedHeaderProvider(
109 std::function<std::optional<MailHeader>(const MailId &)> provider) {
110 795 m_indexedHeader = std::move(provider);
111 795 }
112 // True while a move of this mail is pending in this session (visual
113 // commit until journal settlement); in-memory, no SQLite read.
114 33 bool hasSessionPendingMove(const MailId &mail) const { return m_mutations.intents().hasPendingMove(mail); }
115 // knownFolderId comes from the active account's path-to-ID cache; -1
116 // requests the legacy ensure path for a newly discovered folder.
117 void onFolderSelected(const QString &folderPath, bool countsOnly,
118 std::optional<PublishedFolderSummary> published = {},
119 qint64 knownFolderId = -1);
120 // P90.8: preferred display projection ("text" loads only the plain
121 // variant, "light"/"html" only text_html; anything else loads both).
122 // Driven from the workspace reading mode before each display read.
123 void setPreferredDisplayContent(const QString &mode);
124 // Quick's indexed page can supply its already-published preview while the
125 // canonical body snapshot is still on the read lane.
126 void onMailSelected(const MailId &mailId, const MailHeader &previewHint);
127 void setSubscribedFolders(const QStringList &folders);
128 17 QStringList subscribedFolders() const { return m_subscribedFolders; }
129 1032 void setUndoManager(UndoManager *mgr) { m_undoManager = mgr; }
130 #ifdef MAILJD_E2E_TESTING
131 void setNextBodyCompletionForE2e(int delayMs, bool fail, const MailId &mailId = {});
132 2 ImapService *bodyTransportForE2e() {
133 2 ensureBodyConnection();
134 2 return m_bodyImap;
135 }
136 1 ImapService *searchTransportForE2e() {
137 1 ensureSearchConnection();
138 1 return m_searchImap;
139 }
140 #endif
141
142 public slots:
143 void onFolderSelected(const QString &folderPath);
144 5 void setFolderPresentationEnabled(bool enabled) { m_folderPresentationEnabled = enabled; }
145 // Quick's indexed thread page owns large-folder presentation. Keep its
146 // hidden flat window bounded while headers stream into the canonical cache.
147 805 void setStreamedHeadersVisible(bool visible) { m_streamedHeadersVisible = visible; }
148 // Returns true when the flat list's rows will be replaced by a page read
149 // with this filter (the caller need not filter the loaded rows itself).
150 bool setFlatFilter(const MailHeaderFilter &filter);
151 // The flat list shows a filtered page: a mail missing from it is hidden,
152 // not deleted.
153 1631 bool flatFilterActive() const { return m_flatFilter.active(); }
154 // F100-66: the flat list's order; a change reloads the first page read in
155 // that order from the whole folder (like a filter change).
156 bool setFlatSort(const MailSortOrder &order);
157 ✗ const MailSortOrder &flatSort() const { return m_flatSort; }
158 // Supplies the unread view's retained rows when a filtered page is read.
159 795 void setRetainedUnreadProvider(std::function<QSet<qint64>(qint64 folderId)> provider) {
160 795 m_retainedUnreadProvider = std::move(provider);
161 795 }
162 ✗ bool streamedHeadersVisible() const { return m_streamedHeadersVisible; }
163 void refreshFlatPageIfNeeded(bool allowActiveStream = false);
164 void resyncCurrentFolder();
165 void reconcileAvailableFolders(const QStringList &folderPaths);
166 void setRestoreAnchor(const QString &folderPath, qint64 uid);
167 void onMailSelected(const MailId &mailId);
168 // P90.8: fetch exactly the missing body variant for the still-displayed
169 // mail and deliver it as a merge (no new selection generation).
170 void reprojectBodyVariant(const MailId &mailId, bool needHtml);
171 void clearDisplaySelection();
172 void toggleReadStatus(qint64 uid);
173 void toggleStarred(qint64 uid);
174 // T-519: Idempotent setters (for command bar mark-read/mark-unread/star/unstar)
175 void markMailAsSeen(qint64 uid);
176 void markMailAsUnseen(qint64 uid);
177 void setStarred(qint64 uid, bool starred);
178 void addLabel(qint64 uid, const QString &label);
179 void removeLabel(qint64 uid, const QString &label);
180
181 // T-407: Cross-folder action overloads for search mode.
182 // These accept the source folderId/folderPath so they work correctly
183 // when the mail is not in the controller's currently selected folder.
184 void toggleReadStatusInFolder(qint64 uid, qint64 folderId,
185 6 std::optional<bool> visibleSeen = std::nullopt);
186 void toggleStarredInFolder(qint64 uid, qint64 folderId,
187 16 std::optional<bool> visibleStarred = std::nullopt);
188 void setStarredInFolder(qint64 uid, qint64 folderId, bool starred,
189 9 std::optional<bool> visibleStarred = std::nullopt);
190 // T-79.E1/M5: folder-aware seen setters (undo, notifications)
191 void markMailAsSeenInFolder(qint64 uid, qint64 folderId,
192 72 std::optional<bool> visibleSeen = std::nullopt);
193 void markMailAsUnseenInFolder(qint64 uid, qint64 folderId,
194 3 std::optional<bool> visibleSeen = std::nullopt);
195 // visibleLabels: the labels the caller shows for this mail (e.g. an
196 // indexed thread row outside the flat model). With a visible state the
197 // label commits visually first and journals on the durable writer.
198 void addLabelInFolder(qint64 uid, qint64 folderId, const QString &label,
199
7/7
✓ Branch 20 → 21 taken 1 time.
✓ Branch 41 → 42 taken 1 time.
✓ Branch 58 → 59 taken 1 time.
✓ Branch 78 → 79 taken 1 time.
✓ Branch 176 → 177 taken 1 time.
✓ Branch 219 → 220 taken 1 time.
✓ Branch 260 → 261 taken 1 time.
64 std::optional<QStringList> visibleLabels = std::nullopt);
200 void removeLabelInFolder(qint64 uid, qint64 folderId, const QString &label,
201
1/1
✓ Branch 136 → 137 taken 1 time.
39 std::optional<QStringList> visibleLabels = std::nullopt);
202
203 // T-200: Mark all mails in a folder as read
204 void markFolderAllSeen(const QString &folderPath);
205
206 // Search: configure credentials for the dedicated search connection
207 void setImapConfig(const ImapConfig &config);
208 void setNetworkEnabled(bool enabled);
209 2 bool networkEnabled() const { return m_networkEnabled; }
210
211 // S91.1.3: server-side IMAP SEARCH on the dedicated search connection.
212 1020 ServerSearchSession *search() { return &m_serverSearch; }
213
214 signals:
215 void folderIdentityInvalidated(qint64 folderId);
216 void statusMessage(const QString &message);
217 void actionError(const QString &message);
218 // Visual-first moves: rows left the list before the journal committed.
219 // Settled means the durable writer answered (journaled or rolled back);
220 // the mutation journal is then the authority for pending-move exclusions.
221 void movesVisuallyPending(qint64 folderId, const QList<qint64> &uids);
222 void movesVisuallySettled(qint64 folderId, const QList<qint64> &uids);
223 void seenVisualChanged(qint64 folderId, qint64 uid, bool seen);
224 void starVisualChanged(qint64 folderId, qint64 uid, bool starred);
225 // A label changed visually before (or was rolled back after) its journal
226 // write; projections outside the flat/thread models mirror it.
227 void labelVisualChanged(qint64 folderId, qint64 uid, const QString &label, bool present);
228 void pendingActionChanged(qint64 token, const QString &message);
229 void pendingActionsReset();
230 void backgroundStatusMessage(const QString &message);
231 void unreadCountChanged(const QString &folder, int count);
232 // Authoritative cached counts, including updates while search is displayed.
233 void cachedUnreadCountChanged(const QString &folder, int count);
234 // T-176: Emitted after headers are stored in cache (for predictor training)
235 void headersStored(const QString &folderPath, qint64 folderId,
236 quint32 uidValidity, const QList<MailHeader> &headers);
237 // Worker commit changed the derived thread projection revision. The
238 // committed header total lets Quick cross its indexed threshold without a
239 // GUI-thread SQL read; -1 means the worker could not confirm the epoch.
240 void headerProjectionAdvanced(const QString &folderPath, qint64 folderId,
241 int totalCount);
242 // T-540: Body loaded notification (for tab body fetch)
243 void bodyLoaded(qint64 uid, qint64 folderId);
244 void bodyRequestFinished(const MailId &mailId, BodyOutcome outcome,
245 const QString &error);
246 void cachedFolderPresented(qint64 folderId, int rowCount,
247 quint64 generation);
248 void authoritativeFolderPresented(qint64 folderId, int rowCount,
249 quint64 generation);
250 // 67.A2: First INBOX header sync of this session finished.
251 // initialLoad = true when the INBOX cache was empty when the sync
252 // started (first-ever mailbox load — nothing in it is "new mail").
253 void inboxFirstSyncCompleted(bool initialLoad);
254 void appendMutationCompleted(qint64 token, const QString &folderPath,
255 qint64 uid);
256 void appendMutationFailed(qint64 token, const QString &error);
257
258 // IMAP responses of the main connection (public: tests replay them).
259 public slots:
260 void onFolderSelectedFromImap(const QString &path, int messageCount,
261 quint32 uidValidity,
262 quint64 highestModseq); // T-208
263 void onHeadersReceived(const QList<MailHeader> &headers);
264 void onHeaderFetchComplete();
265
266 void onSearchResultReceived(const QList<qint64> &uids);
267
268
269 public:
270 // Test/diagnostic state contract (Sprint 91 rule 8 replaced the former
271 // test friend declarations). No product caller outside the controller
272 // uses it; tests inject connections and drive or inspect the folder-open
273 // state through it.
274 bool reconcileFolderIdentity(const QString &path, quint32 uidValidity);
275 void fetchNextChunk();
276 void ensureSearchConnection(); // Lazy-init second IMAP for search
277 void ensureBodyConnection(); // T-205: Lazy-init third IMAP for body
278 quint32 displayEpoch(const MailId &mailId) const;
279 bool sameDisplayEpoch(quint32 epoch, const MailDisplaySnapshot &snapshot) const;
280 void displayMailId(const MailId &mailId, const MailHeader *previewHint = nullptr);
281 // T-407: Execute a flag STORE on a different folder via m_bodyImap
282 void crossFolderStoreFlag(const QString &folderPath, qint64 uid,
283 const QString &flag, bool add,
284 qint64 mutationToken = 0);
285 void requestFolderCountStatus();
286 bool beginHeaderStream(const QList<qint64> &uids = {});
287 MailHeaderStreamIdentity headerStreamIdentity() const;
288 883 FolderContext &mutableFolderContext() { return m_folder; }
289 58 ImapService *&bodyConnection() { return m_bodyImap; }
290 30 ImapService *&searchConnection() { return m_searchImap; }
291 8 QString &bodyConnectionFolder() { return m_bodyImapSelectedFolder; }
292 3 ConnectionHealthMonitor *bodyHealth() const { return m_bodyHealth; }
293 1 ConnectionHealthMonitor *searchHealth() const { return m_searchHealth; }
294 5 ImapConfig &imapConfig() { return m_imapConfig; }
295 3 bool &networkEnabledState() { return m_networkEnabled; }
296 8 MailId &displayAwaitingNetwork() { return m_displayAwaitingNetwork; }
297 3 QTimer *folderSwitchTimer() const { return m_folderSwitchTimer; }
298 2 QElapsedTimer &folderSwitchStopwatch() { return m_folderSwitchStopwatch; }
299 1 bool &bodyFetchSelect() { return m_bodyFetchSelect; }
300 14 bool &pendingHeaderFetch() { return m_pendingHeaderFetch; }
301 6 bool &fetchInProgress() { return m_fetchInProgress; }
302 2 bool &openedWithUidSearch() { return m_openedWithUidSearch; }
303 2 bool &openedWithChangedSince() { return m_openedWithChangedSince; }
304 70 int &pendingHeaderWrites() { return m_pendingHeaderWrites; }
305 6 QString &headerCacheError() { return m_headerCacheError; }
306 15 QList<QList<qint64>> &reverseChunks() { return m_reverseChunks; }
307 6 std::shared_ptr<MailHeaderStateFence> &headerStream() { return m_headerStream; }
308
309 private:
310 void finishFlagSync(QList<qint64> missingUids, bool exactMembership);
311 void invalidateUnavailableFolder(const QString &folderPath);
312 // T-87.D1: cached first-page presentation through the bounded foreground
313 // reader. Counts, restore-anchor resolution and the window rows execute on
314 // the worker snapshot; the GUI thread performs no header SQL here.
315 void requestCachedFirstPage(const struct MailHeaderStreamIdentity &identity,
316 quint64 generation, bool anchoredRestore,
317 qint64 restoreUid, int attempt, bool protocolSync = true,
318 bool countsOnly = false);
319 void retryCachedFirstPage(const struct MailHeaderStreamIdentity &identity,
320 quint64 generation, bool anchoredRestore,
321 qint64 restoreUid, int attempt, bool protocolSync,
322 bool countsOnly);
323 void presentCachedFirstPageFailure(const struct MailHeaderStreamIdentity &identity,
324 quint64 generation, const QString &error,
325 bool protocolSync);
326 MailListModel::AsyncPageLoader cachedPageLoader(
327 const struct MailHeaderStreamIdentity &identity, quint64 generation, bool previous,
328 const MailSortOrder &sort);
329 void executeDeferredFolderSwitch(); // T-118: fired by debounce timer
330 QTimer m_prefetchProbeTimer;
331 MailId m_prefetchProbeMail;
332 quint64 m_prefetchProbeGeneration = 0;
333 void selectMailId(const MailId &mailId, const MailHeader *previewHint);
334 bool startSeenVisualMutation(qint64 uid, qint64 folderId, bool desiredSeen,
335 64 std::optional<bool> visibleSeen = std::nullopt);
336 bool startFlagVisualMutation(qint64 uid, qint64 folderId, MutationKind kind,
337 bool desiredState,
338 5 std::optional<bool> visibleState = std::nullopt);
339 // Label counterpart of startFlagVisualMutation. Unavailable: neither the
340 // flat model nor the caller shows this mail (use the synchronous path).
341 enum class LabelVisualStart { Unavailable, Unchanged, Started };
342 LabelVisualStart startLabelVisualMutation(qint64 uid, qint64 folderId, const QString &label,
343 bool present, std::optional<QStringList> visibleLabels);
344 void applyVisibleLabel(qint64 uid, qint64 folderId, const QString &label, bool present);
345
346
347
348 ImapService *m_imap;
349 MailCache *m_cache;
350 MailListModel *m_model;
351 void presentMail(const MailHeader &header, const MailBody &body);
352 void presentError(const MailHeader &header, const QString &reason, bool retryable);
353 MailDisplayPort *m_view = nullptr;
354 bool m_networkEnabled = true;
355 QMetaObject::Connection m_displayDestroyedConnection;
356 QMetaObject::Connection m_displayRetryConnection;
357 MailThreadModel *m_threadModel = nullptr;
358 // T-211. QPointer: a UndoManager destroyed before the controller (object
359 // teardown order) must not be touched from ~MailController.
360 QPointer<UndoManager> m_undoManager;
361 MailId m_displayAwaitingNetwork; // shown offline; retried when the network comes up
362
363 QStringList m_subscribedFolders;
364 ServerSearchSession m_serverSearch; // S91.1.3 (needs m_subscribedFolders)
365 MailMutationPipeline::Deps mutationDeps();
366 MailMutationPipeline m_mutations; // S91.4.2 (before m_flagSync: it binds to intents())
367 FolderFlagSync m_flagSync; // S91.2.6 (reads the members above and below)
368
369 // S91.0.5: account, folder, epoch and generations of the current folder.
370 FolderContext m_folder;
371 // P90.8: projection hint for display reads, driven by the reading mode.
372 MailDisplaySnapshot::PreferredContent m_preferredDisplayContent =
373 MailDisplaySnapshot::PreferredContent::Auto;
374 quint64 m_selectionActionGeneration = 0;
375 QSet<QString> m_unavailableFolders;
376 MailBodyTransport::Deps bodyTransportDeps();
377 MailBodyTransport m_bodies; // S91.4.1 (reads the connection/view members by reference)
378 MailMoveCommands::Deps moveDeps();
379 MailMoveCommands m_moves; // S91.4.4 (reads the connection/folder members by reference)
380 MailSelectionStateCommands::Deps selectionStateDeps();
381 MailSelectionStateCommands m_selectionStates; // MAIL-SELECTION-ACTIONS
382 bool m_folderPresentationEnabled = true;
383 std::function<std::optional<MailHeader>(const MailId &)> m_indexedHeader;
384 // Header of a loaded row (flat page or published indexed page); SQLite
385 // only for a mail outside the loaded pages.
386 std::optional<MailHeader> loadedHeader(qint64 folderId, qint64 uid) const;
387 // Sprint 105 S105.2.5: one bounded header fetch for a displayed mail whose
388 // spam filter fields are unknown (synced before schema v25, no raw source).
389 void requestSpamFields(const MailHeader &header, const MailBody &body);
390 qint64 m_spamFieldFolder = -1; // the folder m_spamFieldRequests belongs to
391 QSet<qint64> m_spamFieldRequests; // UIDs already asked (at most 256 per folder)
392 bool m_streamedHeadersVisible = true;
393 MailHeaderFilter m_flatFilter;
394 MailSortOrder m_flatSort;
395 std::function<QSet<qint64>(qint64)> m_retainedUnreadProvider;
396 MailHeaderFilter flatFilterFor(qint64 folderId) const;
397 bool m_flatPageNeedsReload = false;
398 bool m_flatPageRefreshInFlight = false;
399 quint64 m_flatPageRefreshVersion = 0;
400 QString m_restoreAnchorFolder;
401 qint64 m_restoreAnchorUid = 0;
402 bool m_bodyFetchSelect = false; // Suppress onFolderSelectedFromImap during body fetch
403 bool m_pendingHeaderFetch = false; // T-058: flag sync triggers header fetch
404
405 // Second IMAP connection for server-side search (lazy-init)
406 ImapService *m_searchImap = nullptr;
407 // T-720: Health monitor for the search connection — silently dead
408 // search connections are now detected + reconnected like the others.
409 ConnectionHealthMonitor *m_searchHealth = nullptr;
410 ImapConfig m_imapConfig;
411
412 // T-205: Third IMAP connection for body fetch (lazy-init)
413 ImapService *m_bodyImap = nullptr;
414 QString m_bodyImapSelectedFolder; // Currently selected folder on m_bodyImap
415 // T-720: Health monitor for the body connection (liveness probe +
416 // backoff reconnect). Replaces m_bodyKeepAliveTimer (T-540).
417 ConnectionHealthMonitor *m_bodyHealth = nullptr;
418 QTimer *m_loadingPlaceholderTimer = nullptr; // T-548: Deferred "Loading body…" display
419
420 // T-061: Reverse-chunked fetch state
421 QList<QList<qint64>> m_reverseChunks;
422
423 void markFolderSeenPage(QString account, qint64 folderId, quint32 uidValidity,
424 qint64 afterUid, qint64 throughUid);
425 void settleFolderSeenPage(QString account, qint64 folderId, quint32 uidValidity,
426 qint64 afterUid, qint64 throughUid,
427 QList<qint64> attemptedUids, int checks = 0);
428 QString m_headerCacheError;
429 quint64 m_headerWriteGeneration = 0;
430 int m_pendingHeaderWrites = 0;
431 bool m_headerFetchCompletedPending = false;
432 std::optional<QList<QPair<qint64, quint32>>> m_deferredHeaderFlags;
433 std::shared_ptr<MailHeaderStateFence> m_headerStream;
434 QObject m_streamCountReadOwner;
435 QObject m_folderStatusReadOwner;
436 QTimer m_streamCountTimer;
437 QElapsedTimer m_headerStatusTimer;
438 quint64 m_streamCountGeneration = 0;
439 bool m_streamCountsDirty = false;
440 bool m_streamCountsPending = false;
441 bool m_finalHeaderCountPending = false;
442 quint32 m_headerStreamUidValidity = 0;
443 void resetHeaderWrites();
444 void scheduleStreamCounts();
445 void readStreamCounts();
446 void cancelStreamCounts();
447 void requestFinalHeaderCounts(quint64 folderGeneration, qint64 folderId,
448 const QString &folder, int attempt = 0);
449 void completeHeaderFetchCounts(quint64 folderGeneration, qint64 folderId,
450 const QString &folder, int total, int unread);
451 void failHeaderWrite(const QString &error);
452 void publishStoredHeaders(const QList<MailHeader> &headers,
453 bool projectionChanged, int totalCount,
454 const QSet<qint64> &pendingMoveUids,
455 quint32 uidValidity);
456 // Sticky until a new folder sync starts.
457 bool m_fetchInProgress = false;
458
459 // Folder generations live in m_folder (FolderContext).
460 // The current open pipelined UID SEARCH ALL (empty cache) instead of
461 // FETCH FLAGS; the search result, not a flag snapshot, drives the sync.
462 bool m_openedWithUidSearch = false;
463 // The current open pipelined CHANGEDSINCE + UID membership (CONDSTORE
464 // server, cached folder with a stored HIGHESTMODSEQ); consumed by SELECT.
465 bool m_openedWithChangedSince = false;
466 // HIGHESTMODSEQ reported by the current SELECT. It becomes the next
467 // CHANGEDSINCE base only after this open's flag sync completed, so an
468 // interrupted sync cannot skip changes on the next reopen.
469 qint64 m_pendingModseqFolderId = -1;
470 quint64 m_pendingModseq = 0;
471
472 // T-118: Debounce timer for IMAP SELECT.
473 // Cache load is instant, but the IMAP SELECT (which stops IDLE) is
474 // delayed by 50ms. Rapid switches only trigger one SELECT.
475 QTimer *m_folderSwitchTimer = nullptr;
476
477 QHash<MailId, quint64> m_seenIntentGenerations;
478 QHash<MailId, quint64> m_starIntentGenerations;
479 QHash<MailId, bool> m_seenVisualDesired;
480 QHash<MailId, bool> m_starVisualDesired;
481 quint64 m_nextFlagIntentGeneration = 0;
482 // Newest visual label intent per mail and label (older writer answers are
483 // ignored, like the Seen/star generations above).
484 QHash<MailId, QHash<QString, quint64>> m_labelIntentGenerations;
485
486
487
488 // T-210: Folder-switch total timing (onFolderSelected → startIdleIfPossible)
489 QElapsedTimer m_folderSwitchStopwatch;
490
491 // 67.A2: first-INBOX-sync tracking for notification suppression
492 bool m_inboxFirstSyncSignaled = false;
493 bool m_inboxCacheWasEmpty = false;
494 };
495