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