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 tls_mode,
51 skip_cert_validation,
52 from_address,
53 from_name,
54 } => {
55 let mut transport =
56 lettre::AsyncSmtpTransport::<lettre::Tokio1Executor>::builder_dangerous(
57 host.as_str(),
58 )
59 .port(*port)
60 .tls(match tls_mode {
61 super::settings::TlsMode::None => {
62 lettre::transport::smtp::client::Tls::None
63 }
64 super::settings::TlsMode::StartTls => {
65 lettre::transport::smtp::client::Tls::Required(
66 lettre::transport::smtp::client::TlsParametersBuilder::new(
67 host.to_string(),
68 )
69 .dangerous_accept_invalid_certs(*skip_cert_validation)
70 .build_rustls()?,
71 )
72 }
73 super::settings::TlsMode::ImplicitTls => {
74 lettre::transport::smtp::client::Tls::Wrapper(
75 lettre::transport::smtp::client::TlsParametersBuilder::new(
76 host.to_string(),
77 )
78 .dangerous_accept_invalid_certs(*skip_cert_validation)
79 .build_rustls()?,
80 )
81 }
82 });
83
84 if let Some(username) = username {
85 transport = transport.credentials(
86 lettre::transport::smtp::authentication::Credentials::new(
87 username.to_string(),
88 password.clone().unwrap_or_default().into(),
89 ),
90 );
91 }
92
93 Transport::Smtp {
94 transport: transport.build(),
95 from_address: from_address.clone(),
96 from_name: from_name.clone(),
97 }
98 }
99 super::settings::MailMode::Sendmail {
100 command,
101 from_address,
102 from_name,
103 } => {
104 let transport =
105 lettre::AsyncSendmailTransport::<lettre::Tokio1Executor>::new_with_command(
106 command,
107 );
108
109 Transport::Sendmail {
110 transport,
111 from_address: from_address.clone(),
112 from_name: from_name.clone(),
113 }
114 }
115 super::settings::MailMode::Filesystem {
116 path,
117 from_address,
118 from_name,
119 } => {
120 let transport = lettre::AsyncFileTransport::<lettre::Tokio1Executor>::new(path);
121
122 Transport::Filesystem {
123 transport,
124 from_address: from_address.clone(),
125 from_name: from_name.clone(),
126 }
127 }
128 };
129
130 Ok((settings, transport))
131 }
132
133 pub async fn template_deliverable(
136 &self,
137 state: &crate::State,
138 identifier: &str,
139 ) -> Result<bool, anyhow::Error> {
140 if matches!(
141 self.settings.get().await?.mail_mode,
142 super::settings::MailMode::None
143 ) {
144 return Ok(false);
145 }
146
147 Ok(self
148 .templates
149 .get_template(identifier)?
150 .get(state)
151 .await?
152 .enabled)
153 }
154
155 pub async fn send_template_foreground(
156 &self,
157 state: &crate::State,
158 identifier: &str,
159 destination: compact_str::CompactString,
160 context: minijinja::Value,
161 ) -> Result<(), anyhow::Error> {
162 let template = self.templates.get_template(identifier)?;
163 let fetched_template = template.get(state).await?;
164
165 if !fetched_template.enabled {
166 tracing::debug!(
167 "email template '{}' is disabled, skipping sending email",
168 identifier
169 );
170 return Ok(());
171 }
172
173 self.send_foreground(
174 destination,
175 fetched_template.subject,
176 fetched_template.content,
177 context,
178 )
179 .await
180 }
181
182 pub async fn send_template(
183 &self,
184 state: &crate::State,
185 identifier: &str,
186 destination: compact_str::CompactString,
187 context: minijinja::Value,
188 ) {
189 let template = match self.templates.get_template(identifier) {
190 Ok(template) => template,
191 Err(err) => {
192 tracing::error!("failed to get email template: {:#?}", err);
193 return;
194 }
195 };
196 let fetched_template = match template.get(state).await {
197 Ok(template) => template,
198 Err(err) => {
199 tracing::error!("failed to get email template content: {:#?}", err);
200 return;
201 }
202 };
203
204 if !fetched_template.enabled {
205 tracing::debug!(
206 "email template '{}' is disabled, skipping sending email",
207 identifier
208 );
209 return;
210 }
211
212 self.send(
213 destination,
214 fetched_template.subject,
215 fetched_template.content,
216 context,
217 )
218 .await
219 }
220
221 pub async fn send_foreground(
222 &self,
223 destination: compact_str::CompactString,
224 subject: impl AsRef<str>,
225 body: impl AsRef<str>,
226 context: minijinja::Value,
227 ) -> Result<(), anyhow::Error> {
228 let (settings, transport) = self.get_transport().await?;
229
230 let mut environment = minijinja::Environment::new();
231 environment.set_auto_escape_callback(|_| minijinja::AutoEscape::Html);
232 environment.add_global("settings", minijinja::Value::from_serialize(&*settings));
233 environment.add_global(
234 "subject",
235 minijinja::Value::from_serialize(subject.as_ref()),
236 );
237 drop(settings);
238
239 let rendered_subject = environment.render_str(subject.as_ref(), context.clone())?;
240 let rendered_body = environment.render_str(body.as_ref(), context)?;
241
242 match transport {
243 Transport::None => {}
244 Transport::Smtp {
245 transport,
246 from_address,
247 from_name,
248 } => {
249 transport
250 .send(
251 lettre::message::Message::builder()
252 .subject(rendered_subject)
253 .to(lettre::message::Mailbox::new(None, destination.parse()?))
254 .from(lettre::message::Mailbox::new(
255 from_name.map(String::from),
256 from_address.parse()?,
257 ))
258 .header(lettre::message::header::ContentType::TEXT_HTML)
259 .body(rendered_body)?,
260 )
261 .await?;
262 }
263 Transport::Sendmail {
264 transport,
265 from_address,
266 from_name,
267 } => {
268 transport
269 .send(
270 lettre::message::Message::builder()
271 .subject(rendered_subject)
272 .to(lettre::message::Mailbox::new(None, destination.parse()?))
273 .from(lettre::message::Mailbox::new(
274 from_name.map(String::from),
275 from_address.parse()?,
276 ))
277 .header(lettre::message::header::ContentType::TEXT_HTML)
278 .body(rendered_body)?,
279 )
280 .await?;
281 }
282 Transport::Filesystem {
283 transport,
284 from_address,
285 from_name,
286 } => {
287 transport
288 .send(
289 lettre::message::Message::builder()
290 .subject(rendered_subject)
291 .to(lettre::message::Mailbox::new(None, destination.parse()?))
292 .from(lettre::message::Mailbox::new(
293 from_name.map(String::from),
294 from_address.parse()?,
295 ))
296 .header(lettre::message::header::ContentType::TEXT_HTML)
297 .body(rendered_body)?,
298 )
299 .await?;
300 }
301 };
302
303 Ok(())
304 }
305
306 pub async fn send(
307 &self,
308 destination: compact_str::CompactString,
309 subject: impl AsRef<str>,
310 body: impl AsRef<str>,
311 context: minijinja::Value,
312 ) {
313 let (settings, transport) = match self.get_transport().await {
314 Ok((settings, transport)) => (settings, transport),
315 Err(err) => {
316 tracing::error!("failed to get mail transport: {:#?}", err);
317 return;
318 }
319 };
320
321 let mut environment = minijinja::Environment::new();
322 environment.set_auto_escape_callback(|_| minijinja::AutoEscape::Html);
323 environment.add_global("settings", minijinja::Value::from_serialize(&*settings));
324 environment.add_global(
325 "subject",
326 minijinja::Value::from_serialize(subject.as_ref()),
327 );
328 drop(settings);
329
330 let rendered_subject = match environment.render_str(subject.as_ref(), &context) {
331 Ok(subject) => subject,
332 Err(err) => {
333 tracing::error!(
334 transport = ?transport,
335 destination = ?destination,
336 "error while rendering email template: {:?}",
337 err
338 );
339
340 return;
341 }
342 };
343 let rendered_body = match environment.render_str(body.as_ref(), context) {
344 Ok(body) => body,
345 Err(err) => {
346 tracing::error!(
347 transport = ?transport,
348 destination = ?destination,
349 "error while rendering email template: {:?}",
350 err
351 );
352
353 return;
354 }
355 };
356
357 tracing::debug!(
358 transport = ?transport,
359 destination = ?destination,
360 "sending email"
361 );
362
363 tokio::spawn(async move {
364 let run = async || -> Result<(), anyhow::Error> {
365 match transport {
366 Transport::None => {}
367 Transport::Smtp {
368 transport,
369 from_address,
370 from_name,
371 } => {
372 transport
373 .send(
374 lettre::message::Message::builder()
375 .subject(rendered_subject)
376 .to(lettre::message::Mailbox::new(None, destination.parse()?))
377 .from(lettre::message::Mailbox::new(
378 from_name.map(String::from),
379 from_address.parse()?,
380 ))
381 .header(lettre::message::header::ContentType::TEXT_HTML)
382 .body(rendered_body)?,
383 )
384 .await?;
385 }
386 Transport::Sendmail {
387 transport,
388 from_address,
389 from_name,
390 } => {
391 transport
392 .send(
393 lettre::message::Message::builder()
394 .subject(rendered_subject)
395 .to(lettre::message::Mailbox::new(None, destination.parse()?))
396 .from(lettre::message::Mailbox::new(
397 from_name.map(String::from),
398 from_address.parse()?,
399 ))
400 .header(lettre::message::header::ContentType::TEXT_HTML)
401 .body(rendered_body)?,
402 )
403 .await?;
404 }
405 Transport::Filesystem {
406 transport,
407 from_address,
408 from_name,
409 } => {
410 transport
411 .send(
412 lettre::message::Message::builder()
413 .subject(rendered_subject)
414 .to(lettre::message::Mailbox::new(None, destination.parse()?))
415 .from(lettre::message::Mailbox::new(
416 from_name.map(String::from),
417 from_address.parse()?,
418 ))
419 .header(lettre::message::header::ContentType::TEXT_HTML)
420 .body(rendered_body)?,
421 )
422 .await?;
423 }
424 }
425
426 Ok(())
427 };
428
429 match run().await {
430 Ok(_) => tracing::debug!("email sent successfully"),
431 Err(err) => tracing::error!("failed to send email: {:?}", err),
432 }
433 });
434 }
435}