mailrs-dmarc 1.0.0

DMARC (RFC 7489) aggregate report generation: result recording, XML report builder, report-mail formatter, rua extraction. Pluggable store trait with a Postgres reference impl.
Documentation

mailrs-dmarc

Crates.io docs.rs License Downloads MSRV

DMARC (RFC 7489) aggregate report tooling for Rust — result recording, XML report generation, report-mail formatting, rua extraction. Fills the gap mail-auth leaves on the receiving / aggregating side.

Extracted from mailrs so any Rust SMTP server can produce the daily aggregate reports their reporting partners expect.

What it covers

mail-auth (excellent) handles DMARC verification — pulling the policy, evaluating SPF/DKIM alignment, deciding pass/fail. This crate covers what comes after:

Step mailrs-dmarc mail-auth
Parse _dmarc.<domain> policy
Verify SPF + DKIM + alignment
Record per-message results ✓ ([DmarcStore])
Generate aggregate XML (RFC 7489 §12.4) ✓ ([generate_dmarc_report_xml])
Format report mail (multipart + gzip + base64) ✓ ([format_report_email])
Extract rua mailbox ✓ ([extract_rua_from_dmarc_record])

Quick start

use std::sync::Arc;
use mailrs_dmarc::{
    DmarcResultRecord, DmarcStore, PgDmarcStore,
    extract_rua_from_dmarc_record, format_report_email, generate_dmarc_report_xml,
};

# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let pool = sqlx::PgPool::connect("postgres://localhost/mail").await?;
let store: Arc<PgDmarcStore> = Arc::new(PgDmarcStore::new(pool));

// Inside your inbound pipeline, per-message:
store.record_result(&DmarcResultRecord {
    source_ip: "192.0.2.1".into(),
    from_domain: "sender.example".into(),
    spf_result: "pass".into(),
    dkim_result: "pass".into(),
    dmarc_result: "pass".into(),
    disposition: "none".into(),
}).await?;

// Daily aggregate report:
let results = store.get_results_for_date("2026-05-19").await?;
let xml = generate_dmarc_report_xml(
    "Reporter Org", "postmaster@reporter.example",
    "reporter.example!sender.example!2026-05-19",
    "sender.example", 1715990400, 1716076800, &results,
);
let rua = extract_rua_from_dmarc_record("v=DMARC1; p=quarantine; rua=mailto:dmarc@sender.example");
let email = format_report_email(
    "postmaster@reporter.example",
    rua.as_deref().unwrap_or("dmarc@sender.example"),
    "sender.example",
    "reporter.example!sender.example!2026-05-19",
    "2026-05-19", &xml,
);
// `email` is the raw multipart/mixed RFC 5322 message — hand it to your outbound queue.
# Ok(())
# }

Bring your own store

The pg-store feature gives you PgDmarcStore (the reference impl). Disable it (default-features = false) and implement [DmarcStore] yourself:

use async_trait::async_trait;
use mailrs_dmarc::{DmarcResultRecord, DmarcStore};

struct FileStore { /* ... */ }

#[async_trait]
impl DmarcStore for FileStore {
    type Error = std::io::Error;

    async fn record_result(&self, r: &DmarcResultRecord) -> Result<(), std::io::Error> {
        // append-only log, kafka topic, S3 prefix — your call.
        # let _ = r; Ok(())
    }
    async fn get_results_for_date(&self, _date: &str) -> Result<Vec<DmarcResultRecord>, std::io::Error> {
        # Ok(vec![])
    }
    async fn cleanup_old(&self, _days: i64) -> Result<u64, std::io::Error> {
        # Ok(0)
    }
}

Feature flags

Flag Default What it enables
pg-store yes PgDmarcStore (sqlx + Postgres reference impl)

License

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