---
title: Mail
description: Building mail from HTML and text templates, sending it now or from a job, configuring SMTP, SendGrid or Postmark per environment, previewing mail and reading sent mail in development, testing it, and writing a delivery of your own.
category: Going Further
order: 6
---
An app sends mail from a mailer: a `mailer` block whose `def`s each build
one mail, with its recipients, its subject and its parts. The parts come
from templates: an HTML one and a plain-text one, side by side in
`src/views/`. `deliver_now` sends the mail at once; `deliver_later`
queues a [background job](background_jobs.html) that sends it, inside
the caller's transaction if there is one. Where the mail goes depends on
the environment: an inbox you read in the browser in development, a
mailbox of each test's own in tests, and SMTP, SendGrid or Postmark in
production, as `[mail]` in the config says.
After reading this guide, you will know:
- How to generate a mailer and what it writes.
- How to build a mail, with HTML and text templates and a mail layout.
- When to use `deliver_now` and when `deliver_later`, and how
`deliver_later` works with transactions and the `mailers` queue.
- How to configure delivery for each environment, and keep secrets out
of the config files.
- How to preview mail, and read the mail the app sent, in development.
- How to test mail.
- How to add attachments, copies and headers.
- How to write a delivery of your own.
- What Glacier's mail doesn't do yet.
The examples come from `jobs`, the app from
[Background Jobs](background_jobs.html). A report records who asked for
it, in a column the app added for the purpose:
```glacier
migration do
change_table :reports do
# Who to tell when it's ready. Reports made before this column
# have no one.
string :requested_by, null: false, default: ""
end
end
```
and when the report is ready, the `NotifyReady` job emails them with
`ReportMailer.ready`.
## Mailers
### Generating a Mailer
`glacier generate mailer` takes a mailer's name and the names of its
mails:
```console
$ glacier generate mailer ReportMailer ready
create src/mailers/report_mailer.glacier
create src/views/report_mailer/ready.html
create src/views/report_mailer/ready.txt
update config/default.toml
Next: send one with `ReportMailer.ready(&user.email, &user.name).deliver_later()!`.
Change the sender ([mail] from) in config/default.toml before sending real mail.
In development it's kept at /dev/mail; in tests, `delivered_mail()` lists it.
Delivery in production: `[mail] delivery = "smtp"` (or sendgrid, postmark); see the Mail guide.
```
It writes:
- `src/mailers/report_mailer.glacier`, a `mailer` block with one `def`
per mail. Each takes an address and a name, and builds the mail from
its two templates.
- An HTML and a text template for each mail, in
`src/views/report_mailer/`, each with one prop, `name`.
- A placeholder sender, `[mail] from`, in `config/default.toml`, the
first time, so the first mail sends. Change it before sending real
mail.
A trailing `Mailer` is added if the name doesn't have one, so
`glacier generate mailer Report ready` writes the same files. Without
any mail names, it writes one mail called `notify`.
### Building a Mail (updated)
Each `def` in a mailer builds one mail and ends with `mail`, its
recipients and subject as keyword arguments. The generator's `ready`
takes an address and a name; the `jobs` app changed it to take the
report, and to put each part inside a mail layout:
```glacier
module ReportMailer do
implements Mailer
# Each part inside the mail layout of its own kind.
layout: :mailer
# Tells whoever asked for a report that it's ready: once, so not when
# they've been told.
def ready(report: &Report) do
mail to: &report.requested_by, subject: "#{report.title} is ready", unless: report.notified_at.is_some()
end
```
`mail` finds the mail's templates by convention: `ready` in
`ReportMailer` is `src/views/report_mailer/ready.html` and `ready.txt`.
Each template is given the `def`'s parameters as its props, so `ready`'s
templates declare `{% props report: Report %}`. A reference such as
`&Report` becomes an owned value for the template, as props are owned.
`mail` takes:
| `to:` | The recipient (required) |
| `subject:` | The subject line (required) |
| `cc:`, `bcc:` | Another recipient |
| `from:` | The sender, when it isn't the mailer's `from:` or the configured `mail.from` |
| `reply_to:` | Where replies go |
| `with: [posts: posts]` | The templates' props, in place of the `def`'s parameters |
| `template: :other` | Another template of this mailer's, in place of the `def`'s name |
| `html: false`, `text: false` | A mail with only the other part |
| `if: cond`, `unless: cond` | Sent only when `cond` holds (or doesn't) |
Above its `def`s, a mailer can declare `from: "Reports <reports@example.com>"`
for all its mail, and `layout: :mailer` to put each part inside a
[mail layout](#a-mail-layout). Other `def`s are helpers: a `defp` has
its own parameters and return type, as in a `service`.
An address is a bare address, `ada@example.com`, or a name and an
address, `Reports <reports@example.com>`. A `"#{report.title} is ready"`
string puts the value of each `#{…}` into the text. The templates are
rendered when the mail is built, so a `Mail` holds only text and can be
stored in a job's arguments.
A mail is checked when it's delivered. It needs a sender, from `from` or
the config; at least one recipient, in `to`, `cc` or `bcc`; and an HTML
or a text part. A mail missing any of these isn't sent, and delivering
it returns an error that says which is missing.
A mailer compiles to a struct with a function per `def` returning a
`glacier::mail::Mail`, built with `Mail::new()` and methods named after
the arguments above (`to`, `subject`, `html`, `text` and so on). A mailer
can be written that way in Rust instead, in `src/mailers/<name>.rs`.
### Mail That Isn't Sent (new)
`if:` and `unless:` make a mail that's built but not sent: `ready` isn't
sent to someone already told. Delivering it, now or later, does nothing
(`deliver_later` queues no job and returns `0`), and its `skipped` is
`true`. A user's email preferences go here too: `if:
user.wants(:reports)`.
```glacier
test "a report already notified isn't mailed again" do
report = insert(Report, title: "Q3", requested_by: "ada@example.com")
report.notified_at = Some(Timestamp.now())
mail = ReportMailer.ready(&report)
assert mail.skipped
mail.deliver_now()!
assert delivered_mail().is_empty()
end
```
## Templates
### HTML and Text
A mail's templates are ordinary templates in `src/views/`, in a folder
named after the mailer. The HTML one is written as any page is, and its
values are HTML-escaped:
```html
{% props report: Report %}
<p>Hello,</p>
<p>The report you asked for, “{{ report.title }}”, is ready.
It will be published on the next weekday morning.</p>
```
A template whose file ends in `.txt` is a text template. Its struct is
named after the file with `Text` added, so `ready.txt` is
`views::report_mailer::ReadyText`, beside `ready.html`'s `Ready`. It has
the same syntax, `{% props %}` and all, but its values go in as they
are, not HTML-escaped, since nothing will read it as HTML:
```text
{% props report: Report %}
Hello,
The report you asked for, "{{ report.title }}", is ready.
It will be published on the next weekday morning.
```
A report titled `Q3 <sales> & costs` shows the difference:
```glacier
test "the ready mail goes to whoever asked for the report" do
report = insert(Report, title: "Q3 <sales> & costs", requested_by: "ada@example.com")
mail = ReportMailer.ready(&report)
assert mail.to == ["ada@example.com"]
assert mail.subject == "Q3 <sales> & costs is ready"
# The HTML part escapes the title; the text part keeps it as it is.
assert mail.html.unwrap() =~ "“Q3 <sales> & costs”"
assert mail.text.unwrap() =~ "\"Q3 <sales> & costs\", is ready."
end
```
A mail's own address, subject and the rest aren't in a template, so
they're never escaped.
### A Mail Layout
`ReportMailer`, [above](#building-a-mail), declares `layout: :mailer`,
which puts each part inside a layout, as `render` does for a page. A mail
layout is a template in `src/views/layouts/` with a `content: Rendered`
prop. The `jobs` app has
one of each kind, `mailer.html`:
```html
{% props content: Rendered %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body style="font-family: sans-serif; line-height: 1.5">
{{ content }}
<p style="color: #666">Reports</p>
</body>
</html>
```
and `mailer.txt`:
```text
{% props content: Rendered %}
{{ content }}
Reports
```
`layout: :mailer` renders the mail's own template and passes it to
`layouts/mailer.html`, or `mailer.txt` for the text part, as `content`.
A mailer without `layout:` sends its templates alone.
Mail is read in mail programs, not browsers, so keep its HTML simple:
styles written inline, as here, and no scripts. A mail's links need the
whole address, with the host, since there's no page they could be
relative to.
## Sending Mail
### `deliver_now`
`deliver_now` sends the mail through the configured delivery and waits
until the service has accepted it, or returned an error. `NotifyReady`
sends the report's mail this way:
```glacier
# Emails whoever asked for a report that it's ready, and records when.
module NotifyReady do
implements Job
alias Mailers.ReportMailer
# Run by the `mailers` queue's own workers (see `src/lib.rs`).
queue: :mailers
# A notice an hour late isn't worth sending.
expires_in 1.hours()
arg report_id: i64
def perform do
report = Report.find(report_id)!
# Reports made before `requested_by` have no one to tell.
if report.requested_by.is_empty() do
return Ok(())
end
# Already in a job, so it sends now rather than queueing another.
ReportMailer.ready(&report).deliver_now()!
report.notified_at = Some(Timestamp.now())
report.save()!
end
end
```
It's already a job, run by the `mailers` queue's workers, which drops
its notice if it hasn't started within the hour. Sending with
`deliver_now` keeps the mail inside that job: if the service refuses it,
the job fails and is [retried](background_jobs.html#failures-and-retries),
and `notified_at` is only set once the mail has gone. If the job fails
after the mail was sent but before `save` returns, the retry sends it
again: jobs run [at least once](background_jobs.html#at-least-once).
In a request, `deliver_now` makes the response wait for the mail
service, which can take a second or more, and an error from the service
becomes the action's error. Use `deliver_later` there instead.
### `deliver_later`
`deliver_later` checks the mail, then queues a job, `glacier_deliver_mail`,
that sends it with `deliver_now` when a worker runs it. It returns the
job's id:
```glacier
test "deliver_later sends the mail from a job" do
report = insert(Report, status: "ready")
ReportMailer.ready(&report).deliver_later()!
jobs = enqueued_jobs()!
assert jobs[0].name == "glacier_deliver_mail"
assert jobs[0].queue == "mailers"
assert delivered_mail().is_empty()
assert perform_jobs()! == 1
assert delivered_mail()[0].subject == "Example title is ready"
end
```
The job holds the whole mail, rendered, in its arguments, so it sends
the mail as it was when it was queued, even if the records it was built
from have changed since.
`Jobs::new()` registers `glacier_deliver_mail` itself, and `jobs::all()`
starts from `Jobs::new()`, so any app that has jobs can deliver later.
An app needs [the jobs table and `App::jobs`](background_jobs.html#registering-jobs)
for it: in an app without them, `deliver_later` fails because the table
is missing.
The job's `QUEUE` is `mailers`. The `jobs` app gives that queue two
workers of its own with `Jobs::queue("mailers", 2)`, in
[`background_jobs`](background_jobs.html#queues-with-their-own-workers),
so mail isn't held up behind a long run of other jobs. Without that,
the app's other workers run it with everything else. The job has the
default five attempts, and isn't unique, so delivering the same mail
twice queues two jobs.
### Inside a Transaction
`deliver_later` inside a `transaction do … end` block is part of that
transaction, as any `enqueue` is: the mail is queued if the transaction
commits, and never sent if it rolls back. So a mail about a record that
was never saved isn't sent:
```glacier
# Queues the mail, then fails, inside one transaction.
def mail_then_fail(report: &Report) -> Result<()> do
transaction do
ReportMailer.ready(report).deliver_later().await?
Report.find(report.id + 1000).await?
end
Ok(())
end
test "a mail queued in a rolled-back transaction is never sent" do
report = insert(Report, status: "ready")
assert mail_then_fail(&report).is_err()
assert enqueued_jobs()!.is_empty()
perform_jobs()!
assert delivered_mail().is_empty()
end
```
`deliver_now` inside a transaction sends the mail at once, and can't
take it back if the transaction then rolls back.
## Configuration
### `[mail]`
Mail is configured in the `[mail]` table of the app's config, read by
`App::config` when the app starts. `from` is the sender of every mail
that doesn't set its own, and belongs in `config/default.toml`:
```toml
[mail]
# The sender of every mail that doesn't set its own.
from = "Reports <reports@example.com>"
```
`delivery` says where mail goes. Each environment has a default, so
most apps set it only for production:
| `"inbox"` | Keeps the last 100 mails in memory, for [`/dev/mail`](#the-inbox); sends nothing | development |
| `"test"` | Keeps mail for `delivered_mail()` (see [Testing](#testing-mail)) | test |
| `"smtp"` | Sends by SMTP, from `[mail.smtp]` | |
| `"sendgrid"` | Sends through SendGrid's API, from `[mail.sendgrid]` | |
| `"postmark"` | Sends through Postmark's API, from `[mail.postmark]` | |
| `"none"` | Drops every mail, logging that it wasn't sent | |
Production has no default. Without a `delivery`, every `deliver_now`
in production returns an error, ``can't send `…`: no mail delivery is
configured; set `mail.delivery` ``, and every mail queued with
`deliver_later` fails in its job and is retried, so nothing is sent
until a delivery is configured. Any other value of `delivery` stops the
app when it starts, as does a delivery missing a setting it needs, such
as `mail.smtp.host`.
### SMTP
The `jobs` app sends by SMTP in production, from
`config/production.toml`:
```toml
[mail]
delivery = "smtp"
[mail.smtp]
host = "smtp.example.com"
username = "reports"
# Port 587 and STARTTLS unless set. The password comes from the
# environment: GLACIER__MAIL__SMTP__PASSWORD=…
```
`[mail.smtp]` takes:
| `host` | The server; required |
| `port` | The port; the default depends on `tls` |
| `username`, `password` | Credentials, if the server needs them |
| `tls` | `"starttls"`, `"tls"` or `"none"` |
`tls` says how the connection is secured:
- **`"starttls"`**, the default, connects in plain text and upgrades to
TLS before anything else is sent, on port 587 unless `port` is set. It
refuses to send if the server can't upgrade.
- **`"tls"`** connects with TLS from the start, on port 465 unless set.
- **`"none"`** never uses TLS, on port 25 unless set. It's for a relay on
the same machine or network, or a local mail catcher such as Mailpit
(`port = 1025`), never for a server across the internet.
### Secrets
Passwords and API keys don't belong in the config files, which are
committed with the app. Set them through environment variables, which
override the files: each `__` in the name is a dot in the key.
| `mail.smtp.password` | `GLACIER__MAIL__SMTP__PASSWORD` |
| `mail.sendgrid.api_key` | `GLACIER__MAIL__SENDGRID__API_KEY` |
| `mail.postmark.server_token` | `GLACIER__MAIL__POSTMARK__SERVER_TOKEN` |
Any key can be set this way, `GLACIER__MAIL__DELIVERY=none` included,
which is useful for a staging server that mustn't send mail.
WARNING: A variable's value that reads as a number or a boolean is
stored as one, and a key that expects text then refuses it: a password
of `12345678` set through `GLACIER__MAIL__SMTP__PASSWORD` stops the app
when it starts with ``config key `mail.smtp.password`: invalid type:
integer `12345678`, expected a string``.
### SendGrid and Postmark
The two email APIs are configured the same way, with `delivery` and a
table of their own. SendGrid's, `[mail.sendgrid]`, takes `api_key`.
Postmark's, `[mail.postmark]`, takes `server_token`, and `stream`, the
message stream to send through, which is `outbound` unless set. Each
also takes `endpoint`, the address the request goes to, for pointing it
at a stand-in.
Each mail is one HTTPS request to the service. A response other than
success is an error that includes the status and the first 500
characters of what the service said, so a refused mail's job records why
in its `last_error`.
## Development
### Previews
A preview shows a mail without sending it, built from records already in
the development database. A mailer's `preview :name do … end` builds one,
its last value the mail, and the app shows every mailer's previews by
itself:
```glacier
# At /dev/mail in development: the newest report, or a sample one
# before there are any.
preview :ready do
report = case Report.last()! do
Some(report) -> report
nil -> Report.new(title: "Q3 sales", requested_by: "ada@example.com")
end
ReportMailer.ready(&report)
end
```
A preview is named after its mailer and mail, as `report_mailer/ready`
is. It runs each time the preview is shown, so a preview always shows
the templates as they are now, and `!` waits on the database as in an
action. This one uses the newest report, and a sample report when there
are none, so it works on a new database too.
`mail_previews()` is the app's previews, every mailer's, so a test can
check that each preview builds:
```glacier
test "the ready mail's preview builds" do
previews = jobs_app::mail_previews()
# No reports yet, so it uses its sample.
mail = previews.build("report_mailer/ready")!
assert mail.subject == "Q3 sales is ready"
end
```
### The Inbox
In development, `/dev/mail` lists the app's previews, then the inbox:
the mail the app has sent, newest first, with when it was sent, who to,
and its subject. With `glacier server` running, creating a report with
an email address puts its `ready` mail in the inbox about a second
later, once `GenerateReport` and `NotifyReady` have run, and logs it:
```text
2026-10-05T00:53:32.386669Z INFO glacier::mail: kept in the development inbox: read it at /dev/mail to=bo@example.com subject=Report A is ready
2026-10-05T00:53:32.386724Z INFO glacier::mail: delivered
```
Clicking a preview or a mail shows its sender, recipients, other
headers and attachments, then its HTML part in a frame and its text part
beneath. Each part can be opened on its own by adding `.html` or `.txt`
to the page's address: `/dev/mail/previews/report_mailer/ready.txt`
is the text part alone, as plain text.
The inbox lives in the server's memory. It keeps the last 100 mails,
and is emptied whenever the server restarts, which `glacier server` does
after each rebuild.
These pages are served only in development, and only for paths the app
doesn't route itself: they're tried after the app's routes, and aren't
in its route table, so they don't appear in `glacier routes` and an
app's own `/dev/mail` route would win. In test and production, `/dev/mail`
is a 404 like any other unknown path. The app's middleware runs for them,
but none of its pipelines does, so a check that a pipeline makes, such as
requiring a signed-in user, doesn't apply: don't let others reach a
server running in development.
## Testing Mail
### The Test Mailbox
Each `#[glacier::test]`, which every `.glacier` test is, has a mailbox
of its own. Inside a test, mail always goes there, whatever `[mail]`
configures, so a test never sends real mail. `delivered_mail()` returns
what the test has sent, oldest first, as `Mail` values, so a test can
check any field:
```glacier
test "a generated report's notice is emailed" do
report = insert(Report, status: "pending", requested_by: "ada@example.com")
GenerateReport { report_id: report.id }.enqueue()!
assert delivered_mail().is_empty()
# Generating it queues NotifyReady, which sends the mail.
perform_jobs()!
mail = delivered_mail()
assert mail.len() == 1
assert mail[0].to == ["ada@example.com"]
assert mail[0].from == Some("Reports <reports@example.com>".to_owned())
assert mail[0].subject == "Example title is ready"
assert mail[0].text.clone().unwrap() =~ "\nReports\n"
end
```
Mail queued with `deliver_later` is in the mailbox only once
`perform_jobs()` has run its job, as the
[`deliver_later` test](#deliver-later) above shows. Mail from a job that
never ran isn't there either, such as a notice that expired:
```glacier
test "an expired notice sends nothing" do
freeze_time()
report = insert(Report, status: "ready")
NotifyReady { report_id: report.id }.enqueue()!
travel(2.hours())
perform_jobs()!
assert delivered_mail().is_empty()
end
```
`clear_delivered_mail()` empties the mailbox, for a test that checks the
mail from one step at a time:
```glacier
test "clear_delivered_mail forgets what was sent" do
report = insert(Report, status: "ready")
ReportMailer.ready(&report).deliver_now()!
clear_delivered_mail()
assert delivered_mail().is_empty()
end
```
Both are in `glacier::testing`, and a `.glacier` test can call them
without the path, as it does `perform_jobs`. Test files don't see the
app's mailers as they see its models and jobs, so a test that calls one
needs an `alias`:
```glacier
alias Mailers.ReportMailer
```
Because the mailbox belongs to the test, tests that run at the same time
don't see one another's mail.
```console
$ glacier test mailers::
running 8 tests
test mailers::report_mailer::the_ready_mail_s_preview_builds ... ok
test mailers::report_mailer::the_ready_mail_goes_to_whoever_asked_for_the_report ... ok
test mailers::report_mailer::a_mail_can_carry_attachments_and_headers_of_its_own ... ok
test mailers::report_mailer::clear_delivered_mail_forgets_what_was_sent ... ok
test mailers::report_mailer::deliver_later_sends_the_mail_from_a_job ... ok
test mailers::report_mailer::a_mail_queued_in_a_rolled_back_transaction_is_never_sent ... ok
test mailers::report_mailer::an_expired_notice_sends_nothing ... ok
test mailers::report_mailer::a_generated_report_s_notice_is_emailed ... ok
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 28 filtered out; finished in 0.03s
```
### Outside `#[glacier::test]`
Code that isn't in a `#[glacier::test]`, such as a plain `#[tokio::test]`,
sends mail through the configured delivery, which is the test mailbox in
the test environment unless `[mail]` says otherwise. That mailbox is
shared by everything in the process outside a `#[glacier::test]`, and
`delivered_mail()` there reads the shared one, so such tests can see one
another's mail when they run at the same time.
## Attachments and Headers
`attach` takes a file name, a content type and the file's bytes. `cc`,
`bcc` and `reply_to` add recipients and a reply address, and `header`
adds any other header, such as `List-Unsubscribe`. Since each returns
the mail, they can be added to a mail a mailer built:
```glacier
test "a mail can carry attachments and headers of its own" do
report = insert(Report, status: "ready")
csv = format!("title,status\n{},{}\n", report.title, report.status)
mail = ReportMailer.ready(&report).cc("team@example.com")
mail = mail.reply_to("Reports team <team@example.com>")
mail = mail.attach("report.csv", "text/csv", csv.into_bytes())
mail = mail.header("List-Unsubscribe", "<mailto:unsubscribe@example.com>")
mail.deliver_now()!
sent = delivered_mail()
assert sent[0].cc == ["team@example.com"]
assert sent[0].attachments[0].filename == "report.csv"
assert sent[0].attachments[0].data == b"title,status\nExample title,ready\n"
assert sent[0].headers[0].0 == "List-Unsubscribe"
end
```
`attach_inline` attaches an image the HTML part shows in place, rather
than as a separate download: `attach_inline("logo.png", "image/png",
bytes)` is shown by `<img src="cid:logo.png">` in the template.
An attachment's bytes are part of the mail, so with `deliver_later`
they're stored in the job's arguments, as base64 text, until the mail is
sent. Keep attachments small, or send a link instead.
Headers are sent as given. A header that SMTP, SendGrid or Postmark sets
itself, such as `Subject`, belongs in the method for it, not in
`header`.
## Writing a Delivery
A delivery is anything that implements `glacier::mail::Delivery`: a
`name` for the logs, and `deliver`, which sends one mail and returns a
boxed future. This one, in a test of the `jobs` app, sends every mail to
one address instead of its recipients, through another delivery:
```rust
/// Sends every mail to one address instead of its own recipients, through
/// another delivery: for a staging server, which mustn't mail real people.
pub struct RedirectTo<D> {
pub address: String,
pub then: D,
}
impl<D: Delivery> Delivery for RedirectTo<D> {
fn name(&self) -> &'static str {
"redirect"
}
fn deliver<'a>(
&'a self,
mail: &'a Mail,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
let mut redirected = mail.clone();
redirected.subject = format!("[to {}] {}", mail.to.join(", "), mail.subject);
redirected.to = vec![self.address.clone()];
redirected.cc.clear();
redirected.bcc.clear();
Box::pin(async move { self.then.deliver(&redirected).await })
}
}
```
`App::mail_delivery` makes the app send through it, in place of what
`[mail]` configures:
```rust
let _app = jobs_app::app(config, repo)
.unwrap()
.mail_delivery(RedirectTo {
address: "team@example.com".to_owned(),
// Outside a `#[glacier::test]`, the test mailbox is shared.
then: TestMailbox,
});
```
`mail_delivery` keeps `mail.from` from the config. Call it after
`App::config`: `config` reads `[mail]` again, and replaces a delivery
set before it. The delivery is the process's, not the app's, so the
last one set is used by every app in the process.
`TestMailbox`, `Inbox`, `Smtp`, `SendGrid` and `Postmark` are all public
in `glacier::mail`, so a delivery of your own can pass mail on to one,
as `RedirectTo` does here. `Smtp::from_config`, `SendGrid::from_config`
and `Postmark::from_config` build one from a config.
A delivery set this way is still bypassed inside a `#[glacier::test]`,
whose mail always goes to the test's mailbox. That's why this test is a
plain `#[tokio::test]`, in a test file of its own, so that its delivery
and the shared mailbox it reads affect no other test.
## Logs and Traces
Mail logs with the target `glacier::mail`:
- **`delivered`**, at info, each time a mail is sent, inside the
delivery's span (below).
- What the inbox kept, at info, with the recipients and subject, in
development, as [above](#the-inbox).
- **`not sent: mail.delivery is none`**, at info, for each mail dropped by
`delivery = "none"`.
A mail that fails to send isn't logged by the mail itself. In a job, the
job's failure is logged as a warning, as for any job, with the error.
Each `deliver_now` is a `tracing` span named `glacier.mail`, with the
fields `mail.to`, `mail.subject` and `mail.delivery`. Logs leave spans
out, so the `delivered` line doesn't show which mail it was; the span
reaches a tracing backend through a layer registered with
`App::telemetry`.
## What Isn't Built Yet
Glacier's mail doesn't yet have:
- **Receiving mail**: there's no way to take in mail sent to the app.
- **URL helpers with a host** for links in mail: build the whole address
yourself.
- **Translating mail**: pick the template or subject yourself, with `t`
and the recipient's locale.
- **Mail callbacks or interceptors**, other than a delivery of your own
that changes the mail before passing it on.
- **Tracking delivery**: once the service has accepted a mail, Glacier
doesn't hear whether it arrived, bounced or was opened.
- **A preview's arguments from the address**: a preview builds its mail
from what its closure looks up.