Skip to content

feat(imap): add waitForContinuation flag to idleStart (RFC 2177 hand-off support)#274

Merged
robert-virkus merged 1 commit into
Enough-Software:mainfrom
brlumen:idle-wait-continuation
Apr 25, 2026
Merged

feat(imap): add waitForContinuation flag to idleStart (RFC 2177 hand-off support)#274
robert-virkus merged 1 commit into
Enough-Software:mainfrom
brlumen:idle-wait-continuation

Conversation

@brlumen
Copy link
Copy Markdown
Contributor

@brlumen brlumen commented Apr 14, 2026

Summary

Adds optional waitForContinuation parameter to ImapClient.idleStart() so callers can wait for the server's + idling continuation response (per RFC 2177 §3) before considering IDLE mode active on the server.

Motivation

Current idleStart() returns a pre-completed Future<T>.value() right after queueing the IDLE command (imap_client.dart L2282-2286) — the task's completer is reserved for idleDone() (tagged OK after DONE). This creates a race when the caller plans to do something right after idleStart() returns: the IDLE command may still be in flight, and imap's + idling arrives into a socket buffer the caller has already stopped reading.

I hit this while building a WebSocket-IMAP proxy that hands off the TCP connection to a different process once the mobile client enters IDLE and disconnects: the proxy was receiving + idling after hand-off and mis-interpreting it as a real notification. RFC 2177 §3 explicitly states that "as long as an IDLE command is active, the server is now free to send untagged EXISTS, EXPUNGE, and other messages at any time" — i.e. IDLE isn't actually active from the server's perspective until it has sent the continuation.

Changes

  • idleStart({bool waitForContinuation = false}). Default preserves existing semantics; backwards compatible.
  • With waitForContinuation: true, the returned future resolves when onContinuationResponse receives the + in IDLE mode.
  • Cleanup added in idleDone(), disconnect() and onConnectionError so the future never hangs if the continuation cannot arrive — it fails with an ImapException instead.
  • onContinuationResponse now resolves the pending completer when in IDLE mode (previously only logged when not in IDLE mode).

Tests

Two new tests (file test/imap/imap_client_test.dart):

  • ImapClient idle waitForContinuation resolves on + idling — happy path.
  • ImapClient idle waitForContinuation fails on disconnect — future errors out when disconnect() is called before the continuation arrives.

All 51 existing tests in imap_client_test.dart continue to pass locally (dart test test/imap/imap_client_test.dart --timeout=30s).

Compatibility

  • Default behavior of idleStart() unchanged — no parameter required.
  • Only new public API surface is the optional parameter and the documented failure mode of the returned future when waitForContinuation: true.

🤖 Generated with Claude Code

Per RFC 2177 §3, IDLE mode is not active on the server until it has sent
the `+ idling` continuation response. Callers that plan to rely on this
guarantee (e.g. disconnect immediately after, hand off the TCP connection
to another process, or start parsing untagged responses externally) need
to wait for that response.

Today `idleStart()` returns a pre-completed Future<T>.value() right after
queueing the IDLE command, because the command task's completer is only
resolved on the tagged OK that follows DONE. This creates a race: on slow
networks the caller can wrap up and close the socket before `+ idling`
arrives, leaving the continuation in the peer's TCP buffer.

This change:

- Adds `idleStart({bool waitForContinuation = false})`. Default keeps the
  existing immediate-return semantics for backward compatibility.
- When `waitForContinuation: true`, the returned future resolves on the
  next continuation response received while in IDLE mode, and fails with
  `ImapException` if the client disconnects or hits a connection error
  before the continuation arrives.
- `onContinuationResponse` now resolves the pending completer when in
  IDLE mode (previously it only logged a warning when not in IDLE mode).
- Cleanup added to `idleDone()`, `disconnect()` and `onConnectionError`
  so callers never hang on the returned future.

Tests added for the new flag (resolves on `+ idling`; fails on disconnect).
All existing tests continue to pass.
@robert-virkus robert-virkus merged commit 1fa5c15 into Enough-Software:main Apr 25, 2026
1 check failed
@robert-virkus
Copy link
Copy Markdown
Member

Thanks so much for your contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants