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: 100.0% 31 / 0 / 31
Functions: 100.0% 24 / 0 / 24
Branches: -% 0 / 0 / 0

data/MailCache.h
Line Branch Exec Source
1 #pragma once
2
3 #include <memory>
4 #include <QMutex>
5
6 #include <QList>
7 #include <QHash>
8 #include <QMap>
9 #include <QObject>
10 #include <QPair>
11 #include <QSet>
12 #include <QSqlDatabase>
13 #include <QSqlQuery>
14
15 struct sqlite3;
16 #include <atomic>
17 #include <functional>
18 #include <optional>
19
20 #include "data/Models.h"
21 #include "data/MailDisplaySnapshot.h"
22 #include "data/MailHeaderPage.h"
23 #include "data/MailSearchPage.h"
24 #include "data/MailSelectionSnapshot.h"
25 #include "data/MailSelectionMutation.h"
26 #include "data/MutationJournal.h"
27 #include "data/AttachmentStore.h"
28 #include "data/ExternalContentAllowlist.h"
29 #include "data/MailCacheSchema.h"
30 #include "data/MailSearchIndex.h"
31 #include "data/MutationJournalStore.h"
32 #include "data/PayloadCacheLimits.h"
33 #include "data/MailSnapshotReader.h"
34 #include "data/PredictorTrainingReader.h"
35
36
37 // MailCache provides persistent storage for mail headers and bodies using
38 // SQLite. Designed for cache-first architecture: all reads go through this
39 // class first, IMAP fetches only happen on cache miss or incremental sync.
40 //
41 // Performance: Uses WAL journal mode and batch transactions for fast writes.
42 // Thread safety: Not thread-safe. Use from a single thread or with external
43 // locking.
44 class MailCache : public QObject {
45 Q_OBJECT
46
47 public:
48 // Shared with deterministic fixture validation so schema migrations cannot
49 // silently leave generated performance artifacts on an older contract.
50 static constexpr int SchemaVersion = 26;
51
52 explicit MailCache(QObject *parent = nullptr);
53 ~MailCache() override;
54
55 // Open or create the SQLite database at the given path.
56 // Creates schema (tables + indexes) if the database is new.
57 // Returns false on failure (check lastError()).
58 bool open(const QString &dbPath);
59 // Worker-owned existing databases only: no migrations, eviction or writes.
60 bool openReadOnly(const QString &dbPath, int busyTimeoutMs = 100);
61 // Existing initialized cache only; worker owns connection and transactions.
62 // Writers wait for the lock like the product connection (5 s): the
63 // former 100 ms default (copied from readers, which WAL never blocks)
64 // failed background header, body and journal writes whenever another
65 // connection wrote a larger unit, aborting whole folder syncs.
66 bool openExistingWriter(const QString &dbPath, int busyTimeoutMs = 5000);
67 MailHeaderPageSnapshot headerPageSnapshot(const MailHeaderPageRequest &request,
68 const std::atomic_bool *cancelled = nullptr);
69 // Bounded header-only neighbour in the same account/folder, in list order.
70 // Ready supplies UID/date and a 1024-character title, never body or labels.
71 MailDisplaySnapshot navigationSnapshot(const QString &accountId,
72 qint64 folderId, qint64 uid, bool before,
73 const MailSortOrder &sort = {});
74 // Close the database connection.
75 void close();
76
77 // True if the database is currently open.
78 bool isOpen() const;
79
80 // Path of the database file (for opening secondary connections).
81 8947 QString databasePath() const { return m_dbPath; }
82 // Test/diagnostic contract (Sprint 91 rule 8 replaced the former friend
83 // access): the owning connection and schema helper for fault-injection
84 // tests, and the header row lookup the stores receive through Deps.
85 683 QSqlDatabase &database() { return m_db; }
86 49 MailCacheSchema &schema() { return m_schema; }
87 qint64 headerRowId(qint64 folderId, qint64 uid) const;
88 46820 QString writeDomain() const { return m_writeDomain; }
89
90 // Last error message from a failed operation.
91 QString lastError() const;
92
93 // --- Folder operations ---
94
95 // Ensure a folder entry exists for the given account + IMAP path.
96 // Returns the folder ID (creates if not exists, returns existing if found).
97 // Returns -1 on error.
98 qint64 ensureFolder(const QString &account, const QString &path);
99
100 // P90.17: bulk folder identity snapshot (id ↔ path) so UI admission paths
101 // answer from memory instead of one SQL round trip per call.
102 struct FolderIdentity {
103 qint64 id = -1;
104 QString path;
105 quint32 uidValidity = 0;
106 };
107 QList<FolderIdentity> folderIdentities(const QString &account) const;
108
109
110 qint64 findFolderId(const QString &account, const QString &path) const;
111 QString folderPath(qint64 folderId) const;
112
113 // T-215: Find a mail by Message-ID across all folders.
114 // Returns {folderId, uid} or nullopt if not found.
115 std::optional<QPair<qint64,qint64>> findByMessageId(const QString &messageId) const;
116 // Account-scoped variant for user-facing links. A Message-ID is not an
117 // authorization boundary and can occur in more than one local account.
118 std::optional<QPair<qint64, qint64>>
119 findByMessageId(const QString &accountId, const QString &messageId) const;
120
121 // Set the UIDVALIDITY for a folder. If the value differs from the stored
122 // value, all cached data for that folder is purged (RFC 3501 requirement).
123 // F-63/F-72: atomic epoch transition — quarantine, purge, new epoch and
124 // the CONDSTORE-watermark reset commit together or not at all. Returns
125 // false (epoch unchanged) when any step fails.
126 bool setUidValidity(qint64 folderId, quint32 uidvalidity);
127
128 // Get the stored UIDVALIDITY for a folder. Returns 0 if not set.
129 quint32 uidValidity(qint64 folderId) const;
130 qint64 threadRevision(qint64 folderId) const;
131 struct FolderPresentationState {
132 int headerCount = 0;
133 qint64 threadRevision = -1;
134 quint32 uidValidity = 0;
135 };
136 // One SQLite snapshot for the fields that select a folder's first Quick
137 // presentation path. The returned revision/epoch identify the indexed page.
138 FolderPresentationState folderPresentationState(qint64 folderId) const;
139
140 // T-208: CONDSTORE HIGHESTMODSEQ per folder
141 bool setHighestModseq(qint64 folderId, quint64 modseq); // false + lastError on failure
142 quint64 highestModseq(qint64 folderId) const;
143
144 // T-209: Last sync timestamp (seconds since epoch)
145 bool setLastSync(qint64 folderId); // false + lastError on failure
146 qint64 lastSync(qint64 folderId) const;
147
148 // --- Header operations (batch) ---
149
150 // Store a batch of headers for a folder. Uses a transaction for speed.
151 // Existing headers with the same UID are updated (UPSERT).
152 // True only when the complete header/label/search transaction committed.
153 bool storeHeaders(qint64 folderId, const QList<MailHeader> &headers);
154 // Headers fetched on another connection (server search) under the epoch
155 // of that connection's SELECT. S106-103: they are stored only while the
156 // folder still has that epoch; a folder without an epoch yet (never
157 // opened) adopts it first, so a later change purges these rows too.
158 bool storeEpochHeaders(qint64 folderId, quint32 uidValidity, const QList<MailHeader> &headers);
159 // Prepare a bounded streaming batch only after acquiring the writer lock and
160 // validating its mailbox. The callback may overlay newer local state.
161 bool storeStreamingHeaders(const QString &account, const QString &folder,
162 qint64 folderId, quint32 uidValidity, QList<MailHeader> &headers,
163 const std::function<QString(QList<MailHeader> &)> &prepare);
164
165
166 // Retrieve all cached headers for a folder, sorted by date DESC.
167 QList<MailHeader> headers(
168 qint64 folderId,
169 const std::function<void()> &headerRowsLoaded = {}) const;
170
171 // Stable keyset page for dense flat folders, newest first. Pass the final
172 // header from the previous page as `after`; null starts a new traversal.
173 QList<MailHeader> headerPage(qint64 folderId, int limit,
174 const MailHeader *after = nullptr, bool before = false,
175 QString *error = nullptr, bool includeAnchor = false,
176 const MailHeaderFilter *filter = nullptr,
177 const MailSortOrder &sort = {}) const;
178 // T-87.D1 random-seek index: uid-quantized rank buckets for a folder,
179 // newest-first ranks derivable by cumulative sums. Bounded bucket count;
180 // one covering-index scan per query (no OFFSET scans).
181 struct MailRankBucket {
182 qint64 minUid = 0; // smallest uid in the bucket
183 qint64 count = 0; // rows within [minUid, maxUid-of-bucket]
184 };
185 struct MailRankIndex {
186 qint64 maxUid = 0;
187 QList<MailRankBucket> buckets; // ascending uid order
188 };
189 MailRankIndex rankIndex(qint64 folderId, int bucketCount = 64) const;
190 // Rank of the first row with uid <= given uid (0-based, newest = rank 0);
191 // total row count when the uid is below the smallest stored uid.
192 qint64 rankForUidFromIndex(const MailRankIndex &index, qint64 uid) const;
193 // Newest uid whose rank is the first at-or-after the requested rank.
194 qint64 uidAtRankFromIndex(const MailRankIndex &index, qint64 rank) const;
195
196 // Retrieve a single header by folder + UID. Returns nullopt if not cached.
197 std::optional<MailHeader> header(qint64 folderId, qint64 uid) const;
198
199 // Hydrate an ordered result page with one joined query per bounded chunk.
200 // Missing identities are omitted and duplicate input identities are kept.
201 // A failed chunk ends the hydration; the chunks read before it are
202 // returned (logged).
203 QList<MailHeader> headersByIds(const QList<MailId> &mailIds) const;
204
205 // Get the maximum UID stored for a folder. Returns 0 if no headers cached.
206 // Used for incremental sync: FETCH UID maxUid+1:*
207 qint64 maxUid(qint64 folderId) const;
208
209 // Total number of cached headers for a folder.
210 int headerCount(qint64 folderId) const;
211 // One read snapshot for both badge totals during streamed-header publish.
212 QPair<int, int> folderCounts(qint64 folderId) const;
213 // Connection-local SQLite change token. Changes committed by other cache
214 // connections invalidate retained view snapshots; errors never yield a hit.
215 qint64 externalDataVersion() const;
216
217 // S91.1.4: bounded predictor-training reads on this connection.
218 2415 const PredictorTrainingReader &training() const { return m_training; }
219 // S92.4.1: read-only worker snapshots (display, selection, flag compare,
220 // compose source) on this cache's connection.
221 1350 MailSnapshotReader &snapshots() { return m_snapshots; }
222 const MailSnapshotReader &snapshots() const { return m_snapshots; }
223 using FlagSnapshotDiff = MailSnapshotReader::FlagSnapshotDiff;
224 using ComposeSourceSnapshot = MailSnapshotReader::ComposeSourceSnapshot;
225
226 // --- Body operations ---
227
228 // Store a mail body. The header must already exist in the cache.
229 // True only after body and search projection commit successfully.
230 bool storeBody(qint64 folderId, qint64 uid, const MailBody &body);
231
232 // Replace the complete fetched payload and search projection atomically.
233 bool storeMessageContent(qint64 folderId, qint64 uid, const MailBody &body,
234 const QList<Attachment> &attachments,
235 const QList<QByteArray> &blobs);
236
237 bool storeMessageContentForIdentity(const MailId &mailId, quint32 uidValidity,
238 const MailBody &body, const QList<Attachment> &attachments,
239 const QList<QByteArray> &blobs);
240
241 // Retrieve a cached mail body. Returns nullopt if not cached.
242 std::optional<MailBody> body(qint64 folderId, qint64 uid) const;
243
244 // Retrieve only the fields needed to render or quote a message. The raw
245 // RFC822 source is deliberately excluded so normal navigation does not
246 // allocate and retain a second copy of the complete message.
247 // S106-258/295: the texts are cut at QuoteTextCharacters — the callers
248 // quote or excerpt them (reply, forward, appointment from mail), and a
249 // single-line body could be as large as the payload budget.
250 static constexpr int QuoteTextCharacters = 256 * 1024;
251 std::optional<MailBody> displayBody(qint64 folderId, qint64 uid) const;
252
253 // P90.14: reply/forward composition projection — plain text plus a bounded
254 // envelope prefix of the raw source (header block only, never the whole
255 // message). rawSource carries that prefix; full raw access stays explicit
256 // via rawBody().
257 std::optional<MailBody> replySnapshot(qint64 folderId, qint64 uid) const;
258
259 // Retrieve the raw RFC822 source only when the user explicitly requests it.
260 QByteArray rawBody(qint64 folderId, qint64 uid) const;
261 bool hasRawBody(qint64 folderId, qint64 uid) const;
262
263 // Check if a body is already cached (without loading the data).
264 bool hasBody(qint64 folderId, qint64 uid) const;
265
266 // --- Attachment operations ---
267
268 // S91.2.3: attachment metadata and BLOBs of cached mail.
269 202 AttachmentStore &attachments() { return m_attachments; }
270 9 const AttachmentStore &attachments() const { return m_attachments; }
271
272 // --- Flag operations ---
273
274 // Update flags for a single header.
275 void updateFlags(qint64 folderId, qint64 uid, quint32 flags);
276 // A separate writer committed the flag. Notify GUI-side projections and
277 // in-flight header fences without repeating the SQL write on this thread.
278 148 void announceFlagsCommitted(qint64 folderId, qint64 uid, quint32 flags) {
279 148 emit headerFlagsChanged(folderId, uid, flags);
280 148 }
281
282 // Batch-update flags for multiple UIDs in a single transaction.
283 // Input: list of (UID, flags) pairs.
284 void batchUpdateFlags(qint64 folderId,
285 const QList<QPair<qint64, quint32>> &uidFlags);
286
287 // Remove a single header (and cascaded body/attachments) by UID.
288 void removeHeader(qint64 folderId, qint64 uid);
289 // Removes many headers (bodies cascade) in one write transaction and
290 // emits headerRemoved for each removed UID. Returns false on a failed write.
291 bool removeHeaders(qint64 folderId, const QList<qint64> &uids);
292
293 // Count of unread (unseen) headers for a folder.
294 int unreadCount(qint64 folderId) const;
295 std::optional<QList<qint64>> unreadUidPage(qint64 folderId, qint64 afterUid,
296 qint64 throughUid) const;
297
298 // --- Label operations (T-261) ---
299
300 // Add a label (keyword) to a cached header. No-op if already present.
301 void addLabel(qint64 folderId, qint64 uid, const QString &label);
302
303 // Remove a label (keyword) from a cached header. No-op if not present.
304 void removeLabel(qint64 folderId, qint64 uid, const QString &label);
305 // A separate writer committed the label. Notify GUI-side projections
306 // without repeating the SQL write on this thread.
307 30 void announceLabelCommitted(qint64 folderId, qint64 uid, const QString &label, bool present) {
308 30 emit headerLabelChanged(folderId, uid, label, present);
309 30 }
310
311 // --- Durable mutation journal (Sprint 84) ---
312
313 // S91.3.2: the durable mutation journal (enqueue, claim, phases, APPEND
314 // payloads). mutationPhaseChanged / mutationRemoved stay MailCache signals.
315 6874 MutationJournalStore &journal() { return m_journal; }
316 const MutationJournalStore &journal() const { return m_journal; }
317 struct FlagMutationResult {
318 qint64 token = 0;
319 quint32 flags = 0;
320 };
321 // A background writer journals a Seen/Flagged desired state and updates
322 // its canonical header in one transaction. The row's account, folder,
323 // path and UIDVALIDITY are rechecked inside that transaction.
324 std::optional<FlagMutationResult> enqueueFlagMutationAndUpdate(
325 const MutationRecord &record);
326 // Same contract for a Keyword (label) desired state: journal and the
327 // mail_labels row commit in one transaction on a background writer.
328 // Returns the journal token.
329 std::optional<qint64> enqueueKeywordMutationAndUpdate(const MutationRecord &record);
330 using AppendPayload = MutationJournalStore::AppendPayload;
331 static constexpr qint64 MaximumAppendMessageBytes = MutationJournalStore::MaximumAppendMessageBytes;
332 static constexpr qint64 MaximumPendingAppendBytes = MutationJournalStore::MaximumPendingAppendBytes;
333
334 // Worker-owned writer: validate the complete captured selection and enqueue
335 // one action in the same transaction. No partial admission or UI mutation.
336 MailSelectionMoveResult enqueueSelectionMove(
337 std::shared_ptr<const MailSelectionSnapshot> selection,
338 const QString &targetFolder);
339 MailSelectionCancelResult cancelQueuedSelectionMove(const MailSelectionMoveResult &action);
340 // MAIL-SELECTION-ACTIONS, worker-owned writer: one desired state for all
341 // messages of a selection in one transaction (headers/labels and journal
342 // together); unchanged messages are skipped, nothing is admitted partially.
343 MailSelectionStateResult enqueueSelectionState(MailSelectionStateRequest request);
344
345 // --- Maintenance ---
346
347 // S91.2.4: payload budget, folder purges and cache statistics.
348 1501 PayloadCacheLimits &payloadLimits() { return m_payloadLimits; }
349 const PayloadCacheLimits &payloadLimits() const { return m_payloadLimits; }
350 // F-51: `delimiter` also renames the folder's children.
351 void renameFolderPath(const QString &account, const QString &oldPath,
352 const QString &newPath, const QString &delimiter = QStringLiteral("/"));
353 // F-48 (SPRINT-104): re-key every cached row of an account to its new
354 // name — folders (and through them headers, bodies, attachments, search
355 // documents) and the queued journal actions. Folder ids survive, so
356 // nothing is re-downloaded. One transaction.
357 bool renameAccount(const QString &oldName, const QString &newName);
358 // F-43 (SPRINT-104): delete every cached row of an account (folders
359 // cascade over headers, bodies, attachments and search documents; the
360 // journal and its stored append payloads follow). The payload cache
361 // accounting refreshes with the rows.
362 bool removeAccountData(const QString &account);
363
364 // T-167: Convenience methods for FolderPredictor pre-training
365 QStringList allFolderPaths(const QString &account, QString *error = nullptr) const;
366 QList<MailHeader> headersByFolder(const QString &account,
367 const QString &folderPath) const;
368
369 // --- Badge caching (T-075) ---
370
371 // Store the polled unseen count for a folder.
372 void storeBadge(qint64 folderId, int unseenCount);
373
374 // Load all cached badges for an account. Returns map of folderPath → unseen.
375 QMap<QString, int> loadAllBadges(const QString &account) const;
376
377 // --- External Content Whitelist (T-122) ---
378 // S91.1.5: the allowlist store; whitelistChanged stays on MailCache.
379 938 ExternalContentAllowlist &allowlist() { return m_allowlist; }
380 const ExternalContentAllowlist &allowlist() const { return m_allowlist; }
381
382 // --- Full-Text Search (T-179) ---
383 // S91.2.5: FTS projection, search queries and index maintenance. The
384 // search value types keep their MailCache names for existing callers.
385 using SearchResult = MailSearchIndex::SearchResult;
386 using SearchIndexDiagnostics = MailSearchIndex::SearchIndexDiagnostics;
387 using SearchFilter = MailSearchIndex::SearchFilter;
388 2184 MailSearchIndex &search() { return m_searchIndex; }
389 const MailSearchIndex &search() const { return m_searchIndex; }
390
391 signals:
392 void mutationRemoved(qint64 token);
393 // P90.16: Move journal phase transition, including the initial Queued insert
394 // (Queued/Dispatched/Confirmed/Failed/…);
395 // indexed projections refresh their pending-move admission set from it.
396 void mutationPhaseChanged(const QString &account, qint64 folderId,
397 qint64 token, qint64 uid, bool pending);
398 void headerStateReplaced(qint64 folderId, const MailHeader &header);
399 void headerFlagsChanged(qint64 folderId, qint64 uid, quint32 flags);
400 void headerLabelChanged(qint64 folderId, qint64 uid, const QString &label, bool present);
401 void headerRemoved(qint64 folderId, qint64 uid);
402 void folderHeadersRemoved(qint64 folderId);
403 // Revoke any displayed body lease before a mailbox epoch changes or its
404 // cached rows are purged. An attempted change invalidates conservatively.
405 void folderIdentityInvalidated(qint64 folderId);
406 // Published only after a changed mailbox epoch reached the cache.
407 void folderUidValidityChanged(qint64 folderId, quint32 uidValidity);
408 // Emitted after a whitelist mutation actually changed the stored entries.
409 // Readers (running mail views) use it to refresh their external-image
410 // policy; a rejected or no-op write never emits.
411 void whitelistChanged();
412
413 private:
414 QString m_writeDomain;
415 std::shared_ptr<QRecursiveMutex> m_writeMutex = std::make_shared<QRecursiveMutex>();
416 // P90.3: presentation-only flag maintenance. Applies +/-1 unread deltas to
417 // the stored unread_descendants of every published ancestor so a seen flip
418 // needs no thread-topology rebuild. Caller owns the write transaction.
419 [[nodiscard]] bool writeFlags(qint64 folderId, qint64 uid, quint32 flags);
420 [[nodiscard]] bool adjustThreadUnreadAggregates(qint64 folderId,
421 const QList<QPair<qint64, int>> &uidDeltas);
422 bool storeHeaderBatch(qint64 folderId, QList<MailHeader> &headers,
423 const std::function<bool()> &prepare);
424 // storeHeaderBatch() steps (S91.3.9); each failing step has rolled back.
425 struct ExistingHeader {
426 qint64 id = -1;
427 quint32 flags = 0;
428 QString subject, from, to, messageId, inReplyTo, refIds, cc, listId;
429 bool recipientsKnown = false; // cc_addr IS NOT NULL
430 QVariant date;
431 qint64 size = -1;
432 int hasAttachments = -1;
433 int isSpam = -1;
434 std::optional<double> spamScore, spamThreshold; // Sprint 105 (v25)
435 QString spamTool; // NULL column -> null QString
436 QSet<QString> labels;
437 };
438 bool loadExistingHeaders(qint64 folderId, const QList<MailHeader> &headers,
439 QHash<qint64, ExistingHeader> &existing);
440 static QList<int> lastWinsWriteOrder(const QList<MailHeader> &headers);
441 bool indexHeaderAddresses(qint64 hid, const MailHeader &h, const QVariant &dateValue, const ExistingHeader *prior);
442 bool flushAddressRows();
443 public:
444 // S105.5.6 A/B only: header writes without address rows (never in the product).
445 static void setAddressIndexWritesForBenchmark(bool enabled);
446 private:
447 // Sprint 105: address rows of the batch being written, and the prepared
448 // 100-row INSERT of this connection (lazy).
449 struct PendingAddressRow {
450 qint64 headerId = -1;
451 QString address;
452 int role = 0;
453 QVariant date;
454 };
455 QList<PendingAddressRow> m_pendingAddressRows;
456 std::unique_ptr<QSqlQuery> m_addressStatements;
457 static bool storedRowMatches(const ExistingHeader &prior, const MailHeader &h, const QVariant &dateValue,
458 const QString &canonicalMessageId, const QString &encodedReplyIds,
459 const QString &encodedReferences);
460 static void bindHeaderUpsert(QSqlQuery &q, qint64 folderId, const MailHeader &h,
461 const QVariant &dateValue, const QString &canonicalMessageId,
462 const QString &encodedReplyIds, const QString &encodedReferences);
463 bool reconcileHeaderLabels(qint64 hid, const QSet<QString> *priorLabelsOrNull,
464 const QSet<QString> &labelSet, const QStringList &labels,
465 QSqlQuery &labelQ, QSqlQuery &labelDeleteQ);
466 bool commitHeaderBatch(qint64 folderId, const QList<QPair<qint64, int>> &unreadDeltas,
467 const QList<qint64> &changedRowIds);
468 // Byte-budgeted single-direction page read shared by plain, restore and
469 // subsequent-page snapshots. Caller owns the transaction. Sets
470 // state.error/state.outcome (Limited) when the budget rejects the request.
471 QList<MailHeader> admittedHeaderPage(qint64 folderId, const MailHeader *cursor,
472 bool before, bool includeAnchor, int limit,
473 qint64 reservedBytes,
474 MailHeaderPageSnapshot &state,
475 const MailHeaderFilter *filter = nullptr,
476 const MailSortOrder &sort = {}) const;
477 bool storeBodyPayload(qint64 folderId, qint64 uid, const MailBody &body,
478 const QList<Attachment> *attachments,
479 const QList<QByteArray> *blobs,
480 const MailId *expectedId = nullptr, quint32 expectedValidity = 0);
481
482 using SearchDocument = MailSearchIndex::SearchDocument;
483 // Get the internal row ID for a header by folder + UID.
484
485
486
487 QSqlDatabase m_db;
488 PredictorTrainingReader m_training{m_db};
489 MailSnapshotReader m_snapshots{m_db};
490 // P90.17: folder identity (path, UIDVALIDITY) of the primary cache
491 // connection, which is the only writer of those columns (setUidValidity,
492 // renameFolderPath, ensureFolder). Read-only/worker instances do not cache.
493 // Filled lazily, updated by this instance's writers, cleared on (re)open.
494 struct FolderIdentityEntry { QString path; quint32 uidValidity = 0; bool pathKnown = false; bool epochKnown = false; };
495 mutable QHash<qint64, FolderIdentityEntry> m_folderIdentity;
496 bool m_cacheFolderIdentity = false;
497 QString m_connectionName;
498 QString m_dbPath;
499 QString m_lastError;
500 // Sprint 91 stores: share m_db / m_lastError (declared above).
501 MailCacheSchema m_schema{m_db, m_lastError};
502 MutationJournalStore m_journal{m_db, m_lastError,
503 42 {[this](const QString &account, qint64 folderId, qint64 token, qint64 uid, bool pending) {
504 42 emit mutationPhaseChanged(account, folderId, token, uid, pending);
505 42 },
506 2904 [this](qint64 token) { emit mutationRemoved(token); }}};
507 MailSearchIndex m_searchIndex{m_db, m_lastError,
508 32 {[this](qint64 folderId, qint64 uid) { return headerRowId(folderId, uid); }}};
509 PayloadCacheLimits m_payloadLimits{m_db, m_lastError, m_writeMutex,
510 218 {[this](const QString &account, const QString &path) { return findFolderId(account, path); },
511 15 [this](qint64 rowId) { return m_searchIndex.indexHeaderById(rowId); },
512 11 [this](qint64 folderId) { emit folderHeadersRemoved(folderId); }}};
513 AttachmentStore m_attachments{m_db, m_lastError,
514 178 {[this](qint64 folderId, qint64 uid) { return headerRowId(folderId, uid); },
515 43 [this](qint64 protectedHeaderId, qint64 reserveBytes) {
516 43 return m_payloadLimits.enforcePayloadCacheLimitInTransaction(protectedHeaderId, reserveBytes);
517 },
518 46 [this] { m_payloadLimits.refreshPayloadCacheBytes(); }}};
519 45 ExternalContentAllowlist m_allowlist{m_db, m_lastError, [this] { emit whitelistChanged(); }};
520 };
521