mime

tape
import mime from "mime";

let m: mime.Message = mime.parse(raw);
let root: mime.Part = m.part(0);
io.println(mime.decode_words(root.field("Subject") or { "" }));
let body: i64 = m.text_part() or { return 0; };
io.println(m.part(body).text());

mime reads and writes email: the bytes an IMAP server hands you, and the bytes you give an SMTP server. It splits a message into its parts, decodes headers, addresses, attachments and text in any charset, and composes a plain-text message with attachments.

Reading is lenient. parse never fails, because real mail is often broken: it reads what is there, by fixed rules.

Parts

parse(raw) gives a Message: its parts in order, each part before the parts inside it. Part 0 is the message itself.

tape
pub struct Part {
    header: []const u8;     // the raw header block
    body: []const u8;       // the raw body, still encoded
    parent: i64;            // the enclosing part, -1 for the message
    depth: i64;
    in_digest: bool;
    invalid: bool;          // a multipart whose parts can't be found
}
FunctionDoes
parse(raw) -> MessageSplit a message into parts
m.len(), m.part(i)The number of parts; part i
m.children(i)The parts directly inside part i, for for (c in m.children(i))
m.text_part() -> ?i64The plain-text body
m.html_part() -> ?i64The HTML body
m.attachments() -> collections.Vec_i64The attachments and attached messages

The parts are views into raw, nothing is copied, so keep raw alive while you use the Message.

text_part is the first text/plain part that isn’t an attachment and isn’t inside an attached (forwarded) message; html_part the same for text/html. A message with both offers you the choice.

Headers

FunctionDoes
p.field(name) -> ?[]const u8A header field’s raw value (any case of the name)
fields(p.header)Every field, for for (f in mime.fields(h)): f.name, f.value
decode_words(value) -> stringHeader text, with encoded words like =?UTF-8?Q?caf=C3=A9?= decoded
addresses(value) -> Vec_AddressThe mailboxes of From, To, Cc: .name and .addr
unfold(value) -> stringA folded value on one line
tape
let senders: mime.Vec_Address = mime.addresses(root.field("From") or { "" });
for (a in senders.iter()) {
    io.println("{a.name} <{a.addr}>");
}

addresses reads Name <addr>, "Last, First" <addr>, addr (Name) and groups (team: a@x, b@y;). Names come out decoded.

Header text that isn’t valid UTF-8 and isn’t encoded is read as windows-1252; pass another encoding as decode_words(value, fallback).

Part contents

MethodReturns
p.media_type()"text/plain", "image/png" … (lowercased)
p.param(field, name)A parameter: p.param("Content-Type", "charset")
p.filename() -> ?stringThe attachment’s file name, decoded
p.is_attachment()Whether readers list it as an attachment
p.disposition()"attachment", "inline" or ""
p.content_id() -> ?stringThe Content-ID, without < >
p.decoded() -> stringThe body, base64 or quoted-printable undone
p.text() -> stringThe body as UTF-8 text, \n line ends
p.charset() -> ?charset.EncodingThe part’s charset
tape
let atts: collections.Vec_i64 = m.attachments();
for (i in atts.iter()) {
    let p: mime.Part = m.part(i);
    let name: string = p.filename() or { "attachment" };
    fs.write_file(name, p.decoded()) or { io.println("can't save {name}"); };
}

text() decodes the part’s charset through charset; a part without one is UTF-8 if it is valid UTF-8, else windows-1252 (p.text(fallback) for another). Flowed text (format=flowed, which Thunderbird and Apple Mail send) comes out as whole paragraphs, one per line, so you can wrap it to your window.

Writing a message

tape
var d: mime.Draft = mime.draft();
d.header("From", mime.format_address("Ann Mäkelä", "ann@example.fi"));
d.header("To", mime.format_address("Bob", "bob@example.com"));
d.header("Subject", "Hyvää päivää");
d.header("Date", time.local_now().to_rfc5322());
d.header("Message-ID", mime.message_id(unique_bytes, "example.fi"));
d.text("Hello Bob,\n\nthe report is attached.\n\nAnn");
d.attach("report.pdf", "application/pdf", pdf_bytes);
let raw: string = d.finish();     // CRLF line ends, ready for SMTP
FunctionDoes
draft() -> DraftAn empty message
d.header(name, value)A header field; non-ASCII text is encoded, long lines folded
d.text(body)The plain-text body
d.attach(filename, media_type, data)Attach a file (data must live until finish)
d.finish() -> stringThe whole message
format_address(name, addr)Name <addr>, quoted or encoded as needed
message_id(unique, domain)<hex@domain>; pass random bytes (os.random_bytes)
encode_words(text)Text as encoded words, for a header you write yourself

The text is sent as UTF-8 and format=flowed. Long lines are wrapped so other readers can reflow them, and lines starting with > stay quotes. It goes out as plain 7-bit text when it is ASCII and as quoted-printable otherwise, so any SMTP server takes it. Attachments are base64. A forwarded message ("message/rfc822") is attached as it is.

Composing writes plain text only; HTML messages aren’t composed.

Lenient reading

These broken messages still read:

  • No empty line after the header: the body starts at the first line that isn’t a header field.
  • A multipart without a boundary, or whose boundary never appears: its body is read as text/plain.
  • A multipart without its closing --boundary--: read to the end.
  • 8-bit header text, encoded words split mid-character, encoded words in file names and quoted names.
  • LF line ends instead of CRLF, and an mbox From line at the start.

Last modified: