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 : };
|