How to Read Email Headers to Debug Deliverability Problems
An email header is a log written by every server the message passed through, newest entry on top. To debug deliverability, read three things: the Authentication-Results line your own mail provider added, the Received chain from the bottom up for the route and delays, and the DKIM-Signature and Return-Path domains to check alignment with From.
This post goes line by line through real, published headers. If you are trying to work out why a specific message landed in Gmail's spam folder, start with the shorter Gmail spam diagnosis flow and come back here when you need to understand what a line means.
How to get the full header
Every major client can show the raw header, usually under an option called "Show original", "View source" or "View message details".
- Gmail (web): open the message, click More next to Reply, then Show original. Google's help page says the full header opens in a new window with a Copy to clipboard button.
- Outlook and Apple Mail: both expose the raw source from the message view; the menu name varies by version, so search your client's help for "view message headers".
Always copy the header from the recipient's side. The copy in your own Sent folder has not been through the receiving servers, so it has no Received lines from them and no Authentication-Results from the receiver, which are the lines that matter.
The one rule that makes headers readable
Each server adds its lines to the top, so the header reads like a stack: newest at the top, origin at the bottom.
This is not convention, it is the protocol. RFC 5321, section 4.4 says: "SMTP servers MUST prepend Received lines to messages; they MUST NOT change the order of existing lines or insert Received lines in any other location."
Two practical consequences:
- To follow the route, read
Receivedlines bottom up. - The lines near the top were written by your own provider. The lines near the bottom were written by servers you have never heard of, and some may have been written by the sender. Trust decreases as you go down.
A real header, annotated
The cleanest real example of a multi-hop header with authentication results is in the specification itself. This is Example 5 from RFC 8601, Appendix B.5, quoted exactly (the DKIM signature is truncated with ... in the original):
Authentication-Results: example.com;
dkim=pass (good signature) header.d=example.com
Received: from mail-router.example.com
(mail-router.example.com [192.0.2.1])
by auth-checker.example.com (8.11.6/8.11.6)
with ESMTP id i7PK0sH7021929;
Fri, Feb 15 2002 17:19:22 -0800
DKIM-Signature: v=1; a=rsa-sha256; s=gatsby; d=example.com;
t=1188964191; c=simple/simple; h=From:Date:To:Subject:
Message-Id:Authentication-Results;
bh=sEuZGD/pSr7ANysbY3jtdaQ3Xv9xPQtS0m70;
b=EToRSuvUfQVP3Bkz ... rTB0t0gYnBVCM=
Authentication-Results: example.com;
auth=pass (cram-md5) smtp.auth=sender@example.com;
spf=fail smtp.mailfrom=example.com
Received: from dialup-1-2-3-4.example.net
(dialup-1-2-3-4.example.net [192.0.2.200])
by mail-router.example.com (8.11.6/8.11.6)
with ESMTPA id g1G0r1kA003489;
Fri, Feb 15 2002 17:19:07 -0800
From: sender@example.com
Date: Fri, Feb 15 2002 16:54:30 -0800
To: receiver@example.com
Message-Id: <12345.abc@example.com>
Subject: here's a sample
Read it from the bottom.
| Order | Line | What it tells you |
|---|---|---|
| 1 | From, Date, Subject, Message-Id |
Written by the sender's mail program. All are claims, not facts. |
| 2 | Lower Received |
mail-router.example.com accepted the message from a dial-up host at 192.0.2.200. ESMTPA means the sender logged in with SMTP AUTH. |
| 3 | Lower Authentication-Results |
The router recorded that SMTP AUTH passed and SPF failed: the dial-up network was not listed in example.com's SPF. |
| 4 | DKIM-Signature |
The router signed the message as d=example.com, selector gatsby, and included the lower Authentication-Results in the signed headers (h=). |
| 5 | Upper Received |
auth-checker.example.com received it from the router at 192.0.2.1. |
| 6 | Upper Authentication-Results |
The checker verified the DKIM signature: pass. |
The RFC's own explanation matches: the user was legitimate (they authenticated with a password), SPF failed because the dial-up network was not authorised, and DKIM passed because the router signed with a key example.com publishes.
That is the main lesson of the example, and it applies directly to modern deliverability: an SPF fail on its own does not mean the mail is illegitimate, and DKIM is what survives the trip.
Measuring delays from the Received chain
Each Received line ends with a timestamp, so the difference between consecutive lines is how long each hop took.
RFC 5321 asks servers to use explicit offsets such as -0800, which is what makes the comparison possible across time zones. We wrote a short script to do it, then ran it on the RFC example above. The full script is below.
import sys, re
from email import message_from_string
from email.utils import parsedate_to_datetime
msg = message_from_string(open(sys.argv[1]).read())
hops = msg.get_all("Received") or []
prev = None
for i, h in enumerate(reversed(hops), 1):
h = " ".join(h.split())
stamp = h.rsplit(";", 1)[-1].strip()
try:
when = parsedate_to_datetime(stamp)
except Exception:
when = None
m = re.search(r"from (\S+).*?by (\S+)", h)
gap = f"+{(when - prev).total_seconds():.0f}s" if when and prev else "-"
print(f"hop {i}: {m.group(1) if m else '?'} -> {m.group(2) if m else '?'} {stamp} {gap}")
prev = when or prev
Output, run on 1 October 2026 (python3 hops.py header.txt):
hop 1: dialup-1-2-3-4.example.net -> mail-router.example.com Fri, Feb 15 2002 17:19:07 -0800 -
hop 2: mail-router.example.com -> auth-checker.example.com Fri, Feb 15 2002 17:19:22 -0800 +15s
The hop between the two servers took 15 seconds. The larger gap is between the Date header, 16:54:30, and the first Received line, 17:19:07: about 25 minutes before any server saw the message. Date is set by the sender's software, so a gap there points at the sender's side (a queued outbox, a scheduled send, or a wrong clock), not at any relay.
If you would rather not run code, Google's Messageheader tool does the same arithmetic: paste a header and it identifies delivery delays and the approximate source.
Reading Authentication-Results properly
Authentication-Results records each check as method=result, followed by the identity that was checked. It is the most useful line in the header and the most misread.
The format, from RFC 8601, is:
Authentication-Results: <authserv-id>; <method>=<result> [reason] <ptype>.<property>=<value>
- authserv-id is who did the check, for example
mx.google.com. - method=result is the check and the outcome:
spf=pass,dkim=fail,dmarc=pass. - ptype.property says which identity was checked:
smtp.mailfrom(the envelope sender),header.dorheader.i(the DKIM signing domain),header.from(the visible From domain). - Anything in parentheses is a free-text comment, and receivers word these differently.
The possible results the RFC defines for SPF are none, pass, fail, softfail, neutral, temperror, permerror and policy; DKIM uses none, pass, fail, policy, neutral, temperror and permerror.
Only trust the one your provider wrote
RFC 8601 is explicit that this header should only be believed when it comes from a server inside your own administrative boundary, and that a conforming server must delete copies that claim to come from it but arrive from outside. In practice: use the topmost Authentication-Results whose authserv-id is your own provider. Any lower copy was written by someone else, possibly the sender.
A real Gmail example, checked against DNS
Valimail published this header from a message received at Gmail (source):
Authentication-Results: mx.google.com;
dkim=pass header.i=@valimail.com header.s=google2048 header.b=Z8L6tjHb;
spf=pass (google.com: domain of [redacted]@valimail.com designates 209.85.220.41 as permitted sender) smtp.mailfrom=[redacted]@valimail.com;
dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=valimail.com
Every claim in it can be checked from your own terminal. We did, on 1 October 2026:
| Header says | Command | What DNS returned |
|---|---|---|
DKIM selector google2048 on valimail.com |
dig +short TXT google2048._domainkey.valimail.com |
A key record beginning "v=DKIM1; k=rsa; p=MIIBIjAN..." |
DMARC policy p=REJECT |
dig +short TXT _dmarc.valimail.com |
"v=DMARC1; p=reject; rua=..." |
Sent from 209.85.220.41 |
dig +short -x 209.85.220.41 |
mail-sor-f41.google.com. |
So the selector named in s= exists, the policy Gmail reported matches the published record, and the sending IP is Google's own outbound infrastructure. This cross-check is the habit worth building: a header tells you what the receiver concluded, and DNS tells you whether the conclusion still holds today. Our DKIM checker and DMARC checker run the same lookups without a terminal.
Alignment: the check you do by eye
DMARC compares the header.from domain with the domains that passed SPF and DKIM. At least one must match the From domain's organisation.
In the Gmail example all three are valimail.com, so DMARC passes on either check. The common failure looks like this instead:
| Identity | Domain | Result |
|---|---|---|
smtp.mailfrom (Return-Path) |
bounces.sendingplatform.example |
spf=pass |
header.d (DKIM) |
sendingplatform.example |
dkim=pass |
header.from |
yourcompany.example |
dmarc=fail |
Two passes and a fail, because nothing that passed belongs to the From domain. The current DMARC specification (RFC 9989, May 2026, replacing RFC 7489) needs only one aligned pass, and the usual fix is enabling custom DKIM for your own domain in the sending platform.
Return-Path: where bounces go
Return-Path is the envelope sender, added at final delivery, and it is the address bounces are sent to. It is also the identity SPF checks.
RFC 5321 says the delivering server inserts it at final delivery, preserving the address from the MAIL FROM command, and that its primary purpose is to say where non-delivery notices go. It is often different from From, especially through a sending platform that handles your bounces. That is normal. If it points somewhere you do not control, though, bounces you need to see are going elsewhere, which matters for keeping a list clean.
What headers cannot tell you
Headers show authentication and route, not why a filter chose Spam. Gmail does not write its reputation score into the header.
If everything in the header passes and the message still lands in Spam, the answer is in reputation data, not in the header. For Gmail that means Postmaster Tools. For the order to work through those causes, see the Gmail diagnosis flow.
A checklist for any header
- Copy the header from the recipient's copy, never from Sent.
- Find the topmost
Authentication-Resultswritten by your provider. Ignore any others. - Check
spf,dkimanddmarcresults. Note the domain beside each. - Compare those domains with
header.from. At least one pass must align. - Look up the DKIM selector (
s=) and DMARC record in DNS to confirm they still exist. - Read
Receivedlines bottom up. Note the first IP your provider recorded and any hop with a long gap. - Compare
Datewith the firstReceivedtime to separate sender-side delay from relay delay.
To run the DNS side of this checklist for your own domain in one step, use the free deliverability audit.
Common questions
Which order should I read email headers in?
Read Received lines from the bottom up, because each server adds its line to the top. The bottom one is closest to the sender and the top one is the final delivery. Authentication-Results are best read from the top, since the one your own provider added is the one you can trust.
Can email headers be faked?
Everything a sender writes can be faked, including From, Date and any Received lines added before the message reached a server you trust. Lines added by your own provider cannot be forged by the sender, which is why you trust the top of the header and treat the bottom as claims.
What is the difference between Return-Path and From?
Return-Path is the envelope sender that bounces go to, and it is what SPF checks. From is the address the recipient sees, and it is what DMARC protects. They are often different domains when you send through a platform, which is fine as long as DKIM or SPF aligns with the From domain.
How do I find the sending IP address in an email header?
Look at the Received line added by the first server you trust, usually your own provider's inbound server, and take the IP in square brackets in its 'from' clause. Many receivers also repeat it in the SPF part of Authentication-Results.
Verify unlimited addresses for $29.99/month
Real SMTP mailbox checks. No credits, no per-email fees.
Get Started