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;