How to read email headers to debug delivery problems
Last updated: 26:10:2026
What headers are, and which ones you can trust
Every email starts with a block of header lines before the body. Your mail app shows four or five of them (From, To, Subject, Date) and hides the rest, which is where the useful information is. The headers come from three different places:
- The sender's software writes the message fields defined in RFC 5322:
From,To,Subject,Date,Message-ID, and oftenList-Unsubscribeand theDKIM-Signature. - Every server that handles the message adds a
Receivedline on top (RFC 5321). The server that finally delivers it into a mailbox addsReturn-Pathwith the envelope sender. - The receiving provider adds its verdicts:
Authentication-Results(RFC 8601), sometimesReceived-SPF, ARC headers for forwarded mail, and its own spam headers.
Headers are plain text, so anyone can write anything into them before sending. The rule that keeps you from being misled: trust headers added by your own provider's servers, and treat everything beneath them as a claim by the sender. A fake Received line or a forged "spf=pass" at the bottom of a phishing message proves nothing; the Authentication-Results line your provider added at the top does.
How to see the full headers
| Mail app | Where to find them |
|---|---|
| Gmail (web) | Open the message, click the More (three dots) menu next to Reply and choose "Show original". Gmail lists the SPF, DKIM and DMARC results at the top. |
| Outlook for Windows (classic) | Open the message in its own window (double-click), then File → Properties; the headers are in the "Internet headers" box. |
| New Outlook for Windows, Outlook on the web, Outlook.com | In the message, choose More actions → View → View message details. |
| Apple Mail (Mac) | Select the message and choose View → Message → All Headers (or Raw Source for the complete message). |
| Thunderbird | View → Headers → All shows every header above the message; View → Message Source (Ctrl+U) shows the raw message. |
Phone mail apps generally don't show full headers. Open the same message in webmail or a desktop app instead. When you ask someone for help, copy the whole header block as text - a screenshot cuts lines and can't be pasted into a header analyser.
One note for AnyMail users: AnyMail stores received mail as parsed messages, not as the original raw file, so the original headers of a message you received aren't kept. That rarely matters, because the usual question is how your outgoing mail arrives elsewhere: send a test message to a Gmail or Outlook.com address and read the headers there, where the receiving provider has recorded exactly what it saw.
An annotated example
Here is a shortened header block of a message from anna@example.com, sent through a provider at out.mailhost.example and received by mx1.example.net. Real headers are longer and in a slightly different order, but the important lines look like this:
Return-Path: <anna@example.com>
Delivered-To: jo@example.net
Received: from out.mailhost.example (out.mailhost.example [192.0.2.25])
by mx1.example.net with ESMTPS id 4F2A9C0123
for <jo@example.net>; Mon, 26 Oct 2026 10:15:04 +0100
Authentication-Results: mx1.example.net;
spf=pass smtp.mailfrom=anna@example.com;
dkim=pass header.d=example.com header.s=s1;
dmarc=pass header.from=example.com
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=s1;
h=from:to:subject:date:message-id; bh=2jUSOH9N…; b=dzdVyOfA…
Message-ID: <8c1f02a4-6e1b-4d1e-9a51-3f0c2e7d9b10@example.com>
Date: Mon, 26 Oct 2026 10:15:02 +0100
From: Anna Nowak <anna@example.com>
To: jo@example.net
Subject: Invoice 1042
List-Unsubscribe: <https://out.mailhost.example/unsubscribe/…>
List-Unsubscribe-Post: List-Unsubscribe=One-ClickThe Received chain: read it from the bottom up
Each server prepends its Received line, so the newest is at the top and the first hop at the bottom. To follow the route, start at the lowest Received line and work upwards. Each line says which server it came from (with the name it announced, its reverse-DNS name and IP address in brackets), by which server received it, with which protocol (ESMTPS means SMTP over TLS) and when.
- Finding a delay. Compare the timestamps of consecutive lines. A gap of minutes or hours between two hops shows exactly which server held the message - often greylisting, a busy queue or a retry after a temporary failure. Watch the time zones:
+0100and-0700are hours apart even when the clocks look close. - Finding the real sending server. The trustworthy hop is the one where your provider's server (the first line written by a host you recognise as yours) records the connection from outside. The IP address in that line is the one SPF was checked against and the one blocklists are about.
- Spotting TLS.
ESMTPSor a TLS version and cipher in the line means that hop was encrypted; plainESMTPorSMTPmeans it wasn't.
Authentication-Results: the verdict
This is the most useful single header when mail lands in spam. The receiving server records each check and its result, prefixed with its own name (the "authserv-id", mx1.example.net above) so you can tell its results from anything a sender tried to slip in.
| Field | Meaning | What to check |
|---|---|---|
spf= | Whether the sending IP is allowed by the SPF record of the envelope sender's domain | smtp.mailfrom |
dkim= | Whether a DKIM signature verified - one result per signature | header.d / header.s |
dmarc= | Whether SPF or DKIM passed for a domain aligned with the From address | header.from |
arc= | For forwarded mail: whether the chain of intermediaries could be verified (RFC 8617) | only present on some messages |
spf=smtp.mailfromdkim=header.d / header.sdmarc=header.fromarc=Results are pass, fail, softfail (SPF ~all), neutral, none (no record or no signature), temperror (a DNS lookup failed temporarily) and permerror (the record itself is broken - for SPF, often more than 10 lookups or two SPF records). Some providers add their own compauth or reason codes; those are provider-specific.
A dmarc=fail with spf=pass and dkim=pass is not a contradiction: it means both passed for domains other than the one in From, typically a mail service's own domain. That is an alignment problem, covered below and in the DMARC guide.
DKIM-Signature: who signed, and with which key
The DKIM-Signature header is the signature itself (see DKIM explained). Two tags matter most when debugging:
d=- the signing domain. For DMARC to pass on DKIM, it must be your domain (or, with relaxed alignment, a subdomain of the same organisation as the From address).s=- the selector. The receiver fetches the public key froms1._domainkey.example.comfors=s1; d=example.com. Ifdkim=failsays the key wasn't found, look up exactly that name.
h= lists the signed headers, bh= is the hash of the body and b= the signature. When DKIM fails with a body hash mismatch, something changed the message after it was signed: a mailing list adding a footer, a security gateway rewriting links, or a forwarder. A message can carry several signatures - for example yours and your mail service's - and each gets its own result.
Return-Path, From and alignment
Return-Path shows the envelope sender, the MAIL FROM address given during the SMTP conversation. It is where bounces go, and it is the domain SPF checks. From is the address people see. The two are often different: many sending services use their own bounce-handling domain in the envelope while From shows yours.
DMARC ties them together. It passes only if SPF passed for a Return-Path domain that matches the From domain, or DKIM passed with a d= that matches it. In the example above everything lines up: Return-Path, From and d= are all example.com. When you use AnyMail, that alignment is built in: outgoing mail uses your own address as the envelope sender and is DKIM-signed with your domain.
Message-ID: the message's fingerprint
Message-ID is a globally unique identifier the sending software assigns, written like an address in angle brackets. It is what replies refer to (In-Reply-To and References), which is how mail apps group conversations, and it is the best way to find one message in logs: give your provider the Message-ID rather than the subject and time. Bounce reports quote it too, which is how AnyMail links a bounce that arrives later back to the message it belongs to. A message missing a Message-ID, or with one that doesn't look like <something@domain>, often comes from a misconfigured script.
List-Unsubscribe
List-Unsubscribe (RFC 2369) gives mail apps a link or address for unsubscribing, and List-Unsubscribe-Post: List-Unsubscribe=One-Click (RFC 8058) says that link works with a single POST, without a confirmation page. Gmail's and Yahoo's sender requirements ask bulk senders of marketing and subscription mail to support one-click unsubscribe. If Gmail doesn't offer an "Unsubscribe" option for your newsletter, first check that both headers are present and that the link uses HTTPS. AnyMail adds both headers, backed by a working one-click link, to every message it delivers to addresses outside AnyMail.
A quick diagnosis table
| Symptom | Look at | Usual cause |
|---|---|---|
| Lands in spam | Authentication-Results | SPF/DKIM failing or not aligned with From; see the spam guide |
| Arrived hours late | Received | A gap between two hops: greylisting, retries or a queue |
dkim=fail | DKIM-Signature | Key not published under s=, or the body was modified on the way |
spf=fail / softfail | Return-Path / Received | The sending IP isn't in the envelope domain's SPF record |
dmarc=fail | header.from / header.d / smtp.mailfrom | Passing domains don't match the From domain |
| Replies don't thread | Message-ID / In-Reply-To | Missing or rewritten Message-ID |
Authentication-ResultsReceiveddkim=failDKIM-Signaturespf=fail / softfailReturn-Path / Receiveddmarc=failheader.from / header.d / smtp.mailfromMessage-ID / In-Reply-ToHeaders tell you what happened to one message. To check your domain's records before sending anything, use the email authentication checker, and for the bigger picture of why mail is filtered, see why emails go to spam.
Sharing headers safely
Headers contain email addresses, IP addresses and sometimes internal hostnames, but never your password. Before posting them in a public forum, replace addresses with placeholders; when sending them to your provider's support, send them unedited - edited headers are much harder to debug.
Let AnyMail handle these records for you
AnyMail generates the MX, SPF, DKIM and DMARC records for your domain, checks every one of them automatically and tells you exactly which record to fix - then hosts your inboxes, operated from the EU.