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 <dev@disqualifier.me>
This commit is contained in:
2026-07-06 00:15:39 -04:00
parent 5f23abc9c7
commit 42a1f240f7
+8 -2
View File
@@ -245,12 +245,18 @@ class IMAPClient:
return None return None
async def mark_seen(self, email_id: int) -> bool: 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(): if not await self.ensure_connection():
return False return False
try: try:
if self.use_uid: 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: else:
result, _ = await self._mail.store(str(email_id), "+FLAGS", "(\\Seen)") result, _ = await self._mail.store(str(email_id), "+FLAGS", "(\\Seen)")
return result == "OK" return result == "OK"