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: 95.0% 57 / 0 / 60
Functions: 94.9% 56 / 0 / 59
Branches: 92.3% 12 / 0 / 13

presentation/QuickMailBackend.h
Line Branch Exec Source
1 #pragma once
2 #include "FolderIconCatalog.h"
3 #include "StatusMessageState.h"
4 #include "FolderSubscriptionEditor.h"
5 #include "MessageCopyQueue.h"
6 #include "DavCollectionDiscovery.h"
7 #include "AccountConnectionTest.h"
8 #include "QuickPreferences.h"
9 #include "app/core/SettingsSyncSession.h"
10
11 #include "ComposeDraftStore.h"
12 #include "FolderPresentationModel.h"
13 #include "MailPresentationModel.h"
14 #include "QuickMailSelection.h"
15 #include "QuickPimBackend.h"
16 #include "QuickSourceViewer.h"
17 #include "QuickFolderActions.h"
18 #include "QuickMailMenu.h"
19 #include "QuickAccountSettings.h"
20 #include "QuickComposeBackend.h"
21 #include "QuickInvitation.h"
22 #include "QuickCalendarIcs.h"
23 #include "QuickContactVcf.h"
24 #include "QuickMailEventLink.h"
25 #include "EventQuickInput.h"
26 #include "QuickThreadListModel.h"
27 #include "MailSelectionSummary.h"
28 #include "data/MailSort.h"
29 #include "QuickRulesBackend.h"
30 #include "MailSearchSessions.h"
31 #include "data/MailRequest.h"
32 #include "service/FolderPredictionService.h"
33 #include <QObject>
34 #include <QHash>
35 #include <QPointer>
36 #include <QSet>
37 #include <QThreadPool>
38 #include <QTimer>
39 #include <QUrl>
40 #include <QCache>
41
42 #include "ConflictCenter.h"
43 #include "QuickConflictResolver.h"
44 #include <atomic>
45 #include <functional>
46 #include <memory>
47 #include <optional>
48
49 class MailSessionCore;
50 class WorkspaceState;
51 class SearchCoordinator;
52 class MailSearchReadService;
53 class ExternalWhitelistModel;
54 class MailPayloadPreview;
55 struct FolderInfo;
56
57 // Frontend adapter over the shared product session. It owns no transport/cache.
58 class QuickMailBackend final : public QObject {
59 2316 Q_OBJECT
60 Q_PROPERTY(QuickPreferences *preferences READ preferences CONSTANT)
61 Q_PROPERTY(QAbstractItemModel *folders READ folders CONSTANT)
62 // S91.1.1: calendar, tasks and contacts (QML `backend.pim.*`).
63 Q_PROPERTY(QuickPimBackend *pim READ pim CONSTANT)
64 // S91.1.2: raw source, inline PDF and attachment export (QML `backend.source.*`).
65 Q_PROPERTY(QuickSourceViewer *source READ source CONSTANT)
66 // S91.3.1: send, drafts, sessions, attachments (QML `backend.compose.*`).
67 Q_PROPERTY(QuickComposeBackend *compose READ compose CONSTANT)
68 // Sprint 93 S93.5.2: invitation card of the displayed mail
69 // (QML `backend.invitation.*`); the host reports the displayed mail.
70 Q_PROPERTY(QuickInvitation *invitation READ invitation CONSTANT)
71 // Sprint 93 S93.5.3: calendar file import/export (QML `backend.ics.*`).
72 Q_PROPERTY(QuickCalendarIcs *ics READ ics CONSTANT)
73 // Sprint 97 S97.5.8: vCard import and export (QML `backend.vcf.*`).
74 Q_PROPERTY(QuickContactVcf *vcf READ vcf CONSTANT)
75 // Sprint 93 S93.5.6: mail <-> appointment links (QML `backend.mailEvent.*`).
76 Q_PROPERTY(QuickMailEventLink *mailEvent READ mailEvent CONSTANT)
77 // Sprint 93 S93.4.5: ":event <text>" grammar (QML `backend.eventInput`).
78 Q_PROPERTY(QuickEventInput *eventInput READ eventInput CONSTANT)
79 Q_PROPERTY(QAbstractItemModel *messageCopies READ messageCopies CONSTANT)
80 // S91.2.2: accounts, DAV accounts, image policy, reading mode, settings sync
81 // (QML `backend.settings.*`).
82 Q_PROPERTY(QuickAccountSettings *settings READ settings CONSTANT)
83 Q_PROPERTY(QuickRulesBackend *rules READ rules CONSTANT)
84 // Sprint 107: the conflict resolver (`backend.resolver`) and the open
85 // conflicts of every area (`backend.conflicts`).
86 Q_PROPERTY(QuickConflictResolver *resolver READ resolver CONSTANT)
87 Q_PROPERTY(ConflictCenter *conflicts READ conflicts CONSTANT)
88 // Sprint 101: the search tabs (QML `backend.searchTabs.active`).
89 Q_PROPERTY(MailSearchSessions *searchTabs READ searchTabs CONSTANT)
90 Q_PROPERTY(QuickMailMenu *mailMenu READ mailMenu CONSTANT)
91 // F95-10: the header's "☰" menu (own instance: its picks are no mail commands).
92 Q_PROPERTY(QuickMailMenu *appMenu READ appMenu CONSTANT)
93 // F100-03/F100-09: native menus of the views (PNativeMenu, routed by name).
94 Q_PROPERTY(QuickMailMenu *contextMenu READ contextMenu CONSTANT)
95 Q_PROPERTY(QuickMailMenu *searchMenu READ searchMenu CONSTANT)
96 Q_PROPERTY(QString accountEmail READ accountEmail NOTIFY accountChanged)
97 Q_PROPERTY(QString accountName READ accountName NOTIFY accountChanged)
98 Q_PROPERTY(QString activeAccountId READ activeAccountId NOTIFY accountChanged)
99 Q_PROPERTY(QVariantList accountChoices READ accountChoices NOTIFY accountChanged)
100 Q_PROPERTY(QAbstractItemModel *folderSubscriptions READ folderSubscriptions CONSTANT)
101 // S91.2.1: folder operations/properties/cache maintenance (QML `backend.folderActions.*`).
102 Q_PROPERTY(QuickFolderActions *folderActions READ folderActions CONSTANT)
103 Q_PROPERTY(QString folderPath READ folderPath NOTIFY folderChanged)
104 // Sprint 108 D-21: the shown folder is the account's drafts folder (Continue editing).
105 Q_PROPERTY(bool inDraftsFolder READ inDraftsFolder NOTIFY folderChanged)
106 Q_PROPERTY(bool threaded READ threaded WRITE setThreaded NOTIFY threadedChanged)
107 Q_PROPERTY(bool threadProjectionPending READ threadProjectionPending NOTIFY threadedChanged)
108 // True only while a thread rebuild runs AND it exceeds the feedback
109 // threshold (default 300 ms): fast cached rebuilds never flash a
110 // placeholder, slow ones announce themselves once they are really slow.
111 Q_PROPERTY(bool threadProjectionSlow READ threadProjectionSlow NOTIFY threadProjectionSlowChanged)
112 // Diagnostic (mailjd-bench fan-out gate): folder-prediction batches sent
113 // to the prediction worker since start. No NOTIFY; not used by QML.
114 Q_PROPERTY(qulonglong predictionRequestCount READ predictionRequestCount)
115 Q_PROPERTY(bool folderSwitchInProgress READ folderSwitchInProgress NOTIFY folderSwitchInProgressChanged)
116 Q_PROPERTY(QAbstractItemModel *mailList READ mailList NOTIFY threadedChanged)
117 Q_PROPERTY(QString status READ status NOTIFY statusChanged)
118 Q_PROPERTY(bool configured READ configured NOTIFY accountChanged)
119 Q_PROPERTY(bool navigationPending READ navigationPending NOTIFY navigationChanged)
120 Q_PROPERTY(qint64 selectionCount READ selectionCount NOTIFY selectionChanged)
121 // F100-77: the reader's summary of a multiple selection.
122 Q_PROPERTY(MailSelectionSummary *selectionSummary READ selectionSummary CONSTANT)
123 Q_PROPERTY(QStringList selectedLabels READ selectedLabels NOTIFY selectedLabelsChanged)
124 Q_PROPERTY(bool selectionPending READ selectionPending NOTIFY selectionChanged)
125 Q_PROPERTY(bool actionPending READ actionPending NOTIFY actionChanged)
126 Q_PROPERTY(QVariantList searchSuggestions READ searchSuggestions NOTIFY searchSuggestionsChanged)
127 Q_PROPERTY(QStringList expandedFolders READ expandedFolders NOTIFY folderExpansionChanged)
128 Q_PROPERTY(QVariantMap suggestion READ suggestion NOTIFY suggestionsChanged)
129 // F105 feedback: the reader header shows whether the shown mail is
130 // important and its labels — also in a mail tab, where no list row does
131 // ({important, labels: chips as in the list}; empty without a mail).
132 Q_PROPERTY(QVariantMap readerMarks READ readerMarks NOTIFY readerMarksChanged)
133 Q_PROPERTY(bool suggestionsVisible READ suggestionsVisible NOTIFY suggestionsChanged)
134 // The folder-suggestion column belongs to the inbox only (user decision
135 // 2026-10-06); suggestionsVisible stays the persistent switch.
136 Q_PROPERTY(bool suggestionColumnAvailable READ suggestionColumnAvailable NOTIFY folderChanged)
137 // F100-66: the folder list's order, flat and threaded, read from the cache
138 // in that order (persisted as view/mailSort). Search tabs sort their own
139 // results (MailPresentationModel::sortByColumn).
140 Q_PROPERTY(QString sortColumn READ sortColumn NOTIFY mailSortChanged)
141 Q_PROPERTY(bool sortAscending READ sortAscending NOTIFY mailSortChanged)
142 Q_PROPERTY(QStringList sortableColumns READ sortableColumns NOTIFY threadedChanged)
143 public:
144 // Status line text from outside the backend (S93.5.1 reminders without
145 // a notification service).
146 9 void showStatus(const QString &status) { setStatus(status); }
147 4498 QuickPreferences *preferences() { return &m_preferences; }
148 769 MailSelectionSummary *selectionSummary() { return &m_selectionSummary; }
149 QuickMailBackend(MailSessionCore &session, WorkspaceState &workspace,
150 QString profileDirectory, QObject *parent = nullptr,
151 MailSearchReadService *searchReader = nullptr);
152 ~QuickMailBackend() override;
153 1233 MailPresentationModel *mailModel() { return &m_mail; }
154 int loadedRowForIdentity(const MailId &id) const;
155 84337 QuickPimBackend *pim() { return &m_pim; }
156 864 QAbstractItemModel *folders() { return &m_folders; }
157 5775 QuickAccountSettings *settings() { return &m_settings; }
158 12116 QuickRulesBackend *rules() { return &m_rules; }
159 2062 QuickConflictResolver *resolver() { return &m_resolver; }
160 7359 ConflictCenter *conflicts() { return &m_conflicts; }
161 22236 MailSearchSessions *searchTabs() { return m_searchTabs.get(); }
162 1563 QuickMailMenu *mailMenu() { return &m_mailMenu; }
163 1567 QuickMailMenu *appMenu() { return &m_appMenu; }
164 5539 QuickMailMenu *contextMenu() { return &m_contextMenu; }
165 2331 QuickMailMenu *searchMenu() { return &m_searchMenu; }
166 QString accountName() const;
167 QString accountEmail() const;
168 QString activeAccountId() const;
169 QVariantList accountChoices() const;
170 Q_INVOKABLE bool selectAccount(const QString &id);
171 766 QAbstractItemModel *messageCopies() { return &m_messageCopies; }
172 766 QAbstractItemModel *folderSubscriptions() { return &m_folderSubscriptions; }
173 4062 QuickFolderActions *folderActions() { return &m_folderActions; }
174 Q_INVOKABLE void beginFolderSubscriptions();
175 7990 QString folderPath() const { return m_folderPath; }
176
4/4
✓ Branch 3 → 4 taken 118 times.
✓ Branch 3 → 7 taken 4156 times.
✓ Branch 5 → 6 taken 21 times.
✓ Branch 5 → 7 taken 97 times.
4274 bool inDraftsFolder() const { return !m_draftsFolder.isEmpty() && m_folderPath == m_draftsFolder; }
177 1755 QString status() const { return m_statusState.text(); }
178 // Test/diagnostic access to the status line (the website showcase clears
179 // the feedback of one step before it captures the next).
180 4 StatusMessageState *statusState() { return &m_statusState; }
181 // Unconfirmed server actions of the active account for the status details.
182 Q_INVOKABLE QVariantList pendingActionDetails() const;
183 2452 bool threaded() const { return m_threaded; }
184 85 bool threadProjectionPending() const { return m_threadProjectionPending; }
185 2590 bool folderSwitchInProgress() const { return m_folderSwitchInProgress; }
186 780 bool threadProjectionSlow() const { return m_threadProjectionSlow; }
187 ✗ qulonglong predictionRequestCount() const { return m_predictionRequestCount; }
188 void setThreadProjectionSlowThresholdMs(int ms);
189 void setThreaded(bool on);
190 // The visible main list: the threaded projection while thread view is on
191 // and no search runs (search results always present flat).
192 QAbstractItemModel *mailList();
193 QThreadPool *workerPool() { return &m_threadPool; } // tests (S106-291)
194 bool configured() const;
195
3/4
✓ Branch 2 → 3 taken 3839 times.
✓ Branch 2 → 5 taken 9 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 3839 times.
3848 bool navigationPending() const { return m_navigationActive || !m_navigation.isEmpty(); }
196 qint64 selectionCount() const;
197 // Labels on the currently selected mail (raw IMAP keywords, e.g. "$label1").
198 QStringList selectedLabels() const;
199 8740 bool selectionPending() const { return m_selection.pending(); }
200 3101 bool actionPending() const { return m_actionPending; }
201 300 QVariantList searchSuggestions() const { return m_searchSuggestions; }
202 QStringList expandedFolders() const;
203 QVariantMap suggestion() const;
204 QVariantMap readerMarks() const;
205 // Sprint 105 S105.3.8: why a row's folder suggestion was made and whether
206 // it is confident ({reason, strong}); empty for rows without one.
207 Q_INVOKABLE QVariantMap suggestionDetails(const QVariantMap &identity) const;
208 static QString suggestionReason(const FolderSuggestion &suggestion);
209 7161 bool suggestionsVisible() const { return m_suggestionsVisible; }
210 799 QString sortColumn() const { return MailSortOrder::idOf(m_mailSort.column); }
211 800 bool sortAscending() const { return m_mailSort.ascending; }
212 // Threads sort by their newest mail or their first mail's sender/subject.
213 QStringList sortableColumns() const;
214 // A click on a column title: the same column again flips the direction,
215 // another column starts with its natural direction.
216 Q_INVOKABLE void sortMailList(const QString &columnId);
217 const MailSortOrder &mailSort() const { return m_mailSort; }
218 // IMAP's INBOX name is case-insensitive (RFC 3501 section 5.1).
219 2898 bool suggestionColumnAvailable() const { return m_folderPath.compare(QLatin1String("INBOX"), Qt::CaseInsensitive) == 0; }
220 // List rows get predictions only while the column is on and available.
221
4/4
✓ Branch 2 → 3 taken 1867 times.
✓ Branch 2 → 6 taken 2590 times.
✓ Branch 4 → 5 taken 1762 times.
✓ Branch 4 → 6 taken 105 times.
4457 bool suggestionColumnShown() const { return m_suggestionsVisible && suggestionColumnAvailable(); }
222 Q_INVOKABLE void setSuggestionsVisible(bool on); // F100-55: also from the column chooser
223 bool acceptsIdentity(const MailId &identity) const;
224 void restoreWorkspaceSelection();
225 Q_INVOKABLE bool selectFolder(const QString &path);
226 // Sprint 105 D-14 (g i/s/d/a/j): the path of "inbox", "sent", "drafts",
227 // "archive" or "junk" of the active account ("" when it has none).
228 Q_INVOKABLE QString specialFolder(const QString &role) const;
229 Q_INVOKABLE bool markFolderRead(const QString &path);
230 Q_INVOKABLE QStringList matchingFolders(const QString &prefix) const;
231 Q_INVOKABLE bool executeCommand(const QString &id);
232 // Sprint 98: `line` is a command-bar line ("folder rename", "mark read");
233 // its first word is the command, the second word its value.
234 Q_INVOKABLE QString namedCommandUnavailableReason(const QString &line) const;
235 Q_INVOKABLE bool canExecuteNamedCommand(const QString &line) const;
236 Q_INVOKABLE bool executeNamedCommand(const QString &name, const QString &argument);
237 Q_INVOKABLE bool moveSelectedMail(const QString &path);
238 Q_INVOKABLE void beginMailRowSelection(int row, int modifiers = 0);
239 Q_INVOKABLE void selectMailRow(int row, int modifiers = 0);
240 Q_INVOKABLE void selectAllMails();
241 Q_INVOKABLE void watchSelectionRows(int first, int last);
242 // Lazy header paging for the flat list (Widgets parity: the QTreeView drove
243 // MailListModel::fetchMore as the viewport approached the loaded window end).
244 Q_INVOKABLE bool canFetchMoreMails() const;
245 Q_INVOKABLE void fetchMoreMails();
246 Q_INVOKABLE bool canFetchPreviousMails() const;
247 Q_INVOKABLE void fetchPreviousMails();
248 Q_INVOKABLE void previewSearch(const QString &query);
249 Q_INVOKABLE bool submitSearch(const QString &query, int suggestion = -1);
250 // Sprint 101 D-1/D-2: a full search opens (or reuses) a search tab;
251 // `newTab` asks for a second one (Alt+Enter, "In neuem Tab öffnen").
252 Q_INVOKABLE bool openSearch(const QString &query, bool newTab = false);
253 Q_INVOKABLE void cancelSearchPreview();
254 Q_INVOKABLE bool setFolderExpanded(const QString &path, bool expanded);
255 6365 QuickSourceViewer *source() { return &m_source; }
256 1 int pageRows() const { return m_pageRows; }
257 // The selectable folder paths known to the backend (updated by LIST).
258 ✗ Q_INVOKABLE QSet<QString> availableFolders() const { return m_availableFolders; }
259 17345 QuickComposeBackend *compose() { return &m_compose; }
260 8418 QuickInvitation *invitation() { return &m_invitation; }
261 768 QuickCalendarIcs *ics() { return &m_ics; }
262 1691 QuickContactVcf *vcf() { return &m_vcf; }
263 39 QuickMailEventLink *mailEvent() { return &m_mailEvent; }
264 10 QuickEventInput *eventInput() { return &m_eventInput; }
265 MailId displayedMailId() const;
266 signals:
267 // One viewport commit from the list (measured: <= 1 per rendered frame).
268 void viewportWatched(int first, int last);
269 void suggestionsChanged();
270 void readerMarksChanged();
271 // Persistence only (F95-15); QML keeps reading suggestionsChanged.
272 void suggestionsVisibleChanged();
273 void folderExpansionChanged();
274 void threadedChanged();
275 void mailSortChanged();
276 void threadProjectionSlowChanged();
277 void folderSwitchInProgressChanged();
278 void accountChanged();
279 void folderChanged();
280 void statusChanged();
281 void navigationChanged();
282 void selectionChanged();
283 void selectedLabelsChanged();
284 void actionChanged();
285 void searchSuggestionsChanged();
286 // F4.2(b): emitted when an armed quick-move retry gave up, so the shell
287 // opens the manual move console exactly like the old direct fallback.
288 void manualMoveRequested();
289 private:
290 QuickPreferences m_preferences;
291 // Constructor wiring in its original order (S91.0.4). Qt calls slots in
292 // connection order, so the constructor must call these exactly in this
293 // sequence; each block moves as a whole when its unit is extracted.
294 void wireProjectionTimers();
295 void wireThreadInvalidation();
296 void wireIndexedProviders();
297 void wireHeaderProjection();
298 void wireProjectionSignals();
299 void wireComposeSource();
300 void wireFolderSubscriptionsAndFilters();
301 void wireFolderOperations();
302 void wirePredictions();
303 void wireRemovalFollowUp();
304 void wireFolderChangeResets();
305 void wireSearch(MailSearchReadService *searchReader);
306 void wireSearchSuggestions();
307 void wireSelection();
308 void wireAccountReload();
309 void wireSelectionTrace();
310 void wireNavigation();
311 void wireControllerStatus();
312 void wireConnectionStatus();
313 void refreshConnectionStatus();
314 // Sprint 100 S100.1.5: mail connection and rule state for the Accounts page.
315 AccountOverview::Active activeAccountState();
316 QString m_lastImapError;
317 // executeCommand() handlers in dispatch order (S91.2.7); each returns
318 // nullopt when the command is not its own.
319 std::optional<bool> handleSuggestionCommand(const QString &id);
320 std::optional<bool> handleSurfaceClose(const QString &id);
321 std::optional<bool> handleIndexedNavigation(const QString &id);
322 std::optional<bool> handleTabNavigation(const QString &id);
323 std::optional<bool> handleSearchTabNavigation(const QString &id);
324 std::optional<bool> handleMainListNavigation(const QString &id);
325 std::optional<bool> handleViewCommand(const QString &id);
326 std::optional<bool> handleUndoCommand(const QString &id);
327 std::optional<bool> handleMailMoveCommand(const QString &id, const MailId &selected);
328 std::optional<bool> handleMailViewCommand(const QString &id, const MailId &selected);
329 std::optional<bool> handleLabelCommand(const QString &id, const MailId &selected);
330 std::optional<bool> handleFlagCommand(const QString &id, const MailId &selected);
331 // MAIL-SELECTION-ACTIONS: the selection mail actions act on (more than one
332 // message in the main folder view), else null; and one state for all of it.
333 std::shared_ptr<const MailSelectionSnapshot> actionSelection() const;
334 QMap<qint64, QList<qint64>> searchBulkGroups() const;
335 bool bulkSelection() const;
336 bool actOnSelection(MutationKind kind, const QString &label, bool desired,
337 std::function<void(bool)> done = {});
338 void invalidatePredictions();
339 void requestPredictions();
340 void nextPredictionBatch(quint64 generation);
341 // F4.2(b): a quick-move press that outruns the new selection's prediction
342 // (debounce or worker batch still in flight) arms a bounded retry instead
343 // of falling back to the manual move console.
344 bool canArmQuickMoveRetry(const MailId &selected) const;
345 void armQuickMoveRetry(const MailId &selected);
346 void disarmQuickMove();
347 void tryArmedQuickMove();
348 // Groups identities per folder and queues bounded service batches; the
349 // selected mail's batch is always queued first (see requestPredictions).
350 void appendPredictionRequests(const QList<MailId> &identities);
351 // Drops cached predictions outside the selected mail and the watched
352 // viewport ("clears obsolete results"); the retained in-scope entries make
353 // a revisited mail answer instantly.
354 void prunePredictionsToScope();
355 FolderSuggestion::Candidate visiblePrediction(const MailId &id) const;
356 QString predictionText(const MailId &id) const;
357 // Populates the session thread model from the presented folder's complete
358 // cached projection while thread view is on; clears it when off, so the
359 // full-folder tree is never retained behind a flat list.
360 void syncThreadProjection();
361 // F3.12: large folder projections build off the GUI thread; this cancels a
362 // pending build and bumps the generation.
363 void cancelThreadProjection();
364 // True while the threaded model is the one actually presented (preference on,
365 // no search; pending builds keep this projection selected).
366 bool threadedPresentation() const;
367 // Row-oriented main-list dispatch: search and flat mode answer from the
368 // paged flat model, threaded mode from the flattened thread projection.
369 int activeRowCount() const;
370 QVariantMap activeMailAt(int row) const;
371 int activeRowForIdentity(const QVariantMap &identity) const;
372 MailSelectionKey activeKeyAt(int row) const;
373 // F-41: presented-order identities between two selection keys.
374 QList<qint64> visibleRangeBetween(const MailSelectionKey &anchor,
375 const MailSelectionKey &key) const;
376 void loadLocalFolders();
377 void applyFolderList(const QList<FolderInfo> &folders, bool authoritative);
378 // F-09 (SPRINT-104, D-4): permanent deletion in the open folder.
379 void ensureDeleteForever();
380 QList<qint64> deleteForeverUids(const MailId &selected) const;
381 // P90.16: reserve the visible successor before an removing action (S/X,
382 // archive, delete, move). Identity has priority over row numbers: when a
383 // later projection refresh invalidates the selection, reconcileSelection()
384 // consumes the token instead of jumping to row 0. The token is cleared by
385 // folder/account switches, superseded by a newer action and consumed by
386 // the next reconcileSelection that observes a valid selection.
387 void reserveActionContinuity(int lastRemovedVisualRow);
388 void clearActionContinuity();
389 void reconcileSelection();
390 void presentCachedFolder(const QString &path);
391 void publishSelection();
392 void requestSelectedMail(const MailId &id);
393 void setStatus(const QString &status);
394 void cancelNavigation();
395 void startNavigation();
396 void refreshSelectionScope();
397 MailSessionCore &m_session;
398 WorkspaceState &m_workspace;
399 QString m_profileDirectory;
400 MailPresentationModel m_mail;
401 QuickThreadListModel m_threadList;
402 QuickPimBackend m_pim;
403 FolderPresentationModel m_folders;
404 QuickAccountSettings m_settings;
405 QuickMailSelection m_selection;
406 MailSelectionSummary m_selectionSummary{{}, this}; // header lookup set in the constructor
407 MailId m_mainCurrent;
408 // P90.16 action-continuity token: the mail being removed, its visual rank
409 // and the reserved surviving successor identity.
410 MailId m_actionAnchor;
411 MailId m_actionSuccessor;
412 int m_actionVisualRow = -1;
413 QString m_folderPath;
414 QString m_previousFolder;
415 StatusMessageState m_statusState;
416 FolderSubscriptionEditor m_folderSubscriptions;
417 MessageCopyQueue m_messageCopies;
418 QSet<QString> m_availableFolders;
419 QSet<QString> m_openTaskMessageIds; // S94.5.5, canonical Message-IDs
420 void refreshTaskLinks();
421 // P90.17: in-memory folder identity admission cache (path ↔ id) so the
422 // click/selection paths never pay SQLite round trips in acceptsIdentity().
423 QHash<QString, qint64> m_folderIdByPath;
424 QHash<qint64, QString> m_folderPathById;
425 QHash<qint64, quint32> m_folderEpochById;
426 void rebuildFolderIdentityCache();
427 qint64 folderIdForPath(const QString &path) const;
428 bool m_reconciling = false;
429 bool m_waitingCachedPage = false;
430 QList<int> m_navigation;
431 bool m_navigationActive = false;
432 bool m_applyingNavigation = false;
433 bool m_applyingSelection = false;
434 bool m_actionPending = false;
435 std::unique_ptr<SearchCoordinator> m_search;
436 std::unique_ptr<MailSearchSessions> m_searchTabs;
437 void wireSearchTabs(MailSearchReadService *searchReader);
438 QTimer m_searchDebounce;
439 QString m_searchPreviewQuery;
440 QVariantList m_searchSuggestions;
441 QTimer m_predictionDebounce;
442 // Armed quick-move retry state (F4.2(b)); m_quickMoveArmed.isValid()
443 // means a pressed s is waiting for the selected mail's prediction.
444 QTimer m_quickMoveFallback;
445 MailId m_quickMoveArmed;
446 QHash<MailId, FolderSuggestion> m_predictions;
447 QHash<qint64, quint32> m_predictionEpochs;
448 QHash<qint64, QString> m_predictionFolders;
449 QSet<MailId> m_alternatePredictions;
450 QList<FolderPredictionRequest> m_predictionRequests;
451 // Batches handed to the worker and not yet answered: taken off the queue,
452 // yet still "in flight" for a pressed s (canArmQuickMoveRetry).
453 int m_predictionBatchesRunning = 0;
454 quint64 m_predictionGeneration = 0;
455 qulonglong m_predictionRequestCount = 0;
456 // Viewport batches are small so the suggestion column fills progressively
457 // while later batches are still classifying on the single worker.
458 static constexpr int ViewportPredictionChunk = 32;
459 // Warm per-mail predictions are kept across selection changes (epoch-fenced
460 // at use time); the cap bounds memory when many folders were visited.
461 static constexpr int MaximumCachedPredictions = 512;
462 // The shown mail's flags and labels as read from the cache, with the
463 // visual-first changes of this session on top (the journal writes the
464 // cache later; a rollback sends the restored value).
465 struct ReaderMarks {
466 MailId id;
467 bool loaded = false;
468 bool important = false;
469 QStringList labels;
470 std::optional<bool> importantOverride;
471 QHash<QString, bool> labelOverrides;
472 };
473 mutable ReaderMarks m_readerMarks;
474 void readerMarkChanged(qint64 folderId, qint64 uid, const std::function<void(ReaderMarks &)> &apply);
475 // S105.3.8 (ux.md §12): a suggestion at or above this calibrated
476 // confidence is shown in the normal text colour, below it muted.
477 static constexpr double StrongSuggestion = 0.9;
478 int m_predictionFirst = -1, m_predictionLast = -1;
479 bool m_suggestionsVisible = false;
480 QString m_junkFolder;
481 QuickSourceViewer m_source;
482 // Visible rows reported by the list viewport; one page of mail navigation.
483 int m_pageRows = 10;
484 QuickComposeBackend m_compose;
485 QuickInvitation m_invitation{
486 m_session,
487 29 {[this] { return m_pim.calendarChoices(); }, [this] { return m_pim.lastCalendar(); },
488 1 [this](const QStringList &recipients, const QByteArray &message, const QString &doneText) {
489
1/1
✓ Branch 3 → 4 taken 1 time.
1 return m_compose.enqueuePrepared(recipients, message, doneText);
490 },
491 1 [this](const QString &status) { setStatus(status); }},
492 this};
493 QuickCalendarIcs m_ics{
494 m_session,
495 5072 {[this] { return m_pim.calendarChoices(); }, [this] { return m_pim.lastCalendar(); },
496 3 [this] { return displayedMailId(); }, [this](const QString &status) { setStatus(status); },
497 m_profileDirectory + QStringLiteral("/cache/ics-export"), QString(),
498 1 [this] { return m_pim.taskActions()->lastList(); }},
499 this};
500 QuickContactVcf m_vcf{
501 m_session,
502 5 {[this] { return displayedMailId(); }, [this](const QString &status) { setStatus(status); }, QString()},
503 this};
504 2 QuickMailEventLink m_mailEvent{m_session, m_pim, [this] { return displayedMailId(); }, this};
505 3 QuickEventInput m_eventInput{[this] { return m_pim.calendarChoices(); }, this};
506 QList<FolderInfo> m_folderInfos;
507 QString m_archiveFolder;
508 QString m_trashFolder;
509 class MailPermanentDelete *m_deleteForever = nullptr; // F-09
510 QString m_draftsFolder;
511 QString m_sentFolder;
512 bool m_threaded = false;
513 MailSortOrder m_mailSort;
514 void applyMailSort();
515 // One worker shared with QuickComposeBackend (send and server-draft
516 // builds). Queued jobs must run: a send job is the only path that ends
517 // m_sendPending (S106-291), so nothing clears this pool.
518 QThreadPool m_threadPool;
519 struct ThreadSnapshot {
520 MailThreadModel::Prepared data;
521 qint64 externalVersion = -1;
522 quint32 uidValidity = 0;
523 };
524 // Additional inactive-folder retention: conservative byte charges from the
525 // prepared forest, not a number-of-folders cap. Oversized entries are refused.
526 QCache<qint64, ThreadSnapshot> m_threadSnapshots{1024}; // KiB
527 quint64 m_threadDataGeneration = 0;
528 bool m_threadRefreshQueued = false;
529 QSet<qint64> m_moveExclusionRefreshQueued;
530 bool m_folderSwitchInProgress = false; // list view drops its delegate buffer meanwhile
531 bool hasPendingMove(const MailId &mail) const;
532 QuickFolderActions m_folderActions;
533 QuickMailMenu m_mailMenu{this};
534 QuickMailMenu m_appMenu{this};
535 QuickMailMenu m_contextMenu{this};
536 QuickMailMenu m_searchMenu{this};
537 QHash<qint64, QSet<qint64>> m_visualMoveUids; // folder -> moves awaiting the durable writer
538 void queueIndexedMoveExclusionRefresh(qint64 folderId);
539 QTimer m_headerProjectionTimer;
540 QString m_headerProjectionFolder;
541 void retainThreadSnapshot(qint64 folderId, qint64 externalVersion);
542 bool restoreThreadSnapshot(qint64 folderId);
543 quint64 m_threadProjectionGeneration = 0;
544 bool m_threadProjectionPending = false;
545 bool m_threadProjectionFailed = false;
546 bool m_threadProjectionSlow = false;
547 int m_threadProjectionSlowThresholdMs = 300;
548 quint64 m_folderHoldGeneration = 0; // folder switch that owns the held rows
549 QTimer m_threadProjectionSlowTimer;
550 // P90 cold build: plain cached rows while the first indexed page builds.
551 QTimer m_provisionalPageTimer;
552 QObject m_provisionalPageOwner; // own read lane: never cancels other reads
553 static constexpr int ProvisionalPageDelayMs = 100;
554 void requestProvisionalPage();
555 void armThreadProjectionSlowFlag();
556 void clearThreadProjectionSlowFlag();
557 // Moves the currently selected mail into targetFolder through the shared
558 // journaled move path; the removal follow-up advances the selection.
559 bool moveCurrentMailToFolder(const QString &targetFolder);
560 // Sprint 95: server rules of the active account (`backend.rules`).
561 QuickRulesBackend::Deps rulesDeps();
562 void reportManualMove(const QList<MailId> &mails, const QString &targetPath);
563 QuickRulesBackend m_rules{rulesDeps(), this};
564 // Sprint 107 (QuickMailBackendConflicts.cpp).
565 QuickConflictResolver::Deps resolverDeps();
566 ConflictCenter::Deps conflictDeps();
567 QuickConflictResolver m_resolver{resolverDeps(), this};
568 ConflictCenter m_conflicts{conflictDeps(), this};
569 };
570