emailmarketing.net

SPF (Sender Policy Framework)

RFC 7208 reference — record syntax (mechanisms, qualifiers, modifiers, macros), the check_host() evaluation algorithm, DNS lookup limits, result codes, and common pitfalls.

Referenceesp-operatorsender

SPF (RFC 7208) is a path-based authentication protocol: a domain publishes a DNS TXT record declaring which hosts are authorized to use the domain in the RFC5321.MailFrom identity (the "Envelope From" / Return-Path) and, separately, in the HELO/EHLO identity. Receivers evaluate the connecting client IP against that record.

For how SPF feeds into DMARC alignment, see DMARC.

Identities checked

  • Verifiers MUST check the MAIL FROM identity if a HELO check has not been performed or did not reach a definitive result.
  • It is RECOMMENDED that verifiers also separately check the HELO identity. When MAIL FROM is empty (bounces, MAIL FROM:<>), the HELO domain is used as the MAIL FROM domain (postmaster@<HELO domain>).

The record

  • Published as a DNS TXT record at the domain being checked. The record must begin exactly with v=spf1 (e.g., v=spf10 does not match and is discarded). The separate SPF DNS RR type (99) was deprecated by RFC 7208 — publish TXT only.
  • If zero v=spf1 records exist → result none. If more than one exists → permerror.
  • Terms are evaluated left to right; the first matching mechanism determines the result via its qualifier. If nothing matches and no redirect= is present, the default result is neutral (an implicit ?all).

Qualifiers

Qualifier Result when the mechanism matches
+ (default if omitted) pass
- fail
~ softfail
? neutral

Mechanisms

Mechanism Syntax Semantics DNS lookup?
all all Always matches. Used rightmost as the explicit default (-all, ~all, …). Anything after all is ignored. No
include include:<domain> Recursively evaluates <domain>'s SPF record. Recursive pass → the mechanism matches; fail/softfail/neutral → does not match (evaluation continues); temperror/permerror propagate; recursive nonepermerror. Yes
a a[:<domain>][/<cidr4>][//<cidr6>] Matches if the client IP equals an A (IPv4) or AAAA (IPv6) record of the target domain (default: the current domain), optionally within a CIDR prefix. Yes
mx mx[:<domain>][/<cidr4>][//<cidr6>] Looks up the target's MX records, then the address records of each MX name; matches if the client IP is among them. Yes
ptr ptr[:<domain>] Validated reverse-DNS check. "This mechanism SHOULD NOT be published" — slow, unreliable, deprecated in practice. Yes
ip4 ip4:<network>[/<cidr>] Client IP within the IPv4 network. Default prefix /32. No
ip6 ip6:<network>[/<cidr>] Client IP within the IPv6 network. Default prefix /128. No
exists exists:<domain-spec> Macro-expands the domain, does an A lookup (even for IPv6 connections); matches if any A record is returned. Enables per-IP policies via macros. Yes

CIDR prefix lengths: IPv4 /0/32 (default /32), IPv6 /0/128 (default /128). Only the specified number of high-order bits is compared.

Modifiers

Modifiers are name=value pairs; each may appear at most once anywhere in the record.

Modifier Syntax Semantics
redirect redirect=<domain> If no mechanism matched, evaluation continues with the SPF record of <domain>, whose result is used as-is. If the redirect target has no SPF record, the result is permerror (not none). Counts toward the 10-lookup limit. Ignored when the record also contains all (since all always matches first).
exp exp=<domain> On a fail result, the domain is queried for a TXT record, macro-expanded, and returned as the human-readable explanation string. Does not count toward the 10-lookup limit.

Macros

Macro expansion (%{x}) is available in domain-spec fields and exp text:

Macro Expands to
%{s} Sender (full MAIL FROM address)
%{l} Local-part of sender
%{o} Domain of sender
%{d} Current domain being checked
%{i} Client IP (dotted quad for IPv4; dot-separated nibbles for IPv6)
%{p} Validated reverse-DNS domain of client IP (SHOULD NOT be used — same problems as ptr)
%{v} in-addr for IPv4, ip6 for IPv6
%{h} HELO/EHLO domain
%% / %_ / %- Literal % / space / URL-encoded space (%20)

Transformers: a digit limits the number of right-hand labels kept (%{d2} → last two labels); r reverses label order; alternate delimiter characters may follow.

Result codes

Result Meaning (RFC 7208 §2.6) Recommended receiver handling (§8)
none No valid domain extracted, or no SPF record found No information; inconclusive
neutral Domain explicitly asserts nothing about the IP (?) MUST be treated exactly like none
pass Client is authorized to inject mail for the domain Domain is accountable; proceed
fail Client is explicitly not authorized (-) Local policy; if rejecting, SMTP 550, enhanced status 5.7.1
softfail Host is probably not authorized (~); domain is transitioning SHOULD NOT reject solely on this; MAY apply closer scrutiny
temperror Transient error (usually DNS) during evaluation Accept or defer; if deferring, SMTP 451, status 4.4.3
permerror Published record could not be correctly interpreted (requires DNS operator fix) If rejecting, SMTP 550, status 5.5.2

Note for DMARC: fail, softfail, neutral, none, and both errors all count as not pass — only pass (with alignment) can satisfy DMARC's SPF leg.

DNS lookup limits (§4.6.4)

Limit Value On exceeding
DNS-querying terms per evaluation (include, a, mx, ptr, exists, redirect) 10 total, counted across all recursion via include/redirect permerror
"Void lookups" (NXDOMAIN or empty answer) SHOULD limit to 2 permerror
Address lookups per mx mechanism 10 MX names permerror
Address lookups per ptr evaluation 10 PTR names Records beyond the first 10 ignored

all, ip4, ip6, and exp do not count toward the 10-term limit.

Common pitfalls

  • Exceeding 10 lookups. Nested include chains from ESPs, CRMs, and ticketing tools add up fast; the result is permerror, which DMARC treats as no SPF at all. Flatten includes or trim unused vendors; prefer ip4/ip6 (zero-cost) over a/mx where practical.
  • Multiple v=spf1 records at one name → permerror. Merge into a single record.
  • +all (or a missing/permissive default) authorizes the entire internet — worse than no record. End records with -all or ~all.
  • include of a domain with no SPF record → permerror (recursive none). Audit vendor includes when offboarding services.
  • ptr mechanism / %{p} macro — deprecated, slow, and unreliable; do not publish.
  • Forwarding breaks SPF: the forwarder's IP isn't in the origin domain's record, so SPF fails after any hop that preserves the original MAIL FROM. This is inherent to path-based auth — it's why DKIM (content-based) is preferred for DMARC survival, and why ARC exists.
  • SPF pass ≠ DMARC pass: many ESPs use their own bounce domain in MAIL FROM, so SPF passes but is not aligned with the From: domain. Use a custom Return-Path (bounce) subdomain of the From: domain for alignment — see DMARC.
  • TXT strings over 255 characters must be split into multiple quoted strings within the single record (they are concatenated); large records may also force TCP DNS.
  • redirect= after all is dead code — all matches first.

Related

  • DKIM — the content-based counterpart
  • DMARC — how SPF results and alignment feed policy
  • ARC — preserving authentication results across forwarding
#authentication#spf#dns#rfc7208#envelope-from#return-path