From abf7ac9fc517922a1aada4ea7a519115ffb04a05 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Mon, 11 May 2026 00:41:45 -0700 Subject: [PATCH] fix(fetcher/idle): make mailbox-update channel send non-blocking The Mailbox callback runs on the IMAP socket-reader goroutine. The prior synchronous send on the 32-buffered mailboxUpdates channel blocked the socket reader whenever the channel filled up, and if the consuming select happened to be blocked (e.g. racing with stop-channel close ordering) IDLE quietly hung until the connection timed out. Switch to a non-blocking send with a default branch so the callback never blocks. Dropping an older count is safe: the consumer only acts on the latest value via prevExists tracking, so any dropped update is superseded by the next one that lands. Closes #1124 --- fetcher/idle.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/fetcher/idle.go b/fetcher/idle.go index a2fc0fbd..cb8c60fa 100644 --- a/fetcher/idle.go +++ b/fetcher/idle.go @@ -158,8 +158,17 @@ func (a *accountIdle) idleOnce() error { mailboxUpdates := make(chan uint32, 32) c, err := connectWithHandler(a.account, &imapclient.UnilateralDataHandler{ Mailbox: func(data *imapclient.UnilateralDataMailbox) { - if data.NumMessages != nil { - mailboxUpdates <- *data.NumMessages + if data.NumMessages == nil { + return + } + // Non-blocking send: the callback runs on the IMAP socket-reader + // goroutine, so a synchronous send would stall the socket if the + // channel is full. The consumer only acts on the latest count + // (see prevExists tracking below), so dropping a stale update is + // safe — the next update will deliver the current count. + select { + case mailboxUpdates <- *data.NumMessages: + default: } }, })