tuimail/src/smtp.rs
2026-03-02 16:27:46 +01:00

79 lines
No EOL
2.9 KiB
Rust

use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use lettre::transport::smtp::authentication::Credentials;
use lettre::transport::smtp::client::{Certificate, Tls, TlsParameters};
use lettre::{Message, SmtpTransport, Transport};
use crate::config::{SmtpConfig, TlsMode};
/// Per-operation socket I/O timeout (read/write).
const SMTP_IO_TIMEOUT: Duration = Duration::from_secs(15);
/// Hard wall-clock limit covering DNS + TCP connect + full send.
const SMTP_WALL_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) fn send_email(
cfg: &SmtpConfig,
to: &str,
subject: &str,
body: &str,
) -> Result<(), String> {
let email = Message::builder()
.from(cfg.from.parse().map_err(|e: lettre::address::AddressError| e.to_string())?)
.to(to.parse().map_err(|e: lettre::address::AddressError| e.to_string())?)
.subject(subject)
.body(body.to_string())
.map_err(|e| e.to_string())?;
let creds = Credentials::new(cfg.username.clone(), cfg.password.clone().unwrap_or_default());
let transport = match cfg.tls_mode {
TlsMode::Starttls => {
// STARTTLS: plain connect, then upgrade (port 587)
SmtpTransport::relay(&cfg.host)
.map_err(|e| e.to_string())?
.port(cfg.port)
.credentials(creds)
.timeout(Some(SMTP_IO_TIMEOUT))
.build()
}
TlsMode::Smtps => {
// SMTPS: TLS from the first byte (port 465)
let tls = if let Some(ref der) = cfg.tls_cert_der {
let cert = Certificate::from_der(der.clone()).map_err(|e| e.to_string())?;
TlsParameters::builder(cfg.host.clone())
.add_root_certificate(cert)
.build_native()
.map_err(|e| e.to_string())?
} else {
TlsParameters::new(cfg.host.clone()).map_err(|e| e.to_string())?
};
SmtpTransport::relay(&cfg.host)
.map_err(|e| e.to_string())?
.port(cfg.port)
.tls(Tls::Wrapper(tls))
.credentials(creds)
.timeout(Some(SMTP_IO_TIMEOUT))
.build()
}
TlsMode::None => {
// Plain text, no TLS at all (port 25 or unencrypted 587)
SmtpTransport::builder_dangerous(&cfg.host)
.port(cfg.port)
.credentials(creds)
.timeout(Some(SMTP_IO_TIMEOUT))
.build()
}
};
// Spawn the blocking send on a thread so we can impose a hard wall-clock
// timeout that covers DNS resolution and TCP connect (not just I/O).
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let _ = tx.send(transport.send(&email).map_err(|e| e.to_string()));
});
rx.recv_timeout(SMTP_WALL_TIMEOUT)
.map_err(|_| format!("SMTP timed out after {}s", SMTP_WALL_TIMEOUT.as_secs()))?
.map(|_| ())
}