MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - data - MailCache.h (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 100.0 % 13 13
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 3 3
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 72.7 % 44 32

             Branch data     Line data    Source code
       1                 :             : #pragma once
       2                 :             : 
       3                 :             : #include <QList>
       4                 :             : #include <QMap>
       5                 :             : #include <QObject>
       6                 :             : #include <QPair>
       7                 :             : #include <QSqlDatabase>
       8                 :             : #include <optional>
       9                 :             : 
      10                 :             : #include "data/Models.h"
      11                 :             : 
      12                 :             : // T-122: External content whitelist entry
      13                 :             : struct WhitelistEntry {
      14                 :             :   qint64 id = 0;
      15                 :             :   QString type;       // "sender" or "domain"
      16                 :             :   QString value;      // e.g. "user@example.com" or "example.com"
      17                 :             :   QString createdAt;
      18                 :             : };
      19                 :             : 
      20                 :             : // MailCache provides persistent storage for mail headers and bodies using
      21                 :             : // SQLite. Designed for cache-first architecture: all reads go through this
      22                 :             : // class first, IMAP fetches only happen on cache miss or incremental sync.
      23                 :             : //
      24                 :             : // Performance: Uses WAL journal mode and batch transactions for fast writes.
      25                 :             : // Thread safety: Not thread-safe. Use from a single thread or with external
      26                 :             : // locking.
      27                 :             : class MailCache : public QObject {
      28                 :             :   Q_OBJECT
      29                 :             : 
      30                 :             : public:
      31                 :             :   explicit MailCache(QObject *parent = nullptr);
      32                 :             :   ~MailCache() override;
      33                 :             : 
      34                 :             :   // Open or create the SQLite database at the given path.
      35                 :             :   // Creates schema (tables + indexes) if the database is new.
      36                 :             :   // Returns false on failure (check lastError()).
      37                 :             :   bool open(const QString &dbPath);
      38                 :             : 
      39                 :             :   // Close the database connection.
      40                 :             :   void close();
      41                 :             : 
      42                 :             :   // True if the database is currently open.
      43                 :             :   bool isOpen() const;
      44                 :             : 
      45                 :             :   // Path of the database file (for opening secondary connections).
      46                 :          50 :   QString databasePath() const { return m_dbPath; }
      47                 :             : 
      48                 :             :   // Last error message from a failed operation.
      49                 :             :   QString lastError() const;
      50                 :             : 
      51                 :             :   // --- Folder operations ---
      52                 :             : 
      53                 :             :   // Ensure a folder entry exists for the given account + IMAP path.
      54                 :             :   // Returns the folder ID (creates if not exists, returns existing if found).
      55                 :             :   // Returns -1 on error.
      56                 :             :   qint64 ensureFolder(const QString &account, const QString &path);
      57                 :             :   QString folderPath(qint64 folderId) const;
      58                 :             : 
      59                 :             :   // T-215: Find a mail by Message-ID across all folders.
      60                 :             :   // Returns {folderId, uid} or nullopt if not found.
      61                 :             :   std::optional<QPair<qint64,qint64>> findByMessageId(const QString &messageId) const;
      62                 :             : 
      63                 :             :   // Set the UIDVALIDITY for a folder. If the value differs from the stored
      64                 :             :   // value, all cached data for that folder is purged (RFC 3501 requirement).
      65                 :             :   void setUidValidity(qint64 folderId, quint32 uidvalidity);
      66                 :             : 
      67                 :             :   // Get the stored UIDVALIDITY for a folder. Returns 0 if not set.
      68                 :             :   quint32 uidValidity(qint64 folderId) const;
      69                 :             : 
      70                 :             :   // T-208: CONDSTORE HIGHESTMODSEQ per folder
      71                 :             :   void setHighestModseq(qint64 folderId, quint64 modseq);
      72                 :             :   quint64 highestModseq(qint64 folderId) const;
      73                 :             : 
      74                 :             :   // T-209: Last sync timestamp (seconds since epoch)
      75                 :             :   void setLastSync(qint64 folderId);
      76                 :             :   qint64 lastSync(qint64 folderId) const;
      77                 :             : 
      78                 :             :   // --- Header operations (batch) ---
      79                 :             : 
      80                 :             :   // Store a batch of headers for a folder. Uses a transaction for speed.
      81                 :             :   // Existing headers with the same UID are updated (UPSERT).
      82                 :             :   void storeHeaders(qint64 folderId, const QList<MailHeader> &headers);
      83                 :             : 
      84                 :             :   // Retrieve all cached headers for a folder, sorted by date DESC.
      85                 :             :   QList<MailHeader> headers(qint64 folderId) const;
      86                 :             : 
      87                 :             :   // Retrieve a single header by folder + UID. Returns nullopt if not cached.
      88                 :             :   std::optional<MailHeader> header(qint64 folderId, qint64 uid) const;
      89                 :             : 
      90                 :             :   // Get the maximum UID stored for a folder. Returns 0 if no headers cached.
      91                 :             :   // Used for incremental sync: FETCH UID maxUid+1:*
      92                 :             :   qint64 maxUid(qint64 folderId) const;
      93                 :             : 
      94                 :             :   // Total number of cached headers for a folder.
      95                 :             :   int headerCount(qint64 folderId) const;
      96                 :             : 
      97                 :             :   // --- Body operations ---
      98                 :             : 
      99                 :             :   // Store a mail body. The header must already exist in the cache.
     100                 :             :   void storeBody(qint64 folderId, qint64 uid, const MailBody &body);
     101                 :             : 
     102                 :             :   // Retrieve a cached mail body. Returns nullopt if not cached.
     103                 :             :   std::optional<MailBody> body(qint64 folderId, qint64 uid) const;
     104                 :             : 
     105                 :             :   // Check if a body is already cached (without loading the data).
     106                 :             :   bool hasBody(qint64 folderId, qint64 uid) const;
     107                 :             : 
     108                 :             :   // --- Attachment operations ---
     109                 :             : 
     110                 :             :   // Store attachment metadata and BLOB data for a mail.
     111                 :             :   // The header must already exist in the cache.
     112                 :             :   void storeAttachments(qint64 folderId, qint64 uid,
     113                 :             :                         const QList<Attachment> &attachments,
     114                 :             :                         const QList<QByteArray> &blobs);
     115                 :             : 
     116                 :             :   // Retrieve attachment metadata for a mail (WITHOUT BLOB data, lazy load).
     117                 :             :   QList<Attachment> attachments(qint64 folderId, qint64 uid) const;
     118                 :             : 
     119                 :             :   // Load BLOB data for a single attachment on-demand.
     120                 :             :   QByteArray attachmentData(qint64 attachmentId) const;
     121                 :             : 
     122                 :             :   // --- Flag operations ---
     123                 :             : 
     124                 :             :   // Update flags for a single header.
     125                 :             :   void updateFlags(qint64 folderId, qint64 uid, quint32 flags);
     126                 :             : 
     127                 :             :   // Batch-update flags for multiple UIDs in a single transaction.
     128                 :             :   // Input: list of (UID, flags) pairs.
     129                 :             :   void batchUpdateFlags(qint64 folderId,
     130                 :             :                         const QList<QPair<qint64, quint32>> &uidFlags);
     131                 :             : 
     132                 :             :   // Remove a single header (and cascaded body/attachments) by UID.
     133                 :             :   void removeHeader(qint64 folderId, qint64 uid);
     134                 :             : 
     135                 :             :   // Count of unread (unseen) headers for a folder.
     136                 :             :   int unreadCount(qint64 folderId) const;
     137                 :             : 
     138                 :             :   // --- Label operations (T-261) ---
     139                 :             : 
     140                 :             :   // Add a label (keyword) to a cached header. No-op if already present.
     141                 :             :   void addLabel(qint64 folderId, qint64 uid, const QString &label);
     142                 :             : 
     143                 :             :   // Remove a label (keyword) from a cached header. No-op if not present.
     144                 :             :   void removeLabel(qint64 folderId, qint64 uid, const QString &label);
     145                 :             : 
     146                 :             :   // --- Maintenance ---
     147                 :             : 
     148                 :             :   // Delete all cached data (headers + bodies) for a folder.
     149                 :             :   void purgeFolder(qint64 folderId);
     150                 :             : 
     151                 :             :   // T-138: Path-based convenience overloads for FolderPropertiesDialog
     152                 :             :   int cachedHeaderCount(const QString &account, const QString &folderPath);
     153                 :             :   int cachedBodyCount(const QString &account, const QString &folderPath);
     154                 :             :   void purgeFolderByPath(const QString &account, const QString &folderPath);
     155                 :             : 
     156                 :             :   // T-286: Extended statistics for folder properties dialog
     157                 :             :   qint64 cachedDiskUsage(const QString &account, const QString &folderPath);
     158                 :             :   qint64 totalServerSize(const QString &account, const QString &folderPath);
     159                 :             :   qint64 averageMailSize(const QString &account, const QString &folderPath);
     160                 :             :   void purgeBodyCache(const QString &account, const QString &folderPath);
     161                 :             :   void renameFolderPath(const QString &account, const QString &oldPath,
     162                 :             :                         const QString &newPath);
     163                 :             : 
     164                 :             :   // T-167: Convenience methods for FolderPredictor pre-training
     165                 :             :   QStringList allFolderPaths(const QString &account) const;
     166                 :             :   QList<MailHeader> headersByFolder(const QString &account,
     167                 :             :                                     const QString &folderPath) const;
     168                 :             : 
     169                 :             :   // --- Badge caching (T-075) ---
     170                 :             : 
     171                 :             :   // Store the polled unseen count for a folder.
     172                 :             :   void storeBadge(qint64 folderId, int unseenCount);
     173                 :             : 
     174                 :             :   // Load all cached badges for an account. Returns map of folderPath → unseen.
     175                 :             :   QMap<QString, int> loadAllBadges(const QString &account) const;
     176                 :             : 
     177                 :             :   // --- External Content Whitelist (T-122) ---
     178                 :             : 
     179                 :             :   // Add a whitelist entry. Type must be "sender" or "domain".
     180                 :             :   // Returns true on success, false on error (e.g. duplicate).
     181                 :             :   bool addWhitelistEntry(const QString &type, const QString &value);
     182                 :             : 
     183                 :             :   // Remove a whitelist entry by ID. Returns true if a row was deleted.
     184                 :             :   bool removeWhitelistEntry(qint64 id);
     185                 :             : 
     186                 :             :   // T-313: Remove all whitelist entries (for settings sync apply).
     187                 :             :   void clearWhitelist();
     188                 :             : 
     189                 :             :   // Atomically replace all whitelist entries. The payload is validated before
     190                 :             :   // the current table contents are deleted.
     191                 :             :   bool replaceWhitelistEntries(const QList<QPair<QString, QString>> &entries);
     192                 :             : 
     193                 :             :   // Retrieve all whitelist entries.
     194                 :             :   QList<WhitelistEntry> whitelistEntries() const;
     195                 :             : 
     196                 :             :   // Check if a sender email matches any whitelist entry (sender or domain).
     197                 :             :   bool isWhitelisted(const QString &senderEmail) const;
     198                 :             : 
     199                 :             :   // Get all whitelisted domains as a string list.
     200                 :             :   QStringList whitelistedDomains() const;
     201                 :             : 
     202                 :             :   // Get all whitelisted sender addresses as a string list.
     203                 :             :   QStringList whitelistedSenders() const;
     204                 :             : 
     205                 :             :   // --- Full-Text Search (T-179) ---
     206                 :             : 
     207                 :             :   struct SearchResult {
     208                 :             :     qint64 folderId = 0;
     209                 :             :     qint64 uid = 0;
     210                 :             :     QString folderPath; // resolved folder path
     211                 :             :     QString subject;
     212                 :             :     QString from;
     213                 :             :     QString snippet;    // highlighted match snippet
     214                 :             :     double rank = 0.0;  // BM25 relevance score
     215                 :             :   };
     216                 :             : 
     217                 :             :   // Search for emails matching a query string (FTS5 MATCH syntax).
     218                 :             :   // Returns results sorted by BM25 relevance, limited to maxResults.
     219                 :             :   QList<SearchResult> searchFts(const QString &query, int maxResults = 0,
     220                 :             :                                 int offset = 0) const;
     221                 :             : 
     222                 :             :   // T-192: Optional filters for search.
     223                 :             :   // Sprint 59: extended with subject, tags and tri-state flag/attachment
     224                 :             :   // facets so the visual SearchPanel can express the full set of constraints.
     225                 :             :   struct SearchFilter {
     226                 :             :     // Tri-state for flag-like facets. "Any" means the facet adds no
     227                 :             :     // constraint; it maps 1:1 to the three states of the panel comboboxes.
     228                 :             :     enum class Tri { Any, Yes, No };
     229                 :             : 
     230                 :             :     // Sprint 60 (B1): zero, one or many folder patterns. OR-Semantik: a mail
     231                 :             :     // matches when its folder path contains ANY of the patterns (LIKE
     232                 :             :     // '%pattern%'). Empty list = no folder constraint. Replaces the single
     233                 :             :     // folderPattern of Sprint 59.
     234                 :             :     QStringList folderPatterns; // e.g. {"INBOX", "Archive"} — OR-matched
     235                 :             :     QDateTime dateFrom;    // inclusive lower bound (empty = no lower bound)
     236                 :             :     QDateTime dateTo;      // inclusive upper bound (empty = no upper bound)
     237                 :             :     QString fromFilter;    // from address contains
     238                 :             :     QString toFilter;      // to address contains
     239                 :             : 
     240                 :             :     // Sprint 59 facets:
     241                 :             :     QString subjectFilter;     // folded subject substring (LIKE %X% on f.subject)
     242                 :             :     QStringList tags;          // mail_labels: ALL listed labels must be present (AND)
     243                 :             :     Tri unread = Tri::Any;     // Yes ⇒ Seen bit NOT set; No ⇒ Seen bit set
     244                 :             :     Tri flagged = Tri::Any;    // Yes/No ⇒ Flagged bit set / not set
     245                 :             :     Tri answered = Tri::Any;   // Yes/No ⇒ Answered bit set / not set
     246                 :             :     Tri hasAttachment = Tri::Any; // Yes/No ⇒ has_attachments = 1 / 0
     247                 :             : 
     248                 :             :     // True when no facet constrains the search (every field at its neutral
     249                 :             :     // value). Used by the app path to distinguish "no search state" from
     250                 :             :     // "empty free text but active facets" (Sprint 59 A1).
     251                 :         170 :     bool isEmpty() const {
     252         [ +  + ]:         329 :       return folderPatterns.isEmpty() && !dateFrom.isValid() &&
     253   [ +  -  +  +  :         300 :              !dateTo.isValid() && fromFilter.isEmpty() && toFilter.isEmpty() &&
             +  +  +  + ]
     254         [ +  + ]:         291 :              subjectFilter.isEmpty() && tags.isEmpty() &&
     255   [ +  +  +  + ]:         141 :              unread == Tri::Any && flagged == Tri::Any &&
     256   [ +  +  +  +  :         329 :              answered == Tri::Any && hasAttachment == Tri::Any;
                   +  + ]
     257                 :             :     }
     258                 :             : 
     259                 :           8 :     bool operator==(const SearchFilter &o) const {
     260         [ +  - ]:          16 :       return folderPatterns == o.folderPatterns && dateFrom == o.dateFrom &&
     261   [ +  -  +  -  :          16 :              dateTo == o.dateTo && fromFilter == o.fromFilter &&
                   +  - ]
     262         [ +  - ]:          16 :              toFilter == o.toFilter && subjectFilter == o.subjectFilter &&
     263   [ +  -  +  -  :           8 :              tags == o.tags && unread == o.unread && flagged == o.flagged &&
                   +  - ]
     264   [ +  -  +  -  :          16 :              answered == o.answered && hasAttachment == o.hasAttachment;
                   +  - ]
     265                 :             :     }
     266                 :             :     bool operator!=(const SearchFilter &o) const { return !(*this == o); }
     267                 :             :   };
     268                 :             :   QList<SearchResult> searchFts(const QString &query, const SearchFilter &filter,
     269                 :             :                                 int maxResults = 0, int offset = 0) const;
     270                 :             : 
     271                 :             :   // Sprint 59 (F3): distinct IMAP keywords/labels known to the cache, sorted
     272                 :             :   // case-insensitively. Used to populate tag suggestions in the search UI.
     273                 :             :   QStringList knownLabels() const;
     274                 :             : 
     275                 :             :   // Index a single email for FTS5 (called after storeHeaders/storeBody).
     276                 :             :   void indexForSearch(qint64 folderId, qint64 uid);
     277                 :             :   void batchIndexForSearch(qint64 folderId, const QList<qint64> &uids);
     278                 :             :   bool searchIndexEmpty() const;
     279                 :             : 
     280                 :             :   // Rebuild the entire FTS5 index from existing cached data.
     281                 :             :   // This is a heavyweight operation, intended for initial population or repair.
     282                 :             :   void rebuildSearchIndex();
     283                 :             : 
     284                 :             :   // Fold text for the FTS index: lowercase + strip diacritics (Müller→muller,
     285                 :             :   // Café→cafe, ß→ss). Applied to BOTH indexed text and queries so the trigram
     286                 :             :   // tokenizer (no native remove_diacritics before SQLite 3.45) is accent-blind.
     287                 :             :   // Public + static so it can be unit-tested in isolation.
     288                 :             :   static QString foldForSearch(const QString &text);
     289                 :             : 
     290                 :             : private:
     291                 :             : #ifdef MAILJD_UNIT_TEST
     292                 :             :   friend class TestMailCache;
     293                 :             : #endif
     294                 :             : 
     295                 :             :   // Create database schema (tables + indexes).
     296                 :             :   bool createSchema();
     297                 :             : 
     298                 :             :   // Get the internal row ID for a header by folder + UID.
     299                 :             :   qint64 headerRowId(qint64 folderId, qint64 uid) const;
     300                 :             : 
     301                 :             :   // Replace a header's FTS row from current header/body data. The caller must
     302                 :             :   // provide a transaction when this is used concurrently with other writers.
     303                 :             :   bool indexHeaderById(qint64 rowId);
     304                 :             : 
     305                 :             :   qint64 calculatePayloadCacheSize() const;
     306                 :             :   void enforcePayloadCacheLimit(qint64 protectedHeaderId = -1);
     307                 :             : 
     308                 :             :   QSqlDatabase m_db;
     309                 :             :   QString m_connectionName;
     310                 :             :   QString m_dbPath;
     311                 :             :   QString m_lastError;
     312                 :             :   qint64 m_payloadCacheBytes = 0;
     313                 :             :   qint64 m_payloadCacheLimitBytes = 1024LL * 1024 * 1024;
     314                 :             : };
        

Generated by: LCOV version 2.0-1