howdy/mail/outbox

Keep mail instead of sending it, for development and tests. The admin area (admin.mail) shows what an outbox holds, and tests can read the last message to an address.

// dev/my_app_dev.gleam
let assert Ok(box) = outbox.start_in("tmp/mail")
let mailer = mail.mailer(outbox.adapter(box))

Start the outbox once, outside the function howdy/dev rebuilds the app from, so it survives reloads. With start_in, each message is also written to the directory as an .eml file any mail client can open, and a .json file the outbox reads back after a restart. Add the directory to .gitignore: it holds sign-in tokens and whatever else was sent.

In an app with a supervision tree, supervised and supervised_in give the outbox as a child instead, so it is restarted if it crashes. While it is not running, sending to it is Unavailable, like any other adapter whose provider is away, and reading it is an error rather than a crash.

Only the newest 500 messages are kept; older ones and their files are removed.

Types

pub opaque type Outbox

Values

pub fn adapter(box: Outbox) -> mail.Adapter

Keep messages given to this adapter in the outbox. While the outbox is not running, sending is Unavailable.

pub const capacity: Int

How many messages are kept.

pub fn clear(box: Outbox) -> Result(Nil, mail.Error)

Forget every message, and delete their files.

pub fn directory(box: Outbox) -> option.Option(String)

Where messages are written, if anywhere.

pub fn get(box: Outbox, id: String) -> Result(mail.Outgoing, Nil)

The message with this id. Error(Nil) when there is none, or the outbox is not running.

pub fn latest_to(
  box: Outbox,
  email: String,
) -> Result(mail.Outgoing, Nil)

The newest message with this address among its recipients. Error(Nil) when there is none, or the outbox is not running.

pub fn messages(
  box: Outbox,
) -> Result(List(mail.Outgoing), mail.Error)

Every message kept, newest first, or Unavailable while the outbox is not running.

pub fn pid(box: Outbox) -> Result(process.Pid, Nil)

The outbox process, while it is running.

pub fn start() -> Outbox

An outbox in memory: its messages are gone when the VM stops. It is linked to the caller; see supervised for an app with a supervision tree.

pub fn start_in(directory: String) -> Result(Outbox, String)

An outbox that also writes each message to directory, creating it if needed, and starts with the messages already there.

pub fn subscribe(box: Outbox, notify: fn() -> Nil) -> Nil

Call notify whenever a message arrives or the outbox is cleared, until the calling process exits. It runs in the outbox’s process, so it should only send a message, as a live component’s dispatch does. Nothing is subscribed while the outbox is not running, and a restarted outbox starts without subscribers.

pub fn supervised() -> #(
  Outbox,
  supervision.ChildSpecification(Outbox),
)

start as a child of a supervisor, with the handle to use it by. The handle is made here, before the outbox starts, so it can be given to the app while the supervisor owns the outbox:

let #(box, child) = outbox.supervised()
static_supervisor.new(static_supervisor.OneForOne)
|> static_supervisor.add(child)
|> static_supervisor.add(howdy.supervised(app(outbox.adapter(box))))

A restarted outbox in memory starts empty, and its subscribers have to subscribe again.

pub fn supervised_in(
  directory: String,
) -> #(Outbox, supervision.ChildSpecification(Outbox))

start_in as a child of a supervisor. A restarted outbox reads the directory again, so its messages are kept.

✨ Search Document