# Email

[Send an email](/guides/guide-send-an-email.md) does the whole job, start to finish.

Most apps send a handful of emails: confirm your address, an invoice, a
welcome. Each is a type, `type X = EmailMessage`, whose fields are the
values its text uses. It lives in its domain's `emails/` folder, beside a
template named after it, and an action sends it by emitting it.

Here is the smallest one: a plain-text welcome, in one language, sent when
someone signs up. `EmailMessage`, `Email`, `Locale`, `Driver.smtp` and `emit` are
built into Jadpo. Everything else is your app's, shown here, and each `use` line
imports from another file of it.

The email's type, `domains/accounts/emails/welcome_email.jadpo`:

```jadpo
/// Sent once a user has signed up.
type WelcomeEmail = EmailMessage {
    display_name: Text
}
```

Its text, `domains/accounts/emails/welcome_email.txt`:

```jadpo
Hello {{ display_name }}, thanks for signing up.
```

Its subject line is its entry in the domain's language file, with its fields as
placeholders ([Email subjects](/docs/email-subjects.md)).

`domains/accounts/translations/en_gb.jadpo`:

```jadpo
translations {
    welcome_email: "Welcome, {display_name}"
}
```

The record the action keeps, `domains/accounts/entities/user.jadpo`:

```jadpo
/// Someone with an account.
entity User {
    id: Uuid
    email: Email
    display_name: Text {
        min_length: 1
        max_length: 80
    }

    policy: {
        create: [Access.public]
        read: [Access.public]
    }
}
```

The action, `domains/accounts/actions/sign_up.jadpo`, emits the email, which goes
once the action commits ([Sending](/docs/email.md#sending)):

```jadpo
use accounts/entities { User }
use accounts/emails { WelcomeEmail }

/// Signs someone up, and welcomes them once it is done.
action sign_up(email: User.email, display_name: User.display_name) -> User {
    var user: User = User.create {
        set: {
            email: email
            display_name: display_name
        }
    }
    emit WelcomeEmail {
        to: user.email
        locale: Locale.en_gb
        display_name: user.display_name
    }
    return user
}
```

`app.jadpo` says how mail leaves, here over SMTP, and which languages the app
has:

```jadpo
application {
    connections: {
        mail: {
            driver: Driver.smtp
            host: config.smtp_host
        }
    }
    email: {
        connection: mail
        from: config.mail_sender
    }
}

locales {
    default: Locale.en_gb
    supported: [Locale.en_gb]
}
```

`config.jadpo`:

```jadpo
config {
    mail_sender: Email {
        default: "hello@notes.example"
    }
    smtp_host: Hostname
}
```

A test catches the mail instead of sending it, and reads it from `email.sent`.

`domains/accounts/actions/sign_up.test.jadpo`:

```jadpo
use accounts/entities { User }

test "signing up sends a welcome" {
    var user: User = sign_up("ada@notes.example", "Ada")
    assert user.display_name == "Ada"
    var sent: List<SentEmail> = email.sent
    assert list.any(sent, item -> item.to == "ada@notes.example" and item.subject == "Welcome, Ada")
}
```

On a first run, `jadpo migrate new initial` writes the migration and `jadpo
records update` the records, as `jadpo check`'s `next:` lines say. Commit what
they write.

| Page | What it covers |
|---|---|
| [Email rules: templates and sending](/docs/email-templates-and-sending.md) | An email's templates are checked when you compile and rendered when the email is sent. |
| [Email subjects](/docs/email-subjects.md) | An email's subject line is translated like a message. |
| [Built-in emails](/docs/built-in-emails.md) | Jadpo sends some emails itself: the magic-link email (Magic links) and the privacy emails (Data requests). |
| [The dev inbox and the health score](/docs/the-dev-inbox-and-the-health-score.md) ▲ partly | `jadpo dev` keeps every email your app sends in a local inbox, and scores each one for the common mistakes that send mail to spam. |
| [The SMTP driver](/docs/the-smtp-driver.md) | The SMTP driver sends your app's email through a mail server you name. |

## HTML, a link and translated words

This adds three things to the email above: a `sign_in_link` field, an `.html`
template beside the `.txt`, and words from the domain's translations
([Translations as messages](/docs/translations-as-messages.md)).
A template calls `{{ translations.<domain>.<message>(...) }}`, which
takes the email's locale, and the compiler checks every call and
placeholder against the message types and the email's fields.

`User` and `app.jadpo` stay as they are. `config.jadpo` gains one
setting, `app_url: Url`, for the link.

`domains/accounts/emails/welcome_email.jadpo`, with `sign_in_link` new:

```jadpo
/// Sent once a user has signed up.
type WelcomeEmail = EmailMessage {
    display_name: Text
    sign_in_link: Url
}
```

Each thing a template says is one of the domain's messages, a type of your app.

`domains/accounts/translations/welcome_heading.jadpo`:

```jadpo
/// The heading of the welcome email.
type WelcomeHeading = Message {
    name: Text
}
```

`domains/accounts/translations/sign_in.jadpo`:

```jadpo
/// The welcome email's link to sign in.
type SignIn = Message {}
```

The domain's language file words them, with `welcome_email` still the subject
line.

`domains/accounts/translations/en_gb.jadpo`:

```jadpo
translations {
    sign_in: "Sign in"
    welcome_email: "Welcome, {display_name}"  // the subject line
    welcome_heading: "Welcome, {name}"
}
```

`domains/accounts/emails/welcome_email.html`, beside the type:

```jadpo
<html dir="{{ locale.direction }}">
<h1>{{ translations.accounts.welcome_heading(name: display_name) }}</h1>
<a href="{{ sign_in_link }}">{{ translations.accounts.sign_in() }}</a>
</html>
```

`domains/accounts/emails/welcome_email.txt`, replaced:

```jadpo
{{ translations.accounts.welcome_heading(name: display_name) }}

{{ translations.accounts.sign_in() }}: {{ sign_in_link }}
```

## Sending

You send an email by emitting it. It goes after the action commits, and
is retried like any delivery
([Delivery: at least once, safe to repeat](/docs/delivery-at-least-once-safe-to-repeat.md)),
so a sign-up that is rolled back never sends a welcome. Every
`EmailMessage` has `to:` and `locale:`.

`domains/accounts/actions/sign_up.jadpo`, with `sign_in_link` new:

```jadpo
use accounts/entities { User }
use accounts/emails { WelcomeEmail }

/// Signs someone up, and welcomes them once it is done.
action sign_up(email: User.email, display_name: User.display_name) -> User {
    var user: User = User.create {
        set: {
            email: email
            display_name: display_name
        }
    }
    emit WelcomeEmail {
        to: user.email
        locale: Locale.en_gb
        display_name: user.display_name
        sign_in_link: config.app_url
    }
    return user
}
```

The email is rendered whole when it is sent. Each template is run with
the email's fields and wrapped in the shared layout
([Built-in emails](/docs/built-in-emails.md), level 3), the subject is worded in
the email's locale, and each `File` field is read from its store as an
attachment. The connection is handed that message, and the delivery's
record alone says it went.

## Email in tests

Tests catch mail instead of sending it. `jadpo test` catches every
email, rendered whole once its action commits, so a template that can't
be rendered fails the test. A test reads what it has sent so far as
`email.sent`, a `List<SentEmail>`. Each has its `to`, `from`, `subject`,
`html` and `text` (`none` without that template), every link in it
(`links`), its attachments' file names (`attachments`), and the sign-in
email's token and code (`sign_in_token`, `sign_in_code`). Only a test
can read `email.sent` (`email.sent_context`). The mail is caught whole
whichever connection `email:` names, a mail package's included.

This test signs someone up and checks the welcome email.

`domains/accounts/actions/sign_up.test.jadpo`:

```jadpo
use accounts/entities { User }

test "signing up welcomes the new user, with a link to sign in" {
    var user: User = sign_up("ada@notes.example", "Ada")
    assert user.display_name == "Ada"
    var sent: List<SentEmail> = email.sent
    assert list.count(sent) == 1
    assert list.any(sent, item -> item.to == "ada@notes.example" and item.subject == "Welcome, Ada")
    assert list.all(sent, item -> list.contains(item.links, "https://notes.example"))
}
```

## Where it goes: `email:`

Mail goes through the `email` capability
([Capabilities](/docs/capabilities.md)).
`email: { connection: postmark  from: config.mail_sender }` in
`application { }` names a connection, and every email goes through it,
your app's and Jadpo's own ([Built-in emails](/docs/built-in-emails.md)).

- **SMTP** is a built-in driver, `driver: Driver.smtp`
  ([The SMTP driver](/docs/the-smtp-driver.md)). On Cloudflare Workers it uses
  port 587, since Workers blocks port 25.
- **A mail package** is the other way. Resend's is
  [one of Jadpo's own](/docs/where-packages-come-from.md#jadpos-own-packages), and for another
  mail service you write one.

The `EmailMessage` types, the translations and `emit` are the same
whichever you choose, so changing provider changes only `app.jadpo`.

## Email in development: the dev inbox

`jadpo dev` catches every email in a local inbox, so nothing leaves your
machine ([The dev inbox and the health score](/docs/the-dev-inbox-and-the-health-score.md)).
You open it in the browser, on a page `jadpo dev` serves beside the app.
Each mail is shown as HTML and as text, in its locale, with its links
working: a magic link opens the frontend's page, which signs in locally.
Each mail also names the trace of the request that sent it.

An agent uses the same inbox from the command line, with
`jadpo email list`, `jadpo email show <id> --json`,
`jadpo email links <id>` and `jadpo email open <id> <link>`, so it can
follow a sign-up or a privacy request from start to finish.
`jadpo email clear` empties the inbox.

## Templates

An email has an `.html` template, a `.txt` one or both, named after its
type. An email with neither is `email.template_missing`, and so is a
template that no email type is named after.

Inside `{{ }}` a template writes Jadpo's own expressions:

- its email's fields, `to` and `locale` among them;
- `{{ if (...) }} ... {{ else if (...) }} ... {{ else }} ... {{ end }}`
  on a `Bool`;
- `{{ for item in list }} ... {{ end }}`;
- translations and `text.format`, both written in the email's locale
  without a `locale:`;
- `{{ locale.direction }}`, the locale's text direction in Jadpo's
  wording, `ltr` or `rtl`, for a `dir=` attribute.

## What a template may not do

Templates are strict. Each of these is a compile error:

- a name that isn't a field of the email's type;
- a message that doesn't exist, or a placeholder that is missing or of
  the wrong type;
- a partial that doesn't exist or is given the wrong fields;
- `if` on something that isn't a `Bool`, and `for` over something that
  isn't a list;
- any call other than translations and formatting, and arithmetic or
  building a value (`email.template_not_allowed`).

A field that no template reads, and no wording of the subject writes, is
`email.field_unused`. It is a judgement call, so a `decline` line above
the field answers it
([Declining a judgement call](/docs/declining-a-judgement-call.md)).

## What a template writes out

Only text and numbers are written out, in templates as in placeholders.
`{{ value }}` takes `Text`, `PresentationText`, `Int` or `Decimal`, with
numbers written the locale's way, or an enum whose values have wording
([Enums with wording](/docs/enums-with-wording.md)).
Writing anything else out is a compile error that says how to format it
(`email.template_value_invalid`):

- an `Instant` or `Money`, with `text.format`
  ([What every standard type can do](/docs/every-standard-type-meets-one-contract.md));
- a `Bool`, as a condition for `if`;
- a list, walked with `for`;
- an object, by writing its fields one by one;
- an enum without wording, by adding wording.

Values are escaped. HTML goes in unescaped only as the safe-HTML type
`Html` ([Text formats](/docs/text-formats.md)), which is
safe because of how it is built.

## Partials: pieces several emails share

A partial is a piece several emails share, such as a button or a
signature. You declare it as `type Name = EmailPartial { ... }`, with its
`.html` and `.txt` beside it as an email's are:

- in `shared/emails/`, where every domain's emails may include it;
- or in a domain's own `emails/`, where only that domain's emails may.

A template includes one as `{{ include name(field: value, ...) }}`,
giving each of its fields once, by name. An HTML template includes the
partial's `.html`, and a text one its `.txt`. The partial is written in
the locale of the template that includes it, which it gets as its
`locale`. Its templates follow the same strict rules, and may include
other partials, but never lead back to themselves. A partial that
doesn't exist, can't be reached, lacks the template it is included from,
or is given the wrong fields is `email.partial_invalid`.

Here is a button for the welcome email above, whose other files stay as they
are.

`shared/emails/button.jadpo`:

```jadpo
/// A link set as a button, in every email that asks the reader to act.
type Button = EmailPartial {
    label: PresentationText
    url: Url
}
```

`shared/emails/button.html`:

```jadpo
<a href="{{ url }}" style="display:inline-block;padding:12px 24px;background-color:#1f2937;color:#ffffff;text-decoration:none;">{{ label }}</a>
```

`shared/emails/button.txt`:

```jadpo
{{ label }}: {{ url }}
```

`domains/accounts/emails/welcome_email.html`, with its link replaced by the
button:

```jadpo
<html dir="{{ locale.direction }}">
<h1>{{ translations.accounts.welcome_heading(name: display_name) }}</h1>
{{ include button(label: translations.accounts.sign_in(), url: sign_in_link) }}
</html>
```

`domains/accounts/emails/welcome_email.txt`, the same:

```jadpo
{{ translations.accounts.welcome_heading(name: display_name) }}

{{ include button(label: translations.accounts.sign_in(), url: sign_in_link) }}
```

## Attachments

A `File` field on an `EmailMessage` is an attachment, and `File?` is one
when it holds a file. It is read from
its store when the email is sent, and checked against the size limit the
mail package declares. Through a mail package, each `File` field needs a
`max_size:`, and an email whose fields' sizes add up to more than the
package's `limits: { attachments: }` is `email.attachment_limit`. A
provider that can't send attachments can still be an ordinary package,
but can't be the `email:` connection.

An attachment goes out under the file's own name. A file with no name, as
every file `store.put` makes is, is named from the email's field and the
file's type: `invoice: File` holding a PDF goes out as `invoice.pdf`. A
`FileType.json_lines` file ends in `.jsonl`. No part of the file's key goes
out.

Each attachment is one `File` field. Sending several from one field isn't
built: a `List<File>`, a `Set<File>` or a `Map` of them, and a `FileContents`,
on an email or a partial are `toolchain.not_built_yet`, since each would
compile and never be sent. So is a `File` on a partial, which sends nothing of
its own. An object or an `ExportSummary` on an email is read for its other
fields, and a file it holds isn't attached.

## The mail health score

Every mail in the dev inbox, and `jadpo email show <id>`, shows what
Jadpo's own checks found and how to fix each problem: no plain-text
part, content that is only images, broken HTML, links that aren't
absolute HTTPS, link shorteners, a subject in capitals, and more than
102 KB, where Gmail clips a mail. `jadpo check` reports templates that would fail as
[`email.health_problem`](/docs/errors/email.health_problem.md), an error
you can decline
([Declining a judgement call](/docs/declining-a-judgement-call.md)).
When SpamAssassin is installed, its open rules are added
([SpamAssassin in the health score](/docs/spamassassin-in-the-health-score.md)).
`jadpo email check-domain` compares the sending domain's SPF, DKIM and
DMARC records with what the mail connection needs.

The score is an early indication, not a guarantee, and every screen that
shows one says so. It can't predict whether Gmail, Outlook or any other
provider puts a mail in the inbox, because they use their own filters
and the sender's history, which change constantly. A clean score means
the common mistakes are absent, nothing more, so don't rely on it.
Testing where mail really lands needs seed-account services outside
Jadpo.

## What belongs to the mail package

Everything specific to one provider is the package's own:

- Per-send values, such as Postmark's message streams or SES
  configuration sets, are typed settings on the connection.
- Hosted templates with variables
  (`postmark.email.send_with_template(...)`), suppression lists and
  marketing sends are the package's operations. You call them from an
  action or subscriber in a domain the connection lists. One connection
  serves both: `email:` uses it, and the listed domains call it. Mail
  sent from a hosted template gets no dev inbox entry, no health score
  and no automatic template check, though your app can add a contract
  case.
- Bounces and complaints are the package's own webhook events, which you
  subscribe to like any package event. There are no standard
  `EmailBounced` and `EmailComplained` events, so moving to another
  provider means rewriting the bounce subscribers.

## A send whose outcome is unknown

Postmark, SES, SendGrid and Mailgun take no idempotency key, so a send
that may or may not have gone out can't safely be repeated. Such a send
is parked for an operator and never resent blindly, since a duplicate
receipt isn't harmless
([Retries and giving up](/docs/retries-and-giving-up.md)).
`jadpo deliveries resolve` settles one once the provider shows whether it
went.

## Email and personal data

The personal-data report lists the mail connection as receiving every
marked field a mail carries. A mail package that declares an `erasure:`
operation is called when a person is erased
([Erasure at providers](/docs/erasure-at-providers.md)).

## Details

The exact rules, for when you hit an edge case.

| Page | What it covers |
|---|---|
| [Mail packages, partials and email.sent](/docs/mail-packages-partials-and-email-sent.md) | Partials, `email.sent` in tests and a mail package that fills the `email` capability each have rules of their own. |
| [Jadpo's own magic-link email](/docs/default-magic-link-email.md) | With nothing written, Jadpo sends its own magic-link email, at level 1 of Built-in emails. |
| [Your own magic-link email](/docs/custom-magic-link-email.md) | Your app can send its own magic-link email instead of Jadpo's, at level 4 or level 5 of Built-in emails. |
| [SpamAssassin in the health score](/docs/spamassassin-in-the-health-score.md) | When SpamAssassin is installed on your machine, `jadpo dev` adds its rules to each mail's health score. |
