Skip to main content

gatekpr_email/
config.rs

1//! Email configuration management
2//!
3//! Supports configuration via environment variables for:
4//! - SMTP settings (host, port, credentials, TLS)
5//! - Sender information (from address, name, reply-to)
6//! - Provider-specific settings (AWS SES, Resend, etc.)
7
8use crate::error::{EmailError, Result};
9use serde::{Deserialize, Serialize};
10use std::time::Duration;
11
12/// TLS mode for SMTP connections
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum TlsMode {
16    /// No TLS (insecure, not recommended)
17    None,
18    /// Upgrade to TLS via STARTTLS
19    #[default]
20    StartTls,
21    /// Require TLS from the start
22    Required,
23}
24
25impl std::str::FromStr for TlsMode {
26    type Err = EmailError;
27
28    fn from_str(s: &str) -> Result<Self> {
29        match s.to_lowercase().as_str() {
30            "none" => Ok(TlsMode::None),
31            "starttls" | "start_tls" => Ok(TlsMode::StartTls),
32            "required" | "tls" => Ok(TlsMode::Required),
33            _ => Err(EmailError::InvalidTls(format!(
34                "Unknown TLS mode: {}. Use 'none', 'starttls', or 'required'",
35                s
36            ))),
37        }
38    }
39}
40
41/// Email configuration
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct EmailConfig {
44    /// SMTP server host
45    pub smtp_host: String,
46
47    /// SMTP server port (typically 587 for STARTTLS, 465 for TLS, 25 for none)
48    pub smtp_port: u16,
49
50    /// SMTP username (often 'apikey' for services like SendGrid)
51    pub smtp_username: Option<String>,
52
53    /// SMTP password or API key
54    pub smtp_password: Option<String>,
55
56    /// TLS mode
57    pub tls_mode: TlsMode,
58
59    /// From email address
60    pub from_address: String,
61
62    /// From display name
63    pub from_name: String,
64
65    /// Reply-to address (optional)
66    pub reply_to: Option<String>,
67
68    /// Connection timeout
69    #[serde(with = "humantime_serde")]
70    pub connection_timeout: Duration,
71
72    /// Send timeout per email
73    #[serde(with = "humantime_serde")]
74    pub send_timeout: Duration,
75
76    /// Connection pool size
77    pub pool_size: usize,
78}
79
80impl Default for EmailConfig {
81    fn default() -> Self {
82        Self {
83            smtp_host: "localhost".to_string(),
84            smtp_port: 587,
85            smtp_username: None,
86            smtp_password: None,
87            tls_mode: TlsMode::StartTls,
88            from_address: "noreply@example.com".to_string(),
89            from_name: "Example App".to_string(),
90            reply_to: None,
91            connection_timeout: Duration::from_secs(10),
92            send_timeout: Duration::from_secs(30),
93            pool_size: 4,
94        }
95    }
96}
97
98impl EmailConfig {
99    /// Create configuration from environment variables
100    ///
101    /// # Environment Variables
102    ///
103    /// Required:
104    /// - `EMAIL_SMTP_HOST` - SMTP server hostname
105    /// - `EMAIL_FROM_ADDRESS` - Sender email address
106    ///
107    /// Optional:
108    /// - `EMAIL_SMTP_PORT` - SMTP port (default: 587)
109    /// - `EMAIL_SMTP_USERNAME` - SMTP username
110    /// - `EMAIL_SMTP_PASSWORD` - SMTP password/API key
111    /// - `EMAIL_SMTP_TLS` - TLS mode: none, starttls, required (default: starttls)
112    /// - `EMAIL_FROM_NAME` - Sender display name (default: "Gatekpr")
113    /// - `EMAIL_REPLY_TO` - Reply-to address
114    /// - `EMAIL_CONNECTION_TIMEOUT` - Connection timeout in seconds (default: 10)
115    /// - `EMAIL_SEND_TIMEOUT` - Send timeout in seconds (default: 30)
116    /// - `EMAIL_POOL_SIZE` - Connection pool size (default: 4)
117    pub fn from_env() -> Result<Self> {
118        let smtp_host = std::env::var("EMAIL_SMTP_HOST")
119            .map_err(|_| EmailError::MissingEnvVar("EMAIL_SMTP_HOST".to_string()))?;
120
121        let from_address = std::env::var("EMAIL_FROM_ADDRESS")
122            .map_err(|_| EmailError::MissingEnvVar("EMAIL_FROM_ADDRESS".to_string()))?;
123
124        let smtp_port = std::env::var("EMAIL_SMTP_PORT")
125            .ok()
126            .and_then(|s| s.parse().ok())
127            .unwrap_or(587);
128
129        let tls_mode = std::env::var("EMAIL_SMTP_TLS")
130            .ok()
131            .map(|s| s.parse::<TlsMode>())
132            .transpose()?
133            .unwrap_or(TlsMode::StartTls);
134
135        let connection_timeout = std::env::var("EMAIL_CONNECTION_TIMEOUT")
136            .ok()
137            .and_then(|s| s.parse().ok())
138            .map(Duration::from_secs)
139            .unwrap_or(Duration::from_secs(10));
140
141        let send_timeout = std::env::var("EMAIL_SEND_TIMEOUT")
142            .ok()
143            .and_then(|s| s.parse().ok())
144            .map(Duration::from_secs)
145            .unwrap_or(Duration::from_secs(30));
146
147        let pool_size = std::env::var("EMAIL_POOL_SIZE")
148            .ok()
149            .and_then(|s| s.parse().ok())
150            .unwrap_or(4);
151
152        Ok(Self {
153            smtp_host,
154            smtp_port,
155            smtp_username: std::env::var("EMAIL_SMTP_USERNAME").ok(),
156            smtp_password: std::env::var("EMAIL_SMTP_PASSWORD").ok(),
157            tls_mode,
158            from_address,
159            from_name: std::env::var("EMAIL_FROM_NAME").unwrap_or_else(|_| "Gatekpr".to_string()),
160            reply_to: std::env::var("EMAIL_REPLY_TO").ok(),
161            connection_timeout,
162            send_timeout,
163            pool_size,
164        })
165    }
166
167    /// Create a builder for custom configuration
168    pub fn builder() -> EmailConfigBuilder {
169        EmailConfigBuilder::new()
170    }
171}
172
173/// Builder for EmailConfig
174#[derive(Debug, Default)]
175pub struct EmailConfigBuilder {
176    config: EmailConfig,
177}
178
179impl EmailConfigBuilder {
180    /// Create a new builder with defaults
181    pub fn new() -> Self {
182        Self::default()
183    }
184
185    /// Set SMTP host
186    pub fn smtp_host(mut self, host: impl Into<String>) -> Self {
187        self.config.smtp_host = host.into();
188        self
189    }
190
191    /// Set SMTP port
192    pub fn smtp_port(mut self, port: u16) -> Self {
193        self.config.smtp_port = port;
194        self
195    }
196
197    /// Set SMTP credentials
198    pub fn credentials(mut self, username: impl Into<String>, password: impl Into<String>) -> Self {
199        self.config.smtp_username = Some(username.into());
200        self.config.smtp_password = Some(password.into());
201        self
202    }
203
204    /// Set TLS mode
205    pub fn tls_mode(mut self, mode: TlsMode) -> Self {
206        self.config.tls_mode = mode;
207        self
208    }
209
210    /// Set from address
211    pub fn from_address(mut self, address: impl Into<String>) -> Self {
212        self.config.from_address = address.into();
213        self
214    }
215
216    /// Set from name
217    pub fn from_name(mut self, name: impl Into<String>) -> Self {
218        self.config.from_name = name.into();
219        self
220    }
221
222    /// Set reply-to address
223    pub fn reply_to(mut self, address: impl Into<String>) -> Self {
224        self.config.reply_to = Some(address.into());
225        self
226    }
227
228    /// Set connection timeout
229    pub fn connection_timeout(mut self, timeout: Duration) -> Self {
230        self.config.connection_timeout = timeout;
231        self
232    }
233
234    /// Set send timeout
235    pub fn send_timeout(mut self, timeout: Duration) -> Self {
236        self.config.send_timeout = timeout;
237        self
238    }
239
240    /// Set connection pool size
241    pub fn pool_size(mut self, size: usize) -> Self {
242        self.config.pool_size = size;
243        self
244    }
245
246    /// Build the configuration
247    pub fn build(self) -> EmailConfig {
248        self.config
249    }
250}
251
252// Support for humantime in serde
253mod humantime_serde {
254    use serde::{Deserialize, Deserializer, Serialize, Serializer};
255    use std::time::Duration;
256
257    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
258    where
259        S: Serializer,
260    {
261        duration.as_secs().serialize(serializer)
262    }
263
264    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
265    where
266        D: Deserializer<'de>,
267    {
268        let secs = u64::deserialize(deserializer)?;
269        Ok(Duration::from_secs(secs))
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn test_tls_mode_from_str() {
279        assert_eq!("none".parse::<TlsMode>().unwrap(), TlsMode::None);
280        assert_eq!("starttls".parse::<TlsMode>().unwrap(), TlsMode::StartTls);
281        assert_eq!("STARTTLS".parse::<TlsMode>().unwrap(), TlsMode::StartTls);
282        assert_eq!("required".parse::<TlsMode>().unwrap(), TlsMode::Required);
283        assert_eq!("tls".parse::<TlsMode>().unwrap(), TlsMode::Required);
284        assert!("invalid".parse::<TlsMode>().is_err());
285    }
286
287    #[test]
288    fn test_config_builder() {
289        let config = EmailConfig::builder()
290            .smtp_host("smtp.example.com")
291            .smtp_port(587)
292            .credentials("user", "pass")
293            .from_address("test@example.com")
294            .from_name("Test App")
295            .tls_mode(TlsMode::StartTls)
296            .build();
297
298        assert_eq!(config.smtp_host, "smtp.example.com");
299        assert_eq!(config.smtp_port, 587);
300        assert_eq!(config.smtp_username, Some("user".to_string()));
301        assert_eq!(config.from_address, "test@example.com");
302    }
303
304    #[test]
305    fn test_default_config() {
306        let config = EmailConfig::default();
307        assert_eq!(config.smtp_port, 587);
308        assert_eq!(config.tls_mode, TlsMode::StartTls);
309        assert_eq!(config.pool_size, 4);
310    }
311}