Skip to content

Fix Folder::idle() sending NOOP while the session is in IDLE - #634

Open
mathe42 wants to merge 1 commit into
Webklex:masterfrom
accountdesk:fix/idle-no-noop-during-idle
Open

mathe42 wants to merge 1 commit into
Webklex:masterfrom
accountdesk:fix/idle-no-noop-during-idle

Conversation

@mathe42

@mathe42 mathe42 commented Oct 22, 2026

Copy link
Copy Markdown

Problem

Folder::idle() calls $idle_client->getConnection() on every loop iteration. Client::getConnection() runs checkConnection() → isConnected() → ImapProtocol::connected(), which sends a NOOP — while the idle session is in IDLE.

RFC 2177 only allows DONE during IDLE. Dovecot answers BAD Expected DONE and terminates the IDLE command. checkConnection() catches the failure and silently calls connect(), which opens a new session without SELECT and without IDLE. From then on no EXISTS is ever reported; new messages are only seen by whatever polls next. The connected() check in the catch block has the same effect.

Fix

  • Fetch the protocol connection once, before entering IDLE, and use it directly inside the loop (nextLine(), meta()).
  • After an empty read, decide via the stream metadata (eof) whether the connection is still alive, instead of connected() (which sends a NOOP).

The main client ($this->client) is not in IDLE, so its isConnected() check in the EXISTS branch is left unchanged.

Test

tests/IdleTest.php starts a small fake IMAP server (tests/support/idle_server.php) that reacts to any command other than DONE during IDLE like Dovecot does, and records every command. The test asserts that no command except DONE is sent while idling and that the idle session is not silently re-established. It fails on current master (TAG6 NOOP received during IDLE) and passes with this change.

Related: #624 (busy loop / throw after silent periods in the same method).

Client::getConnection() runs checkConnection(), which verifies the session
with a NOOP. idle() called it on every loop iteration, i.e. while the idle
session was in IDLE. rfc2177 only allows DONE there - Dovecot answers
"BAD Expected DONE" and ends the IDLE, checkConnection() then silently
opens a new session without SELECT/IDLE, so no EXISTS is ever reported.

Fetch the protocol connection once before entering IDLE and use it
directly inside the loop. The liveness check after an empty read now uses
the stream metadata (eof) instead of connected(), which also sends a NOOP.

Adds a regression test with a small fake IMAP server that reacts to any
command other than DONE during IDLE the way Dovecot does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant