Skip to content

fix(email): guard IMAP FETCH response against non-bytes payloads#12498

Open
Tipiweb wants to merge 1 commit into
NousResearch:mainfrom
Tipiweb:fix/email-imap-int-decode
Open

fix(email): guard IMAP FETCH response against non-bytes payloads#12498
Tipiweb wants to merge 1 commit into
NousResearch:mainfrom
Tipiweb:fix/email-imap-int-decode

Conversation

@Tipiweb

@Tipiweb Tipiweb commented Apr 19, 2026

Copy link
Copy Markdown

What changed and why

Some IMAP servers return malformed FETCH responses where the body slot is an int, plain bytes, or None instead of the expected (header, body) tuple. Accessing msg_data[0][1] then either raises IndexError/TypeError, or returns an int (indexing bytes yields an int in Python 3), which only surfaces later as the opaque error:

ERROR gateway.platforms.email: [Email] IMAP fetch error: 'int' object has no attribute 'decode'

The exception kills the inner loop iteration and is caught by the outer try/except at the bottom of _fetch_new_messages, so the entire polling pass returns empty — no messages are dispatched for that tick.

Observed in production on a Raspberry Pi 4 / Debian bookworm talking to an OVH IMAP server. The same UID was retried every poll and flooded the error log.

Related

Complements #2794 (which adds a general try/except around IMAP response parsing) by specifically handling the non-bytes payload case that the bare exception catch would still mask behind a cryptic error message.

The fix

Two-stage guard around the fetch response, inside the per-UID loop in _fetch_new_messages:

  1. try/except (IndexError, TypeError) around msg_data[0][1] access.
  2. isinstance(raw_email, (bytes, bytearray)) check before handing off to email_lib.message_from_bytes.

In both branches we log a warning with the offending UID (and the raw response, or the observed payload type) and continue, so the poll loop keeps processing the rest of the inbox rather than bailing on a single malformed message.

How I tested

Unit-tested the guard logic against 6 shapes observed in the wild:

Input Behavior
[(b'flags', b'body')] parses OK
[b')'] skipped (IndexError)
[None] skipped (TypeError)
[] skipped (IndexError)
[(b'flags', b'body'), b')'] parses OK
[(1, 2)]our case skipped (not bytes)

Deployed on the affected Raspberry Pi — previously the error fired every ~10 min; since the patch the log is clean and legitimate mail is still processed.

Platforms tested

  • Raspberry Pi 4, Debian bookworm, Python 3.11
  • IMAP server: OVH (ssl0.ovh.net)

Some IMAP servers return malformed FETCH responses in which the body slot

is an int, plain bytes, or None instead of the expected (header, body)

tuple. Accessing msg_data[0][1] then either raises IndexError/TypeError

or returns an int, which surfaces later as the opaque error:

    [Email] IMAP fetch error: int object has no attribute decode

This commit adds a two-stage guard around the fetch response:

  1. try/except around msg_data[0][1] access (IndexError, TypeError)

  2. isinstance(raw_email, (bytes, bytearray)) check before parsing

In both branches we log a warning with the offending UID and continue,

so the poll loop keeps processing the rest of the inbox rather than

dying on a single malformed message.

Observed on Raspberry Pi 4 / Debian bookworm talking to an OVH IMAP

server; prior to this patch the same UID would be retried forever

and fill the error log.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists platform/email Email (IMAP/SMTP) adapter comp/gateway Gateway runner, session dispatch, delivery labels Apr 23, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related to #2794 (general IMAP response hardening) — this PR specifically handles the non-bytes payload case that #2794's bare except would still mask.

@mwhuss

mwhuss commented May 1, 2026

Copy link
Copy Markdown

I'm testing this patch and now instead of an error (#18106) I see a warning. Just sharing in case this is unexpected. I also never got an email response so I may have a case where the build in email messaging doesn't work with @iCloud.com email accounts.

2026-05-01 20:23:03,853 WARNING gateway.platforms.email: [Email] Skipping UID b'17': IMAP payload not bytes (got int)

@Tipiweb

Tipiweb commented May 20, 2026

Copy link
Copy Markdown
Author

Thanks for testing! The warning is the intended behavior — when iCloud (or any IMAP server) returns a FETCH response we don't recognize, we now skip that specific UID and keep polling the rest, instead of killing the whole poll loop on a cryptic 'int' object has no attribute 'decode'. So this PR fixes the crash, it doesn't try to recover the malformed payload itself.

Your log shows UID 17 was skipped because the server returned an int where a bytes body was expected — that's exactly the case this PR catches.

Re: "never got an email response" — if you were waiting on a reply to UID 17 specifically, that tracks: we skipped it. A fresh message should be picked up normally (different UID, different response shape). If you keep seeing the same skip pattern on every iCloud message, that's a separate iCloud-specific issue worth its own bug report (with a redacted raw IMAP trace from imaplib.Debug = 4 so we can see what iCloud is actually returning). Happy to take a look if you open one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/email Email (IMAP/SMTP) adapter type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants