From 42a1f240f7bcd5d7f3747b1485255ef950a7d0eb Mon Sep 17 00:00:00 2001 From: disqualifier Date: Mon, 6 Jul 2026 00:15:39 -0400 Subject: [PATCH] fix: bound mark_seen's UID STORE with the configured timeout aioimaplib forwards a timeout into IMAP4.uid(...) but drops it specifically for the STORE command (protocol.uid() calls self.store(*criteria, by_uid=True) without passing timeout through, so the Command never arms its internal timer). Combined with IMAP4_SSL's default conn_lost_cb=None, a connection that goes silent during a use_uid=True mark_seen call can hang the coroutine forever, unlike the non-uid store path which is already wrapped by aioimaplib itself. Wrap the uid-store call in asyncio.wait_for(self.timeout) so a stalled server times out and mark_seen returns False like the rest of this method's contract. Signed-off-by: disqualifier --- src/aiomail/client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/aiomail/client.py b/src/aiomail/client.py index 5932139..8d06419 100644 --- a/src/aiomail/client.py +++ b/src/aiomail/client.py @@ -245,12 +245,18 @@ class IMAPClient: return None async def mark_seen(self, email_id: int) -> bool: - """flag a message as read without deleting it""" + """flag a message as read without deleting it + + aioimaplib does not apply its own timeout to the UID STORE command path, so the + use_uid=True call is wrapped here to bound it the same as the non-uid path. + """ if not await self.ensure_connection(): return False try: if self.use_uid: - result, _ = await self._mail.uid("store", str(email_id), "+FLAGS", "(\\Seen)") + result, _ = await asyncio.wait_for( + self._mail.uid("store", str(email_id), "+FLAGS", "(\\Seen)"), self.timeout + ) else: result, _ = await self._mail.store(str(email_id), "+FLAGS", "(\\Seen)") return result == "OK"