Decoding Email Headers: SPF, DKIM, DMARC and What Our Spam Engine Actually Scores

Most people never look at their email headers. That’s a shame, because when your emails start landing in spam, the header is the first place the answer is hiding.

Sean O'Brien
Published August 19, 2026

Every email your server sends carries a hidden paper trail. By the time a message lands in someone’s inbox, it’s picked up a stack of headers that record exactly where it came from, which servers handled it, whether the cryptographic signatures checked out, and how the receiving spam filter scored it. Most people never look at this stuff. Once you know how to read it, you’ll never look at a deliverability problem the same way again.

We’re going to tear apart a real outbound email header generated by our Stalwart mail server here at CloudSonic, explain what every line means, and show you how to use that information to diagnose deliverability problems before they start costing you emails.

[sonic_advert]
Note To pull the raw headers from an email you sent, open it in Gmail and click the three-dot menu in the top right of the message, then choose Show original. That gives you the full unprocessed header exactly as Gmail’s servers received it.

The full email header we’re working with

Here’s the complete header from an outbound email sent from a CloudSonic Stalwart mail server to a Gmail recipient. We’ll work through it section by section below.


Delivered-To: recipient@gmail.com
X-Spam-Status: No
Received: from mail.cloudsonic.com.au (mail.cloudsonic.com.au [178.104.246.82])
(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384)
by mx.google.com with ESMTPS id a12sm3849201pjq.4
for <recipient@gmail.com>
Thu, 30 Jul 2026 10:14:52 +1000
Authentication-Results: mx.google.com;
dkim=pass header.i=@cloudsonic.com.au header.s=cs1 header.b=Wkn/+PNB;
spf=pass (google.com: domain of hello@cloudsonic.com.au designates 178.104.246.82 as permitted sender) smtp.mailfrom=hello@cloudsonic.com.au;
dmarc=pass (p=reject sp=reject dis=none) header.from=cloudsonic.com.au
Received-SPF: pass (google.com: domain of hello@cloudsonic.com.au designates 178.104.246.82 as permitted sender)
receiver=mx.google.com; client-ip=178.104.246.82; envelope-from="hello@cloudsonic.com.au";
X-Spam-Result: DMARC_POLICY_ALLOW (-0.50),
DKIM_ALLOW (-0.20),
SPF_ALLOW (-0.20),
ARC_NA (0.00),
DKIM_SIGNED (0.00),
FROM_HAS_DN (0.00),
RCPT_COUNT_ONE (0.00),
RCVD_COUNT_ONE (0.00),
RCVD_TLS_LAST (0.00),
TO_DN_NONE (0.00),
TO_MATCH_ENVRCPT_ALL (0.00)
X-Spam-Score: ham, score=-0.90
Return-Path: <hello@cloudsonic.com.au>
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cloudsonic.com.au;
s=cs1; t=1785261186;
h=from:to:subject:date:message-id:mime-version:content-type;
bh=bkKwKf/DJKajCkNOZfBWzYM8w4ZDZ/5N/zzBnnvlKPo=;
b=Wkn/+PNBTQrk5rIaWegW/WcC9Y2ErTPxe3SYa80gmnaZhjq2jexYiTArCyccpk
 C2wHwQ9XImMI6qnA3omk9YbymYGaZUUwcLylIVTh1wbfz1J9030eaa07YL8jXn3
 zsk4khRh+pWHVRH1Gglp5Q0rWJiLTBmYVR2SOPu2nGGwb4OevCEhkmGacftvwN3;
q=dns/txt;
Message-ID: <20260730011452.A1B2C3D4@mail.cloudsonic.com.au>
From: CloudSonic Support <hello@cloudsonic.com.au>
To: recipient@gmail.com
Subject: Your CloudSonic hosting account is ready
Date: Thu, 30 Jul 2026 10:14:52 +1000
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8

That’s a lot to take in at once. Let’s break it down into chunks that actually make sense.

The Received header: tracing the path your email took

The Received: header is the mail system’s equivalent of a courier signature chain. Every server that handles an email stamps its own Received: line onto the top of the message. In a simple outbound email like this one, you’ll see just one hop because the message went directly from our Stalwart server to Gmail’s inbound mail exchangers.


Received: from mail.cloudsonic.com.au (mail.cloudsonic.com.au [178.104.246.82])
(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384)
by mx.google.com with ESMTPS id a12sm3849201pjq.4
for <recipient@gmail.com>
Thu, 30 Jul 2026 10:14:52 +1000

Breaking this down line by line:

from mail.cloudsonic.com.au (mail.cloudsonic.com.au [178.104.246.82]) tells us the sending server’s hostname and IP address. The hostname in parentheses is what the server announced itself as during the SMTP handshake (its HELO/EHLO value). The IP in square brackets is what Gmail actually connected from. When these match, it’s a good sign. When they don’t, it’s a red flag that can hurt deliverability.

using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 means the connection between our server and Gmail was encrypted in transit using TLS 1.3 with a strong cipher suite. If you ever see an email that arrived without TLS, that’s a problem worth investigating. Gmail will sometimes show a broken padlock icon for messages that arrived unencrypted.

by mx.google.com with ESMTPS tells you which of Gmail’s mail exchangers actually accepted the message. ESMTPS means Extended SMTP with TLS, which is the standard for authenticated encrypted mail delivery.

Why your PTR record matters for the Received header

Notice that the hostname mail.cloudsonic.com.au and the IP 178.104.246.82 are shown together. Gmail (and most receiving mail servers) will do a reverse DNS lookup on that IP to verify the hostname matches. This is called a PTR record check, and failing it will hurt your spam score on many servers.

You can check your PTR record from the command line:


dig -x 178.104.246.82 +short

That should return mail.cloudsonic.com.au. If it returns nothing, or returns a different hostname, you need to update your PTR record. On CloudSonic, PTR records are configured at the server level. Open a support ticket and we’ll set it up for you.

You can also verify it the other way, confirming the hostname resolves back to the same IP:


dig mail.cloudsonic.com.au A +short

Both lookups should match. Forward and reverse DNS that agree with each other is called a valid FCrDNS (Forward-Confirmed reverse DNS) and it’s one of the first things spam filters check.

Authentication-Results: the most important block in the header

This is the section that tells you whether your email passed or failed the three main email authentication standards. Gmail stamps this onto every incoming message after running its own checks. It’s authoritative because it comes from the receiving server, not the sender.


Authentication-Results: mx.google.com;
dkim=pass header.i=@cloudsonic.com.au header.s=cs1 header.b=Wkn/+PNB;
spf=pass (google.com: domain of hello@cloudsonic.com.au designates 178.104.246.82 as permitted sender) smtp.mailfrom=hello@cloudsonic.com.au;
dmarc=pass (p=reject sp=reject dis=none) header.from=cloudsonic.com.au

Three checks, three passes. This is exactly what you want to see. Let’s go through each one.

SPF: telling the world which IPs can send your email

SPF stands for Sender Policy Framework. It’s a DNS record that lists which IP addresses are authorised to send email for your domain. When Gmail receives a message claiming to be from cloudsonic.com.au, it looks up the SPF record for that domain and checks whether 178.104.246.82 is on the approved list.


spf=pass (google.com: domain of hello@cloudsonic.com.au designates 178.104.246.82 as permitted sender) smtp.mailfrom=hello@cloudsonic.com.au;

The SPF record itself lives in your DNS as a TXT record. In Cloudflare it looks like this:


Type: TXT
Name: @
Content: v=spf1 ip4:178.104.246.82 ~all
TTL: Auto

v=spf1 identifies this as an SPF record. ip4:178.104.246.82 authorises our mail server IP. The ~all at the end is a softfail, meaning mail from any other source should be treated with suspicion but not outright rejected. If you’re confident your SPF record is complete and you want stricter enforcement, you can use -all for a hard fail instead.

Warning If you’re using a third-party service to send email on behalf of your domain (like Mailchimp, Postmark, or Google Workspace), you need to include their sending infrastructure in your SPF record too. A common mistake is setting up SPF for your main mail server and forgetting about transactional email services, which then fail SPF and end up in spam.

If you use multiple sending sources, the record gets longer:


v=spf1 ip4:178.104.246.82 include:sendgrid.net include:_spf.google.com ~all

One important constraint: SPF records have a lookup limit of 10 DNS lookups. Each include: directive triggers at least one lookup. If your SPF record causes more than 10 lookups during evaluation, it will fail with a PermError, which some servers treat as a hard fail. Keep your SPF lean.

Checking your SPF record in Cloudflare

If you manage DNS through Cloudflare (which all CloudSonic accounts do), you can verify the record is in place from the command line:


dig TXT cloudsonic.com.au +short | grep spf

Replace cloudsonic.com.au with your own domain. You should see your SPF record returned. If you see nothing, the record is missing and your emails are likely failing SPF checks on arrival.

DKIM: cryptographic proof that your email wasn’t tampered with

DKIM stands for DomainKeys Identified Mail. Where SPF proves which server sent the email, DKIM proves the email content hasn’t been altered in transit. It works by having your mail server cryptographically sign outgoing messages using a private key. The receiving server then fetches the corresponding public key from your DNS and verifies the signature.


dkim=pass header.i=@cloudsonic.com.au header.s=cs1 header.b=Wkn/+PNB;

header.i=@cloudsonic.com.au is the signing domain identity. header.s=cs1 is the selector, which is the name of the specific key pair used to sign this message. The selector tells the receiving server which DNS record to look up to find the public key. header.b=Wkn/+PNB is the first eight characters of the base64-encoded signature, shown for reference.

The full DKIM signature block is what Stalwart attaches to every outgoing message:


DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=cloudsonic.com.au;
s=cs1; t=1785261186;
h=from:to:subject:date:message-id:mime-version:content-type;
bh=bkKwKf/DJKajCkNOZfBWzYM8w4ZDZ/5N/zzBnnvlKPo=;
b=Wkn/+PNBTQrk5rIaWegW/WcC9Y2ErTPxe3SYa80gmnaZhjq2jexYiTArCyccpk
 C2wHwQ9XImMI6qnA3omk9YbymYGaZUUwcLylIVTh1wbfz1J9030eaa07YL8jXn3
 zsk4khRh+pWHVRH1Gglp5Q0rWJiLTBmYVR2SOPu2nGGwb4OevCEhkmGacftvwN3;
q=dns/txt;

Let’s decode this:

a=rsa-sha256 is the signing algorithm. RSA with SHA-256 is the current standard. You may start seeing ed25519 as an alternative in newer setups, which produces shorter signatures with equivalent security.

c=relaxed/relaxed is the canonicalisation algorithm, applied to the headers and body respectively. Relaxed canonicalisation tolerates minor whitespace changes that can happen as a message passes through mail relays. Strict canonicalisation (simple/simple) would fail if even a single space was added during transit, which is too fragile for real-world use.

h=from:to:subject:date:message-id:mime-version:content-type lists the headers that were included in the signature. If any of these headers are modified after signing, the DKIM check will fail. Note that the message body is also covered, via the bh= (body hash) value.

t=1785261186 is the Unix timestamp of when the signature was created. q=dns/txt tells the receiving server to look up the public key via a DNS TXT record.

Where the DKIM public key lives in Cloudflare DNS

The public key for selector cs1 on domain cloudsonic.com.au is published at:


cs1._domainkey.cloudsonic.com.au

In Cloudflare, the record looks like this:


Type: TXT
Name: cs1._domainkey
Content: v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
TTL: Auto

v=DKIM1 identifies this as a DKIM key record. k=rsa specifies the key type. p= is the base64-encoded public key itself, generated by Stalwart when you set up DKIM signing. Stalwart handles key generation automatically on CloudSonic. The public key value in your DNS will be much longer than the truncated example above.

You can verify the key is published and readable:


dig TXT cs1._domainkey.yourdomain.com.au +short

If that returns nothing, your DKIM public key isn’t in DNS and every outgoing message will fail DKIM verification.

DMARC: the policy that ties SPF and DKIM together

DMARC stands for Domain-based Message Authentication, Reporting and Conformance. It’s the policy layer that sits on top of SPF and DKIM and tells receiving mail servers what to do when messages fail those checks. It also adds an alignment requirement: the domain in the From: header has to match the domain that passed SPF or DKIM.


dmarc=pass (p=reject sp=reject dis=none) header.from=cloudsonic.com.au

p=reject is the strictest possible DMARC policy. It tells receiving servers to outright reject any message that fails DMARC alignment. This is what you want once your setup is confirmed working, but you should start with p=none (monitoring only) and work up to p=reject gradually.

sp=reject applies the same reject policy to subdomains. Without this, a spammer could send email from subdomain.yourdomain.com.au and your DMARC policy wouldn’t cover it.

dis=none means no messages have been disposed of under this policy during the reporting period.

The DMARC record lives in DNS at _dmarc.yourdomain.com.au. In Cloudflare:


Type: TXT
Name: _dmarc
Content: v=DMARC1; p=reject; sp=reject; rua=mailto:dmarc@yourdomain.com.au; ruf=mailto:dmarc@yourdomain.com.au; fo=1;
TTL: Auto

rua= is the address where aggregate DMARC reports get sent. These are XML reports that mail servers send you daily showing how many messages passed and failed DMARC for your domain. They’re not human-readable but tools like dmarcian or MXToolbox will parse them for you.

ruf= is the address for forensic reports, which are sent when individual messages fail. Not all mail servers send these, but they’re useful for debugging.

fo=1 requests forensic reports when either SPF or DKIM fails, rather than only when both fail.

Warning Don’t publish a p=reject DMARC policy until you’ve monitored p=none for at least a few weeks and confirmed that all your legitimate sending sources are passing SPF and DKIM. Jumping straight to reject can cause your own emails to be silently dropped.

The spam scoring block: how Stalwart evaluates incoming mail

The X-Spam-Result: and X-Spam-Score: headers are added by Stalwart’s built-in spam filtering engine on inbound mail. They show you every rule that fired and its score contribution. For our clean outbound example, the scores are negative across the board because the message passes all authentication checks.


X-Spam-Result: DMARC_POLICY_ALLOW (-0.50),
DKIM_ALLOW (-0.20),
SPF_ALLOW (-0.20),
ARC_NA (0.00),
DKIM_SIGNED (0.00),
FROM_HAS_DN (0.00),
RCPT_COUNT_ONE (0.00),
RCVD_COUNT_ONE (0.00),
RCVD_TLS_LAST (0.00),
TO_DN_NONE (0.00),
TO_MATCH_ENVRCPT_ALL (0.00)
X-Spam-Score: ham, score=-0.90

Negative scores are good. They pull the total down, and a lower total score means a cleaner message. The three big negative contributors here are DMARC_POLICY_ALLOW at -0.50, DKIM_ALLOW at -0.20, and SPF_ALLOW at -0.20. These are direct rewards for passing email authentication.

The rules scoring at 0.00 are neutral observations. RCVD_TLS_LAST noting that the last hop used TLS is a good sign even at zero. TO_MATCH_ENVRCPT_ALL confirms the To: header matches the SMTP envelope recipient, which is expected for legitimate mail.

A final score of -0.90 classified as ham means this message is clean. By contrast, here’s what the scoring block looks like for a message that fails authentication:


X-Spam-Result: DKIM_FAIL (2.00),
SPF_FAIL (2.00),
DMARC_POLICY_QUARANTINE (1.00),
MISSING_FROM (1.00),
RCVD_NO_TLS_LAST (1.00),
FROM_EXCESS_BASE64 (0.50)
X-Spam-Score: spam, score=7.50

That message is going to spam or getting rejected outright. The contrast illustrates exactly why getting your authentication records right matters so much.

Return-Path and Message-ID


Return-Path: <hello@cloudsonic.com.au>
Message-ID: <20260730011452.A1B2C3D4@mail.cloudsonic.com.au>

The Return-Path: is the envelope sender address, also called the bounce address. This is where delivery failure notifications (NDRs) get sent when a message can’t be delivered. It’s separate from the From: header that recipients see. SPF checks are evaluated against the Return-Path domain, not the From: domain, which is why it’s possible to pass SPF while still failing DMARC alignment if the two domains don’t match.

The Message-ID: is a globally unique identifier for this specific email. Stalwart generates it automatically using a combination of a timestamp and the sending server’s hostname. The format timestamp.uniquestring@mail.cloudsonic.com.au is standard. A Message-ID that doesn’t contain a valid hostname in the right-hand side (after the @) is a minor spam signal on some filters.

Putting it all together: a quick DNS checklist for email deliverability

If you’re setting up email for a domain on CloudSonic for the first time, here’s the order to do it in. All of these records go into Cloudflare DNS.

First, add your MX record so incoming mail knows where to go:


Type: MX
Name: @
Content: mail.yourdomain.com.au
Priority: 10
TTL: Auto

Then add an A record pointing your mail hostname to the server IP:


Type: A
Name: mail
Content: 178.104.246.82
TTL: Auto
Proxy status: DNS only (grey cloud, not orange)
Warning Mail records in Cloudflare must be set to DNS only, not proxied. If you enable the Cloudflare proxy on your mail subdomain, SMTP connections will fail because Cloudflare does not proxy SMTP traffic.

Then your SPF record:


Type: TXT
Name: @
Content: v=spf1 ip4:178.104.246.82 ~all
TTL: Auto

Then your DKIM public key (the value comes from Stalwart’s admin interface):


Type: TXT
Name: cs1._domainkey
Content: v=DKIM1; k=rsa; p=YOUR_PUBLIC_KEY_HERE
TTL: Auto

Then start DMARC in monitoring mode:


Type: TXT
Name: _dmarc
Content: v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com.au;
TTL: Auto

Wait a few weeks, check your aggregate DMARC reports, and once you’re confident everything is passing, tighten the policy:


Type: TXT
Name: _dmarc
Content: v=DMARC1; p=reject; sp=reject; rua=mailto:dmarc@yourdomain.com.au; fo=1;
TTL: Auto

How to test your email authentication is working

The fastest way to test everything end to end is to send an email to mail-tester.com. It gives you a temporary address to send to and then scores your message out of ten, showing exactly which checks passed and which didn’t.

You can also test individual records from the command line. To check all three DNS records for a domain at once:


# Check SPF
dig TXT yourdomain.com.au +short | grep spf

# Check DKIM (replace cs1 with your selector)
dig TXT cs1._domainkey.yourdomain.com.au +short

# Check DMARC
dig TXT _dmarc.yourdomain.com.au +short

All three should return records. If any of them returns nothing, that authentication method is not configured and messages will fail that check on arrival.

For a more thorough test, Google Postmaster Tools at postmaster.google.com will show you your domain’s reputation and authentication pass rates over time, pulled from real delivery data to Gmail addresses. It’s free and worth setting up for any domain sending meaningful email volume.

Note On CloudSonic, Stalwart is configured and DKIM signing is set up for you during account provisioning. If you’ve added a new domain and need DKIM configured for it, open a support ticket with the domain name and we’ll generate the key pair and give you the DNS record to add in Cloudflare.

Once you can read a header fluently, deliverability problems stop being mysterious. A message going to spam either has a broken authentication record, a PTR mismatch, a spam rule firing on the content, or a reputation issue with the sending IP. The header tells you exactly which one it is. That’s a lot more useful than just wondering why emails aren’t arriving.