1use std::sync::Arc;
4
5use async_trait::async_trait;
6use lettre::message::header::ContentType;
7use lettre::transport::smtp::authentication::Credentials;
8use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
9use parking_lot::Mutex;
10
11use crate::config::MailConfig;
12use crate::Error;
13
14#[async_trait]
15pub trait MailDriver: Send + Sync {
16 async fn send(&self, message: OutgoingMessage) -> Result<(), Error>;
17}
18
19#[derive(Debug, Clone)]
20pub struct OutgoingMessage {
21 pub from: String,
22 pub to: Vec<String>,
23 pub cc: Vec<String>,
24 pub bcc: Vec<String>,
25 pub subject: String,
26 pub html_body: Option<String>,
27 pub text_body: Option<String>,
28}
29
30#[derive(Clone)]
31pub struct MailerHandle {
32 driver: Arc<dyn MailDriver>,
33}
34
35impl MailerHandle {
36 pub fn new(driver: Arc<dyn MailDriver>) -> Self {
37 Self { driver }
38 }
39
40 pub fn null() -> Self {
41 Self {
42 driver: Arc::new(NullMailDriver),
43 }
44 }
45
46 pub fn fake() -> (Self, Arc<Mutex<Vec<OutgoingMessage>>>) {
47 let outbox = Arc::new(Mutex::new(Vec::new()));
48 let driver = FakeDriver {
49 outbox: outbox.clone(),
50 };
51 (
52 Self {
53 driver: Arc::new(driver),
54 },
55 outbox,
56 )
57 }
58
59 pub fn smtp(config: &MailConfig) -> Result<Self, Error> {
60 let mut builder =
61 AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&config.host).port(config.port);
62 if !config.username.is_empty() {
63 builder = builder.credentials(Credentials::new(
64 config.username.clone(),
65 config.password.clone(),
66 ));
67 }
68 let transport = builder.build();
69 Ok(Self {
70 driver: Arc::new(SmtpDriver {
71 transport,
72 default_from: format!("{} <{}>", config.from_name, config.from_address),
73 }),
74 })
75 }
76
77 pub async fn send(&self, message: OutgoingMessage) -> Result<(), Error> {
78 self.driver.send(message).await
79 }
80}
81
82#[async_trait]
84pub trait Mailable: Send + Sync {
85 async fn build(&self) -> Result<OutgoingMessage, Error>;
86}
87
88pub async fn to(
89 addr: impl Into<String>,
90 mailer: &MailerHandle,
91 mailable: impl Mailable,
92) -> Result<(), Error> {
93 let mut msg = mailable.build().await?;
94 msg.to.push(addr.into());
95 mailer.send(msg).await
96}
97
98struct NullMailDriver;
99
100#[async_trait]
101impl MailDriver for NullMailDriver {
102 async fn send(&self, message: OutgoingMessage) -> Result<(), Error> {
103 tracing::debug!(?message, "null mail driver dropped message");
104 Ok(())
105 }
106}
107
108struct FakeDriver {
109 outbox: Arc<Mutex<Vec<OutgoingMessage>>>,
110}
111
112#[async_trait]
113impl MailDriver for FakeDriver {
114 async fn send(&self, message: OutgoingMessage) -> Result<(), Error> {
115 self.outbox.lock().push(message);
116 Ok(())
117 }
118}
119
120struct SmtpDriver {
121 transport: AsyncSmtpTransport<Tokio1Executor>,
122 default_from: String,
123}
124
125#[async_trait]
126impl MailDriver for SmtpDriver {
127 async fn send(&self, message: OutgoingMessage) -> Result<(), Error> {
128 let from = if message.from.is_empty() {
129 self.default_from.clone()
130 } else {
131 message.from.clone()
132 };
133
134 let mut builder = Message::builder().from(
135 from.parse()
136 .map_err(|e: lettre::address::AddressError| Error::Mail(e.to_string()))?,
137 );
138 for to in &message.to {
139 builder = builder.to(to
140 .parse()
141 .map_err(|e: lettre::address::AddressError| Error::Mail(e.to_string()))?);
142 }
143 for cc in &message.cc {
144 builder = builder.cc(cc
145 .parse()
146 .map_err(|e: lettre::address::AddressError| Error::Mail(e.to_string()))?);
147 }
148 for bcc in &message.bcc {
149 builder = builder.bcc(
150 bcc.parse()
151 .map_err(|e: lettre::address::AddressError| Error::Mail(e.to_string()))?,
152 );
153 }
154 let builder = builder.subject(&message.subject);
155
156 let msg = if let Some(html) = &message.html_body {
157 builder
158 .header(ContentType::TEXT_HTML)
159 .body(html.clone())
160 .map_err(|e| Error::Mail(e.to_string()))?
161 } else if let Some(text) = &message.text_body {
162 builder
163 .header(ContentType::TEXT_PLAIN)
164 .body(text.clone())
165 .map_err(|e| Error::Mail(e.to_string()))?
166 } else {
167 return Err(Error::Mail("mail has no body".into()));
168 };
169
170 self.transport
171 .send(msg)
172 .await
173 .map_err(|e| Error::Mail(e.to_string()))?;
174 Ok(())
175 }
176}