use std::sync::mpsc; use std::thread; use std::time::Duration; use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::client::{Certificate, Tls, TlsParameters}; use lettre::{Message, SmtpTransport, Transport}; use crate::config::{SmtpConfig, TlsMode}; /// Per-operation socket I/O timeout (read/write). const SMTP_IO_TIMEOUT: Duration = Duration::from_secs(15); /// Hard wall-clock limit covering DNS + TCP connect + full send. const SMTP_WALL_TIMEOUT: Duration = Duration::from_secs(30); pub(crate) fn send_email( cfg: &SmtpConfig, to: &str, subject: &str, body: &str, ) -> Result<(), String> { let email = Message::builder() .from(cfg.from.parse().map_err(|e: lettre::address::AddressError| e.to_string())?) .to(to.parse().map_err(|e: lettre::address::AddressError| e.to_string())?) .subject(subject) .body(body.to_string()) .map_err(|e| e.to_string())?; let creds = Credentials::new(cfg.username.clone(), cfg.password.clone().unwrap_or_default()); let transport = match cfg.tls_mode { TlsMode::Starttls => { // STARTTLS: plain connect, then upgrade (port 587) SmtpTransport::relay(&cfg.host) .map_err(|e| e.to_string())? .port(cfg.port) .credentials(creds) .timeout(Some(SMTP_IO_TIMEOUT)) .build() } TlsMode::Smtps => { // SMTPS: TLS from the first byte (port 465) let tls = if let Some(ref der) = cfg.tls_cert_der { let cert = Certificate::from_der(der.clone()).map_err(|e| e.to_string())?; TlsParameters::builder(cfg.host.clone()) .add_root_certificate(cert) .build_native() .map_err(|e| e.to_string())? } else { TlsParameters::new(cfg.host.clone()).map_err(|e| e.to_string())? }; SmtpTransport::relay(&cfg.host) .map_err(|e| e.to_string())? .port(cfg.port) .tls(Tls::Wrapper(tls)) .credentials(creds) .timeout(Some(SMTP_IO_TIMEOUT)) .build() } TlsMode::None => { // Plain text, no TLS at all (port 25 or unencrypted 587) SmtpTransport::builder_dangerous(&cfg.host) .port(cfg.port) .credentials(creds) .timeout(Some(SMTP_IO_TIMEOUT)) .build() } }; // Spawn the blocking send on a thread so we can impose a hard wall-clock // timeout that covers DNS resolution and TCP connect (not just I/O). let (tx, rx) = mpsc::channel(); thread::spawn(move || { let _ = tx.send(transport.send(&email).map_err(|e| e.to_string())); }); rx.recv_timeout(SMTP_WALL_TIMEOUT) .map_err(|_| format!("SMTP timed out after {}s", SMTP_WALL_TIMEOUT.as_secs()))? .map(|_| ()) }