fix(email): resolve SMTP connection failures (port 465 SMTPS, URL double scheme, retry backoff)
This commit is contained in:
parent
882e86dc33
commit
39d30678b5
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2277,6 +2277,7 @@ dependencies = [
|
||||
"lettre",
|
||||
"regex",
|
||||
"serde",
|
||||
"slog",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<Self> {
|
||||
pub async fn init(cfg: &AppConfig, logs: slog::Logger) -> anyhow::Result<Self> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -95,12 +95,13 @@ impl AppService {
|
||||
pub async fn new(config: AppConfig) -> anyhow::Result<Self> {
|
||||
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<anyhow::Result<deadpool_redis::cluster::Connection>>
|
||||
|
||||
@ -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()
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user