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    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}