# `EmailParser`
[🔗](https://github.com/saasformula/email_parser/blob/v0.1.0/lib/email_parser.ex#L1)

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`](https://github.com/saasformula/email_parser/tree/main/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.

# `extract_attachments_to_disk`

```elixir
@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"]}

# `extract_nested_attachments`

```elixir
@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: "..."}]}

# `strip_attachments`

```elixir
@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!"

---

*Consult [api-reference.md](api-reference.md) for complete listing*
