1use anyhow::{anyhow, Result};
9use serde::{Deserialize, Serialize};
10use std::env;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Config {
17 pub target_emails: Vec<String>,
19
20 pub webhook_url: String,
22
23 pub smtp_bind_address: String,
25
26 pub smtp_port: u16,
28
29 pub health_check_bind_address: String,
31
32 pub health_check_port: u16,
34
35 pub header_prefixes: Vec<String>,
38
39 pub webhook_timeout_secs: u64,
41
42 pub webhook_max_retries: u32,
44
45 pub circuit_breaker_threshold: u32,
47
48 pub circuit_breaker_reset_secs: u64,
50}
51
52impl Config {
53 pub fn from_env() -> Result<Self> {
65 let _ = dotenv::dotenv();
67
68 let target_emails_str = match env::var("MAIL_LASER_TARGET_EMAILS") {
71 Ok(val) => val,
72 Err(e) => {
73 let err_msg = "MAIL_LASER_TARGET_EMAILS environment variable must be set";
74 log::error!("{}: {}", err_msg, e);
75 return Err(anyhow!(e).context(err_msg));
76 }
77 };
78
79 let target_emails: Vec<String> = target_emails_str
81 .split(',')
82 .map(|email| email.trim().to_string()) .filter(|email| !email.is_empty()) .collect();
85
86 if target_emails.is_empty() {
88 let err_msg = if target_emails_str.trim().is_empty() {
89 "MAIL_LASER_TARGET_EMAILS cannot be empty"
90 } else {
91 "MAIL_LASER_TARGET_EMAILS must contain at least one valid email after trimming and splitting"
92 };
93 log::error!("{}", err_msg);
94 return Err(anyhow!(err_msg.to_string()));
95 }
96
97 log::info!("Config: Using target_emails: {:?}", target_emails);
98
99 let webhook_url = match env::var("MAIL_LASER_WEBHOOK_URL") {
100 Ok(val) => val,
101 Err(e) => {
102 let err_msg = "MAIL_LASER_WEBHOOK_URL environment variable must be set";
103 log::error!("{}: {}", err_msg, e); return Err(anyhow!(e).context(err_msg));
105 }
106 };
107 log::info!("Config: Using webhook_url: {}", webhook_url);
108
109 let smtp_bind_address = env::var("MAIL_LASER_BIND_ADDRESS")
111 .map(|val| {
112 log::info!("Config: Using smtp_bind_address from env: {}", val);
113 val
114 })
115 .unwrap_or_else(|_| {
116 let default_val = "0.0.0.0".to_string();
117 log::info!("Config: Using default smtp_bind_address: {}", default_val);
118 default_val });
120
121 let smtp_port_str = env::var("MAIL_LASER_PORT").unwrap_or_else(|_| "2525".to_string()); let smtp_port = match smtp_port_str.parse::<u16>() {
123 Ok(port) => port,
124 Err(e) => {
125 let err_msg = format!(
126 "MAIL_LASER_PORT ('{}') must be a valid u16 port number",
127 smtp_port_str
128 );
129 log::error!("{}: {}", err_msg, e); return Err(anyhow!(e).context(err_msg));
131 }
132 };
133 log::info!("Config: Using smtp_port: {}", smtp_port);
134
135 let health_check_bind_address = env::var("MAIL_LASER_HEALTH_BIND_ADDRESS")
136 .map(|val| {
137 log::info!("Config: Using health_check_bind_address from env: {}", val);
138 val
139 })
140 .unwrap_or_else(|_| {
141 let default_val = "0.0.0.0".to_string();
142 log::info!(
143 "Config: Using default health_check_bind_address: {}",
144 default_val
145 );
146 default_val });
148
149 let health_check_port_str =
150 env::var("MAIL_LASER_HEALTH_PORT").unwrap_or_else(|_| "8080".to_string()); let health_check_port = match health_check_port_str.parse::<u16>() {
152 Ok(port) => port,
153 Err(e) => {
154 let err_msg = format!(
155 "MAIL_LASER_HEALTH_PORT ('{}') must be a valid u16 port number",
156 health_check_port_str
157 );
158 log::error!("{}: {}", err_msg, e); return Err(anyhow!(e).context(err_msg));
160 }
161 };
162 log::info!("Config: Using health_check_port: {}", health_check_port);
163
164 let header_prefixes: Vec<String> = env::var("MAIL_LASER_HEADER_PREFIX")
166 .map(|val| {
167 val.split(',')
168 .map(|prefix| prefix.trim().to_string())
169 .filter(|prefix| !prefix.is_empty())
170 .collect()
171 })
172 .unwrap_or_default();
173 log::info!("Config: Using header_prefixes: {:?}", header_prefixes);
174
175 let webhook_timeout_secs: u64 = env::var("MAIL_LASER_WEBHOOK_TIMEOUT")
177 .unwrap_or_else(|_| "30".to_string())
178 .parse()
179 .map_err(|e| anyhow!("MAIL_LASER_WEBHOOK_TIMEOUT must be a valid u64: {}", e))?;
180 log::info!(
181 "Config: Using webhook_timeout_secs: {}",
182 webhook_timeout_secs
183 );
184
185 let webhook_max_retries: u32 = env::var("MAIL_LASER_WEBHOOK_MAX_RETRIES")
186 .unwrap_or_else(|_| "3".to_string())
187 .parse()
188 .map_err(|e| anyhow!("MAIL_LASER_WEBHOOK_MAX_RETRIES must be a valid u32: {}", e))?;
189 log::info!("Config: Using webhook_max_retries: {}", webhook_max_retries);
190
191 let circuit_breaker_threshold: u32 = env::var("MAIL_LASER_CIRCUIT_BREAKER_THRESHOLD")
192 .unwrap_or_else(|_| "5".to_string())
193 .parse()
194 .map_err(|e| {
195 anyhow!(
196 "MAIL_LASER_CIRCUIT_BREAKER_THRESHOLD must be a valid u32: {}",
197 e
198 )
199 })?;
200 log::info!(
201 "Config: Using circuit_breaker_threshold: {}",
202 circuit_breaker_threshold
203 );
204
205 let circuit_breaker_reset_secs: u64 = env::var("MAIL_LASER_CIRCUIT_BREAKER_RESET")
206 .unwrap_or_else(|_| "60".to_string())
207 .parse()
208 .map_err(|e| {
209 anyhow!(
210 "MAIL_LASER_CIRCUIT_BREAKER_RESET must be a valid u64: {}",
211 e
212 )
213 })?;
214 log::info!(
215 "Config: Using circuit_breaker_reset_secs: {}",
216 circuit_breaker_reset_secs
217 );
218
219 Ok(Config {
221 target_emails,
222 webhook_url,
223 smtp_bind_address,
224 smtp_port,
225 health_check_bind_address,
226 health_check_port,
227 header_prefixes,
228 webhook_timeout_secs,
229 webhook_max_retries,
230 circuit_breaker_threshold,
231 circuit_breaker_reset_secs,
232 })
233 }
234}
235
236mod tests;