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;