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.
Every public value, type, method, and error, grouped by package and ordered for lookup. Generated from the package interface and the source comments.
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.
println(@moonmime.content_disposition("menu.pdf"))
println(@moonmime.content_disposition("receta \"de la abuela\".pdf"))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.
test {
let text = @moonmime.decode_charset(name="iso-8859-1", data=b"\xe9") catch {
_ => ""
}
inspect(text, content="é")
}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.
test {
inspect(@moonmime.decode_words("=?utf-8?B?Q2Fmw6k=?="), content="Café")
}Write text in name, refusing a character the set has no room for.
test {
let bytes = @moonmime.encode_charset(name="iso-8859-1", text="é") catch {
_ => b""
}
inspect(bytes == b"\xe9", content="true")
}One encoded word, however long it comes out.
test {
let word = @moonmime.encode_word("Café") catch { _ => "" }
inspect(@moonmime.decode_words(word[:]), content="Café")
}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.
What a response says it is when nothing says otherwise.
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.
let form = @moonmime.formdata(body, "----edge")
println(form.field("recipe").unwrap_or("")) // chiles en nogadaThe 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.
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.
println(@moonmime.guess_type("factura.PDF")) // application/pdf
println(@moonmime.guess_type("archive.tar.gz")) // application/gzipA 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.
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.
let mt = @moonmime.media_type("text/plain; charset=utf-8")
println(mt.essence) // text/plain
println(mt.param("charset").unwrap_or("")) // utf-8Split 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.
let parts = @moonmime.multipart(body, "b")
println(parts.length()) // 1
println(parts[0].header("content-type").unwrap_or("")) // text/plainHow 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.
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.
The body body holds, as a source that hands it over once.
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.
The filename the disposition names, filename* first (RFC 6266 §4.3).
A filename* that is not UTF-8 falls back to the plain filename.
A parameter by lowercased name.
Parse a header value. Lenient, like media_type.
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é.txtA named text value from a form.
A parsed form: its text fields and its files.
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.
Every field, in the order they were sent.
The first value under this name.
The first file under this name.
Whether anything was parsed at all.
A form holding these fields and files, for a decoder of another encoding to hand back the same type this package reads multipart into.
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.
Every file, in the order they were sent.
Every value under this name — a set of checkboxes sends the same name many times, and taking only the first silently drops the rest.
A sink that keeps what it is handed, for a caller that wants the octets after all.
Everything written so far, as one value.
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.
Build a bound by naming the parts that differ from the default.
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.
A parameter by lowercased name, quoted or bare.
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.
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",
)
}The subtype half: plain in text/plain, empty when there is no slash.
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.
The type half: text in text/plain.
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.
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.
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.
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")
}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).
The subtype as it is written on the wire: the half that follows multipart/.
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.
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>\"")
}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.
The next octets of the current part's body.
The boundary the delimiters are written with.
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.
The Content-Type that goes with the body, to be written before or beside it: the subtype and the boundary the receiver splits on.
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.
One part of a multipart body: its header fields and its body octets.
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.
The part's Content-Type, parsed; None when it has none.
The part's Content-Disposition, parsed; None when it has none.
One of the part's own headers, by lowercased name.
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.
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.
The part's Content-Type, parsed; None when it has none.
The part's Content-Disposition, parsed; None when it has none.
One of the part's own headers, by lowercased name.
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.
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")
}The boundary the parts are delimited by.
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.
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.
Read parts out of a body that is already in hand.
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.
A multipart/related body (RFC 2387): a compound object whose parts belong together, one of them the root the rest hang off.
The framing is the one every multipart subtype shares; what this type adds is the three parameters that say what the parts are — type for the media type of the root, start for the root's Content-ID, start-info passed through to that type — and the reading of start back into the part it names.
parts is what a reader found and what a writer frames; body() writes the octets those parts frame to, so a value read from a body that carried a preamble or a folded header writes back this library's own framing of the same parts.
The body the parts frame to, delimited by this value's boundary.
The Content-Type that announces this body: boundary first, because the receiver cannot read the parts without it, then the three parameters RFC 2387 defines, in the order it names them. A parameter that was not given is not written.
A related body of parts.
The boundary works as it does for any other multipart body: the caller's, checked against RFC 2046 and against the parts, or generated from a stem stepped on until it is proven absent.
Left without parts the value is the header alone, which is what a caller streaming the body needs: the writer frames the parts, the value names them.
test {
let related = @moonmime.Related::new(
[
@moonmime.Part::{
headers: [("content-id", "<root>"), ("content-type", "text/html")],
body: b"<b>hola</b>",
},
],
boundary="b",
type_="text/html",
start="<root>",
)
inspect(
related.content_type(),
content="multipart/related; boundary=b; type=\"text/html\"; start=\"<root>\"",
)
inspect(related.root().unwrap().body, content="b\"<b>hola</b>\"")
}The root part: the one start names by its Content-ID, or the first part when the header names none, which is RFC 2387's default.
None when the header names a part the body does not hold, which is a disagreement between the two halves rather than something to guess at.
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.
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\"")
}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.
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)
}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.
One of the part's own headers, by lowercased name.
The size of the uploaded content in bytes.
The two encodings RFC 2047 §4 allows inside an encoded word, named for the letter that announces them in the header itself.
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.
More parts than allowed.
One part longer than allowed.
A body that stops making sense at this offset. Only raised under strict; otherwise the parts read so far are returned.
A charset neither shipped nor passed in, by its lower-cased name.
A content type that is not a form encoding, for whoever picks a decoder by Content-Type and was asked to say so.
A content type that is not multipart/related, for the same reason.
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.
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.
test {
let out = @qp.decode(b"a=3Db") catch { _ => b"" }
inspect(out == b"a=b", content="true")
}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.
test {
let out = @qp.encode(b"a=b")
inspect(out == b"a=3Db", content="true")
}Why a body is not quoted-printable: an = that starts no escape, at this offset into the input. Only raised under strict.