Parse and build RFC 5322 + MIME multipart email messages. The surface covers parsing inbound messages, inspecting headers and bodies, extracting attachments, decoding encoded-words and address lists, and building outgoing messages with text + HTML alternatives and attachments. Delivery-status (bounce) reports are lifted into structured form. Transport is out of scope — send/receive lives at the directive layer; this module is the data layer those transports produce or consume.
email:parse
[$email:parse] -> element — Parse RFC 5322 bytes into a [message] element, decoding headers, bodies, and the multipart tree.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "From: a@b.com\r\nTo: c@d.com\r\nSubject: Hi\r\n\r\nHello body\r\n"]]]
[$email:is-multipart $msg]]
false
email:emit
[$email:emit] -> bytes — Re-encode a [message] element to canonical RFC 5322 bytes; same input emits byte-identical output.
[?lib 'cx-stdlib/email']
[?let [= $msg [$email:build {"from": "agent@example.com",
"to": "customer@example.com",
"subject": "Re: Invoice question",
"body": "Thank you for reaching out..."}]]
[$email:subject [$email:parse [$email:emit $msg]]]]
'Re: Invoice question'
email:headers
[$email:headers] -> map — Return all headers as a map of lowercased name to a [sequence string] of values.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "From: a@b.com\r\nSubject: Hi\r\n\r\nbody"]]]
[$count [$email:headers $msg]]]
2
email:header
[$email:header] -> string — Return the first value of the named header (case-insensitive), or empty string if absent.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "From: a@b.com\r\nSubject: Hi\r\n\r\nbody"]]]
[$email:header $msg "subject"]]
'Hi'
email:header-values
[$email:header-values] -> [sequence string] — Return every value of the named header (case-insensitive), for multi-instance headers like Received.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "Received: from a\r\nReceived: from b\r\nFrom: a@b.com\r\n\r\nbody"]]]
[$email:header-values $msg "Received"]]
('from a', 'from b')
email:subject
[$email:subject] -> string — Return the Subject header with encoded-words decoded to UTF-8.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $crlf [$email:parse [$bytes:from-string-utf8 "Subject: Hi\r\n\r\nbody"]]]
[= $lf [$email:parse [$bytes:from-string-utf8 "Subject: Hi\n\nbody"]]]
[$eq [$email:subject $crlf] [$email:subject $lf]]]
true
email:from-addr
[$email:from-addr] -> element — Return the first From address as an [address] element.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "From: John Doe \r\n\r\nbody"]]]
[$email:format-address [$email:from-addr $msg]]]
'John Doe '
email:to-addrs
[$email:to-addrs] -> [sequence element] — Return all To addresses, expanding any RFC 5322 groups.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "To: a@x.com, b@x.com\r\nFrom: c@y.com\r\n\r\nbody"]]]
[$count [$email:to-addrs $msg]]]
2
email:cc-addrs
[$email:cc-addrs] -> [sequence element] — Return all Cc addresses, expanding any RFC 5322 groups.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "From: c@y.com\r\nCc: a@x.com\r\n\r\nbody"]]]
[$count [$email:cc-addrs $msg]]]
1
email:bcc-addrs
[$email:bcc-addrs] -> [sequence element] — Return all Bcc addresses, expanding any RFC 5322 groups.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "From: c@y.com\r\nBcc: a@x.com, b@x.com\r\n\r\nbody"]]]
[$count [$email:bcc-addrs $msg]]]
2
email:date-header
[$email:date-header] -> datetime — Return the Date header parsed from RFC 5322 form into a datetime.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "Date: Tue, 26 May 2026 14:30:00 +0000\r\nFrom: a@b.com\r\n\r\nbody"]]]
[$email:date-header $msg]]
'2026-05-26T14:30:00Z'
email:message-id
[$email:message-id] -> string — Return the Message-ID value with the surrounding angle brackets stripped.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "Message-ID: \r\nFrom: a@b.com\r\n\r\nbody"]]]
[$email:message-id $msg]]
'abc123@example.com'
email:in-reply-to
[$email:in-reply-to] -> string — Return the In-Reply-To value with the surrounding angle brackets stripped.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "In-Reply-To: \r\nFrom: a@b.com\r\n\r\nbody"]]]
[$email:in-reply-to $msg]]
'def456@example.com'
email:references
[$email:references] -> [sequence string] — Return the References threading chain as a sequence of message IDs.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "References: \r\nFrom: c@y.com\r\n\r\nbody"]]]
[$count [$email:references $msg]]]
2
email:list-unsubscribe
[$email:list-unsubscribe] -> [sequence string] — Return the URIs from the List-Unsubscribe header as a sequence.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "List-Unsubscribe: , \r\nFrom: a@b.com\r\n\r\nbody"]]]
[$count [$email:list-unsubscribe $msg]]]
2
email:parts
[$email:parts] -> [sequence element] — Return all body parts flattened (a single part for non-multipart messages).
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "From: a@b.com\r\nContent-Type: text/plain\r\n\r\njust text"]]]
[$count [$email:parts $msg]]]
1
email:attachments
[$email:attachments] -> [sequence element] — Return parts marked as attachments, each carrying filename, content-type, and decoded bytes.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "From: a@b.com\r\nContent-Type: multipart/mixed; boundary=\"B\"\r\n\r\n--B\r\nContent-Type: text/plain\r\n\r\nhi\r\n--B\r\nContent-Type: application/pdf\r\nContent-Disposition: attachment; filename=\"invoice.pdf\"\r\n\r\nPDF\r\n--B\r\nContent-Type: image/png\r\nContent-Disposition: attachment; filename=\"img.png\"\r\n\r\nPNG\r\n--B--\r\n"]]]
[$count [$email:attachments $msg]]]
2
email:text-body
[$email:text-body] -> string — Return the first text/plain part decoded to UTF-8, or empty if absent.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "From: a@b.com\r\nContent-Type: multipart/alternative; boundary=\"B\"\r\n\r\n--B\r\nContent-Type: text/plain\r\n\r\nplain text here\r\n--B\r\nContent-Type: text/html\r\n\r\nhtml here
\r\n--B--\r\n"]]]
[$email:text-body $msg]]
'plain text here'
email:html-body
[$email:html-body] -> string — Return the first text/html part decoded to UTF-8 — raw and unsanitized; sanitize before rendering.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "From: a@b.com\r\nContent-Type: multipart/alternative; boundary=\"B\"\r\n\r\n--B\r\nContent-Type: text/plain\r\n\r\nplain text here\r\n--B\r\nContent-Type: text/html\r\n\r\nhtml here
\r\n--B--\r\n"]]]
[$email:html-body $msg]]
'html here
'
email:is-multipart
[$email:is-multipart] -> bool — Report whether the message body is a multipart tree rather than a single part.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "From: a@b.com\r\nTo: c@d.com\r\nSubject: Hi\r\n\r\nHello body\r\n"]]]
[$email:is-multipart $msg]]
false
email:decode-encoded-word
[$email:decode-encoded-word] -> string — Decode an RFC 2047 encoded-word (Q or B) from its declared charset to UTF-8.
[?lib 'cx-stdlib/email']
[$email:decode-encoded-word "=?UTF-8?Q?Hello_=E4=B8=96=E7=95=8C?="]
'Hello 世界'
email:encode-encoded-word
[$email:encode-encoded-word] -> string — Encode a string as an RFC 2047 encoded-word using the given charset and Q or B encoding.
[?lib 'cx-stdlib/email']
[$email:encode-encoded-word "Hi" "UTF-8" "Q"]
'=?UTF-8?Q?Hi?='
email:parse-address
[$email:parse-address] -> element — Parse a single address string into an [address] element.
[?lib 'cx-stdlib/email']
[$email:format-address [$email:parse-address "John Doe "]]
'John Doe '
email:parse-address-list
[$email:parse-address-list] -> [sequence element] — Parse an address list into a sequence mixing [address] and [address-group] elements.
[?lib 'cx-stdlib/email']
[$count [$email:parse-address-list "alice@x.com, bob@y.com"]]
2
email:format-address
[$email:format-address] -> string — Format an [address] element back to its RFC 5322 mailbox string.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "From: John Doe \r\n\r\nbody"]]]
[$email:format-address [$email:from-addr $msg]]]
'John Doe '
email:format-address-list
[$email:format-address-list] -> string — Format a sequence of addresses and groups into an RFC 5322 address-list string.
[?lib 'cx-stdlib/email']
[$email:format-address-list [$email:parse-address-list "team: alice@x, bob@x;"]]
'team: alice@x, bob@x;'
email:build
[$email:build] -> element — Build a plaintext [message] from a parts map (from, to, subject, body, plus optional headers).
[?lib 'cx-stdlib/email']
[?let [= $msg [$email:build {"from": "agent@example.com",
"to": "customer@example.com",
"subject": "Re: Invoice question",
"body": "Thank you for reaching out..."}]]
[$email:subject [$email:parse [$email:emit $msg]]]]
'Re: Invoice question'
email:build-multipart
[$email:build-multipart] -> element — Build a multipart [message] from a parts map plus a sequence of alternative part elements.
[?lib 'cx-stdlib/email']
[?let [= $msg [$email:build-multipart {"from": "a@b.com", "to": "c@d.com", "subject": "x", "body": "plain"}
([part content-type="text/html" body="rich
"])]]
[$email:is-multipart $msg]]
true
email:reply
[$email:reply] -> element — Build a reply to a message, preserving threading and prefixing the subject with Re:.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $orig [$email:parse [$bytes:from-string-utf8 "From: sender@x.com\r\nMessage-ID: \r\nSubject: Question\r\n\r\nbody"]]]
[= $r [$email:reply $orig {"body": "Answer."}]]
[$email:in-reply-to $r]]
'abc123@example.com'
email:forward
[$email:forward] -> element — Build a forward that wraps the original as a message/rfc822 attachment, prefixing the subject with Fwd:.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $orig [$email:parse [$bytes:from-string-utf8 "From: sender@x.com\r\nSubject: Report\r\n\r\nbody"]]]
[= $f [$email:forward $orig {"to": "dest@x.com", "body": "FYI"}]]
[$email:subject $f]]
'Fwd: Report'
email:parse-dsn
[$email:parse-dsn] -> element — Lift an already-parsed delivery-status report into a structured [dsn] element.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "From: MAILER-DAEMON@mx\r\nContent-Type: multipart/report; report-type=delivery-status; boundary=\"B\"\r\n\r\n--B\r\nContent-Type: text/plain\r\n\r\nfailed\r\n--B\r\nContent-Type: message/delivery-status\r\n\r\nReporting-MTA: dns; mail.example.com\r\n\r\nFinal-Recipient: rfc822; user@example.com\r\nAction: failed\r\nStatus: 5.1.1\r\nDiagnostic-Code: smtp; 550 no such user\r\n--B--\r\n"]]]
[= $dsn [$email:parse-dsn $msg]]
[$email:is-bounce $dsn]]
true
email:parse-dsn-bytes
[$email:parse-dsn-bytes] -> element — Parse raw bytes and lift the delivery-status report into a structured [dsn] element.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $dsn [$email:parse-dsn-bytes [$bytes:from-string-utf8 "From: MAILER-DAEMON@mx\r\nContent-Type: multipart/report; report-type=delivery-status; boundary=\"B\"\r\n\r\n--B\r\nContent-Type: text/plain\r\n\r\nfailed\r\n--B\r\nContent-Type: message/delivery-status\r\n\r\nReporting-MTA: dns; mail.example.com\r\n\r\nFinal-Recipient: rfc822; user@example.com\r\nAction: failed\r\nStatus: 5.1.1\r\nDiagnostic-Code: smtp; 550 no such user\r\n--B--\r\n"]]]
[$email:is-bounce $dsn]]
true
email:is-bounce
[$email:is-bounce] -> bool — Report whether any recipient in the DSN failed or was delayed.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "From: MAILER-DAEMON@mx\r\nContent-Type: multipart/report; report-type=delivery-status; boundary=\"B\"\r\n\r\n--B\r\nContent-Type: text/plain\r\n\r\nfailed\r\n--B\r\nContent-Type: message/delivery-status\r\n\r\nReporting-MTA: dns; mail.example.com\r\n\r\nFinal-Recipient: rfc822; user@example.com\r\nAction: failed\r\nStatus: 5.1.1\r\nDiagnostic-Code: smtp; 550 no such user\r\n--B--\r\n"]]]
[= $dsn [$email:parse-dsn $msg]]
[$email:is-bounce $dsn]]
true
email:is-hard-bounce
[$email:is-hard-bounce] -> bool — Report whether any recipient failed permanently with a 5.x.x status.
[?lib 'cx-stdlib/email']
[?lib 'cx-stdlib/bytes']
[?let [= $msg [$email:parse [$bytes:from-string-utf8 "From: MAILER-DAEMON@mx\r\nContent-Type: multipart/report; report-type=delivery-status; boundary=\"B\"\r\n\r\n--B\r\nContent-Type: text/plain\r\n\r\nfailed\r\n--B\r\nContent-Type: message/delivery-status\r\n\r\nReporting-MTA: dns; mail.example.com\r\n\r\nFinal-Recipient: rfc822; user@example.com\r\nAction: failed\r\nStatus: 5.1.1\r\nDiagnostic-Code: smtp; 550 no such user\r\n--B--\r\n"]]]
[= $dsn [$email:parse-dsn $msg]]
[$email:is-hard-bounce $dsn]]
true