From 4b347fda2b869b9ccde8edcfdb3147286deccf31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Tue, 2 Jun 2026 00:34:12 +0200 Subject: [PATCH] refactor: improve sasl config --- config.sample.toml | 25 ++++++++++--------------- src/account/check.rs | 20 ++++++++++++++++++-- src/config.rs | 25 ++++++++++++++----------- src/imap/client.rs | 9 ++++++++- src/shared/client.rs | 18 ++++++++++++++++-- src/smtp/client.rs | 9 ++++++++- 6 files changed, 74 insertions(+), 32 deletions(-) diff --git a/config.sample.toml b/config.sample.toml index dad06709..9ad3a2b3 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -180,22 +180,19 @@ imap.server = "example.com" # SASL PLAIN # https://datatracker.ietf.org/doc/html/rfc4616 -imap.sasl.plain.authcid = "user@example.com" -imap.sasl.plain.passwd.raw = "***" -#imap.sasl.plain.passwd.command = "pass show example" -#imap.sasl.plain.passwd.command = ["mimosa", "password", "read", "example"] +imap.sasl.plain.username = "user@example.com" +imap.sasl.plain.password.raw = "***" +#imap.sasl.plain.password.command = "pass show example" # SASL LOGIN # https://datatracker.ietf.org/doc/html/draft-murchison-sasl-login-00 #imap.sasl.login.username = "user@example.com" #imap.sasl.login.password.raw = "***" -# SASL OAUTHBEARER (host/port echoed in the GS2 header; usually mirror the -# server you actually connect to). +# SASL OAUTHBEARER (host/port for the GS2 header are derived from the +# IMAP server URL at connect time). # https://datatracker.ietf.org/doc/html/rfc7628 #imap.sasl.oauthbearer.username = "user@example.com" -#imap.sasl.oauthbearer.host = "imap.example.com" -#imap.sasl.oauthbearer.port = 993 #imap.sasl.oauthbearer.token.raw = "***" #imap.sasl.oauthbearer.token.command = ["ortie", "token", "read", "example"] @@ -312,19 +309,17 @@ smtp.server = "example.com" #smtp.sasl.anonymous.message = "himalaya" # SASL PLAIN -smtp.sasl.plain.authcid = "user@example.com" -smtp.sasl.plain.passwd.raw = "***" -#smtp.sasl.plain.passwd.command = "pass show example" -#smtp.sasl.plain.passwd.command = ["mimosa", "password", "read", "example"] +smtp.sasl.plain.username = "user@example.com" +smtp.sasl.plain.password.raw = "***" +#smtp.sasl.plain.password.command = "pass show example" # SASL LOGIN #smtp.sasl.login.username = "user@example.com" #smtp.sasl.login.password.raw = "***" -# SASL OAUTHBEARER +# SASL OAUTHBEARER (host/port for the GS2 header are derived from the +# SMTP server URL at connect time). #smtp.sasl.oauthbearer.username = "user@example.com" -#smtp.sasl.oauthbearer.host = "smtp.example.com" -#smtp.sasl.oauthbearer.port = 465 #smtp.sasl.oauthbearer.token.raw = "***" # SASL XOAUTH2 diff --git a/src/account/check.rs b/src/account/check.rs index f54c76c8..4fb11bb7 100644 --- a/src/account/check.rs +++ b/src/account/check.rs @@ -120,9 +120,17 @@ fn check_imap( let result = (|| -> Result<()> { let tls = imap_config.tls.clone().into_tls(imap_config.alpn.clone()); - let sasl: Option = imap_config.sasl.clone().map(Sasl::try_from).transpose()?; let auto_id = resolve_auto_id_params(&imap_config.id)?; let server = crate::imap::client::parse_imap_server(&imap_config.server)?; + let sasl: Option = imap_config + .sasl + .clone() + .and_then(|cfg| { + let host = server.host_str()?; + let port = server.port_or_known_default()?; + Some(cfg.try_into_sasl(host, port)) + }) + .transpose()?; let _ = ImapClientStd::connect(&server, &tls, imap_config.starttls, sasl, auto_id)?; Ok(()) })(); @@ -184,9 +192,17 @@ fn check_smtp( let result = (|| -> Result<()> { let tls = smtp_config.tls.clone().into_tls(smtp_config.alpn.clone()); - let sasl: Option = smtp_config.sasl.clone().map(Sasl::try_from).transpose()?; let domain: EhloDomain<'static> = Ipv4Addr::new(127, 0, 0, 1).into(); let server = crate::smtp::client::parse_smtp_server(&smtp_config.server)?; + let sasl: Option = smtp_config + .sasl + .clone() + .and_then(|cfg| { + let host = server.host_str()?; + let port = server.port_or_known_default()?; + Some(cfg.try_into_sasl(host, port)) + }) + .transpose()?; let _client = SmtpClientStd::connect(&server, &tls, smtp_config.starttls, domain, sasl)?; Ok(()) })(); diff --git a/src/config.rs b/src/config.rs index 509890c2..f1c3c3b0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -518,14 +518,17 @@ pub struct SaslLoginConfig { pub struct SaslPlainConfig { pub authzid: Option, #[serde(deserialize_with = "shell_expanded_string")] + #[serde(alias = "username")] pub authcid: String, + #[serde(alias = "password")] pub passwd: Secret, } /// SASL OAUTHBEARER configuration [rfc7628]. /// -/// `host` and `port` are echoed verbatim in the GS2 header and should -/// match the server the connection is actually opened against. +/// The `host` and `port` echoed in the GS2 header are derived from +/// the live IMAP/SMTP server URL at connect time, so they aren't part +/// of the user-facing config. /// /// [rfc7628]: https://www.iana.org/go/rfc7628 #[derive(Clone, Debug, Deserialize, Serialize)] @@ -533,8 +536,6 @@ pub struct SaslPlainConfig { pub struct SaslOauthbearerConfig { #[serde(deserialize_with = "shell_expanded_string")] pub username: String, - pub host: String, - pub port: u16, pub token: Secret, } @@ -559,11 +560,13 @@ pub struct SaslScramSha256Config { pub password: Secret, } -impl TryFrom for Sasl { - type Error = anyhow::Error; - - fn try_from(config: SaslConfig) -> Result { - Ok(match config { +impl SaslConfig { + /// Resolves the SASL config into a runtime [`Sasl`]. `host` and + /// `port` come from the live server URL; they are only used by + /// OAUTHBEARER (echoed in the GS2 header) and ignored by every + /// other mechanism. + pub fn try_into_sasl(self, host: impl ToString, port: u16) -> Result { + Ok(match self { SaslConfig::Anonymous(c) => Sasl::Anonymous(SaslAnonymous { message: c.message }), SaslConfig::Login(c) => Sasl::Login(SaslLogin { username: c.username, @@ -576,8 +579,8 @@ impl TryFrom for Sasl { }), SaslConfig::Oauthbearer(c) => Sasl::Oauthbearer(SaslOauthbearer { username: c.username, - host: c.host, - port: c.port, + host: host.to_string(), + port, token: c.token.get()?, }), SaslConfig::Xoauth2(c) => Sasl::Xoauth2(SaslXoauth2 { diff --git a/src/imap/client.rs b/src/imap/client.rs index 3615b0a5..d3b722ea 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -49,9 +49,16 @@ impl ImapClient { /// [`Inner::capability`] explicitly. pub fn new(config: ImapConfig) -> Result { let tls = config.tls.into_tls(config.alpn); - let sasl: Option = config.sasl.map(Sasl::try_from).transpose()?; let auto_id = resolve_auto_id_params(&config.id)?; let server = parse_imap_server(&config.server)?; + let sasl: Option = config + .sasl + .and_then(|cfg| { + let host = server.host_str()?; + let port = server.port_or_known_default()?; + Some(cfg.try_into_sasl(host, port)) + }) + .transpose()?; let (inner, _capability) = Inner::connect(&server, &tls, config.starttls, sasl, auto_id)?; Ok(Self { inner }) } diff --git a/src/shared/client.rs b/src/shared/client.rs index df4f0bea..95713826 100644 --- a/src/shared/client.rs +++ b/src/shared/client.rs @@ -78,9 +78,16 @@ impl EmailClient { use crate::imap::id::resolve_auto_id_params; let tls = imap_config.tls.into_tls(imap_config.alpn); - let sasl: Option = imap_config.sasl.map(Sasl::try_from).transpose()?; let auto_id = resolve_auto_id_params(&imap_config.id)?; let server = crate::imap::client::parse_imap_server(&imap_config.server)?; + let sasl: Option = imap_config + .sasl + .and_then(|cfg| { + let host = server.host_str()?; + let port = server.port_or_known_default()?; + Some(cfg.try_into_sasl(host, port)) + }) + .transpose()?; let imap = ImapClientStd::connect(&server, &tls, imap_config.starttls, sasl, auto_id)?; inner = inner.with_imap(imap); @@ -126,9 +133,16 @@ impl EmailClient { use pimalaya_stream::sasl::Sasl; let tls = smtp_config.tls.into_tls(smtp_config.alpn); - let sasl: Option = smtp_config.sasl.map(Sasl::try_from).transpose()?; let domain: EhloDomain<'static> = Ipv4Addr::new(127, 0, 0, 1).into(); let server = crate::smtp::client::parse_smtp_server(&smtp_config.server)?; + let sasl: Option = smtp_config + .sasl + .and_then(|cfg| { + let host = server.host_str()?; + let port = server.port_or_known_default()?; + Some(cfg.try_into_sasl(host, port)) + }) + .transpose()?; inner = inner.with_smtp(SmtpClientStd::connect( &server, &tls, diff --git a/src/smtp/client.rs b/src/smtp/client.rs index 17969906..5a79cf02 100644 --- a/src/smtp/client.rs +++ b/src/smtp/client.rs @@ -46,9 +46,16 @@ impl SmtpClient { /// SASL). pub fn new(config: SmtpConfig) -> Result { let tls = config.tls.into_tls(config.alpn); - let sasl: Option = config.sasl.map(Sasl::try_from).transpose()?; let domain: EhloDomain<'static> = Ipv4Addr::new(127, 0, 0, 1).into(); let server = parse_smtp_server(&config.server)?; + let sasl: Option = config + .sasl + .and_then(|cfg| { + let host = server.host_str()?; + let port = server.port_or_known_default()?; + Some(cfg.try_into_sasl(host, port)) + }) + .transpose()?; let inner = Inner::connect(&server, &tls, config.starttls, domain, sasl)?; Ok(Self { inner }) }