Skip to main content

shared/extensions/
email_templates.rs

1use 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    /// Add a new email template to the system, this will not override any existing templates, if you want to override an existing template, use `mutate_template` instead
104    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    /// Mutate an existing template, useful for changing the default content, should not extend the variables, as the caller will not be
117    /// aware of the new variables and thus will not be able to use them, if you need to add variables, consider adding a new template instead
118    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}