1use crate::error::{EmailError, Result};
9use serde::{Deserialize, Serialize};
10use std::time::Duration;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum TlsMode {
16 None,
18 #[default]
20 StartTls,
21 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#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct EmailConfig {
44 pub smtp_host: String,
46
47 pub smtp_port: u16,
49
50 pub smtp_username: Option<String>,
52
53 pub smtp_password: Option<String>,
55
56 pub tls_mode: TlsMode,
58
59 pub from_address: String,
61
62 pub from_name: String,
64
65 pub reply_to: Option<String>,
67
68 #[serde(with = "humantime_serde")]
70 pub connection_timeout: Duration,
71
72 #[serde(with = "humantime_serde")]
74 pub send_timeout: Duration,
75
76 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 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 pub fn builder() -> EmailConfigBuilder {
169 EmailConfigBuilder::new()
170 }
171}
172
173#[derive(Debug, Default)]
175pub struct EmailConfigBuilder {
176 config: EmailConfig,
177}
178
179impl EmailConfigBuilder {
180 pub fn new() -> Self {
182 Self::default()
183 }
184
185 pub fn smtp_host(mut self, host: impl Into<String>) -> Self {
187 self.config.smtp_host = host.into();
188 self
189 }
190
191 pub fn smtp_port(mut self, port: u16) -> Self {
193 self.config.smtp_port = port;
194 self
195 }
196
197 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 pub fn tls_mode(mut self, mode: TlsMode) -> Self {
206 self.config.tls_mode = mode;
207 self
208 }
209
210 pub fn from_address(mut self, address: impl Into<String>) -> Self {
212 self.config.from_address = address.into();
213 self
214 }
215
216 pub fn from_name(mut self, name: impl Into<String>) -> Self {
218 self.config.from_name = name.into();
219 self
220 }
221
222 pub fn reply_to(mut self, address: impl Into<String>) -> Self {
224 self.config.reply_to = Some(address.into());
225 self
226 }
227
228 pub fn connection_timeout(mut self, timeout: Duration) -> Self {
230 self.config.connection_timeout = timeout;
231 self
232 }
233
234 pub fn send_timeout(mut self, timeout: Duration) -> Self {
236 self.config.send_timeout = timeout;
237 self
238 }
239
240 pub fn pool_size(mut self, size: usize) -> Self {
242 self.config.pool_size = size;
243 self
244 }
245
246 pub fn build(self) -> EmailConfig {
248 self.config
249 }
250}
251
252mod 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}