1use garde::Validate;
2use serde::{Deserialize, Serialize};
3use sqlx::Row;
4use std::{borrow::Cow, sync::Arc};
5use utoipa::ToSchema;
6
7pub struct EmailTemplate {
8 pub identifier: &'static str,
9 pub available_variables: Vec<&'static str>,
10 pub default_subject: &'static str,
11 pub default_content: &'static str,
12 pub default_enabled: bool,
13}
14
15#[derive(ToSchema, Validate, Serialize, Deserialize)]
16pub struct UpdateEmailTemplate {
17 #[garde(length(chars, min = 1))]
18 #[schema(min_length = 1)]
19 #[serde(default, with = "::serde_with::rust::double_option")]
20 pub content: Option<Option<String>>,
21 #[garde(length(chars, min = 1, max = 255))]
22 #[schema(min_length = 1, max_length = 255)]
23 #[serde(default, with = "::serde_with::rust::double_option")]
24 pub subject: Option<Option<String>>,
25 #[garde(skip)]
26 pub enabled: Option<bool>,
27}
28
29pub struct FetchedEmailTemplate {
30 pub identifier: &'static str,
31 pub available_variables: Vec<&'static str>,
32 pub subject: Cow<'static, str>,
33 pub content: Cow<'static, str>,
34 pub enabled: bool,
35}
36
37impl EmailTemplate {
38 pub async fn get(&self, state: &crate::State) -> Result<FetchedEmailTemplate, anyhow::Error> {
39 let db_content: Option<(bool, String, String)> = state
40 .cache
41 .cached(
42 &format!("email_templates::{}", self.identifier),
43 15,
44 || async {
45 let Some(row) = sqlx::query("SELECT enabled, subject, content FROM email_templates WHERE identifier = $1")
46 .bind(self.identifier)
47 .fetch_optional(state.database.read())
48 .await? else {
49 return Ok(None);
50 };
51
52 Ok::<_, anyhow::Error>(Some((
53 row.try_get("enabled")?,
54 row.try_get("subject")?,
55 row.try_get("content")?,
56 )))
57 },
58 )
59 .await?;
60
61 Ok(match db_content {
62 Some((enabled, subject, content)) => FetchedEmailTemplate {
63 identifier: self.identifier,
64 available_variables: self.available_variables.clone(),
65 subject: Cow::Owned(subject),
66 content: Cow::Owned(content),
67 enabled,
68 },
69 None => FetchedEmailTemplate {
70 identifier: self.identifier,
71 available_variables: self.available_variables.clone(),
72 subject: Cow::Borrowed(self.default_subject),
73 content: Cow::Borrowed(self.default_content),
74 enabled: self.default_enabled,
75 },
76 })
77 }
78
79 pub async fn update(
80 &self,
81 state: &crate::State,
82 data: UpdateEmailTemplate,
83 ) -> Result<(), anyhow::Error> {
84 let (subject_set, subject_val) = match data.subject {
85 None => (false, None),
86 Some(inner) => (true, inner),
87 };
88 let (content_set, content_val) = match data.content {
89 None => (false, None),
90 Some(inner) => (true, inner),
91 };
92
93 let insert_subject = subject_val
94 .clone()
95 .unwrap_or_else(|| self.default_subject.to_string());
96 let insert_content = content_val
97 .clone()
98 .unwrap_or_else(|| self.default_content.to_string());
99 let insert_enabled = data.enabled.unwrap_or(self.default_enabled);
100
101 sqlx::query(
102 "INSERT INTO email_templates (identifier, subject, content, enabled)
103 VALUES ($1, $2, $3, $4)
104 ON CONFLICT (identifier) DO UPDATE SET
105 subject = CASE
106 WHEN $5 THEN COALESCE($6, $7)
107 ELSE email_templates.subject
108 END,
109 content = CASE
110 WHEN $8 THEN COALESCE($9, $10)
111 ELSE email_templates.content
112 END,
113 enabled = COALESCE($11, email_templates.enabled)",
114 )
115 .bind(self.identifier)
116 .bind(&insert_subject)
117 .bind(&insert_content)
118 .bind(insert_enabled)
119 .bind(subject_set)
120 .bind(subject_val.as_deref())
121 .bind(self.default_subject)
122 .bind(content_set)
123 .bind(content_val.as_deref())
124 .bind(self.default_content)
125 .bind(data.enabled)
126 .execute(state.database.write())
127 .await?;
128
129 state
130 .cache
131 .invalidate(&format!("email_templates::{}", self.identifier))
132 .await?;
133
134 Ok(())
135 }
136}
137
138pub struct ExtensionEmailTemplateBuilder {
139 pub templates: Vec<EmailTemplate>,
140}
141
142impl Default for ExtensionEmailTemplateBuilder {
143 fn default() -> Self {
144 Self {
145 templates: vec![
146 EmailTemplate {
147 identifier: "account_created",
148 available_variables: vec!["user", "reset_link"],
149 default_subject: "{{ settings.app.name }} - Account Created",
150 default_content: include_str!("../../mails/account_created.html"),
151 default_enabled: true,
152 },
153 EmailTemplate {
154 identifier: "password_reset",
155 available_variables: vec!["user", "reset_link"],
156 default_subject: "{{ settings.app.name }} - Password Reset",
157 default_content: include_str!("../../mails/password_reset.html"),
158 default_enabled: true,
159 },
160 EmailTemplate {
161 identifier: "connection_test",
162 available_variables: vec![],
163 default_subject: "{{ settings.app.name }} - Connection Test",
164 default_content: include_str!("../../mails/connection_test.html"),
165 default_enabled: true,
166 },
167 EmailTemplate {
168 identifier: "added_to_server",
169 available_variables: vec!["server", "server_link"],
170 default_subject: "{{ settings.app.name }} - Added to Server",
171 default_content: include_str!("../../mails/added_to_server.html"),
172 default_enabled: true,
173 },
174 EmailTemplate {
175 identifier: "removed_from_server",
176 available_variables: vec!["server"],
177 default_subject: "{{ settings.app.name }} - Removed from Server",
178 default_content: include_str!("../../mails/removed_from_server.html"),
179 default_enabled: true,
180 },
181 EmailTemplate {
182 identifier: "server_installed",
183 available_variables: vec!["server", "server_link"],
184 default_subject: "{{ settings.app.name }} - Server Installed",
185 default_content: include_str!("../../mails/server_installed.html"),
186 default_enabled: false,
187 },
188 EmailTemplate {
189 identifier: "server_restored",
190 available_variables: vec!["server", "server_link"],
191 default_subject: "{{ settings.app.name }} - Server Restored",
192 default_content: include_str!("../../mails/server_restored.html"),
193 default_enabled: false,
194 },
195 ],
196 }
197 }
198}
199
200impl ExtensionEmailTemplateBuilder {
201 pub fn add_template(mut self, template: EmailTemplate) -> Self {
203 if self
204 .templates
205 .iter()
206 .all(|t| t.identifier != template.identifier)
207 {
208 self.templates.push(template);
209 }
210
211 self
212 }
213
214 pub fn mutate_template(
217 mut self,
218 identifier: &'static str,
219 mutation: impl FnOnce(&mut EmailTemplate),
220 ) -> Self {
221 if let Some(template) = self
222 .templates
223 .iter_mut()
224 .find(|t| t.identifier == identifier)
225 {
226 mutation(template);
227 }
228
229 self
230 }
231
232 pub(super) fn finish(mut self) -> Vec<Arc<EmailTemplate>> {
233 for template in &mut self.templates {
234 if !template.available_variables.contains(&"settings") {
235 template.available_variables.push("settings");
236 }
237 }
238
239 self.templates.into_iter().map(Arc::new).collect()
240 }
241}
242
243pub struct EmailTemplateManager {
244 pub(super) templates: parking_lot::RwLock<Vec<Arc<EmailTemplate>>>,
245}
246
247impl Default for EmailTemplateManager {
248 fn default() -> Self {
249 Self {
250 templates: parking_lot::RwLock::new(vec![]),
251 }
252 }
253}
254
255impl EmailTemplateManager {
256 pub fn get_templates(&self) -> parking_lot::RwLockReadGuard<'_, Vec<Arc<EmailTemplate>>> {
257 self.templates.read()
258 }
259
260 pub fn get_template(&self, identifier: &str) -> Result<Arc<EmailTemplate>, anyhow::Error> {
261 self.templates
262 .read()
263 .iter()
264 .find(|t| t.identifier == identifier)
265 .cloned()
266 .ok_or_else(|| anyhow::anyhow!("template with identifier '{}' not found", identifier))
267 }
268}