use std::time::Duration; use lettre::AsyncTransport; use lettre::Tokio1Executor; use lettre::message::Mailbox; use lettre::message::Message; use lettre::transport::smtp::AsyncSmtpTransport; use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::client::{Tls, TlsParameters}; use crate::config::AppEmailSettings; use crate::error::EmailError; pub struct EmailTransport { inner: AsyncSmtpTransport, from: Mailbox, } impl EmailTransport { pub fn from_config(cfg: &AppEmailSettings) -> Result { cfg.is_valid().map_err(EmailError::Config)?; let port = cfg.smtp_port; let resp_timeout = Some(Duration::from_millis(cfg.response_timeout_ms)); let host = cfg.smtp_host.clone(); let builder = if port == 465 { AsyncSmtpTransport::::builder_dangerous(host.clone()) .port(port) .timeout(resp_timeout) .tls(Tls::Wrapper(TlsParameters::new(host.clone())?)) } else if port == 587 { AsyncSmtpTransport::::relay(&host)? .port(port) .timeout(resp_timeout) } else { AsyncSmtpTransport::::builder_dangerous(host) .port(port) .timeout(resp_timeout) }; let transport = if let (Some(u), Some(p)) = (&cfg.smtp_username, &cfg.smtp_password) { builder .credentials(Credentials::new(u.clone(), p.clone())) .build() } else { builder.build() }; let from = Mailbox::new(cfg.from_name.clone(), cfg.from_address.clone().parse()?); Ok(Self { inner: transport, from, }) } pub async fn send(&self, message: Message) -> Result { let response = self.inner.send(message).await?; let text = response.message().collect::>().join(" "); Ok(text) } pub fn from_mailbox(&self) -> Mailbox { self.from.clone() } } #[cfg(test)] mod tests { use super::*; fn base_cfg() -> AppEmailSettings { AppEmailSettings { enabled: true, smtp_host: "smtp.example.com".into(), smtp_port: 587, ..Default::default() } } #[test] fn build_transport_with_default_config() { let transport = EmailTransport::from_config(&base_cfg()).unwrap(); assert_eq!(transport.from.email.to_string(), "no-reply@example.com"); } #[test] fn build_transport_with_credentials() { let cfg = AppEmailSettings { smtp_username: Some("user".into()), smtp_password: Some("pw".into()), ..base_cfg() }; let _transport = EmailTransport::from_config(&cfg).unwrap(); } #[test] fn build_transport_implicit_tls_465() { let cfg = AppEmailSettings { smtp_port: 465, ..base_cfg() }; let _transport = EmailTransport::from_config(&cfg).unwrap(); } #[test] fn build_transport_plain_25() { let cfg = AppEmailSettings { smtp_port: 25, ..base_cfg() }; let _transport = EmailTransport::from_config(&cfg).unwrap(); } #[test] fn invalid_config_rejected() { let cfg = AppEmailSettings { enabled: true, smtp_host: "".into(), ..Default::default() }; assert!(EmailTransport::from_config(&cfg).is_err()); } }