Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

mime - build and parse MIME messages

Import with import "mime.j" as mime;. Builds and parses MIME messages (RFC 5322 headers plus RFC 2045/2046 bodies) - the header-and-boundary structure behind email. Pure Jennifer over strings, convert, and encoding; it does no networking, so it runs on either binary and is the message-structure foundation the mail protocol clients (SMTP / POP3 / IMAP) build on.

use io;
import "mime.j" as mime;

def msg as mime.Part init mime.text("text/plain", "Hello, café.");
$msg = mime.withHeader($msg, "Subject", "Hi");
io.printf("%s", mime.encode($msg));

def back as mime.Part init mime.parse(mime.encode($msg));
io.printf("%s\n", mime.body($back));      # Hello, café.

Runnable: examples/modules/mime_demo.j.

The Part model

A message is a Part tree. A part is either a leaf (headers plus a decoded-text body with a transfer encoding) or a multipart container (headers plus child parts under a boundary):

export def struct Part {
    headers as list of Header, body as string, encoding as string,
    parts as list of Part, boundary as string
};
export def struct Header { name as string, value as string };

Bodies are held decoded, as text: encode applies the transfer encoding and parse removes it, so you always read plain content.

Surface

CallReturnsNotes
mime.text(contentType, body)PartLeaf text part; 7bit if ASCII, else quoted-printable. Adds charset=utf-8.
mime.attachment(filename, contentType, body)PartBase64 leaf with a Content-Disposition filename.
mime.multipart(subtype, boundary, parts)PartContainer (multipart/subtype) over one boundary.
mime.withHeader(part, name, value)PartCopy with a header set (case-insensitive replace, else append).
mime.encode(part)stringSerialize to a CRLF MIME message with transfer encodings applied.
mime.parse(text)PartParse a message: unfold headers, split multipart, transfer-decode.
mime.headerValue(part, name)stringHeader value (case-insensitive) or "".
mime.body(part)stringA leaf’s decoded text body.
mime.parts(part)list of PartA container’s child parts.
mime.contentType(part)stringMedia type without parameters (e.g. text/plain).
mime.address(name, email)stringRFC 5322 mailbox: email, or Name <email> (name quoted, or RFC 2047-encoded when non-ASCII).
mime.encodeWord(text)stringRFC 2047 UTF-8 base64 encoded-word(s), =?UTF-8?B?...?=, folded when long.
mime.decodeWord(value)stringDecode every encoded-word in a header value back to text (B and Q).

Encoding

encode produces a canonical message with CRLF line endings and picks the transfer encoding automatically:

  • 7bit - text bodies that are pure ASCII pass through unencoded.
  • quoted-printable - text bodies with non-ASCII are QP-encoded (e.g. café to caf=C3=A9).
  • base64 - attachment bodies are base64-encoded and folded at 76 columns.

A multipart container writes its child parts between --boundary delimiters and closes with --boundary--; nesting works (a part can itself be a multipart).

def parts as list of mime.Part init [];
$parts[] = mime.text("text/plain", "plain");
$parts[] = mime.text("text/html", "<b>rich</b>");
def msg as mime.Part init mime.multipart("alternative", "b0", $parts);
io.printf("%s", mime.encode($msg));

The boundary is yours to pass (not auto-generated), so output is deterministic and testable; pick a string that cannot appear in the content.

Parsing

parse is the inverse: it splits the header block from the body at the first blank line, unfolds continuation lines (a header value wrapped onto an indented next line), reads Content-Type / Content-Transfer-Encoding, and either splits a multipart body on its boundary (recursively) or transfer-decodes a leaf. encode and parse round-trip:

def back as mime.Part init mime.parse(wire);
for (def part in mime.parts($back)) {
    io.printf("%s: %s\n", mime.contentType($part), mime.body($part));
}

Non-ASCII headers (RFC 2047 encoded-words)

Header values must be ASCII on the wire, so a non-ASCII Subject or display name is carried as an RFC 2047 encoded-word (=?UTF-8?B?...?=). This is applied automatically and symmetrically:

  • encode encodes a non-ASCII Subject / Comments value, and the display-name half of an address header (From / To / Cc / Bcc / Reply-To / Sender), leaving the <addr> untouched. Long values fold into several encoded-words split on rune boundaries.
  • parse decodes those same headers back to plain text - so a fetched Subject: =?UTF-8?B?QmVyaWNodCBhdXMgTcO8bmNoZW4=?= reads back as Bericht aus München. A word that fails to decode is left verbatim, so a malformed header never crashes parse.
  • mime.address("Jörg Müller", "j@x.de") encodes the name for you.
def m as mime.Part init mime.withHeader(
    mime.text("text/plain", "hi"), "Subject", "Grüße aus München");
io.printf("%s", mime.encode($m));   # Subject: =?UTF-8?B?R3LDvMOfZSBhdXMgTcO8bmNoZW4=?=

The primitives mime.encodeWord / mime.decodeWord are exposed for the cases the auto-hooks don’t cover (a custom header, a multi-address line). Both B (base64) and Q (quoted-printable) encoded-words decode; encoding always emits B. us-ascii / iso-8859-* / windows-* charsets decode through encoding; UTF-8 is the default.

Out of scope

Deliberately a foundation, not a full mail stack:

  • Binary bodies. A Part body is text (UTF-8); an attachment takes text content. True binary attachments (a bytes body, e.g. an image) are not yet supported - that needs a bytes-typed body field.
  • Multi-address name encoding. A comma-separated address list is left raw on encode; encode each mailbox’s name with mime.address when building it.
  • Networking. This module only shapes messages; sending / fetching them is the mail SMTP / POP3 / IMAP clients (built on mime).

See also

  • encoding.md - toText / fromText (base64, quoted-printable), the transfer codecs mime delegates to.
  • strings.md - the text operations the header and boundary handling build on.
  • modules/index.md - the module catalog and import rules.