mailrs-shield 1.0.4

SMTP anti-spam primitives: DNS blocklist (DNSBL) queries, greylisting policy with optional Redis store, and PTR / forward-confirmed reverse DNS (FCrDNS) checks.
Documentation

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);

The optional redis-store feature (on by default) ships a GreylistDb that combines Redis (hot cache) + Postgres (cold backup) behind a single check() call:

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

let cm = redis::aio::ConnectionManager::new(
    redis::Client::open("redis://localhost")?
).await?;
let db = GreylistDb::new(cm);
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(())
# }

Disable the feature to plug in your own store — the trait surface is just "given a key + clock, look up the first-seen timestamp."

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::is_allowed) aren't bench-able offline; production latency is dominated by DNS / Redis round-trips, not the pure helpers above.

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

Feature flags

Flag Default What it enables
redis-store yes greylist::GreylistDb (Redis + optional PG cold backup)

Disable both default features (default-features = false) if you're plugging in your own backends.

License

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