MoonMail

the catalogue · api reference

Every part,
on its own card.

Eight packages, one mailroom. Each card below states what a package is for; the slips hanging under it are what the package exposes, in the order you are likely to reach for them.

8packages 3protocols 6sasl mechanisms 0.1.2interface
Lush green rainforest leaves in sunlight 01
moonmail

The message itself

Addresses, ordered headers, a tree of parts, and a builder that picks the right nesting for you.

RFC 5322 RFC 2045 RFC 6532 12 entries
fn fn encode_addresses(ArrayView[Address], utf8? : Bool) -> String
fn fn format_date(@moondate.Moment) -> String
fn fn parse_address(StringView) -> Address raise Refused
fn fn parse_addresses(StringView) -> Array[Address] raise Refused
fn fn parse_date(StringView) -> @moondate.Moment raise Refused
enum enum Address { Mailbox(Mailbox); Group(name~ : String, members~ : Array[Mailbox]) } fn Address::encode(Self, utf8? : Bool) -> String fn Address::mailboxes(Self) -> Array[Mailbox]
struct struct Attachment
  • name : String
  • media_type : String
  • data : Bytes
  • inline : Bool
  • id : String?
fn Attachment::new(String, Bytes, media_type? : String, inline? : Bool, id? : String) -> Self
enum enum Body { Leaf(Bytes); Multipart(Array[Message]); Embedded(Message) }
struct struct Headers // private fields fn Headers::add(Self, String, String) -> Unit raise Refused fn Headers::all(Self, StringView) -> Array[String] fn Headers::get(Self, StringView) -> String? fn Headers::iter(Self) -> Iter[(String, String)] fn Headers::length(Self) -> Int32 fn Headers::new() -> Self fn Headers::remove(Self, StringView) -> Unit fn Headers::set(Self, String, String) -> Unit raise Refused
struct struct Mailbox { name : String?; address : String } fn Mailbox::encode(Self, utf8? : Bool) -> String fn Mailbox::new(String, name? : String) -> Self
struct struct Message { headers : Headers; body : Body } fn Message::addresses(Self, StringView) -> Array[Address] raise Refused fn Message::attachments(Self) -> Array[Attachment] fn Message::bcc(Self) -> Array[Address] raise Refused fn Message::cc(Self) -> Array[Address] raise Refused fn Message::content_type(Self) -> @moonmime.MediaType fn Message::date(Self) -> @moondate.Moment? raise Refused fn Message::encode(Self, utf8? : Bool) -> Bytes fn Message::from(Self) -> Array[Mailbox] raise Refused fn Message::html(Self, charsets? : Map[String, &@moonmime.Charset]) -> String? raise Refused fn Message::message_id(Self) -> String? fn Message::new(from~ : Mailbox, to~ : Array[Address], subject~ : String, text? : String, html? : String, attachments? : Array[Attachment], cc? : Array[Address], date? : @moondate.Moment, message_id? : String) -> Self raise Refused fn Message::parse(BytesView, strict? : Bool) -> Self raise Refused fn Message::reply_to(Self) -> Array[Address] raise Refused fn Message::subject(Self) -> String? fn Message::text(Self, charsets? : Map[String, &@moonmime.Charset]) -> String? raise Refused fn Message::to(Self) -> Array[Address] raise Refused
suberror suberror Refused
  • Header(at~ : Int32)
  • Address(at~ : Int32)
  • Boundary(at~ : Int32)
  • Encoding(at~ : Int32)
  • Charset(name~ : String)
  • Date
An orange autumn forest with a soft path 02
moonmail/dkim

Sign it, then check the claim

One signature over the headers you name and the body you hash, and a verdict for every signature a letter carries.

RFC 6376 RFC 8463 7 entries
fn fn sign(BytesView, Key, domain~ : String, selector~ : String, headers? : Array[String], header_canon? : Canon, body_canon? : Canon, timestamp? : Int64, expire? : Int64, length? : Bool, identity? : String) -> String raise Refused
value let signed : Array[String]
fn fn verify(BytesView, (String) -> String? raise, now~ : Int64) -> Array[Verdict]
enum enum Canon { Simple; Relaxed }
enum enum Key { Rsa(@rsa.PrivateKey); Ed25519(@ed25519.PrivateKey) }
enum enum Verdict
  • Pass(domain~ : String, selector~ : String)
  • Fail(domain~ : String, reason~ : String)
  • PermError(String)
  • TempError(String)
suberror suberror Refused { Missing(String); Malformed(String) }
Golden desert dunes under a pale sky 03
moonmail/sasl

Mechanisms, as a trait

Six mechanisms behind one trait, so a mechanism written once works on all three protocols.

RFC 4616 RFC 7677 RFC 7628 10 entries
fn fn prep(StringView, unassigned? : Bool) -> String raise Refused
fn fn respond(&Mechanism, StringView) -> String raise Refused
struct struct CramMd5 // private fields impl Mechanism for CramMd5 fn CramMd5::finish(Self) -> Unit raise Refused fn CramMd5::name(Self) -> String fn CramMd5::new(String, String) -> Self fn CramMd5::start(Self) -> Bytes? fn CramMd5::step(Self, BytesView) -> Bytes raise Refused
struct struct Login // private fields impl Mechanism for Login fn Login::finish(Self) -> Unit raise Refused fn Login::name(Self) -> String fn Login::new(String, String) -> Self fn Login::start(Self) -> Bytes? fn Login::step(Self, BytesView) -> Bytes raise Refused
struct struct OAuthBearer // private fields impl Mechanism for OAuthBearer fn OAuthBearer::error(Self) -> String? fn OAuthBearer::finish(Self) -> Unit raise Refused fn OAuthBearer::name(Self) -> String fn OAuthBearer::new(String, username? : String, host? : String, port? : Int32) -> Self fn OAuthBearer::start(Self) -> Bytes? fn OAuthBearer::step(Self, BytesView) -> Bytes raise Refused
struct struct Plain // private fields impl Mechanism for Plain fn Plain::finish(Self) -> Unit raise Refused fn Plain::name(Self) -> String fn Plain::new(String, String, authzid? : String) -> Self fn Plain::start(Self) -> Bytes? fn Plain::step(Self, BytesView) -> Bytes raise Refused
struct struct ScramSha256 // private fields impl Mechanism for ScramSha256 fn ScramSha256::finish(Self) -> Unit raise Refused fn ScramSha256::name(Self) -> String fn ScramSha256::new(String, String, nonce~ : String) -> Self raise Refused fn ScramSha256::start(Self) -> Bytes? fn ScramSha256::step(Self, BytesView) -> Bytes raise Refused
struct struct XOAuth2 // private fields impl Mechanism for XOAuth2 fn XOAuth2::error(Self) -> String? fn XOAuth2::finish(Self) -> Unit raise Refused fn XOAuth2::name(Self) -> String fn XOAuth2::new(String, String) -> Self fn XOAuth2::start(Self) -> Bytes? fn XOAuth2::step(Self, BytesView) -> Bytes raise Refused
trait trait Mechanism
  • fn name(Self) -> String
  • fn start(Self) -> Bytes?
  • fn step(Self, BytesView) -> Bytes raise Refused
  • fn finish(Self) -> Unit raise Refused = _
suberror suberror Refused
  • Prep(Char)
  • Malformed(String)
  • Server(String)
  • Signature
A turquoise river winding through forested hills 04
moonmail/smtp

Both ends of the relay

A client that paces commands by the extensions a server announced, and a server that answers the protocol itself.

RFC 5321 RFC 1870 RFC 3207 RFC 4954 12 entries
value let limits : Limits
enum enum Body { SevenBit; EightBit; Binary }
struct struct Client // private fields fn Client::auth(Self, &@sasl.Mechanism) -> Unit raise Refused fn Client::bdat(Self, BytesView, last? : Bool) -> Unit raise Refused fn Client::data(Self, BytesView) -> Unit raise Refused fn Client::ehlo(Self, String) -> Unit raise Refused fn Client::expn(Self, String) -> Unit raise Refused fn Client::extensions(Self) -> Map[String, String] fn Client::feed(Self, BytesView) -> Array[Event] raise Refused fn Client::has(Self, String) -> Bool fn Client::helo(Self, String) -> Unit raise Refused fn Client::help(Self, topic? : String) -> Unit raise Refused fn Client::mail(Self, String, size? : Int32, body? : Body, utf8? : Bool, ret? : Ret, envid? : String, auth? : String) -> Unit raise Refused fn Client::new(lmtp? : Bool, limits? : Limits) -> Self fn Client::next_write(Self) -> Bytes? fn Client::noop(Self) -> Unit fn Client::pending(Self) -> Int32 fn Client::quit(Self) -> Unit fn Client::rcpt(Self, String, notify? : Array[Notify], orcpt? : String) -> Unit raise Refused fn Client::rset(Self) -> Unit fn Client::send(Self, String, Array[String], BytesView) -> Unit raise Refused fn Client::starttls(Self) -> Unit raise Refused fn Client::vrfy(Self, String) -> Unit raise Refused
enum enum Command
  • Greeting
  • Hello(String)
  • StartTls
  • Auth(String)
  • Mail(String)
  • Rcpt(String)
  • Data
  • Bdat(Int32)
  • Delivery(String)
  • Rset
  • Noop
  • Vrfy(String)
  • Expn(String)
  • Help
  • Quit
enum enum Event { Reply(Command, Reply); Sasl(@sasl.Refused) }
struct struct Limits { line : Int32; recipients : Int32; size : Int32 } fn Limits::new(line? : Int32, recipients? : Int32, size? : Int32) -> Self
enum enum Notify { Success; Failure; Delay }
struct struct Reply { code : Int32; lines : Array[String] } fn Reply::encode(Self) -> Bytes fn Reply::is_ok(Self) -> Bool fn Reply::new(Int32, StringView) -> Self fn Reply::status(Self) -> String? fn Reply::text(Self) -> String
enum enum Request
  • Hello(String, extended~ : Bool)
  • StartTls
  • Auth(String, Bytes?)
  • Response(Bytes)
  • Mail(String, Map[String, String?])
  • Rcpt(String, Map[String, String?])
  • Data(Bytes)
  • Vrfy(String)
  • Expn(String)
  • Rset
  • Quit
enum enum Ret { Full; Headers }
struct struct Server // private fields fn Server::accept(Self) -> Unit raise Refused fn Server::challenge(Self, BytesView) -> Unit raise Refused fn Server::closed(Self) -> Bool fn Server::feed(Self, BytesView) -> Unit fn Server::new(String, lmtp? : Bool, starttls? : Bool, auth? : Array[String], greeting? : String, limits? : Limits) -> Self fn Server::next(Self) -> Request? fn Server::next_write(Self) -> Bytes? fn Server::reply(Self, Int32, String) -> Unit raise Refused
suberror suberror Refused
  • Malformed(String)
  • Exceeded(limit~ : Int32, got~ : Int32)
  • Order(String)
  • Unsupported(String)
A clear turquoise lake ringed by pines 05
moonmail/imap

Stay in the mailbox

IMAP4rev2 with IDLE and CONDSTORE. Every command gets a tag and every response comes back decoded.

RFC 9051 RFC 2177 RFC 7162 10 entries
value let limits : Limits
struct struct Client // private fields fn Client::append(Self, String, BytesView, flags? : Array[String], date? : String) -> Int32 raise Refused fn Client::authenticate(Self, &@sasl.Mechanism) -> Int32 raise Refused fn Client::capabilities(Self) -> Array[String] fn Client::capability(Self) -> Int32 fn Client::close(Self) -> Int32 raise Refused fn Client::copy(Self, String, String, uid? : Bool) -> Int32 raise Refused fn Client::create(Self, String) -> Int32 raise Refused fn Client::delete(Self, String) -> Int32 raise Refused fn Client::done(Self) -> Unit raise Refused fn Client::enable(Self, Array[String]) -> Int32 raise Refused fn Client::examine(Self, String, condstore? : Bool) -> Int32 raise Refused fn Client::expunge(Self) -> Int32 raise Refused fn Client::feed(Self, BytesView) -> Array[Event] raise Refused fn Client::fetch(Self, String, String, uid? : Bool, changedsince? : Int64) -> Int32 raise Refused fn Client::has(Self, String) -> Bool fn Client::id(Self, Array[(String, String)]) -> Int32 raise Refused fn Client::idle(Self) -> Int32 raise Refused fn Client::list(Self, String, String) -> Int32 raise Refused fn Client::login(Self, String, String) -> Int32 raise Refused fn Client::logout(Self) -> Int32 fn Client::move_to(Self, String, String, uid? : Bool) -> Int32 raise Refused fn Client::namespaces(Self) -> Int32 raise Refused fn Client::new(limits? : Limits) -> Self fn Client::next_write(Self) -> Bytes? fn Client::noop(Self) -> Int32 fn Client::pending(Self) -> Int32 fn Client::rename(Self, String, String) -> Int32 raise Refused fn Client::search(Self, String, uid? : Bool) -> Int32 raise Refused fn Client::select(Self, String, condstore? : Bool) -> Int32 raise Refused fn Client::starttls(Self) -> Int32 raise Refused fn Client::state(Self) -> State fn Client::status(Self, String, Array[String]) -> Int32 raise Refused fn Client::store(Self, String, String, uid? : Bool, unchangedsince? : Int64) -> Int32 raise Refused fn Client::subscribe(Self, String) -> Int32 raise Refused fn Client::uid_expunge(Self, String) -> Int32 raise Refused fn Client::unselect(Self) -> Int32 raise Refused fn Client::unsubscribe(Self, String) -> Int32 raise Refused
enum enum Code
  • Alert
  • Capability(Array[String])
  • PermanentFlags(Array[String])
  • ReadOnly
  • ReadWrite
  • TryCreate
  • UidNext(Int64)
  • UidValidity(Int64)
  • HighestModseq(Int64)
  • NoModseq
  • Modified(String)
  • AppendUid(Int64, String)
  • CopyUid(Int64, String, String)
  • Closed
  • Other(String, String?)
enum enum Event
  • Data(Response)
  • Done(Int32, Status, Code?, String)
  • Sasl(@sasl.Refused)
struct struct Limits { line : Int32; literal : Int32 } fn Limits::new(line? : Int32, literal? : Int32) -> Self
enum enum Response
  • Condition(String, Code?, String)
  • Capability(Array[String])
  • Enabled(Array[String])
  • Flags(Array[String])
  • Exists(Int64)
  • Expunge(Int64)
  • List(flags~ : Array[String], delimiter~ : String?, name~ : String)
  • Status(String, Map[String, Int64])
  • Esearch(tag~ : String?, uid~ : Bool, items~ : Map[String, Value])
  • Search(Array[Int64], modseq~ : Int64?)
  • Fetch(Int64, Map[String, Value])
  • Namespace(Value, Value, Value)
  • Other(String, Array[Value])
enum enum State
  • NotAuthenticated
  • Authenticated
  • Selected(String)
  • Logout
enum enum Status { Ok; No; Bad }
enum enum Value
  • Nil
  • Number(Int64)
  • Atom(String)
  • Text(Bytes)
  • List(Array[Value])
fn Value::list(Self) -> Array[Self]? fn Value::number(Self) -> Int64? fn Value::text(Self) -> String?
suberror suberror Refused
  • Malformed(String)
  • Exceeded(limit~ : Int32, got~ : Int32)
  • Order(String)
  • Unsupported(String)
A bright green bamboo grove 06
moonmail/pop3

Pick up and go

The small one: a greeting, a login, a list, and the messages themselves.

RFC 1939 RFC 2449 8 entries
value let limits : Limits
struct struct Client // private fields fn Client::apop(Self, String, String) -> Unit raise Refused fn Client::auth(Self, &@sasl.Mechanism) -> Unit raise Refused fn Client::capa(Self) -> Unit fn Client::capabilities(Self) -> Array[String] fn Client::dele(Self, Int32) -> Unit raise Refused fn Client::feed(Self, BytesView) -> Array[Event] raise Refused fn Client::has(Self, String) -> Bool fn Client::list(Self, message? : Int32) -> Unit raise Refused fn Client::new(limits? : Limits) -> Self fn Client::next_write(Self) -> Bytes? fn Client::noop(Self) -> Unit raise Refused fn Client::pass(Self, String) -> Unit raise Refused fn Client::pending(Self) -> Int32 fn Client::quit(Self) -> Unit fn Client::retr(Self, Int32) -> Unit raise Refused fn Client::rset(Self) -> Unit raise Refused fn Client::stat(Self) -> Unit raise Refused fn Client::state(Self) -> State fn Client::stls(Self) -> Unit raise Refused fn Client::top(Self, Int32, Int32) -> Unit raise Refused fn Client::uidl(Self, message? : Int32) -> Unit raise Refused fn Client::user(Self, String) -> Unit raise Refused
enum enum Command
  • Greeting
  • User(String)
  • Pass
  • Apop(String)
  • Auth(String)
  • Stls
  • Capa
  • Stat
  • List(Int32?)
  • Uidl(Int32?)
  • Retr(Int32)
  • Top(Int32, Int32)
  • Dele(Int32)
  • Rset
  • Noop
  • Quit
enum enum Event { Reply(Command, Reply); Sasl(@sasl.Refused) }
struct struct Limits { line : Int32; message : Int32 } fn Limits::new(line? : Int32, message? : Int32) -> Self
enum enum Reply
  • Greeting(text~ : String, timestamp~ : String?)
  • Ok(String)
  • Err(String)
  • Stat(count~ : Int32, size~ : Int64)
  • Listing(Array[(Int32, Int64)])
  • Uids(Array[(Int32, String)])
  • Content(Bytes)
  • Capabilities(Array[String])
enum enum State { Authorization; Transaction; Update }
suberror suberror Refused
  • Malformed(String)
  • Exceeded(limit~ : Int32, got~ : Int32)
  • Order(String)
  • Unsupported(String)
A green meadow under soft light 07
moonmail/jmap

The protocol that fits in a page

JMAP as documents: the session, the request envelope, the answers. No socket, which is how a browser can speak it.

RFC 8620 RFC 8621 no I/O 17 entries
fn fn created_id(Response, String, String) -> String raise Refused
fn fn discovery_url(String) -> String
fn fn email_ids(Response, String) -> Array[String] raise Refused
fn fn emails(Response, String) -> Array[Email] raise Refused
fn fn identities(Response, String) -> Array[Identity] raise Refused
fn fn mailboxes(Response, String) -> Array[Mailbox] raise Refused
fn fn resolve(String, String) -> String raise Refused
struct struct Call { name : String; args : Json; tag : String }
struct struct Draft
  • from : String?
  • to : Array[String]
  • cc : Array[String]
  • bcc : Array[String]
  • subject : String
  • text : String
fn Draft::new(to~ : Array[String], subject~ : String, text~ : String) -> Self
struct struct Email
  • id : String
  • subject : String
  • from : String?
  • received_at : String?
  • preview : String
  • unread : Bool
struct struct Identity { id : String; name : String?; email : String }
struct struct Invocation { name : String; args : Json; tag : String }
struct struct Mailbox
  • id : String
  • name : String
  • role : String?
  • parent_id : String?
  • total : Int32
  • unread : Int32
struct struct Request { capabilities : Array[String]; calls : Array[Call] } fn Request::call(Self, String, Json) -> String fn Request::email_destroy(Self, String, String) -> String fn Request::email_get(Self, String, Array[String], Array[String]) -> String fn Request::email_mark_seen(Self, String, String, Bool) -> String fn Request::email_move(Self, String, String, String) -> String fn Request::email_query(Self, String, String, Int32) -> String fn Request::email_set_draft(Self, String, String, Draft) -> String fn Request::email_set_update(Self, String, String, Array[(String, Json)]) -> String fn Request::email_submission_set(Self, String, String, String) -> String fn Request::identity_get(Self, String) -> String fn Request::mailbox_get(Self, String) -> String fn Request::new() -> Self fn Request::to_json(Self) -> Json fn Request::to_string(Self) -> String fn Request::using_capability(Self, String) -> Unit
struct struct Response { invocations : Array[Invocation]; session_state : String } fn Response::answer(Self, String) -> Invocation raise Refused fn Response::invocation(Self, String) -> Invocation? fn Response::parse(String) -> Self raise Refused
struct struct Session
  • username : String?
  • account_id : String
  • api_url : String
  • download_url : String?
  • upload_url : String?
  • event_source_url : String?
  • state : String
fn Session::parse(String, String) -> Self raise Refused
suberror suberror Refused
  • Malformed(at~ : String)
  • NotAnObject
  • Missing(name~ : String)
  • WrongType(name~ : String)
  • BadUrl(url~ : String)
  • Failed(call~ : String, kind~ : String, description~ : String)
Clear blue sea and sky 08
moonmail/net

The one that touches a socket

The same state machines over TCP and TLS, including the upgrade to STARTTLS mid-conversation.

TCP TLS native only 6 entries
struct struct Imap // private fields async fn Imap::authenticate(Self, &@sasl.Mechanism) -> Unit fn Imap::client(Self) -> @imap.Client async fn Imap::connect(String, Security, port? : Int32, limits? : @imap.Limits, now? : Int64) -> Self async fn Imap::login(Self, String, String) -> Unit async fn Imap::logout(Self) -> Unit async fn Imap::read(Self) -> Array[@imap.Event] async fn Imap::run(Self) -> Array[@imap.Event] async fn Imap::wait(Self, Int32) -> Reply
struct struct Pop3 // private fields async fn Pop3::auth(Self, &@sasl.Mechanism) -> Unit fn Pop3::client(Self) -> @pop3.Client async fn Pop3::connect(String, Security, port? : Int32, limits? : @pop3.Limits, now? : Int64) -> Self async fn Pop3::list(Self) -> Array[(Int32, Int64)] async fn Pop3::login(Self, String, String) -> Unit async fn Pop3::quit(Self) -> Unit async fn Pop3::retr(Self, Int32) -> Bytes async fn Pop3::run(Self) -> Array[@pop3.Event]
struct struct Reply
  • data : Array[@imap.Response]
  • status : @imap.Status
  • code : @imap.Code?
  • text : String
enum enum Security { Tls(@tls.Client); StartTls(@tls.Client); Clear }
struct struct Smtp // private fields async fn Smtp::auth(Self, &@sasl.Mechanism) -> Unit fn Smtp::client(Self) -> @smtp.Client async fn Smtp::connect(String, Security, port? : Int32, hello? : String, lmtp? : Bool, limits? : @smtp.Limits, now? : Int64) -> Self async fn Smtp::quit(Self) -> Unit async fn Smtp::run(Self) -> Array[@smtp.Event] async fn Smtp::send(Self, String, Array[String], BytesView) -> Array[(String, @smtp.Reply)]
suberror suberror Rejected
  • Reply(@smtp.Command, @smtp.Reply)
  • Done(@imap.Status, @imap.Code?, String)
  • Err(@pop3.Command, String)
  • Sasl(@sasl.Refused)
  • Closed