Skip to main content

braze_sync/resource/
email_template.rs

1//! Email Template domain type. See IMPLEMENTATION.md §6.4.
2//!
3//! `body_plaintext` is always present (`String`, not `Option<String>`); the
4//! empty string is the legitimate value for HTML-only templates. This is a
5//! deliberate decision recorded in §17.
6//!
7//! API verification (2026-04-12):
8//! - `from_address`, `from_display_name`, `reply_to` do NOT exist in Braze API
9//! - `description` is returned by /info but NOT settable via create/update (read-only)
10//! - `should_inline_css` is supported by create/update/info
11//! - Braze field name mapping: `template_name`→`name`, `body`→`body_html`,
12//!   `plaintext_body`→`body_plaintext`
13
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct EmailTemplate {
18    pub name: String,
19    pub subject: String,
20    /// HTML body (may contain Liquid; treated as opaque text in v1.0).
21    pub body_html: String,
22    /// Plaintext fallback. Empty string allowed; field always present.
23    #[serde(default)]
24    pub body_plaintext: String,
25    /// Returned by Braze /info but not settable via create/update.
26    /// Excluded from syncable_eq (same pattern as ContentBlock `state`).
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub description: Option<String>,
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub preheader: Option<String>,
31    /// CSS inline processing toggle. Supported by create/update/info.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub should_inline_css: Option<bool>,
34    #[serde(default)]
35    pub tags: Vec<String>,
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn email_template_yaml_roundtrip() {
44        let t = EmailTemplate {
45            name: "welcome".into(),
46            subject: "Welcome".into(),
47            body_html: "<p>hi</p>".into(),
48            body_plaintext: "hi".into(),
49            description: Some("Welcome email".into()),
50            preheader: Some("Get started".into()),
51            should_inline_css: Some(true),
52            tags: vec!["onboarding".into()],
53        };
54        let yaml = serde_norway::to_string(&t).unwrap();
55        let parsed: EmailTemplate = serde_norway::from_str(&yaml).unwrap();
56        assert_eq!(t, parsed);
57    }
58
59    #[test]
60    fn empty_plaintext_is_valid() {
61        let t = EmailTemplate {
62            name: "html_only".into(),
63            subject: "x".into(),
64            body_html: "<p>x</p>".into(),
65            body_plaintext: String::new(),
66            description: None,
67            preheader: None,
68            should_inline_css: None,
69            tags: vec![],
70        };
71        let yaml = serde_norway::to_string(&t).unwrap();
72        let parsed: EmailTemplate = serde_norway::from_str(&yaml).unwrap();
73        assert_eq!(parsed.body_plaintext, "");
74    }
75}