DevKitLab Logo DevKitLab
Email / Headers / DMARC / Security / Phishing

How to Read Email Headers (and Spot a Forged Sender)

Headers are the only first-hand record of how a message travelled. Read Authentication-Results, walk the Received chain backwards, and recognise the marks of a spoofed sender.

Someone forwards you a message and asks the question that has no short answer: “is this real?”

The body is no help. Logos are copied in seconds, tone is imitated, and a link’s visible text has never had to match where it goes. But every message carries a second document that the sender doesn’t fully control — the headers. That block records which servers handled the message, when, and what the receiving side concluded about who sent it. It’s the closest thing email has to a chain of custody.

This is a guide to reading it. If you want the model behind the acronyms first — what SPF, DKIM and DMARC each check and why alignment matters — SPF, DKIM, and DMARC: what each one actually does covers that. Here we assume it and read an actual message.

Getting the raw headers

What you need is the source, not what the client displays:

  • Gmail — open the message, the three-dot menu, Show original.
  • Outlook (desktop) — File → Properties, and read the Internet headers box.
  • Apple MailView → Message → All Headers, or ⌥⌘U for the raw source.
  • ThunderbirdCtrl+U.

Copy everything from the top down to the first blank line. That blank line is where headers end and the body begins.

One habit worth adopting before you paste that anywhere: headers contain the recipient’s address, internal hostnames, and often private IP ranges. That’s a reason to be careful about which tool you hand them to. The email header analyzer parses them entirely in your browser — nothing is uploaded — and it can mask mailbox names in what you copy back out, so a header block stays shareable with a colleague.

Start at the verdict: Authentication-Results

Don’t start at the top of the file. Start with the one header where a server wrote down its conclusions:

Authentication-Results: mx.example.net;
  spf=pass smtp.mailfrom=bounces.provider.com;
  dkim=pass header.d=yourcompany.com;
  dmarc=pass header.from=yourcompany.com

Read it as three separate findings plus the identity each one applies to:

  • spf=pass smtp.mailfrom=... — the connecting IP was authorised to send for that domain. Note the domain: it is frequently a provider’s bounce domain, not the one in the From line.
  • dkim=pass header.d=... — a signature verified, and it was made by the key of that domain.
  • dmarc=pass header.from=... — an SPF or DKIM pass belonged to a domain aligned with the visible From. This is the one that speaks to the sender the reader sees.

Two cautions about this header, and both matter.

Start with the topmost normal entry, but do not trust it simply because it is on top. It is usually the newest result, while anyone upstream can insert a header that says dmarc=pass. The authserv-id at the front (mx.example.net above) names the system making the claim; it matters only when it belongs to a mail system you actually trust — usually your own provider. A dmarc=pass from an authserv-id you do not recognise is still a stranger’s claim.

It is a record, not a re-check. The receiver evaluated these at delivery time and wrote down the result. Nothing you do now re-runs them, and DNS may have changed since.

If the header is missing entirely, that’s not proof of anything either — plenty of systems don’t add one. It just means this shortcut isn’t available and you’re reading the rest more carefully.

When spf=pass sits next to dmarc=fail

This pairing sends people hunting for a bug, and it’s the single most useful thing to recognise on sight:

Authentication-Results: mx.example.net;
  spf=pass smtp.mailfrom=bounces.mailer.example;
  dkim=none;
  dmarc=fail header.from=yourcompany.com

Nothing is broken. SPF passed — for bounces.mailer.example, a domain that has nothing to do with yourcompany.com. There was no DKIM signature to supply the other half. So no authenticated identity aligned with the From header, and DMARC failed.

That shape has two very different explanations, and telling them apart is the actual work:

  • A legitimate sender that was never configured to align — a marketing platform or ticketing system sending as you without DKIM signing under your domain. Extremely common, and fixable at the provider.
  • An outright forgery — someone else’s domain passing SPF while your name sits in the From line.

The rest of the headers are what separate the two.

Walk the Received chain backwards

Received: headers are the routing history. The crucial mechanic: each server prepends its own line at the top. So the chain reads newest-first — the topmost Received is the last hop, the bottom one claims to be the origin.

Received: from mx.example.net by inbox.example.net; Tue, 4 Aug 2026 10:00:12 +0000
Received: from mail.sender.example (mail.sender.example [198.51.100.7])
        by mx.example.net; Tue, 4 Aug 2026 10:00:09 +0000

Read from the bottom up to follow the message forward in time. Three things are worth extracting:

Where it entered your infrastructure. Find the lowest hop written by a server you trust. That’s the boundary — and it’s the important one, because everything below it was written by someone else. A forged message can carry an entirely invented history beneath that line, complete with plausible hostnames and timestamps. Below the boundary, treat every hop as a claim, not a fact.

The IP in brackets. Take the bracketed literal ([198.51.100.7]) rather than the hostname before it: the hostname is what the connecting server said it was, while the bracketed address is what your server observed. If the source IP doesn’t fit the sender’s story — a bank’s mail arriving from a residential range in an unrelated country — that’s a signal worth chasing. Run it through the IP address lookup to see who actually holds it.

The time between hops. Each hop carries a timestamp. Minutes of delay usually just mean queuing. What’s more interesting is a gap that doesn’t fit — a message that sat somewhere for hours, or hop timestamps that run backwards, which usually means a clock is wrong but occasionally means the chain was assembled by hand.

The identity fields, and where they disagree

A message states who it’s from in several places, and a forgery usually shows up as a disagreement between them.

From: — what the reader sees. Note that the display name is free text with no relationship to the address:

From: "Support <support@yourbank.example>" <ceo@random-domain.example>

Most clients show only Support <support@yourbank.example>. The actual address is the second one. An address embedded inside a display name can be a deliberate trick, so compare the real From address before acting. It is a strong warning in context, not proof by itself.

Return-Path: — where bounces go. This is the envelope sender recorded at delivery. It differs from the From domain constantly and legitimately — that is often just a provider handling bounces. It is useful context, but not proof of the identity SPF evaluated: SPF can use the SMTP MAIL FROM identity or, for a null reverse path, HELO. When present, read smtp.mailfrom or smtp.helo in Authentication-Results for the identity the receiver recorded.

Reply-To: — where your reply goes. Also legitimate in plenty of setups (a shared inbox, a helpdesk). But in a message that already fails DMARC, a Reply-To pointing at an unrelated domain is the mechanism by which a conversation gets redirected to the attacker.

Message-ID: — the assigned identifier. Its domain part is usually the generating system. Different from the From domain is normal when a provider sends on your behalf. Malformed or missing is worth noting.

None of these is damning alone. That’s the point: the signal is the pattern, not any single field. A From domain under DMARC failure, plus a display name containing a different address, plus a Reply-To elsewhere, is a coherent story that a legitimate misconfiguration doesn’t tell.

Reading a DKIM signature

If there’s a DKIM-Signature: header, a few tags decide what it’s worth:

DKIM-Signature: v=1; a=rsa-sha256; d=yourcompany.com; s=selector1;
  h=from:to:subject:date; bh=...; b=...
  • d= — the signing domain. Compare it to the From domain. A perfectly valid signature from an unrelated domain contributes nothing to DMARC.
  • s= — the selector, which with d= locates the public key at selector1._domainkey.yourcompany.com. You can fetch that TXT record yourself with the DNS lookup tool to confirm a key is published there.
  • h= — the list of headers covered. A signature that doesn’t include from is close to worthless: the visible sender can be swapped without disturbing it. Worth checking explicitly, because it’s a subtle failure that still reports as dkim=pass.
  • l= — a body length limit, signing only the first N bytes. Anything appended past that is unsigned. Uncommon, and a reason for suspicion when present.

One limit to be honest about: reading a signature is not verifying it. Verification needs the unmodified body and the public key from DNS, and any change in transit invalidates a signature that was perfectly good when written. The analyzer reads the fields and checks the structural properties above; the dkim=pass in Authentication-Results is the receiver’s verification, done at delivery.

The ARC headers, and what they’re worth

Forwarding and mailing lists break authentication by design: SPF fails because the relay’s IP isn’t yours, and DKIM fails if the list touched the message. ARC (Authenticated Received Chain, RFC 8617) exists to carry the earlier verdict across that break. Read real mail from Google or Microsoft and you’ll see it constantly:

ARC-Seal: i=1; a=rsa-sha256; cv=none; d=lists.example.org; s=arc; b=...
ARC-Message-Signature: i=1; a=rsa-sha256; d=lists.example.org; s=arc;
  h=from:to:subject; b=...
ARC-Authentication-Results: i=1; lists.example.org;
  spf=pass smtp.mailfrom=you@yourcompany.com;
  dkim=pass header.d=yourcompany.com; dmarc=pass

Three headers per intermediary, numbered by i=:

  • ARC-Authentication-Results — the authentication results that hop saw, before it changed anything. This is the actual payload.
  • ARC-Message-Signature — that hop’s signature over the message, so its snapshot can’t be edited afterwards.
  • ARC-Seal — a signature over the chain itself, which is what makes it a chain rather than a pile of separate claims.

Read i= as position: i=1 is the first intermediary, i=2 resealed after it. On the highest-numbered ARC-Seal, cv= reports chain validationnone for the first hop, pass if the chain so far verified, fail if it didn’t. A cv=fail means the chain broke somewhere, and the results it carries prove nothing.

Now the part that matters for judging a message: ARC is not a bypass, and a valid chain is not a DMARC pass. Anyone can attach an ARC set asserting the message passed everything — an attacker included. It counts as evidence only if the final receiver trusts the sealing domain, which is the same test you applied to the authserv-id earlier. Receivers decide for themselves which intermediaries they honour; ARC gives them material for that decision, not an obligation.

So read an ARC chain as an explanation, not a verdict: it tells you why a forwarded message failed SPF and DKIM and was delivered anyway. The analyzer reports how many ARC sets it found and the chain validation state, so you can see at a glance whether the message passed through intermediaries at all.

What forged and legitimate-but-broken look like side by side

Both fail DMARC. The headers tell them apart:

SignalMisconfigured but legitimateLikely forged
Source IPA known provider’s rangeUnrelated hosting, residential, or a mismatched country
SPF domainProvider’s bounce domainAn unrelated throwaway domain
DKIMAbsent, or d= is the provider’sAbsent, or signed by an unrelated domain
Display nameOrdinaryContains an email address, or imitates a different sender
Reply-ToAbsent, or same organisationUnrelated domain
Received chainCoherent, plausible hostnamesGaps, odd timestamps, invented hops below your boundary
PatternOne thing is wrongSeveral point the same way

The honest summary: a single anomaly usually means a misconfiguration; several pointing the same direction mean you should treat the message as hostile.

A reading checklist

  1. Get the raw source and copy down to the first blank line.
  2. Read Authentication-Results first — and check the authserv-id belongs to a system you trust.
  3. Note which domain each result applies to, not just pass or fail. spf=pass on a stranger’s domain proves nothing about the From line.
  4. Find your trust boundary in the Received chain. Everything below it is a claim.
  5. Take the bracketed IP from the entry hop and check who owns it.
  6. Compare the identity fields — From address versus display name, Return-Path, Reply-To, Message-ID.
  7. Check d= and h= on any DKIM signature; confirm from is covered.
  8. If there are ARC sets, check cv= on the highest one — and ask whether you trust the domain that sealed it.
  9. Weigh the pattern, not one field.

Rather than doing that by eye every time, paste the block into the email header analyzer: it lays out the hop-by-hop path with the time each hop cost, parses the authentication results instead of just echoing them, flags display-name spoofing and identity mismatches, and takes DKIM signatures apart field by field. It runs locally, so the message stays on your machine.

And when the conclusion is that your own domain is the one being forged — or that a legitimate sender of yours can’t pass alignment — that’s a configuration problem rather than a forensic one. Check what your domain publishes with the SPF and DMARC checker, and if the answer is that enforcement isn’t switched on yet, moving from p=none to p=reject is the staged way to fix it without losing mail along the way.