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: 92.6% 25 / 0 / 27
Functions: 91.7% 22 / 0 / 24
Branches: 100.0% 1 / 0 / 1

service/ImapService.h
Line Branch Exec Source
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 <QTemporaryFile>
13 #include <QTimer>
14
15 #include <chrono>
16 #include <functional>
17 #include <memory>
18 #include <optional>
19
20 #include "data/AccountConfig.h"
21 #include "data/MailRequest.h"
22 #include "data/Models.h"
23
24 class ImapCommandTable;
25 struct TaggedResponse;
26 struct ImapPendingCommand;
27 class ImapConnectionSetup;
28 class ImapPushSession;
29 class ImapBodyFetch;
30 class ImapMutationCommands;
31 class ImapAppendStream;
32 class ImapLineReader;
33
34 // ImapService manages a single IMAP connection.
35 // Communicates asynchronously via Qt signals/slots.
36 // All network I/O happens on the caller's thread (main thread in Sprint 01).
37 class ImapService : public QObject {
38 Q_OBJECT
39
40 public:
41 enum class State {
42 Disconnected,
43 Connecting,
44 Connected, // TCP connected, waiting for server greeting
45 Greeting, // Server greeting received
46 Capability, // CAPABILITY response received
47 StartingTLS, // STARTTLS in progress
48 Authenticating, // LOGIN sent, waiting for response
49 Authenticated, // LOGIN successful, ready for commands
50 Selected, // Folder selected, ready for FETCH commands
51 Idling, // IDLE command active, waiting for server push
52 Error
53 };
54 2 Q_ENUM(State)
55
56 enum class DisconnectReason {
57 UserRequested,
58 AccountReload,
59 ApplicationShutdown,
60 RemoteBye,
61 SocketError,
62 ProtocolError,
63 CommandTimeout,
64 NetworkUnavailable,
65 ForcedHealthProbe,
66 AuthenticationRejected,
67 TlsPolicyRejected,
68 };
69 53 Q_ENUM(DisconnectReason)
70
71 enum class MutationOutcome {
72 Confirmed,
73 Rejected,
74 CancelledBeforeDispatch,
75 Ambiguous,
76 };
77 2 Q_ENUM(MutationOutcome)
78
79 explicit ImapService(QObject *parent = nullptr);
80 // Post-login capabilities of the current connection (Sprint 95: the
81 // IMAPSIEVE= rule-server hint, RFC 6785).
82 31 QStringList serverCapabilities() const { return m_capabilities; }
83 ~ImapService() override;
84
85 void connectToServer(const ImapConfig &config);
86 void disconnect(DisconnectReason reason = DisconnectReason::UserRequested);
87
88 // Commands (only valid when Authenticated or Selected)
89 void listFolders();
90 void selectFolder(const QString &folderPath);
91 void fetchHeaders(qint64 uidFrom = 1);
92 void fetchBody(qint64 uid);
93 void fetchBody(qint64 uid, qint64 maxBytes);
94 void requestBody(quint64 requestId, qint64 uid);
95
96 // Full-message downloads are deliberately bounded below the protocol hard
97 // cap. Users can raise/lower the limit in Settings; callers with a tighter
98 // protocol payload (for example settings sync) may pass an explicit limit.
99 // F-34 (SPRINT-104, D-9): ONE limit — the composer accepts up to
100 // 100 MiB of attachments (MaxComposeAttachmentsBytes), so a mail sent
101 // from MailJD must also be openable in its own Sent folder. The read
102 // limit matches the send limit; large payloads still spool to a
103 // private temp file instead of RAM (T-87.C2) and stay subject to the
104 // payload cache budget.
105 static constexpr int DefaultMaxMessageSizeMiB = 100;
106 static constexpr int MinMaxMessageSizeMiB = 1;
107 static constexpr int MaxMaxMessageSizeMiB = 100;
108 static qint64 configuredMaxMessageBytes();
109
110 // T-205: Pipeline SELECT + FETCH BODY in one burst (for body connection).
111 void selectAndFetchBody(const QString &folderPath, qint64 uid);
112 void requestBodyInFolder(quint64 requestId, const QString &folderPath,
113 qint64 uid);
114 void markSeen(qint64 uid);
115 void markUnseen(qint64 uid);
116
117 // Generic flag store: add or remove any flag (system flag or keyword)
118 // e.g. storeFlag(uid, "\\Flagged", true) or storeFlag(uid, "$label1", false)
119 void storeFlag(qint64 uid, const QString &flag, bool add,
120 qint64 mutationToken = 0);
121
122 // T-100: Move/copy messages between folders
123 void moveMessage(qint64 uid, const QString &targetFolder,
124 qint64 mutationToken = 0);
125 void moveMessages(const QList<qint64> &uids, const QString &targetFolder,
126 qint64 mutationToken = 0);
127 // F4.3: one UID MOVE uid-set wire command for a batch of journal records;
128 // every token completes with the command's terminal response.
129 void moveMessageSet(const QList<qint64> &uids, const QString &targetFolder,
130 const QList<qint64> &mutationTokens);
131 void copyMessage(qint64 uid, const QString &targetFolder,
132 qint64 mutationToken = 0);
133
134 // Search UIDs in the selected folder (fromUid=1 → all, fromUid>1 → delta).
135 void searchAllUids(qint64 fromUid = 1);
136
137 // T-187: Text-based IMAP SEARCH in the selected folder.
138 // criteria: "TEXT", "SUBJECT", "FROM", "TO", "BODY"
139 void searchText(const QString &query,
140 const QString &criteria = QStringLiteral("TEXT"));
141
142 // Sprint 59 (S2): composite IMAP SEARCH for the visual search facets.
143 // ImapService stays independent of MailCache — the caller (MailController)
144 // translates SearchFilter into this server-friendly subset. Tri-state mirrors
145 // SearchFilter::Tri: Any adds no constraint.
146 enum class SearchTri { Any, Yes, No };
147 struct SearchCriteria {
148 QString text; // TEXT "..."
149 QString from; // FROM "..."
150 QString to; // OR TO "..." CC "..."
151 QString listId; // HEADER List-Id "..." (Sprint 95)
152 QString subject; // SUBJECT "..."
153 QDate since; // SINCE dd-MMM-yyyy (inclusive)
154 QDate before; // BEFORE dd-MMM-yyyy (exclusive per RFC 3501)
155 SearchTri unread = SearchTri::Any; // Yes ⇒ UNSEEN, No ⇒ SEEN
156 SearchTri flagged = SearchTri::Any; // Yes ⇒ FLAGGED, No ⇒ UNFLAGGED
157 SearchTri answered = SearchTri::Any; // Yes ⇒ ANSWERED, No ⇒ UNANSWERED
158 QStringList keywords; // KEYWORD "..." per entry
159 // Sprint 101 D-14: negated criteria, each "NOT …" (UNKEYWORD for labels).
160 QStringList notText, notFrom, notTo, notListId, notSubject, notKeywords;
161
162 // True when nothing constrains the search — such criteria are not sent.
163 bool isEmpty() const;
164 };
165
166 // Run a composite SEARCH built from the criteria. No-op when isEmpty().
167 void search(const SearchCriteria &criteria);
168
169 // T-211: Search by Message-ID header (for undo-move). Returns the search's
170 // sequence number; messageIdSearchFinished carries it back together with the
171 // mailbox the SEARCH actually ran in (S106-187).
172 quint64 searchByMessageId(const QString &messageId);
173
174 // Fetch headers for a specific set of UIDs (comma-separated).
175 void fetchHeadersByUids(const QList<qint64> &uids);
176
177 // T-176: IMAP APPEND – Upload a complete RFC-2822 message to a folder.
178 // flags: e.g. "\\Seen", "\\Draft", or "\\Seen \\Draft"
179 quint64 appendMessage(const QString &folder, const QByteArray &rfcMessage,
180 const QString &flags = {}, qint64 mutationToken = 0);
181 quint64 reserveAppendRequestId();
182 void appendMessage(quint64 requestId, const QString &folder,
183 const QByteArray &rfcMessage,
184 const QString &flags = {}, qint64 mutationToken = 0);
185
186 // S106-011/016: the folder-wide EXPUNGE and "UID STORE 1:* \Seen" had no
187 // production caller and a queued plain EXPUNGE could hit another mailbox;
188 // both were removed. Only the UID-exact form below exists.
189 // RFC 4315 / RFC 9051: remove only these \Deleted UIDs, with an explicit
190 // selected-mailbox epoch. No folder-wide fallback is permitted.
191 void expungeUids(const QList<qint64> &uids, const QString &folder,
192 quint32 uidValidity);
193 bool hasUidExpungeCapability() const;
194
195 // T-281: Folder management commands (RFC 3501 §6.3.3–§6.3.5)
196 void createFolder(const QString &folderPath);
197 void deleteFolder(const QString &folderPath);
198 void renameFolder(const QString &oldPath, const QString &newPath);
199
200 // IDLE support (RFC 2177)
201 void startIdle(); // Enter IDLE mode (must be in Selected state)
202 void stopIdle(); // Send DONE to exit IDLE mode
203 bool isIdling() const;
204 bool hasIdleCapability() const;
205 bool hasCondstoreCapability() const; // T-208: CONDSTORE support
206
207 // T-320: NOTIFY support (RFC 5465) — replaces IDLE + STATUS polling
208 void startNotify(const QStringList &subscribedFolders);
209 void stopNotify(); // Send NOTIFY NONE
210 bool isNotifying() const;
211 bool hasNotifyCapability() const;
212 // NOTIFY (RFC 5465) push is used only when allowed. It was never run
213 // against a real server (GreenMail lacks NOTIFY; Dovecot's post-login
214 // NOTIFY was invisible before its capabilities were read), so IDLE plus
215 // folder polling stays the default until the Dovecot online benchmark
216 // validates new-mail, flag and STATUS pushes.
217 bool usesNotifyPush() const;
218 void setAllowNotifyPush(bool allow);
219 void executeAfterNotify(std::function<void()> command);
220
221 // T-205: Disable automatic IDLE entry after FETCH_BODY/STORE.
222 // Set to false for connections that only do body fetches.
223 void setAutoIdle(bool enabled);
224
225 // Execute a command after safely stopping IDLE or NOTIFY.
226 // If not idling/notifying: executes immediately.
227 // If idling: sends DONE, queues command, executes on IDLE OK.
228 // If notifying: sends NOTIFY NONE, queues command, executes on NOTIFY OK.
229 void executeAfterIdle(std::function<void()> command);
230
231 // T-114: Clear all pending deferred commands (e.g. on folder switch).
232 // Prevents stale SELECTs/FETCHes from executing after the user
233 // switches folders rapidly.
234 void clearDeferredCommands();
235
236 // Flag sync: fetch only UID+FLAGS for all messages in selected folder.
237 void fetchFlags();
238 // F-38: verification readback of one changed UID (partial snapshot).
239 void fetchVerificationFlags(qint64 uid);
240
241 // T-207: Pipeline SELECT + FETCH FLAGS in one burst (saves one round-trip).
242 // Sends both commands immediately; server processes them in-order per RFC 3501.
243 void selectAndFetchFlags(const QString &folderPath);
244 // First open of a folder with an empty cache: SELECT + UID SEARCH ALL.
245 // The UID list is enough to fetch the newest headers first; their FETCH
246 // carries the flags, so no separate UID FETCH 1:* (FLAGS) is needed.
247 void selectAndSearchAll(const QString &folderPath);
248 // Reopen of a cached folder on a CONDSTORE server (RFC 7162): SELECT
249 // (CONDSTORE) + UID FETCH 1:* (UID FLAGS) (CHANGEDSINCE modseq) + the
250 // folder's UID list (ESEARCH when available). Emits changedFlagsReceived
251 // (only messages changed since `modseq`) and uidMembershipReceived.
252 void selectAndFetchChangedFlags(const QString &folderPath, quint64 modseq);
253 bool hasEsearchCapability() const;
254 // Bytes received on this connection since construction (diagnostics and
255 // mailjd-bench transfer metrics).
256 quint64 receivedBytes() const;
257
258 // T-208: Incremental flag sync using CONDSTORE (RFC 4551).
259 // Only fetches flags changed since the given HIGHESTMODSEQ value.
260 void fetchFlagsChanged(quint64 modseq);
261
262 // Fetch UID+FLAGS for a single sequence number (used after IDLE flag push).
263 void fetchUidForSeqNo(int seqNo);
264
265 // Folder status: query UNSEEN/MESSAGES/RECENT without selecting.
266 void statusFolder(const QString &folderPath);
267
268 // T-540: NOOP command for keep-alive (body connection)
269 void sendNoop();
270
271 // T-720: Liveness probe API for ConnectionHealthMonitor.
272 // Idempotent while a probe is already running. The concrete probe depends
273 // on the current state:
274 // Authenticated/Selected → tagged NOOP with a 15 s watchdog
275 // Idling → DONE/OK round-trip with a 15 s watchdog
276 // Connecting/Auth/Error/Disconnected → no-op (handled elsewhere)
277 // On watchdog timeout the connection is failed and the monitor schedules
278 // a reconnect.
279 void requestLivenessProbe(const QString &reason);
280 // S92.5.6: another connection of the account detected a dead network
281 // path (its probe timed out). Kept when it received data within 3 s,
282 // failed when a command waits in the silent path, probed otherwise.
283 void verifyAfterPathFailure(const QString &reason);
284
285 // T-720: Public wrapper around private failConnection() so the
286 // ConnectionHealthMonitor can force a teardown + reconnect on
287 // suspend/resume or a network change. Validates/logs the reason.
288 void abortForReconnect(const QString &reason);
289
290 // T-720: Test seam — true after the TCP-keepalive tuning helper ran
291 // (i.e. onConnected()/onEncrypted() ran the platform setsockopt path).
292 bool keepAliveTuned() const;
293
294 // T-720: Test seam — true while a liveness probe is in flight.
295 bool isLivenessProbeInFlight() const;
296
297 // Currently selected folder path.
298 134 QString selectedFolder() const { return m_selectedFolder; }
299 79 quint32 selectedUidValidity() const { return m_selectedUidValidity; }
300
301 9239 State state() const { return m_state; }
302
303 #ifdef MAILJD_E2E_TESTING
304 // One-shot completion control for deterministic real-server UI races. The
305 // fetch still traverses GreenMail/TLS; only delivery of its terminal result
306 // is delayed or replaced after the server response has completed.
307 void setNextBodyCompletionForE2e(int delayMs, bool fail,
308 const QString &folderPath = {}, qint64 uid = 0);
309
310 // Correlate a product command boundary with one TLS-blind proxy connection
311 // without exposing commands, credentials, or payload bytes.
312 quint16 localPortForE2e() const { return m_socket->localPort(); }
313 #endif
314
315 signals:
316 // Lifecycle intent is separate from stateChanged(): monitors must not turn
317 // an explicit account switch/shutdown into an automatic reconnect.
318 void connectionAttemptStarted();
319 void intentionalDisconnect();
320 void disconnectReasonChanged(ImapService::DisconnectReason reason);
321 void stateChanged(ImapService::State newState);
322 void errorOccurred(const QString &error);
323 void folderListReceived(const QList<FolderInfo> &folders);
324 void folderSelected(const QString &path, int messageCount,
325 quint32 uidValidity, quint64 highestModseq = 0); // T-208
326 // Emitted when a SELECT is rejected by the server (e.g. \Noselect folder).
327 // Lets multi-folder flows (server search) skip the folder instead of
328 // waiting forever for a folderSelected that will never arrive.
329 void folderSelectFailed(const QString &path);
330 void headersReceived(const QList<MailHeader> &headers);
331 void headerFetchComplete(); // All header batches delivered
332 void rawBodyReceived(qint64 uid, const QByteArray &rawBody);
333 void bodyFetchTooLarge(qint64 uid, qint64 maxBytes);
334 void bodyRequestCompleted(const BodyResult &result);
335 void bodyMailboxSelected(quint64 requestId, bool success);
336
337 // IDLE push signals
338 void idleNewMessages(int newCount); // * N EXISTS (N > previous)
339 void idleMessageExpunged(int seqNo); // * N EXPUNGE
340 void idleFlagsChanged(qint64 uid, quint32 flags); // * N FETCH (FLAGS ...)
341 void idleFlagsNeedRefetch(int seqNo); // * N FETCH (FLAGS ...) without UID
342
343 // Flag sync result
344 void flagsReceived(const QList<QPair<qint64, quint32>> &uidFlags);
345 // CHANGEDSINCE result: a partial list, never a membership snapshot.
346 void changedFlagsReceived(const QList<QPair<qint64, quint32>> &uidFlags);
347 // Every UID of the selected folder (UID SEARCH ALL / ESEARCH ALL).
348 void uidMembershipReceived(const QList<qint64> &uids);
349 void flagSnapshotsReceived(const QList<ImapFlagSnapshot> &snapshots,
350 const QString &folderPath);
351 // F-38: partial readback of fetchVerificationFlags — never a complete
352 // snapshot, so the flag-sync reconciliation must not consume it.
353 // S106-195: `requestedUid` is the one UID the readback asked for; its
354 // absence is evidence about that UID only.
355 void verifyFlagsReceived(const QList<ImapFlagSnapshot> &snapshots,
356 const QString &folderPath, qint64 requestedUid);
357
358 // Search result: list of UIDs matching the search query.
359 void searchResultReceived(const QList<qint64> &uids);
360 // S106-024: the server answered a SEARCH with NO/BAD (emitted before the
361 // empty searchResultReceived of the same completion).
362 void searchFailed(const QString &message);
363 // S106-187: the identified completion of searchByMessageId(). `folder` and
364 // `uidValidity` are the selected mailbox at the moment the SEARCH was sent;
365 // `ok` is false for a tagged NO/BAD (uids then empty). Emitted before
366 // searchResultReceived for the same completion.
367 void messageIdSearchFinished(quint64 sequence, const QString &folder,
368 quint32 uidValidity, const QList<qint64> &uids, bool ok);
369
370 // Folder status result
371 void folderStatusReceived(const StatusResult &result);
372 // F-26: a tagged NO/BAD for STATUS — the folder's counts stay as they
373 // were; the polling round continues with the next folder.
374 void folderStatusFailed(const QString &folder);
375 void storeComplete();
376 void mutationDispatched(qint64 token);
377 void mutationCompleted(qint64 token, ImapService::MutationOutcome outcome,
378 const QString &error,
379 const QString &uidPlusEvidence);
380
381 #ifdef MAILJD_E2E_TESTING
382 // Metadata-only command-boundary seam for the external TLS-blind fault
383 // proxy. It intentionally exposes neither command text nor payload bytes.
384 void e2eTransportPhase(const QString &phase, quint64 requestToken);
385 #endif
386
387 // T-100: Move/copy result signals
388 void messageMoved(qint64 uid, const QString &targetFolder);
389 void messagesMoved(const QList<qint64> &uids, const QString &targetFolder);
390 void messageCopied(qint64 uid, const QString &targetFolder);
391 void moveError(const QString &error);
392
393 // T-176: APPEND result signals
394 void messageAppended(const QString &folder, qint64 uid); // uid=0 if no UIDPLUS
395 void appendError(const QString &error);
396 // uid/uidValidity from APPENDUID (RFC 4315); 0 without UIDPLUS.
397 void appendCompleted(quint64 requestId, const QString &folder, qint64 uid, quint32 uidValidity);
398 void appendFailed(quint64 requestId, const QString &error);
399
400 // T-176: EXPUNGE result
401 void expungeComplete();
402 void expungeFailed(const QString &error);
403
404 // T-281: Folder management result signals
405 void folderCreated(const QString &folderPath);
406 void folderDeleted(const QString &folderPath);
407 void folderRenamed(const QString &oldPath, const QString &newPath);
408 void folderOperationError(const QString &operation, const QString &error);
409
410 // Socket and timer handlers. Public only for the test/diagnostic state
411 // contract below (tests drive them without a live socket or real timeouts).
412 public slots:
413 void onConnected();
414 void onEncrypted();
415 void onReadyRead();
416 void onSocketError(QAbstractSocket::SocketError error);
417 void onTimeout();
418 void onCommandTimeout();
419 void onCommandDeadline();
420 void onIdleRenew();
421 // T-720: Liveness-probe watchdog slots.
422 void onLivenessProbeTimeout();
423 void onIdleRenewWatchdogTimeout();
424
425 public:
426 // Test/diagnostic state contract (Sprint 92 S92.0.2 replaced the former
427 // test friend declarations, Sprint 91 rule 8). No product caller outside
428 // ImapService uses it; tests drive the protocol state machine with
429 // synthetic server lines and inject or inspect connection state through
430 // it. The contract stays stable across the Sprint 92 unit extraction:
431 // accessors of state that moves into an internal unit forward to it.
432 enum class SetupPhase {
433 AwaitingGreeting,
434 InitialCapability,
435 StartingTls,
436 PostTlsCapability,
437 PreAuthCapability, // S106-005: PREAUTH greeting on an encrypted connection
438 Login,
439 Complete,
440 };
441 // In-flight commands (tag → command type), a snapshot in tag order.
442 // Accessors of state owned by an internal unit are defined in
443 // ImapServiceContract.cpp.
444 QMap<QString, QString> pendingCommands() const;
445 // FUNC-08: injects the record of an in-flight command; tests set the
446 // command's own state on the returned record.
447 ImapPendingCommand &injectPendingCommand(const QString &tag, const QString &type);
448 // Record of an in-flight tag (nullptr when unknown).
449 ImapPendingCommand *pendingRecord(const QString &tag);
450 // Writes a command with a prepared record (like the product's senders).
451 void sendRecordedCommand(const QString &type, const QString &command,
452 const ImapPendingCommand &record);
453 void removePendingCommand(const QString &tag);
454 void clearPendingCommands();
455 QMap<QString, QElapsedTimer> commandTimers() const;
456 void injectCommandTimer(const QString &tag, const QElapsedTimer &timer);
457 // Connection and session state.
458 263 State &mutableState() { return m_state; }
459 ImapConfig &config();
460 22 QSslSocket *socket() const { return m_socket; }
461 QByteArray &readBuffer();
462 SetupPhase &setupPhase();
463 56 QStringList &capabilities() { return m_capabilities; }
464 QStringList &pendingCapabilities();
465 bool &mutableKeepAliveTuned();
466 bool &autoIdle();
467 QTimer *setupTimeoutTimer() const;
468 // Selected mailbox.
469 217 QString &mutableSelectedFolder() { return m_selectedFolder; }
470 // Folder of the in-flight SELECT (empty when none).
471 QString pendingSelectFolder() const;
472 17 int &selectedMessageCount() { return m_selectedMessageCount; }
473 19 quint32 &mutableSelectedUidValidity() { return m_selectedUidValidity; }
474 4 quint64 &selectedHighestModseq() { return m_selectedHighestModseq; }
475 // Command queues, timers and budgets.
476 QQueue<std::function<void()>> &deferredCommands();
477 QQueue<std::function<void()>> &serializedCommands();
478 QTimer *commandTimeoutTimer() const;
479 QTimer *commandDeadlineTimer() const;
480 qint64 &commandResponseBytes();
481 qint64 &commandLiteralCount();
482 qint64 &commandResultItems();
483 // Response accumulators.
484 5 QList<FolderInfo> &pendingFolders() { return m_pendingFolders; }
485 4 QList<MailHeader> &pendingHeaders() { return m_pendingHeaders; }
486 10 QList<QPair<qint64, quint32>> &pendingFlags() { return m_pendingFlags; }
487 6 QList<qint64> &pendingSearchUids() { return m_pendingSearchUids; }
488 // Push session (IDLE/NOTIFY) and liveness probe.
489 bool &mutableIdling();
490 bool &idleStartPending();
491 QString &idleTag();
492 bool &mutableNotifying();
493 QString &notifyTag();
494 QStringList &notifyFolders();
495 QTimer *idleRenewWatchdog() const;
496 QTimer *livenessProbeWatchdog() const;
497 QString &probeTag();
498 QString &lastProbeReason();
499 // S92.5.3: RTT-aware probe budget of the push session.
500 void noteRoundTrip(qint64 elapsedMs);
501 int probeWatchdogMs() const;
502 // S92.5.2: monotonic clock of the quiet-time measurement (fake in tests).
503 13 void setMonotonicClock(std::function<qint64()> nowMs) { m_nowMs = std::move(nowMs); }
504 qint64 quietMs() const;
505 qint64 &lastReceiveMs();
506 // Per-command state, read from the record of the in-flight command
507 // (FUNC-08; default values when no such command is in flight).
508 QString pendingStatusFolder() const;
509 std::optional<StatusResult> pendingStatusResult() const;
510 qint64 statusRequestStartedNs() const;
511 3 QSet<QString> &queuedStatusFolders() { return m_queuedStatusFolders; }
512 QString pendingMoveTarget() const; // in-flight MOVE/COPY family
513 QList<qint64> pendingMoveUids() const;
514 QList<qint64> pendingMoveMutationTokens() const;
515 qint64 pendingStoreMutationToken() const; // in-flight STORE
516 QSet<qint64> &knownMutationTokens();
517 QByteArray pendingAppendData() const; // in-flight APPEND
518 QString pendingAppendFolder() const;
519 quint64 pendingAppendRequestId() const;
520 qint64 pendingAppendMutationToken() const;
521 quint64 &nextAppendRequestId();
522 // Literal framing and the active body request.
523 qint64 &literalBytesRemaining();
524 QString &literalLine();
525 QByteArray &literalData();
526 std::unique_ptr<QTemporaryFile> &bodySpool();
527 qint64 &bodySpoolThreshold();
528 qint64 &activeBodyFetchUid();
529 quint64 &activeBodyRequestId();
530 QString &activeBodyRequestFolder();
531 qint64 &activeBodyFetchLimit();
532 qint64 &activeBodyFetchRequestBytes();
533 bool &bodyFetchRequiresSelect();
534 // Protocol steps (tests call them with synthetic input).
535 void setState(State newState);
536 void sendCommand(const QString &type, const QString &command);
537 void processLine(const QString &line);
538 void handleUntagged(const QString &line);
539 void handleTagged(const QString &line);
540 void restartPush(); // T-320: restart NOTIFY or IDLE based on capabilities
541 bool hasStatefulCommandInFlight() const;
542 bool hasTimeoutTrackedCommandInFlight() const;
543 void enqueueSerializedCommand(std::function<void()> command);
544 void runNextSerializedCommand();
545 void refreshCommandTimeout();
546 void refreshCommandDeadline();
547 bool accountCommandResponseBytes(qint64 bytes);
548 bool accountCommandLiteral();
549 bool accountCommandResultItems(qint64 items = 1);
550 void completeLiteral();
551 void fetchBodyWithLimits(quint64 requestId, qint64 uid,
552 qint64 acceptedBytes, qint64 requestedBytes);
553 void completeBodyRequest(BodyOutcome outcome, QByteArray bytes = {},
554 const QString &error = {}, qint64 maxBytes = 0);
555 void failConnection(
556 const QString &error,
557 DisconnectReason reason = DisconnectReason::ProtocolError);
558 // T-720: Apply SO_KEEPALIVE + platform-specific interval tuning to the
559 // connected socket. Called once from onConnected()/onEncrypted().
560 void tuneKeepAlive();
561 bool beginLogin();
562 static QString buildFetchBodyCommand(qint64 uid, qint64 maxBytes = -1);
563 static QString quoteImapString(const QString &str);
564 // F-03: modified-UTF-7 encode + quote for every mailbox-name argument.
565 static QString quoteMailboxName(const QString &path);
566 static bool isValidImapFlag(const QString &flag);
567 // Sprint 59 (S2): assemble "UID SEARCH <criteria…>" from the facet criteria.
568 // Returns an empty string when no criterion is set. Pure + testable.
569 static QString buildSearchCommand(const SearchCriteria &criteria);
570 // Protocol limits.
571 static constexpr qint64 MAX_LITERAL_SIZE =
572 (static_cast<qint64>(MaxMaxMessageSizeMiB) * 1024 * 1024) + 1;
573 static constexpr qint64 MAX_COMMAND_RESPONSE_SIZE = 96LL * 1024 * 1024;
574 static constexpr qint64 MAX_LITERALS_PER_COMMAND = 50000;
575 static constexpr qint64 MAX_RESULT_ITEMS_PER_COMMAND = 50000;
576 // Hard cap for one folded logical response. Unlike a lifetime transfer
577 // quota, this protects command-free phases without disconnecting healthy
578 // long-running IDLE/NOTIFY sessions after normal cumulative traffic.
579 static constexpr qint64 MAX_NON_BODY_LITERAL_LINE_SIZE = 8LL * 1024 * 1024;
580
581 private:
582 // S92.1.2: builds the command table with its Deps bound to this service.
583 std::unique_ptr<ImapCommandTable> createCommandTable();
584 // S92.1.1: builds the line reader with its Deps bound to this service.
585 std::unique_ptr<ImapLineReader> createLineReader();
586 // S92.1.3: builds the connection setup with its Deps bound to this service.
587 std::unique_ptr<ImapConnectionSetup> createConnectionSetup();
588 // S92.1.4: builds the push session with its Deps bound to this service.
589 std::unique_ptr<ImapPushSession> createPushSession();
590 // S92.1.5: builds the body fetch unit with its Deps bound to this service.
591 std::unique_ptr<ImapBodyFetch> createBodyFetch();
592 // S92.1.6: builds the mutation commands with their Deps bound to this service.
593 std::unique_ptr<ImapMutationCommands> createMutationCommands();
594 // S92.1.7: builds the APPEND stream with its Deps bound to this service.
595 std::unique_ptr<ImapAppendStream> createAppendStream();
596 // S92.1.8: response dispatch. Tagged completions go through an ordered
597 // handler table; untagged responses are cut by response kind.
598 void dispatchTagged(ImapPendingCommand &command, const TaggedResponse &response);
599 void handleTaggedList(const TaggedResponse &response);
600 void handleTaggedSelect(const TaggedResponse &response, ImapPendingCommand &command);
601 void handleTaggedFetchHeaders(const TaggedResponse &response);
602 void handleTaggedFetchFlags(const TaggedResponse &response);
603 void handleTaggedVerifyFlags(const TaggedResponse &response);
604 void handleTaggedFetchFlagsChanged(const TaggedResponse &response);
605 void handleTaggedSearchMembership(const TaggedResponse &response);
606 void handleTaggedSearch(const TaggedResponse &response);
607 quint64 sendMessageIdSearch(const QString &messageId, quint64 sequence);
608 void handleTaggedFetchSeqno(const TaggedResponse &response);
609 void handleTaggedStatus(const TaggedResponse &response, ImapPendingCommand &command);
610 void handleTaggedCreate(const TaggedResponse &response, ImapPendingCommand &command);
611 void handleTaggedDelete(const TaggedResponse &response, ImapPendingCommand &command);
612 void handleTaggedRename(const TaggedResponse &response, ImapPendingCommand &command);
613 void handleUntaggedList(const QString &data);
614 void handleUntaggedFetch(const QString &type, const QString &data);
615 bool searchResultExpected() const;
616 void handleUntaggedEsearch(const QString &data);
617 void handleUntaggedSearch(const QString &data);
618 void handleUntaggedExists(const QString &type);
619 void handleUntaggedExpunge(const QString &type);
620 void handleUntaggedStatus(const QString &data);
621 void handleUntaggedOk(const QString &data);
622 #ifdef MAILJD_E2E_TESTING
623 quint64 e2eRequestTokenFor(const ImapPendingCommand &command) const;
624 void reportE2eTransportPhase(const QString &phase,
625 quint64 requestToken = 0);
626 #endif
627
628 QSslSocket *m_socket = nullptr;
629 // Monotonic milliseconds for the connection quiet time (S92.5.2).
630 3038 std::function<qint64()> m_nowMs = [] {
631
1/1
✓ Branch 4 → 5 taken 3038 times.
3038 return qint64(std::chrono::duration_cast<std::chrono::milliseconds>(
632 6076 std::chrono::steady_clock::now().time_since_epoch())
633 6076 .count());
634 };
635 // S92.1.2: tags, pending commands, queues, command timeout/deadline and
636 // budgets (declared here so its timers keep their creation position).
637 std::unique_ptr<ImapCommandTable> m_commands;
638 // S92.1.3: TLS/CAPABILITY/LOGIN setup, setup timer, credentials.
639 std::unique_ptr<ImapConnectionSetup> m_setup;
640 // S92.1.4: IDLE/NOTIFY push and the liveness probe (creates the IDLE
641 // renew timer and both watchdogs at their former position).
642 std::unique_ptr<ImapPushSession> m_push;
643 // S92.1.5: body requests and the active body request.
644 std::unique_ptr<ImapBodyFetch> m_body;
645 // S92.1.6: STORE/MOVE/COPY/EXPUNGE and the mutation tokens.
646 std::unique_ptr<ImapMutationCommands> m_mutations;
647 // S92.1.7: APPEND request ids, literal upload and completion.
648 std::unique_ptr<ImapAppendStream> m_append;
649
650 State m_state = State::Disconnected;
651
652 // Accumulate LIST responses until the tagged OK
653 QList<FolderInfo> m_pendingFolders;
654
655 // Accumulate FETCH responses until the tagged OK
656 QList<MailHeader> m_pendingHeaders;
657
658 // Accumulate FETCH FLAGS responses
659 QList<QPair<qint64, quint32>> m_pendingFlags;
660 QList<ImapFlagSnapshot> m_pendingFlagSnapshots;
661
662 // Accumulate SEARCH responses
663 QList<qint64> m_pendingSearchUids;
664 // S106-187: identity of the in-flight Message-ID SEARCH (SEARCH is a
665 // stateful command, so at most one is on the wire). Sequence 0 = none.
666 struct MessageIdSearch {
667 quint64 sequence = 0;
668 QString folder;
669 quint32 uidValidity = 0;
670 };
671 MessageIdSearch m_activeMessageIdSearch;
672 quint64 m_nextMessageIdSearch = 0;
673
674 // SELECT state
675 QString m_selectedFolder;
676 int m_selectedMessageCount = 0;
677 quint32 m_selectedUidValidity = 0;
678 qint64 m_verificationUid = 0; // UID of the FETCH_VERIFY_FLAGS in flight
679 quint64 m_selectedHighestModseq = 0; // T-208: CONDSTORE HIGHESTMODSEQ
680
681 QStringList m_capabilities;
682
683 QSet<QString> m_queuedStatusFolders;
684
685
686
687 // S92.1.1: response framing (read buffer, literals, body spool); created
688 // after the body fetch unit whose active request it references.
689 std::unique_ptr<ImapLineReader> m_reader;
690
691 static constexpr int TIMEOUT_MS = 10000; // 10 seconds
692 static constexpr qint64 MAX_SOCKET_READ_BUFFER_SIZE = 1024LL * 1024;
693 static constexpr int HEADER_BATCH_SIZE = 50; // Streaming batch
694 };
695