A single IDLE connection held indefinitely gets dropped by servers after ~29 min
(RFC 2177) and severed by NAT/firewalls sooner — silently stranding push.
- IdleService bounds each IDLE session with withTimeoutOrNull(IDLE_RENEWAL_MS = 9 min)
and reconnects, re-issuing IDLE well within those limits. Each reconnect catch-up
syncs, so no mail is missed across renewals.
- ImapClient.idle() now closes the store from an awaitCancellation() child that runs at
cancellation *start*. A Job completion handler never runs while idle()'s blocking read
is stuck cancelling, so it could not unblock idle(); this can, so both renewal and
service stop break out of idle() promptly.
- Verified on the Android 17 emulator against GreenMail: the IDLE connection
re-established on schedule (each cycle = fresh connect + IDLE), and a message delivered
mid-run still pushed a notification within ~2s.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>