How to read email headers
Updated ·10 min read
Every email carries a block of headers above the part you read. Most of them are added by the servers that handled the message rather than by the sender, and read in the right order they tell you where a message came from, how long each hop took, and whether the receiving system could prove the sender was who it claimed to be.
The single most important thing to understand first: almost every header a sender controls can be set to anything. From, Reply-To, Date, even a fabricated Received line — all of it is text the sending system chose. What cannot be faked is what the receiving infrastructure added on the way in, and the whole skill of reading headers is knowing which is which.
Check yours now
Getting the raw headers
Every client hides them by default and every client can show them.
- Gmail: the three-dot menu on the message, then "Show original". This opens a page with the full source and a summary of the authentication results.
- Outlook on the web: the message menu, then "View", then "View message source".
- Outlook for Windows: open the message in its own window, then File, Properties, and read the "Internet headers" box.
- Apple Mail: View, Message, Raw Source.
- Thunderbird: View, Message Source, or Ctrl+U.
Copy the whole block from the top down to the first blank line. The blank line is where headers end and the body begins, and everything below it is content rather than metadata.
Read the Received chain from the bottom up
Each server that handles a message adds a Received header at the top of the block. The result is a stack in reverse chronological order: the bottom Received line is the first hop, closest to the sender, and the top one is the last hop, closest to you.
Read from the bottom. Each line names the host that sent this hop, the host that received it, the protocol used, the recipient it was for, and a timestamp. The from field is partly what the connecting server claimed about itself and partly what the receiving server observed — the part in brackets, the IP address and the reverse-DNS name, was observed and is the trustworthy part. The name outside the brackets was claimed in the SMTP greeting and is not.
Two things are worth extracting. First, the earliest hop you trust — everything below the first server that belongs to a system you know is unverifiable, because a sender can fabricate Received lines to make a message look like it has a longer history than it does. Second, the gaps between timestamps, which is where a delay lives. A message that took four hours to arrive usually spent all four in one hop, and the chain shows you which.
Received: from mx.example.com (mx.example.com [203.0.113.10])
by imap.example.com with LMTP id 4f2a1c9b
for <you@example.com>; Fri, 11 Sep 2026 09:14:07 +0000 <- 3rd, +1s
Received: from mail-out.sender.example (mail-out.sender.example
[198.51.100.25])
by mx.example.com with ESMTPS id 8b31ee02
(version=TLS1.3 cipher=TLS_AES_256_GCM_SHA384)
for <you@example.com>; Fri, 11 Sep 2026 09:14:06 +0000 <- 2nd, +4s
Received: from app01.internal.sender.example (localhost [127.0.0.1])
by mail-out.sender.example with ESMTP id 1a77c0de;
Fri, 11 Sep 2026 09:14:02 +0000 <- 1st, origin
; the bracketed IPs were observed by the receiving server
; the names before the brackets were claimed by the connecting one
; hop 2 shows the transfer was encrypted: version=TLS1.3Authentication-Results: the verdict, and who reached it
The Authentication-Results header (RFC 8601) is where the receiving system records what it concluded about SPF, DKIM and DMARC. It is the fastest answer to "is this really from who it says", and it is also the header people most often misread.
It begins with the identity of the system that performed the checks — mx.example.com in the example below. That matters, because a message can carry several Authentication-Results headers and only the one added by your own receiving infrastructure is trustworthy. A header claiming to be from a system upstream of yours could have been written by anyone. When in doubt, trust only the topmost one, added by the server that delivered to you.
Then come the results. spf=pass names the domain SPF validated, which is the envelope sender — not necessarily the domain in the From header. dkim=pass names the signing domain from the signature's d= tag. dmarc=pass is the one that ties them together: it means at least one of SPF or DKIM passed for a domain that aligns with the visible From address.
The distinction between a mechanism passing and DMARC passing is the whole point. A phishing message can legitimately pass SPF — for the attacker's own domain, which they control and have configured correctly. Only alignment makes that pass mean something about the From address you can see.
Authentication-Results: mx.example.com;
spf=pass (mx.example.com: domain of bounce@mail.sender.example
designates 198.51.100.25 as permitted sender)
smtp.mailfrom=bounce@mail.sender.example;
dkim=pass header.d=sender.example header.s=selector1;
dmarc=pass (p=REJECT sp=REJECT) header.from=sender.example
; dkim d= is sender.example, From: is sender.example -> aligned
; dmarc=pass is the only line that says anything about the visible FromWhat each result value means
- pass — the check succeeded. For SPF and DKIM this is a statement about a domain the sender chose; only dmarc=pass ties it to the From address.
- fail — the check ran and the message did not satisfy it. For SPF this means the sending IP is not authorised. For DKIM it means the signature did not verify, which can be forgery but is very often a mailing list or forwarder that modified the message in transit.
- softfail — SPF only; the record ends in ~all and the sender was not listed. It means "probably not authorised, but the domain owner is not confident enough to say so".
- neutral — SPF only; the domain published a record that explicitly declines to make a statement about this sender. Treat it as equivalent to no policy.
- none — no record was published to check against. Not a failure; an absence.
- permerror — the record exists but could not be processed. For SPF this is most often the ten-lookup limit being exceeded, and it is a fault in the sending domain's configuration rather than anything about the message.
- temperror — a transient DNS or network problem prevented the check. It says nothing about the message and the same message checked again might pass.
The two errors are worth separating in your head. A permerror is a standing configuration fault that will affect every message from that domain until someone fixes it; a temperror is noise.
ARC: what happens when a forwarder breaks the chain
Forwarding is genuinely hard for email authentication. A mailing list that appends a footer invalidates the DKIM signature, and a forwarder that relays a message sends it from its own IP address, which is not in the original domain's SPF record. Both SPF and DKIM legitimately fail, and under an enforcing DMARC policy that mail gets rejected despite being entirely genuine.
ARC — Authenticated Received Chain, RFC 8617 — is the mechanism designed to carry the original verdict across those hops. Each intermediary adds three headers: ARC-Authentication-Results recording what it saw when it received the message, ARC-Message-Signature signing the message as it stood at that point, and ARC-Seal signing the chain so far so that earlier entries cannot be altered.
The result is that a final receiver can see that a trusted forwarder verified the message successfully before modifying it, and can choose to accept it even though its own checks now fail. Two things follow. ARC is a basis for a decision, not a decision: whether to honour a chain depends on whether the receiver trusts the intermediaries in it, and receivers differ. And ARC is published as Experimental rather than as a standards-track specification, so support is a matter of deployment rather than obligation, even though the major providers do implement it.
In practice: cv=pass on the outermost ARC-Seal means the chain is intact and the earliest entry's verdict can be read. A dmarc=fail alongside an intact ARC chain showing an earlier pass is the signature of ordinary forwarding, not of an attack.
Reading a message you suspect is forged
Work through five checks in this order. Each is cheap and the first one that comes back wrong is usually the answer.
- Does dmarc=pass appear in the Authentication-Results header your own infrastructure added, and does header.from in that line match the address displayed to you? This alone settles most cases.
- If DKIM passed, is the d= domain the same as the From domain, or a subdomain of it? A pass for an unrelated domain is a pass for the attacker's domain.
- Does the Return-Path or smtp.mailfrom match the From domain? A mismatch is normal for bulk mail and suspicious in a message that claims to be a personal one from a colleague.
- Is there a Reply-To pointing somewhere other than the From address? Entirely legitimate in plenty of systems, and also the standard mechanism for a business-email-compromise attempt, since the attacker needs replies to reach them.
- Does the earliest trustworthy Received hop come from infrastructure that makes sense for the claimed sender? A message claiming to be from a large provider that entered through a consumer broadband address is not.
Two non-signals worth discounting. Display names are free text and prove nothing whatsoever. And IP geolocation tells you where a network block was registered, not where a person is, so a sending address registered in an unexpected country is weak evidence at best — plenty of legitimate providers route through infrastructure that looks surprising.
The other headers worth knowing
- Message-ID — a unique identifier assigned by the first system to handle the message. Useful when asking a provider to trace a specific delivery, and the thing to quote in a support ticket.
- Return-Path — the envelope sender, where bounces go. This is the domain SPF is checked against, and it is frequently different from the From address by design.
- References and In-Reply-To — the Message-IDs this message is a reply to, which is how clients reconstruct threads.
- List-Unsubscribe and List-Unsubscribe-Post — the machine-readable unsubscribe mechanism, which large receivers now require on bulk mail.
- Received-SPF — an older, SPF-specific header that predates Authentication-Results. It carries the same information for SPF alone and is still emitted by some systems.
- X- headers — anything beginning X- is non-standard and defined by whoever added it. Spam scores, internal routing tags and platform identifiers all live here. They can be informative and they are not guaranteed to mean the same thing anywhere else.
What commonly goes wrong
- Trusting an Authentication-Results header that your own systems did not add. Anything below your boundary could have been written by the sender.
- Reading the Received chain top-down and concluding the message originated at your own mail server.
- Treating spf=pass as proof the From address is genuine. It is proof about the envelope domain, which the sender chose.
- Reading dkim=fail on a mailing list message as evidence of forgery. A list that appends a footer breaks the signature by design; check for an ARC chain before concluding anything.
- Copying headers out of a client that wraps or reflows long lines. A reflowed header block can break signature verification if you try to re-check it, and makes the Received chain hard to read. Copy from the raw source view, not from a rendered page.
- Pasting headers from a sensitive message into a public tool without thinking about what is in them. Headers carry recipient addresses, internal hostnames and internal IP addresses. Our header analyzer parses in your browser and sends only the IP addresses it found for network lookups, but that is a property of this tool rather than of header analyzers generally.