jmap-mime
MIME-to-JMAP adapter: converts mime_tree parsed output to jmap-mail-types
body types. Used by jmap-mail-server backends that parse raw RFC 5322
messages.
All MIME parsing lives in mime_tree. This crate only maps field names and
assembles the RFC 8621 §4.1.4 body structure lists. No parsing logic,
content-type matching, or encoding/decoding logic belongs here.
What it is
Three public functions cover the full conversion surface:
part_to_jmap(part, blob_id_for) -> EmailBodyPart
Converts a mime_tree::ParsedPart (and all its children, recursively) to a
jmap_mail_types::email::EmailBodyPart tree.
- The
blob_id_forclosure is called once per non-multipart leaf to assign ablobId; the storage layer decides how to construct these IDs. The closure MUST be idempotent onpart.part_id— repeated calls for parts with the samepart_idmust return the sameId. Counter-based or remote-allocate schemes that return a freshIdon every call produce a silent wire-format defect (the same logical leaf appears under multiple distinctblobIds in the response). Pure functions ofpart.part_idor of the part bytes satisfy the contract trivially. See the "Gotchas" section for the related call-frequency caveat inmessage_to_jmap_body. - Multipart parts receive
Noneforpart_id,blob_id, andsize. - The
headers,language, andlocationfields are not populated because they require access to the raw message bytes; callers that need per-part raw headers can extract them frompart.header_rangeand the original&[u8].
body_value_to_jmap(val) -> EmailBodyValue
Converts a mime_tree::DecodedBodyValue to a
jmap_mail_types::email::EmailBodyValue. This is a direct field rename with no
logic.
message_to_jmap_body(msg, blob_id_for) -> JmapBodyFields
Builds the complete RFC 8621 §4.1.4 body structure from a
mime_tree::ParsedMessage. Returns a JmapBodyFields struct with:
body_structure— full MIME part tree (bodyStructure)text_body—Vec<EmailBodyPart>oftext/plaindisplay parts (textBody)html_body—Vec<EmailBodyPart>oftext/htmldisplay parts (htmlBody)attachments— non-inline, non-display parts (attachments)preview— short plaintext preview, as computed bymime_tree(preview)body_value_part_ids— concatenation oftextBodyandhtmlBodypart IDs (in that order, with no dedup); the caller should decode each viamime_tree::decode_body_valueand insert the resultingEmailBodyValues into thebodyValuesmap of the JMAPEmailresponse. Note: for plain-text-only messages wherehtmlBodymirrorstextBodyper RFC 8621 §4.1.4, the samepart_idappears twice in this list. A caller that builds aHashMapkeyed bypart_idsilently dedups, but a caller that preserves order in aVecor that emits each entry directly to a JSON sink must dedup at the call site.
What it's for
jmap-mime is the thin adapter between mime-tree (the RFC 5322 / 2045
MIME parser) and jmap-mail-types (the RFC 8621 JMAP wire shape). Consumed
by jmap-mail-server backends that parse raw .eml messages, and by any
other consumer needing to convert a mime_tree::ParsedMessage or
ParsedPart into JMAP EmailBodyPart / EmailBodyValue shapes. The crate
exists so that parsing logic stays in mime-tree and JMAP wire shapes stay
in jmap-mail-types; this crate is pure field-rename glue.
How to use
A typical MailBackend::parse_email implementation. A real backend MUST
surface parse/decode errors as JMAP method errors per RFC 8620 §3.6.2 —
never panic on attacker-controlled input.
use ;
use Id;
use ;
Examples
A runnable end-to-end demo lives in examples/parse_eml.rs:
It parses a hand-supplied multipart/alternative .eml fixture, maps it into
JmapBodyFields, and prints the textBody / htmlBody / attachments
shape plus a decoded bodyValues entry for the first text part.
How it works
- Adapter-only design — no parsing logic, no content-type matching, no
encoding/decoding logic lives here. Every function should be obvious
field mapping; if a future change wants to add a
match content_typeor a parsing loop it belongs inmime-treeinstead. - The three
*_to_jmap*entry points cover the full conversion surface:part_to_jmapfor the recursiveParsedPart→EmailBodyPartwalk,body_value_to_jmapforDecodedBodyValue→EmailBodyValuefield renaming, andmessage_to_jmap_bodyfor assembling the full RFC 8621 §4.1.4JmapBodyFields(bodyStructure / textBody / htmlBody / attachments / preview / body_value_part_ids). - Multipart vs leaf-part handling: leaves get a
blobIdfrom the caller-suppliedblob_id_forclosure; multipart parts getNoneforpart_id,blob_id, andsizeper RFC 8621. - Pure conversion: no I/O, no async, no allocator opinions beyond what
mime-treeandjmap-mail-typesalready pull in.
Gotchas
- RFC 2047 encoded-word decoding is
mime_tree's responsibility. Headers containing=?UTF-8?B?...?=or similar encoded-word sequences (e.g. encoded subject lines) are decoded bymime_tree, not this crate. Ensure themime_treeversion in use supports the encodings your server receives. - Body value decoding (quoted-printable, base64) is
mime_tree's responsibility. This crate receives already-decodedDecodedBodyValuestrings frommime_tree::decode_body_valueand maps them toEmailBodyValuefields. Encoding errors surface asis_encoding_problem: trueflags set bymime_treebefore this crate sees them. - No multipart/alternative part selection.
message_to_jmap_bodyreturns alltext/plainandtext/htmlparts intext_bodyandhtml_bodyrespectively; it does not choose between alternatives. RFC 8621 §4.1.4 specifies that the server returns all parts and the client selects. Callers that must present a single body to a user must pick themselves. - No S/MIME or PGP/MIME signature verification. Signed and encrypted
multipart types (
multipart/signed,multipart/encrypted) are treated as ordinary multipart containers. Signature verification and decryption are out of scope for this crate. - Blob ID construction is the consumer's responsibility — and must be
unguessable if access control depends on it. The closure passed to
message_to_jmap_body/part_to_jmapreceives a&ParsedPartwhosepart_idis a deterministic dotted IMAP path ("1","2.1","3.4.7"). The illustrativeformat!("blob-{part_id}")form used throughout the examples is for demonstration only — its output is enumerable across all mailboxes, so a consumer that treats blobId existence as authorization will leak blobs across users. Production backends should either (a) makeblobIdcontent-addressed (SHA-256(part_bytes), optionally surfaced viaurn:ietf:params:jmap:cidper draft-atwood-jmap-cid-00 /jmap-cid-types), (b) makeblobIda random opaque token bound to the (account, part) pair in the storage layer, or (c) enforce access control on(caller, blobId)rather than onblobIdalone. blob_id_foris invoked once per appearance, not once per unique leaf.message_to_jmap_bodywalksbody_structure,text_body,html_body, andattachmentsindependently, calling the closure on every visited leaf. A plain-only message with onetext/plainpart triggers three invocations on that one part (once frombody_structure, once fromtext_body, once fromhtml_body— RFC 8621 §4.1.4 hashtml_bodymirrortext_bodywhen no HTML part exists). The closure MUST be idempotent onpart.part_id(see thepart_to_jmapsection). Non-idempotent closures (counter-based, remote-allocate) produce a silent wire-format defect.- Recursion in this adapter is bounded by
jmap_mime::MAX_PART_DEPTH. A multipart subtree deeper than the bound is emitted as an opaque leaf (a multipart-typedEmailBodyPartwithsub_parts = None). This is defense-in-depth against deeply-nestedmultipart/*framing supplied by hostile senders. The bound applies topart_to_jmapand to every entry point inmessage_to_jmap_body(bodyStructure,textBody,htmlBody,attachments). - Upstream MIME parsing (
mime_tree::parse) is currently unbounded. The adapter's own depth bound (above) does NOT protect the upstream parser from running first on the same hostile input. Consumers MUST bound raw message size upstream — typical SMTP MaxMessageSize caps (25–50 MB) are not sufficient on their own, because a deeply-nested multipart message can pack tens of thousands of nesting levels into well under 1 MB. Consumers that accept arbitrary RFC 5322 bytes from the public internet should either (a) cap message size below the depth at whichmime_tree::parsestack-overflows on their platform, (b) parse on a worker thread with a controlled stack size and a supervisor that restarts on overflow, or (c) wait formime_treeto add its own recursion bound.
Crate family
jmap-types
└── jmap-mail-types EmailBodyPart, EmailBodyValue, etc.
└── jmap-mime ← this crate
jmap-mime also depends on mime_tree (external) for the source types.