use config::AppConfig; use lettre::message::Mailbox; use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::client::Tls; use lettre::transport::smtp::{PoolConfig, SmtpTransport}; use lettre::Transport; use regex::Regex; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; use std::time::Duration; use tokio::sync::mpsc; #[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()); #[derive(Clone)] pub struct AppEmail { sender: mpsc::Sender, } 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!("TLS build error: {}", 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) .pool_config(PoolConfig::new().min_idle(5).max_size(100)) .build(); let from: Mailbox = smtp_from.parse()?; let (tx, mut rx) = mpsc::channel::(100); tokio::spawn(async move { while let Some(msg) = rx.recv().await { if !EMAIL_REGEX.is_match(&msg.clone().to) { continue; } let email = match lettre::Message::builder() .from(from.clone()) .to(msg.clone().to.parse().unwrap()) .subject(msg.clone().subject) .body(msg.clone().body) { Ok(e) => e, Err(_) => continue, }; let mut success = false; for i in 0..3 { let mailer = mailer.clone(); let email = email.clone(); let result = tokio::task::spawn_blocking(move || mailer.send(&email)).await; match result { Ok(Ok(_)) => { success = true; break; } _ => { let backoff = 100 * (i + 1); tokio::time::sleep(Duration::from_millis(backoff)).await; } } } if !success { println!("[System] email send fail: {:?}", msg.clone()); } } }); Ok(Self { sender: tx }) } pub async fn send(&self, msg: EmailMessage) -> anyhow::Result<()> { self.sender .send(msg) .await .map_err(|e| anyhow::anyhow!("queue send error: {}", e)) } }