//! Outgoing mail: SMTP submission (lettre) or local sendmail binary.
use std::time::Duration;
use anyhow::{anyhow, bail, Context, Result};
use lettre::address::Envelope;
use lettre::transport::smtp::authentication::Credentials;
use lettre::transport::smtp::client::{Tls, TlsParameters};
use lettre::{Address, AsyncSmtpTransport, AsyncTransport, Tokio1Executor};
use tokio::io::AsyncWriteExt;
use crate::config::{SendMethod, SmtpAuth, SmtpSettings, TlsMode};
pub async fn send(
cfg: &SmtpSettings,
login: &str,
password: &str,
from: &str,
recipients: &[String],
raw: &[u8],
) -> Result<()> {
if recipients.is_empty() {
bail!("message has no recipients");
}
if raw.len() as u64 > cfg.max_message_mb * 1024 * 1024 {
bail!("message too large");
}
let from_addr: Address = from
.parse()
.map_err(|_| anyhow!("invalid sender address {from}"))?;
let mut to = Vec::with_capacity(recipients.len());
for r in recipients {
to.push(
r.parse::
()
.map_err(|_| anyhow!("invalid recipient address {r}"))?,
);
}
match cfg.method {
SendMethod::Smtp => {
let tls = |host: &str| -> Result {
Ok(TlsParameters::builder(host.to_string())
.dangerous_accept_invalid_certs(!cfg.verify_tls)
.dangerous_accept_invalid_hostnames(!cfg.verify_tls)
.build_rustls()?)
};
let mut b = AsyncSmtpTransport::::builder_dangerous(&cfg.host)
.port(cfg.port)
.timeout(Some(Duration::from_secs(cfg.timeout_secs.max(5))));
b = match cfg.security {
TlsMode::None => b.tls(Tls::None),
TlsMode::StartTls => b.tls(Tls::Required(tls(&cfg.host)?)),
TlsMode::Tls => b.tls(Tls::Wrapper(tls(&cfg.host)?)),
};
b = match cfg.auth {
SmtpAuth::None => b,
SmtpAuth::UserCredentials => {
b.credentials(Credentials::new(login.to_string(), password.to_string()))
}
SmtpAuth::Static => {
b.credentials(Credentials::new(cfg.username.clone(), cfg.password.clone()))
}
};
let transport = b.build();
let env = Envelope::new(Some(from_addr), to).context("invalid envelope")?;
transport
.send_raw(&env, raw)
.await
.map_err(|e| anyhow!("SMTP: {e}"))?;
Ok(())
}
SendMethod::Sendmail => {
let mut cmd = tokio::process::Command::new(&cfg.sendmail_path);
cmd.arg("-i").arg("-f").arg(&from_addr).arg("--");
for t in &to {
cmd.arg(t);
}
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true);
let mut child = cmd.spawn().context("spawn sendmail")?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(raw).await?;
stdin.shutdown().await.ok();
}
let out = tokio::time::timeout(
Duration::from_secs(cfg.timeout_secs.max(5)),
child.wait_with_output(),
)
.await
.map_err(|_| anyhow!("sendmail timeout"))??;
if !out.status.success() {
bail!(
"sendmail failed ({}): {}",
out.status,
String::from_utf8_lossy(&out.stderr).trim()
);
}
Ok(())
}
}
}
/// Check connectivity (and optionally authentication) for the panel "test" button.
pub async fn test(cfg: &SmtpSettings, login: &str, password: &str) -> Result {
match cfg.method {
SendMethod::Sendmail => {
if std::path::Path::new(&cfg.sendmail_path).exists() {
Ok(format!("{} exists", cfg.sendmail_path))
} else {
bail!("{} not found", cfg.sendmail_path)
}
}
SendMethod::Smtp => {
let tls = TlsParameters::builder(cfg.host.clone())
.dangerous_accept_invalid_certs(!cfg.verify_tls)
.dangerous_accept_invalid_hostnames(!cfg.verify_tls)
.build_rustls()?;
let mut b = AsyncSmtpTransport::::builder_dangerous(&cfg.host)
.port(cfg.port)
.timeout(Some(Duration::from_secs(10)));
b = match cfg.security {
TlsMode::None => b.tls(Tls::None),
TlsMode::StartTls => b.tls(Tls::Required(tls)),
TlsMode::Tls => b.tls(Tls::Wrapper(tls)),
};
match cfg.auth {
SmtpAuth::UserCredentials if !login.is_empty() => {
b = b.credentials(Credentials::new(login.into(), password.into()))
}
SmtpAuth::Static => {
b = b.credentials(Credentials::new(cfg.username.clone(), cfg.password.clone()))
}
_ => {}
}
let t: AsyncSmtpTransport = b.build();
let ok = t.test_connection().await.map_err(|e| anyhow!("{e}"))?;
if ok {
Ok(format!("SMTP {}:{} OK", cfg.host, cfg.port))
} else {
bail!("SMTP connection test failed")
}
}
}
}