From 04bb55600d88c26aadb70d6538d24d81a1ac6894 Mon Sep 17 00:00:00 2001 From: mkn Date: Wed, 1 Jul 2026 00:28:13 +0200 Subject: [PATCH] fix(email): skip SMTP AUTH when no username is set (local/anonymous relay) The test email failed with "No compatible authentication mechanism was found": send_text always attached SMTP credentials, so against a local postfix on localhost:25 that accepts mail WITHOUT auth (and advertises no AUTH mechanism) lettre refused to send. Only attach credentials when smtp_user is non-empty; an empty username now means "no AUTH", which is the right behavior for an unauthenticated localhost relay. Authenticated provider relays (user set) are unchanged. clippy -D warnings clean; adapters suite green. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/hyperion-adapters/src/email.rs | 50 +++++++++++++++++++-------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/crates/hyperion-adapters/src/email.rs b/crates/hyperion-adapters/src/email.rs index 5801eb6..039f47b 100644 --- a/crates/hyperion-adapters/src/email.rs +++ b/crates/hyperion-adapters/src/email.rs @@ -93,31 +93,51 @@ pub async fn send_text( .body(body.to_string()) .map_err(|e| AdapterError::Other(format!("smtp: build message: {e}")))?; - let creds = Credentials::new(cfg.smtp_user.clone(), cfg.smtp_password.clone()); + // Only authenticate when a username is configured. A local/anonymous relay + // (e.g. postfix on localhost:25 that accepts mail without auth) advertises + // no AUTH mechanism, and forcing credentials makes lettre fail with + // "No compatible authentication mechanism was found" instead of just + // sending. Empty user ⇒ no AUTH; non-empty user ⇒ authenticate. + let creds = if cfg.smtp_user.trim().is_empty() { + None + } else { + Some(Credentials::new( + cfg.smtp_user.clone(), + cfg.smtp_password.clone(), + )) + }; let transport: AsyncSmtpTransport = match cfg.security.as_str() { - "tls" => AsyncSmtpTransport::::relay(&host) - .map_err(|e| AdapterError::Other(format!("smtp: relay: {e}")))? - .port(port) - .credentials(creds) - .build(), + "tls" => { + let mut b = AsyncSmtpTransport::::relay(&host) + .map_err(|e| AdapterError::Other(format!("smtp: relay: {e}")))? + .port(port); + if let Some(c) = creds { + b = b.credentials(c); + } + b.build() + } "plain" => { // No TLS at all — useful for local dev with a mail catcher - // like mailhog. Wrap in builder() so we can set port + no TLS. - AsyncSmtpTransport::::builder_dangerous(&host) - .port(port) - .credentials(creds) - .build() + // like mailhog, or a localhost postfix relay. Wrap in builder() so + // we can set port + no TLS. + let mut b = AsyncSmtpTransport::::builder_dangerous(&host).port(port); + if let Some(c) = creds { + b = b.credentials(c); + } + b.build() } _ => { // Default: STARTTLS upgrade (most relays expect this on 587). let tls = TlsParameters::new(host.clone()) .map_err(|e| AdapterError::Other(format!("smtp: tls params: {e}")))?; - AsyncSmtpTransport::::starttls_relay(&host) + let mut b = AsyncSmtpTransport::::starttls_relay(&host) .map_err(|e| AdapterError::Other(format!("smtp: starttls: {e}")))? .port(port) - .credentials(creds) - .tls(Tls::Required(tls)) - .build() + .tls(Tls::Required(tls)); + if let Some(c) = creds { + b = b.credentials(c); + } + b.build() } };