# The dev inbox and the health score

`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. This
topic is how the inbox, its commands and the score work
([Email](email.md) has the overview).

## One catcher, kept on disk

`jadpo dev` gives the connection `email:` names the same catching driver
a test's host gives it, so each message is handed over whole, a mail
package's included. Each is kept in `build/dev/inbox/` as a JSON file,
with its attachments' bytes beside it, written whole and then renamed.
`jadpo email` reads the inbox from another terminal, while `jadpo dev`
runs or after.

The last 200 mails are kept. Their ids are `mail_<n>`, and an id is
never given twice, even after `jadpo email clear`. A mail package's
other operations still go to its fake. An SMTP connection's settings
that nothing gives get stand-ins, as a provider's do, since its server
is never reached.

## Codes and links only in the inbox

A mail is kept whole, its sign-in link and code included, because
development needs them. `jadpo dev`'s line for a caught mail names its
id, its email and its address, never its subject or its words, and the
call log never sees it.

## The inbox page

`jadpo dev` serves the page on `127.0.0.1`, on the port after the app's,
or on one the system chooses when that is taken. Its address is on the
ready screen once the app sends mail, and it answers only requests
addressed to `localhost`. The page is read-only, as the console is
([The developer console and MCP](the-developer-console-and-mcp.md)).

It lists the mail newest first. The mail you choose is shown with its
HTML in a sandboxed frame on white, its links opening a new tab, then
its text, its links numbered with the `jadpo email open` command for
each, its attachments and its health. It looks for new mail every two
seconds. It shows the id of the trace that sent each mail; the link to
the trace comes with `jadpo trace`
([The request timeline in `jadpo dev`](the-request-timeline-in-jadpo-dev.md)).

## `jadpo email`

- `list`, the default, shows the mail oldest first, and `--to` narrows
  it.
- `show <id>` shows the text part, or the HTML with `--html` or when
  there is no text.
- `links <id>` lists the links, numbered from 1.
- `open <id> <link>` opens a link, by number or written out, in
  `$BROWSER` or the system's opener. It opens only `http://` and
  `https://` links.
- `clear` empties the inbox.

An id is written `mail_3` or `3`.

## The score's checks

The score runs seven checks. Each gives the same answer every time, and
each comes with its fix:

1. No plain-text part beside the HTML.
2. Images with fewer than 20 characters of text.
3. HTML that doesn't parse cleanly: an end tag that closes nothing or
   closes another element first, an element never closed (other than
   those HTML lets a writer leave open), or a tag never finished.
4. A link that isn't absolute `https://`. This covers every `href` and
   every web address in the text. `mailto:`, `tel:` and `sms:` pass, and
   in the dev inbox so do links to this machine.
5. A link through a shared shortener, from a fixed list.
6. A subject of four or more letters that have a case, none of them
   lower case.
7. HTML over 102 KB.

A score is the problems found and "N of 7 checks pass", with the note
that it can't predict inbox placement. The check for an unsubscribe
header waits for bulk mail, which no email is yet.
[SpamAssassin in the health score](spamassassin-in-the-health-score.md)
adds SpamAssassin's rules.

## What `jadpo check` scores

`jadpo check` scores what a template's own text answers, as
`email.health_problem`, which you can decline on the email's or
partial's type:

- an `.html` with no `.txt`;
- a link written out whole, not built inside `{{ }}`, links to this
  machine included;
- the HTML's structure with its `{{ }}` left out, only in a template
  with no `{{ if }}`, since a condition may open an element in one
  branch and close it in another;
- an email's own HTML with images and neither text nor a value written
  as text;
- a template over 102 KB;
- a subject whose words, as a locale writes them, are in capitals.

What a template writes from its fields is scored once it is rendered,
in the dev inbox. One `decline` above the type answers every finding
there, as a decline covers its rule on its declaration
([Declining a judgement call](declining-a-judgement-call.md)).

## `jadpo email check-domain`

`jadpo email check-domain` reads the domain you give it. Without one it
reads `email: { from: }`'s: the address written out, or the setting's
value from the environment, `.env.local` or its default. It checks:

- **SPF**, at the domain: one `v=spf1` record, ending `-all` or `~all`,
  its own terms needing at most 10 lookups. `+all` fails, and no record,
  or `?all`, is an alert.
- **DKIM**, at `<selector>._domainkey.<domain>`, only with `--selector`:
  a key with a `p=` that isn't empty.
- **DMARC**, at `_dmarc.<domain>`: one `v=DMARC1` record. `p=quarantine`
  or `p=reject` passes, and `p=none` is an alert.

What a provider needs beyond these, such as its include and its
selector, waits for a mail package to declare it. The records are asked
of the name server `/etc/resolv.conf` names, over UDP, then over TCP
when the answer is cut short, behind a seam its tests fake. The command
exits 0 with its verdicts, and `--json` says whether every record
`passed`.
