---
title: "Authentication"
description: "How MachineMail gets SPF, DKIM and DMARC results for inbound mail without trusting forgeable headers, checks From alignment and feeds the trust level and score."
canonical: "https://machinemail.dev/docs/trust/authentication"
---

# Authentication

> How MachineMail gets SPF, DKIM and DMARC results for inbound mail without trusting forgeable headers, checks From alignment and feeds the trust level and score.

Source: https://machinemail.dev/docs/trust/authentication

The `From` header is free text: anyone can write `billing@stripe.com` in it. Authentication is how a receiver learns whether the sending domain actually stood behind the message. The trust report summarises it in four fields:

```json
"authentication": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "aligned": true }
```

## What each check means

- **SPF** checks that the server that delivered the message is allowed to send for the envelope sender's domain (the `Return-Path`, not the `From`).
- **DKIM** checks a cryptographic signature added by the sending domain, named by the signature's `d=` tag.
- **DMARC** checks that SPF or DKIM passed for a domain that matches the `From` domain, and applies the domain owner's published policy.
- **ARC** carries authentication results across forwarders and mailing lists that would otherwise break SPF and DKIM.

SPF or DKIM passing on its own proves only that *some* domain sent the message. A phisher can pass SPF for `paypa1-security.com` while writing `From: PayPal <service@paypal.com>`. That is why alignment matters.

## Where the results come from

MachineMail does not re-run DNS lookups or signature checks. It uses the results recorded by the mail service that received the message for MachineMail, and nothing the sender could have written:

1. **Verdicts reported by the receiving provider.** When the inbound provider reports SPF, DKIM and DMARC results alongside the message, they win. The reasons then include `Authentication verdicts from <provider>`.
2. **The `Authentication-Results` header added by our receiving server.** Only the topmost header carrying our receiving server's identifier counts.
3. **Nothing else.** Any other `Authentication-Results` header, `ARC-Authentication-Results` and `Received-SPF` is ignored, because a sender can add them to a message before it reaches us. A forged header that claims `dmarc=pass` gets no credit.

If none of this is available, every field is `none`, never `pass`.

Results are mapped to the report's values:

| Field   | `pass`               | `fail`                                                                        | other values                                                       |
| ------- | -------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `spf`   | `pass`               | `fail`, `hardfail`, `permerror`                                               | `softfail` and `neutral` are kept as such; anything else is `none` |
| `dkim`  | any signature passed | a signature failed (`fail`, `neutral`, `policy`, `permerror`) and none passed | `none`                                                             |
| `dmarc` | `pass`               | `fail`, `quarantine`, `reject`, `permerror`                                   | `none`                                                             |

When no trusted results are found, every field is `none` and the reasons say `No trusted authentication results`. If the message carried `Authentication-Results` headers from other servers, the reasons also say how many were ignored. A `DKIM-Signature` header with no verification result is noted in the reasons but does not count as a pass.

## `authentication_results` on the message

Every inbound message (and the `message.received` webhook payload) also carries the results themselves as `authentication_results`, the field AgentMail clients read. It comes from the same sources as the summary above, but keeps more detail:

```json
"authentication_results": {
  "spf": "pass", "spf_domain": "mail.linear.app",
  "dkim": "pass", "dkim_domain": "linear.app",
  "dmarc": "pass", "dmarc_domain": "linear.app"
}
```

- **Verdicts as recorded.** `spf`, `dkim` and `dmarc` use the `Authentication-Results` vocabulary: `pass`, `fail`, `none`, `neutral`, `softfail`, `temperror`, `permerror`. The summary's mapping above does not apply: a `permerror` stays `permerror`. A provider's "nothing to evaluate" (`GRAY` from Amazon SES) is `none`; a provider check that could not run is `temperror`.
- **Domains.** `spf_domain` is the envelope sender domain SPF checked, `dkim_domain` the `d=` domain of the signature the verdict is about (for a pass, a passing signature, preferring one aligned with `From`; only for `pass` or `fail`), and `dmarc_domain` the `From` domain. When the provider's verdicts are used, the domains come from our receiving server's header, or for DKIM from the message's only signing domain.
- **Absent, not `none`.** A mechanism nobody evaluated is left out. The whole field is absent on sent mail, on mail injected through the API, on mail whose results no trusted server recorded, and on messages received before the field was introduced.
- **Only what speaks for the sender**, as in AgentMail. `spf` is kept only when `spf_domain` aligns with the `From` domain (shares its organisational domain, so `mail.acme.com` aligns with `acme.com`), or with the `Sender` domain when `From` names several domains; an unaligned SPF result is withheld whatever its value, and so is a provider's SPF verdict when no trusted header names the domain. `dkim` and `dmarc` are withheld when `From` names more than one domain or cannot be parsed. The summary above still records every result.
- **Domains, not decisions.** `dkim_domain` is the signing domain as written: compare it with the `From` domain before relying on a DKIM `pass`, and decide with `trust.level`. Do not reject mail on `dmarc: fail` alone, since forwarding breaks it.
- **`arc_sealer`** names the forwarder whose ARC seal the verdicts came from. MachineMail does not validate ARC chains and by default ignores ARC results (see above), so it does not send this key.

## Alignment

`aligned` is true when a passing DKIM `d=` domain, or the SPF domain when SPF passed, has the same organisational domain as the `From` address. A DMARC pass reported by the receiving provider also counts as aligned, since DMARC itself requires alignment. The organisational domain is the registrable part: `mail.acme.com` aligns with `acme.com`, and suffixes such as `co.uk` are handled, so `a.example.co.uk` and `b.example.co.uk` align but `example.co.uk` and `other.co.uk` do not.

The `reasons` list says which domain passed and whether it aligned:

```json
"reasons": [
  "SPF passed for mail.linear.app",
  "DKIM passed for linear.app",
  "DMARC passed",
  "Authenticated domain aligns with From domain linear.app"
]
```

## How authentication feeds the verdict

Authentication affects the trust report in four places.

**Level.**

- Any `fail` in SPF, DKIM or DMARC makes the message at least `suspicious`.
- `verified` requires DMARC `pass` with `aligned: true`, plus prior correspondence or an allow-list entry. A receive allow entry also blocks every sender not on the list, so prior correspondence is the usual route.
- `known` requires at least one of SPF, DKIM or DMARC to pass. Mail from a domain every MachineMail organization shares (such as `machinemail.dev`) also needs prior correspondence with that exact address or an allow-list entry; otherwise it is `unknown`.
- With nothing passing and nothing else wrong, the level is `unknown`, and the score is capped at 0.7.
- A [trusted domain](https://machinemail.dev/docs/concepts/trusted-domains) asks for more than a DMARC pass: the message must carry a DKIM signature whose `d=` is the trusted domain itself (or a subdomain, when the entry includes them). A relaxed DMARC pass, SPF alone or a forwarder's signature does not count.

**Score.** The authentication component carries 25% of the score:

| Result                    | Component |
| ------------------------- | --------- |
| DMARC pass and aligned    | 1.0       |
| SPF or DKIM pass, aligned | 0.8       |
| a pass, not aligned       | 0.5       |
| nothing passed            | 0.3       |
| anything failed           | 0         |

**Classifier.** Failures raise the phishing and spam probabilities (DMARC fail most, then DKIM, SPF and SPF softfail), no passing authentication raises them a little, and an aligned DMARC pass lowers them. Mail that claims to come from your own domain without passing SPF or DKIM is flagged as spoofing.

**Labels.** If DMARC failed, or SPF and DKIM both failed, the message gets the `UNAUTHENTICATED` label and a `message.received.unauthenticated` event. It keeps the `INBOX` label, but message lists hide `UNAUTHENTICATED` mail unless you pass `include_unauthenticated=true`. The MCP `wait_for_message` tool does not wake for it.

## Mail injected through the API

Messages posted to `POST /inbound/raw` (which needs a key with the explicit `inbound_raw` permission) carry whatever headers the caller wrote. MachineMail ignores all of them: authentication is always `none`, so injected mail is never `verified` or `known`. The message gets `received_via: "raw"` and the reason `Injected via API (/inbound/raw); authentication results not verified`. Use it for tests and fixtures.

## Common patterns

| What you see                                         | Usually means                                                                                          |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `spf: pass, dkim: pass, dmarc: pass, aligned: true`  | Real mail from the domain in `From`.                                                                   |
| `spf: pass, dkim: none, dmarc: none, aligned: false` | Sent through a provider that signs with its own domain, or a lookalike domain. Check `sender.domain`.  |
| `dkim: fail`                                         | The message was modified in transit, often by a forwarder or mailing list, or the signature is forged. |
| everything `none`                                    | No trusted results: the receiving server recorded none, or the mail was injected through the API.      |
| `dmarc: fail`                                        | The domain owner says this message is not theirs. Treat it as spoofed.                                 |
