RelayEmailData.from_email_message raises AttributeError: 'int' object has no attribute 'lower' when an attachment is a stdlib email.message.Message that is not a MIMEBase subclass.
This is currently the single largest error source in our Sentry org — 465,554 occurrences (LEASE-BACKEND-73, Seer actionability super_high), with a second instance in a sibling task (LEASE-BACKEND-72).
Cause
src/email_relay/email.py branches on isinstance(attachment, MIMEBase):
for attachment in email_message.attachments:
if isinstance(attachment, MIMEBase):
...
else:
if isinstance(attachment[1], bytes): # <-- assumes a tuple
The else branch assumes a (filename, content, mimetype) tuple and subscripts it. MIMEBase is a subclass of email.message.Message, but the reverse is not true — a plain email.message.Message (or email.message.EmailMessage) fails the isinstance check, falls into else, and attachment[1] invokes Message.__getitem__(1), which does name.lower() on the int 1:
File "email_relay/email.py", line 86, in from_email_message
if isinstance(attachment[1], bytes):
File "email/message.py", line 429, in __getitem__
return self.get(name)
File "email/message.py", line 510, in get
name = name.lower()
AttributeError: 'int' object has no attribute 'lower'
Because Message defines __getitem__, this fails deep inside stdlib with a confusing error instead of a clear type error at the boundary.
Suggested fix
Widen the isinstance check to the base class, so every Message subclass takes the message branch:
from email.message import Message
if isinstance(attachment, Message):
...
MIMEBase instances still match, and get_payload/get_filename/get_content_type are all defined on Message, so the existing branch body should work unchanged. Worth considering an explicit else guard that raises a clear TypeError when the attachment is neither a Message nor a 3-tuple, so a future mismatch fails legibly.
Reproducing
Attach a plain email.message.Message (not a MIMEBase subclass) to a Django EmailMessage and send it through the relay backend.
Notes
Observed on CPython 3.12.13, environment: production, via django_q workers. Reported from the Sentry issue above; the calling code is in westerveltco/lease (lease/clubs/tasks.py:268), but the defect is here.
RelayEmailData.from_email_messageraisesAttributeError: 'int' object has no attribute 'lower'when an attachment is a stdlibemail.message.Messagethat is not aMIMEBasesubclass.This is currently the single largest error source in our Sentry org — 465,554 occurrences (LEASE-BACKEND-73, Seer actionability
super_high), with a second instance in a sibling task (LEASE-BACKEND-72).Cause
src/email_relay/email.pybranches onisinstance(attachment, MIMEBase):The
elsebranch assumes a(filename, content, mimetype)tuple and subscripts it.MIMEBaseis a subclass ofemail.message.Message, but the reverse is not true — a plainemail.message.Message(oremail.message.EmailMessage) fails the isinstance check, falls intoelse, andattachment[1]invokesMessage.__getitem__(1), which doesname.lower()on the int1:Because
Messagedefines__getitem__, this fails deep inside stdlib with a confusing error instead of a clear type error at the boundary.Suggested fix
Widen the isinstance check to the base class, so every
Messagesubclass takes the message branch:MIMEBaseinstances still match, andget_payload/get_filename/get_content_typeare all defined onMessage, so the existing branch body should work unchanged. Worth considering an explicitelseguard that raises a clearTypeErrorwhen the attachment is neither aMessagenor a 3-tuple, so a future mismatch fails legibly.Reproducing
Attach a plain
email.message.Message(not aMIMEBasesubclass) to a DjangoEmailMessageand send it through the relay backend.Notes
Observed on CPython 3.12.13,
environment: production, viadjango_qworkers. Reported from the Sentry issue above; the calling code is inwesterveltco/lease(lease/clubs/tasks.py:268), but the defect is here.