Skip to main content

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    /// Header name prefixes to match and forward in the webhook payload.
36    /// (Optional: `MAIL_LASER_HEADER_PREFIX`, comma-separated, Default: empty)
37    pub header_prefixes: Vec<String>,
38}
39
40impl Config {
41    /// Loads configuration settings from environment variables.
42    ///
43    /// Reads variables prefixed with `MAIL_LASER_`. Supports loading from a `.env` file
44    /// if present. Provides default values for bind addresses and ports if not specified.
45    /// Logs the configuration values being used.
46    ///
47    /// # Errors
48    ///
49    /// Returns an `Err` if:
50    /// - Required environment variables (`MAIL_LASER_TARGET_EMAILS`, `MAIL_LASER_WEBHOOK_URL`) are missing or `MAIL_LASER_TARGET_EMAILS` is empty/invalid.
51    /// - Optional port variables (`MAIL_LASER_PORT`, `MAIL_LASER_HEALTH_PORT`) are set but cannot be parsed as `u16`.
52    pub fn from_env() -> Result<Self> {
53        // Attempt to load variables from a .env file, if it exists. Ignore errors.
54        let _ = dotenv::dotenv();
55
56        // --- Required Variables ---
57        // --- Required Variables ---
58        let target_emails_str = match env::var("MAIL_LASER_TARGET_EMAILS") {
59            Ok(val) => val,
60            Err(e) => {
61                let err_msg = "MAIL_LASER_TARGET_EMAILS environment variable must be set";
62                log::error!("{}: {}", err_msg, e);
63                return Err(anyhow!(e).context(err_msg));
64            }
65        };
66
67        // Parse the comma-separated string into a Vec<String>, trimming whitespace
68        let target_emails: Vec<String> = target_emails_str
69            .split(',')
70            .map(|email| email.trim().to_string()) // Trim whitespace from each part
71            .filter(|email| !email.is_empty()) // Remove any empty strings resulting from extra commas or whitespace
72            .collect();
73
74        // Ensure at least one valid email was provided
75        if target_emails.is_empty() {
76            let err_msg = if target_emails_str.trim().is_empty() {
77                "MAIL_LASER_TARGET_EMAILS cannot be empty"
78            } else {
79                "MAIL_LASER_TARGET_EMAILS must contain at least one valid email after trimming and splitting"
80            };
81             log::error!("{}", err_msg);
82             return Err(anyhow!(err_msg.to_string()));
83        }
84
85        log::info!("Config: Using target_emails: {:?}", target_emails);
86
87        let webhook_url = match env::var("MAIL_LASER_WEBHOOK_URL") {
88            Ok(val) => val,
89            Err(e) => {
90                let err_msg = "MAIL_LASER_WEBHOOK_URL environment variable must be set";
91                log::error!("{}: {}", err_msg, e); // Log specific error before returning
92                return Err(anyhow!(e).context(err_msg));
93            }
94        };
95        log::info!("Config: Using webhook_url: {}", webhook_url);
96
97        // --- Optional Variables with Defaults ---
98        let smtp_bind_address = env::var("MAIL_LASER_BIND_ADDRESS")
99            .map(|val| {
100                log::info!("Config: Using smtp_bind_address from env: {}", val);
101                val
102            })
103            .unwrap_or_else(|_| {
104                let default_val = "0.0.0.0".to_string();
105                log::info!("Config: Using default smtp_bind_address: {}", default_val);
106                default_val // Default: Listen on all interfaces
107            });
108
109        let smtp_port_str = env::var("MAIL_LASER_PORT")
110            .unwrap_or_else(|_| "2525".to_string()); // Default SMTP port
111        let smtp_port = match smtp_port_str.parse::<u16>() {
112            Ok(port) => port,
113            Err(e) => {
114                let err_msg = format!("MAIL_LASER_PORT ('{}') must be a valid u16 port number", smtp_port_str);
115                log::error!("{}: {}", err_msg, e); // Log specific error before returning
116                return Err(anyhow!(e).context(err_msg));
117            }
118        };
119        log::info!("Config: Using smtp_port: {}", smtp_port);
120
121        let health_check_bind_address = env::var("MAIL_LASER_HEALTH_BIND_ADDRESS")
122            .map(|val| {
123                log::info!("Config: Using health_check_bind_address from env: {}", val);
124                val
125            })
126            .unwrap_or_else(|_| {
127                let default_val = "0.0.0.0".to_string();
128                log::info!("Config: Using default health_check_bind_address: {}", default_val);
129                default_val // Default: Listen on all interfaces
130            });
131
132        let health_check_port_str = env::var("MAIL_LASER_HEALTH_PORT")
133            .unwrap_or_else(|_| "8080".to_string()); // Default health check port
134        let health_check_port = match health_check_port_str.parse::<u16>() {
135            Ok(port) => port,
136            Err(e) => {
137                let err_msg = format!("MAIL_LASER_HEALTH_PORT ('{}') must be a valid u16 port number", health_check_port_str);
138                log::error!("{}: {}", err_msg, e); // Log specific error before returning
139                return Err(anyhow!(e).context(err_msg));
140            }
141        };
142        log::info!("Config: Using health_check_port: {}", health_check_port);
143
144        // --- Optional: Header Prefixes ---
145        let header_prefixes: Vec<String> = env::var("MAIL_LASER_HEADER_PREFIX")
146            .map(|val| {
147                val.split(',')
148                    .map(|prefix| prefix.trim().to_string())
149                    .filter(|prefix| !prefix.is_empty())
150                    .collect()
151            })
152            .unwrap_or_default();
153        log::info!("Config: Using header_prefixes: {:?}", header_prefixes);
154
155        // Construct the final Config object
156        Ok(Config {
157            target_emails,
158            webhook_url,
159            smtp_bind_address,
160            smtp_port,
161            health_check_bind_address,
162            health_check_port,
163            header_prefixes,
164        })
165    }
166}
167
168// The inline tests module has been moved to src/config/tests.rs
169// and is included via `mod tests;` below.
170
171// Include the tests defined in tests.rs
172mod tests;