Email header analysis: how to read and diagnose problems
A message never arrived. It landed in spam. The subscriber swears they never got it, but the ESP logs say "delivered." Every time you need to trace what happened to a specific message, the answer is in one place: the headers. The problem is that almost nobody knows how to read them.
What email headers are
Every email has two parts: the body (what the recipient reads) and the headers (metadata servers process). Headers record the full journey from send to delivery. Each server that handles the message prepends its own line, like passport stamps you can trace back. In Gmail, headers are under "Show original." In Outlook, "Message properties." In Thunderbird, Ctrl+U. Intimidating at first glance, but the structure is consistent.
Header anatomy: what each part does
Here is an anonymized header set from a transactional email, broken down block by block.
Return-Path: <bounces@mail.example.com>
Received: from mx1.recipient.com (mx1.recipient.com [203.0.113.10])
by inbox.recipient.com with ESMTPS id a1b2c3d4
for <user@recipient.com>; Wed, 23 Apr 2026 09:14:02 +0000
Received: from sender-mta.example.com (sender-mta.example.com [198.51.100.25])
by mx1.recipient.com with ESMTPS id e5f6g7h8
for <user@recipient.com>; Wed, 23 Apr 2026 09:14:01 +0000
Authentication-Results: mx1.recipient.com;
dkim=pass header.d=example.com header.s=s1 header.b=aB3kL9;
spf=pass (mx1.recipient.com: domain of bounces@mail.example.com
designates 198.51.100.25 as permitted sender)
smtp.mailfrom=bounces@mail.example.com;
dmarc=pass (p=REJECT sp=REJECT dis=NONE) 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=LPJNul+wow5MvFz8kU5OR6Hkz9TyF4RR3DN3kQ4bLEk=;
b=aB3kL9mN...
From: Notifications <notify@example.com>
To: user@recipient.com
Subject: Your order #4821 has shipped
Date: Wed, 23 Apr 2026 09:13:58 +0000
Message-ID: <20260423091358.a1b2c3@sender-mta.example.com>
MIME-Version: 1.0
Content-Type: text/html; charset=UTF-8The Received chain: tracing the message route
Received headers are the core of diagnostics. Each server that processes the message prepends its own block at the top. So you read them bottom to top, from sender to recipient.
# Step 1 (bottom Received): sending server handed off the message
Received: from sender-mta.example.com (sender-mta.example.com [198.51.100.25])
by mx1.recipient.com with ESMTPS id e5f6g7h8
for <user@recipient.com>; Wed, 23 Apr 2026 09:14:01 +0000
# Step 2 (top Received): recipient MX handed off to internal system
Received: from mx1.recipient.com (mx1.recipient.com [203.0.113.10])
by inbox.recipient.com with ESMTPS id a1b2c3d4
for <user@recipient.com>; Wed, 23 Apr 2026 09:14:02 +0000The sending IP (198.51.100.25) comes from the bottom block. Check it against Talos, Barracuda Central, or MXToolbox; a blocklist hit explains most delivery failures without further investigation. The protocol field tells you whether TLS was used: ESMTPS means encrypted, ESMTP means not. Unencrypted connections are a red flag for many providers in 2026. Timestamps between hops should be seconds. Minutes or hours mean the server queued the message: greylisting, overload, or rate limiting. The IP in parentheses after the hostname should match the PTR record; a mismatch signals spam filters.
Return-Path and envelope-from
Return-Path: <bounces@mail.example.com>
Return-Path is the bounce address, set by the receiving server from the SMTP MAIL FROM command (envelope-from). It can differ entirely from the visible From header; ESPs routinely use their own domain there to handle bounces. This is why DKIM alignment matters for DMARC: SPF checks the ESP's envelope-from domain, not your From domain. DKIM signed by your domain is what makes alignment pass.
Authentication-Results: the receiving server's verdict
This is the most useful header for deliverability diagnosis. The receiving server records all authentication check results in one place.
Authentication-Results: mx1.recipient.com;
dkim=pass header.d=example.com header.s=s1 header.b=aB3kL9;
spf=pass (mx1.recipient.com: domain of bounces@mail.example.com
designates 198.51.100.25 as permitted sender)
smtp.mailfrom=bounces@mail.example.com;
dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.comdkim=pass means the signature is valid; header.d tells you which domain signed it and header.s is the selector. dkim=fail means the key did not match: the record is missing, the body was modified in transit, or the selector is wrong. spf=pass means the sending IP is in the SPF record for the envelope-from domain — note that SPF is checked against bounces@mail.example.com, not the visible From. dmarc=pass means at least one protocol passed with correct domain alignment; dis=NONE means no quarantine or reject action was applied.
What authentication failure looks like
Now let's look at headers from a message with problems. A common scenario: a marketer switched to a new ESP, forgot to update DNS records, and part of the campaign went to spam.
Authentication-Results: mx.google.com;
dkim=fail (body hash did not verify)
header.d=example.com header.s=brevo01;
spf=softfail (google.com: domain of transitioning bounces@sendinblue.com
does not designate 185.41.28.98 as permitted sender)
smtp.mailfrom=bounces@sendinblue.com;
dmarc=fail (p=QUARANTINE sp=QUARANTINE dis=QUARANTINE)
header.from=example.comEverything is broken. dkim=fail (body hash did not verify): the selector brevo01 DNS record was never added, or the body was modified after signing by a proxy or antivirus. spf=softfail: IP 185.41.28.98 is not in sendinblue.com's SPF record as an authorized sender. dmarc=fail, dis=QUARANTINE: neither check passed with correct alignment, so the domain's quarantine policy sent the message straight to spam.
The fix: add the DKIM record for the new ESP, confirm that d= in the signature matches the From domain, wait for DNS propagation, then send a test message and check the headers again.
DKIM-Signature: what the fields mean
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=example.com; s=s1;
h=from:to:subject:date:message-id;
bh=LPJNul+wow5MvFz8kU5OR6Hkz9TyF4RR3DN3kQ4bLEk=;
b=aB3kL9mN...The key fields: a=rsa-sha256 is the algorithm (RSA/SHA-256 is standard; ed25519-sha256 is faster but not universally supported). c=relaxed/relaxed is canonicalization: relaxed tolerates minor whitespace changes that intermediate servers often introduce; always use it for bulk mail or signatures will break in transit. d= is the signing domain and must match (or be the parent of) the From domain for DMARC alignment. s=s1 is the selector; the public key lives at s1._domainkey.example.com. The h= field lists signed headers: From must be in there or the signature has no value for DMARC. bh= is the body hash (content integrity). b= is the signature itself; the receiving server recomputes the hash and verifies it against this value using the public key from DNS.
X-headers: information from ESPs and spam filters
Beyond the standard headers, servers and services add their own. These start with X- (RFC 6648 from 2012 discourages this convention, but everyone still does it).
X-Spam-Status: No, score=-2.1 required=5.0
tests=[BAYES_00=-1.9, DKIM_SIGNED=0.1, DKIM_VALID=-0.1,
DKIM_VALID_AU=-0.1, RCVD_IN_DNSWL_NONE=-0.0001,
SPF_HELO_NONE=0.001, SPF_PASS=-0.001]
X-Spam-Score: -2.1
X-Mailer: Postmark
X-PM-Message-Id: f47ac10b-58cc-4372-a567-0e02b2c3d479X-Spam-Status / X-Spam-Score show SpamAssassin results. A negative score is fine; positive above 5.0 means spam. The tests= field lists each rule and its score contribution, giving you a direct read on what pushed the message over the threshold. X-Mailer and ESP-specific IDs like X-PM-Message-Id let you find the message in the service's delivery logs. X-Google-DKIM-Signature is Gmail's own forwarding signature, not yours.
Practical diagnosis: a step-by-step process
When you need to figure out why a message did not arrive or went to spam, work through this order. It starts with the most common causes.
- Open Authentication-Results first. Any of dkim=fail, spf=fail, or dmarc=fail means the problem is authentication. Stop digging until DNS is fixed.
- Check the Received chain. Unusual delay between hops points to greylisting or a sender-side queue. An unexpected intermediate server often means forwarding, which breaks SPF.
- Look at Return-Path. A domain unrelated to yours is normal for ESP bounces, but confirm DKIM alignment.
- Find X-Spam-Status. High score? Work through the rule list.
HTML_IMAGE_RATIO,URIBL_BLOCKED, andMISSING_MIDare the usual suspects. - Check the sending IP. Copy the IP from the bottom Received block and run it through MXToolbox Blacklist Check or Talos Intelligence.
Forwarded messages: why authentication breaks
Forwarding is one of the most common causes of DMARC failure. A subscriber set up an auto-forward from work to personal. The message passes through an intermediate server, and SPF breaks immediately: the forwarder sends from its own IP, which is not in the original SPF record. DKIM may survive if the forwarder does not touch signed headers or the body, but servers that append footers, change encoding, or inject headers will break the signature. ARC (Authenticated Received Chain) solves this: the intermediate server seals the authentication results into ARC-Authentication-Results, ARC-Message-Signature, and ARC-Seal. Gmail and Microsoft both factor ARC into delivery decisions.
ARC-Authentication-Results: i=1; mx.forwarder.com;
dkim=pass header.d=example.com;
spf=pass smtp.mailfrom=bounces@mail.example.com;
dmarc=pass header.from=example.com
ARC-Seal: i=1; a=rsa-sha256; d=forwarder.com; s=arc-key;
cv=none; b=Xk9mR2...
ARC-Message-Signature: i=1; a=rsa-sha256; d=forwarder.com;
s=arc-key; h=from:to:subject:date;
bh=LPJNul+wow5MvFz8kU5OR6Hkz9TyF4RR3DN3kQ4bLEk=;
b=pQ7nW1...If you see ARC headers in the chain, the intermediate server handled forwarding correctly. cv=none means this is the first ARC hop. cv=pass on subsequent hops means earlier links in the chain are valid. cv=fail means the chain is broken and cannot be trusted.
Tools for working with headers
Reading headers by eye is a useful skill, but at volume, tools are faster. Google Admin Toolbox Messageheader gives you a visual route map with per-hop timing. MXToolbox Header Analyzer flags delays, missing TLS, and auth failures in color. For the terminal:
# Extract all Received headers from an .eml file grep -E "^Received:" message.eml # Verify a DKIM signature locally (requires opendkim-tools) opendkim-testmsg < message.eml # Quick DNS lookup for a DKIM key by selector from the header dig TXT s1._domainkey.example.com +short # Check whether the IP from Received is blocklisted dig +short 25.100.51.198.zen.spamhaus.org # A 127.0.0.x response means the IP is blocked
Common header patterns and what they mean
Multiple DKIM-Signature headers mean the message was signed twice: by the ESP and by your custom DKIM. Normal and useful. spf=none (not fail) means no SPF record exists at all for the envelope-from domain. Add one. dkim=temperror means the receiving server could not fetch the public key from DNS, usually a timeout: check the TTL and DNS availability. An unexpected hostname in the Received chain that is not in your ESP's IP ranges may mean an account compromise or unwanted relay. X-MS-Exchange-Organization-AuthAs: Anonymous is Exchange Online telling you the message arrived from outside the org. Not an error, but useful context for internal routing issues.
Headers and deliverability
Authentication is necessary but not sufficient. All three checks can pass and the message still ends up in spam, because filters also weigh domain and IP reputation, content, engagement, and complaint-to-volume ratios. Headers let you rule out infrastructure. When Authentication-Results shows three passes and messages are still going to spam, the problem is reputation or content, not DNS. Sending to nonexistent addresses, spam traps, and dormant mailboxes destroys reputation. Headers diagnose individual messages; keeping bad addresses out of your list prevents the problem from accumulating.
Headers show symptoms. Your list is the cause.
Reading email headers is a useful diagnostic skill. But the best diagnostic is prevention. When your list contains only valid, active addresses, the reasons to dig through headers become far less frequent.
Verify your list in uChecker and within minutes you will see invalid addresses, spam traps, and risky contacts. A clean list means fewer bounces, a healthy domain reputation, and headers that read pass instead of fail.
