SPF.Guru

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.