emailverifier.dev
Posts

How to Read an SPF Record: A Token-by-Token Walkthrough

| 7 min read | Usama Ejaz
Five SPF terms evaluated from left to right: v=spf1, ip4, include, mx, and a terminal -all stop.

An SPF record is read from left to right. Start with v=spf1, evaluate each mechanism against the connecting IP and the SMTP identity being checked, and stop at the first match or error. A qualifier such as - or ~ determines the result only when its mechanism matches. Modifiers such as redirect= are not ordinary mechanisms.

That is the short answer. The part that causes mistakes is treating an SPF record like an unordered list of approved senders. It is an evaluation program with order, recursion, a DNS budget, and a stopping point.

Read one record as an evaluation trace

Consider this hypothetical record. The domain names and IP ranges are reserved for documentation.

example.test. TXT "v=spf1 ip4:192.0.2.0/24 include:_spf.vendor.example mx -all"

Suppose a receiving server sees a connection from 198.51.100.44 and evaluates the MAIL FROM domain example.test. Here is the trace I would write down:

v=spf1
  select this as an SPF version 1 record

ip4:192.0.2.0/24
  198.51.100.44 is outside the range
  no match, continue
  DNS-triggering term count: 0

include:_spf.vendor.example
  evaluate the vendor's SPF record with the same client IP
  if that recursive result is pass: this include matches, return pass
  if it is fail, softfail, or neutral: no match, continue
  if it is temperror or permerror: stop with that error
  if it is none: stop with permerror
  DNS-triggering term count: at least 1, plus nested terms

mx
  resolve the MX hosts for example.test, then their addresses
  if 198.51.100.44 matches one of those addresses: return pass
  otherwise continue
  DNS-triggering term count: +1

-all
  all always matches
  the - qualifier returns fail
  stop

RFC 7208 section 4.6.2 defines that left-to-right behavior. A matching mechanism ends evaluation. If it does not match, the evaluator moves to the next mechanism. An exception ends evaluation with an error.

Notice what the trace does not prove. The record alone cannot tell us whether the message is safe, whether DMARC aligns, whether the recipient mailbox exists, or whether delivery will succeed. SPF answers a narrower question: may this client use the evaluated SMTP identity?

What does each SPF mechanism test?

The mechanism name tells you what set of client IP addresses can match. A missing qualifier means +, so a normal match returns pass.

MechanismWhat it testsDNS budget
ip4:The client IP is inside an IPv4 address or CIDR range.No lookup term
ip6:The client IP is inside an IPv6 address or CIDR range.No lookup term
aThe client IP matches an A or AAAA address for the named domain, or the current domain when none is supplied.One lookup term
mxThe client IP matches an address of an MX host for the named domain, or the current domain.One lookup term
include:A recursive SPF evaluation of another domain returns pass.One term, plus nested evaluation
exists:An A query for the expanded domain name returns at least one address.One lookup term
allEvery client. It always matches, so later mechanisms are unreachable.No lookup term
ptrA validated reverse-DNS name falls within the target domain.One lookup term, with extra limits

RFC 7208 section 5 defines those mechanisms. It also says the ptr mechanism should not be published. It is slow, puts extra load on reverse-DNS infrastructure, and has more reliable alternatives. Compliant evaluators still have to understand it when they encounter it.

The optional domain and CIDR details matter. Bare a and mx use the domain currently being evaluated. A term such as a:mail.example.test/24 uses another name and compares only the specified network prefix. Do not reduce either form to “allow this hostname.” SPF ultimately compares the SMTP client IP with resolved addresses.

Why is include not a text include?

include: does not paste another record into the current record. It runs a recursive SPF check and asks one very specific question: did that policy return pass for this client?

If the referenced policy returns fail, softfail, or neutral, the parent include simply does not match and evaluation continues. If it returns temperror or permerror, that error propagates. If no SPF record is found, the parent evaluation returns permerror. This distinction is why reading only the referenced record's final -all can lead you to the wrong conclusion.

The RFC describes include as the mechanism for crossing administrative boundaries, such as authorizing an outside sending provider. It recommends redirect= when one administrative owner wants several domains to share the same complete policy.

How is redirect= different?

redirect=policy.example is a modifier. It is used only after every mechanism in the current record has failed to match. Evaluation then continues using the redirected domain's SPF record.

example.test. TXT "v=spf1 ip4:192.0.2.0/24 redirect=policy.example"

In this example, the local IPv4 range gets the first chance to match. Every other client is evaluated against the complete policy at policy.example.

If that redirected domain has no SPF record, the result is permerror. A redirect is a policy dependency, not a harmless fallback.

Do not combine a terminal all mechanism with redirect=. all always matches, and RFC 7208 section 6.1 requires the redirect to be ignored when all appears anywhere in the record.

The strongest fair objection is that include and redirect often point to another SPF record, so the difference can feel academic. It is not. include adds one authorization branch to the current policy. redirect delegates the remaining policy. Replacing one with the other can change both the result and which terminal qualifier controls unmatched senders.

Which terms spend the 10-term DNS budget?

SPF limits DNS-triggering terms across the whole evaluation, including recursive includes and redirects. The terms that count are include, a, mx, ptr, exists, and redirect. The evaluator must return permerror when the total exceeds 10.

ip4, ip6, and all do not spend that term budget. The exp modifier is also outside it during ordinary evaluation because it is queried only after a fail result when an explanation is needed. RFC 7208 section 4.6.4 defines the limit and separate caps for address work triggered by mx and ptr.

This is why counting the visible include: strings is not enough. Each referenced policy can introduce more DNS-triggering terms. My SPF lookup-limit walkthrough owns the full budget calculation.

How do qualifiers change a match?

A qualifier belongs to the mechanism immediately after it:

  • + returns pass and is the default when no character appears.
  • - returns fail.
  • ~ returns softfail.
  • ? returns neutral.

The common endings -all, ~all, and ?all therefore describe clients that missed every earlier authorization. They do not change earlier matches. The SPF qualifier guide covers policy choice and safe rollout without turning a fail result into a promise that every receiver will reject.

How do you inspect a real record safely?

Start with the exact domain being evaluated. SPF normally checks the SMTP MAIL FROM domain. When the reverse path is null, the protocol uses the HELO identity for the mail-from check. That domain may be a bounce subdomain and may differ from the visible From: address.

domain="example.com"
dig +short TXT "$domain"

Then use this sequence:

  1. Reassemble character-strings that belong to the same TXT resource record.
  2. Select records beginning exactly with v=spf1. Zero means none; more than one means permerror.
  3. Walk mechanisms left to right with the actual client IP.
  4. Expand every include recursively and preserve its result semantics.
  5. Track DNS-triggering terms as one global budget.
  6. Stop at the first match or propagated error.
  7. Use redirect= only if no mechanism matched and no all exists.

SPF is published as a DNS TXT record, and one owner name cannot have multiple selectable SPF records. The site's SPF checker can show the published record, terminal policy, includes, and redirect target. It deliberately does not claim that one message passes because a full result also needs the client IP, evaluated identity, recursive policies, and DNS state observed by the receiver.

For a delivered message, check the trusted receiver's Authentication-Results header after tracing the record. A published policy is configuration evidence. The receiver's recorded result is evidence of how that connection evaluated. Even a pass is only domain authorization, not a safety verdict, mailbox confirmation, or delivery guarantee.

My practical rule is simple: parse SPF like a small program, not a bag of domains. Keep the identity, client IP, order, recursion, DNS budget, and stopping result visible on the same trace. Once those six facts are written down, most SPF records stop looking mysterious.

Continue reading