Pure Elixir RFC 5322 / MIME email parser that extracts nested attachments.
This module exposes the public API; the parsing internals live under
lib/email_parser. The parser is validated against a corpus of real-world
messages (see
test/fixtures/corpus).
Parsing is best-effort and never raises on malformed input:
- any binary is accepted, valid UTF-8 or not;
- text parts in UTF-8/ASCII, ISO-8859-1, windows-1252 and UTF-16 are
converted to UTF-8; text in other charsets is kept with unmappable
bytes replaced by
U+FFFD; - parts with a broken transfer encoding fall back to their raw bytes, and malformed MIME structures are recovered as far as possible.
Every function returns :error for input that carries no parseable headers
at all.
Summary
Functions
Like extract_nested_attachments/1, but writes the attachments to disk.
Parses a string containing a RFC5322 raw message and extracts all nested attachments.
Parses a raw message and returns it without its attachments.
Functions
@spec extract_attachments_to_disk(binary(), keyword()) :: {:ok, [String.t()]} | :error | {:error, File.posix()}
Like extract_nested_attachments/1, but writes the attachments to disk.
Returns the names of the files written, in the order the attachments appear
in the message and including :prefix. Attachments sharing a name write to
the same file.
Options
:directory- directory to write the attachments to, created when missing. Defaults to the current directory (".").:prefix- prepended to each filename, to keep attachments of different messages apart in a shared directory. Defaults to"".:mime_types- when given, only attachments whose content type is in the list are written. Attachments without a content type are never written. Defaults to[], which writes them all.
Only the last path segment of an attachment's name is used, so a message
cannot write outside :directory. A write failure removes the files already
written and returns the reason.
Examples
iex> EmailParser.extract_attachments_to_disk("Subject: no attachments\r\n\r\nhi", [])
{:ok, []}
# EmailParser.extract_attachments_to_disk(raw_message,
# directory: "/tmp/inbox",
# prefix: "account-",
# mime_types: ["application/pdf"])
# {:ok, ["account-example.pdf"]}
@spec extract_nested_attachments(binary()) :: {:ok, [EmailParser.Attachment.t()]} | :error
Parses a string containing a RFC5322 raw message and extracts all nested attachments.
A best-effort is made to parse the message and if no headers are found
:error is returned.
Example
iex> EmailParser.extract_nested_attachments(raw_message)
{:ok, [%EmailParser.Attachment{name: "example.pdf", content_type: "application/pdf", content_bytes: "..."}]}
@spec strip_attachments(binary()) :: {:ok, EmailParser.Email.t()} | :error
Parses a raw message and returns it without its attachments.
The returned EmailParser.Email carries the message's header fields and its
bodies. Header names are downcased, since RFC 5322 field names are
case-insensitive, their values are RFC 2047 decoded, and the first
occurrence of a repeated field wins. Content-Transfer-Encoding and
Content-Disposition are left out: they describe the raw body, and the
bodies here are decoded.
Example
iex> {:ok, email} = EmailParser.strip_attachments("From: bob@example.com\r\nSubject: Hi\r\n\r\nHello!")
iex> email.headers
%{"from" => "bob@example.com", "subject" => "Hi"}
iex> email.text_body
"Hello!"