shared/
mail.rs

1use crate::settings::SettingsReadGuard;
2use lettre::AsyncTransport;
3use std::sync::Arc;
4
5pub const MAIL_CONNECTION_TEST: &str = include_str!("../mails/connection_test.html");
6pub const MAIL_PASSWORD_RESET: &str = include_str!("../mails/password_reset.html");
7pub const MAIL_ACCOUNT_CREATED: &str = include_str!("../mails/account_created.html");
8
9#[derive(Debug)]
10enum Transport {
11    None,
12    Smtp {
13        transport: lettre::AsyncSmtpTransport<lettre::Tokio1Executor>,
14        from_address: compact_str::CompactString,
15        from_name: Option<compact_str::CompactString>,
16    },
17    Sendmail {
18        transport: lettre::AsyncSendmailTransport<lettre::Tokio1Executor>,
19        from_address: compact_str::CompactString,
20        from_name: Option<compact_str::CompactString>,
21    },
22    Filesystem {
23        transport: lettre::AsyncFileTransport<lettre::Tokio1Executor>,
24        from_address: compact_str::CompactString,
25        from_name: Option<compact_str::CompactString>,
26    },
27}
28
29pub struct Mail {
30    settings: Arc<super::settings::Settings>,
31}
32
33impl Mail {
34    pub fn new(settings: Arc<super::settings::Settings>) -> Self {
35        Self { settings }
36    }
37
38    async fn get_transport(&self) -> Result<(SettingsReadGuard<'_>, Transport), anyhow::Error> {
39        let settings = self.settings.get().await?;
40
41        let transport = match &settings.mail_mode {
42            super::settings::MailMode::None => Transport::None,
43            super::settings::MailMode::Smtp {
44                host,
45                port,
46                username,
47                password,
48                use_tls,
49                from_address,
50                from_name,
51            } => {
52                let mut transport =
53                    lettre::AsyncSmtpTransport::<lettre::Tokio1Executor>::builder_dangerous(
54                        host.as_str(),
55                    )
56                    .port(*port)
57                    .tls(if *use_tls {
58                        lettre::transport::smtp::client::Tls::Required(
59                            lettre::transport::smtp::client::TlsParametersBuilder::new(
60                                host.to_string(),
61                            )
62                            .build_native()
63                            .unwrap(),
64                        )
65                    } else {
66                        lettre::transport::smtp::client::Tls::None
67                    });
68
69                if let Some(username) = username {
70                    transport = transport.credentials(
71                        lettre::transport::smtp::authentication::Credentials::new(
72                            username.to_string(),
73                            password.clone().unwrap_or_default().into(),
74                        ),
75                    );
76                }
77
78                Transport::Smtp {
79                    transport: transport.build(),
80                    from_address: from_address.clone(),
81                    from_name: from_name.clone(),
82                }
83            }
84            super::settings::MailMode::Sendmail {
85                command,
86                from_address,
87                from_name,
88            } => {
89                let transport =
90                    lettre::AsyncSendmailTransport::<lettre::Tokio1Executor>::new_with_command(
91                        command,
92                    );
93
94                Transport::Sendmail {
95                    transport,
96                    from_address: from_address.clone(),
97                    from_name: from_name.clone(),
98                }
99            }
100            super::settings::MailMode::Filesystem {
101                path,
102                from_address,
103                from_name,
104            } => {
105                let transport = lettre::AsyncFileTransport::<lettre::Tokio1Executor>::new(path);
106
107                Transport::Filesystem {
108                    transport,
109                    from_address: from_address.clone(),
110                    from_name: from_name.clone(),
111                }
112            }
113        };
114
115        Ok((settings, transport))
116    }
117
118    pub async fn send(
119        &self,
120        destination: compact_str::CompactString,
121        subject: compact_str::CompactString,
122        body: impl AsRef<str>,
123        context: minijinja::Value,
124    ) {
125        let (settings, transport) = match self.get_transport().await {
126            Ok((settings, transport)) => (settings, transport),
127            Err(err) => {
128                tracing::error!("failed to get mail transport: {:#?}", err);
129                return;
130            }
131        };
132
133        let mut environment = minijinja::Environment::new();
134        environment.set_auto_escape_callback(|_| minijinja::AutoEscape::Html);
135        environment.add_global("settings", minijinja::Value::from_serialize(&*settings));
136        drop(settings);
137
138        let rendered_body = match environment.render_str(body.as_ref(), context) {
139            Ok(body) => body,
140            Err(err) => {
141                tracing::error!(
142                    transport = ?transport,
143                    destination = ?destination,
144                    subject = ?subject,
145                    "error while rendering email template: {:?}",
146                    err
147                );
148
149                return;
150            }
151        };
152
153        tracing::debug!(
154            transport = ?transport,
155            destination = ?destination,
156            subject = ?subject,
157            "sending email"
158        );
159
160        tokio::spawn(async move {
161            let run = async || -> Result<(), anyhow::Error> {
162                match transport {
163                    Transport::None => {}
164                    Transport::Smtp {
165                        transport,
166                        from_address,
167                        from_name,
168                    } => {
169                        transport
170                            .send(
171                                lettre::message::Message::builder()
172                                    .subject(subject)
173                                    .to(lettre::message::Mailbox::new(None, destination.parse()?))
174                                    .from(lettre::message::Mailbox::new(
175                                        from_name.map(String::from),
176                                        from_address.parse()?,
177                                    ))
178                                    .header(lettre::message::header::ContentType::TEXT_HTML)
179                                    .body(rendered_body)?,
180                            )
181                            .await?;
182                    }
183                    Transport::Sendmail {
184                        transport,
185                        from_address,
186                        from_name,
187                    } => {
188                        transport
189                            .send(
190                                lettre::message::Message::builder()
191                                    .subject(subject)
192                                    .to(lettre::message::Mailbox::new(None, destination.parse()?))
193                                    .from(lettre::message::Mailbox::new(
194                                        from_name.map(String::from),
195                                        from_address.parse()?,
196                                    ))
197                                    .header(lettre::message::header::ContentType::TEXT_HTML)
198                                    .body(rendered_body)?,
199                            )
200                            .await?;
201                    }
202                    Transport::Filesystem {
203                        transport,
204                        from_address,
205                        from_name,
206                    } => {
207                        transport
208                            .send(
209                                lettre::message::Message::builder()
210                                    .subject(subject)
211                                    .to(lettre::message::Mailbox::new(None, destination.parse()?))
212                                    .from(lettre::message::Mailbox::new(
213                                        from_name.map(String::from),
214                                        from_address.parse()?,
215                                    ))
216                                    .header(lettre::message::header::ContentType::TEXT_HTML)
217                                    .body(rendered_body)?,
218                            )
219                            .await?;
220                    }
221                }
222
223                Ok(())
224            };
225
226            match run().await {
227                Ok(_) => tracing::debug!("email sent successfully"),
228                Err(err) => tracing::error!("failed to send email: {:?}", err),
229            }
230        });
231    }
232}