use config::AppConfig; use lettre::message::Mailbox; use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::client::Tls; use lettre::{SmtpTransport, Transport}; use regex::Regex; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; use std::time::Duration; #[derive(Clone)] pub struct AppEmail { pub cred: Credentials, pub mailer: SmtpTransport, pub from: Mailbox, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct EmailMessage { pub to: String, pub subject: String, pub body: String, } static EMAIL_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap()); impl AppEmail { pub async fn init(cfg: &AppConfig) -> anyhow::Result { let smtp_host = cfg.smtp_host()?; let smtp_port = cfg.smtp_port()?; let smtp_username = cfg.smtp_username()?; let smtp_password = cfg.smtp_password()?; let smtp_from = cfg.smtp_from()?; let smtp_tls = cfg.smtp_tls()?; let smtp_timeout = cfg.smtp_timeout()?; let cred = Credentials::new(smtp_username, smtp_password); let tls_param = if smtp_tls { Tls::Required( lettre::transport::smtp::client::TlsParameters::builder(smtp_host.clone()) .build() .map_err(|e| anyhow::anyhow!("Failed to build TLS parameters: {}", e))?, ) } else { Tls::None }; let mailer = SmtpTransport::builder_dangerous(smtp_host) .port(smtp_port) .tls(tls_param) .timeout(Some(Duration::from_secs(smtp_timeout))) .credentials(cred.clone()) .build(); Ok(AppEmail { cred, mailer, from: smtp_from .parse() .map_err(|e| anyhow::anyhow!("Invalid from address: {}", e))?, }) } pub async fn send(&self, msg: EmailMessage) -> anyhow::Result<()> { if !EMAIL_REGEX.is_match(&msg.to) { return Err(anyhow::anyhow!("Invalid email address format: {}", msg.to)); } let email = lettre::Message::builder() .from(self.from.clone()) .to(msg.to.parse()?) .subject(msg.subject) .body(msg.body)?; self.mailer .send(&email) .map_err(|e| anyhow::anyhow!("{}", e))?; Ok(()) } }