diff --git a/Cargo.lock b/Cargo.lock index b169829..331646d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2277,6 +2277,7 @@ dependencies = [ "lettre", "regex", "serde", + "slog", "tokio", ] diff --git a/libs/email/Cargo.toml b/libs/email/Cargo.toml index 9779194..738f115 100644 --- a/libs/email/Cargo.toml +++ b/libs/email/Cargo.toml @@ -17,9 +17,10 @@ path = "lib.rs" [dependencies] config = { workspace = true } lettre = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread", "rt"] } +tokio = { workspace = true, features = ["rt-multi-thread", "rt", "sync", "macros"] } serde = { workspace = true, features = ["derive"] } anyhow = { workspace = true } regex = { workspace = true } +slog = { workspace = true } [lints] workspace = true diff --git a/libs/email/lib.rs b/libs/email/lib.rs index 094688c..3b651f5 100644 --- a/libs/email/lib.rs +++ b/libs/email/lib.rs @@ -1,7 +1,5 @@ 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; @@ -26,7 +24,7 @@ pub struct AppEmail { } impl AppEmail { - pub async fn init(cfg: &AppConfig) -> anyhow::Result { + pub async fn init(cfg: &AppConfig, logs: slog::Logger) -> anyhow::Result { let smtp_host = cfg.smtp_host()?; let smtp_port = cfg.smtp_port()?; let smtp_username = cfg.smtp_username()?; @@ -35,24 +33,28 @@ impl AppEmail { 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))?, + // Port 465 = SMTPS (implicit TLS via smtps://), others = STARTTLS via smtp:// + let url = if smtp_port == 465 && smtp_tls { + format!( + "smtps://{}:{}@{}:{}", + smtp_username, smtp_password, smtp_host, smtp_port ) } else { - Tls::None + let tls_mode = if smtp_tls { + "required" + } else { + "opportunistic" + }; + format!( + "smtp://{}:{}@{}:{}?tls={}", + smtp_username, smtp_password, smtp_host, smtp_port, tls_mode + ) }; - let mailer = SmtpTransport::builder_dangerous(smtp_host) - .port(smtp_port) - .tls(tls_param) + let mailer = SmtpTransport::from_url(&url) + .map_err(|e| anyhow::anyhow!("SMTP transport build error: {}", e))? .timeout(Some(Duration::from_secs(smtp_timeout))) - .credentials(cred) - .pool_config(PoolConfig::new().min_idle(5).max_size(100)) + .pool_config(PoolConfig::new().min_idle(0).max_size(10)) .build(); let from: Mailbox = smtp_from.parse()?; @@ -72,13 +74,15 @@ impl AppEmail { .body(msg.clone().body) { Ok(e) => e, - Err(_) => continue, + Err(_) => { + slog::warn!(logs, "Email build error: to={}", msg.to); + 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 { @@ -86,15 +90,26 @@ impl AppEmail { success = true; break; } - _ => { - let backoff = 100 * (i + 1); - tokio::time::sleep(Duration::from_millis(backoff)).await; + Ok(Err(e)) => { + if i == 2 { + slog::error!( + logs, + "Email send failed after retries: to={}, error={}", + msg.to, + e + ); + } + tokio::time::sleep(Duration::from_secs((1 << i) as u64)).await; + } + Err(e) => { + slog::error!(logs, "Email spawn error: to={}, err={}", msg.to, e); + break; } } } if !success { - println!("[System] email send fail: {:?}", msg.clone()); + slog::warn!(logs, "Email send permanently failed: to={}", msg.to); } } }); diff --git a/libs/service/auth/email.rs b/libs/service/auth/email.rs index d1bb2c2..fb5dd85 100644 --- a/libs/service/auth/email.rs +++ b/libs/service/auth/email.rs @@ -104,7 +104,7 @@ impl AppService { .main_domain() .map_err(|_| AppError::DoMainNotSet)?; - let verify_link = format!("https://{}/auth/verify-email?token={}", domain, token); + let verify_link = format!("{}/auth/verify-email?token={}", domain, token); let envelope = queue::EmailEnvelope { id: Uuid::new_v4(), diff --git a/libs/service/auth/password.rs b/libs/service/auth/password.rs index c25a2d0..0b84505 100644 --- a/libs/service/auth/password.rs +++ b/libs/service/auth/password.rs @@ -122,7 +122,7 @@ impl AppService { .map_err(|_| AppError::DoMainNotSet)?; let email_address = params.email.clone(); - let reset_link = format!("https://{}/auth/reset-password?token={}", domain, token); + let reset_link = format!("{}/auth/reset-password?token={}", domain, token); let envelope = queue::EmailEnvelope { id: Uuid::new_v4(), diff --git a/libs/service/lib.rs b/libs/service/lib.rs index 8a108e9..eb7c790 100644 --- a/libs/service/lib.rs +++ b/libs/service/lib.rs @@ -95,12 +95,13 @@ impl AppService { pub async fn new(config: AppConfig) -> anyhow::Result { let db = AppDatabase::init(&config).await?; let cache = AppCache::init(&config).await?; - let email = AppEmail::init(&config).await?; - let avatar = AppAvatar::init(&config).await?; let log_level = config.log_level().unwrap_or_else(|_| "info".to_string()); let logs = Self::build_slog_logger(&log_level); + let email = AppEmail::init(&config, logs.clone()).await?; + let avatar = AppAvatar::init(&config).await?; + // Build get_redis closure for MessageProducer let get_redis: Arc< dyn Fn() -> tokio::task::JoinHandle> diff --git a/libs/service/workspace/members.rs b/libs/service/workspace/members.rs index 1c4d5d4..63b4d44 100644 --- a/libs/service/workspace/members.rs +++ b/libs/service/workspace/members.rs @@ -350,7 +350,7 @@ impl AppService { .map_err(|_| AppError::DoMainNotSet)?; let invite_link = format!( - "https://{}/auth/accept-workspace-invite?token={}", + "{}/auth/accept-workspace-invite?token={}", domain, token.clone() );