Add src/watcher.py with IMAP IDLE FETCH handling fix
This commit is contained in:
+111
@@ -0,0 +1,111 @@
|
||||
async def watch_folder(
|
||||
account: dict,
|
||||
folder: str,
|
||||
webhook_url: str,
|
||||
api_key: str,
|
||||
stop_event: asyncio.Event,
|
||||
):
|
||||
backoff = 5
|
||||
max_backoff = 60
|
||||
|
||||
while not stop_event.is_set():
|
||||
client = None
|
||||
try:
|
||||
client = await asyncio.to_thread(get_imap_client, account)
|
||||
await asyncio.to_thread(
|
||||
client.select_folder,
|
||||
folder,
|
||||
readonly=not account.get("mark_as_read", False),
|
||||
)
|
||||
|
||||
# Check IDLE support
|
||||
if not client.has_capability("IDLE"):
|
||||
logger.warning(
|
||||
"[%s/%s] Server does not support IDLE, falling back to polling",
|
||||
account["id"],
|
||||
folder,
|
||||
)
|
||||
await _poll_folder(
|
||||
client, account, folder, webhook_url, api_key, stop_event
|
||||
)
|
||||
return
|
||||
|
||||
# Record existing unseen UIDs so we only forward truly new ones
|
||||
existing_unseen = set(await asyncio.to_thread(client.search, ["UNSEEN"]))
|
||||
logger.info(
|
||||
"[%s/%s] Watching for new emails via IDLE (%d existing unseen skipped)",
|
||||
account["id"],
|
||||
folder,
|
||||
len(existing_unseen),
|
||||
)
|
||||
backoff = 5
|
||||
|
||||
while not stop_event.is_set():
|
||||
await asyncio.to_thread(client.idle)
|
||||
try:
|
||||
responses = await asyncio.to_thread(client.idle_check, 120)
|
||||
except Exception:
|
||||
try:
|
||||
await asyncio.to_thread(client.idle_done)
|
||||
except Exception:
|
||||
pass
|
||||
break
|
||||
await asyncio.to_thread(client.idle_done)
|
||||
|
||||
logger.debug(
|
||||
"[%s/%s] IDLE responses: %s", account["id"], folder, responses
|
||||
)
|
||||
|
||||
uids = await asyncio.to_thread(client.search, ["UNSEEN"])
|
||||
# Only forward emails that arrived after we started watching
|
||||
new_uids = [u for u in uids if u not in existing_unseen]
|
||||
if not new_uids:
|
||||
continue
|
||||
|
||||
# Fetch the full RFC822 payload for any newly‑seen messages.
|
||||
# Some IMAP servers may emit malformed FETCH responses that cause
|
||||
# ``client.fetch`` to raise a ``ValueError``. Previously this would
|
||||
# crash the watcher loop. We now catch that error, log a warning,
|
||||
# and skip the offending UIDs so the watcher can continue.
|
||||
try:
|
||||
raw_messages = await asyncio.to_thread(
|
||||
client.fetch, new_uids, ["RFC822"]
|
||||
)
|
||||
except ValueError as e:
|
||||
logger.warning(
|
||||
"[%s/%s] IMAP FETCH error for UIDs %s: %s — skipping",
|
||||
account["id"],
|
||||
folder,
|
||||
new_uids,
|
||||
e,
|
||||
)
|
||||
raw_messages = {}
|
||||
for uid, data in raw_messages.items():
|
||||
raw = data.get(b"RFC822", b"")
|
||||
if not raw:
|
||||
continue
|
||||
email_data = parse_email_message(raw)
|
||||
await forward_to_poke(email_data, account, webhook_url, api_key)
|
||||
|
||||
if account.get("mark_as_read", False):
|
||||
await asyncio.to_thread(client.set_flags, new_uids, [b"\\Seen"])
|
||||
existing_unseen.update(new_uids)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"[%s/%s] Watcher error: %s (reconnecting in %ds)",
|
||||
account["id"],
|
||||
folder,
|
||||
e,
|
||||
backoff,
|
||||
)
|
||||
await asyncio.sleep(backoff)
|
||||
backoff = min(backoff * 2, max_backoff)
|
||||
finally:
|
||||
if client:
|
||||
try:
|
||||
await asyncio.to_thread(client.logout)
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user