Rewrite as a Rust, Apache-2.0 workspace

Supersedes the Go + embed-Mox design. The Go tree is removed; its
architecture doc is preserved at docs/archive/ARCHITECTURE-go-embed-mox.md
because its competitive analysis and data model still hold.

Five decisions recorded as ADRs:

  0001  Rust, not Go — accepting ~5,500 lines of protocol code that Mox
        would have given us free, to get the first permissively licensed
        Rust mail server. Costs stated plainly.
  0002  Apache-2.0, not MIT or AGPL — patent grant, trademark, CLA-free
        contribution. Public on GitHub; Gitea stays as the private fallback.
  0003  Stalwart's primitive crates (Apache-2.0/MIT) yes; its AGPL server
        crates never. DANE and MTA-STS sit on the AGPL side of that line,
        which is why we write our own.
  0004  Milestones, reordered: embedded inbound is required at launch.
  0005  Oracle Cloud blocks outbound :25, so direct-to-MX is impossible on
        the launch host. Split delivery is mandatory, not an on-ramp.

Twelve crates in three tiers. Tier 1 (mail-dane, mail-mta-sts, mail-dsn)
is standalone and publishable — no `dane` or `mta-sts` crate exists on
crates.io at all today.

openmail-relay ships the provider table as data, with SES and Oracle from
the start. Oracle's and Resend's SPF includes are deliberately None: a
guessed include turns the DNS check green against a mechanism the provider
does not honour, and mail still fails SPF silently.

cargo check/test/clippy/fmt all green; unsafe_code is forbidden workspace
wide; cargo-deny enforces the licence policy in CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkyvfNJGTshJNE9FtwPLk7
This commit is contained in:
Karti Tripathi
2026-09-02 13:08:05 -07:00
co-authored by Claude Opus 5
parent 428040d964
commit 36b15ddcaf
59 changed files with 5540 additions and 1762 deletions
+94
View File
@@ -0,0 +1,94 @@
//! MTA-STS — RFC 8461.
//!
//! There is **no MTA-STS crate on crates.io** at the time of writing. Stalwart
//! implements it in AGPL server crates. This is the permissive implementation.
//!
//! MTA-STS is DANE's non-DNSSEC cousin: a domain publishes a TXT record naming
//! a policy `id`, and serves the policy itself over HTTPS at
//! `https://mta-sts.<domain>/.well-known/mta-sts.txt`. The HTTPS certificate is
//! what makes the policy trustworthy — so **the fetch must use full `WebPKI`
//! validation with no exceptions**, and a policy fetched over a connection
//! whose certificate failed validation must be discarded, not cached.
//!
//! # Caching is the correctness problem
//!
//! The `max_age` in a policy can be a year. A cached `enforce` policy that is
//! wrong will silently defer a domain's mail for as long as it is cached, and
//! nothing in the sending path will look broken. So:
//!
//! - a policy is cached only after a fully validated HTTPS fetch;
//! - the TXT `id` changing invalidates the cache immediately;
//! - a fetch failure **never** evicts a valid cached policy (RFC 8461 §5.1) —
//! an attacker who can block HTTPS must not be able to strip the policy.
#![doc(html_root_url = "https://docs.rs/mail-mta-sts/0.1.0")]
/// What the domain asks senders to do.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
/// Deliver only over a validated TLS connection to a listed MX. On failure,
/// **defer** — never fall back to cleartext.
Enforce,
/// Behave as `Enforce` but deliver anyway on failure, reporting via TLS-RPT.
Testing,
/// Policy withdrawn. Cached policies for this domain must be dropped.
None,
}
/// A parsed policy.
#[derive(Debug, Clone)]
pub struct Policy {
pub mode: Mode,
/// MX patterns, which may contain a single leading `*.` wildcard.
pub mx: Vec<String>,
/// Seconds this policy may be cached. RFC 8461 caps meaningful values at
/// `31_557_600` (one year).
pub max_age: u32,
/// The `id` from the DNS TXT record this policy was fetched for.
pub id: String,
}
impl Policy {
/// Does `host` satisfy this policy's MX patterns?
///
/// Wildcards match exactly one label (`*.example.com` matches
/// `mx.example.com` but not `a.mx.example.com`), per RFC 8461 §4.1.
#[must_use]
pub fn allows_mx(&self, _host: &str) -> bool {
todo!("v0.2")
}
}
/// The outcome of applying MTA-STS to one delivery attempt. As in
/// [`mail_dane`](https://docs.rs/mail-dane), every case is explicit — there is
/// no boolean that could be read as "fine".
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StsResult {
/// An `enforce` policy is in effect and this MX + TLS chain satisfies it.
Enforced,
/// A `testing` policy failed. Deliver, but emit a TLS-RPT failure.
TestingFailure,
/// An `enforce` policy is in effect and was **not** satisfied. Defer.
Violation,
/// No policy published. Fall back to opportunistic TLS.
NotApplicable,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("DNS lookup failed: {0}")]
Dns(String),
#[error("policy fetch failed: {0}")]
Fetch(String),
#[error("malformed policy: {0}")]
Malformed(String),
}
/// Parse the body of an `mta-sts.txt` policy file.
///
/// # Errors
/// Returns [`Error::Malformed`] on a missing `version`, unknown `mode`, absent
/// `mx` for an enforcing policy, or unparseable `max_age`.
pub fn parse_policy(_body: &str, _id: &str) -> Result<Policy, Error> {
todo!("v0.2")
}