Skip to content

Email Header Forensics: Received, SPF, DKIM and MIME

Read Received chains bottom-up, compare Return-Path, From and Reply-To, interpret SPF/DKIM/DMARC results and decode MIME across EML, MBOX, MSG and PST.

Published on 8 min read

Most email investigations come down to two questions: where did this message really come from, and when. The answer is almost never in the visible From: line. It is spread across a dozen header fields. Some were written by servers you can trust, and some were typed by the sender. This guide covers how to read them, which ones carry weight, and how the container format changes what you can recover.

Reading the Received chain bottom-up

Every mail transfer agent (MTA) that accepts a message adds a Received: header at the top of the header block. The chain therefore reads in reverse. The lowest Received: line is the earliest hop, and the topmost one was added by the server that delivered to the mailbox.

Received: from mx.example.com (mx.example.com [198.51.100.25])
        by mail.corp.example (Postfix) with ESMTPS id 4XyZ1
        for <ap@corp.example>; Tue, 05 Mar 2024 09:14:07 +0000
Received: from [10.0.0.12] (unknown [203.0.113.45])
        by mx.example.com with ESMTPSA id abc123
        for <ap@corp.example>; Tue, 05 Mar 2024 09:14:05 +0000

Read it as: a client at 203.0.113.45 authenticated (ESMTPSA) to mx.example.com, which relayed to mail.corp.example two seconds later.

The rule that matters is trust boundary. Only the hops written by infrastructure you control, or by a provider whose logs you can subpoena, are reliable. Anything below the first hop your organization wrote was supplied by the sending side and can be fabricated. Attackers sometimes pad messages with fake lower Received: lines to suggest a different origin.

Things to check on each hop:

  • The from clause has two parts: the HELO/EHLO name the client claimed, and the reverse-DNS name plus IP in parentheses that the receiving server observed. The observed IP is the useful part.
  • Timestamps should increase as you move up the chain. Large gaps point to queueing, greylisting or a relay outage. A later hop showing an earlier time than the hop below it points to clock skew or fabrication.
  • with ESMTPSA, with ESMTPA or (Authenticated sender: …) indicate the sender logged in. That ties the message to a credential, which matters in account-takeover cases.

Message-ID, In-Reply-To and References

Message-ID is normally generated by the sender's mail client or first submission server, for example <a1b2c3d4@mail.example.com>. It is not authenticated, but it is still valuable:

  • The right-hand side often reveals the real sending platform, even when the From: domain says otherwise.
  • The same ID appearing in two mailboxes proves you are looking at the same message, not a re-send.
  • In-Reply-To and References carry the IDs of earlier messages in the thread. A reply whose In-Reply-To points at a message nobody in the organization ever received is a classic sign of a fabricated thread in business email compromise (BEC).

Return-Path vs From vs Reply-To

These three fields hold different identities, and comparing them is the core of phishing and BEC triage.

HeaderSet byWhat it represents
Return-PathFinal delivering MTA, from the SMTP MAIL FROMEnvelope sender, where bounces go. This is the domain SPF evaluates.
FromSender's clientThe author shown to the user. This is the domain DMARC protects.
Reply-ToSender's clientWhere replies go. Nothing authenticates it.

A typical invoice-fraud pattern:

Return-Path: <bounce-7731@mailer.example.net>
From: "Claire Martin" <claire@acme-payments.test>
Reply-To: <claire@acme-paymemts.test>

The Reply-To domain is a lookalike (paymemts vs payments). The victim sees a legitimate From:, hits reply, and the conversation moves to a domain the attacker controls. Compare domains character by character. Watch for transposed letters, rn vs m, extra hyphens, different TLDs, and internationalized domains that render as look-alike Unicode.

X-Originating-IP and other vendor headers

Some webmail services and mail platforms have historically recorded the client IP in X-Originating-IP or similar headers (X-Sender-IP). Coverage varies by provider and has changed over time, since many large providers stopped exposing client IPs for privacy reasons. When present, it can identify the sender's network. When absent, that tells you nothing. Microsoft 365 adds a family of X-MS-Exchange-* headers, and X-MS-Exchange-Organization-AuthAs indicates whether the submission was anonymous, internal or authenticated from the tenant's point of view.

Authentication-Results, SPF, DKIM and DMARC

The receiving server records its authentication verdicts in Authentication-Results (RFC 8601):

Authentication-Results: mail.corp.example;
       spf=pass smtp.mailfrom=mailer.example.net;
       dkim=pass header.d=mailer.example.net header.s=s1;
       dmarc=fail (p=reject) header.from=acme-payments.test

How to read it:

  • SPF checks whether the connecting IP is authorized to send for the envelope (Return-Path) domain. A pass says nothing about the From: domain.
  • DKIM verifies a cryptographic signature. header.d= is the signing domain and header.s= is the selector. A pass proves that d= domain signed the covered headers and body, not that it matches From:.
  • DMARC requires SPF or DKIM to pass and align with the From: domain. In the example, SPF and DKIM both pass for mailer.example.net, but neither aligns with acme-payments.test, so DMARC fails.

Only trust an Authentication-Results header whose authserv-id (the first token, here mail.corp.example) is your own boundary server. A sender can insert a fake one lower in the block. Received-SPF is an older, SPF-only equivalent. Forwarded mail may also carry ARC headers (ARC-Authentication-Results, ARC-Seal) that preserve verdicts from earlier hops.

You can re-verify DKIM later only if the signing key is still published in DNS and the message bytes were not altered. Re-save, re-encode or convert a message and the body hash can break.

Date header vs Received timestamps

Date: is written by the sender's client from the sender's clock. It can be wrong or deliberately backdated. The Received: timestamps written by your own servers are the reliable delivery times. Compare them:

  • A Date: a few seconds or minutes before your first trusted Received: is normal.
  • A Date: hours or days off suggests a skewed clock, a message that sat in a queue or outbox, or manipulation.
  • Normalize everything to UTC before comparing. The offset (+0000, -0500) is part of each timestamp.

MIME structure

The body of a modern email is a MIME tree. Common shapes:

multipart/mixed
├── multipart/alternative
│   ├── text/plain; charset=utf-8   (quoted-printable)
│   └── text/html; charset=utf-8    (quoted-printable)
└── application/pdf; name="invoice.pdf"   (base64)
  • multipart/alternative holds the same content in several formats. The plain and HTML parts can differ, and attackers occasionally hide text in one version only.
  • multipart/related bundles HTML with inline images referenced by cid:.
  • message/rfc822 is an entire email attached inside another, which is common in forwarded-as-attachment phishing reports.
  • Content-Transfer-Encoding is usually base64 for binary parts and quoted-printable for mostly-ASCII text (=C3=A9 is UTF-8 "é", a trailing = is a soft line break).
  • RFC 2047 encoded words encode non-ASCII headers: Subject: =?UTF-8?B?RmFjdHVyZSDDqWNodWU=?= (B = base64, Q = quoted-printable variant).
  • RFC 2231 parameters encode attachment names: filename*=UTF-8''facture%20%C3%A9chue.pdf, sometimes split into filename*0*=, filename*1*= continuations.

Always look at the decoded filename and the declared MIME type. invoice.pdf declared as application/octet-stream, or a name with a right-to-left override character, deserves a closer look.

Container formats

FormatWhat it isNotes
EMLOne RFC 5322 message as raw textThe best evidence format, since headers and bytes are intact.
MBOXMany messages concatenated, each starting with a From separator lineBody lines starting with From are escaped as >From . Used by Thunderbird and Gmail Takeout.
EMLXApple Mail's per-message fileFirst line is a byte count, then the RFC 822 message, then an Apple property list.
MSGOutlook item in an OLE compound file with MAPI propertiesReceived internet mail usually keeps the original headers in the transport headers property. Sent items and internal mail may not have them.
PST / OSTOutlook data files holding whole mailboxesMust be converted before per-message analysis.

For PST, readpst from the libpst project is the common open-source route:

# one mbox per folder, keep deleted items, recurse the folder tree
readpst -D -r -o ./out mailbox.pst

# or one .eml file per message
readpst -D -e -o ./out mailbox.pst

Hash the PST before converting, and check message counts per folder against what Outlook or your review platform reports. OST support in libpst exists but is less predictable, so treat OST conversion output with extra verification.

Gmail Takeout

Google Takeout exports mail as mbox, typically Takeout/Mail/All mail Including Spam and Trash.mbox. Each message carries an X-Gmail-Labels header, such as X-Gmail-Labels: Inbox,Important,Opened, which records the Gmail labels, including Sent, Trash and Spam. There is also an X-GM-THRID header with Gmail's thread ID. Labels are the only way to reconstruct the folder view from a single flat mbox.

Doing this in MessagingForensics

MessagingForensics parses .eml, .emlx, .mbox and Outlook .msg entirely in your browser. Every file you add is SHA-256 hashed before parsing, and ZIP archives are expanded in memory. What it does with email:

  • Headers: the message detail panel shows Message-ID, In-Reply-To, References, Return-Path, Reply-To, X-Originating-IP, X-Sender-IP, X-Mailer, User-Agent, Authentication-Results, Received-SPF, DKIM-Signature (signature value truncated), X-MS-Exchange-Organization-AuthAs, List-Id, and for mbox, X-Gmail-Labels. It also shows the full Received: chain, the hop count, and every bracketed IP found in it.
  • MIME: it walks multipart trees, decodes base64 and quoted-printable, honors declared charsets, and decodes RFC 2047 headers and RFC 2231 filenames. Attachments are listed with name, type and decoded size. They are not extracted or hashed, so do that in your forensic suite. The text of attached message/rfc822 parts is folded into the parent message's body.
  • Time: for EML, EMLX and mbox, the timeline time is the Date: header, converted to UTC. For MSG, it is the client submit time, falling back to delivery time and then creation time. Compare against the Received: chain yourself.
  • Threads: messages are grouped by subject with Re:/Fwd:-style prefixes stripped (several languages).
  • Limits: it records authentication verdicts but does not re-verify SPF, DKIM or DMARC. It keeps only the first (topmost) Authentication-Results header. PST and OST files are rejected with a prompt to convert them with readpst first. Header fields are not covered by the search box or the indicators tab. Export to JSON to keep every header alongside each message, since the CSV export has a fixed column set without headers.

Open the analyzer and drop in an .eml or mbox to see the header view. Related guides: Microsoft Teams forensics, Slack export forensics, and what is messaging forensics.

Related articles

Acquire Teams chats via Purview eDiscovery, the Microsoft Graph chatMessage API or a Teams (free) export, and know which fields, edits and deletions matter.
What a Slack workspace export contains, how to read ts, thread_ts, edits, tombstones and message markup, what exports leave out, and how to verify integrity.
Messaging forensics explained: acquisition options, a normalized message model, the timestamp zoo, evidence hashing and the pitfalls that break timelines.