Email for MoonBit: the message model, MIME and DKIM, and SMTP, LMTP, IMAP and POP3.
let message = @moonmail.Message::parse(raw[:])
message.subject() // encoded words already decoded
message.from() // the mailboxes, display names decoded
message.text() // the body a reader would show, by its charset
message.attachments() // name, type, octets, inline and Content-ID
let reply = @moonmail.Message::new(
from=@moonmail.Mailbox::new("alice@example.com", name="Alice"),
to=[Mailbox(@moonmail.Mailbox::new("bob@example.com"))],
subject="Re: lunch",
text="Noon works.",
attachments=[@moonmail.Attachment::new("map.png", png)],
)
let wire = reply.encode() // CRLF, folded, encoded words; reads back the samemoon run src/examples/tourlet pop = @pop3.Client::new()
pop.user("alice")
pop.pass("secret")
pop.retr(1)
// ... write pop.next_write(), feed the server's octets
for event in pop.feed(octets) {
if event is Reply(Retr(_), Content(message)) {
println(message.length())
}
}let smtp = @smtp.Client::new()
smtp.ehlo("client.example")
// ... write smtp.next_write(), feed the server's octets until the EHLO reply
smtp.send("alice@example.com", ["bob@example.com"], raw[:])
smtp.quit()
for event in smtp.feed(octets) {
match event {
Reply(Rcpt(to), reply) if !reply.is_ok() => println("refused: " + to)
_ => ()
}
}let server = @smtp.Server::new("mx.example", auth=["PLAIN"])
server.feed(octets)
while server.next() is Some(request) {
match request {
Rcpt(to, _) if !known(to) => server.reply(550, "5.1.1 No such user")
Data(message) => { store(message); server.accept() }
Rset | Quit => ()
_ => server.accept()
}
}
// write server.next_write() back to the clientlet request = @jmap.Request::new()
let tag = request.mailbox_get(account_id) // Mailbox/get
let body = request.to_string() // POST this to session.api_url
let answer = @jmap.Response::parse(reply)
for box in @jmap.mailboxes(answer, tag) {
println("\{box.name}: \{box.unread} unread of \{box.total}")
}flowchart LR
page["writing desk<br/>(a page, no socket)"]
desk["moonmail<br/>composes the MIME"]
jmap["JMAP server<br/>HTTPS · no helper"]
office["local post office<br/>127.0.0.1:12000"]
smtp["SMTP · IMAP · POP3"]
page --> desk
desk -->|"jmap.request"| jmap
desk -->|"compose"| office
office -->|"@moonmail/net"| smtplet smtp = @net.Smtp::connect("smtp.example.com", StartTls(tls))
smtp.auth(@sasl.Plain::new("alice", password))
let refused = smtp.send("alice@example.com", ["bob@example.com"], raw[:])
smtp.quit()
let imap = @net.Imap::connect("imap.example.com", Tls(tls))
imap.login("alice", password)
let inbox = imap.wait(imap.client().select("INBOX"))
let pop = @net.Pop3::connect("pop.example.com", Tls(tls))
pop.login("alice", password)
let first = pop.retr(1)// SCRAM never sends the password, and refuses a server that cannot prove it
// holds the salted key. The nonce is fresh random printable ASCII each time.
smtp.auth(@sasl.ScramSha256::new("alice", "secret", nonce~))
imap.authenticate(@sasl.OAuthBearer::new(token, username="alice@example.com"))let imap = @imap.Client::new()
let _ = imap.login("alice", "secret")
let _ = imap.select("INBOX")
let fetch = imap.fetch("1:*", "(FLAGS BODY.PEEK[HEADER])", uid=true)
// ... write imap.next_write(), feed the server's octets
for event in imap.feed(octets) {
match event {
Data(Fetch(n, items)) => println(n.to_string() + " " + items.length().to_string())
Done(tag, No, _, text) if tag == fetch => println("fetch failed: " + text)
_ => ()
}
}let options = @dkim.SignOptions::new(
domain="example.com",
selector="sel",
headers=["From", "To", "Subject", "Date"],
)
let signature = @dkim.sign(raw[:], private_key, options)
// "DKIM-Signature: " + signature is the header to add; verify() checks it back.// IMAP: ask about a mailbox without selecting it, wait for changes, and
// store flags only where nothing changed them since (CONDSTORE).
let _ = imap.status("INBOX", ["MESSAGES", "UIDNEXT", "UIDVALIDITY"])
let _ = imap.idle() // changes arrive as Data until done()
imap.done()
let _ = imap.store("1:5", "+FLAGS (\\Seen)", uid=true, unchangedsince=modseq)
// SMTP: chunk the body instead of using DATA, and ask for delivery status.
smtp.mail("alice@example.com", size=1234, body=Binary)
smtp.rcpt("bob@example.com", notify=[Success, Failure])
smtp.bdat(chunk[:], last=true) // LAST ends the transactionlet store = @imap.Store::new()
let _ = store.deliver("INBOX", raw[:], date="06-Nov-2026 12:00:00 +0000")
let server = @imap.Server::new(
store~,
credentials=(user, password) => user == "tim" && password == "secret",
clock=() => "06-Nov-2026 12:00:00 +0000",
)
server.feed(octets)
while server.next_write() is Some(out) {
write(out)
}| Package | Specification | Worth knowing |
|---|---|---|
| moonmail | RFC 5322, 2045, 2046, 6532 | Writing what was read and reading it again gives the same tree, byte for byte. |
| moonmail/smtp | RFC 5321, 1870, 6152, 6531, 2920, 3030, 3207, 4954, 3461 | Pipelining, chunking and DSN follow what the server announces; a command that needs an extension it did not announce is refused when it is queued, not when it is sent. |
| moonmail/imap | RFC 9051, 2177, 7162, 3501 | Toward a server that announced neither IMAP4rev2 nor UTF8=ACCEPT, names are written and read in modified UTF-7. A search for text outside US-ASCII has to name its charset and quote the string — CHARSET UTF-8 SUBJECT "日本語" — because eight-bit octets cannot travel in an atom. The server answers IMAP4rev1 and is exercised by imaptest, the Dovecot project's own client. |
| moonmail/pop3 | RFC 1939, 2449 | The line limit covers status lines, which the protocol bounds; a line of a message has no length of its own and is bounded by the message limit instead. |
| moonmail/dkim | RFC 6376, 8463 | Both canonicalizations, RSA-SHA256 and Ed25519-SHA256, l=, t=, x= and h= checked item by item. |
| moonmail/sasl | RFC 4616, 7677, 7628, 4013 | SCRAM-SHA-256 checks the server's signature before the session is trusted; PLAIN, LOGIN, CRAM-MD5, XOAUTH2 and OAUTHBEARER complete it. |
moon clean && moon fmt && moon check --target all --deny-warn \
&& moon build --target all && moon test --target allfn Attachment::new(name : String, data : Bytes, media_type? : String, inline? : Bool, id? : String) -> AttachmentInstall
Download zipEmail for MoonBit: the message model, MIME and DKIM, and SMTP, LMTP, IMAP and POP3.