mailrs-spf 1.0.4

RFC 7208 Sender Policy Framework verifier. Pure-Rust evaluator with a pluggable DNS resolver trait; ships an optional hickory-resolver-backed implementation. Bounded DNS-lookup limits + loop detection per spec.
Documentation
# mailrs-spf

[![Crates.io](https://img.shields.io/crates/v/mailrs-spf?style=flat-square&logo=rust)](https://crates.io/crates/mailrs-spf)
[![docs.rs](https://img.shields.io/docsrs/mailrs-spf?style=flat-square&logo=docs.rs)](https://docs.rs/mailrs-spf)
[![License](https://img.shields.io/crates/l/mailrs-spf?style=flat-square)](#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):

| Operation | Median |
|---|---:|
| `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.