mailrs-shield 5.0.0

SMTP anti-spam primitives: DNS blocklist (DNSBL) queries, greylisting policy with optional in-process kevy store, and PTR / forward-confirmed reverse DNS (FCrDNS) checks.
docs.rs failed to build mailrs-shield-5.0.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: mailrs-shield-3.1.0

mailrs-shield

Crates.io docs.rs License Downloads

SMTP server anti-spam primitives in three modules: DNSBL lookups, greylisting policy, and FCrDNS (forward-confirmed reverse DNS) checks — async, transport-agnostic, mostly zero-I/O.

Extracted from mailrs so any Rust mail server can drop these in without re-implementing the same DNS-walking patterns. The Rust ecosystem currently has no dedicated crates for any of these three primitives.

What's inside

shield::dnsbl — DNS blocklist queries

Look up an inbound client IP against zones like Spamhaus ZEN, Barracuda, etc. Comes with an in-process TTL cache so repeat connections don't re-query.

use hickory_resolver::TokioResolver;
use mailrs_shield::dnsbl::check_dnsbl;
use std::net::IpAddr;

# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let resolver = TokioResolver::builder_tokio()?.build()?;
let ip: IpAddr = "203.0.113.42".parse()?;
let zones = &["zen.spamhaus.org".to_string(), "b.barracudacentral.org".to_string()];
let result = check_dnsbl(&resolver, ip, zones).await;
println!("{result:?}");
# Ok(())
# }

shield::greylist — Greylisting policy

Pure policy (Harris 2003 / RFC 6647): defer the first time you see a (client_ip, sender, recipient) triplet, accept after the configured delay if the sender retries. Legitimate MTAs queue and retry; most spam bots don't.

use mailrs_shield::greylist::{GreylistConfig, GreylistDecision, evaluate_triplet};

let cfg = GreylistConfig::default();   // 5-minute initial delay, 36-day pass window
assert_eq!(evaluate_triplet(None, 1000, &cfg), GreylistDecision::Defer);
assert_eq!(evaluate_triplet(Some(1000), 1100, &cfg), GreylistDecision::TooEarly);
assert_eq!(evaluate_triplet(Some(1000), 1400, &cfg), GreylistDecision::Accept);

GreylistDb runs the policy over a pluggable GreylistBackend behind a single check() call. The default kevy-store feature ships an in-process kevy backend:

# #[cfg(feature = "kevy-store")]
# fn _ex() -> Result<(), Box<dyn std::error::Error>> {
# tokio_test::block_on(async {
use mailrs_shield::greylist::{GreylistConfig, GreylistDb, triplet_key};

let store = kevy_embedded::Store::open(kevy_embedded::Config::default())?;
let db = GreylistDb::new(store);
let cfg = GreylistConfig::default();
let key = triplet_key("192.0.2.1", "alice@example.com", "bob@example.com");
let now = 1700000000;
let decision = db.check(&key, now, &cfg).await;
# Ok::<(), Box<dyn std::error::Error>>(())
# })
# }

To plug in your own store, implement GreylistBackend (get, set_with_ttl, expire — three key-value operations), build the store with GreylistDb::with_backend, and check the backend from a test with greylist::check_greylist_backend(&backend).await. With default-features = false, features = ["store"] the crate has no storage dependency at all.

shield::ptr — FCrDNS check

Score an inbound client by whether its IP's reverse DNS forward-resolves back to a name matching the EHLO domain. Returns 0.0 on full match, 1.0 on no match — easy to fold into a spam score.

use hickory_resolver::TokioResolver;
use mailrs_shield::ptr::check_client_ptr;
use std::net::IpAddr;

# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let resolver = TokioResolver::builder_tokio()?.build()?;
let ip: IpAddr = "192.0.2.1".parse()?;
let score = check_client_ptr(&resolver, ip, "mta.example.com").await;
println!("{score:.1}");
# Ok(())
# }

Performance

Microbenchmarks for the pure helpers (no live resolver hits) live in benches/ops.rs. Measured with criterion 0.8 on Apple Silicon (M-series), cargo bench, release profile.

Operation Median Notes
dnsbl::reverse_ipv4(1.2.3.4) ~110 ns builds the 4.3.2.1.zen.spamhaus.org.-shape name
dnsbl::interpret_spamhaus(127.0.0.2) ~700 ps match-arm dispatch, no allocation
greylist::evaluate_triplet(first seen) ~850 ps always defers
greylist::evaluate_triplet(retry within delay) ~1.5 ns timestamp delta + comparison
greylist::triplet_key(ip, sender, rcpt) ~120 ns one format! + lowercase normalization
ptr::ptr_score_from_names(match) ~85 ns scans the candidate names for the EHLO domain
ptr::ptr_score_from_names(no match) ~200 ns runs the full FCrDNS scoring fallback

Live-resolver paths (check_client_ptr, dnsbl::check, greylist::GreylistDb::check) aren't bench-able offline; production latency is dominated by DNS round-trips on the resolver paths. With the in-process kevy backend the GreylistDb store path makes no network call.

Run with cargo bench -p mailrs-shield. See tests/perf_gate.rs for the regression budgets.

Feature flags

Flag Default What it enables
store via kevy-store greylist::GreylistDb, the GreylistBackend trait and check_greylist_backend
kevy-store yes store + greylist::EmbeddedGreylistBackend (in-process kevy)

Use default-features = false, features = ["store"] when you bring your own backend.

License

Licensed under either of Apache License 2.0 or MIT license at your option.