Fundamentals gave you the parts. This course is about operating them, and that starts with being able to run the algorithm in your head exactly as a receiver runs it — because every diagnosis in module 7 is a matter of saying which term the evaluation reached and what it did there.
The three inputs
An evaluation takes three values, and RFC 7208 §4.1 names them:
| Input | Where it comes from | Notes |
|---|---|---|
ip | The connecting client address. | The only one that cannot be forged. |
sender | The MAIL FROM address. | If MAIL FROM is empty — a bounce — the HELO name is used instead. |
helo | The HELO/EHLO name. | Used for the check above, and to expand the h macro. |
The empty-sender case matters more than it looks. A bounce message has no envelope sender, so SPF is evaluated against the HELO name instead — which means a server whoseHELO is wrong can fail SPF on bounces while passing on everything else.
The loop
1. Fetch the TXT records at the sender's domain.
none found -> none
more than one v=spf1 -> permerror
DNS error -> temperror
2. For each term, left to right:
a. If it is a modifier (redirect=, exp=), set it aside.
b. Otherwise evaluate the mechanism against ip.
- Does it need DNS? Spend one from the budget.
- Budget exhausted, or >2 void lookups -> permerror
- DNS error -> temperror
c. Match? Return the result its qualifier names. STOP.
3. No mechanism matched.
redirect= present -> evaluate that domain and return its result
otherwise -> neutralTwo things that trip people at step 2c
Evaluation stops at the first match. Not the best match, not the most specific — the first. A record is read like a firewall ruleset, and everything after a matching term is unreachable.
A term that does not match is not a failure. An include whose target evaluates to fail simply does not match, and evaluation continues to the next term. That is the single most misread behaviour in the whole specification.
A worked evaluation
Take isc.org, checked 2026-09-15, and a connection from 149.20.1.5:
v=spf1 a mx ip4:149.20.0.0/20 ip6:2001:04F8::0/32 ... include:shops.shopify.com ... -all
a— resolveisc.org’s address records. One lookup. Is 149.20.1.5 among them? No. Continue.mx— resolve the MX set, then each host’s addresses. Several lookups. No match. Continue.ip4:149.20.0.0/20— range 149.20.0.0 to 149.20.15.255. 149.20.1.5 is inside it. Match. Qualifier is the implicit+, so the result ispass.
Everything after that ip4 term — the remaining ranges, three includes, the -all — was never evaluated, and cost nothing. That is why term order is a performance decision as well as a correctness one.
Where this bites in practice
Because the budget is spent left to right, a record that puts a and mx before its ip4 ranges spends DNS lookups on every evaluation, including the ones that were always going to match a literal range two terms later. Moving cheap literal terms ahead of costly ones changes no result and removes work from every receiver that evaluates you.