glacier-web-cli 0.5.0

Command-line tool for creating and running Glacier applications.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
---
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:

| Argument | What |
| --- | --- |
| `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, &ldquo;{{ report.title }}&rdquo;, 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() =~ "&ldquo;Q3 &lt;sales&gt; &amp; costs&rdquo;"
  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:

| `delivery` | What | Default in |
| --- | --- | --- |
| `"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:

| Key | What |
| --- | --- |
| `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.

| Key | Variable |
| --- | --- |
| `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.