Problem
PyMail is a flattened projection — bodies here, attachments there — and the open classification bugs (#22 phantom parts, #25 ignored Content-Disposition) are symptoms of a deeper limitation: any flattening policy is wrong for someone. Forensics tooling needs the exact MIME topology; nested message/rfc822 (bounced/forwarded mail — ubiquitous in abuse pipelines) is currently smashed into the parent's attachment list with its structure lost; multipart/alternative vs multipart/mixed distinctions (which HTML body corresponds to which plain-text sibling) are unrecoverable. Python's stdlib email has walk(); the fast library should not force a fallback to the slow one for structure.
Proposal
parse_email_tree — the structural API (additive; parse_email unchanged)
node = parse_email_tree(payload) # PyMimePart
# PyMimePart:
# content_type: str # "multipart/alternative", "text/plain", ...
# headers: ... # this part's headers (same header semantics as PyMail post-#23)
# disposition / filename / content_id
# children: list[PyMimePart] # empty for leaves
# content: bytes | None # decoded leaf content (None for multipart containers)
# is_message: bool # message/rfc822 — children contain the parsed inner message's root
Acceptance criteria
Out of scope
Part of the roadmap (see tracking issue). Coordinates with #21 (shared recursion caps), #22/#25 (gives their fix a precise vocabulary), and the lazy-parsing issue (shared modes).
Problem
PyMailis a flattened projection — bodies here, attachments there — and the open classification bugs (#22 phantom parts, #25 ignored Content-Disposition) are symptoms of a deeper limitation: any flattening policy is wrong for someone. Forensics tooling needs the exact MIME topology; nestedmessage/rfc822(bounced/forwarded mail — ubiquitous in abuse pipelines) is currently smashed into the parent's attachment list with its structure lost;multipart/alternativevsmultipart/mixeddistinctions (which HTML body corresponds to which plain-text sibling) are unrecoverable. Python's stdlibemailhaswalk(); the fast library should not force a fallback to the slow one for structure.Proposal
parse_email_tree— the structural API (additive;parse_emailunchanged)walk()generator (depth-first, stdlib-compatible order) implemented Python-side overchildrenfor familiarity.message/rfc822parts expose their inner structure as a subtree (bounded by the existing recursion caps from Harden against untrusted-input DoS: cap input size and MIME recursion depth #21 — nesting counts against the same depth limit).PyMailbecomes (or is documented as) a view over this tree; the flattening rules that attachments includes phantom multipart/body parts (classifier returns non-attachments) #22/Body classification ignores Content-Disposition (RFC 2183) #25 fix get stated in terms of the tree ("a leaf is an attachment iff …") — turning the classifier from folklore into a spec.parse_email_tree(payload, mode="metadata"|"lazy")honors the same modes as the lazy-parsing issue (content deferral per leaf).Acceptance criteria
email.message_from_bytes(...).walk()topology (content types in order) for the full test corpus + RFC corpus (parity test — the strongest correctness oracle available).message/rfc822fixtures (single and doubly-nested bounce) expose inner subject/headers/bodies via the subtree; recursion cap fires at the documented depth with a clearParseError(extendstest_dos_limits.py).contentequals the correspondingPyMailbody/attachment bytes where both APIs expose the part (cross-API consistency test).multipart/alternativefixture: plain and HTML siblings are children of the same node (the relationshipPyMailloses — asserted).walk()order stdlib-identical (fixture test); generator is lazy (no full materialization of a deferred-mode tree on iteration).parse_emailoutputs across the corpus unchanged (pure addition).Out of scope
PyMail's (that spec belongs to attachments includes phantom multipart/body parts (classifier returns non-attachments) #22/Body classification ignores Content-Disposition (RFC 2183) #25's resolution).Part of the roadmap (see tracking issue). Coordinates with #21 (shared recursion caps), #22/#25 (gives their fix a precise vocabulary), and the lazy-parsing issue (shared modes).