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