Build RFC 5322 / MIME messages as Bytes, safe for LLM-supplied input.

  • No header injection. CR, LF and other control characters in the subject and extra header values are folded to a single space; addresses containing them, malformed addresses and malformed header names are rejected with an Err. Structural headers (Content-Type, MIME-Version, ...) cannot be overridden through headers.
  • Non-ASCII subjects, header values and display names are RFC 2047 encoded (=?UTF-8?B?...?=, folded at 75 characters); non-ASCII attachment names use RFC 2231 filename*.
  • Bodies. Plain text, HTML, or both (multipart/alternative), plus attachments (multipart/mixed). ASCII text with short lines is sent 7bit; anything else is base64 with 76-character lines. Attachments are base64 (a forwarded message/rfc822 is 8bit). Everything is concatenated as Bytes, so binary attachments arrive intact.

Message fields (all optional except a body for sending): from, to, cc, bcc, reply-to (a Str such as "Ada <ada@example.com>, bob@example.com", a Vec of such Strs, or {name, email} Maps), subject, text, html, attachments (a Vec of {filename, content, mime-type?, content-id?, inline?}, content being Bytes or a Str), in-reply-to, references (a Message-ID Str or Vec), and headers (extra headers as a Map). Gmail adds Date and Message-ID and strips Bcc from delivered copies.

::mime ::google::gmail::mime

raw ::mime/build-message({
  to: ["Zoë Example <zoe@example.com>"],
  subject: "Résumé attached",
  text: "Hi Zoë, see attached.",
  html: "<p>Hi Zoë, see attached.</p>",
  attachments: [{filename: "résumé.pdf", content: Bytes([37, 80, 68, 70]), mime-type: "application/pdf"}]
})

Values

ATOM_PHRASE_PATTERN

ATOM_PHRASE_PATTERN: Str "^[A-Za-z0-9!#$%&'*+/=?^_`{|}~ -]+$"

CONTROL_PATTERN

CONTROL_PATTERN: Str "[\\x00-\\x08\\x0A-\\x1F\\x7F]+"

EMAIL_PATTERN

EMAIL_PATTERN: Str "^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$"

HEADER_NAME_PATTERN

HEADER_NAME_PATTERN: Str "^[!-9;-~]+$"

LINE_BREAK_PATTERN

LINE_BREAK_PATTERN: Str "[\\r\\n]"

MAX_PLAIN_HEADER

MAX_PLAIN_HEADER: Int 900

MIME_TYPE_PATTERN

MIME_TYPE_PATTERN: Str "^[A-Za-z0-9!#$&^_.+-]+/[A-Za-z0-9!#$&^_.+-]+$"

RESERVED_HEADERS

RESERVED_HEADERS: Vec [
  "from",
  "to",
  "cc",
  "bcc",
  "reply-to",
  "subject",
  "in-reply-to",
  "references",
  "mime-version",
  "content-type",
  "content-transfer-encoding",
  "content-disposition"
]

WORD_BYTES

WORD_BYTES: Int 39

Functions

address-header

fn (name: Str, value: Any): Any

address-items

fn (value: Any): Vec

attachment-error

fn (attachment: Any): Str?

Why an attachment Map is unusable (missing filename or content, a bad MIME type), or null (pure).

attachment-mime-type

fn (attachment: Map): Str

The MIME type for an attachment: its mime-type, else guessed from the filename, else application/octet-stream.

attachment-part

fn (attachment: Map): Bytes

An attachment part as Bytes: Content-Type with name, Content-Disposition (attachment, or inline with a content-id), and the content base64 encoded in 76-character lines. A message/rfc822 attachment (a forwarded email) is embedded as is, 8bit.

build-body

fn (message: Map, boundary: Str): Bytes

The body entity (its Content-Type headers and content) as Bytes: text, html, or both as multipart/alternative; wrapped in multipart/mixed with the attachments when there are any. Nested boundaries derive from boundary (a_ / m_ prefixes).

build-headers

fn (message: Map): Str

The top-level header block of a message (pure), each line ending in CRLF, ending with MIME-Version: 1.0: From, To, Cc, Bcc, Reply-To, Subject, In-Reply-To, References, then message.headers. Returns an Err for an invalid address or header name.

build-message

fn (message: Map): Bytes
fn (message: Map, boundary: Str): Bytes

Build a complete RFC 5322 message as Bytes from a message Map (see the namespace doc for the fields). Returns an Err for invalid addresses, header names or attachments. Pass boundary for reproducible output in tests.

raw ::google::gmail::mime/build-message({to: "ada@example.com", subject: "Hi", text: "Hello!"})
fn (metadata: Map, message: Bytes, boundary: Str): Bytes

A Gmail media-upload body as Bytes (multipart/related): a JSON metadata part (nulls dropped) and the raw message as message/rfc822, concatenated as bytes. Send it with Content-Type: multipart/related; boundary=<boundary>.

byte-length

fn (text: Str): Int

clean-header-value

fn (value: Any): Str

Fold every run of CR, LF and other control characters (tab excepted) to one space and trim, so a value can never start a new header line.

emails

fn (value: Any): Vec

Just the email addresses of an address field, lowercased; invalid entries are skipped (pure). Handy for comparing senders.

encode-header-text

fn (value: Any): Str

A header value safe to emit: control characters folded (clean-header-value), then RFC 2047 encoded when it is not ASCII or is very long.

encode-phrase

fn (name: Str): Str

A display name for an address header: as is when it is plain ASCII words, a quoted string when it has specials ("Doe, Jane"), or RFC 2047 encoded when it is not ASCII.

encode-words

fn (text: Str): Str

RFC 2047 encode text as UTF-8 B encoded words, split on character boundaries by byte count so each word stays within 75 characters and a character is never cut in two, joined with a folding CRLF SP: encode-words("Zoë") is "=?UTF-8?B?Wm/Dqw==?=".

filename-params

fn (filename: Str): Map

The name (Content-Type) and filename (Content-Disposition) parameters for a file name: quoted ASCII, or RFC 2047 / RFC 2231 encoded when not ASCII.

format-address

fn (address: Map): Str

Render {name, email} for a header: Ada <ada@example.com>, with the name quoted or RFC 2047 encoded as needed.

format-addresses

fn (addresses: Vec): Str

Render a Vec of {name, email} as one header value, comma separated (folded when long).

has-line-break

fn (s: Str): Bool

True when s contains a CR or LF.

header-error

fn (name: Any): Str?

is-ascii

fn (s: Str): Bool

True when s has only 7-bit ASCII characters.

is-email

fn (email: Any): Bool

True when email is a plain local@domain.tld address (ASCII; no display name, spaces or line breaks).

message-ids

fn (value: Any): Vec

multipart

fn (subtype: Str, parts: Vec, boundary: Str): Bytes

A multipart/<subtype> entity as Bytes: its Content-Type header, then each part (Bytes, already carrying its own headers) between boundary delimiters.

new-boundary

fn (): Str

A random MIME boundary. It contains =_, which never occurs in base64 content.

normalize-newlines

fn (text: Str): Str

Convert LF, CR and CRLF line endings to CRLF (what SMTP requires).

parse-addresses

fn (value: Any): Vec

Parse an address field (Str list, Vec, or {name, email} Map) into a Vec of {name, email}, or an Err naming the first invalid address.

read-address

fn (value: Any): Map

Parse one address ("ada@example.com", "Ada Lovelace <ada@example.com>", or {name, email}) into {name, email, error} (pure); error is null when the address is valid.

split-address-list

fn (value: Str): Vec

Split an address-list Str on commas outside double quotes: "\"Doe, Jane\" <j@x.io>, bob@x.io" gives two entries.

text-part

fn (text: Str, subtype: Str): Bytes

A text/<subtype>; charset="UTF-8" part (headers, blank line, body) as Bytes: 7bit when the text is ASCII with lines under 999 characters, else base64. Line endings become CRLF.

to-bytes

fn (content: Any): Bytes

Content as Bytes: Bytes pass through, a Str becomes its UTF-8 bytes, null is empty, anything else is JSON-encoded.

to-raw

fn (message: Any): Str

Base64url-encode a message (Bytes or Str) for the Gmail raw field.

unquote

fn (s: Str): Str

utf8-chunks

fn (text: Str, max-bytes: Int): Vec

wrap-base64

fn (encoded: Str): Str

Break a base64 Str into CRLF-separated lines of at most 76 characters (RFC 2045).