146 lines
5.5 KiB
Rust
146 lines
5.5 KiB
Rust
//! 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::<Address>()
|
|
.map_err(|_| anyhow!("invalid recipient address {r}"))?,
|
|
);
|
|
}
|
|
match cfg.method {
|
|
SendMethod::Smtp => {
|
|
let tls = |host: &str| -> Result<TlsParameters> {
|
|
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::<Tokio1Executor>::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<String> {
|
|
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::<Tokio1Executor>::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<Tokio1Executor> = 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")
|
|
}
|
|
}
|
|
}
|
|
}
|