From f1bf86a5c9a5e2bb73b5ccb2534185e43cfce03d Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Mon, 13 Jul 2026 14:12:46 -0700 Subject: [PATCH] fix(bot): preserve HTML body content and isolate mailbox poll failures _extract_body previously discarded real HTML content, replacing it with a placeholder whenever a multipart email had no text/plain part. Now falls back to the raw text/html payload before giving up, only using the placeholder when a message has no text-bearing part at all. poll_mailboxes also had no error isolation: an unhandled IMAP exception from any one mailbox would stop the discord.ext.tasks.Loop permanently, silently killing ingestion for all three mailboxes. Each mailbox's poll is now wrapped in try/except so one failure doesn't block the others. --- bot/chorus_bot/cogs/email_ingest.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/bot/chorus_bot/cogs/email_ingest.py b/bot/chorus_bot/cogs/email_ingest.py index 8abf1bc..fb3075c 100644 --- a/bot/chorus_bot/cogs/email_ingest.py +++ b/bot/chorus_bot/cogs/email_ingest.py @@ -46,7 +46,10 @@ class EmailIngestCog(commands.Cog): host = getattr(self.config, host_attr) user = getattr(self.config, user_attr) password = getattr(self.config, pass_attr) - await self._poll_one(modality, host, user, password) + try: + await self._poll_one(modality, host, user, password) + except Exception as exc: + print(f"Chorus: failed to poll {modality} mailbox ({host}): {exc} — will retry next cycle") async def _poll_one(self, modality: str, host: str, user: str, password: str): seen = self.seen_uids.setdefault(modality, []) @@ -83,10 +86,18 @@ class EmailIngestCog(commands.Cog): @staticmethod def _extract_body(msg) -> str: if msg.is_multipart(): + plain_part = None + html_part = None for part in msg.walk(): - if part.get_content_type() == "text/plain": - return part.get_payload(decode=True).decode(errors="replace") - return "(no plain-text body found)" + if part.get_content_type() == "text/plain" and plain_part is None: + plain_part = part.get_payload(decode=True).decode(errors="replace") + elif part.get_content_type() == "text/html" and html_part is None: + html_part = part.get_payload(decode=True).decode(errors="replace") + if plain_part is not None: + return plain_part + if html_part is not None: + return html_part + return "(no text content found)" return msg.get_payload(decode=True).decode(errors="replace")