MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - service - ImapService.h (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 100.0 % 8 8
Test Date: 2026-06-21 21:10:19 Functions: 88.9 % 9 8
Legend: Lines:     hit not hit

            Line data    Source code
       1              : #pragma once
       2              : 
       3              : #include <QDate>
       4              : #include <QElapsedTimer>
       5              : #include <QList>
       6              : #include <QMap>
       7              : #include <QObject>
       8              : #include <QPair>
       9              : #include <QQueue>
      10              : #include <QSslSocket>
      11              : #include <QStringList>
      12              : #include <QTimer>
      13              : 
      14              : #include <functional>
      15              : 
      16              : #include "data/AccountConfig.h"
      17              : #include "data/Models.h"
      18              : 
      19              : // ImapService manages a single IMAP connection.
      20              : // Communicates asynchronously via Qt signals/slots.
      21              : // All network I/O happens on the caller's thread (main thread in Sprint 01).
      22              : class ImapService : public QObject {
      23              :   Q_OBJECT
      24              : #ifdef MAILJD_UNIT_TEST
      25              :   friend class TestImapServiceSerialization;
      26              :   friend class TestSprint55Controller;
      27              :   friend class TestConnectionHealth;
      28              : #endif
      29              : 
      30              : public:
      31              :   enum class State {
      32              :     Disconnected,
      33              :     Connecting,
      34              :     Connected,      // TCP connected, waiting for server greeting
      35              :     Greeting,       // Server greeting received
      36              :     Capability,     // CAPABILITY response received
      37              :     StartingTLS,    // STARTTLS in progress
      38              :     Authenticating, // LOGIN sent, waiting for response
      39              :     Authenticated,  // LOGIN successful, ready for commands
      40              :     Selected,       // Folder selected, ready for FETCH commands
      41              :     Idling,         // IDLE command active, waiting for server push
      42              :     Error
      43              :   };
      44            3 :   Q_ENUM(State)
      45              : 
      46              :   explicit ImapService(QObject *parent = nullptr);
      47              :   ~ImapService() override;
      48              : 
      49              :   void connectToServer(const ImapConfig &config);
      50              :   void disconnect();
      51              : 
      52              :   // Commands (only valid when Authenticated or Selected)
      53              :   void listFolders();
      54              :   void selectFolder(const QString &folderPath);
      55              :   void fetchHeaders(qint64 uidFrom = 1);
      56              :   void fetchBody(qint64 uid);
      57              :   void fetchBody(qint64 uid, qint64 maxBytes);
      58              : 
      59              :   // T-205: Pipeline SELECT + FETCH BODY in one burst (for body connection).
      60              :   void selectAndFetchBody(const QString &folderPath, qint64 uid);
      61              :   void markSeen(qint64 uid);
      62              :   void markUnseen(qint64 uid);
      63              :   void markAllSeen(); // T-200: UID STORE 1:* +FLAGS (\Seen)
      64              : 
      65              :   // Generic flag store: add or remove any flag (system flag or keyword)
      66              :   // e.g. storeFlag(uid, "\\Flagged", true) or storeFlag(uid, "$label1", false)
      67              :   void storeFlag(qint64 uid, const QString &flag, bool add);
      68              : 
      69              :   // T-100: Move/copy messages between folders
      70              :   void moveMessage(qint64 uid, const QString &targetFolder);
      71              :   void moveMessages(const QList<qint64> &uids, const QString &targetFolder);
      72              :   void copyMessage(qint64 uid, const QString &targetFolder);
      73              : 
      74              :   // Search UIDs in the selected folder (fromUid=1 → all, fromUid>1 → delta).
      75              :   void searchAllUids(qint64 fromUid = 1);
      76              : 
      77              :   // T-187: Text-based IMAP SEARCH in the selected folder.
      78              :   // criteria: "TEXT", "SUBJECT", "FROM", "TO", "BODY"
      79              :   void searchText(const QString &query,
      80              :                   const QString &criteria = QStringLiteral("TEXT"));
      81              : 
      82              :   // Sprint 59 (S2): composite IMAP SEARCH for the visual search facets.
      83              :   // ImapService stays independent of MailCache — the caller (MailController)
      84              :   // translates SearchFilter into this server-friendly subset. Tri-state mirrors
      85              :   // SearchFilter::Tri: Any adds no constraint.
      86              :   enum class SearchTri { Any, Yes, No };
      87              :   struct SearchCriteria {
      88              :     QString text;    // TEXT "..."
      89              :     QString from;    // FROM "..."
      90              :     QString to;      // TO "..."
      91              :     QString subject; // SUBJECT "..."
      92              :     QDate since;     // SINCE dd-MMM-yyyy (inclusive)
      93              :     QDate before;    // BEFORE dd-MMM-yyyy (exclusive per RFC 3501)
      94              :     SearchTri unread = SearchTri::Any;   // Yes ⇒ UNSEEN, No ⇒ SEEN
      95              :     SearchTri flagged = SearchTri::Any;  // Yes ⇒ FLAGGED, No ⇒ UNFLAGGED
      96              :     SearchTri answered = SearchTri::Any; // Yes ⇒ ANSWERED, No ⇒ UNANSWERED
      97              :     QStringList keywords;                // KEYWORD "..." per entry
      98              : 
      99              :     // True when nothing constrains the search — such criteria are not sent.
     100              :     bool isEmpty() const;
     101              :   };
     102              : 
     103              :   // Run a composite SEARCH built from the criteria. No-op when isEmpty().
     104              :   void search(const SearchCriteria &criteria);
     105              : 
     106              :   // T-211: Search by Message-ID header (for undo-move).
     107              :   void searchByMessageId(const QString &messageId);
     108              : 
     109              :   // Fetch headers for a specific set of UIDs (comma-separated).
     110              :   void fetchHeadersByUids(const QList<qint64> &uids);
     111              : 
     112              :   // T-176: IMAP APPEND – Upload a complete RFC-2822 message to a folder.
     113              :   // flags: e.g. "\\Seen", "\\Draft", or "\\Seen \\Draft"
     114              :   void appendMessage(const QString &folder, const QByteArray &rfcMessage,
     115              :                      const QString &flags = {});
     116              : 
     117              :   // T-176: EXPUNGE – Permanently remove messages marked \Deleted in the
     118              :   // currently selected folder.  Used by T-177 (Drafts) to delete old drafts
     119              :   // after re-saving.
     120              :   void expunge();
     121              : 
     122              :   // T-281: Folder management commands (RFC 3501 §6.3.3–§6.3.5)
     123              :   void createFolder(const QString &folderPath);
     124              :   void deleteFolder(const QString &folderPath);
     125              :   void renameFolder(const QString &oldPath, const QString &newPath);
     126              : 
     127              :   // IDLE support (RFC 2177)
     128              :   void startIdle(); // Enter IDLE mode (must be in Selected state)
     129              :   void stopIdle();  // Send DONE to exit IDLE mode
     130           79 :   bool isIdling() const { return m_isIdling; }
     131              :   bool hasIdleCapability() const;
     132              :   bool hasCondstoreCapability() const; // T-208: CONDSTORE support
     133              : 
     134              :   // T-320: NOTIFY support (RFC 5465) — replaces IDLE + STATUS polling
     135              :   void startNotify(const QStringList &subscribedFolders);
     136              :   void stopNotify();  // Send NOTIFY NONE
     137           90 :   bool isNotifying() const { return m_isNotifying; }
     138              :   bool hasNotifyCapability() const;
     139              :   void executeAfterNotify(std::function<void()> command);
     140              : 
     141              :   // T-205: Disable automatic IDLE entry after FETCH_BODY/STORE.
     142              :   // Set to false for connections that only do body fetches.
     143           36 :   void setAutoIdle(bool enabled) { m_autoIdle = enabled; }
     144              : 
     145              :   // Execute a command after safely stopping IDLE or NOTIFY.
     146              :   // If not idling/notifying: executes immediately.
     147              :   // If idling: sends DONE, queues command, executes on IDLE OK.
     148              :   // If notifying: sends NOTIFY NONE, queues command, executes on NOTIFY OK.
     149              :   void executeAfterIdle(std::function<void()> command);
     150              : 
     151              :   // T-114: Clear all pending deferred commands (e.g. on folder switch).
     152              :   // Prevents stale SELECTs/FETCHes from executing after the user
     153              :   // switches folders rapidly.
     154              :   void clearDeferredCommands();
     155              : 
     156              :   // Flag sync: fetch only UID+FLAGS for all messages in selected folder.
     157              :   void fetchFlags();
     158              : 
     159              :   // T-207: Pipeline SELECT + FETCH FLAGS in one burst (saves one round-trip).
     160              :   // Sends both commands immediately; server processes them in-order per RFC 3501.
     161              :   void selectAndFetchFlags(const QString &folderPath);
     162              : 
     163              :   // T-208: Incremental flag sync using CONDSTORE (RFC 4551).
     164              :   // Only fetches flags changed since the given HIGHESTMODSEQ value.
     165              :   void fetchFlagsChanged(quint64 modseq);
     166              : 
     167              :   // Fetch UID+FLAGS for a single sequence number (used after IDLE flag push).
     168              :   void fetchUidForSeqNo(int seqNo);
     169              : 
     170              :   // Folder status: query UNSEEN/MESSAGES/RECENT without selecting.
     171              :   void statusFolder(const QString &folderPath);
     172              : 
     173              :   // T-540: NOOP command for keep-alive (body connection)
     174              :   void sendNoop();
     175              : 
     176              :   // T-720: Liveness probe API for ConnectionHealthMonitor.
     177              :   // Idempotent while a probe is already running. The concrete probe depends
     178              :   // on the current state:
     179              :   //   Authenticated/Selected → tagged NOOP with a 15 s watchdog
     180              :   //   Idling                  → DONE/OK round-trip with a 15 s watchdog
     181              :   //   Connecting/Auth/Error/Disconnected → no-op (handled elsewhere)
     182              :   // On watchdog timeout the connection is failed and the monitor schedules
     183              :   // a reconnect.
     184              :   void requestLivenessProbe(const QString &reason);
     185              : 
     186              :   // T-720: Public wrapper around private failConnection() so the
     187              :   // ConnectionHealthMonitor can force a teardown + reconnect on
     188              :   // suspend/resume or a network change. Validates/logs the reason.
     189              :   void abortForReconnect(const QString &reason);
     190              : 
     191              :   // T-720: Test seam — true after the TCP-keepalive tuning helper ran
     192              :   // (i.e. onConnected()/onEncrypted() ran the platform setsockopt path).
     193            3 :   bool keepAliveTuned() const { return m_keepAliveTuned; }
     194              : 
     195              :   // T-720: Test seam — true while a liveness probe is in flight.
     196            6 :   bool isLivenessProbeInFlight() const { return !m_probeTag.isEmpty(); }
     197              : 
     198              :   // Currently selected folder path.
     199           17 :   QString selectedFolder() const { return m_selectedFolder; }
     200              : 
     201         1059 :   State state() const { return m_state; }
     202              : 
     203              : signals:
     204              :   void stateChanged(ImapService::State newState);
     205              :   void errorOccurred(const QString &error);
     206              :   void folderListReceived(const QList<FolderInfo> &folders);
     207              :   void folderSelected(const QString &path, int messageCount,
     208              :                       quint32 uidValidity, quint64 highestModseq = 0); // T-208
     209              :   // Emitted when a SELECT is rejected by the server (e.g. \Noselect folder).
     210              :   // Lets multi-folder flows (server search) skip the folder instead of
     211              :   // waiting forever for a folderSelected that will never arrive.
     212              :   void folderSelectFailed(const QString &path);
     213              :   void headersReceived(const QList<MailHeader> &headers);
     214              :   void headerFetchComplete(); // All header batches delivered
     215              :   void rawBodyReceived(qint64 uid, const QByteArray &rawBody);
     216              : 
     217              :   // IDLE push signals
     218              :   void idleNewMessages(int newCount);               // * N EXISTS (N > previous)
     219              :   void idleMessageExpunged(int seqNo);              // * N EXPUNGE
     220              :   void idleFlagsChanged(qint64 uid, quint32 flags); // * N FETCH (FLAGS ...)
     221              :   void idleFlagsNeedRefetch(int seqNo); // * N FETCH (FLAGS ...) without UID
     222              : 
     223              :   // Flag sync result
     224              :   void flagsReceived(const QList<QPair<qint64, quint32>> &uidFlags);
     225              : 
     226              :   // Search result: list of UIDs matching the search query.
     227              :   void searchResultReceived(const QList<qint64> &uids);
     228              : 
     229              :   // Folder status result
     230              :   void folderStatusReceived(const StatusResult &result);
     231              :   void storeComplete();
     232              : 
     233              :   // T-100: Move/copy result signals
     234              :   void messageMoved(qint64 uid, const QString &targetFolder);
     235              :   void messagesMoved(const QList<qint64> &uids, const QString &targetFolder);
     236              :   void messageCopied(qint64 uid, const QString &targetFolder);
     237              :   void moveError(const QString &error);
     238              : 
     239              :   // T-176: APPEND result signals
     240              :   void messageAppended(const QString &folder, qint64 uid); // uid=0 if no UIDPLUS
     241              :   void appendError(const QString &error);
     242              : 
     243              :   // T-176: EXPUNGE result
     244              :   void expungeComplete();
     245              : 
     246              :   // T-281: Folder management result signals
     247              :   void folderCreated(const QString &folderPath);
     248              :   void folderDeleted(const QString &folderPath);
     249              :   void folderRenamed(const QString &oldPath, const QString &newPath);
     250              :   void folderOperationError(const QString &operation, const QString &error);
     251              : 
     252              : private slots:
     253              :   void onConnected();
     254              :   void onEncrypted();
     255              :   void onReadyRead();
     256              :   void onSocketError(QAbstractSocket::SocketError error);
     257              :   void onTimeout();
     258              :   void onCommandTimeout();
     259              :   void onIdleRenew();
     260              :   // T-720: Liveness-probe watchdog slots.
     261              :   void onLivenessProbeTimeout();
     262              :   void onIdleRenewWatchdogTimeout();
     263              : 
     264              : private:
     265              :   void setState(State newState);
     266              :   void sendCommand(const QString &type, const QString &command);
     267              :   // T-720: like sendCommand() but exposes the generated tag (for probes).
     268              :   QString sendTaggedCommand(const QString &type, const QString &command);
     269              :   // T-720: Send a probe NOOP and remember its tag. Does not enqueue —
     270              :   // call only in a state that allows commands (Authenticated/Selected).
     271              :   void sendProbeNoop();
     272              :   // T-720: Apply SO_KEEPALIVE + platform-specific interval tuning to the
     273              :   // connected socket. Called once from onConnected()/onEncrypted().
     274              :   void tuneKeepAlive();
     275              :   bool beginLogin();
     276              :   void processLine(const QString &line);
     277              :   void handleUntagged(const QString &line);
     278              :   void handleTagged(const QString &line);
     279              :   void restartPush(); // T-320: restart NOTIFY or IDLE based on capabilities
     280              :   bool hasStatefulCommandInFlight() const;
     281              :   bool hasTimeoutTrackedCommandInFlight() const;
     282              :   void enqueueSerializedCommand(std::function<void()> command);
     283              :   void runNextSerializedCommand();
     284              :   void refreshCommandTimeout();
     285              :   void failConnection(const QString &error);
     286              :   QString nextTag();
     287              :   static QString buildFetchBodyCommand(qint64 uid, qint64 maxBytes = -1);
     288              :   static QString quoteImapString(const QString &str);
     289              :   // Sprint 59 (S2): assemble "UID SEARCH <criteria…>" from the facet criteria.
     290              :   // Returns an empty string when no criterion is set. Pure + testable.
     291              :   static QString buildSearchCommand(const SearchCriteria &criteria);
     292              : 
     293              :   QSslSocket *m_socket = nullptr;
     294              :   QTimer *m_timeoutTimer = nullptr;
     295              :   QTimer *m_commandTimeoutTimer = nullptr;
     296              :   QTimer *m_idleRenewTimer = nullptr;
     297              :   // T-720: Liveness-probe watchdogs (single-shot). PROBE_WATCHDOG_MS is the
     298              :   // budget for a probe NOOP or an IDLE DONE/OK round-trip; it is shorter
     299              :   // than COMMAND_TIMEOUT_MS so the monitor can claim a ~30 s worst-case
     300              :   // detection window.
     301              :   QTimer *m_livenessProbeWatchdog = nullptr;
     302              :   QTimer *m_idleRenewWatchdog = nullptr;
     303              :   QString m_probeTag;       // T-720: tag of the in-flight probe NOOP
     304              :   QString m_lastProbeReason; // T-720: reason string for diagnostic logging
     305              :   // T-720: True once tuneKeepAlive() ran (test seam for native tuning).
     306              :   bool m_keepAliveTuned = false;
     307              : 
     308              :   State m_state = State::Disconnected;
     309              :   ImapConfig m_config;
     310              : 
     311              :   int m_tagCounter = 0;
     312              :   QByteArray m_readBuffer;
     313              : 
     314              :   // Track pending commands: tag → command-type ("CAPABILITY", "LOGIN", "LIST")
     315              :   QMap<QString, QString> m_pendingCommands;
     316              : 
     317              :   // Accumulate LIST responses until the tagged OK
     318              :   QList<FolderInfo> m_pendingFolders;
     319              : 
     320              :   // Accumulate FETCH responses until the tagged OK
     321              :   QList<MailHeader> m_pendingHeaders;
     322              : 
     323              :   // Accumulate FETCH FLAGS responses
     324              :   QList<QPair<qint64, quint32>> m_pendingFlags;
     325              : 
     326              :   // Accumulate SEARCH responses
     327              :   QList<qint64> m_pendingSearchUids;
     328              : 
     329              :   // SELECT state
     330              :   QString m_selectedFolder;
     331              :   QString m_pendingSelectFolder; // Bug 34: assigned on SELECT OK, not before
     332              :   int m_selectedMessageCount = 0;
     333              :   quint32 m_selectedUidValidity = 0;
     334              :   quint64 m_selectedHighestModseq = 0; // T-208: CONDSTORE HIGHESTMODSEQ
     335              : 
     336              :   QStringList m_capabilities;
     337              :   bool m_autoIdle = true; // T-205: false for body-fetch connections
     338              : 
     339              :   // IDLE state
     340              :   bool m_isIdling = false;
     341              :   QString m_idleTag; // Tag of the IDLE command (for matching tagged OK)
     342              : 
     343              :   // T-320: NOTIFY state (RFC 5465)
     344              :   bool m_isNotifying = false;
     345              :   QString m_notifyTag;  // Tag of the NOTIFY SET command
     346              :   QStringList m_notifyFolders; // Folders being watched via NOTIFY
     347              : 
     348              :   QString m_pendingStatusFolder; // Folder path for STATUS command in-flight
     349              :   QString m_pendingMoveTarget;            // T-100: Target folder for MOVE/COPY
     350              :   QList<qint64> m_pendingMoveUids;        // T-100: UIDs being moved/copied
     351              : 
     352              :   // T-176: APPEND literal state
     353              :   QByteArray m_pendingAppendData;  // RFC-2822 message bytes to send after +
     354              :   QString m_pendingAppendFolder;   // Target folder for APPEND
     355              : 
     356              :   // T-281: Folder management pending state
     357              :   QString m_pendingFolderOp;       // Folder path for CREATE/DELETE/RENAME
     358              :   QString m_pendingFolderNewPath;  // New path for RENAME
     359              : 
     360              :   // Deferred command queue: commands to execute after IDLE stops
     361              :   QQueue<std::function<void()>> m_deferredCommands;
     362              :   QQueue<std::function<void()>> m_serializedCommands;
     363              : 
     364              :   // IMAP literal accumulation: {N}\r\n followed by N raw bytes
     365              :   qint64 m_literalBytesRemaining = 0; // T-510: was int, overflow on >2GB
     366              :   QString m_literalLine;    // line that triggered the literal
     367              :   QByteArray m_literalData; // accumulated raw bytes
     368              : 
     369              :   // Body literal bypass: when fetching BODY[], keep raw QByteArray
     370              :   // instead of converting through QString (which corrupts binary MIME data)
     371              :   bool m_isBodyLiteral = false;
     372              :   qint64 m_bodyLiteralUid = -1;
     373              : 
     374              :   // T-210: Per-command timing — maps tag → elapsed timer
     375              :   QMap<QString, QElapsedTimer> m_commandTimers;
     376              : 
     377              :   static constexpr int TIMEOUT_MS = 10000;             // 10 seconds
     378              :   static constexpr int COMMAND_TIMEOUT_MS = 30000;     // 30 seconds
     379              :   // T-720: Liveness probe budget. The monitor's 15 s probe interval + this
     380              :   // 15 s watchdog yields ~30 s worst-case dead-socket detection.
     381              :   static constexpr int PROBE_WATCHDOG_MS = 15000;
     382              :   static constexpr qint64 MAX_READ_BUFFER_SIZE = 256LL * 1024 * 1024;
     383              :   static constexpr qint64 MAX_LITERAL_SIZE = 100LL * 1024 * 1024;
     384              :   static constexpr int IDLE_RENEW_MS = 25 * 60 * 1000; // T-271: 25 min (was 28)
     385              :   static constexpr int HEADER_BATCH_SIZE = 50;         // Streaming batch
     386              : };
        

Generated by: LCOV version 2.0-1