Repository navigation
v0.7.0
A breaking release. Two changes need action; everything else is additive.
⚠️ Migrating from 0.6.x
1. headers is now dict[str, list[str]]
Repeated headers used to collapse to their last value, so every earlier Received, DKIM-Signature and Received-SPF was silently discarded — which made delivery-path tracing and signature verification impossible. Each key now maps to every value it appeared with, in order.
- sender = mail.headers["From"]
+ sender = mail.headers["From"][0]
+ hops = mail.headers["Received"] # all of them now, in message orderSingle-valued headers are one-element lists, so you never branch on the type. subject and date are unaffected — they're read from the parsed headers directly.
2. attachments no longer contains body parts or MIME containers
Every node of the MIME tree used to be reported, so a one-image message returned four "attachments": the image, both text bodies, and the multipart/mixed container. Bodies and attachments are now disjoint, following RFC 2183.
- attachments = [a for a in mail.attachments if a.filename] # hand-filtering
+ attachments = mail.attachmentsIf a text/plain part is marked Content-Disposition: attachment, its content is no longer concatenated into text_plain — that previously corrupted the body.
See docs/migrating.md for the full path, including from the stdlib email module. Its snippets are executed in CI, so they can't go stale.
Added
parse_many— batch parsing in one FFI call, results in input order, GIL released once for the batch. Each slot is aPyMailor aParseErrorinstance, so one malformed message doesn't cost you the rest of the batch. See the note on throughput below.- Typed addresses —
from_,to,cc,bcc,reply_toas parsedPyAddressvalues (display_name,address). RFC 5322 groups flattened; a malformed header yields an empty list rather than raising. date_parsed—dateas a timezone-awaredatetime, computed on access so you pay nothing if you don't read it.content_idanddispositionon attachments —content_idis exposed without angle brackets, the form RFC 2392cid:URLs use, so resolving inline images is a dictionary lookup.- A
ParseErrorhierarchy —HeaderParseError,MimeStructureError,DecodeError. All subclassParseError, so existing handlers keep working, but a corrupt attachment is now distinguishable from bytes that were never an email.
Fixed
filenameis read fromContent-Disposition(including RFC 2231 encoded values), falling back toContent-Type; name. Attachments that declare a filename only via the disposition — which is whatemail.message.EmailMessageemits, so most modern mail — previously reported"".subjectanddateno longer inherit the header map's representation.
Compatibility with the stdlib
New docs/compatibility.md documents every known difference from Python's email module, each one enforced by a test that fails if it changes or disappears. The corpus now matches the stdlib byte for byte, attachment payloads included, except five documented differences — plus one case where this library is more correct: the stdlib surrogate-escapes raw UTF-8 in address headers (RFC 6532) where this decodes it.
The one most likely to affect you: body line endings are preserved, not normalised to LF.
On batch throughput
parse_many is strongly regime-dependent, so it is worth knowing which side you are on. Measured against ThreadPoolExecutor(max_workers=4) + parse_email on a 4-vCPU runner:
| Messages | Size each | vs thread pool |
|---|---|---|
| 2000 | 1 KB | 12x faster |
| 400 | 50 KB | 1.2x slower |
| 200 | 786 KB | 1.5x slower |
parse_email already releases the GIL, so a thread pool over it is already parallel. What parse_many removes on top is per-call FFI and scheduling overhead — which scales with the number of messages — while its one cost, copying each payload before parsing starts, scales with total bytes.
So it is a large win for many small messages, which is the mail-pipeline case, and currently a loss for a few very large ones. Removing that copy is tracked in #96.
(These notes originally quoted only the pessimistic half of this, from a benchmark that used 200 × 786 KB — close to the worst case the API can be handed.)
Full changelog: https://github.com/namecheap/fast_mail_parser/blob/v0.7.0/CHANGELOG.md