//! Outgoing mail: SMTP submission (lettre) or local sendmail binary. use std::time::Duration; use anyhow::{anyhow, bail, Context, Result}; use lettre::address::Envelope; use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::client::{Tls, TlsParameters}; use lettre::{Address, AsyncSmtpTransport, AsyncTransport, Tokio1Executor}; use tokio::io::AsyncWriteExt; use crate::config::{SendMethod, SmtpAuth, SmtpSettings, TlsMode}; pub async fn send( cfg: &SmtpSettings, login: &str, password: &str, from: &str, recipients: &[String], raw: &[u8], ) -> Result<()> { if recipients.is_empty() { bail!("message has no recipients"); } if raw.len() as u64 > cfg.max_message_mb * 1024 * 1024 { bail!("message too large"); } let from_addr: Address = from .parse() .map_err(|_| anyhow!("invalid sender address {from}"))?; let mut to = Vec::with_capacity(recipients.len()); for r in recipients { to.push( r.parse::
() .map_err(|_| anyhow!("invalid recipient address {r}"))?, ); } match cfg.method { SendMethod::Smtp => { let tls = |host: &str| -> Result { Ok(TlsParameters::builder(host.to_string()) .dangerous_accept_invalid_certs(!cfg.verify_tls) .dangerous_accept_invalid_hostnames(!cfg.verify_tls) .build_rustls()?) }; let mut b = AsyncSmtpTransport::::builder_dangerous(&cfg.host) .port(cfg.port) .timeout(Some(Duration::from_secs(cfg.timeout_secs.max(5)))); b = match cfg.security { TlsMode::None => b.tls(Tls::None), TlsMode::StartTls => b.tls(Tls::Required(tls(&cfg.host)?)), TlsMode::Tls => b.tls(Tls::Wrapper(tls(&cfg.host)?)), }; b = match cfg.auth { SmtpAuth::None => b, SmtpAuth::UserCredentials => { b.credentials(Credentials::new(login.to_string(), password.to_string())) } SmtpAuth::Static => { b.credentials(Credentials::new(cfg.username.clone(), cfg.password.clone())) } }; let transport = b.build(); let env = Envelope::new(Some(from_addr), to).context("invalid envelope")?; transport .send_raw(&env, raw) .await .map_err(|e| anyhow!("SMTP: {e}"))?; Ok(()) } SendMethod::Sendmail => { let mut cmd = tokio::process::Command::new(&cfg.sendmail_path); cmd.arg("-i").arg("-f").arg(&from_addr).arg("--"); for t in &to { cmd.arg(t); } cmd.stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::piped()) .kill_on_drop(true); let mut child = cmd.spawn().context("spawn sendmail")?; if let Some(mut stdin) = child.stdin.take() { stdin.write_all(raw).await?; stdin.shutdown().await.ok(); } let out = tokio::time::timeout( Duration::from_secs(cfg.timeout_secs.max(5)), child.wait_with_output(), ) .await .map_err(|_| anyhow!("sendmail timeout"))??; if !out.status.success() { bail!( "sendmail failed ({}): {}", out.status, String::from_utf8_lossy(&out.stderr).trim() ); } Ok(()) } } } /// Check connectivity (and optionally authentication) for the panel "test" button. pub async fn test(cfg: &SmtpSettings, login: &str, password: &str) -> Result { match cfg.method { SendMethod::Sendmail => { if std::path::Path::new(&cfg.sendmail_path).exists() { Ok(format!("{} exists", cfg.sendmail_path)) } else { bail!("{} not found", cfg.sendmail_path) } } SendMethod::Smtp => { let tls = TlsParameters::builder(cfg.host.clone()) .dangerous_accept_invalid_certs(!cfg.verify_tls) .dangerous_accept_invalid_hostnames(!cfg.verify_tls) .build_rustls()?; let mut b = AsyncSmtpTransport::::builder_dangerous(&cfg.host) .port(cfg.port) .timeout(Some(Duration::from_secs(10))); b = match cfg.security { TlsMode::None => b.tls(Tls::None), TlsMode::StartTls => b.tls(Tls::Required(tls)), TlsMode::Tls => b.tls(Tls::Wrapper(tls)), }; match cfg.auth { SmtpAuth::UserCredentials if !login.is_empty() => { b = b.credentials(Credentials::new(login.into(), password.into())) } SmtpAuth::Static => { b = b.credentials(Credentials::new(cfg.username.clone(), cfg.password.clone())) } _ => {} } let t: AsyncSmtpTransport = b.build(); let ok = t.test_connection().await.map_err(|e| anyhow!("{e}"))?; if ok { Ok(format!("SMTP {}:{} OK", cfg.host, cfg.port)) } else { bail!("SMTP connection test failed") } } } }