spftrace: the SPF library behind this site
Every number on SPF Guru comes from spftrace, an open-source RFC 7208 evaluator written for this project and published separately. It is a Python library with no web framework and one runtime dependency, so anyone can build their own tooling on it.
The problem: SPF tools throw away the reasoning
Ask almost any SPF checker why a message failed and you get a verdict:
pass, fail, permerror. That is the one
piece of information you already had from the mail logs.
What you actually need when a vendor's mail stops authenticating is the working. Which mechanism matched, and which one was being evaluated when the budget ran out. What a macro expanded to for that specific sending IP. Whether a lookup returned nothing because the include is stale or because the vendor answers "not this IP" that way by design. Whether the record is over the limit by one term or by seven.
Without that, debugging SPF is re-reading a record by eye and guessing, which is exactly how a domain sits broken for months.
The solution: return the trace, not just the answer
spftrace evaluates a record the way a receiving server does and keeps everything it learned on the way: every DNS query with its rcode and timing, every macro expansion before and after, every mechanism with its qualifier and why it did or did not match, the running DNS-term and void-lookup counters, and the exact term that broke a limit.
pip install spftrace
import spftrace
result = spftrace.check("203.0.113.1", "user@example.com")
result.verdict # pass, fail, softfail, neutral, none,
# permerror or temperror
result.dns_terms_used # against the RFC limit of 10
result.to_dict() # JSON-safe, with the full event trace
There is an async form, acheck(), which is what this site uses.
Calling the blocking one inside a running event loop raises an error telling you
so, rather than a confusing asyncio failure several frames down.
Where you have already seen it
The SPF checker is a thin presentation layer over a
single acheck() call: the step list is its event trace and the
query table is its query log. The include tree rebuilds
nesting from the same trace. The lookup count on the
home page is its term counter, not a count of includes in the
text.
That matters for trusting the numbers. This site is not counting mechanisms with a regular expression and hoping; it is running a full evaluation and reporting what the evaluation did.
Design decisions worth knowing
Errors are verdicts, not exceptions
A malformed record, a blown lookup limit, an exhausted query budget and a DNS
timeout all come back as a permerror or temperror
verdict with the reason in the trace. You do not have to wrap a check in
try to survive a hostile zone. The one exception you may see means
the calling code is wrong, not the record.
Audit mode counts past the limit
Evaluation normally stops at the eleventh lookup, so neither a real MTA nor a
normal check can tell you what an over-limit record actually costs; every one of
them reports eleven. Audit mode keeps counting and reports the true figure, and
the verdict is still forced to permerror. Visibility changes, the
answer never does. That is why this site can tell you a record needs seventeen
lookups rather than just "too many".
Void lookups counted per lookup, not per term
The two void lookup limit is enforced the moment the third one returns.
Counting per term instead double counts, because every enclosing
include re-counts its children, and a single void three includes
deep became a false permerror. This is the kind of detail that is
invisible until it is wrong.
A resolver is transport only
Since 0.2.0, a resolver holds nameservers, a timeout and a socket, and
nothing else. Counters, cache and trace live in a session built fresh for each
check, so one resolver can serve many concurrent evaluations without leaking
state. Before that change, reusing a resolver could carry one check's state into
the next and turn a passing message into a permerror.
No hidden configuration
It reads no environment variables and no system resolver config. Everything is explicit, so the consuming application stays in charge of which resolvers are used and what the limits are.
Correctness
It is its own implementation of check_host() rather than a
wrapper around another evaluator, and the project reports passing all 203 cases
of the official openspf.org RFC 7208 test suite. The suite is fetched from a
pinned commit with its hash verified, so the gate cannot quietly shift.
Get it
MIT licensed, Python 3.10 and later, one runtime dependency (dnspython). There is a command line entry point as well as the library. It is beta: the API changed between 0.1 and 0.2, and this site had to change with it.
You do not need any of this to use SPF Guru. It is here because people reasonably ask what the numbers on this site are based on, and the honest answer is a library you can read, install and check for yourself.
If you would rather just fix a record, check your domain or read how to fix SPF PermError.