Skip to main content

shared/
mail.rs

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    /// Sending a disabled template is a silent no-op, so flows that depend on the mail arriving
134    /// must check this rather than trust the `Ok(())` from a send.
135    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}