Letterbox

Letterbox — Inbound email for Phoenix

Inbound email for Phoenix: emails to your app, kept, read and given to your code.

Replies to the notifications your app sends, a support address, receipts forwarded to be filed, photographs emailed to a library. An email comes in through your provider or a relay, Letterbox keeps it, reads it (its text, its HTML, its attachments, who sent it) and gives it to the mailbox your routes name.

Early release. Before 1.0, a minor version may change the API.

Install

mix igniter.install letterbox

or add {:letterbox, "~> 0.1"} and run mix letterbox.install. It writes the migration of Letterbox's table, the webhooks' route, the config, your routes and a first mailbox, and shows each change before it writes (see Mix.Tasks.Letterbox.Install). Letterbox needs Elixir 1.18+, Phoenix 1.7.14+ and Storehouse, which keeps the raw emails in your store.

Your first mailbox

Routes say which mailbox takes which message:

defmodule MyApp.Mailboxes do
  use Letterbox.Routing

  route "support@", to: MyApp.SupportMailbox
  route ~r/\Areply\+/, to: MyApp.RepliesMailbox
  route "@in.example.com", to: MyApp.SitesMailbox
  route :all, to: MyApp.Mailboxes.Inbox
end

A mailbox gets a Letterbox.Message:

defmodule MyApp.SupportMailbox do
  use Letterbox.Mailbox

  @impl true
  def process(message) do
    MyApp.Tickets.open(message.from, message.subject, message.text)
  end
end

:ok delivers it; {:bounce, reason} refuses it for good; an error or an exception tries it again, then marks it failed. Every message has a row (Letterbox.Inbound) with its status, and its raw email in your store, so a failed one can be retried once the bug is fixed.

How emails reach your app

Your domain's mail goes to a provider, or a server of yours, which hands it to Letterbox: that is an ingress. Point a domain's MX records at it once (in.example.com), and every address there reaches your routes.

# config/runtime.exs
config :letterbox,
  ingresses: [
    resend: [secret: System.fetch_env!("RESEND_WEBHOOK_SECRET"), api_key: System.fetch_env!("RESEND_API_KEY")],
    relay: [token: System.fetch_env!("LETTERBOX_RELAY_TOKEN")]
  ]

The webhooks are a plain route of your router, yours to move and rename (Letterbox.Plug):

scope "/inbound" do
  pipe_through :webhooks
  post "/webhooks/:ingress", Letterbox.Plug, []
end

A signed webhook needs its body as it was sent: the installer gives your endpoint's Plug.Parsers body_reader: {PhoenixMountable.RawBody, :read_body, [MyAppWeb.Router]}, which keeps it wherever you put the route.

Attachments

Nothing is decoded until you ask, and a large file is written to the disk as it is decoded, never whole in memory:

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

store/2 keeps it with Storehouse, checked by its bytes, under your limits, and gives back a file you have already (:existing) rather than a copy. :inline? says whether the HTML shows it in its text, as a phone does with the photographs of its body, and message.file_links the files sent as links (Mail Drop, Google Drive, …).

The development page

import Letterbox.Dev.Router

if Application.compile_env(:my_app, :dev_routes) do
  scope "/dev" do
    pipe_through :browser
    letterbox_dev "/letterbox"
  end
end

At /dev/letterbox: write an email to your app, with attachments, and see what became of it, its mailbox, its status, its text and HTML (in a frame that runs nothing and loads nothing from elsewhere), its files and its raw email; retry it, delete it.

What you get

How Letterbox compares

The Elixir libraries that receive email, as their documentation and code describe them in October 2026. A correction is welcome as an issue.

Letterbox 0.1 mailroom 0.7 gen_smtp 1.3 mail 0.5
Webhooks of providers Yes No No No
A relay of raw emails Yes No No No
Polling a mailbox (IMAP, POP3) Later Yes No No
An SMTP server Later No Yes No
Routing to the app's code Yes No No No
Kept, with a status, retried Yes No No No
Charsets beyond UTF-8 and Latin-1 All of WHATWG's With a handler of yours With iconv installed No
RFC 2231 file names, read Yes No (mail's parser) No (written only) No
Attachments streamed to the disk Yes No No No
Senders verified Yes No No No
A development page Yes No No No

Roadmap

Development

mix deps.get
DATABASE_URL=postgres://postgres:postgres@localhost/letterbox_test mix test
mix credo --strict

License

MIT. See LICENSE.