Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-mail/src/lib.rs

//! **monetize's ONE mail path**: an SMTP submission relay, read from
//! `MONETIZE_SMTP_URL` / `MONETIZE_SMTP_FROM` (or the host's `secret.smtp_url` /
//! `secret.smtp_from`), TLS always, the relay's acceptance returned as the
//! audit record's proof. The welcome mail (monetize-impl's signup sink) and the
//! notices to existing customers — nonpay warnings, the renewal reminder, a
//! declined card (owner rulings g-nonpay and g-renewal, 2026-09-29) — go
//! through this and nothing else. Moved here from monetize-impl's
//! `monetize-signup-impl::mail` on 2026-09-29 so the OPEN server on the front
//! (which links no private sink) can deliver them; one implementation.
//!
//! # Not configured is not an error
//!
//! `MONETIZE_SMTP_URL` and `MONETIZE_SMTP_FROM` unset means this deployment has
//! no relay, and a signup must not fail over it. The line
//!
//! ```text
//!   no mailer configured; the return page is the only door
//! ```
//!
//! is written once per signup and the account is created anyway. A sink that
//! answered `Unavailable` here would hold a paid customer PENDING over a
//! *convenience*, and after twenty-four hours the engine would turn that into a
//! refusal (`monetize_server::signup::PENDING_CEILING_MS`) — a customer refused
//! for a mail nobody was going to read.
//!
//! **A send that FAILS is not an error either**, for the same reason and one
//! more: by the time it is attempted the principal exists and the entitlement
//! is on the appliance. There is nothing to undo, and reporting the signup as
//! unavailable would make the engine run the whole thing again. It is written
//! to the [`WelcomeLog`], and `monetize-server --resend-welcome` sends it again.
//!
//! # The transport, and the defect it exists to prevent
//!
//! Measured 2026-09-15 on the first paid signup: `MONETIZE_SMTP_URL` was
//! `smtp://…@smtp.protonmail.ch:587` and lettre's `from_url` reads a bare
//! `smtp://` as **plaintext, no STARTTLS ever**. Proton offers AUTH only after
//! STARTTLS, so the mail died with *"No compatible authentication mechanism was
//! found"* — at the first customer, not at start. So this module reads the URL
//! itself:
//!
//! | URL                          | port default | security                                   |
//! |------------------------------|--------------|--------------------------------------------|
//! | `smtp://user:pw@host[:port]` | 587          | **STARTTLS required** (never opportunistic) |
//! | `smtp://user:pw@host:465`    | —            | implicit TLS (465 is SMTPS whatever the scheme says) |
//! | `smtps://user:pw@host[:port]`| 465          | implicit TLS                               |
//!
//! `?tls=opportunistic` (or anything but `required`) is refused by name: this
//! mailer sends a relay password and a customer's address, and neither goes over
//! plaintext. And [`Mailer::verify`] connects, negotiates TLS and authenticates
//! **at start**, so a relay that cannot take a mail is an operator's refusal
//! while an operator is watching.
//!
//! # `lettre`, on rustls
//!
//! Pure Rust (LAW): `default-features = false`, `rustls-tls` + `ring` +
//! `webpki-roots`. lettre's default set turns on `native-tls`, which is openssl
//! on Linux — a C library and a second TLS stack beside the rustls this
//! workspace already carries for `reqwest`. See the manifest for the whole

use std::time::Duration;

use lettre::message::Mailbox;
use lettre::transport::smtp::authentication::Credentials;
use lettre::transport::smtp::client::{Certificate, Tls, TlsParameters};
use lettre::transport::smtp::extension::ClientId;
use lettre::transport::smtp::SmtpTransport;
use lettre::{Message, Transport};

/// The two environment names a relay is configured with.
pub const CONFIG_ENVS: &[&str] = &["MONETIZE_SMTP_URL", "MONETIZE_SMTP_FROM"];

/// Mail submission with STARTTLS (RFC 6409).
pub const SUBMISSION_PORT: u16 = 587;
/// Mail submission over implicit TLS (RFC 8314).
pub const SUBMISSIONS_PORT: u16 = 465;

/// One relay conversation may not hold a signup — or the start — for longer.
const TIMEOUT: Duration = Duration::from_secs(30);

/// How the relay connection is protected. There is no plaintext variant.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Security {
    /// TLS from the first byte (SMTPS, 465).
    ImplicitTls,
    /// Plain greeting, then `STARTTLS` — REQUIRED: a relay that does not offer
    /// it is a refusal, never a plaintext fallback.
    StartTls,
}

/// `MONETIZE_SMTP_URL`, read. The password is held but never printed.
#[derive(Clone, PartialEq, Eq)]
pub struct Relay {
    pub host: String,
    pub port: u16,
    pub security: Security,
    pub user: Option<String>,
    password: Option<String>,
    /// The EHLO name, from the URL's path; `None` is the host name.
    pub hello: Option<String>,
}

impl std::fmt::Debug for Relay {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Relay")
            .field("host", &self.host)
            .field("port", &self.port)
            .field("security", &self.security)
            .field("user", &self.user)
            .field("password", &self.password.as_ref().map(|_| "<set>"))
            .finish()
    }
}

impl Relay {
    /// `host:port (STARTTLS)` — the words every line about this relay uses.
    pub fn describe(&self) -> String {
        let how = match self.security {
            Security::ImplicitTls => "implicit TLS",
            Security::StartTls => "STARTTLS required",
        };
        format!("{}:{} ({how})", self.host, self.port)
    }
}

/// The URL with its credentials replaced, for an error message. A URL that does
/// not parse is not echoed at all: its password could be anywhere in it.
pub fn redact(url: &str) -> String {
    match url::Url::parse(url) {
        Ok(mut u) => {
            let had = !u.username().is_empty() || u.password().is_some();
            let _ = u.set_password(None);
            let _ = u.set_username("");
            let s = u.to_string();
            if had {
                s.replacen("://", "://<credentials>@", 1)
            } else {
                s
            }
        }
        Err(_) => "<a value that is not a URL>".into(),
    }
}

fn decode(s: &str, what: &str) -> Result<String, String> {
    percent_encoding::percent_decode_str(s)
        .decode_utf8()
        .map(|c| c.into_owned())
        .map_err(|e| format!("MONETIZE_SMTP_URL's {what} is not UTF-8 once decoded: {e}"))
}

/// **Read `MONETIZE_SMTP_URL`** by the table in the module header. Every
/// refusal names what was wrong and never contains the password.
pub fn parse_relay(url: &str) -> Result<Relay, String> {
    let shown = redact(url);
    let u =
        url::Url::parse(url).map_err(|e| format!("MONETIZE_SMTP_URL {shown} is not a URL: {e}"))?;
    let host = u
        .host_str()
        .filter(|h| !h.is_empty())
        .ok_or_else(|| format!("MONETIZE_SMTP_URL {shown} names no host"))?
        .to_owned();
    let (security, port) = match u.scheme() {
        "smtps" => (Security::ImplicitTls, u.port().unwrap_or(SUBMISSIONS_PORT)),
        "smtp" => {
            let port = u.port().unwrap_or(SUBMISSION_PORT);
            (if port == SUBMISSIONS_PORT { Security::ImplicitTls } else { Security::StartTls }, port)
        }
        other => {
            return Err(format!(
                "MONETIZE_SMTP_URL {shown} has scheme {other:?}; use smtp://user:pw@host:587 (STARTTLS) or smtps://user:pw@host:465 (implicit TLS)"
            ))
        }
    };
    if let Some((_, tls)) = u.query_pairs().find(|(k, _)| k == "tls") {
        if tls != "required" {
            return Err(format!(
                "MONETIZE_SMTP_URL {shown} asks for tls={tls}; refused — this mailer sends a relay password and a customer's address and \
                 neither goes over plaintext. Drop the parameter: smtp:// always requires STARTTLS, smtps:// is TLS from the first byte"
            ));
        }
    }
    let user = Some(decode(u.username(), "user")?).filter(|s| !s.is_empty());
    let password = match u.password() {
        Some(p) => Some(decode(p, "password")?),
        None => None,
    };
    match (&user, &password) {
        (Some(_), None) => {
            return Err(format!(
                "MONETIZE_SMTP_URL {shown} names a user and no password"
            ))
        }
        (None, Some(_)) => {
            return Err(format!(
                "MONETIZE_SMTP_URL {shown} carries a password and no user"
            ))
        }
        _ => {}
    }
    let path = u.path().trim_matches('/');
    let hello = (!path.is_empty()).then(|| path.to_owned());
    Ok(Relay {
        host,
        port,
        security,
        user,
        password,
        hello,
    })
}

/// What [`Mailer::verify`] found that is not a refusal.
#[derive(Debug, PartialEq, Eq)]
pub enum RelayCheck {
    /// Connected, TLS negotiated, authenticated (when credentials are set).
    Ready,
    /// The relay could not be REACHED (network, timeout, a transient 4xx). Not a
    /// configuration fault, so the service starts and says so loudly: a relay
    /// that is down for a minute must not stop payments from being taken.
    Unreachable(String),
}

/// A configured relay. `None` from [`Mailer::from_env`] is a deployment with no
/// mailer, which is a supported deployment.
pub struct Mailer {
    transport: SmtpTransport,
    from: Mailbox,
    relay: Relay,
    /// What a transport through a front's door is built with (feature `egress`): the same TLS
    /// (the RELAY's name), credentials and EHLO name as [`Mailer::transport`].
    #[cfg_attr(not(feature = "egress"), allow(dead_code))]
    how: How,
}

#[derive(Clone)]
struct How {
    tls: Tls,
    credentials: Option<Credentials>,
    hello: ClientId,
}

impl std::fmt::Debug for Mailer {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // The relay's own Debug never prints the password.
        f.debug_struct("Mailer")
            .field("relay", &self.relay.describe())
            .field("from", &self.from.to_string())
            .finish()
    }
}

impl Mailer {
    /// `MONETIZE_SMTP_URL` (`smtp://user:pass@host:587`) and
    /// `MONETIZE_SMTP_FROM` (`gunnar.rs <no-reply@gunnar.rs>` or a bare
    /// address).
    ///
    /// * both unset → `Ok(None)`, the supported no-mailer deployment;
    /// * one set and not the other, or either unparseable → `Err`, at BUILD
    ///   time. A half-configured relay is a typo, and finding it when the
    ///   service starts is the difference between an operator fixing it and a
    ///   customer not getting a mail nobody knew was configured.
    ///
    /// **Set-and-empty is unset**, the rule `StripeConfig::from_env` already
    /// enforces: `MONETIZE_SMTP_URL=` is one keystroke from the real thing and
    /// `std::env::var` answers `Ok("")` for it.
    pub fn from_env() -> Result<Option<Mailer>, String> {
        let var = |k: &str| std::env::var(k).ok().filter(|v| !v.trim().is_empty());
        match (var("MONETIZE_SMTP_URL"), var("MONETIZE_SMTP_FROM")) {
            (None, None) => Ok(None),
            (Some(url), Some(from)) => Mailer::new(&url, &from).map(Some),
            (Some(_), None) => Err("MONETIZE_SMTP_URL is set and MONETIZE_SMTP_FROM is not; a relay with no sender sends nothing".into()),
            (None, Some(_)) => Err("MONETIZE_SMTP_FROM is set and MONETIZE_SMTP_URL is not; there is no relay to send through".into()),
        }
    }

    pub fn new(url: &str, from: &str) -> Result<Mailer, String> {
        Mailer::with_root(url, from, None)
    }

    /// [`Mailer::new`], trusting ONE more root beside the compiled-in set: a PEM
    /// certificate (`MONETIZE_SMTP_CA` / the host's `secret.smtp_ca`). For a
    /// relay on a private CA — the mock estate's own relay, whose certificate
    /// the bench's CA issued. TLS is still required and still verified.
    pub fn with_root(url: &str, from: &str, root_pem: Option<&[u8]>) -> Result<Mailer, String> {
        let relay = parse_relay(url)?;
        let from: Mailbox = from
            .parse()
            .map_err(|e| format!("MONETIZE_SMTP_FROM {from:?} is not a mailbox: {e}"))?;
        let mut params = TlsParameters::builder(relay.host.clone());
        if let Some(pem) = root_pem {
            params = params.add_root_certificate(
                Certificate::from_pem(pem)
                    .map_err(|e| format!("MONETIZE_SMTP_CA is not a PEM certificate: {e}"))?,
            );
        }
        let params = params.build().map_err(|e| {
            format!(
                "MONETIZE_SMTP_URL: no TLS parameters for {}: {e}",
                relay.host
            )
        })?;
        let tls = match relay.security {
            Security::ImplicitTls => Tls::Wrapper(params),
            Security::StartTls => Tls::Required(params),
        };
        let credentials = match (&relay.user, &relay.password) {
            (Some(user), Some(password)) => Some(Credentials::new(user.clone(), password.clone())),
            _ => None,
        };
        // The EHLO name: the URL's path, else the sender's own domain — never
        // lettre's placeholder, which strict relays refuse.
        let hello = ClientId::Domain(
            relay
                .hello
                .clone()
                .unwrap_or_else(|| from.email.domain().to_owned()),
        );
        let how = How {
            tls,
            credentials,
            hello,
        };
        let transport = transport_at(&relay.host, relay.port, &how);
        Ok(Mailer {
            transport,
            from,
            relay,
            how,
        })
    }

    /// **The relay a box was FED** (`secret.smtp_url` / `secret.smtp_from`, monetize
    /// `estate_order::relay_feed`): the config switch that turns a box's mail on. Nothing fed is
    /// the ordinary state, and it is a refusal by name: no relay, no transport, no socket. Half a
    /// relay is refused by name too. Blank is unset.
    pub fn from_feed(fed: &std::collections::BTreeMap<String, String>) -> Result<Mailer, String> {
        let get = |k: &str| {
            fed.get(k)
                .map(|v| v.trim().to_owned())
                .filter(|v| !v.is_empty())
        };
        match (get(FEED_SMTP_URL), get(FEED_SMTP_FROM)) {
            (Some(url), Some(from)) => Mailer::new(&url, &from),
            (None, _) => Err(format!(
                "mail is off: this box was fed no relay ({FEED_SMTP_URL}), so it dials nothing"
            )),
            (Some(_), None) => Err(format!(
                "mail is off: this box was fed a relay and no sender ({FEED_SMTP_FROM})"
            )),
        }
    }

    pub fn relay(&self) -> &Relay {
        &self.relay
    }

    /// **Can this relay take a mail — asked at START, not at the first
    /// customer.** One conversation: connect, TLS (implicit or STARTTLS), AUTH
    /// with the configured credentials, QUIT. Nothing is sent.
    ///
    /// * `Ok(Ready)` — it can.
    /// * `Ok(Unreachable)` — it could not be reached; see [`RelayCheck`].
    /// * `Err(reason)` — it was reached and REFUSED this mailer: no STARTTLS, a
    ///   certificate that does not verify, no AUTH mechanism in common, a wrong
    ///   password. A configuration fault, refused by name.
    pub fn verify(&self) -> Result<RelayCheck, String> {
        let at = self.relay.describe();
        match self.transport.test_connection() {
            Ok(true) => Ok(RelayCheck::Ready),
            Ok(false) => Ok(RelayCheck::Unreachable(format!("the relay {at} dropped the connection after the handshake"))),
            Err(e) if e.is_permanent() || e.is_client() || e.is_tls() || e.is_response() || tls_broke(&e) => Err(format!(
                "MONETIZE_SMTP_URL: the relay {at} refused this mailer at start: {e}. \
                 A relay on 587 must offer STARTTLS and, after it, an AUTH mechanism for the configured user; \
                 465 is TLS from the first byte. Fix the URL or the credential — a mailer that cannot authenticate \
                 is refused here, not at the first paying customer"
            )),
            Err(e) => Ok(RelayCheck::Unreachable(format!("the relay {at} could not be reached at start: {e}"))),
        }
    }

    /// **One mail, the caller's subject and text, through this relay** — a
    /// welcome, a nonpay warning, a renewal reminder (g-nonpay, g-renewal).
    /// `kind` names it in a refusal. `Ok` carries the relay's own acceptance
    /// (`250 2.0.0 Ok: queued as …`), which is the audit record's proof.
    pub fn notice(
        &self,
        to: &str,
        kind: &str,
        subject: &str,
        body: &str,
    ) -> Result<String, String> {
        let message = compose(&self.from, to, kind, subject, body)?;
        self.hand(&self.transport, &message, kind)
    }

    fn hand(
        &self,
        transport: &SmtpTransport,
        message: &Message,
        kind: &str,
    ) -> Result<String, String> {
        let reply = transport.send(message).map_err(|e| {
            format!(
                "the relay {} refused the {kind} mail: {e}",
                self.relay.describe()
            )
        })?;
        Ok(format!(
            "{} {}",
            reply.code(),
            reply.message().collect::<Vec<_>>().join(" ")
        ))
    }

    /// [`Mailer::notice`] **from a box that may sit behind its front** (feature `egress`; the
    /// lean estate, A19 row 13): `door` is the egress door the box was fed (`http://addr:port`,
    /// `nordisk_pairing::egress::proxy()` or the product's feed in force). lettre speaks no
    /// proxy, so the mail dials a loopback forward carried through the door as `CONNECT
    /// relay:port`, held until the relay answered; TLS still names the RELAY, so its certificate
    /// is checked as ever. `None`: the box has its own way out, and this is [`Mailer::notice`].
    #[cfg(feature = "egress")]
    pub fn notice_via(
        &self,
        door: Option<&str>,
        to: &str,
        kind: &str,
        subject: &str,
        body: &str,
    ) -> Result<String, String> {
        let Some(door) = door else {
            return self.notice(to, kind, subject, body);
        };
        let message = compose(&self.from, to, kind, subject, body)?;
        let forward =
            nordisk_pairing::egress::Forward::open(door, &self.relay.host, self.relay.port)
                .map_err(|e| {
                    format!(
                        "the relay {} through the egress door: {e}",
                        self.relay.describe()
                    )
                })?;
        let at = forward.addr();
        let transport = transport_at(&at.ip().to_string(), at.port(), &self.how);
        let said = self.hand(&transport, &message, kind);
        drop(forward);
        said
    }
}

/// The feed keys a sold box's relay is named under (monetize `estate_order::relay_feed`).
pub const FEED_SMTP_URL: &str = "secret.smtp_url";
pub const FEED_SMTP_FROM: &str = "secret.smtp_from";

/// One transport to `host:port` with `how`'s TLS, credentials and EHLO name.
fn transport_at(host: &str, port: u16, how: &How) -> SmtpTransport {
    let mut builder = SmtpTransport::builder_dangerous(host.to_owned())
        .port(port)
        .tls(how.tls.clone())
        .timeout(Some(TIMEOUT))
        .hello_name(how.hello.clone());
    if let Some(c) = &how.credentials {
        builder = builder.credentials(c.clone());
    }
    builder.build()
}

/// **One notice as a proper MIME message** (live 2026-10-14: the "ready" mail went out
/// quoted-printable with no `MIME-Version` and no `Content-Type`, so the owner's mail client
/// showed `invite=3D…` and the soft line breaks, and the first-login link broke).
///
/// `MIME-Version: 1.0`, `Content-Type: text/plain; charset=utf-8`, and the body sent `8bit`
/// whenever its lines allow it (every line under 990 bytes): no line is wrapped, so a link
/// stays one unbroken line. A body that cannot go `8bit` falls back to the encoding lettre
/// chooses, always declared.
pub fn compose(
    from: &Mailbox,
    to: &str,
    kind: &str,
    subject: &str,
    body: &str,
) -> Result<Message, String> {
    use lettre::message::header::{ContentTransferEncoding, ContentType};
    use lettre::message::{Body, SinglePart};
    let to: Mailbox = to
        .parse()
        .map_err(|e| format!("{:?} is not a mailbox: {e}", masked(to)))?;
    // lettre picks quoted-printable for any line of 76 or more, which a link is; 8bit allows
    // lines up to 998 bytes (RFC 5322 2.1.1), so a body whose lines all fit goes 8bit as it is,
    // its line ends CRLF.
    let crlf = body.replace("\r\n", "\n").replace('\n', "\r\n");
    let body = if crlf
        .split("\r\n")
        .all(|l| l.len() < 990 && !l.contains('\r') && !l.contains('\0'))
    {
        Body::dangerous_pre_encoded(crlf.into_bytes(), ContentTransferEncoding::EightBit)
    } else {
        Body::new(body.to_owned())
    };
    Message::builder()
        .from(from.clone())
        .to(to)
        .subject(subject)
        .singlepart(
            SinglePart::builder()
                .header(ContentType::TEXT_PLAIN)
                .body(body),
        )
        .map_err(|e| format!("the {kind} mail would not build: {e}"))
}

/// [`compose`] as the bytes a relay is handed — for a test that reads the message back.
pub fn render(from: &str, to: &str, subject: &str, body: &str) -> Result<Vec<u8>, String> {
    let from: Mailbox = from
        .parse()
        .map_err(|e| format!("{from:?} is not a mailbox: {e}"))?;
    Ok(compose(&from, to, "notice", subject, body)?.formatted())
}

/// **Did the TLS session itself fail** — a certificate that does not verify, a
/// peer that answered STARTTLS with something that is not TLS? lettre reports
/// those as a NETWORK error (rustls fails inside the stream's read/write), so
/// the kind alone would file a misconfigured relay under "unreachable". rustls
/// surfaces them as `io::ErrorKind::InvalidData`; a relay that merely went away
/// is `UnexpectedEof`/`ConnectionReset`/a timeout and stays unreachable.
fn tls_broke(e: &lettre::transport::smtp::Error) -> bool {
    let mut source = std::error::Error::source(e);
    while let Some(s) = source {
        if let Some(io) = s.downcast_ref::<std::io::Error>() {
            if io.kind() == std::io::ErrorKind::InvalidData {
                return true;
            }
        }
        source = s.source();
    }
    false
}

/// `elvira@example.com` → `el***@example.com`, for a log line. The audit file
/// holds the whole address; journald does not need it.
pub fn masked(addr: &str) -> String {
    match addr.split_once('@') {
        Some((local, domain)) => {
            format!("{}***@{domain}", local.chars().take(2).collect::<String>())
        }
        None => "***".into(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::{BufRead, BufReader, Read, Write};
    use std::net::TcpListener;

    #[test]
    fn a_relay_with_no_sender_is_refused_by_name() {
        assert!(Mailer::new("not a url", "a@b.se").is_err());
        assert!(Mailer::new("smtp://localhost:25", "not a mailbox at all").is_err());
        assert!(Mailer::new("smtp://localhost:25", "gunnar.rs <no-reply@gunnar.rs>").is_ok());
    }

    /// ★ **The table in the module header, one row at a time.** The 2026-09-15
    /// front's URL shape is the first row: `smtp://` on 587 is STARTTLS, required.
    #[test]
    fn the_url_decides_the_security_and_there_is_no_plaintext_row() {
        let r =
            parse_relay("smtp://no-reply%40gunnar.rs:s3cr3t-pw@smtp.protonmail.ch:587").unwrap();
        assert_eq!(
            (r.host.as_str(), r.port, r.security),
            ("smtp.protonmail.ch", 587, Security::StartTls)
        );
        assert_eq!(
            r.user.as_deref(),
            Some("no-reply@gunnar.rs"),
            "the %40 is decoded"
        );
        assert_eq!(
            parse_relay("smtp://u:p@relay.example").unwrap().port,
            587,
            "smtp:// defaults to submission"
        );
        assert_eq!(
            parse_relay("smtp://u:p@relay.example:465")
                .unwrap()
                .security,
            Security::ImplicitTls,
            "465 is TLS from the first byte"
        );
        let s = parse_relay("smtps://u:p@relay.example").unwrap();
        assert_eq!((s.port, s.security), (465, Security::ImplicitTls));
        assert_eq!(
            parse_relay("smtp://u:p@relay.example:2525?tls=required")
                .unwrap()
                .security,
            Security::StartTls
        );

        let err = parse_relay("smtp://u:s3cr3t-pw@relay.example:25?tls=opportunistic").unwrap_err();
        assert!(
            err.contains("tls=opportunistic") && err.contains("plaintext"),
            "{err}"
        );
        assert!(parse_relay("http://u:p@relay.example")
            .unwrap_err()
            .contains("scheme"));
        assert!(parse_relay("smtp://u@relay.example")
            .unwrap_err()
            .contains("no password"));
        for bad in [
            "smtp://u:s3cr3t-pw@relay.example:25?tls=none",
            "ftp://u:s3cr3t-pw@relay.example",
            "smtp://u:s3cr3t-pw@",
        ] {
            let err = parse_relay(bad).unwrap_err();
            assert!(
                !err.contains("s3cr3t-pw"),
                "a refusal printed the password: {err}"
            );
        }
        let m = Mailer::new("smtp://u:s3cr3t-pw@relay.example:587", "a@b.se").unwrap();
        assert!(!format!("{m:?}").contains("s3cr3t-pw"), "{m:?}");
        assert!(!format!("{:?}", m.relay()).contains("s3cr3t-pw"));
    }

    /// What [`fake_relay`] heard: every plaintext line, and the first byte
    /// after the client asked for TLS.
    type Heard = (Vec<String>, Option<u8>);

    /// A one-connection fake relay that records every line the client sends in
    /// plaintext and the first byte after it asked for TLS.
    fn fake_relay(greet: bool, ehlo_reply: &'static str) -> (u16, std::thread::JoinHandle<Heard>) {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let port = listener.local_addr().unwrap().port();
        let handle = std::thread::spawn(move || {
            let (mut sock, _) = listener.accept().unwrap();
            sock.set_read_timeout(Some(Duration::from_secs(10)))
                .unwrap();
            let mut lines = Vec::new();
            if !greet {
                // Implicit TLS: the CLIENT speaks first, and it speaks TLS.
                let mut b = [0u8; 1];
                let first = sock.read_exact(&mut b).ok().map(|_| b[0]);
                return (lines, first);
            }
            sock.write_all(b"220 fake.relay ESMTP\r\n").unwrap();
            let mut reader = BufReader::new(sock.try_clone().unwrap());
            loop {
                let mut line = String::new();
                if reader.read_line(&mut line).unwrap_or(0) == 0 {
                    return (lines, None);
                }
                let cmd = line.trim_end().to_owned();
                lines.push(cmd.clone());
                let upper = cmd.to_ascii_uppercase();
                if upper.starts_with("EHLO") {
                    sock.write_all(ehlo_reply.as_bytes()).unwrap();
                } else if upper == "STARTTLS" {
                    sock.write_all(b"220 2.0.0 Ready to start TLS\r\n").unwrap();
                    let mut b = [0u8; 1];
                    let first = reader.read_exact(&mut b).ok().map(|_| b[0]);
                    // Answer the ClientHello with bytes that are not TLS — the
                    // shape of a relay whose TLS is broken, not one that went away.
                    let _ =
                        sock.write_all(b"500 this is not a TLS record at all, not even close\r\n");
                    std::thread::sleep(Duration::from_millis(200));
                    return (lines, first);
                } else {
                    // AUTH, MAIL, anything: the point is that it was recorded.
                    sock.write_all(b"530 5.7.0 not like this\r\n").unwrap();
                }
            }
        });
        (port, handle)
    }

    /// ★ **The 2026-09-15 defect, as a test.** A relay that tempts the client
    /// with `AUTH PLAIN` in plaintext and offers STARTTLS: the mailer must ask
    /// for STARTTLS FIRST, send no AUTH in the clear, and speak TLS (a 0x16
    /// handshake record) right after. The fake cannot finish the handshake, so
    /// `verify` refuses — by name, naming the relay.
    #[test]
    fn smtp_on_submission_negotiates_starttls_before_any_auth_and_refuses_at_start() {
        let (port, relay) = fake_relay(
            true,
            "250-fake.relay\r\n250-AUTH PLAIN LOGIN\r\n250 STARTTLS\r\n",
        );
        let m = Mailer::new(
            &format!("smtp://u%40gunnar.rs:s3cr3t-pw@127.0.0.1:{port}"),
            "gunnar.rs <no-reply@gunnar.rs>",
        )
        .unwrap();
        assert_eq!(m.relay().security, Security::StartTls);
        let out = m.verify();
        let (lines, first_tls_byte) = relay.join().unwrap();
        assert_eq!(lines.len(), 2, "{lines:?}");
        assert!(lines[0].starts_with("EHLO"), "{lines:?}");
        assert_eq!(
            lines[1], "STARTTLS",
            "the second word on the wire must be STARTTLS: {lines:?}"
        );
        assert!(
            !lines
                .iter()
                .any(|l| l.to_ascii_uppercase().starts_with("AUTH")),
            "a credential went over plaintext: {lines:?}"
        );
        assert_eq!(
            first_tls_byte,
            Some(0x16),
            "after STARTTLS the client must speak TLS"
        );
        let err = out.expect_err("a relay whose TLS fails is a refusal at start");
        assert!(
            err.contains(&format!("127.0.0.1:{port}"))
                && err.contains("STARTTLS required")
                && err.contains("at start"),
            "{err}"
        );
        assert!(!err.contains("s3cr3t-pw"), "{err}");
    }

    /// A relay that offers no STARTTLS at all is refused, never used in plaintext.
    #[test]
    fn a_relay_without_starttls_is_refused_and_never_sent_a_credential() {
        let (port, relay) = fake_relay(true, "250-fake.relay\r\n250 AUTH PLAIN LOGIN\r\n");
        let m = Mailer::new(&format!("smtp://u:s3cr3t-pw@127.0.0.1:{port}"), "a@b.se").unwrap();
        let err = m.verify().expect_err("no STARTTLS, no mailer");
        let (lines, _) = relay.join().unwrap();
        assert!(
            !lines
                .iter()
                .any(|l| l.to_ascii_uppercase().starts_with("AUTH")),
            "{lines:?}"
        );
        assert!(err.contains("refused this mailer at start"), "{err}");
    }

    /// 465: TLS from the first byte — the client does not wait for a greeting.
    #[test]
    fn smtps_speaks_tls_before_the_relay_says_anything() {
        let (port, relay) = fake_relay(false, "");
        let m = Mailer::new(&format!("smtps://u:p@127.0.0.1:{port}"), "a@b.se").unwrap();
        let _ = m.verify();
        let (_, first) = relay.join().unwrap();
        assert_eq!(
            first,
            Some(0x16),
            "implicit TLS opens with a handshake record"
        );
    }

    /// Nothing listening is not a configuration fault: the service starts.
    #[test]
    fn a_relay_that_cannot_be_reached_is_unreachable_not_refused() {
        let port = TcpListener::bind("127.0.0.1:0")
            .unwrap()
            .local_addr()
            .unwrap()
            .port();
        let m = Mailer::new(&format!("smtp://u:p@127.0.0.1:{port}"), "a@b.se").unwrap();
        assert!(
            matches!(m.verify(), Ok(RelayCheck::Unreachable(_))),
            "{:?}",
            m.verify()
        );
    }

    /// ★ **A box's mail is OFF until a relay is fed** (P4c; owner 2026-10-25, the airgap
    /// default): nothing fed, or half a relay, is a refusal by name and no transport at all.
    #[test]
    fn a_box_fed_no_relay_sends_nothing_and_says_so_by_name() {
        use std::collections::BTreeMap;
        let none = Mailer::from_feed(&BTreeMap::new()).unwrap_err();
        assert!(
            none.contains("mail is off") && none.contains(FEED_SMTP_URL),
            "{none}"
        );
        let blank = BTreeMap::from([
            (FEED_SMTP_URL.to_string(), "  ".to_string()),
            (FEED_SMTP_FROM.to_string(), "a@mock.invalid".to_string()),
        ]);
        assert!(
            Mailer::from_feed(&blank)
                .unwrap_err()
                .contains(FEED_SMTP_URL),
            "blank is unset"
        );
        let half = BTreeMap::from([(
            FEED_SMTP_URL.to_string(),
            "smtp://u:s3cr3t-pw@relay.mock.invalid".to_string(),
        )]);
        let e = Mailer::from_feed(&half).unwrap_err();
        assert!(
            e.contains(FEED_SMTP_FROM) && !e.contains("s3cr3t-pw"),
            "{e}"
        );
        let fed = BTreeMap::from([
            (
                FEED_SMTP_URL.to_string(),
                "smtps://u:p@relay.mock.invalid".to_string(),
            ),
            (
                FEED_SMTP_FROM.to_string(),
                "Holger <estate@acme.mock.invalid>".to_string(),
            ),
        ]);
        let m = Mailer::from_feed(&fed).unwrap();
        assert_eq!(
            (m.relay().host.as_str(), m.relay().port, m.relay().security),
            ("relay.mock.invalid", 465, Security::ImplicitTls)
        );
    }

    /// ★ **Through the front's door** (feature `egress`; the lean estate, A19 row 13): the
    /// relay is reached THROUGH the door (`CONNECT relay:port`), greeted with the sender's own
    /// EHLO name, and a relay that offers no STARTTLS is refused: nothing goes in the clear.
    #[cfg(feature = "egress")]
    #[test]
    fn a_notice_through_the_door_reaches_the_relay_through_it_and_stays_tls() {
        use std::sync::Mutex;
        static ASKED: Mutex<Vec<(String, u16)>> = Mutex::new(Vec::new());
        fn record(host: &str, port: u16, _: &[std::net::SocketAddr]) -> Result<(), String> {
            ASKED.lock().unwrap().push((host.to_string(), port));
            Ok(())
        }
        let door = nordisk_pairing::egress::serve(
            "127.0.0.1:0".parse().unwrap(),
            nordisk_pairing::egress::Policy {
                peer: |_| true,
                admits: record,
            },
        )
        .unwrap();
        let (port, relay) = fake_relay(true, "250-fake.relay\r\n250 AUTH PLAIN LOGIN\r\n");
        let m = Mailer::new(
            &format!("smtp://u:s3cr3t-pw@127.0.0.1:{port}"),
            "Holger <estate@acme.mock.invalid>",
        )
        .unwrap();
        let e = m
            .notice_via(
                Some(&format!("http://{door}")),
                "buyer@mock.invalid",
                "sign-in link",
                "sign in",
                "a link",
            )
            .unwrap_err();
        let (lines, _) = relay.join().unwrap();
        assert!(
            ASKED
                .lock()
                .unwrap()
                .iter()
                .any(|(h, p)| h == "127.0.0.1" && *p == port),
            "the door carried it: {:?}",
            ASKED.lock().unwrap()
        );
        assert_eq!(
            lines.first().map(String::as_str),
            Some("EHLO acme.mock.invalid"),
            "{lines:?}"
        );
        assert!(
            !lines
                .iter()
                .any(|l| l.to_ascii_uppercase().starts_with("AUTH")
                    || l.to_ascii_uppercase().starts_with("MAIL")),
            "{lines:?}"
        );
        assert!(
            e.contains("refused the sign-in link mail") && !e.contains("s3cr3t-pw"),
            "{e}"
        );
    }

    /// A door that is not one is said by name before any relay is dialled.
    #[cfg(feature = "egress")]
    #[test]
    fn a_door_that_does_not_answer_is_said_by_name() {
        let dead = TcpListener::bind("127.0.0.1:0")
            .unwrap()
            .local_addr()
            .unwrap();
        let m = Mailer::new("smtp://u:p@relay.mock.invalid", "a@mock.invalid").unwrap();
        let e = m
            .notice_via(
                Some(&format!("http://{dead}")),
                "b@mock.invalid",
                "sign-in link",
                "s",
                "b",
            )
            .unwrap_err();
        assert!(e.contains("egress door"), "{e}");
    }

    #[test]
    fn a_log_line_never_carries_the_whole_address() {
        assert_eq!(masked("elvira@example.com"), "el***@example.com");
        assert_eq!(masked("nope"), "***");
    }

    /// ★ **The ready mail reads back whole through a standard MIME parser** (live 2026-10-14):
    /// declared MIME, text/plain in UTF-8, its transfer encoding said, and the first-login link
    /// on ONE unbroken line of the raw message, equal to the link that was written.
    #[test]
    fn a_notice_is_proper_mime_and_its_link_reads_back_unbroken() {
        let link = format!(
            "https://acme.test2.gunnar.rs/login?invite={}&user=alice",
            "3d27".repeat(16)
        );
        let body = format!("Your Gunnar acme.test2.gunnar.rs is ready — its first admin is alice, and this line is long enough to wrap at seventy-six.\n\n  {link}\n\nAfter that, sign in with your passkey.\n");
        let raw = render(
            "gunnar.rs <no-reply@gunnar.rs>",
            "buyer@example.com",
            "Your Gunnar is ready",
            &body,
        )
        .unwrap();
        let text = String::from_utf8(raw.clone()).unwrap();
        assert!(text.contains("MIME-Version: 1.0"), "{text}");
        assert!(
            text.to_ascii_lowercase()
                .contains("content-type: text/plain; charset=utf-8"),
            "{text}"
        );
        assert!(text.contains("Content-Transfer-Encoding: "), "{text}");
        assert!(
            text.lines().any(|l| l.trim() == link),
            "the link is not one unbroken line of the raw message:\n{text}"
        );
        let parsed = mail_parser::MessageParser::default()
            .parse(&raw)
            .expect("a MIME message");
        let read = parsed.body_text(0).expect("a text body").into_owned();
        let found = read
            .lines()
            .map(str::trim)
            .find(|l| l.starts_with("https://"))
            .expect("the link");
        assert_eq!(found, link);
        assert!(
            read.contains("ready — its first admin"),
            "UTF-8 reads back: {read}"
        );
    }
}