//! 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 { 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 { 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") ); } }