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}