elif_email/
config.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// Email system configuration
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct EmailConfig {
7    /// Default sender email
8    pub default_from: String,
9    /// Default email provider
10    pub default_provider: String,
11    /// Provider configurations
12    pub providers: HashMap<String, ProviderConfig>,
13    /// Template configuration
14    pub templates: TemplateConfig,
15    /// Tracking configuration
16    pub tracking: GlobalTrackingConfig,
17    /// Attachment configuration
18    pub attachments: AttachmentConfig,
19}
20
21/// Provider-specific configuration
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(tag = "type")]
24pub enum ProviderConfig {
25    #[serde(rename = "smtp")]
26    Smtp(SmtpConfig),
27    #[serde(rename = "sendgrid")]
28    SendGrid(SendGridConfig),
29    #[serde(rename = "mailgun")]
30    Mailgun(MailgunConfig),
31}
32
33/// SMTP authentication method
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub enum SmtpAuthMethod {
36    #[serde(rename = "plain")]
37    Plain,
38    #[serde(rename = "login")]
39    Login,
40    #[serde(rename = "xoauth2")]
41    XOAuth2,
42}
43
44/// SMTP TLS configuration
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46pub enum SmtpTlsConfig {
47    #[serde(rename = "none")]
48    None,
49    #[serde(rename = "tls")]
50    Tls,
51    #[serde(rename = "starttls")]
52    StartTls,
53    #[serde(rename = "starttls_required")]
54    StartTlsRequired,
55}
56
57/// SMTP provider configuration
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct SmtpConfig {
60    /// SMTP server hostname
61    pub host: String,
62    /// SMTP server port
63    pub port: u16,
64    /// Username for authentication
65    pub username: String,
66    /// Password for authentication
67    pub password: String,
68    /// TLS configuration
69    pub tls: SmtpTlsConfig,
70    /// Authentication method
71    pub auth_method: SmtpAuthMethod,
72    /// Connection timeout in seconds
73    pub timeout: Option<u64>,
74    /// Connection pool size
75    pub pool_size: Option<u32>,
76    /// Enable connection keepalive
77    pub keepalive: bool,
78    /// Max retry attempts
79    pub max_retries: u32,
80    /// Retry delay in seconds
81    pub retry_delay: u64,
82    
83    /// Legacy fields for backward compatibility
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub use_tls: Option<bool>,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub use_starttls: Option<bool>,
88}
89
90/// SendGrid provider configuration
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct SendGridConfig {
93    /// SendGrid API key
94    pub api_key: String,
95    /// API endpoint (usually v3/mail/send)
96    pub endpoint: Option<String>,
97    /// Request timeout in seconds
98    pub timeout: Option<u64>,
99}
100
101/// Mailgun provider configuration
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct MailgunConfig {
104    /// Mailgun API key
105    pub api_key: String,
106    /// Mailgun domain
107    pub domain: String,
108    /// API endpoint region (us/eu)
109    pub region: Option<String>,
110    /// Request timeout in seconds
111    pub timeout: Option<u64>,
112}
113
114/// Template system configuration
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct TemplateConfig {
117    /// Templates directory path
118    pub templates_dir: String,
119    /// Layouts directory path
120    pub layouts_dir: String,
121    /// Partials directory path
122    pub partials_dir: String,
123    /// Enable template caching
124    pub enable_cache: bool,
125    /// Template file extension (now supports .html, .tera, .hbs for compatibility)
126    pub template_extension: String,
127    /// Cache size for moka cache (None uses default)
128    pub cache_size: Option<u64>,
129    /// Enable file watching for hot-reloading
130    pub watch_files: bool,
131}
132
133/// Email queue configuration (placeholder for future queue integration)
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct QueueConfig {
136    /// Enable background email queuing
137    pub enabled: bool,
138    /// Queue name for emails
139    pub queue_name: String,
140    /// Max retry attempts
141    pub max_retries: u32,
142    /// Retry delay in seconds
143    pub retry_delay: u64,
144    /// Batch size for processing
145    pub batch_size: usize,
146}
147
148/// Global tracking configuration
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct GlobalTrackingConfig {
151    /// Enable tracking by default
152    pub enabled: bool,
153    /// Tracking base URL
154    pub base_url: Option<String>,
155    /// Tracking pixel endpoint
156    pub pixel_endpoint: String,
157    /// Link redirect endpoint
158    pub link_endpoint: String,
159}
160
161/// Attachment configuration
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct AttachmentConfig {
164    /// Maximum attachment size in bytes (default 25MB)
165    pub max_size: usize,
166    /// Maximum total attachments size per email (default 50MB)
167    pub max_total_size: usize,
168    /// Maximum number of attachments per email
169    pub max_count: usize,
170    /// Allowed MIME types (empty means all allowed)
171    pub allowed_types: Vec<String>,
172    /// Blocked MIME types
173    pub blocked_types: Vec<String>,
174    /// Enable automatic compression for supported types
175    pub auto_compress: bool,
176    /// Compression quality (0-100, only for images)
177    pub compression_quality: Option<u8>,
178}
179
180impl Default for EmailConfig {
181    fn default() -> Self {
182        Self {
183            default_from: "noreply@example.com".to_string(),
184            default_provider: "smtp".to_string(),
185            providers: HashMap::new(),
186            templates: TemplateConfig::default(),
187            tracking: GlobalTrackingConfig::default(),
188            attachments: AttachmentConfig::default(),
189        }
190    }
191}
192
193impl Default for TemplateConfig {
194    fn default() -> Self {
195        Self {
196            templates_dir: "templates/emails".to_string(),
197            layouts_dir: "templates/emails/layouts".to_string(),
198            partials_dir: "templates/emails/partials".to_string(),
199            enable_cache: true,
200            template_extension: ".html".to_string(),
201            cache_size: None,
202            watch_files: false,
203        }
204    }
205}
206
207impl Default for QueueConfig {
208    fn default() -> Self {
209        Self {
210            enabled: true,
211            queue_name: "emails".to_string(),
212            max_retries: 3,
213            retry_delay: 60,
214            batch_size: 10,
215        }
216    }
217}
218
219impl Default for GlobalTrackingConfig {
220    fn default() -> Self {
221        Self {
222            enabled: false,
223            base_url: None,
224            pixel_endpoint: "/email/track/open".to_string(),
225            link_endpoint: "/email/track/click".to_string(),
226        }
227    }
228}
229
230impl Default for AttachmentConfig {
231    fn default() -> Self {
232        Self {
233            max_size: 25 * 1024 * 1024,        // 25MB
234            max_total_size: 50 * 1024 * 1024,  // 50MB
235            max_count: 10,
236            allowed_types: vec![],  // Empty means all allowed
237            blocked_types: vec![
238                "application/x-executable".to_string(),
239                "application/x-dosexec".to_string(),
240                "application/x-msdownload".to_string(),
241            ],
242            auto_compress: false,
243            compression_quality: Some(85),
244        }
245    }
246}
247
248impl Default for SmtpAuthMethod {
249    fn default() -> Self {
250        SmtpAuthMethod::Plain
251    }
252}
253
254impl Default for SmtpTlsConfig {
255    fn default() -> Self {
256        SmtpTlsConfig::StartTls
257    }
258}
259
260impl SmtpConfig {
261    /// Create new SMTP configuration
262    pub fn new(
263        host: impl Into<String>,
264        port: u16,
265        username: impl Into<String>,
266        password: impl Into<String>,
267    ) -> Self {
268        Self {
269            host: host.into(),
270            port,
271            username: username.into(),
272            password: password.into(),
273            tls: SmtpTlsConfig::StartTls,
274            auth_method: SmtpAuthMethod::Plain,
275            timeout: Some(30),
276            pool_size: Some(10),
277            keepalive: true,
278            max_retries: 3,
279            retry_delay: 5,
280            use_tls: None,
281            use_starttls: None,
282        }
283    }
284
285    /// Get effective TLS configuration, handling legacy settings
286    pub fn effective_tls_config(&self) -> SmtpTlsConfig {
287        // Handle legacy configuration - if either field is present, use legacy logic
288        if self.use_tls.is_some() || self.use_starttls.is_some() {
289            let use_tls = self.use_tls.unwrap_or(false);
290            let use_starttls = self.use_starttls.unwrap_or(false);
291            match (use_tls, use_starttls) {
292                (true, false) => SmtpTlsConfig::Tls,
293                (false, true) => SmtpTlsConfig::StartTls,
294                (false, false) => SmtpTlsConfig::None,
295                (true, true) => SmtpTlsConfig::StartTls, // Prefer STARTTLS when both are set
296            }
297        } else {
298            self.tls.clone()
299        }
300    }
301}
302
303impl SendGridConfig {
304    /// Create new SendGrid configuration
305    pub fn new(api_key: impl Into<String>) -> Self {
306        Self {
307            api_key: api_key.into(),
308            endpoint: None,
309            timeout: Some(30),
310        }
311    }
312}
313
314impl MailgunConfig {
315    /// Create new Mailgun configuration
316    pub fn new(api_key: impl Into<String>, domain: impl Into<String>) -> Self {
317        Self {
318            api_key: api_key.into(),
319            domain: domain.into(),
320            region: None,
321            timeout: Some(30),
322        }
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn test_effective_tls_config_new_field() {
332        let mut config = SmtpConfig::new("smtp.gmail.com", 587, "user", "pass");
333        config.tls = SmtpTlsConfig::Tls;
334        
335        assert!(matches!(config.effective_tls_config(), SmtpTlsConfig::Tls));
336    }
337
338    #[test]
339    fn test_effective_tls_config_legacy_both() {
340        let mut config = SmtpConfig::new("smtp.gmail.com", 587, "user", "pass");
341        config.use_tls = Some(true);
342        config.use_starttls = Some(false);
343        
344        assert!(matches!(config.effective_tls_config(), SmtpTlsConfig::Tls));
345    }
346
347    #[test]
348    fn test_effective_tls_config_legacy_only_tls() {
349        let mut config = SmtpConfig::new("smtp.gmail.com", 587, "user", "pass");
350        config.use_tls = Some(true);
351        // use_starttls remains None, should default to false
352        
353        assert!(matches!(config.effective_tls_config(), SmtpTlsConfig::Tls));
354    }
355
356    #[test]
357    fn test_effective_tls_config_legacy_only_starttls() {
358        let mut config = SmtpConfig::new("smtp.gmail.com", 587, "user", "pass");
359        config.use_starttls = Some(true);
360        // use_tls remains None, should default to false
361        
362        assert!(matches!(config.effective_tls_config(), SmtpTlsConfig::StartTls));
363    }
364
365    #[test]
366    fn test_effective_tls_config_legacy_both_false() {
367        let mut config = SmtpConfig::new("smtp.gmail.com", 587, "user", "pass");
368        config.use_tls = Some(false);
369        config.use_starttls = Some(false);
370        
371        assert!(matches!(config.effective_tls_config(), SmtpTlsConfig::None));
372    }
373
374    #[test]
375    fn test_effective_tls_config_legacy_both_true() {
376        let mut config = SmtpConfig::new("smtp.gmail.com", 587, "user", "pass");
377        config.use_tls = Some(true);
378        config.use_starttls = Some(true);
379        
380        // Should prefer STARTTLS when both are true
381        assert!(matches!(config.effective_tls_config(), SmtpTlsConfig::StartTls));
382    }
383
384    #[test]
385    fn test_effective_tls_config_legacy_false_none() {
386        let mut config = SmtpConfig::new("smtp.gmail.com", 587, "user", "pass");
387        config.use_tls = Some(false);
388        // use_starttls remains None, should default to false
389        
390        assert!(matches!(config.effective_tls_config(), SmtpTlsConfig::None));
391    }
392}