# mailrs-spf
[](https://crates.io/crates/mailrs-spf)
[](https://docs.rs/mailrs-spf)
[](#license)
RFC 7208 Sender Policy Framework verifier. Pure-Rust evaluator with a
pluggable DNS resolver trait; ships an optional
`hickory-resolver`-backed implementation behind the `hickory` feature.
Pairs with [`mailrs-rfc5322`](https://crates.io/crates/mailrs-rfc5322)
and [`mailrs-dmarc`](https://crates.io/crates/mailrs-dmarc) to give
mailrs a full owned email-auth stack — replacing the SPF half of
`mail-auth` with shape we control.
## Quickstart
```rust,ignore
use mailrs_spf::{verify, VerifyInput, SpfResult, HickoryResolver};
use hickory_resolver::TokioResolver;
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let resolver_inner = TokioResolver::builder_tokio()?.build();
let resolver = HickoryResolver::new(resolver_inner);
let input = VerifyInput {
ip: "203.0.113.42".parse()?,
helo: "mta.example.com".into(),
mail_from: "alice@example.com".into(),
};
let result = verify(&resolver, &input).await;
match result {
SpfResult::Pass => { /* accept */ }
SpfResult::Fail => { /* reject 5xx with SPF reason */ }
SpfResult::SoftFail => { /* accept but tag suspicious */ }
SpfResult::Neutral | SpfResult::None => { /* no policy / no record */ }
SpfResult::PermError | SpfResult::TempError => { /* see RFC 7208 §8 */ }
}
# Ok(())
# }
```
## What this crate does
- Parse SPF TXT records into a typed [`Record`] with [`Mechanism`]s
- Evaluate against `(IP, HELO, MAIL FROM)` per RFC 7208 §4
- All seven result values: `none / pass / fail / softfail / neutral
/ permerror / temperror`
- Mechanism support: `all`, `ip4`, `ip6`, `a`, `mx`, `include`,
`exists`
- Qualifier support: `+` (default), `-`, `~`, `?`
- DNS lookup budget (≤10 per RFC §4.6.4) + recursion depth cap
- Multi-record detection (multiple `v=spf1` → `PermError` per §4.5)
- DNS resolver trait so callers plug their own DNS (hickory included
behind a feature flag)
## What this crate does not (yet)
These are out-of-scope for 1.0 and deferred to 1.x minors:
- **Macro expansion** (RFC 7208 §7) — `%{i}`, `%{s}`, `%{d}`, etc. in
`exists:` / `include:` domain templates. Common patterns work
because most SPF records use literal domains; macro-heavy records
(some bulk-mailer providers use `exists:%{ir}._spf.provider.com`)
will compute against the literal template string. Add `macros`
feature when expansion is needed.
- **`redirect=` modifier** (RFC 7208 §6.1) — would extend the lookup
to another domain. Detected and skipped without erroring.
- **`exp=` modifier** (§6.2) — explanation text on Fail. Detected
and skipped.
- **`ptr` mechanism** (§5.5) — RFC marks it not-recommended; we
return `PermError` if a record uses it.
These are intentional v1 scope limits, not bugs. None of them affect
the common case (literal-domain records from major senders); add as
1.x minors when a use case demands.
## Why a new crate?
`mail-auth` covers SPF + DKIM + DMARC + ARC in one crate. We use it
in mailrs's inbound pipeline. The shape works but:
- The combined surface is heavy for the SPF use case alone
- We can't measure its perf cleanly against `mailrs-rfc5322` (the
underlying message-parsing layer)
- Owning SPF + DKIM + DMARC as separate, focused stones lets us
tune each per the dep-audit doc
`mailrs-dmarc` already exists. This crate carves the SPF half;
`mailrs-dkim` will follow.
## Performance
Measured (criterion, M-series Mac, release):
| `Record::parse` (simple `v=spf1 ip4 -all`) | **82 ns** |
| `Record::parse` (complex 8-mechanism record) | **484 ns** |
| `verify` pass-path (no real DNS) | **244 ns** |
The verify number is the pure CPU work; actual production `verify`
is dominated by DNS round-trips (typical 5-50 ms). Reproduce:
`cargo bench -p mailrs-spf --bench spf`.
## License
Apache-2.0 OR MIT.