scufflecloud_core_emails/
lib.rs1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
9#![cfg_attr(docsrs, feature(doc_auto_cfg))]
10#![deny(unsafe_code)]
12#![deny(unreachable_pub)]
13#![deny(clippy::mod_module_files)]
14
15use chrono::Datelike;
16use sailfish::{TemplateOnce, TemplateSimple};
17
18pub struct Email {
19 pub to_address: String,
20 pub from_address: String,
21 pub subject: String,
22 pub text: String,
23 pub html: String,
24}
25
26#[derive(sailfish::TemplateSimple)]
27#[template(path = "emails/register_with_email/subject.stpl")]
28struct RegisterWithEmailSubjectTemplate;
29
30#[derive(sailfish::Template)]
31#[template(path = "emails/register_with_email/text.stpl")]
32struct RegisterWithEmailTextTemplate {
33 url: String,
34 timeout_minutes: u32,
35 copyright_year: u32,
36}
37
38#[derive(sailfish::Template)]
39#[template(path = "emails/register_with_email/html.stpl")]
40struct RegisterWithEmailHtmlTemplate {
41 url: String,
42 timeout_minutes: u32,
43 copyright_year: u32,
44}
45
46pub fn register_with_email_email(
47 from_address: String,
48 to_address: String,
49 dashboard_origin: &url::Url,
50 code: String,
51 timeout: std::time::Duration,
52) -> Result<Email, sailfish::RenderError> {
53 let url = dashboard_origin
54 .join(&format!("/register/confirm?code={code}"))
55 .unwrap()
56 .to_string();
57
58 let timeout_minutes = timeout.as_secs() as u32 / 60;
59 let copyright_year = chrono::Utc::now().year() as u32;
60
61 let subject = RegisterWithEmailSubjectTemplate.render_once()?;
62 let text = RegisterWithEmailTextTemplate {
63 url: url.clone(),
64 timeout_minutes,
65 copyright_year,
66 }
67 .render_once()?;
68 let html = RegisterWithEmailHtmlTemplate {
69 url,
70 timeout_minutes,
71 copyright_year,
72 }
73 .render_once()?;
74
75 Ok(Email {
76 to_address,
77 from_address,
78 subject,
79 text,
80 html,
81 })
82}
83
84#[derive(sailfish::TemplateSimple)]
85#[template(path = "emails/add_new_email/subject.stpl")]
86struct AddNewEmailSubjectTemplate;
87
88#[derive(sailfish::Template)]
89#[template(path = "emails/add_new_email/text.stpl")]
90struct AddNewEmailTextTemplate {
91 url: String,
92 timeout_minutes: u32,
93 copyright_year: u32,
94}
95
96#[derive(sailfish::Template)]
97#[template(path = "emails/add_new_email/html.stpl")]
98struct AddNewEmailHtmlTemplate {
99 url: String,
100 timeout_minutes: u32,
101 copyright_year: u32,
102}
103
104pub fn add_new_email_email(
105 from_address: String,
106 to_address: String,
107 dashboard_origin: &url::Url,
108 code: String,
109 timeout: std::time::Duration,
110) -> Result<Email, sailfish::RenderError> {
111 let url = dashboard_origin
112 .join(&format!("/settings/emails/confirm?code={code}"))
113 .unwrap()
114 .to_string();
115 let timeout_minutes = timeout.as_secs() as u32 / 60;
116 let copyright_year = chrono::Utc::now().year() as u32;
117
118 let subject = AddNewEmailSubjectTemplate.render_once()?;
119 let text = AddNewEmailTextTemplate {
120 url: url.clone(),
121 timeout_minutes,
122 copyright_year,
123 }
124 .render_once()?;
125 let html = AddNewEmailHtmlTemplate {
126 url,
127 timeout_minutes,
128 copyright_year,
129 }
130 .render_once()?;
131
132 Ok(Email {
133 to_address,
134 from_address,
135 subject,
136 text,
137 html,
138 })
139}
140
141#[derive(sailfish::TemplateSimple)]
142#[template(path = "emails/magic_link/subject.stpl")]
143struct MagicLinkSubjectTemplate;
144
145#[derive(sailfish::Template)]
146#[template(path = "emails/magic_link/text.stpl")]
147struct MagicLinkTextTemplate {
148 url: String,
149 timeout_minutes: u32,
150 copyright_year: u32,
151}
152
153#[derive(sailfish::Template)]
154#[template(path = "emails/magic_link/html.stpl")]
155struct MagicLinkHtmlTemplate {
156 url: String,
157 timeout_minutes: u32,
158 copyright_year: u32,
159}
160
161pub fn magic_link_email(
162 from_address: String,
163 to_address: String,
164 dashboard_origin: &url::Url,
165 code: String,
166 timeout: std::time::Duration,
167) -> Result<Email, sailfish::RenderError> {
168 let url = dashboard_origin
169 .join(&format!("/login/magic-link?code={code}"))
170 .unwrap()
171 .to_string();
172 let timeout_minutes = timeout.as_secs() as u32 / 60;
173 let copyright_year = chrono::Utc::now().year() as u32;
174
175 let subject = MagicLinkSubjectTemplate.render_once()?;
176 let text = MagicLinkTextTemplate {
177 url: url.clone(),
178 timeout_minutes,
179 copyright_year,
180 }
181 .render_once()?;
182 let html = MagicLinkHtmlTemplate {
183 url,
184 timeout_minutes,
185 copyright_year,
186 }
187 .render_once()?;
188
189 Ok(Email {
190 to_address,
191 from_address,
192 subject,
193 text,
194 html,
195 })
196}
197
198#[derive(sailfish::TemplateSimple)]
199#[template(path = "emails/new_device/subject.stpl")]
200struct NewDeviceSubjectTemplate;
201
202#[derive(Clone, Debug, Default)]
203pub struct GeoInfo {
204 pub city: Option<String>,
205 pub country: Option<String>,
206}
207
208impl GeoInfo {
209 fn is_empty(&self) -> bool {
210 self.city.is_none() && self.country.is_none()
211 }
212}
213
214impl From<maxminddb::geoip2::City<'_>> for GeoInfo {
215 fn from(value: maxminddb::geoip2::City) -> GeoInfo {
216 let city = value
217 .city
218 .and_then(|c| c.names)
219 .and_then(|names| names.get("en").map(|s| s.to_string()));
220 let country = value
221 .country
222 .and_then(|c| c.names)
223 .and_then(|names| names.get("en").map(|s| s.to_string()));
224
225 GeoInfo { city, country }
226 }
227}
228
229#[derive(sailfish::Template)]
230#[template(path = "emails/new_device/text.stpl")]
231struct NewDeviceTextTemplate {
232 activity_url: String,
233 ip_address: String,
234 geo_info: GeoInfo,
235 copyright_year: u32,
236}
237
238#[derive(sailfish::Template)]
239#[template(path = "emails/new_device/html.stpl")]
240struct NewDeviceHtmlTemplate {
241 activity_url: String,
242 ip_address: String,
243 geo_info: GeoInfo,
244 copyright_year: u32,
245}
246
247pub fn new_device_email(
248 from_address: String,
249 to_address: String,
250 dashboard_origin: &url::Url,
251 ip_addr: std::net::IpAddr,
252 geo_info: GeoInfo,
253) -> Result<Email, sailfish::RenderError> {
254 let ip_address = ip_addr.to_string();
255 let activity_url = dashboard_origin.join("/activity").unwrap().to_string();
257 let copyright_year = chrono::Utc::now().year() as u32;
258
259 let subject = NewDeviceSubjectTemplate.render_once()?;
260
261 let text = NewDeviceTextTemplate {
262 ip_address: ip_address.clone(),
263 geo_info: geo_info.clone(),
264 activity_url: activity_url.clone(),
265 copyright_year,
266 }
267 .render_once()?;
268
269 let html = NewDeviceHtmlTemplate {
270 ip_address,
271 geo_info,
272 activity_url,
273 copyright_year,
274 }
275 .render_once()?;
276
277 Ok(Email {
278 to_address,
279 from_address,
280 subject,
281 text,
282 html,
283 })
284}