Skip to main content

cratefield_module_notifications/
mail.rs

1//! The notification email's body: rendered through the template registry
2//! as `notifications/email`, in the venture's [`MailTheme`] via
3//! `cratefield-mail-templates`, so a venture can restyle or reword it like
4//! every other module's mail.
5//!
6//! The module decides everything that is not presentation — the subject
7//! (the catalog's, the category's, or the title), the recipient's language
8//! and direction, both unsubscribe links — and hands them to the template
9//! as [`EmailMail`].
10
11use cratefield_core::{Rendered, Template, TemplateError, TemplateRegistry};
12use cratefield_mail_templates::{self as mt, Dir, MailTheme};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16/// The registry id a venture overrides.
17pub const TEMPLATE_EMAIL: &str = "notifications/email";
18
19/// What the email template is given. Serialized through the registry, so
20/// it is a wire format: adding a field is fine, renaming one breaks
21/// overrides. The module also puts the resolved theme under `theme`.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct EmailMail {
24    /// The venture's name.
25    pub venture: String,
26    /// The subject, already in the recipient's language.
27    pub subject: String,
28    /// The notification's title, its heading.
29    pub title: String,
30    /// The notification's body.
31    pub body: String,
32    /// Where the notification points, if anywhere.
33    pub url: Option<String>,
34    /// The BCP 47 tag the mail is in.
35    pub lang: String,
36    /// `ltr` or `rtl`.
37    pub dir: String,
38    /// Stops this category (the one-click link, or the settings page).
39    pub unsubscribe: String,
40    /// Stops every notification email.
41    pub unsubscribe_all: String,
42}
43
44struct EmailTemplate {
45    theme: Option<MailTheme>,
46}
47
48impl Template for EmailTemplate {
49    fn render(&self, data: &Value, _locale: &str) -> Result<Rendered, TemplateError> {
50        let theme = mt::theme_for_template(self.theme.as_ref(), data);
51        let data: EmailMail =
52            serde_json::from_value(data.clone()).map_err(|_| TemplateError::RenderFailed {
53                id: TEMPLATE_EMAIL.to_owned(),
54                reason: "the mail data is not the shape this template takes".to_owned(),
55            })?;
56        let mut message = mt::Message::new(&data.subject, &data.title)
57            .preheader(&data.body)
58            .lang(&data.lang)
59            .dir(if data.dir == "rtl" {
60                Dir::Rtl
61            } else {
62                Dir::Ltr
63            })
64            .paragraph(&data.body);
65        if let Some(url) = &data.url {
66            message = message.button("Open", url);
67        }
68        Ok(message
69            .footer_link("Stop receiving these", &data.unsubscribe)
70            .footer_link("stop all notification email", &data.unsubscribe_all)
71            .render(&theme)
72            .into())
73    }
74}
75
76/// The module's default template, for `Harness::builder().templates(..)`.
77/// It renders in the theme the module resolves for each mail (the
78/// venture's core `Brand`, with the deployment's `MAIL_THEME` config on
79/// top); use [`themed_templates`] to compose the venture's own theme.
80#[must_use]
81pub fn default_templates() -> Vec<(String, Box<dyn Template>)> {
82    templates(None)
83}
84
85/// The module's template in `theme`, the venture's own style. The
86/// deployment's `MAIL_THEME` config still applies on top.
87#[must_use]
88pub fn themed_templates(theme: &MailTheme) -> Vec<(String, Box<dyn Template>)> {
89    templates(Some(theme))
90}
91
92fn templates(theme: Option<&MailTheme>) -> Vec<(String, Box<dyn Template>)> {
93    vec![(
94        TEMPLATE_EMAIL.to_owned(),
95        Box::new(EmailTemplate {
96            theme: theme.cloned(),
97        }) as Box<dyn Template>,
98    )]
99}
100
101/// Renders through the venture's registry, falling back to the compiled
102/// default when the registry misses (the conformance kit registers
103/// nothing).
104pub(crate) fn render(
105    registry: &TemplateRegistry,
106    data: &Value,
107    locale: &str,
108) -> Result<Rendered, TemplateError> {
109    match registry.render(TEMPLATE_EMAIL, data, locale) {
110        Err(TemplateError::UnknownTemplate { .. }) => {
111            EmailTemplate { theme: None }.render(data, locale)
112        }
113        other => other,
114    }
115}