API reference

The whole surface.

Every public value, type, method, and error, grouped by package and ordered for lookup. Generated from the package interface and the source comments.

Values

pub fn content_disposition(StringView, inline? : Bool) -> String

The Content-Disposition value naming filename (RFC 6266).

A name that is plain ASCII goes out quoted; anything else — a space is not enough, but an accent, a quote, a newline, a byte outside ASCII — goes out as filename*, the only form that carries them and the only one a name cannot break out of.

inline asks the browser to show the content rather than save it.

example
println(@moonmime.content_disposition("menu.pdf"))
println(@moonmime.content_disposition("receta \"de la abuela\".pdf"))
attachment; filename="menu.pdf" attachment; filename*=utf-8''receta%20%22de%20la%20abuela%22.pdf
pub fn decode_charset(name~ : StringView, data~ : BytesView, charsets? : Map[String, &Charset]) -> String raise Refused

Read data as name says it was written. Unmapping octets become U+FFFD so that one bad byte costs one character and not the whole part.

mbt check
test {
  let text = @moonmime.decode_charset(name="iso-8859-1", data=b"\xe9") catch {
    _ => ""
  }
  inspect(text, content="é")
}
pub fn decode_words(StringView, charsets? : Map[String, &Charset]) -> String

Undo what encode_words did, and what every other mail library did too: encoded words are the one corner of a header everybody writes the same way.

Whitespace between two adjacent encoded words disappears, because that was folding and not content; whitespace next to ordinary text stays put.

mbt check
test {
  inspect(@moonmime.decode_words("=?utf-8?B?Q2Fmw6k=?="), content="Café")
}
pub fn encode_charset(name~ : StringView, text~ : StringView, charsets? : Map[String, &Charset]) -> Bytes raise Refused

Write text in name, refusing a character the set has no room for.

mbt check
test {
  let bytes = @moonmime.encode_charset(name="iso-8859-1", text="é") catch {
    _ => b""
  }
  inspect(bytes == b"\xe9", content="true")
}
pub fn encode_word(StringView, charset? : String, encoding? : WordEncoding, charsets? : Map[String, &Charset]) -> String raise Refused

One encoded word, however long it comes out.

mbt check
test {
  let word = @moonmime.encode_word("Café") catch { _ => "" }
  inspect(@moonmime.decode_words(word[:]), content="Café")
}
pub fn encode_words(StringView, charset? : String, encoding? : WordEncoding, column? : Int32, max? : Int32, charsets? : Map[String, &Charset]) -> String raise Refused

A header value written as encoded words, splitting whenever the next word would run past max.

Text that is already printable US-ASCII goes back unchanged: it needs no encoding, and writing it plainly is what keeps Subject: Hello readable in a raw view.

pub let fallback : String

What a response says it is when nothing says otherwise.

pub fn formdata(BytesView, StringView, limits? : Limits, strict? : Bool) -> Form raise Refused

Parse a multipart/form-data body (RFC 7578) given its boundary.

A part goes into the files when its Content-Disposition carries a filename and into the fields when it does not, which is the only thing distinguishing them on the wire.

example
let form = @moonmime.formdata(body, "----edge")
println(form.field("recipe").unwrap_or(""))  // chiles en nogada
chiles en nogada
pub fn guess_extension(StringView) -> String?

The extension most people use for a media type, with its dot; None when the type is not in the table. Parameters are ignored, so the value may be given with its charset.

pub fn guess_type(StringView) -> String

The media type a filename's extension implies; fallback for anything else.

The last dot wins, so archive.tar.gz resolves on gz. The table is the common part of the IANA media-type registry; a caller that knows its type names it rather than hoping the extension does.

example
println(@moonmime.guess_type("factura.PDF"))        // application/pdf
println(@moonmime.guess_type("archive.tar.gz"))     // application/gzip
application/pdf application/gzip
pub let limits : Limits

A thousand parts of a megabyte each — a bound wide enough for a usual form upload, tight enough to stop a body from asking for more than a server has. A caller that knows its own shape should say so.

pub fn media_type(StringView) -> MediaType

Parse a header value into a media type. Lenient: anything that is not a parameter is left out, so an empty or foreign value yields an empty essence rather than an error.

example
let mt = @moonmime.media_type("text/plain; charset=utf-8")
println(mt.essence)                    // text/plain
println(mt.param("charset").unwrap_or(""))  // utf-8
text/plain utf-8
pub fn multipart(BytesView, StringView, limits? : Limits, strict? : Bool) -> Array[Part] raise Refused

Split a multipart body into its parts (RFC 2046 §5.1) given its boundary.

Preamble and epilogue are dropped; the two CRLF before each delimiter belong to the delimiter, not to the part. A body that stops making sense returns what it read so far, unless strict says to refuse it.

This is the reader in PartsReader walked to its end: a caller that does not want the whole body in hand reads the parts as they arrive instead, and gets the same parts out.

example
let parts = @moonmime.multipart(body, "b")
println(parts.length())                       // 1
println(parts[0].header("content-type").unwrap_or(""))  // text/plain
1 text/plain

Types

pub(open) trait Charset

How the bytes of a body turn into the text they carry, and back. Three come with the library; everything else arrives through this trait, so a codebase that needs windows-1252 does not need a fork of this one.

fn decode(Self, BytesView) -> String raise Refused
fn encode(Self, StringView) -> Bytes raise Refused
type Chunks

A body held whole, handed over as one chunk.

It is the adapter for a caller that already has the octets: streaming from memory and streaming from a socket reach the reader the same way.

pub fn Chunks::new(BytesView) -> Self

The body body holds, as a source that hands it over once.

pub fn Chunks::next(Self) -> Bytes?
pub(all) struct Disposition

A parsed Content-Disposition header (RFC 6266).

kind is the lowercased disposition type — inline, attachment, form-data — and params the rest, names lowercased and quoted values unquoted.

kind : String
params : Array[(String, String)]
pub fn Disposition::filename(Self) -> String?

The filename the disposition names, filename* first (RFC 6266 §4.3).

A filename* that is not UTF-8 falls back to the plain filename.

pub fn Disposition::param(Self, StringView) -> String?

A parameter by lowercased name.

pub fn Disposition::parse(StringView) -> Self

Parse a header value. Lenient, like media_type.

example
let d = @moonmime.Disposition::parse(
  "attachment; filename=\"fallback.txt\"; filename*=utf-8''r%C3%A9sum%C3%A9.txt",
)
println(d.filename().unwrap_or(""))  // résumé.txt
résumé.txt
pub(all) struct Field

A named text value from a form.

name : String
value : String
type Form

A parsed form: its text fields and its files.

pub fn Form::encode(Self, boundary? : StringView) -> Multipart raise Refused

Write the form as a multipart/form-data body (RFC 7578).

Fields go out first, then files, each in the order the form holds them: a Form keeps the two apart, so it cannot reproduce the interleaving of the body it was read from. A file whose name leaves ASCII is named with filename* (RFC 5987), the one form a receiver decodes back to the same characters.

pub fn Form::entries(Self) -> Array[Field]

Every field, in the order they were sent.

pub fn Form::field(Self, StringView) -> String?

The first value under this name.

pub fn Form::file(Self, StringView) -> Upload?

The first file under this name.

pub fn Form::is_empty(Self) -> Bool

Whether anything was parsed at all.

pub fn Form::new(fields? : Array[Field], files? : Array[Upload]) -> Self

A form holding these fields and files, for a decoder of another encoding to hand back the same type this package reads multipart into.

pub fn Form::parts(Self) -> Array[Part]

The parts the form frames to, in the order a body carries them: fields first, then files.

A form is a multipart body, so a caller that writes one part at a time — streaming an upload rather than holding the body — takes the parts from here and frames them itself.

pub fn Form::uploads(Self) -> Array[Upload]

Every file, in the order they were sent.

pub fn Form::values(Self, StringView) -> Array[String]

Every value under this name — a set of checkboxes sends the same name many times, and taking only the first silently drops the rest.

type Gathered

A sink that keeps what it is handed, for a caller that wants the octets after all.

pub fn Gathered::bytes(Self) -> Bytes

Everything written so far, as one value.

pub fn Gathered::new() -> Self
pub fn Gathered::write(Self, BytesView) -> Unit raise
pub(all) struct Limits

What a body is allowed to be.

A body arrives from whoever sent it, so both bounds are needed: a million empty parts and one enormous part are different ways of asking a server to allocate more than it has.

parts : Int32
part_size : Int32
pub fn Limits::new(parts? : Int32, part_size? : Int32) -> Self

Build a bound by naming the parts that differ from the default.

pub(all) struct MediaType

A media type: text/plain; charset=utf-8.

essence is the lowercased type/subtype; params keeps the parameters in order, names lowercased and quoted values unquoted. A malformed parameter is dropped rather than guessed at.

essence : String
params : Array[(String, String)]
pub fn MediaType::param(Self, StringView) -> String?

A parameter by lowercased name, quoted or bare.

pub fn MediaType::parse(StringView) -> Self?

Parse a header value the way the MIME Sniffing standard reads one, and refuse the rest: None when the value is not a type/subtype of tokens.

Two readers, two jobs. media_type above takes whatever a server sent and keeps what it can; this one answers "is this a media type at all", which is the question worth asking when a value decides what a file is. A parameter whose name is not a token, or whose value cannot be written into a quoted string, is dropped; the first of two with the same name wins. Both are the standard's readings.

mbt check
test {
  let mt = @moonmime.MediaType::parse("Text/HTML; charset=\"utf-8\"").unwrap()
  inspect(mt.essence, content="text/html")
  inspect(mt.param("charset").unwrap_or(""), content="utf-8")
  inspect(
    @moonmime.MediaType::parse("text/html, text/plain") is None,
    content="true",
  )
}
pub fn MediaType::subtype(Self) -> String

The subtype half: plain in text/plain, empty when there is no slash.

pub fn MediaType::to_string(Self) -> String

Back to a header value. A value that is not a token goes out quoted, which keeps a boundary with spaces or quotes in one piece.

pub fn MediaType::type_(Self) -> String

The type half: text in text/plain.

pub(all) struct Multipart

A multipart body on its way out: the octets, the boundary that delimits them, and the subtype the two have to agree on.

The octets are what a Content-Type of content_type() describes and what multipart reads back with the same boundary.

kind : MultipartKind
boundary : String
body : Bytes
pub fn Multipart::content_type(Self) -> String

The Content-Type that goes with the body: the subtype, and the boundary the receiver needs to split it.

Written as a header value, so a boundary with a space in it — RFC 2046 allows the space, not as the last character — comes out quoted.

pub fn Multipart::new(Array[Part], boundary? : StringView, kind? : MultipartKind) -> Self raise Refused

Frame parts as a multipart body (RFC 2046 §5.1).

A part's headers go out in the order the part holds them, with a lowercased name — the shape a parse hands back — restored to Content-Type casing. A part with no headers at all is still a part: it is delimited like any other and its body is read back as it stands.

The boundary is generated when the caller gives none, from a fixed stem and a single pass over the parts, so the same parts always come out as the same octets and the boundary is proven absent from them rather than merely unlikely. A caller that gives one keeps control of what the receiver sees — the dash-stem browsers use, or a value it has already named elsewhere.

mbt check
test {
  let form = @moonmime.Form::new(fields=[{ name: "title", value: "a report", }])
  let body = form.encode(boundary="----edge")
  inspect(body.content_type(), content="multipart/form-data; boundary=----edge")
  let read = @moonmime.formdata(body.body[:], body.boundary)
  inspect(read.field("title").unwrap_or(""), content="a report")
}
pub(all) enum MultipartKind

Which multipart type a written body announces.

RFC 2046 §5.1 is the framing either way; the subtype says what the parts mean. FormData is the one a form upload carries (RFC 7578), Mixed the one an email with an attachment carries (RFC 2046 §5.1.3).

FormData
Mixed
pub fn MultipartKind::code(Self) -> String

The subtype as it is written on the wire: the half that follows multipart/.

type MultipartWriter

Writes a multipart body a part at a time: the delimiter and header block when a part begins, its body octets as they are handed over, and the closing delimiter when it ends.

The boundary is the caller's or a fixed stem, and the octets that would make it a delimiter inside a part are watched for as they go by: a body that holds the boundary is refused at that point rather than framed into a body the receiver would split somewhere else. A refusal cannot take back what the sink already holds, which is why a caller that cannot lose the octets passes a boundary of its own.

mbt check
test {
  let gathered = @moonmime.Gathered::new()
  let writer = @moonmime.MultipartWriter::new(gathered, boundary="b")
  inspect(writer.content_type(), content="multipart/mixed; boundary=b")
  writer.begin_part([("content-type", "text/plain")])
  writer.body(b"hola")
  writer.begin_part([("content-type", "text/html")])
  writer.body(b"<b>hola</b>")
  writer.close()
  let parts = @moonmime.multipart(gathered.bytes()[:], "b")
  inspect(parts.length(), content="2")
  inspect(parts[1].body, content="b\"<b>hola</b>\"")
}
pub fn MultipartWriter::begin_part(Self, Array[(String, String)]) -> Unit raise

Begin a part: its delimiter, then its header fields and the empty line that ends them.

A part is over when the next one begins; there is nothing to write between them but the delimiter, which carries the CRLF the receiver reads as the end of the previous body.

pub fn MultipartWriter::body(Self, BytesView) -> Unit raise

The next octets of the current part's body.

pub fn MultipartWriter::boundary(Self) -> String

The boundary the delimiters are written with.

pub fn MultipartWriter::close(Self) -> Unit raise

Write the closing delimiter. Closing a body with no parts writes it alone, which is the shape a reader takes for a body that carries none.

pub fn MultipartWriter::content_type(Self) -> String

The Content-Type that goes with the body, to be written before or beside it: the subtype and the boundary the receiver splits on.

pub fn MultipartWriter::new(&Sink, boundary? : StringView, kind? : MultipartKind) -> Self raise Refused

A writer that frames parts for sink.

The boundary is generated when the caller gives none: a fixed stem, so the same parts always frame the same way. Unlike a body written in one call it cannot be proven absent from parts that have not arrived yet, so it is watched for as they go by and a collision is refused.

pub(all) struct Part

One part of a multipart body: its header fields and its body octets.

headers : Array[(String, String)]
body : Bytes
pub fn Part::content_id(Self) -> String?

The part's Content-ID without its angle brackets, which is the form the start parameter of multipart/related names it by; None when it has none.

pub fn Part::content_type(Self) -> MediaType?

The part's Content-Type, parsed; None when it has none.

pub fn Part::disposition(Self) -> Disposition?

The part's Content-Disposition, parsed; None when it has none.

pub fn Part::header(Self, StringView) -> String?

One of the part's own headers, by lowercased name.

pub(all) struct PartHead

The head of a part: its header fields, handed over before its body.

What a reader sees first, and all it needs to decide whether it wants the body at all — a form field can be dropped on its name without its octets having been read.

headers : Array[(String, String)]
pub fn PartHead::content_id(Self) -> String?

The part's Content-ID without its angle brackets, which is the form the start parameter of multipart/related names it by; None when it has none.

pub fn PartHead::content_type(Self) -> MediaType?

The part's Content-Type, parsed; None when it has none.

pub fn PartHead::disposition(Self) -> Disposition?

The part's Content-Disposition, parsed; None when it has none.

pub fn PartHead::header(Self, StringView) -> String?

One of the part's own headers, by lowercased name.

type PartsReader

Reads a multipart body as it arrives: a part's header fields first, then its body, one chunk at a time.

The bound is part of the reading rather than a check afterwards: a part that passes its size is refused where it is crossed, and the octets after it are never pulled. What is already handed over cannot be taken back, so an unread remainder of a part is discarded — and still counted — before the next head is read.

mbt check
test {
  let reader = @moonmime.PartsReader::over(
    b"--b\r\nContent-Type: text/plain\r\n\r\nhola\r\n--b--\r\n", "b",
  )
  let head = reader.next_part().unwrap()
  inspect(head.header("content-type").unwrap_or(""), content="text/plain")
  inspect(reader.read().unwrap(), content="b\"hola\"")
  inspect(reader.read() is None, content="true")
  inspect(reader.next_part() is None, content="true")
}
pub fn PartsReader::boundary(Self) -> String

The boundary the parts are delimited by.

pub fn PartsReader::new(&Source, StringView, limits? : Limits, strict? : Bool) -> Self raise Refused

Read boundary-delimited parts from source.

The boundary is refused when it cannot delimit a body at all — the same bchars and the same 70-octet limit a written body is held to.

pub fn PartsReader::next_part(Self) -> PartHead? raise Refused

The next part's head, or None once the body has ended.

A body the caller did not read to its end is discarded first, and its octets are counted against the bound like any other.

pub fn PartsReader::over(BytesView, StringView, limits? : Limits, strict? : Bool) -> Self raise Refused

Read parts out of a body that is already in hand.

pub fn PartsReader::read(Self) -> Bytes? raise Refused

The next octets of the current part's body, or None at its end.

A part with an empty body answers None on the first call; the delimiter after it has already been consumed by then, so the next next_part reads a head rather than looking for a boundary it passed.

pub(open) trait Sink

Octets on their way out: what a caller's sink takes, a piece at a time.

The writer hands over the delimiter, the header block and the body octets of a part as it is given them, so a body larger than memory never stands in it.

mbt check
test {
  let gathered = @moonmime.Gathered::new()
  let writer = @moonmime.MultipartWriter::new(gathered, boundary="b")
  writer.begin_part([("content-type", "text/plain")])
  writer.body(b"hola")
  writer.close()
  let parts = @moonmime.multipart(gathered.bytes()[:], "b")
  inspect(parts[0].body, content="b\"hola\"")
}
fn write(Self, BytesView) -> Unit raise
pub(open) trait Source

A body arriving in pieces: the next octets, and None once there are none.

The chunk is the source's to size — a socket read, a slice of a file, a window of a buffer. A reader asks for one only when it needs the octets in it, which is what lets a bound stop a body where it is crossed instead of after the whole of it has been read.

mbt check
test {
  let source : &@moonmime.Source = @moonmime.Chunks::new(b"--b--\r\n")
  match source.next() {
    Some(chunk) => assert_eq(chunk, b"--b--\r\n")
    None => fail("the body was not handed over")
  }
  assert_eq(source.next() is None, true)
}
fn next(Self) -> Bytes?
pub(all) struct Upload

A file from a multipart/form-data body.

The content is held whole. Streaming a part to disk instead of into memory is what limits exists to make unnecessary until it is built.

name : String
filename : String
content_type : String
content : Bytes
headers : Array[(String, String)]
pub fn Upload::header(Self, StringView) -> String?

One of the part's own headers, by lowercased name.

pub fn Upload::size(Self) -> Int32

The size of the uploaded content in bytes.

pub(all) enum WordEncoding

The two encodings RFC 2047 §4 allows inside an encoded word, named for the letter that announces them in the header itself.

Base64
QuotedPrintable

Errors

pub(all) suberror Refused

Why a body was not read as a form.

A body over its bounds is refused whole rather than truncated: a handler given the first thousand parts of a larger body would be answering a request nobody sent.

Parts(limit~ : Int32)

More parts than allowed.

Part(limit~ : Int32, got~ : Int32)

One part longer than allowed.

Malformed(at~ : Int32)

A body that stops making sense at this offset. Only raised under strict; otherwise the parts read so far are returned.

Charset(name~ : String)

A charset neither shipped nor passed in, by its lower-cased name.

NotAForm(content_type~ : String)

A content type that is not a form encoding, for whoever picks a decoder by Content-Type and was asked to say so.

NotRelated(content_type~ : String)

A content type that is not multipart/related, for the same reason.

Boundary(got~ : String)

A boundary that cannot delimit the body it was given: not RFC 2046's bchars, longer than the 70 octets it allows, or already standing inside a part, where a receiver would split on it.

moonmime/qp

pub fn decode(BytesView, strict? : Bool) -> Bytes raise Refused

Decode quoted-printable.

=XY becomes the octet, = at the end of a line is the soft break it stands for and contributes nothing, and whitespace at the end of a line is dropped: a gateway further upstream may already have taken it, so keeping it here would not recover it.

strict turns the lenient last two readings into Encoding, which is what a mail filter that wants to see exactly what was signed asks for.

mbt check
test {
  let out = @qp.decode(b"a=3Db") catch { _ => b"" }
  inspect(out == b"a=b", content="true")
}
pub fn encode(BytesView) -> Bytes

Encode as quoted-printable. Lines are broken at 75 columns so that the soft break that ends one stays inside the 76 RFC 2045 allows, and line endings come out as CRLF whatever they arrived as.

mbt check
test {
  let out = @qp.encode(b"a=b")
  inspect(out == b"a=3Db", content="true")
}
pub(all) suberror Refused

Why a body is not quoted-printable: an = that starts no escape, at this offset into the input. Only raised under strict.

Malformed(at~ : Int32)