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

Copy Markdown View Source

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.

Summary

Types

t()

An attachment

Functions

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

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.

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.

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

Types

t()

@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.

Functions

read(attachment)

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

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

save(attachment, path, opts \\ [])

@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(attachment, opts \\ [])

@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(attachment, opts \\ [])

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

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