# `Letterbox.Attachment`
[🔗](https://github.com/hattoriai/letterbox/blob/v0.1.0-rc.0/lib/letterbox/attachment.ex#L1)

A file that came with a message: a photograph, a PDF, an invitation,
a forwarded email.

Nothing of it is decoded until the app asks: `read/1` gives its bytes,
for a small file; `save/3` writes it to a file as it is decoded, with
its size and checksum; `store/2` keeps it in the app's store with
Storehouse, checked by its bytes, under the app's limits:

    for attachment <- message.attachments, String.starts_with?(attachment.content_type, "image/") do
      Letterbox.Attachment.store(attachment,
        accept: Storehouse.Check.images(),
        max_bytes: 50_000_000,
        owner: "sites/#{site.id}",
        room: Media.room(site),
        existing: &Media.by_checksum(site, &1)
      )
    end

`:inline?` is true for a file that the message's HTML shows in its text
(a `cid:` image): a logo in a signature, but also, from a phone, the
photographs someone put in the body. Whether to keep those is the app's
choice.

# `t`

```elixir
@type t() :: %Letterbox.Attachment{
  content_id: String.t() | nil,
  content_type: String.t(),
  encoded_bytes: non_neg_integer(),
  inline?: boolean(),
  name: String.t() | nil,
  part: Letterbox.MIME.t()
}
```

An attachment:

  * `:name` - its file name, as the sender's mail client gave it, decoded;
    nil when it has none.
  * `:content_type` - the type the email claims, in lowercase. Only its
    bytes say what it is (`store/2` checks them).
  * `:content_id` - the `Content-ID` the HTML's `cid:` points to, or nil.
  * `:inline?` - whether the HTML shows it in the text.
  * `:encoded_bytes` - the size of its body in the email, before it is
    decoded: about a third more than the file, in Base64.
  * `:part` - its `Letterbox.MIME` part.

# `read`

```elixir
@spec read(t()) :: binary()
```

Its bytes, decoded, in memory: for a small file.

# `save`

```elixir
@spec save(t(), Path.t(), keyword()) ::
  {:ok, %{bytes: non_neg_integer(), checksum: String.t()}}
  | {:error, :too_large | File.posix()}
```

Writes it to `path` as it is decoded, a chunk at a time. Gives `{:ok,
%{bytes: …, checksum: …}}`, its size and its SHA-256 (in Base64, as
Storehouse gives checksums), or `{:error, :too_large}` past
`:max_bytes`, with nothing left at `path`.

# `store`

```elixir
@spec store(t(), keyword()) ::
  {:ok | :existing, Storehouse.File.t()} | {:error, term()}
```

Keeps it in the app's store: `Storehouse.store/2` of its bytes, with its
name and claimed type, and the options given (`:accept`, `:max_bytes`,
`:owner`, `:room`, `:existing`, `:adapter`, …). It is decoded to a
temporary file first, which goes once it is stored.

Gives what `Storehouse.store/2` gives: `{:ok, file}`, `{:existing,
file}` when the app has the same file already (`:existing`), or `{:error,
reason}`: `:not_accepted`, `:too_large`, `:storage_full`, …

# `stream`

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

Its bytes, decoded, as a stream of chunks (`Letterbox.MIME.stream/2`).

---

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