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 send_template_foreground(
134 &self,
135 state: &crate::State,
136 identifier: &str,
137 destination: compact_str::CompactString,
138 context: minijinja::Value,
139 ) -> Result<(), anyhow::Error> {
140 let template = self.templates.get_template(identifier)?;
141 let fetched_template = template.get(state).await?;
142
143 if !fetched_template.enabled {
144 tracing::debug!(
145 "email template '{}' is disabled, skipping sending email",
146 identifier
147 );
148 return Ok(());
149 }
150
151 self.send_foreground(
152 destination,
153 fetched_template.subject,
154 fetched_template.content,
155 context,
156 )
157 .await
158 }
159
160 pub async fn send_template(
161 &self,
162 state: &crate::State,
163 identifier: &str,
164 destination: compact_str::CompactString,
165 context: minijinja::Value,
166 ) {
167 let template = match self.templates.get_template(identifier) {
168 Ok(template) => template,
169 Err(err) => {
170 tracing::error!("failed to get email template: {:#?}", err);
171 return;
172 }
173 };
174 let fetched_template = match template.get(state).await {
175 Ok(template) => template,
176 Err(err) => {
177 tracing::error!("failed to get email template content: {:#?}", err);
178 return;
179 }
180 };
181
182 if !fetched_template.enabled {
183 tracing::debug!(
184 "email template '{}' is disabled, skipping sending email",
185 identifier
186 );
187 return;
188 }
189
190 self.send(
191 destination,
192 fetched_template.subject,
193 fetched_template.content,
194 context,
195 )
196 .await
197 }
198
199 pub async fn send_foreground(
200 &self,
201 destination: compact_str::CompactString,
202 subject: impl AsRef<str>,
203 body: impl AsRef<str>,
204 context: minijinja::Value,
205 ) -> Result<(), anyhow::Error> {
206 let (settings, transport) = self.get_transport().await?;
207
208 let mut environment = minijinja::Environment::new();
209 environment.set_auto_escape_callback(|_| minijinja::AutoEscape::Html);
210 environment.add_global("settings", minijinja::Value::from_serialize(&*settings));
211 environment.add_global(
212 "subject",
213 minijinja::Value::from_serialize(subject.as_ref()),
214 );
215 drop(settings);
216
217 let rendered_subject = environment.render_str(subject.as_ref(), context.clone())?;
218 let rendered_body = environment.render_str(body.as_ref(), context)?;
219
220 match transport {
221 Transport::None => {}
222 Transport::Smtp {
223 transport,
224 from_address,
225 from_name,
226 } => {
227 transport
228 .send(
229 lettre::message::Message::builder()
230 .subject(rendered_subject)
231 .to(lettre::message::Mailbox::new(None, destination.parse()?))
232 .from(lettre::message::Mailbox::new(
233 from_name.map(String::from),
234 from_address.parse()?,
235 ))
236 .header(lettre::message::header::ContentType::TEXT_HTML)
237 .body(rendered_body)?,
238 )
239 .await?;
240 }
241 Transport::Sendmail {
242 transport,
243 from_address,
244 from_name,
245 } => {
246 transport
247 .send(
248 lettre::message::Message::builder()
249 .subject(rendered_subject)
250 .to(lettre::message::Mailbox::new(None, destination.parse()?))
251 .from(lettre::message::Mailbox::new(
252 from_name.map(String::from),
253 from_address.parse()?,
254 ))
255 .header(lettre::message::header::ContentType::TEXT_HTML)
256 .body(rendered_body)?,
257 )
258 .await?;
259 }
260 Transport::Filesystem {
261 transport,
262 from_address,
263 from_name,
264 } => {
265 transport
266 .send(
267 lettre::message::Message::builder()
268 .subject(rendered_subject)
269 .to(lettre::message::Mailbox::new(None, destination.parse()?))
270 .from(lettre::message::Mailbox::new(
271 from_name.map(String::from),
272 from_address.parse()?,
273 ))
274 .header(lettre::message::header::ContentType::TEXT_HTML)
275 .body(rendered_body)?,
276 )
277 .await?;
278 }
279 };
280
281 Ok(())
282 }
283
284 pub async fn send(
285 &self,
286 destination: compact_str::CompactString,
287 subject: impl AsRef<str>,
288 body: impl AsRef<str>,
289 context: minijinja::Value,
290 ) {
291 let (settings, transport) = match self.get_transport().await {
292 Ok((settings, transport)) => (settings, transport),
293 Err(err) => {
294 tracing::error!("failed to get mail transport: {:#?}", err);
295 return;
296 }
297 };
298
299 let mut environment = minijinja::Environment::new();
300 environment.set_auto_escape_callback(|_| minijinja::AutoEscape::Html);
301 environment.add_global("settings", minijinja::Value::from_serialize(&*settings));
302 environment.add_global(
303 "subject",
304 minijinja::Value::from_serialize(subject.as_ref()),
305 );
306 drop(settings);
307
308 let rendered_subject = match environment.render_str(subject.as_ref(), &context) {
309 Ok(subject) => subject,
310 Err(err) => {
311 tracing::error!(
312 transport = ?transport,
313 destination = ?destination,
314 "error while rendering email template: {:?}",
315 err
316 );
317
318 return;
319 }
320 };
321 let rendered_body = match environment.render_str(body.as_ref(), context) {
322 Ok(body) => body,
323 Err(err) => {
324 tracing::error!(
325 transport = ?transport,
326 destination = ?destination,
327 "error while rendering email template: {:?}",
328 err
329 );
330
331 return;
332 }
333 };
334
335 tracing::debug!(
336 transport = ?transport,
337 destination = ?destination,
338 "sending email"
339 );
340
341 tokio::spawn(async move {
342 let run = async || -> Result<(), anyhow::Error> {
343 match transport {
344 Transport::None => {}
345 Transport::Smtp {
346 transport,
347 from_address,
348 from_name,
349 } => {
350 transport
351 .send(
352 lettre::message::Message::builder()
353 .subject(rendered_subject)
354 .to(lettre::message::Mailbox::new(None, destination.parse()?))
355 .from(lettre::message::Mailbox::new(
356 from_name.map(String::from),
357 from_address.parse()?,
358 ))
359 .header(lettre::message::header::ContentType::TEXT_HTML)
360 .body(rendered_body)?,
361 )
362 .await?;
363 }
364 Transport::Sendmail {
365 transport,
366 from_address,
367 from_name,
368 } => {
369 transport
370 .send(
371 lettre::message::Message::builder()
372 .subject(rendered_subject)
373 .to(lettre::message::Mailbox::new(None, destination.parse()?))
374 .from(lettre::message::Mailbox::new(
375 from_name.map(String::from),
376 from_address.parse()?,
377 ))
378 .header(lettre::message::header::ContentType::TEXT_HTML)
379 .body(rendered_body)?,
380 )
381 .await?;
382 }
383 Transport::Filesystem {
384 transport,
385 from_address,
386 from_name,
387 } => {
388 transport
389 .send(
390 lettre::message::Message::builder()
391 .subject(rendered_subject)
392 .to(lettre::message::Mailbox::new(None, destination.parse()?))
393 .from(lettre::message::Mailbox::new(
394 from_name.map(String::from),
395 from_address.parse()?,
396 ))
397 .header(lettre::message::header::ContentType::TEXT_HTML)
398 .body(rendered_body)?,
399 )
400 .await?;
401 }
402 }
403
404 Ok(())
405 };
406
407 match run().await {
408 Ok(_) => tracing::debug!("email sent successfully"),
409 Err(err) => tracing::error!("failed to send email: {:?}", err),
410 }
411 });
412 }
413}