use std::{ffi::OsString, num::NonZeroU16, sync::Arc};
use async_trait::async_trait;
use lettre::{
address::Envelope,
transport::{
sendmail::AsyncSendmailTransport,
smtp::{authentication::Credentials, AsyncSmtpTransport},
},
AsyncTransport, Tokio1Executor,
};
use thiserror::Error;
#[derive(Debug, Clone, Copy)]
pub enum SmtpMode {
Plain,
StartTls,
Tls,
}
#[derive(Default, Clone)]
pub struct Transport {
inner: Arc<TransportInner>,
}
enum TransportInner {
Blackhole,
Smtp(AsyncSmtpTransport<Tokio1Executor>),
Sendmail(AsyncSendmailTransport<Tokio1Executor>),
}
impl Transport {
fn new(inner: TransportInner) -> Self {
let inner = Arc::new(inner);
Self { inner }
}
#[must_use]
pub fn blackhole() -> Self {
Self::new(TransportInner::Blackhole)
}
pub fn smtp(
mode: SmtpMode,
hostname: &str,
port: Option<NonZeroU16>,
credentials: Option<Credentials>,
) -> Result<Self, lettre::transport::smtp::Error> {
let mut t = match mode {
SmtpMode::Plain => AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(hostname),
SmtpMode::StartTls => AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(hostname)?,
SmtpMode::Tls => AsyncSmtpTransport::<Tokio1Executor>::relay(hostname)?,
};
if let Some(credentials) = credentials {
t = t.credentials(credentials);
}
if let Some(port) = port {
t = t.port(port.into());
}
Ok(Self::new(TransportInner::Smtp(t.build())))
}
#[must_use]
pub fn sendmail(command: Option<impl Into<OsString>>) -> Self {
let transport = if let Some(command) = command {
AsyncSendmailTransport::new_with_command(command)
} else {
AsyncSendmailTransport::new()
};
Self::new(TransportInner::Sendmail(transport))
}
}
impl Transport {
pub async fn test_connection(&self) -> Result<(), Error> {
match self.inner.as_ref() {
TransportInner::Smtp(t) => {
t.test_connection().await?;
}
TransportInner::Blackhole | TransportInner::Sendmail(_) => {}
}
Ok(())
}
}
impl Default for TransportInner {
fn default() -> Self {
Self::Blackhole
}
}
#[derive(Debug, Error)]
#[error(transparent)]
pub enum Error {
Smtp(#[from] lettre::transport::smtp::Error),
Sendmail(#[from] lettre::transport::sendmail::Error),
}
#[async_trait]
impl AsyncTransport for Transport {
type Ok = ();
type Error = Error;
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
match self.inner.as_ref() {
TransportInner::Blackhole => {
tracing::warn!(
"An email was supposed to be sent but no email backend is configured"
);
}
TransportInner::Smtp(t) => {
t.send_raw(envelope, email).await?;
}
TransportInner::Sendmail(t) => {
t.send_raw(envelope, email).await?;
}
};
Ok(())
}
}