How to Read Email Headers: One Real Dump, Line by Line
An email header is the message’s delivery record — every server that touched it, every authentication check that ran, and every verdict those checks produced. Learning how to read email headers is how you decide whether a specific message is what it claims to be. The trick is knowing which lines your own infrastructure wrote and which came from whoever sent it.
You are probably here with one message open: a user forwarded something that
looks like it came from the CFO, or legitimate forwarded mail keeps landing in
Junk. Note that this is not an aggregate report —
reading a DMARC aggregate report is a different job,
summarizing millions of messages in XML, where a header is the record of
exactly one. Below: a complete redacted dump read top to bottom, the trust
boundary drawn explicitly, the ARC set dissected tag by tag, and the vendor
X-* headers decoded against their publishers’ own keys.
40.8% of scanned domains publish no email authentication at all (2,243,877 of 5,499,028)
For senders like those, your Authentication-Results line reads
spf=none dkim=none dmarc=none. A none is not a fail — it means nothing was
published to check, so the header tells you only that the domain never made a
claim you could test.
Where to find the raw headers
Every mail client exposes the raw source somewhere: Gmail’s “Show original”, Outlook on the web’s “View message source”, Apple Mail’s View → Message → All Headers, Thunderbird’s Ctrl+U. Export the whole thing, not a screenshot of the summary pane — the fields you need are the ones the client hides.
Our retrieval reference already has the per-client steps in a table, so use it to pull the raw source from your mail client, then come back with the text. A raw header decoder only works on what you paste into it, and clients reorder and pretty-print for display — the physical order of the lines is itself evidence, as the ARC section shows.
Every sample here is redacted the same way, so you can line yours up against
ours: mailboxes become user@example.com, hostnames mail.example.net,
subjects are replaced, addresses move into the documentation ranges
192.0.2.0/24, 198.51.100.0/24 and 2001:db8::/32, and base64 is truncated
with an ellipsis, never invented. Field order of every retained line is exactly
as captured; about ten diagnostic X-* fields were dropped for length, none
from between the lines shown. Both captures are public list archives — MIT,
2025-12-09, Microsoft 365 at the edge, and the
IEEE 802.3 EFMP list, 2026-03-26, a
Google two-hop chain. We link the archive index, not the messages, because a
direct link would undo the redaction.
How to read email headers: the trust order, not the time order
Read an email header outward from your own infrastructure. The newest
Received: line sits at the top and the oldest at the bottom, so the physical
top of the file is the last hop before delivery — but the useful reading order
is not “bottom to top”, it is “from the first line your own servers wrote,
upward”. Everything below that line is the sender’s account of events.
Here is a real message, captured from the MIT mailing-list archive on 2025-12-09 and redacted as above. The recipient organization runs Microsoft 365 at its edge and an internal list host behind it, so two infrastructures appear.
# Real message, captured 2025-12-09 from a public mailing-list archive.
# Redacted: mailboxes -> user@example.com, sender domains -> example.net /
# example.org / esp.example, subject line replaced, tenant names and GUIDs
# replaced, country code masked, base64 truncated with "..." and never invented.
# Addresses moved into the documentation ranges 192.0.2.0/24, 198.51.100.0/24
# and 2001:db8::/32; the one RFC 1918 address (10.167.242.41, inside the
# receiving infrastructure) is left as captured because it identifies nothing.
# Microsoft's own shared infrastructure hostnames are left intact -- recognizing
# them is the point. Field order of every RETAINED line and every tag are
# exactly as captured; roughly ten diagnostic X-* fields were dropped for
# length, and none of the dropped lines sat between the ones shown.
Return-Path: <bounces+SRS=aBcDe=1X@exampleorg.onmicrosoft.com>
Delivered-To: user@example.com
Received: (qmail 10692 invoked from network); 9 Dec 2025 05:35:35 -0000
Received: from mail-westcentralusazon11013009.outbound.protection.outlook.com
(HELO CY3PR05CU001.outbound.protection.outlook.com) (198.51.100.24)
by list.example.com with SMTP; 9 Dec 2025 05:35:35 -0000
ARC-Seal: i=1; a=rsa-sha256; s=arcselector10001; d=microsoft.com; cv=none;
b=pjhLg4r9H8RErNashOh7G5pdILkjxL0jg1+7Jl7pOwsEsIEzKisa9Car0GXVBk2AglXGxh5...
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com;
s=arcselector10001;
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1;
bh=2ufHaa+foYewDMWg/jX9v2weX+vKIraWnocUpjD6VxU=;
b=nf1GP9LHHPsrsZajp85YjFrNOus1iUO7MCjlMbo5AZEfVCnYM7pVRPEk7cbonQOqh3s...
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=pass (sender ip is
192.0.2.10) smtp.rcpttodomain=example.com
smtp.mailfrom=example.net; dmarc=bestguesspass action=none
header.from=example.net; dkim=pass (signature was verified)
header.d=example.net; dkim=pass (signature was verified)
header.d=fbl.esp.example; arc=none (0)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=exampleorg.onmicrosoft.com; s=selector2-exampleorg-onmicrosoft-com;
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
bh=2ufHaa+foYewDMWg/jX9v2weX+vKIraWnocUpjD6VxU=;
b=lT9mrgkoW8MI9YtgzntSgZyWaO5nb6nVg1kZZh93/TWwn9powZVsBFbzGmLxo2VgU/o...
Received: from DM6PR06CA0043.namprd06.prod.outlook.com (2001:db8:5:54::20) by
DS4PR01MB9275.prod.exchangelabs.com (2001:db8:8:281::14) with Microsoft SMTP
Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id
15.20.9388.14; Tue, 9 Dec 2025 05:35:32 +0000
Received: from CY4PEPF0000EE35.namprd05.prod.outlook.com
(2001:db8:5:54:cafe::d0) by DM6PR06CA0043.outlook.office365.com
(2001:db8:5:54::20) with Microsoft SMTP Server (version=TLS1_3,
cipher=TLS_AES_256_GCM_SHA384) id 15.20.9388.14 via Frontend Transport; Tue,
9 Dec 2025 05:35:31 +0000
Authentication-Results: spf=pass (sender IP is 192.0.2.10)
smtp.mailfrom=example.net; dkim=pass (signature was verified)
header.d=example.net;dkim=pass (signature was verified)
header.d=fbl.esp.example;dmarc=bestguesspass action=none
header.from=example.net;
Received-SPF: Pass (protection.outlook.com: domain of example.net
designates 192.0.2.10 as permitted sender)
receiver=protection.outlook.com; client-ip=192.0.2.10;
helo=out21-142.mail.example.net; pr=C
# ---------------- TRUST BOUNDARY ----------------
# The line below is the lowest Received: this organization's own
# infrastructure wrote. Everything ABOVE it is attested. Everything
# BELOW it is the sender's account of events.
Received: from out21-142.mail.example.net (192.0.2.10) by
CY4PEPF0000EE35.mail.protection.outlook.com (10.167.242.41) with Microsoft
SMTP Server (version=TLS1_3, cipher=TLS_AES_256_GCM_SHA384) id 15.20.9412.4
via Frontend Transport; Tue, 9 Dec 2025 05:35:30 +0000
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=example.net; s=esp-relay-01;
t=1765258528; h=Date:From:To:Message-ID:Subject:MIME-Version:Content-Type;
bh=2ufHaa+foYewDMWg/jX9v2weX+vKIraWnocUpjD6VxU=;
b=Rrj9vDUobOOv+0QH063neCNcWOsFqFEZeU7P4kFkbuOcxydJslIcvwDXmZIkYWOUwm9...
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=fbl.esp.example; s=feedback;
t=1765258528; h=Date:From:To:Message-ID:Subject:MIME-Version:Content-Type;
bh=2ufHaa+foYewDMWg/jX9v2weX+vKIraWnocUpjD6VxU=;
b=2ObMkjNAdlD6AzgRWJdJ+VXACDac45u0NN15UcszG0pl87pFIiWAzCkKodQtN2c6sQO...
Received: from mta-07(mailfrom:sales@example.net fp:ma_600000253313635856 cluster:AY35D)
by smtp.mail.example.net(127.0.0.1);
Tue, 09 Dec 2025 13:35:05 +0800
Date: Tue, 09 Dec 2025 13:35:05 +0800
From: "Sales" <sales@example.net>
Return-Path:
random_NjAwMDAwMjUzMzEzNjM1ODYx@example.net
To: <user@example.com>
Reply-To: <replies@example.org>
Message-ID: <d64d0d50-3da6-4820-974a-19dc2bbd7093@mail.esp.example>
Subject: Product enquiry - replacement parts
MIME-Version: 1.0
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable
X-Microsoft-Antispam:
BCL:0;ARA:13230040|61400799027|48200799018|9140799003|...;
X-Forefront-Antispam-Report:
CIP:192.0.2.10;CTRY:XX;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:out21-142.mail.example.net;PTR:out21-142.mail.example.net;CAT:NONE;SFS:(13230040)(61400799027)(48200799018)(9140799003)(376014);DIR:OUT;SFP:1101;
X-OriginatorOrg: exampleorg.onmicrosoft.com
X-MS-Exchange-CrossTenant-OriginalArrivalTime: 09 Dec 2025 05:35:30.7365 (UTC)
X-MS-Exchange-CrossTenant-AuthAs: Anonymous
X-MS-Exchange-CrossTenant-FromEntityHeader: Internet Walk it in five passes. The Received: stack (RFC 5321) is the delivery
path: each hop prepends a line naming who it received from, which host took it
by, and when. Authentication-Results (RFC 8601) carries the SPF, DKIM
and DMARC verdicts the receiving side computed — here it sits above the boundary
line, exactly where the spec expects a trustworthy one to be. The ARC block
records what an intermediary saw. From, Reply-To and Return-Path are
the identity claims, and here they disagree three ways: the Reply-To domain is
not the From domain, the Message-ID domain is neither, and two Return-Path
values appear because a relay rewrote the envelope sender. The vendor X-*
block at the bottom carries the filtering verdict.
Now the correction that matters most. “Read bottom to top” is the right
chronological order and the wrong trust order. The bottom Received: line
is where the sender claims the message originated, which is exactly where
someone fabricating one puts it. A
2007 AnandTech thread
makes the mistake and corrects it in-thread — “you can only ‘trust’ the last
header you can verify” — and that correction has held for nearly two decades.
That thread is old; the behavior is not. It is cited for durable behavior,
never for a number.
One arithmetic trap: each Received: line carries the recording server’s own
local offset. Above, the sender’s hop stamps 13:35:05 +0800 and the receiving
hop 05:35:30 +0000 — the same wall-clock instant, thirty seconds apart.
Subtract the raw clock faces and the message appears to arrive eight hours
before it was sent. Normalize every hop to UTC before you do any arithmetic
at all.
What is the trust boundary in an email header?
The trust boundary is the first Received: line your own mail infrastructure
added — the lowest line in the file whose by clause names a host you operate.
Lines above it were recorded by hosts you run and can be trusted; lines below
it were supplied by the sender and can be fabricated. The same rule governs
Authentication-Results: only a line carrying your own authserv-id is
evidence, because a message can arrive with a forged one already in it.
That is not a heuristic we invented. RFC 8601 §5 requires a conforming MTA to
delete any Authentication-Results header claiming, by its authserv-id, to
have been added inside its own trust boundary but which did not come from
another trusted MTA. §7.1 spells out why: an attacker can forge the header
using the receiving domain’s own name, and a non-conformant MTA will pass it
straight through to the mail client.
| Field | Who writes it | Trustworthy? | What it actually proves |
|---|---|---|---|
| From / display name | Sender | No | Nothing on its own — DMARC alignment is the test |
| Reply-To | Sender | No | Where replies go, chosen by the sender |
| Message-ID / Date | Originating client | No | Nothing; a mismatched Message-ID domain is a tell, not proof |
| X-Originating-IP / X-Sender-IP | Outside your boundary | No | Non-authoritative; the same class of header as X-Forwarded-For |
| Received: below your boundary | Sender or upstream | No | The sender's account of the path |
| Received: at or above your boundary | Your infrastructure | Yes | The connecting IP your server actually saw |
| Authentication-Results under your authserv-id | Your infrastructure | Yes | The verdicts your server computed |
| Authentication-Results under any other authserv-id | Unknown | No | Nothing — it may have been planted by the sender |
SpamAssassin operationalizes the same split: internal_networks (your MX and
relays) should be a subset of trusted_networks (those plus external relays you
trust not to forge headers), and each defaults to the other when only one is
set. Both are exposed as the
X-Spam-Relays-Trusted and -Untrusted pseudo-headers,
with
authres_trusted_authserv
gating the same decision for Authentication-Results. Draw the boundary one hop
too wide and it shows: behind NAT, SpamAssassin cannot tell from Received:
lines whether your MX has a public address, assumes it does, and
fires ALL_TRUSTED on inbound spam.
One more distinction if you want to decode email header fields correctly: SPF
(RFC 7208) evaluates the envelope sender, the MailFrom of RFC 5321, which is
what Return-Path records. DMARC (RFC 9989) then tests whether the visible
RFC 5322 From aligns with whatever passed. An spf=pass alone vouches for the
envelope, not for the name your user sees —
Microsoft documents the same 5321-versus-5322 split.
How to read an ARC set: ARC-Seal, ARC-Message-Signature, ARC-Authentication-Results
An ARC set is three headers written together at one hop, defined in RFC 8617.
ARC-Authentication-Results (AAR) records the verdicts that hop computed,
ARC-Message-Signature (AMS) signs the message as that hop saw it, and
ARC-Seal (AS) signs the chain itself and carries cv=, the chain-validation
status. All three share an instance number i=, and a forwarded message
carries one complete set per hop.
# A two-hop Google chain, captured 2026-03-26 from a public standards-list
# archive (Gmail sender -> LISTSERV mailing list -> Gmail recipient), plus a
# Microsoft set for contrast. Redacted per the convention in this post;
# base64 truncated with "..." and never invented.
#
# NOTE ON ORDER: the source archive renders header fields alphabetically, so
# the grouping below is by instance, not wire order. In a RAW dump the observed
# physical order within one set is ARC-Seal -> ARC-Message-Signature ->
# ARC-Authentication-Results, and the i=2 set sits ABOVE the i=1 set.
# ---- Google, instance 1: the origin hop ----
ARC-Seal: i=1; a=rsa-sha256; t=1774552407; cv=none; d=google.com;
s=arc-20240605;
b=FYoRL24mDIPQ3QGYABAxNwRrjObaCFAcKRMV16hkh2CxurYhuCIdX608UBD0rbh/u6...
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com;
s=arc-20240605;
h=to:subject:message-id:date:from:mime-version:dkim-signature;
bh=EPXSm8djzZDDzCjsfhHsyKYYHxgNzzDg4bqqd3Tvc7k=;
fh=eNOXSQWankAFvswhJk6Gounc4+lPTV6KRKuORC+YdQI=;
b=Dx5a0BugxavAvo83XQjIwufw5O5eGW56up8zvw6uRl9wVhtLqF480Jyywomb/dF0+P...;
darn=listserv.example.net
ARC-Authentication-Results: i=1; mx.google.com; arc=none
# ^ cv=none at i=1 is mandatory (RFC 8617). arc=none in the AAR says this
# sealer found no chain to inherit -- it is the first link.
# ---- Google, instance 2: the hop after the mailing list ----
ARC-Seal: i=2; a=rsa-sha256; t=1774552409; cv=pass; d=google.com;
s=arc-20240605;
b=CH02yJcjGJ5KTPBas4hn0TnAM7+BW+c4rEkPOSl0Tu5nStJv46LBWvZFw4V+SyeTbx...
ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed; d=google.com;
s=arc-20240605;
h=to:subject:message-id:date:from:mime-version:dkim-signature;
bh=EPXSm8djzZDDzCjsfhHsyKYYHxgNzzDg4bqqd3Tvc7k=;
fh=eNOXSQWankAFvswhJk6Gounc4+lPTV6KRKuORC+YdQI=;
b=EtGVhU/pwQW77P1PvGzUlRXmkdM6WPca7EWY00+bblkq6CxMjhClgGxycB/bUv8RNk...;
dara=google.com
ARC-Authentication-Results: i=2; mx.google.com;
dkim=pass header.i=@example.com header.s=google header.b="IU/01rcm";
arc=pass (i=1);
spf=pass (google.com: domain of user@example.com designates 192.0.2.1 as
permitted sender) smtp.mailfrom=user@example.com;
dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.com;
dara=neutral header.i=@listserv.example.net
# ^ THIS is the mechanism. The i=2 AAR carries the SPF, DKIM and DMARC
# verdicts Google computed at that hop, plus arc=pass (i=1) naming the
# instance it validated. The bh= at i=1 and i=2 is byte-identical, so the
# body never changed and the chain held.
# Note the asymmetry: the i=1 AAR above carries ONLY arc=none. Gmail's
# origin seal records no SPF/DKIM/DMARC verdicts at all, so in THIS chain
# the earliest verdicts sit at i=2. RFC 8617's DMARC-override model reads
# the i=1 AAR for the original results -- that is the rule; this capture
# is Gmail behaviour, not a counter-example to it.
#
# fh=, dara= and darn= are NOT in RFC 8617. All three are defined in
# draft-chuang-replay-resistant-arc-11 (W. Chuang, Google; B. Gondwana,
# Fastmail; 20 February 2024), an Internet-Draft that has since EXPIRED,
# so treat the meanings as provisional:
# fh= base64 hash over the recipient-disclosure headers, so a verifier
# can prove they were not altered
# darn= the recipient domain name when that receiver is NAIVE of the
# DARA protocol -- here darn=listserv.example.net at i=1, i.e.
# "the next hop is a list that does not speak DARA"
# dara= the receiver's ARC-Seal d= domain when the receiver IS DARA
# capable -- here dara=google.com at i=2
# The draft places these tags on the DKIM-Signature or ARC-Seal; Google
# writes them on the ARC-Message-Signature. In the AAR, dara= is a result
# (pass / fail / neutral) -- dara=neutral above.
# ---- Microsoft, instance 1, for contrast ----
ARC-Seal: i=1; a=rsa-sha256; s=arcselector10001; d=microsoft.com; cv=none;
b=pjhLg4r9H8RErNashOh7G5pdILkjxL0jg1+7Jl7pOwsEsIEzKisa9Car0GXVBk2AglXGxh5...
# ^ no t= timestamp, and s= before d= -- Google writes t= and puts d= first.
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com;
s=arcselector10001;
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1;
bh=2ufHaa+foYewDMWg/jX9v2weX+vKIraWnocUpjD6VxU=;
b=nf1GP9LHHPsrsZajp85YjFrNOus1iUO7MCjlMbo5AZEfVCnYM7pVRPEk7cbonQOqh3s...
# ^ no fh=, no dara= -- and h= is dominated by Microsoft's own X-MS-* names.
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=pass (sender ip is
192.0.2.10) smtp.rcpttodomain=example.com smtp.mailfrom=example.net;
dmarc=bestguesspass action=none header.from=example.net;
dkim=pass (signature was verified) header.d=example.net; arc=none (0)
# ^ authserv-id "mx.microsoft.com 1", a smtp.rcpttodomain= property no other
# sealer emits, dmarc=bestguesspass, and a numeric reason after arc=. Read cv= first, because it has an invariant you can check at a glance. RFC
8617 requires cv=none on i=1 and cv=pass on every instance above it; a
cv=fail anywhere means the chain is broken and cannot recover. Instance
numbers run 1 to N with no gaps, capped at 50.
Then read down, not up. After cv=, the instance that matters is i=1:
RFC 8617’s DMARC-override model has the receiver examine the i=1 AAR for the
original authentication results, because that is the hop closest to origin.
Higher instances record what each later intermediary saw. Our free ARC chain
analyzer walks a set in that order for you, but the rule is worth holding in
your head, because plenty of guides tell you to read the outermost AAR and
stop.
Here is what the capture above actually shows. At i=2 the AAR carries the
verdicts Google computed at that hop — spf=pass, dkim=pass,
dmarc=pass (p=REJECT sp=REJECT dis=NONE) — plus arc=pass (i=1) naming the
instance it validated. The bh= body hash at i=1 and i=2 is byte-identical,
which is the proof the body was never altered, and is why the chain held. Note
the asymmetry: the i=1 AAR in this chain carries only arc=none. Gmail’s
origin seal records no SPF, DKIM or DMARC verdicts at all, so here the earliest
verdicts available sit at i=2. That is Gmail behavior in this capture, not
the general rule.
And a correction worth owning. The presence of an ARC set is not evidence
that a message was forwarded through a third party. In the Microsoft set above,
the AAR ends arc=none (0) — the sealer is telling you it found no prior chain
to inherit. One set with cv=none and arc=none means “a sealer touched this
and there was nothing before it,” which is a different statement entirely.
Sealers also differ inside the same RFC, and the differences are what let you recognize whose infrastructure you are looking at:
| Header | Google (arc-20240605) | Microsoft (arcselector10001) |
|---|---|---|
| AS | i= a= t= cv= d= s= b= | i= a= s= d= cv= b= — no t=, and s= before d= |
| AMS | i= a= c= d= s= h= bh= b= + fh=, dara=/darn= | i= a= c= d= s= h= bh= b= — no fh=, no dara= |
| AAR | i= + mx.google.com + methods | i= + mx.microsoft.com 1 + smtp.rcpttodomain= |
Google’s AMS carries three tags RFC 8617 never mentions: fh=, plus darn= on
the i=1 instance and dara= on i=2. They are not undocumented, only
unstandardized. All three are defined in
draft-chuang-replay-resistant-arc,
an Internet-Draft by Wei Chuang (Google) and Bron Gondwana (Fastmail), version
11 dated 20 February 2024 — expired, not a standard, so treat the meanings as
provisional. In that draft fh= is a hash over the recipient-disclosure
headers so a verifier can prove they were not altered; darn= carries the
recipient domain name when that receiver is naive of the draft’s DARA
recipient-validation protocol; and dara= carries the receiver’s ARC-Seal
d= domain when the receiver is DARA-capable. Read against the capture, the
pair decodes cleanly: darn=listserv.example.net at i=1 says the next hop is
a mailing list that does not speak DARA, and dara=google.com at i=2 names
the DARA-capable receiver on the far side. The draft puts these tags on the
DKIM-Signature or ARC-Seal; Google writes them on the AMS instead.
Microsoft’s AAR is the more distinctive of the two: the authserv-id
mx.microsoft.com 1 carries a trailing numeric token, smtp.rcpttodomain= is a
recipient-domain property that appears in neither of our other captures,
dmarc=bestguesspass is
Microsoft’s documented token
for “no DMARC record exists, but the check would pass if one did”, and arc=
closes with a numeric reason — arc=none (0) here, arc=fail (47) on a broken
chain.
Physical order carries information too. In the raw MIT dump the order within one
set is AS → AMS → AAR, sitting above the Received: lines that hop wrote. Sets
are prepended like any other header, so i=2 necessarily sits above i=1 —
that follows from the prepend rule, not from either capture: the MIT message has
one set and the IEEE archive renders fields alphabetically. In practice i=2 is
the ceiling you will meet.
One gap stated plainly, because that beats a reconstruction. Fastmail says it is “not only validating and adding ARC headers” but also using them between trusted systems (Fastmail, March 2023). That post does not name its signing domain or enumerate the three headers, and no dated public capture of Fastmail’s literal sealed triple could be found, so we print neither.
Three questions this section deliberately does not answer, each with a home: what ARC is, and how to configure trusted sealers in Microsoft 365 and Google Workspace; why a chain fails and what each reason string means; and which receivers seal ARC and which honor a seal.
Why every provider writes Authentication-Results differently
RFC 8601 defines the syntax of Authentication-Results, but each provider
populates it differently — different authserv-id, different method order,
different optional properties. Knowing your provider’s dialect is what stops
you reading a missing field as a failure.
# Four Authentication-Results dialects. Two are dated live captures, one is
# a dated capture over two years old, and one is a vendor-repo sample with no
# capture date -- each is labelled below, because "we could not date this" is
# part of the evidence. Redacted per the convention in this post. Line
# wrapping is ours; every token, its order and its spelling are as captured.
# ---- Microsoft 365 / Exchange Online (captured 2025-12-09) ----
# No leading authserv-id at all: the value starts straight at spf=.
# action=none is a bare token, not the ptype.property form RFC 8601 defines.
Authentication-Results: spf=pass (sender IP is 192.0.2.10)
smtp.mailfrom=example.net; dkim=pass (signature was verified)
header.d=example.net;dkim=pass (signature was verified)
header.d=fbl.esp.example;dmarc=bestguesspass action=none
header.from=example.net;
# ---- Google Workspace / Gmail (captured 2026-03-26, from the i=2 AAR) ----
# authserv-id is always mx.google.com. Richest DKIM property set of the four:
# header.i=, header.s= and a truncated header.b= fragment. The DMARC
# parenthetical carries all three of p=, sp= and dis=.
Authentication-Results: mx.google.com;
dkim=pass header.i=@example.com header.s=google header.b="IU/01rcm";
spf=pass (google.com: domain of user@example.com designates 192.0.2.1 as
permitted sender) smtp.mailfrom=user@example.com;
dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.com
# ---- Mimecast (VENDOR-REPO SAMPLE -- undated, provenance is a connector
# repository, not a capture of live mail; shown for shape only) ----
# authserv-id relay.mimecast.com. Method order is dkim -> dmarc -> spf, with
# SPF last. Terse (policy=none) parenthetical. The spam verdict is NOT here --
# it lives in a separate integer X-Mimecast-Spam-Score: line.
Authentication-Results: relay.mimecast.com;
dkim=pass header.d=example.com header.s=selector header.b=QuZ7ohIh;
dmarc=pass (policy=none) header.from=example.com;
spf=pass (relay.mimecast.com: domain of user@example.com designates
192.0.2.1 as permitted sender) smtp.mailfrom=user@example.com
# ---- Zoho (captured 2021-09-15 -- over two years old; shown for shape) ----
# authserv-id mx.zohomail.com. Sparsest DKIM result of the four: a bare
# dkim=pass with no header.d, header.i or header.b. Reports arc= inline with
# a free-text reason in parentheses.
Authentication-Results: mx.zohomail.com;
dkim=pass;
spf=pass (zohomail.com: domain of mail.example.net designates 192.0.2.1 as
permitted sender) smtp.mailfrom=user@example.com;
arc=fail (Bad Signature) | Platform | authserv-id | Method order | Distinguishing token |
|---|---|---|---|
| Microsoft 365 | none emitted | spf → dkim → dmarc | bare action=none; compauth=pass reason=NNN (per docs) |
| Google Workspace | mx.google.com | dkim → spf → dmarc | header.b= fragment; (p= sp= dis=) |
| Mimecast | relay.mimecast.com | dkim → dmarc → spf | verdict lives in X-Mimecast-Spam-Score: |
| Zoho | mx.zohomail.com | dkim → spf → arc | bare dkim=pass, no header.d |
Microsoft 365 is the outlier that trips parsers: its line starts straight at
spf= with no leading authserv-id, which RFC 8601’s grammar requires. It also
adds compauth, a composite-authentication result with a numeric reason that
combines SPF, DKIM, DMARC and other signals against the From domain — that one
comes from Microsoft’s documentation, not from our capture, which carries no
compauth token. Google is the most verbose, populating header.i=,
header.s= and a truncated header.b= on every DKIM result; its line above is
lifted from the i=2 AAR, because the archive that preserved the message kept
no top-level Authentication-Results. Mimecast reorders the methods and moves
the spam verdict out of the header entirely — that sample comes from a vendor
connector repository and carries no capture date, which is why the snippet
labels it as such. Zoho emits the sparsest DKIM result of the four, a bare
dkim=pass, from a 2021 capture shown only for shape.
Three dialects we could not verify, said out loud rather than
reconstructed: Fastmail’s top-level line, a Proofpoint line with a *.ppops.net
authserv-id, and a branded Barracuda authserv-id. Barracuda expresses its
verdict through X-Barracuda-* and SpamAssassin-style headers instead, and
hosted Proofpoint deployments typically show the downstream receiver as the
authserv-id. If your dump has one of these, trust what you see over what any
guide told you to expect.
The vendor X-headers worth decoding
X-* headers are vendor-specific and carry no cross-platform meaning, but a
handful have published decoding keys that turn a bare number into a verdict.
Microsoft’s Forefront report, Microsoft’s bulk complaint level, Mimecast’s spam
score and SpamAssassin’s X-Spam-Status are the four you will hit most.
# Vendor X-* headers with published decoding keys, redacted per the convention
# in this post. Every key below was re-checked against the vendor's own
# documentation on 2026-08-19.
# ---- Microsoft: X-Forefront-Antispam-Report (captured 2025-12-09) ----
X-Forefront-Antispam-Report:
CIP:192.0.2.10;CTRY:XX;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;
H:out21-142.mail.example.net;PTR:out21-142.mail.example.net;CAT:NONE;
SFS:(13230040)(61400799027)(48200799018);DIR:OUT;SFP:1101;
#
# CIP: the connecting IP address, as Microsoft's edge saw it
# H: the HELO/EHLO string the connecting server offered
# PTR: reverse DNS of the source IP
# IPV:NLI the IP was not found on any IP reputation list
# SFV:NSPM spam filtering marked the message non-spam and delivered it
# CAT: which threat policy fired -- SPM spam, HSPM high-confidence spam,
# PHSH phishing, SPOOF spoofing, BULK bulk, MALW malware. The CAT:NONE
# in the capture above is observed-but-undocumented: NONE does not
# appear in Microsoft's published CAT list.
# DIR: directionality -- INB inbound, OUT outbound, INT internal
# SCL: spam confidence level. Microsoft's current guidance: in cloud
# organizations this value does NOT determine the verdict or the
# action -- read CAT: and DIR: instead. SCL is primarily for
# on-premises Exchange, where 5 or higher generally means "bad".
# ---- Microsoft: X-Microsoft-Antispam ----
X-Microsoft-Antispam: BCL:0;ARA:13230040|61400799027|...;
#
# BCL:0 not from a bulk sender
# BCL:1,2,3 bulk sender that generates few complaints
# BCL:4,5,6,7 bulk sender that generates a mixed number of complaints
# BCL:8,9 bulk sender that generates a high number of complaints
# Threshold: 7 in the default anti-spam policy, 6 Standard preset,
# 5 Strict preset. At or above the threshold: Junk (default/Standard) or
# Quarantine (Strict). PCL: on the same header is phish confidence.
# ---- Mimecast: X-Mimecast-Spam-Score ----
X-Mimecast-Spam-Score: 4
#
# A single integer. 28 or higher is rejected in protocol regardless of any
# policy. Below that, the hold band depends on the scan setting:
# 7-27 Relaxed, 5-27 Moderate, 3-27 Aggressive.
# ---- SpamAssassin: X-Spam-Status / X-Spam-Level ----
# (seen behind Barracuda, Zimbra, cPanel and amavisd deployments)
X-Spam-Status: No, score=-1.2 required=5.0
tests=DKIM_SIGNED,DKIM_VALID,SPF_PASS,URIBL_BLOCKED
autolearn=ham autolearn_force=no version=3.4.4
X-Spam-Level:
#
# Yes/No is the verdict; score= is the total; required= is the configured
# threshold; tests= names every rule that matched. X-Spam-Level renders the
# rounded score as a run of asterisks -- empty here because the score is
# negative. X-Forefront-Antispam-Report is where a Microsoft 365 verdict actually lives.
CIP: is the connecting IP, H: the HELO string, PTR: its reverse DNS,
SFV: the filter verdict, CAT: the category that fired, and DIR: the
directionality — the snippet above lists the values for each, and flags
CAT:NONE as observed-but-undocumented, since NONE is not in Microsoft’s
published category list.
SCL: is the trap, because most guides still print a key Microsoft has retired.
Its current documentation
says of the SCL value in cloud organizations: “The value doesn’t determine
whether spam filtering identifies a message as Spam or High confidence spam, and
it doesn’t determine the action taken on the message.” The same page notes that
the same SCL value can therefore appear on messages with different verdicts, and
tells you to read CAT: and DIR: instead. SCL keeps its meaning on-premises,
where 5 or higher generally indicates a bad message.
Bulk complaint level
on X-Microsoft-Antispam is one scale: BCL:0 is not bulk, 1–3 few complaints,
4–7 a mixed number, 8–9 a high number, with the default anti-spam policy acting
at 7 (Standard preset 6, Strict 5).
Mimecast’s spam score
is one integer: 28 or higher is rejected in protocol regardless of policy, and
below that the hold band follows the setting — 7–27 Relaxed, 5–27 Moderate,
3–27 Aggressive. SpamAssassin’s X-Spam-Status gives the verdict, score=,
the required= threshold and every matched rule in tests=. For
X-Gm-Message-State we found no published key — treat it as opaque until
Google documents one.
Five things people get wrong when reading email headers
The five recurring misreads are: trusting the bottom Received: line as the
origin, trusting an X-* IP header as authoritative, reading dkim=pass as
proof the visible From is genuine, assuming your own DMARC record filters your
inbox, and treating ARC headers as junk to be stripped.
The bottom line is not the origin. It is where the sender says the message started. Only the lowest line your own infrastructure wrote records an IP that someone other than the sender observed.
X-Originating-IP is not authoritative — and X-IronPort-RemoteIP fails for
a different reason. X-Originating-IP is sender-supplied and sits below your
boundary, which makes it the email equivalent of trusting a raw
X-Forwarded-For. X-IronPort-RemoteIP is not that: your own Cisco appliance
wrote it, so it is evidence — but only of the host that connected to the
appliance, which is not where the message started if anything relayed it in
between. A
2017 Cisco Community thread
records the expectation that the field “should ideally contain the originating
IP”. That thread is old; the behavior is not.
dkim=pass does not authenticate the From your user sees. A pass proves
the domain in d= signed the message and it arrived intact (RFC 6376). Whether
that says anything about the visible From is a question of alignment, which is
DMARC’s job. A
2010 Cisco thread
puts it exactly: “It came back as dkim=pass (1:0:0:good)… I’d imagine if someone
tried to spoof, it would hardfail correct?” Old thread, durable behavior.
Your own DMARC record is not an inbound filter. It tells other receivers how
to treat mail claiming to be from you; p=reject changes nothing about mail
arriving from other domains. The one exception is what makes the thread below
surprising: your own receiver does evaluate your DMARC record on inbound mail
that claims to be from you. A
2022 Microsoft Q&A
opens with exactly that situation — an admin whose organization “has already
configured DKIM and DMARC” asking why Microsoft delivered a message spoofing
their own domain that had already failed both checks. Old thread, durable
behavior.
ARC headers are not junk. A
2020 Microsoft Q&A
opens with an admin asking how to remove ARC-Authentication-Results from
Exchange Online — that is a request to delete the only record of why forwarded
mail was accepted. Strip the set and the next hop has nothing to validate.
FAQ
In what order should you read email headers?
Chronologically, the oldest Received: line sits at the bottom and the newest
at the top. For trust, start at the first Received: line your own
infrastructure wrote and read upward — everything below that line was supplied
by the sender and can be fabricated.
What does arc=none (0) mean in an ARC-Authentication-Results header?
It means the sealer found no prior ARC chain to inherit, so this set is the
first link. Microsoft appends a numeric reason code, which is why it writes
arc=none (0). It is not a failure, and on its own it does not mean the message
was forwarded.
Can email headers be faked?
Yes. From, Reply-To, Message-ID, Date, X-Originating-IP and every
Received: line added before the message reached your servers can all be
forged — including a planted Authentication-Results line. Only headers
written by your own infrastructure are evidence, which is why RFC 8601 requires
a conforming server to strip forged ones.
Which line in an email header shows the real sender IP?
The Received: line your own inbound server wrote. It records the IP that
actually connected to you. Lines below it, and any X-Originating-IP or
X-Sender-IP header, are the sender’s claims and are not authoritative — they
were added outside your trust boundary, where anyone can write them.
Does dkim=pass mean the email is genuine?
No. A dkim=pass proves the domain in d= signed the message intact. It says
nothing about the address the recipient sees. DMARC alignment is the test that
connects the signing domain to the visible From, so compare header.d=
against the From domain.
Conclusion
Knowing how to read email headers is not memorizing fields — it is knowing which lines are evidence and which are testimony. Four moves, every time:
- Export the raw source, not a screenshot of the summary pane.
- Find the first
Received:line your own infrastructure wrote and treat it as the boundary. - Read only the
Authentication-Resultsstamped with your own authserv-id. - Check the ARC set if the message was forwarded:
cv=on the outermost seal first, then down to thei=1AAR.