shared/extensions/
email_templates.rs1use std::{
2 borrow::Cow,
3 sync::{Arc, RwLock, RwLockReadGuard},
4};
5
6pub struct EmailTemplate {
7 pub identifier: &'static str,
8 pub available_variables: Vec<&'static str>,
9 pub default_content: &'static str,
10}
11
12impl EmailTemplate {
13 pub async fn get_content(
14 &self,
15 state: &crate::State,
16 ) -> Result<Cow<'static, str>, anyhow::Error> {
17 let db_content: Option<String> = state
18 .cache
19 .cached(
20 &format!("email_templates::{}", self.identifier),
21 15,
22 || async {
23 sqlx::query_scalar("SELECT content FROM email_templates WHERE identifier = $1")
24 .bind(self.identifier)
25 .fetch_optional(state.database.read())
26 .await
27 },
28 )
29 .await?;
30
31 Ok(match db_content {
32 Some(content) => Cow::Owned(content),
33 None => Cow::Borrowed(self.default_content),
34 })
35 }
36
37 pub async fn set_content(
38 &self,
39 state: &crate::State,
40 content: &str,
41 ) -> Result<(), anyhow::Error> {
42 sqlx::query(
43 "INSERT INTO email_templates (identifier, content) VALUES ($1, $2)
44 ON CONFLICT (identifier) DO UPDATE SET content = EXCLUDED.content",
45 )
46 .bind(self.identifier)
47 .bind(content)
48 .execute(state.database.write())
49 .await?;
50
51 state
52 .cache
53 .invalidate(&format!("email_templates::{}", self.identifier))
54 .await?;
55
56 Ok(())
57 }
58
59 pub async fn reset_content(&self, state: &crate::State) -> Result<(), anyhow::Error> {
60 sqlx::query("DELETE FROM email_templates WHERE identifier = $1")
61 .bind(self.identifier)
62 .execute(state.database.write())
63 .await?;
64
65 state
66 .cache
67 .invalidate(&format!("email_templates::{}", self.identifier))
68 .await?;
69
70 Ok(())
71 }
72}
73
74pub struct ExtensionEmailTemplateBuilder {
75 pub templates: Vec<EmailTemplate>,
76}
77
78impl Default for ExtensionEmailTemplateBuilder {
79 fn default() -> Self {
80 Self {
81 templates: vec![
82 EmailTemplate {
83 identifier: "account_created",
84 available_variables: vec!["user", "reset_link"],
85 default_content: include_str!("../../mails/account_created.html"),
86 },
87 EmailTemplate {
88 identifier: "password_reset",
89 available_variables: vec!["user", "reset_link"],
90 default_content: include_str!("../../mails/password_reset.html"),
91 },
92 EmailTemplate {
93 identifier: "connection_test",
94 available_variables: vec![],
95 default_content: include_str!("../../mails/connection_test.html"),
96 },
97 ],
98 }
99 }
100}
101
102impl ExtensionEmailTemplateBuilder {
103 pub fn add_template(mut self, template: EmailTemplate) -> Self {
105 if self
106 .templates
107 .iter()
108 .all(|t| t.identifier != template.identifier)
109 {
110 self.templates.push(template);
111 }
112
113 self
114 }
115
116 pub fn mutate_template(
119 mut self,
120 identifier: &'static str,
121 mutation: impl FnOnce(&mut EmailTemplate),
122 ) -> Self {
123 if let Some(template) = self
124 .templates
125 .iter_mut()
126 .find(|t| t.identifier == identifier)
127 {
128 mutation(template);
129 }
130
131 self
132 }
133
134 pub(super) fn finish(mut self) -> Vec<Arc<EmailTemplate>> {
135 for template in &mut self.templates {
136 if !template.available_variables.contains(&"settings") {
137 template.available_variables.push("settings");
138 }
139 }
140
141 self.templates.into_iter().map(Arc::new).collect()
142 }
143}
144
145pub struct EmailTemplateManager {
146 pub(super) templates: RwLock<Vec<Arc<EmailTemplate>>>,
147}
148
149impl Default for EmailTemplateManager {
150 fn default() -> Self {
151 Self {
152 templates: RwLock::new(vec![]),
153 }
154 }
155}
156
157impl EmailTemplateManager {
158 pub fn get_templates(&self) -> RwLockReadGuard<'_, Vec<Arc<EmailTemplate>>> {
159 self.templates.read().unwrap()
160 }
161
162 pub fn get_template(&self, identifier: &str) -> Result<Arc<EmailTemplate>, anyhow::Error> {
163 self.templates
164 .read()
165 .unwrap()
166 .iter()
167 .find(|t| t.identifier == identifier)
168 .cloned()
169 .ok_or_else(|| anyhow::anyhow!("template with identifier '{}' not found", identifier))
170 }
171}