Skip to main content

modo_email/
config.rs

1use serde::Deserialize;
2
3/// Which delivery backend to use for outgoing email.
4///
5/// Serialized as lowercase strings (`"smtp"`, `"resend"`) in YAML/JSON config.
6#[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq)]
7#[serde(rename_all = "lowercase")]
8pub enum TransportBackend {
9    /// Send via SMTP (default). Requires the `smtp` feature.
10    #[default]
11    Smtp,
12    /// Send via the Resend HTTP API. Requires the `resend` feature.
13    Resend,
14}
15
16/// Top-level email configuration loaded from YAML or environment.
17///
18/// All fields implement `Default`, so partial YAML is valid — only override
19/// what differs from the defaults.
20///
21/// Feature-gated fields (`smtp`, `resend`) are only present when the
22/// corresponding Cargo feature is enabled.
23#[derive(Debug, Clone, Deserialize)]
24#[serde(default)]
25pub struct EmailConfig {
26    /// Which transport backend to use. Defaults to `smtp`.
27    pub transport: TransportBackend,
28    /// Directory that contains `.md` template files. Defaults to `"emails"`.
29    pub templates_path: String,
30    /// Display name used in the `From` header when no per-email sender is set.
31    pub default_from_name: String,
32    /// Email address used in the `From` header when no per-email sender is set.
33    pub default_from_email: String,
34    /// Optional default `Reply-To` address.
35    pub default_reply_to: Option<String>,
36    /// Whether to cache compiled email templates. Defaults to `true`.
37    /// Set to `false` in development for live template reloading.
38    pub cache_templates: bool,
39    /// Maximum number of compiled templates to keep in cache.
40    /// Defaults to `100`. Only used when `cache_templates` is `true`.
41    pub template_cache_size: usize,
42
43    /// SMTP connection settings. Requires the `smtp` feature.
44    #[cfg(feature = "smtp")]
45    pub smtp: SmtpConfig,
46
47    /// Resend API settings. Requires the `resend` feature.
48    #[cfg(feature = "resend")]
49    pub resend: ResendConfig,
50}
51
52impl Default for EmailConfig {
53    fn default() -> Self {
54        Self {
55            transport: TransportBackend::default(),
56            templates_path: "emails".to_string(),
57            default_from_name: String::new(),
58            default_from_email: String::new(),
59            default_reply_to: None,
60            cache_templates: true,
61            template_cache_size: 100,
62            #[cfg(feature = "smtp")]
63            smtp: SmtpConfig::default(),
64            #[cfg(feature = "resend")]
65            resend: ResendConfig::default(),
66        }
67    }
68}
69
70/// TLS mode for SMTP connections.
71#[cfg(feature = "smtp")]
72#[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq)]
73#[serde(rename_all = "snake_case")]
74pub enum SmtpSecurity {
75    /// No TLS — plaintext connection (use only for local dev or trusted networks).
76    None,
77    /// Upgrade a plaintext connection to TLS via the STARTTLS command (port 587).
78    #[default]
79    #[serde(rename = "starttls")]
80    StartTls,
81    /// Connect with TLS from the start — SMTPS (port 465).
82    ImplicitTls,
83}
84
85/// SMTP connection settings. Requires the `smtp` feature.
86#[cfg(feature = "smtp")]
87#[derive(Debug, Clone, Deserialize)]
88#[serde(default)]
89pub struct SmtpConfig {
90    /// SMTP server hostname. Defaults to `"localhost"`.
91    pub host: String,
92    /// SMTP server port. Defaults to `587`.
93    pub port: u16,
94    /// SMTP authentication username.
95    pub username: String,
96    /// SMTP authentication password.
97    pub password: String,
98    /// TLS security mode. Defaults to `StartTls`.
99    pub security: SmtpSecurity,
100}
101
102#[cfg(feature = "smtp")]
103impl Default for SmtpConfig {
104    fn default() -> Self {
105        Self {
106            host: "localhost".to_string(),
107            port: 587,
108            username: String::new(),
109            password: String::new(),
110            security: SmtpSecurity::default(),
111        }
112    }
113}
114
115/// Resend HTTP API settings. Requires the `resend` feature.
116#[cfg(feature = "resend")]
117#[derive(Debug, Clone, Deserialize, Default)]
118#[serde(default)]
119pub struct ResendConfig {
120    /// Resend API key (starts with `re_`).
121    pub api_key: String,
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn default_values() {
130        let config = EmailConfig::default();
131        assert_eq!(config.templates_path, "emails");
132        assert_eq!(config.default_from_name, "");
133        assert_eq!(config.default_from_email, "");
134        assert!(config.default_reply_to.is_none());
135        assert_eq!(config.transport, TransportBackend::Smtp);
136    }
137
138    #[test]
139    fn partial_yaml_deserialization() {
140        let yaml = r#"
141templates_path: "mail"
142default_from_name: "Acme"
143default_from_email: "hi@acme.com"
144"#;
145        let config: EmailConfig = serde_yaml_ng::from_str(yaml).unwrap();
146        assert_eq!(config.templates_path, "mail");
147        assert_eq!(config.default_from_name, "Acme");
148        assert_eq!(config.default_from_email, "hi@acme.com");
149        assert_eq!(config.transport, TransportBackend::Smtp);
150    }
151
152    #[test]
153    fn transport_backend_deserialization() {
154        let yaml = "transport: resend";
155        let config: EmailConfig = serde_yaml_ng::from_str(yaml).unwrap();
156        assert_eq!(config.transport, TransportBackend::Resend);
157    }
158
159    #[cfg(feature = "smtp")]
160    #[test]
161    fn smtp_security_none_deserialization() {
162        let yaml = r#"
163transport: smtp
164smtp:
165  host: "localhost"
166  security: none
167"#;
168        let config: EmailConfig = serde_yaml_ng::from_str(yaml).unwrap();
169        assert_eq!(config.smtp.security, SmtpSecurity::None);
170    }
171
172    #[cfg(feature = "smtp")]
173    #[test]
174    fn smtp_security_starttls_deserialization() {
175        let yaml = r#"
176transport: smtp
177smtp:
178  host: "mail.example.com"
179  security: starttls
180"#;
181        let config: EmailConfig = serde_yaml_ng::from_str(yaml).unwrap();
182        assert_eq!(config.smtp.security, SmtpSecurity::StartTls);
183    }
184
185    #[cfg(feature = "smtp")]
186    #[test]
187    fn smtp_security_implicit_tls_deserialization() {
188        let yaml = r#"
189transport: smtp
190smtp:
191  host: "mail.example.com"
192  port: 465
193  security: implicit_tls
194"#;
195        let config: EmailConfig = serde_yaml_ng::from_str(yaml).unwrap();
196        assert_eq!(config.smtp.security, SmtpSecurity::ImplicitTls);
197        assert_eq!(config.smtp.port, 465);
198    }
199
200    #[cfg(feature = "smtp")]
201    #[test]
202    fn smtp_security_default_is_starttls() {
203        let config = SmtpConfig::default();
204        assert_eq!(config.security, SmtpSecurity::StartTls);
205    }
206
207    #[test]
208    fn cache_config_deserialization() {
209        let yaml = r#"
210cache_templates: false
211template_cache_size: 50
212"#;
213        let config: EmailConfig = serde_yaml_ng::from_str(yaml).unwrap();
214        assert!(!config.cache_templates);
215        assert_eq!(config.template_cache_size, 50);
216    }
217
218    #[test]
219    fn cache_config_defaults() {
220        let config = EmailConfig::default();
221        assert!(config.cache_templates);
222        assert_eq!(config.template_cache_size, 100);
223    }
224}