mail_laser/config/mod.rs
1//! Manages application configuration loaded from environment variables.
2//!
3//! This module defines the `Config` struct which holds all runtime settings
4//! and provides the `from_env` function to populate this struct. It supports
5//! loading variables from a `.env` file via the `dotenv` crate and provides
6//! default values for optional settings.
7
8use std::env;
9use anyhow::{Result, anyhow};
10use serde::{Serialize, Deserialize};
11
12/// Holds the application's runtime configuration settings.
13///
14/// These settings are typically loaded from environment variables via `from_env`.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Config {
17 /// The list of email addresses MailLaser will accept mail for. (Required: `MAIL_LASER_TARGET_EMAILS`, comma-separated)
18 pub target_emails: Vec<String>,
19
20 /// The URL where the extracted email payload will be sent via POST request. (Required: `MAIL_LASER_WEBHOOK_URL`)
21 pub webhook_url: String,
22
23 /// The IP address the SMTP server should listen on. (Optional: `MAIL_LASER_BIND_ADDRESS`, Default: "0.0.0.0")
24 pub smtp_bind_address: String,
25
26 /// The network port the SMTP server should listen on. (Optional: `MAIL_LASER_PORT`, Default: 2525)
27 pub smtp_port: u16,
28
29 /// The IP address the health check HTTP server should listen on. (Optional: `MAIL_LASER_HEALTH_BIND_ADDRESS`, Default: "0.0.0.0")
30 pub health_check_bind_address: String,
31
32 /// The network port the health check HTTP server should listen on. (Optional: `MAIL_LASER_HEALTH_PORT`, Default: 8080)
33 pub health_check_port: u16,
34}
35
36impl Config {
37 /// Loads configuration settings from environment variables.
38 ///
39 /// Reads variables prefixed with `MAIL_LASER_`. Supports loading from a `.env` file
40 /// if present. Provides default values for bind addresses and ports if not specified.
41 /// Logs the configuration values being used.
42 ///
43 /// # Errors
44 ///
45 /// Returns an `Err` if:
46 /// - Required environment variables (`MAIL_LASER_TARGET_EMAILS`, `MAIL_LASER_WEBHOOK_URL`) are missing or `MAIL_LASER_TARGET_EMAILS` is empty/invalid.
47 /// - Optional port variables (`MAIL_LASER_PORT`, `MAIL_LASER_HEALTH_PORT`) are set but cannot be parsed as `u16`.
48 pub fn from_env() -> Result<Self> {
49 // Attempt to load variables from a .env file, if it exists. Ignore errors.
50 let _ = dotenv::dotenv();
51
52 // --- Required Variables ---
53 // --- Required Variables ---
54 let target_emails_str = match env::var("MAIL_LASER_TARGET_EMAILS") {
55 Ok(val) => val,
56 Err(e) => {
57 let err_msg = "MAIL_LASER_TARGET_EMAILS environment variable must be set";
58 log::error!("{}: {}", err_msg, e);
59 return Err(anyhow!(e).context(err_msg));
60 }
61 };
62
63 // Parse the comma-separated string into a Vec<String>, trimming whitespace
64 let target_emails: Vec<String> = target_emails_str
65 .split(',')
66 .map(|email| email.trim().to_string()) // Trim whitespace from each part
67 .filter(|email| !email.is_empty()) // Remove any empty strings resulting from extra commas or whitespace
68 .collect();
69
70 // Ensure at least one valid email was provided
71 if target_emails.is_empty() {
72 let err_msg = if target_emails_str.trim().is_empty() {
73 "MAIL_LASER_TARGET_EMAILS cannot be empty"
74 } else {
75 "MAIL_LASER_TARGET_EMAILS must contain at least one valid email after trimming and splitting"
76 };
77 log::error!("{}", err_msg);
78 return Err(anyhow!(err_msg.to_string()));
79 }
80
81 log::info!("Config: Using target_emails: {:?}", target_emails);
82
83 let webhook_url = match env::var("MAIL_LASER_WEBHOOK_URL") {
84 Ok(val) => val,
85 Err(e) => {
86 let err_msg = "MAIL_LASER_WEBHOOK_URL environment variable must be set";
87 log::error!("{}: {}", err_msg, e); // Log specific error before returning
88 return Err(anyhow!(e).context(err_msg));
89 }
90 };
91 log::info!("Config: Using webhook_url: {}", webhook_url);
92
93 // --- Optional Variables with Defaults ---
94 let smtp_bind_address = env::var("MAIL_LASER_BIND_ADDRESS")
95 .map(|val| {
96 log::info!("Config: Using smtp_bind_address from env: {}", val);
97 val
98 })
99 .unwrap_or_else(|_| {
100 let default_val = "0.0.0.0".to_string();
101 log::info!("Config: Using default smtp_bind_address: {}", default_val);
102 default_val // Default: Listen on all interfaces
103 });
104
105 let smtp_port_str = env::var("MAIL_LASER_PORT")
106 .unwrap_or_else(|_| "2525".to_string()); // Default SMTP port
107 let smtp_port = match smtp_port_str.parse::<u16>() {
108 Ok(port) => port,
109 Err(e) => {
110 let err_msg = format!("MAIL_LASER_PORT ('{}') must be a valid u16 port number", smtp_port_str);
111 log::error!("{}: {}", err_msg, e); // Log specific error before returning
112 return Err(anyhow!(e).context(err_msg));
113 }
114 };
115 log::info!("Config: Using smtp_port: {}", smtp_port);
116
117 let health_check_bind_address = env::var("MAIL_LASER_HEALTH_BIND_ADDRESS")
118 .map(|val| {
119 log::info!("Config: Using health_check_bind_address from env: {}", val);
120 val
121 })
122 .unwrap_or_else(|_| {
123 let default_val = "0.0.0.0".to_string();
124 log::info!("Config: Using default health_check_bind_address: {}", default_val);
125 default_val // Default: Listen on all interfaces
126 });
127
128 let health_check_port_str = env::var("MAIL_LASER_HEALTH_PORT")
129 .unwrap_or_else(|_| "8080".to_string()); // Default health check port
130 let health_check_port = match health_check_port_str.parse::<u16>() {
131 Ok(port) => port,
132 Err(e) => {
133 let err_msg = format!("MAIL_LASER_HEALTH_PORT ('{}') must be a valid u16 port number", health_check_port_str);
134 log::error!("{}: {}", err_msg, e); // Log specific error before returning
135 return Err(anyhow!(e).context(err_msg));
136 }
137 };
138 log::info!("Config: Using health_check_port: {}", health_check_port);
139
140 // Construct the final Config object
141 Ok(Config {
142 target_emails,
143 webhook_url,
144 smtp_bind_address,
145 smtp_port,
146 health_check_bind_address,
147 health_check_port,
148 })
149 }
150}
151
152// The inline tests module has been moved to src/config/tests.rs
153// and is included via `mod tests;` below.
154
155// Include the tests defined in tests.rs
156mod tests;