Reading a message: a From or Subject sent as raw UTF-8 loses the sender and garbles the subject #21
Labels
No labels
bug
documentation
enhancement
performance
security
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo-imap#21
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo-imap at
cc09c6381, runningparseon messages whose headers carry unencoded bytes outside ASCII.parsereads headers with the standard library's default (compat32) policy. For a header that contains a byte outside ASCII which is not wrapped in an RFC 2047 encoded word, that policy returns anemail.header.Headerobject, not astr.email.utils.parseaddranswers('', '')for anything that is not a string, so the sender and the sender's name come out empty, and_decodedturns the same kind ofSubjectinto replacement characters instead of decoding it. Internationalised mail (RFC 6532) and a good many in-house systems send headers this way.src/postulo_imap/mailbox.py:135-136,parse:message = email.message_from_bytes(raw)andemail.utils.parseaddr(message.get("From", "")).src/postulo_imap/mailbox.py:83-90,_decoded:str(make_header(decode_header(value)))on aHeaderwhose charset is unknown yields U+FFFD for every such byte.src/postulo_imap/mailbox.py:150-153:sender,sender_nameandsubjectare built from those two.From: Müller GmbH <jobs@mueller.test>andSubject: Ihre Bewerbung bei Müller, both as raw UTF-8:mail.senderis'',mail.sender_nameis'',mail.domainis'', andmail.subjectis'Ihre Bewerbung bei M��ller'. With the same headers in Latin-1 the result is the same with one replacement character.type(message.get("From")).__name__isHeader;parseaddrof it is('', '').matchruns (src/postulo_imap/matching.py:105and:117-118), so the two strongest signals after the thread are lost. The company name, nowM��ller, can no longer be found in the subject either, which is half of what the last step searches (mailbox.py:79-80).From: jobs@mueller.test, raw UTF-8 subject) keeps the sender and still garbles the subject. Headers written as RFC 2047 encoded words are read correctly;tests/test_imap.py:242-245covers only that form.Headerobject is what makesparsedate_to_datetimeraise on aDateheader with such a byte, which is reported separately in this review; one way of reading headers fixes both.message.raw_items()as a string in which each undecodable byte is a surrogate escape: encode it back with"ascii", "surrogateescape", decode the bytes as UTF-8 and fall back to Latin-1, then hand the result toparseaddr,_decodedandparsedate_to_datetime. Tried on the snapshot: this gives('Müller GmbH', 'jobs@mueller.test')and the right subject for both encodings. Switching topolicy=email.policy.defaulton its own is not enough:message["From"].addresses[0].display_namecame back as'M\udcc3\udcbcller GmbH', lone surrogates that cannot be encoded as UTF-8, so storing or rendering the suggestion would fail (reasoned, not run), which trades a garbled name for a failed pass.Done when
tests/test_imap.pyhas a test in whichparseof a message withFrom: Müller GmbH <jobs@mueller.test>andSubject: Ihre Bewerbung bei Müller, encoded once as UTF-8 and once as Latin-1 with no encoded words, returnssender == "jobs@mueller.test",sender_name == "Müller GmbH"and the subject unchanged, and no string of theMailcontains U+FFFD or a surrogate. It fails today on all three fields.Fixed by pull request #25.