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}