Repository navigation
[2.x] Add typed mailbox events for IDLE - #187
Merged
Merged
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
FakeFolder::idle() incorrectly rejects valid non-closure callable timeout values.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Adds typed mailbox events and a dedicated, reconnectable IMAP IDLE session.
Changes:
- Introduces typed selection, count, FETCH, EXPUNGE, VANISHED, and unknown events.
- Moves IDLE lifecycle and buffering into
IdleSession. - Updates folder APIs, fakes, and tests for event-driven watching.
| File | Description |
|---|---|
tests/Unit/IdleTest.php |
Tests watcher lifecycle and renewal. |
tests/Unit/IdleEventTest.php |
Tests event parsing and fake delivery. |
tests/Unit/Connection/ImapConnectionTest.php |
Tests connection IDLE and I/O APIs. |
tests/Unit/Connection/IdleTest.php |
Tests IDLE session protocol handling. |
src/Testing/FakeFolder.php |
Adds configurable fake IDLE events. |
src/Idle/Events/UnknownEvent.php |
Represents unmodeled responses. |
src/Idle/Events/ResponseEvent.php |
Provides shared response event behavior. |
src/Idle/Events/MessagesVanished.php |
Exposes vanished UIDs. |
src/Idle/Events/MessagesExist.php |
Exposes mailbox message counts. |
src/Idle/Events/MessageFetched.php |
Exposes FETCH changes. |
src/Idle/Events/MessageExpunged.php |
Exposes expunged sequence numbers. |
src/Idle/Events/FolderSelected.php |
Exposes selection metadata. |
src/Idle/Events/EventInterface.php |
Defines the event contract. |
src/Idle/EventFactory.php |
Maps responses to event classes. |
src/Idle.php |
Implements event-based watching and reconnects. |
src/FolderInterface.php |
Updates the public IDLE API. |
src/Folder.php |
Uses a dedicated event watcher connection. |
src/Connection/ImapConnection.php |
Integrates sessions and public protocol I/O. |
src/Connection/IdleSession.php |
Manages the IDLE protocol lifecycle. |
src/Connection/ConnectionInterface.php |
Exposes the revised connection APIs. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…-Closure callable timeouts' Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

IMAP IDLE reports mailbox changes, not individual new messages. Treating each EXISTS response as a single message to fetch can miss arrivals when several messages arrive together.
This PR separates mailbox events from message delivery and moves the IDLE protocol exchange into a dedicated session.
Mailbox events
Folder::events($callback, $timeout = 300, ...$options)exposes typed mailbox events without fetching messages:Idle\Events, sharing anEventInterfacefor callback type hints and instanceof checks.FolderSelectedon initial selection and after reconnecting so applications can reconcile saved checkpoints.UnknownEvent.The watcher uses its own connection. Returning false stops it, callback exceptions propagate, and the dedicated connection closes on exit.
The functions above represent application code. Applications using this API own message retrieval, checkpoint storage, and reconciliation. EXISTS contains the current mailbox count, not the UID of a newly arrived message.
Message delivery
Folder::idle($callback, $query = null, $timeout = 300, ...$options)continues to deliver messages and retains its optional query callback.Folder::poll($callback, $query = null, $frequency = 60)provides the same message-focused API through periodic checks.Both methods track arrivals in ascending UID order, exclude existing messages at startup, preserve the cursor across reconnects with the same UID validity, and establish a new baseline when UID validity changes. Returning false stops delivery, and callback exceptions propagate.
Message queries expose
cursor($chunkSize = 10)for bounded fetching. The cursor loads matching UIDs once and retrieves messages in batches; IDLE and polling use this instead of fetching every arrival at once. Query pagination does not apply to the cursor.IDLE retrieves messages through the original folder connection while
Watchlistens on a separate connection. Reconnects do not provide durable synchronization guarantees; applications needing those should useevents()and maintain checkpoints.Session handling
IdleSessionowns the continuation handshake, buffered updates, renewal deadline, and DONE exchange. Updates received before the continuation or while finishing IDLE are preserved.Connection
idle()returns anIdleSession. Consume updates throughresponses($timeout)and finish throughfinish(), which returns remaining updates and replaces connectiondone().The connection blocks normal commands while IDLE is active and invalidates the session when disconnected. The watcher uses the mailbox's selected-folder state. Selection options passed to
events()oridle()apply to the dedicated watching connection.Renewal defaults to 300 seconds and honors the developer's chosen interval without an upper cap. Incoming updates do not extend the deadline. Polling frequency controls the delay between checks instead.
Connection
read()andwrite()expose parsed/logged protocol IO, whilestream()provides low-level transport access. Direct stream access bypasses connection bookkeeping.Fakes
FakeFolder::setIdleEvents(array $events)supplies events toevents()after its initial selection event. Fakeidle()andpoll()deliver configured messages and support query callbacks and stopping.