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
+26
View File
@@ -0,0 +1,26 @@
//! Outbound delivery: smarthost relays and direct-to-MX.
//!
//! Two paths behind one interface:
//!
//! - **relay** — hand the message to SES / OCI Email Delivery / any smarthost
//! on submission (587). Someone else's IP reputation. Works everywhere,
//! including hosts that block outbound :25.
//! - **direct** — resolve MX, apply [`mail_mta_sts`] and [`mail_dane`], deliver
//! ourselves. Our reputation, our control, and impossible on a host that
//! blocks outbound :25 (see `docs/adr/0005-oracle-cloud.md`).
pub mod providers;
pub use providers::{PROVIDERS, RelayProvider, provider, resolve_host, spf_include};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("no usable MX for {0}")]
NoMx(String),
#[error("relay rejected: {code} {text}")]
Rejected { code: u16, text: String },
#[error("TLS policy violation: {0}")]
TlsPolicy(String),
#[error("transient failure, retry: {0}")]
Transient(String),
}
+181
View File
@@ -0,0 +1,181 @@
//! Smarthost providers, as **data**.
//!
//! # Why this is a table and not an enum with special cases
//!
//! Openship shipped a `provider: "ses" | "custom"` union and every non-SES
//! provider collapsed into `custom` the moment it was saved: no SPF include,
//! no round-trip in the UI, and adding a provider meant editing an `if` in the
//! service, the DNS builder, and the scanner. We start where they ended up.
//!
//! # `spf_include` is deliberately absent for some providers
//!
//! Where the SPF token is account- or region-scoped, publishing a *guessed*
//! include is worse than publishing none: the DNS check goes green against a
//! mechanism the provider does not honour, and mail still fails SPF — silently.
//! Those providers get `None` and the operator supplies theirs.
/// Everything that differs between smarthosts, as inert data.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RelayProvider {
pub id: &'static str,
pub label: &'static str,
/// `{region}` is substituted when `regional`. `None` = operator supplies it.
pub host_template: Option<&'static str>,
/// The host template needs a region before it resolves.
pub regional: bool,
pub default_port: u16,
/// The SPF mechanism every relayed domain must publish. `None` where the
/// token is account/region-scoped — see the module docs.
pub spf_include: Option<&'static str>,
/// SASL username the provider mandates. Prefilled, still editable.
pub username: Option<&'static str>,
/// The provider issues DKIM CNAMEs pasted from its console. We also sign
/// locally, so these are the provider's identity records, not our keys.
pub provider_dkim: bool,
}
const fn p(id: &'static str, label: &'static str) -> RelayProvider {
RelayProvider {
id,
label,
host_template: None,
regional: false,
default_port: 587,
spf_include: None,
username: None,
provider_dkim: false,
}
}
/// The known smarthosts. `custom` is last and is the fallback for any
/// unrecognised id — see [`provider`].
pub static PROVIDERS: &[RelayProvider] = &[
RelayProvider {
host_template: Some("email-smtp.{region}.amazonaws.com"),
regional: true,
spf_include: Some("include:amazonses.com"),
provider_dkim: true,
..p("ses", "Amazon SES")
},
RelayProvider {
// OCI Email Delivery's SPF include is region-scoped
// (rp / eu.rp / ap.rp .oracleemaildelivery.com) — the operator pastes
// theirs. Guessing one is how mail silently fails SPF.
host_template: Some("smtp.email.{region}.oci.oraclecloud.com"),
regional: true,
provider_dkim: true,
..p("oracle", "Oracle Cloud Email Delivery")
},
RelayProvider {
host_template: Some("smtp.sendgrid.net"),
spf_include: Some("include:sendgrid.net"),
username: Some("apikey"),
provider_dkim: true,
..p("sendgrid", "SendGrid")
},
RelayProvider {
host_template: Some("smtp.postmarkapp.com"),
spf_include: Some("include:spf.mtasv.net"),
provider_dkim: true,
..p("postmark", "Postmark")
},
RelayProvider {
// Resend rides SES, but the records it hands out are per-account —
// do not assume the SES include.
host_template: Some("smtp.resend.com"),
username: Some("resend"),
provider_dkim: true,
..p("resend", "Resend")
},
CUSTOM,
];
/// The fallback. Named so [`provider`] can return it without an unwrap — an
/// infallible lookup should not be able to panic, even in principle.
pub const CUSTOM: RelayProvider = p("custom", "Custom SMTP");
/// The spec for an id. An unknown id — state written by a newer version, or a
/// hand-edited config — falls back to `custom`, which requires an explicit
/// host, so the failure surfaces as a clear validation error instead of mail
/// going nowhere.
#[must_use]
pub fn provider(id: &str) -> &'static RelayProvider {
PROVIDERS.iter().find(|p| p.id == id).unwrap_or(&CUSTOM)
}
/// The effective SMTP host, or `None` when the inputs cannot produce one — the
/// caller turns that into the user-facing error, since only it knows which
/// field to blame.
#[must_use]
pub fn resolve_host(id: &str, host_override: Option<&str>, region: Option<&str>) -> Option<String> {
let spec = provider(id);
if spec.regional {
let region = region.map(str::trim).filter(|r| !r.is_empty())?;
return spec.host_template.map(|t| t.replace("{region}", region));
}
host_override
.map(str::trim)
.filter(|h| !h.is_empty())
.map(ToOwned::to_owned)
.or_else(|| spec.host_template.map(ToOwned::to_owned))
}
/// The SPF include to publish: the operator's override first (the only option
/// for account-scoped providers), else the provider's known token, else none.
#[must_use]
pub fn spf_include(id: &str, override_: Option<&str>) -> Option<String> {
override_
.map(str::trim)
.filter(|s| !s.is_empty())
.map(ToOwned::to_owned)
.or_else(|| provider(id).spf_include.map(ToOwned::to_owned))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_provider_falls_back_to_custom() {
assert_eq!(provider("nope").id, "custom");
assert_eq!(provider("").id, "custom");
}
#[test]
fn regional_hosts_need_a_region() {
assert_eq!(resolve_host("ses", None, None), None);
assert_eq!(
resolve_host("ses", None, Some("us-east-1")).as_deref(),
Some("email-smtp.us-east-1.amazonaws.com")
);
assert_eq!(
resolve_host("oracle", None, Some("us-ashburn-1")).as_deref(),
Some("smtp.email.us-ashburn-1.oci.oraclecloud.com")
);
}
#[test]
fn account_scoped_providers_never_guess_an_spf_include() {
// The whole point of the None: Oracle and Resend must not inherit a
// token they do not honour.
assert_eq!(spf_include("oracle", None), None);
assert_eq!(spf_include("resend", None), None);
assert_eq!(
spf_include("ses", None).as_deref(),
Some("include:amazonses.com")
);
assert_eq!(
spf_include("oracle", Some("include:rp.oracleemaildelivery.com")).as_deref(),
Some("include:rp.oracleemaildelivery.com")
);
}
#[test]
fn custom_requires_an_explicit_host() {
assert_eq!(resolve_host("custom", None, None), None);
assert_eq!(
resolve_host("custom", Some("mail.acme.com"), None).as_deref(),
Some("mail.acme.com")
);
}
}