79 lines
No EOL
2.9 KiB
Rust
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(|_| ())
|
|
} |