Letterbox.MIME (Letterbox v0.1.0-rc.0)

Copy Markdown View Source

An email's structure (MIME, RFC 2045 to 2049): its parts, their headers and their bodies, still encoded.

part = Letterbox.MIME.parse(raw)
part.type          #=> "multipart/mixed"
Enum.map(part.parts, & &1.type)
#=> ["multipart/alternative", "image/jpeg"]

Parsing copies nothing: each part's :body is a sub-binary of raw, the bytes where they are. A body is decoded only when it is asked for: a text with text/1, any body with decode/1, and an attachment as a stream of chunks with stream/2, so a photograph of 8 MB goes to a file without its decoded bytes ever all in memory.

The parser takes what mail clients send: LF as well as CRLF, a multipart with no closing boundary, a part with no headers, a boundary in quotes or not. It stops where an email would make it work without end: past :max_depth levels of parts in parts (20) or :max_parts parts (1,000), the rest of a multipart is one part of its type, not split.

Summary

Types

t()

A part

Functions

A part's body, decoded from its transfer encoding: the bytes it carries.

Every part with no parts of its own (the texts, images, files), in the order a reader meets them, a message/rfc822 included as one.

The structure of an email, raw as it came (RFC 5322).

A part's body, decoded, as a stream of chunks: for a large attachment, written to a file as it is decoded. :chunk is how many bytes of the encoded body each chunk is decoded from (64 KB).

A text part's body in UTF-8, from its charset, with its line breaks as \n: {:ok, text}, or {:lossy, text} when bytes of it could not be read (see Letterbox.Charset).

Types

t()

@type t() :: %Letterbox.MIME{
  body: binary(),
  content_id: String.t() | nil,
  disposition: String.t() | nil,
  disposition_params: %{required(String.t()) => String.t()},
  encoding: String.t(),
  headers: [Letterbox.MIME.Header.t()],
  params: %{required(String.t()) => String.t()},
  parts: [t()],
  path: [non_neg_integer()],
  type: String.t()
}

A part:

  • :type - its content type, in lowercase ("text/plain" when it has none, as RFC 2045 says).
  • :params - the content type's parameters ("charset", "boundary", "name"), decoded.
  • :disposition - "attachment", "inline", or nil when it has none, and :disposition_params ("filename", decoded).
  • :encoding - its Content-Transfer-Encoding, in lowercase: "base64", "quoted-printable", "7bit", "8bit", "binary".
  • :content_id - its Content-ID, without the angle brackets: what an HTML part's cid: points to.
  • :headers - all its headers (Letterbox.MIME.Header.parse/1).
  • :body - its body, still encoded, a sub-binary of the email.
  • :parts - the parts of a multipart, and the one email of a message/rfc822 (a forwarded email, attached).
  • :path - where it is: [] for the email, [1, 0] for the first part of the second.

Functions

decode(mime)

@spec decode(t()) :: binary()

A part's body, decoded from its transfer encoding: the bytes it carries.

leaves(part)

@spec leaves(t()) :: [t()]

Every part with no parts of its own (the texts, images, files), in the order a reader meets them, a message/rfc822 included as one.

parse(raw, opts \\ [])

@spec parse(binary(), keyword()) :: t()

The structure of an email, raw as it came (RFC 5322).

Options: :max_depth (20) and :max_parts (1,000).

stream(part, opts \\ [])

@spec stream(t(), keyword()) :: Enumerable.t()

A part's body, decoded, as a stream of chunks: for a large attachment, written to a file as it is decoded. :chunk is how many bytes of the encoded body each chunk is decoded from (64 KB).

text(part)

@spec text(t()) :: {:ok, String.t()} | {:lossy, String.t()}

A text part's body in UTF-8, from its charset, with its line breaks as \n: {:ok, text}, or {:lossy, text} when bytes of it could not be read (see Letterbox.Charset).