# Built-in emails

Jadpo sends some emails itself: the magic-link email
([Magic links](magic-links.md)) and the privacy emails
([Data requests](data-requests.md)). Each works with
nothing written, and your app can change as much of it as it needs, in
five levels. Each level keeps the ones below it.

Every email, built in or your app's own, goes through your app's one
`email:` setting in `application { }`. It names a connection that
provides `email`: the built-in [SMTP driver](the-smtp-driver.md) or any
package that provides it.

```jadpo
// app.jadpo
application {
    connections: {
        postmark: {
            package: Postmark.v1
            credential: config.postmark_token
        }
    }
    email: {
        connection: postmark
        from: config.mail_sender
    }
}
```

`magic_link:` with no `email:` setting is a compile error, unless it
sends its own email (level 5).

## Level 1: nothing

Jadpo renders its own email, HTML and text, in the person's language,
with Jadpo's wording ([Jadpo's own magic-link email](default-magic-link-email.md)).
Jadpo ships its wording in `en_gb` and `en_us`, and releases add
locales. If your app supports a locale Jadpo doesn't ship, you supply
the wording (level 2). Until you do, `jadpo check` fails with one error
listing every Jadpo message the locale is missing, unless the locale is
`in_progress:`, which falls back as any translation does
([Languages](translations-as-messages.md#languages)).

## Level 2: change the words

You write Jadpo's messages in the root `translations/<locale>.jadpo`,
under `jadpo: { }`
([Jadpo's and packages' text](failures-for-client-apps.md#jadpos-and-packages-text)):

```jadpo
// translations/en_gb.jadpo
translations {
    jadpo: {
        magic_link_email: "Your link to sign in to {app_name}"
        magic_link_intro: "Someone asked to sign in to {app_name} as {address}."
        sign_in_codes_disabled: "Too many wrong codes today. Use the link in your email instead."
    }
}
```

You can reword only the messages of features your app has switched on.
Magic-link wording without `magic_link:` is an error saying the feature
is off. `jadpo translations builtin` lists the messages of the features
your app has on, with their fields. Jadpo shows no pages, so its wording
is for emails and failures only.

Every Jadpo message may also use `{app_name}`, the application's
`display_name:` ([The application's name](built-in-emails.md#the-applications-name)). An
app that needs the name translated rewords the messages.

The magic-link email uses these messages:

| Message | Fields | `en_gb` wording |
|---|---|---|
| `magic_link_email` (the subject) | | "Sign in to {app_name}" |
| `magic_link_heading` | | "Sign in" |
| `magic_link_intro` | `address: Email` | "Someone asked to sign in as {address}." |
| `magic_link_button` | | "Sign in" |
| `magic_link_code_line` | `code: SignInCode` | "Or enter this code: {code}" |
| `magic_link_expiry_line` | `expires_in: PresentationText` | "The link and the code work for {expires_in}." |
| `magic_link_ignore_line` | | "If you didn't ask to sign in, you can ignore this email." |
| `sign_in_link_expired` | | "This link has expired or has been used. Ask for a new one." |
| `sign_in_link_other_browser` | | "Open this link in the browser where you asked for it, or enter the code instead." |
| `sign_in_code_invalid` | | "That code isn't right. Check it, or ask for a new one." |
| `sign_in_codes_disabled` | | "Too many wrong codes today. Use the link in your email instead." |
| `sign_in_refused` | | "We couldn't sign you in." |
| `sign_in_return_invalid` (only with `generate_routes: false`, [Signing in from code the app writes](signing-in-from-code-the-app-writes.md)) | | "We can't send you back there after signing in. Start again from this site." |

`expires_in` is `expires_after:` as Jadpo formats it in the email's
language: "15 minutes". The failures' wording also goes into the
per-locale failure export for client apps
([Failures for client apps](failures-for-client-apps.md)).

These twelve messages are in Jadpo's catalogue, with the wording above.
They are on once you declare `magic_link:`. Then they are listed,
required in your app's other locales and rewordable, the email is
written with them, and the failures' wording is in the per-locale
failure export.

[Passkeys](passkeys.md) bring no email, only the wording of
their failures, which a client app shows. These messages are on, listed,
required in your app's other locales and rewordable once you declare
`passkey:`:

| Message | `en_gb` wording |
|---|---|
| `passkey_challenge_expired` | "That took too long. Try your passkey again." |
| `passkey_rejected` | "We couldn't sign you in with that passkey. Try another passkey, or sign in with your email." |
| `passkey_already_registered` | "This passkey is already set up." |
| `passkey_limit_reached` | "You have as many passkeys as an account can hold. Remove one to add another." |
| `passkey_not_found` | "That passkey isn't on your account." |

## Level 3: branding

Every email, built in or your app's own, is wrapped in your app's shared
email layout, so built-in emails carry your branding with no other
change. The layout is optional, and an app has at most one, in
`shared/emails/layout.html` and `shared/emails/layout.txt`. Without it,
Jadpo's plain layout is used. The layout is checked under the same strict
rules as [templates](email.md#what-a-template-may-not-do), may call translations,
and receives:

| In the layout | What it is |
|---|---|
| `{{ content }}` | The rendered body: the safe-HTML type in `layout.html`, text in `layout.txt` |
| `{{ subject }}` | The email's subject line, `PresentationText` |
| `{{ locale.direction }}` | `ltr` or `rtl` ([Locales](locales.md)) |
| `{{ app.display_name }}` | The application's `display_name:` |

```jadpo
<html dir="{{ locale.direction }}">
<head><title>{{ subject }}</title></head>
<body>
  <header>{{ app.display_name }}</header>
  {{ content }}
</body>
</html>
```

The layout wraps each of your app's emails: its `.html` in `layout.html`
and its `.txt` in `layout.txt`. You can keep either file without the
other. `{{ app.display_name }}` without a `display_name:` is
`email.settings_invalid`.

### The application's name

`application { display_name: "Acme" }` in `app.jadpo`, plain text, is
required once any built-in email is switched on, such as magic links or
the privacy emails. Without it, `jadpo check` names the feature that
needs it. Templates read it as `{{ app.display_name }}`, and Jadpo's own
wording as `{app_name}`.

## Level 4: your own email

`magic_link: { email: accounts.SignInEmail }` names an `EmailMessage`
type of your app's, which Jadpo fills and sends instead of its own. The
type takes its fields from the built-in `MagicLinkEmail` with `pick:`,
and may have no others, since Jadpo fills it:

| `MagicLinkEmail` field | Type | Holds |
|---|---|---|
| `address` | `Email` | The address the link was asked for; also the email's `to:` |
| `sign_in_link` | `Url` | The link to the frontend's `link:` path, carrying the token |
| `code` | `SignInCode` | The six-digit code, text so a leading `0` stays |
| `expires_in` | `PresentationText` | How long both work, formatted by Jadpo in the email's language |

Every `EmailMessage` also has `to:` and `locale:` ([Sending](email.md#sending)).
Jadpo sets the locale as it does for its own email
([Magic links](magic-links.md)).

Here the app's sign-in email picks three fields. In the domain's
language file, `sign_in_email` is the email's subject line:

```jadpo
// domains/accounts/emails/sign_in_email.jadpo
/// The sign-in email, in the app's own words and design.
type SignInEmail = EmailMessage {
    pick: [MagicLinkEmail.sign_in_link, MagicLinkEmail.code, MagicLinkEmail.expires_in]
}

// domains/accounts/translations/en_gb.jadpo
translations {
    sign_in_email: "Your Notes sign-in link"
    sign_in_heading: "Welcome back to Notes"
    sign_in_button: "Open Notes"
    sign_in_code_line: "Or type {code} on the sign-in page."
    sign_in_expiry: "Both work for {expires_in}."
}

// authentication.jadpo
authentication {
    ...
    sign_in: {
        magic_link: {
            email: accounts.SignInEmail
        }
    }
}
```

Its HTML template, `domains/accounts/emails/sign_in_email.html`:

```text
<!-- domains/accounts/emails/sign_in_email.html -->
<h1>{{ translations.accounts.sign_in_heading() }}</h1>
<p><a href="{{ sign_in_link }}">{{ translations.accounts.sign_in_button() }}</a></p>
<p>{{ translations.accounts.sign_in_code_line(code: code) }}</p>
<p>{{ translations.accounts.sign_in_expiry(expires_in: expires_in) }}</p>
```

The messages it calls, such as `SignInHeading` and `SignInCodeLine`, are
the domain's own `Message` types, one to a file in `translations/`. Its
`.html` and `.txt` templates sit beside it in `emails/`, and its subject
is its entry in the domain's language files. All of it follows the
strict [template rules](email.md#what-a-template-may-not-do), and it is wrapped
in the shared layout like any of your app's emails. Your app's code never
emits `MagicLinkEmail`. [Your own magic-link email](custom-magic-link-email.md)
says how Jadpo fills and sends it.

## Level 5: your own sending

`magic_link: { send: accounts.send_sign_in_link }` names an action that
sends the email itself, through any package, instead of Jadpo sending
one. Its signature is fixed: it takes one built-in `MagicLinkRequest`
and returns `Unit`.

| `MagicLinkRequest` field | Type |
|---|---|
| `address` | `Email` |
| `sign_in_link` | `Url` |
| `code` | `SignInCode` |
| `expires_at` | `Instant` |
| `locale` | `Locale` |

Here `messages` is a connection to a package of the app's choice:

```jadpo
// domains/accounts/actions/send_sign_in_link.jadpo
action send_sign_in_link(request: MagicLinkRequest) fails MessageNotSent -> Unit {
    attempt messages.send(...)
}
```

- The action runs after the request commits, like a subscriber, and is
  retried under the same rules
  ([Retries and giving up](retries-and-giving-up.md)).
  Its failures are a subscriber's.
- Each attempt is given a link and a code of its own, which ends the
  last attempt's. So its provider calls go under new keys each attempt,
  and keep no record of their steps: a provider that replays a key's
  first answer still sends the link that works.
- In development its provider calls are faked and logged, as every
  provider call is
  ([Provider calls in development](provider-calls-in-development.md)).
  The dev inbox doesn't show the mail, since Jadpo sends none.
- Levels 4 and 5 can't be combined: `email:` and `send:` on one email is
  a compile error.

## Every built-in email

The same five levels apply to each email Jadpo sends. A feature with one
email takes `email:` or `send:` directly. A feature with several takes
`emails: { }`, with one entry for each email, and each entry takes
`email:` or `send:`, never both. Delivery is always through the one
`email:` connection.

| Email | Built-in types | Sent when | Configured in |
|---|---|---|---|
| Magic link | `MagicLinkEmail`, `MagicLinkRequest` | `POST /auth/magic-link` ([Magic links](magic-links.md)) | `authentication { sign_in: { magic_link: { email: / send: } } }` |
| Address check | `PrivacyAddressCheckEmail`, `PrivacyAddressCheckRequest` | A person asks through `POST /privacy/requests` ([Data requests](data-requests.md)) | `privacy: { emails: { address_check: { } } }` |
| Export ready | `PrivacyExportEmail`, `PrivacyExportRequest` | `PrivacyExportReady` ([Export](personal-data-export.md)) | `privacy: { emails: { export_ready: { } } }` |
| Erasure confirmed, with its cancel link | `PrivacyErasureConfirmedEmail`, `PrivacyErasureConfirmedRequest` | `PrivacyErasureScheduled` ([Erasure](erasure.md)) | `privacy: { emails: { erasure_confirmed: { } } }` |
| Erasure completed, with what was kept | `PrivacyErasureCompletedEmail`, `PrivacyErasureCompletedRequest` | `PrivacyErasureCompleted` ([Erasure](erasure.md)) | `privacy: { emails: { erasure_completed: { } } }` |

```jadpo
// app.jadpo
application {
    display_name: "Acme"
    privacy: {
        self_service: {}
        emails: {
            export_ready: {
                email: privacy_mail.ExportEmail
            }
            erasure_confirmed: {
                send: privacy_mail.send_erasure_confirmation
            }
        }
    }
}
```

Jadpo fills the privacy emails' fields. Every one has `address: Email`,
which is also its `to:`, and the `locale:` of the request. Each request
type, for `send:`, has the same fields, with times as `Instant` in place
of text:

| Email type | Its other fields | Request type's other fields |
|---|---|---|
| `PrivacyAddressCheckEmail` | `confirm_link: Url`, `expires_in: PresentationText` | `confirm_link: Url`, `expires_at: Instant` |
| `PrivacyExportEmail` | `download_link: Url`, `expires_in: PresentationText` (7 days) | `download_link: Url`, `expires_at: Instant` |
| `PrivacyErasureConfirmedEmail` | `erases_on: PresentationText`, `cancel_link: Url` | `erases_at: Instant`, `cancel_link: Url` |
| `PrivacyErasureCompletedEmail` | `kept: List<PrivacyKeptRecord>` | `kept: List<PrivacyKeptRecord>` |

`PrivacyKeptRecord` has two fields:

- `name: PresentationText`, the kept record's wording. It is the
  entity's entry in its domain's language files, which every entity
  marked `Erasure.retained` must have.
- `kept_until: PresentationText`, the end of its retention, in the
  email's language.

A template walks `kept` with `for`. The address-check link lasts 15
minutes, as a magic link does.
