1use std::{ffi::OsString, num::NonZeroU16, sync::Arc};
10
11use async_trait::async_trait;
12use lettre::{
13    AsyncTransport, Tokio1Executor,
14    address::Envelope,
15    transport::{
16        sendmail::AsyncSendmailTransport,
17        smtp::{AsyncSmtpTransport, authentication::Credentials},
18    },
19};
20use thiserror::Error;
21
22#[derive(Debug, Clone, Copy)]
24pub enum SmtpMode {
25    Plain,
27    StartTls,
29    Tls,
31}
32
33#[derive(Default, Clone)]
35pub struct Transport {
36    inner: Arc<TransportInner>,
37}
38
39#[derive(Default)]
40enum TransportInner {
41    #[default]
42    Blackhole,
43    Smtp(AsyncSmtpTransport<Tokio1Executor>),
44    Sendmail(AsyncSendmailTransport<Tokio1Executor>),
45}
46
47impl Transport {
48    fn new(inner: TransportInner) -> Self {
49        let inner = Arc::new(inner);
50        Self { inner }
51    }
52
53    #[must_use]
55    pub fn blackhole() -> Self {
56        Self::new(TransportInner::Blackhole)
57    }
58
59    pub fn smtp(
65        mode: SmtpMode,
66        hostname: &str,
67        port: Option<NonZeroU16>,
68        credentials: Option<Credentials>,
69    ) -> Result<Self, lettre::transport::smtp::Error> {
70        let mut t = match mode {
71            SmtpMode::Plain => AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(hostname),
72            SmtpMode::StartTls => AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(hostname)?,
73            SmtpMode::Tls => AsyncSmtpTransport::<Tokio1Executor>::relay(hostname)?,
74        };
75
76        if let Some(credentials) = credentials {
77            t = t.credentials(credentials);
78        }
79
80        if let Some(port) = port {
81            t = t.port(port.into());
82        }
83
84        Ok(Self::new(TransportInner::Smtp(t.build())))
85    }
86
87    #[must_use]
89    pub fn sendmail(command: Option<impl Into<OsString>>) -> Self {
90        let transport = if let Some(command) = command {
91            AsyncSendmailTransport::new_with_command(command)
92        } else {
93            AsyncSendmailTransport::new()
94        };
95        Self::new(TransportInner::Sendmail(transport))
96    }
97}
98
99impl Transport {
100    pub async fn test_connection(&self) -> Result<(), Error> {
107        match self.inner.as_ref() {
108            TransportInner::Smtp(t) => {
109                t.test_connection().await?;
110            }
111            TransportInner::Blackhole | TransportInner::Sendmail(_) => {}
112        }
113
114        Ok(())
115    }
116}
117
118#[derive(Debug, Error)]
119#[error(transparent)]
120pub enum Error {
121    Smtp(#[from] lettre::transport::smtp::Error),
122    Sendmail(#[from] lettre::transport::sendmail::Error),
123}
124
125#[async_trait]
126impl AsyncTransport for Transport {
127    type Ok = ();
128    type Error = Error;
129
130    async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
131        match self.inner.as_ref() {
132            TransportInner::Blackhole => {
133                tracing::warn!(
134                    "An email was supposed to be sent but no email backend is configured"
135                );
136            }
137            TransportInner::Smtp(t) => {
138                t.send_raw(envelope, email).await?;
139            }
140            TransportInner::Sendmail(t) => {
141                t.send_raw(envelope, email).await?;
142            }
143        }
144
145        Ok(())
146    }
147}