use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::env;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub target_emails: Vec<String>,
pub webhook_url: String,
pub smtp_bind_address: String,
pub smtp_port: u16,
pub health_check_bind_address: String,
pub health_check_port: u16,
pub header_prefixes: Vec<String>,
pub webhook_timeout_secs: u64,
pub webhook_max_retries: u32,
pub circuit_breaker_threshold: u32,
pub circuit_breaker_reset_secs: u64,
}
impl Config {
pub fn from_env() -> Result<Self> {
let _ = dotenv::dotenv();
let target_emails_str = match env::var("MAIL_LASER_TARGET_EMAILS") {
Ok(val) => val,
Err(e) => {
let err_msg = "MAIL_LASER_TARGET_EMAILS environment variable must be set";
log::error!("{}: {}", err_msg, e);
return Err(anyhow!(e).context(err_msg));
}
};
let target_emails: Vec<String> = target_emails_str
.split(',')
.map(|email| email.trim().to_string()) .filter(|email| !email.is_empty()) .collect();
if target_emails.is_empty() {
let err_msg = if target_emails_str.trim().is_empty() {
"MAIL_LASER_TARGET_EMAILS cannot be empty"
} else {
"MAIL_LASER_TARGET_EMAILS must contain at least one valid email after trimming and splitting"
};
log::error!("{}", err_msg);
return Err(anyhow!(err_msg.to_string()));
}
log::info!("Config: Using target_emails: {:?}", target_emails);
let webhook_url = match env::var("MAIL_LASER_WEBHOOK_URL") {
Ok(val) => val,
Err(e) => {
let err_msg = "MAIL_LASER_WEBHOOK_URL environment variable must be set";
log::error!("{}: {}", err_msg, e); return Err(anyhow!(e).context(err_msg));
}
};
log::info!("Config: Using webhook_url: {}", webhook_url);
let smtp_bind_address = env::var("MAIL_LASER_BIND_ADDRESS")
.map(|val| {
log::info!("Config: Using smtp_bind_address from env: {}", val);
val
})
.unwrap_or_else(|_| {
let default_val = "0.0.0.0".to_string();
log::info!("Config: Using default smtp_bind_address: {}", default_val);
default_val });
let smtp_port_str = env::var("MAIL_LASER_PORT").unwrap_or_else(|_| "2525".to_string()); let smtp_port = match smtp_port_str.parse::<u16>() {
Ok(port) => port,
Err(e) => {
let err_msg = format!(
"MAIL_LASER_PORT ('{}') must be a valid u16 port number",
smtp_port_str
);
log::error!("{}: {}", err_msg, e); return Err(anyhow!(e).context(err_msg));
}
};
log::info!("Config: Using smtp_port: {}", smtp_port);
let health_check_bind_address = env::var("MAIL_LASER_HEALTH_BIND_ADDRESS")
.map(|val| {
log::info!("Config: Using health_check_bind_address from env: {}", val);
val
})
.unwrap_or_else(|_| {
let default_val = "0.0.0.0".to_string();
log::info!(
"Config: Using default health_check_bind_address: {}",
default_val
);
default_val });
let health_check_port_str =
env::var("MAIL_LASER_HEALTH_PORT").unwrap_or_else(|_| "8080".to_string()); let health_check_port = match health_check_port_str.parse::<u16>() {
Ok(port) => port,
Err(e) => {
let err_msg = format!(
"MAIL_LASER_HEALTH_PORT ('{}') must be a valid u16 port number",
health_check_port_str
);
log::error!("{}: {}", err_msg, e); return Err(anyhow!(e).context(err_msg));
}
};
log::info!("Config: Using health_check_port: {}", health_check_port);
let header_prefixes: Vec<String> = env::var("MAIL_LASER_HEADER_PREFIX")
.map(|val| {
val.split(',')
.map(|prefix| prefix.trim().to_string())
.filter(|prefix| !prefix.is_empty())
.collect()
})
.unwrap_or_default();
log::info!("Config: Using header_prefixes: {:?}", header_prefixes);
let webhook_timeout_secs: u64 = env::var("MAIL_LASER_WEBHOOK_TIMEOUT")
.unwrap_or_else(|_| "30".to_string())
.parse()
.map_err(|e| anyhow!("MAIL_LASER_WEBHOOK_TIMEOUT must be a valid u64: {}", e))?;
log::info!(
"Config: Using webhook_timeout_secs: {}",
webhook_timeout_secs
);
let webhook_max_retries: u32 = env::var("MAIL_LASER_WEBHOOK_MAX_RETRIES")
.unwrap_or_else(|_| "3".to_string())
.parse()
.map_err(|e| anyhow!("MAIL_LASER_WEBHOOK_MAX_RETRIES must be a valid u32: {}", e))?;
log::info!("Config: Using webhook_max_retries: {}", webhook_max_retries);
let circuit_breaker_threshold: u32 = env::var("MAIL_LASER_CIRCUIT_BREAKER_THRESHOLD")
.unwrap_or_else(|_| "5".to_string())
.parse()
.map_err(|e| {
anyhow!(
"MAIL_LASER_CIRCUIT_BREAKER_THRESHOLD must be a valid u32: {}",
e
)
})?;
log::info!(
"Config: Using circuit_breaker_threshold: {}",
circuit_breaker_threshold
);
let circuit_breaker_reset_secs: u64 = env::var("MAIL_LASER_CIRCUIT_BREAKER_RESET")
.unwrap_or_else(|_| "60".to_string())
.parse()
.map_err(|e| {
anyhow!(
"MAIL_LASER_CIRCUIT_BREAKER_RESET must be a valid u64: {}",
e
)
})?;
log::info!(
"Config: Using circuit_breaker_reset_secs: {}",
circuit_breaker_reset_secs
);
Ok(Config {
target_emails,
webhook_url,
smtp_bind_address,
smtp_port,
health_check_bind_address,
health_check_port,
header_prefixes,
webhook_timeout_secs,
webhook_max_retries,
circuit_breaker_threshold,
circuit_breaker_reset_secs,
})
}
}
mod tests;