1use crate::settings::SettingsReadGuard;
2use lettre::AsyncTransport;
3use std::sync::Arc;
4
5#[derive(Debug)]
6enum Transport {
7 None,
8 Smtp {
9 transport: lettre::AsyncSmtpTransport<lettre::Tokio1Executor>,
10 from_address: compact_str::CompactString,
11 from_name: Option<compact_str::CompactString>,
12 },
13 Sendmail {
14 transport: lettre::AsyncSendmailTransport<lettre::Tokio1Executor>,
15 from_address: compact_str::CompactString,
16 from_name: Option<compact_str::CompactString>,
17 },
18 Filesystem {
19 transport: lettre::AsyncFileTransport<lettre::Tokio1Executor>,
20 from_address: compact_str::CompactString,
21 from_name: Option<compact_str::CompactString>,
22 },
23}
24
25pub struct Mail {
26 settings: Arc<super::settings::Settings>,
27 pub templates: Arc<super::extensions::email_templates::EmailTemplateManager>,
28}
29
30impl Mail {
31 pub fn new(settings: Arc<super::settings::Settings>) -> Self {
32 Self {
33 settings,
34 templates: Arc::new(
35 super::extensions::email_templates::EmailTemplateManager::default(),
36 ),
37 }
38 }
39
40 async fn get_transport(&self) -> Result<(SettingsReadGuard<'_>, Transport), anyhow::Error> {
41 let settings = self.settings.get().await?;
42
43 let transport = match &settings.mail_mode {
44 super::settings::MailMode::None => Transport::None,
45 super::settings::MailMode::Smtp {
46 host,
47 port,
48 username,
49 password,
50 use_tls,
51 from_address,
52 from_name,
53 } => {
54 let mut transport =
55 lettre::AsyncSmtpTransport::<lettre::Tokio1Executor>::builder_dangerous(
56 host.as_str(),
57 )
58 .port(*port)
59 .tls(if *use_tls {
60 lettre::transport::smtp::client::Tls::Required(
61 lettre::transport::smtp::client::TlsParametersBuilder::new(
62 host.to_string(),
63 )
64 .build_native()
65 .unwrap(),
66 )
67 } else {
68 lettre::transport::smtp::client::Tls::None
69 });
70
71 if let Some(username) = username {
72 transport = transport.credentials(
73 lettre::transport::smtp::authentication::Credentials::new(
74 username.to_string(),
75 password.clone().unwrap_or_default().into(),
76 ),
77 );
78 }
79
80 Transport::Smtp {
81 transport: transport.build(),
82 from_address: from_address.clone(),
83 from_name: from_name.clone(),
84 }
85 }
86 super::settings::MailMode::Sendmail {
87 command,
88 from_address,
89 from_name,
90 } => {
91 let transport =
92 lettre::AsyncSendmailTransport::<lettre::Tokio1Executor>::new_with_command(
93 command,
94 );
95
96 Transport::Sendmail {
97 transport,
98 from_address: from_address.clone(),
99 from_name: from_name.clone(),
100 }
101 }
102 super::settings::MailMode::Filesystem {
103 path,
104 from_address,
105 from_name,
106 } => {
107 let transport = lettre::AsyncFileTransport::<lettre::Tokio1Executor>::new(path);
108
109 Transport::Filesystem {
110 transport,
111 from_address: from_address.clone(),
112 from_name: from_name.clone(),
113 }
114 }
115 };
116
117 Ok((settings, transport))
118 }
119
120 pub async fn send_foreground(
121 &self,
122 destination: compact_str::CompactString,
123 subject: compact_str::CompactString,
124 body: impl AsRef<str>,
125 context: minijinja::Value,
126 ) -> Result<(), anyhow::Error> {
127 let (settings, transport) = self.get_transport().await?;
128
129 let mut environment = minijinja::Environment::new();
130 environment.set_auto_escape_callback(|_| minijinja::AutoEscape::Html);
131 environment.add_global("settings", minijinja::Value::from_serialize(&*settings));
132 drop(settings);
133
134 let rendered_body = environment.render_str(body.as_ref(), context)?;
135
136 match transport {
137 Transport::None => {}
138 Transport::Smtp {
139 transport,
140 from_address,
141 from_name,
142 } => {
143 transport
144 .send(
145 lettre::message::Message::builder()
146 .subject(subject)
147 .to(lettre::message::Mailbox::new(None, destination.parse()?))
148 .from(lettre::message::Mailbox::new(
149 from_name.map(String::from),
150 from_address.parse()?,
151 ))
152 .header(lettre::message::header::ContentType::TEXT_HTML)
153 .body(rendered_body)?,
154 )
155 .await?;
156 }
157 Transport::Sendmail {
158 transport,
159 from_address,
160 from_name,
161 } => {
162 transport
163 .send(
164 lettre::message::Message::builder()
165 .subject(subject)
166 .to(lettre::message::Mailbox::new(None, destination.parse()?))
167 .from(lettre::message::Mailbox::new(
168 from_name.map(String::from),
169 from_address.parse()?,
170 ))
171 .header(lettre::message::header::ContentType::TEXT_HTML)
172 .body(rendered_body)?,
173 )
174 .await?;
175 }
176 Transport::Filesystem {
177 transport,
178 from_address,
179 from_name,
180 } => {
181 transport
182 .send(
183 lettre::message::Message::builder()
184 .subject(subject)
185 .to(lettre::message::Mailbox::new(None, destination.parse()?))
186 .from(lettre::message::Mailbox::new(
187 from_name.map(String::from),
188 from_address.parse()?,
189 ))
190 .header(lettre::message::header::ContentType::TEXT_HTML)
191 .body(rendered_body)?,
192 )
193 .await?;
194 }
195 };
196
197 Ok(())
198 }
199
200 pub async fn send(
201 &self,
202 destination: compact_str::CompactString,
203 subject: compact_str::CompactString,
204 body: impl AsRef<str>,
205 context: minijinja::Value,
206 ) {
207 let (settings, transport) = match self.get_transport().await {
208 Ok((settings, transport)) => (settings, transport),
209 Err(err) => {
210 tracing::error!("failed to get mail transport: {:#?}", err);
211 return;
212 }
213 };
214
215 let mut environment = minijinja::Environment::new();
216 environment.set_auto_escape_callback(|_| minijinja::AutoEscape::Html);
217 environment.add_global("settings", minijinja::Value::from_serialize(&*settings));
218 drop(settings);
219
220 let rendered_body = match environment.render_str(body.as_ref(), context) {
221 Ok(body) => body,
222 Err(err) => {
223 tracing::error!(
224 transport = ?transport,
225 destination = ?destination,
226 subject = ?subject,
227 "error while rendering email template: {:?}",
228 err
229 );
230
231 return;
232 }
233 };
234
235 tracing::debug!(
236 transport = ?transport,
237 destination = ?destination,
238 subject = ?subject,
239 "sending email"
240 );
241
242 tokio::spawn(async move {
243 let run = async || -> Result<(), anyhow::Error> {
244 match transport {
245 Transport::None => {}
246 Transport::Smtp {
247 transport,
248 from_address,
249 from_name,
250 } => {
251 transport
252 .send(
253 lettre::message::Message::builder()
254 .subject(subject)
255 .to(lettre::message::Mailbox::new(None, destination.parse()?))
256 .from(lettre::message::Mailbox::new(
257 from_name.map(String::from),
258 from_address.parse()?,
259 ))
260 .header(lettre::message::header::ContentType::TEXT_HTML)
261 .body(rendered_body)?,
262 )
263 .await?;
264 }
265 Transport::Sendmail {
266 transport,
267 from_address,
268 from_name,
269 } => {
270 transport
271 .send(
272 lettre::message::Message::builder()
273 .subject(subject)
274 .to(lettre::message::Mailbox::new(None, destination.parse()?))
275 .from(lettre::message::Mailbox::new(
276 from_name.map(String::from),
277 from_address.parse()?,
278 ))
279 .header(lettre::message::header::ContentType::TEXT_HTML)
280 .body(rendered_body)?,
281 )
282 .await?;
283 }
284 Transport::Filesystem {
285 transport,
286 from_address,
287 from_name,
288 } => {
289 transport
290 .send(
291 lettre::message::Message::builder()
292 .subject(subject)
293 .to(lettre::message::Mailbox::new(None, destination.parse()?))
294 .from(lettre::message::Mailbox::new(
295 from_name.map(String::from),
296 from_address.parse()?,
297 ))
298 .header(lettre::message::header::ContentType::TEXT_HTML)
299 .body(rendered_body)?,
300 )
301 .await?;
302 }
303 }
304
305 Ok(())
306 };
307
308 match run().await {
309 Ok(_) => tracing::debug!("email sent successfully"),
310 Err(err) => tracing::error!("failed to send email: {:?}", err),
311 }
312 });
313 }
314}