use std::time::Duration;
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
use tokio::time::timeout;
use tracing::error;
use crate::{config::SmtpConfig, error::AppError};
pub type SmtpTransport = AsyncSmtpTransport<Tokio1Executor>;
pub fn build_transport(cfg: &SmtpConfig) -> Result<SmtpTransport, AppError> {
use lettre::transport::smtp::authentication::Credentials;
let creds = if let (Some(user), Some(pass)) = (&cfg.auth_user, &cfg.auth_password) {
tracing::debug!(smtp_user = %user, "SMTP AUTH enabled");
Some(Credentials::new(user.clone(), pass.expose().to_string()))
} else {
None
};
let timeout = Some(Duration::from_secs(cfg.connect_timeout_seconds));
let transport = match cfg.tls.as_str() {
"starttls" => {
let mut b = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&cfg.host)
.map_err(|e| {
tracing::error!(error = %e, "STARTTLS transport build failed");
AppError::Internal
})?
.port(cfg.port)
.timeout(timeout);
if let Some(c) = creds { b = b.credentials(c); }
b.build()
}
"tls" => {
let mut b = AsyncSmtpTransport::<Tokio1Executor>::relay(&cfg.host)
.map_err(|e| {
tracing::error!(error = %e, "TLS transport build failed");
AppError::Internal
})?
.port(cfg.port)
.timeout(timeout);
if let Some(c) = creds { b = b.credentials(c); }
b.build()
}
_ => {
let mut b = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&cfg.host)
.port(cfg.port)
.timeout(timeout);
if let Some(c) = creds { b = b.credentials(c); }
b.build()
}
};
Ok(transport)
}
pub async fn submit(
transport: &SmtpTransport,
message: Message,
timeout_seconds: u64,
) -> Result<(), AppError> {
let result = timeout(
Duration::from_secs(timeout_seconds),
transport.send(message),
)
.await;
match result {
Ok(Ok(_response)) => Ok(()),
Ok(Err(e)) => {
error!(smtp_error = %e, "SMTP submission failed");
Err(AppError::SmtpUnavailable)
}
Err(_elapsed) => {
error!("SMTP submission timed out after {timeout_seconds}s");
Err(AppError::SmtpUnavailable)
}
}
}
pub async fn is_smtp_reachable(cfg: &SmtpConfig) -> bool {
let addr = format!("{}:{}", cfg.host, cfg.port);
timeout(
Duration::from_secs(cfg.connect_timeout_seconds),
tokio::net::TcpStream::connect(&addr),
)
.await
.map(|r| r.is_ok())
.unwrap_or(false)
}
pub async fn submit_pipe(
message: Message,
pipe_command: &str,
timeout_seconds: u64,
) -> Result<(), AppError> {
use tokio::process::Command;
use tokio::io::AsyncWriteExt;
use std::process::Stdio;
let raw = message.formatted();
let result = tokio::time::timeout(
std::time::Duration::from_secs(timeout_seconds),
async {
let mut child = Command::new(pipe_command)
.arg("-t")
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| {
tracing::error!(
command = pipe_command,
error = %e,
"failed to spawn pipe command"
);
AppError::SmtpUnavailable
})?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(&raw).await.map_err(|e| {
tracing::error!(error = %e, "failed to write to pipe command stdin");
AppError::SmtpUnavailable
})?;
}
let output = child.wait_with_output().await.map_err(|e| {
tracing::error!(error = %e, "pipe command wait failed");
AppError::SmtpUnavailable
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
tracing::error!(
command = pipe_command,
exit_code = ?output.status.code(),
stderr = %stderr,
"pipe command exited with error"
);
return Err(AppError::SmtpUnavailable);
}
Ok(())
}
).await;
match result {
Ok(r) => r,
Err(_elapsed) => {
tracing::error!(
command = pipe_command,
timeout_seconds,
"pipe command timed out"
);
Err(AppError::SmtpUnavailable)
}
}
}