mime
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.
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
}| Function | Does |
|---|---|
parse(raw) -> Message | Split 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() -> ?i64 | The plain-text body |
m.html_part() -> ?i64 | The HTML body |
m.attachments() -> collections.Vec_i64 | The 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
| Function | Does |
|---|---|
p.field(name) -> ?[]const u8 | A 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) -> string | Header text, with encoded words like =?UTF-8?Q?caf=C3=A9?= decoded |
addresses(value) -> Vec_Address | The mailboxes of From, To, Cc: .name and .addr |
unfold(value) -> string | A folded value on one line |
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
| Method | Returns |
|---|---|
p.media_type() | "text/plain", "image/png" … (lowercased) |
p.param(field, name) | A parameter: p.param("Content-Type", "charset") |
p.filename() -> ?string | The attachment’s file name, decoded |
p.is_attachment() | Whether readers list it as an attachment |
p.disposition() | "attachment", "inline" or "" |
p.content_id() -> ?string | The Content-ID, without < > |
p.decoded() -> string | The body, base64 or quoted-printable undone |
p.text() -> string | The body as UTF-8 text, \n line ends |
p.charset() -> ?charset.Encoding | The part’s charset |
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
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| Function | Does |
|---|---|
draft() -> Draft | An 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() -> string | The 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
Fromline at the start.
Last modified: