rusmes_cli/commands/
check_config.rs1use anyhow::{Context, Result};
4use rusmes_config::ServerConfig;
5use std::path::Path;
6
7pub fn run(config_path: &str) -> Result<()> {
9 println!("Checking configuration: {}", config_path);
10
11 let path = Path::new(config_path);
13 if !path.exists() {
14 anyhow::bail!("Configuration file not found: {}", config_path);
15 }
16
17 let config = ServerConfig::from_file(config_path).context("Failed to load configuration")?;
19
20 println!("Configuration loaded successfully");
21 println!();
22
23 println!("Basic settings:");
25 println!(" Domain: {}", config.domain);
26 println!(" Postmaster: {}", config.postmaster);
27
28 config
30 .postmaster_address()
31 .context("Invalid postmaster address")?;
32 println!(" Postmaster address: valid");
33
34 println!();
36 println!("SMTP server:");
37 println!(" Host: {}", config.smtp.host);
38 println!(" Port: {}", config.smtp.port);
39 println!(" Max message size: {}", config.smtp.max_message_size);
40
41 let max_size = config
42 .smtp
43 .max_message_size_bytes()
44 .context("Invalid max_message_size format")?;
45 println!(" Max message size (bytes): {}", max_size);
46
47 if let Some(tls_port) = config.smtp.tls_port {
48 println!(" TLS port: {}", tls_port);
49 }
50
51 if let Some(rate_limit) = &config.smtp.rate_limit {
52 println!(
53 " Rate limit: {} msgs/hour",
54 rate_limit.max_messages_per_hour
55 );
56 let window = rate_limit
57 .window_duration_seconds()
58 .context("Invalid rate_limit window_duration")?;
59 println!(" Rate limit window: {}s", window);
60 }
61
62 if let Some(imap) = &config.imap {
64 println!();
65 println!("IMAP server:");
66 println!(" Host: {}", imap.host);
67 println!(" Port: {}", imap.port);
68 if let Some(tls_port) = imap.tls_port {
69 println!(" TLS port: {}", tls_port);
70 }
71 }
72
73 if let Some(jmap) = &config.jmap {
75 println!();
76 println!("JMAP server:");
77 println!(" Host: {}", jmap.host);
78 println!(" Port: {}", jmap.port);
79 println!(" Base URL: {}", jmap.base_url);
80 }
81
82 println!();
84 println!("Storage:");
85 match &config.storage {
86 rusmes_config::StorageConfig::Filesystem { path } => {
87 println!(" Backend: filesystem");
88 println!(" Path: {}", path);
89 }
90 rusmes_config::StorageConfig::Postgres { connection_string } => {
91 println!(" Backend: postgres");
92 println!(" Connection: {}", connection_string);
93 }
94 rusmes_config::StorageConfig::AmateRS {
95 endpoints,
96 replication_factor,
97 } => {
98 println!(" Backend: AmateRS");
99 println!(" Endpoints: {:?}", endpoints);
100 println!(" Replication factor: {}", replication_factor);
101 }
102 }
103
104 if let Some(auth) = &config.auth {
106 println!();
107 println!("Authentication:");
108 match auth {
109 rusmes_config::AuthConfig::File {
110 config: file_config,
111 } => {
112 println!(" Backend: file");
113 println!(" Path: {}", file_config.path);
114 }
115 rusmes_config::AuthConfig::Ldap {
116 config: ldap_config,
117 } => {
118 println!(" Backend: LDAP");
119 println!(" URL: {}", ldap_config.url);
120 println!(" Base DN: {}", ldap_config.base_dn);
121 }
122 rusmes_config::AuthConfig::Sql { config: sql_config } => {
123 println!(" Backend: SQL");
124 println!(" Connection: {}", sql_config.connection_string);
125 }
126 rusmes_config::AuthConfig::OAuth2 {
127 config: oauth_config,
128 } => {
129 println!(" Backend: OAuth2");
130 println!(" Token URL: {}", oauth_config.token_url);
131 }
132 }
133 }
134
135 if let Some(logging) = &config.logging {
137 println!();
138 println!("Logging:");
139 println!(" Level: {}", logging.level);
140 println!(" Format: {}", logging.format);
141 println!(" Output: {}", logging.output);
142
143 logging.validate_level().context("Invalid log level")?;
144 logging.validate_format().context("Invalid log format")?;
145
146 if let Some(file) = &logging.file {
147 println!(" File path: {}", file.path);
148 println!(" Max size: {}", file.max_size);
149 println!(" Max backups: {}", file.max_backups);
150 println!(" Compress: {}", file.compress);
151
152 let max_size = file
153 .max_size_bytes()
154 .context("Invalid log file max_size format")?;
155 println!(" Max size (bytes): {}", max_size);
156 }
157 }
158
159 if let Some(queue) = &config.queue {
161 println!();
162 println!("Queue:");
163 println!(" Initial delay: {}", queue.initial_delay);
164 println!(" Max delay: {}", queue.max_delay);
165 println!(" Backoff multiplier: {}", queue.backoff_multiplier);
166 println!(" Max attempts: {}", queue.max_attempts);
167 println!(" Worker threads: {}", queue.worker_threads);
168 println!(" Batch size: {}", queue.batch_size);
169
170 let initial_delay = queue
171 .initial_delay_seconds()
172 .context("Invalid queue initial_delay format")?;
173 let max_delay = queue
174 .max_delay_seconds()
175 .context("Invalid queue max_delay format")?;
176 println!(" Initial delay (seconds): {}", initial_delay);
177 println!(" Max delay (seconds): {}", max_delay);
178
179 queue
180 .validate_backoff_multiplier()
181 .context("Invalid backoff_multiplier")?;
182 queue
183 .validate_worker_threads()
184 .context("Invalid worker_threads")?;
185 }
186
187 if let Some(security) = &config.security {
189 println!();
190 println!("Security:");
191 println!(" Relay networks: {:?}", security.relay_networks);
192 println!(" Blocked IPs: {:?}", security.blocked_ips);
193 println!(
194 " Check recipient exists: {}",
195 security.check_recipient_exists
196 );
197 println!(
198 " Reject unknown recipients: {}",
199 security.reject_unknown_recipients
200 );
201
202 security
203 .validate_relay_networks()
204 .context("Invalid relay_networks")?;
205 security
206 .validate_blocked_ips()
207 .context("Invalid blocked_ips")?;
208 }
209
210 if let Some(domains) = &config.domains {
212 println!();
213 println!("Domains:");
214 println!(" Local domains: {:?}", domains.local_domains);
215
216 domains
217 .validate_local_domains()
218 .context("Invalid local_domains")?;
219
220 if !domains.aliases.is_empty() {
221 println!(" Aliases: {} configured", domains.aliases.len());
222 domains.validate_aliases().context("Invalid aliases")?;
223 }
224 }
225
226 if let Some(metrics) = &config.metrics {
228 println!();
229 println!("Metrics:");
230 println!(" Enabled: {}", metrics.enabled);
231 println!(" Bind address: {}", metrics.bind_address);
232 println!(" Path: {}", metrics.path);
233
234 metrics
235 .validate_bind_address()
236 .context("Invalid metrics bind_address")?;
237 metrics.validate_path().context("Invalid metrics path")?;
238 }
239
240 println!();
242 println!("Processors:");
243 println!(" Total processors: {}", config.processors.len());
244 for processor in &config.processors {
245 println!(" - {} (state: {})", processor.name, processor.state);
246 println!(" Mailets: {}", processor.mailets.len());
247 }
248
249 println!();
251 println!("Configuration is valid!");
252
253 Ok(())
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use std::io::Write;
260 use tempfile::Builder;
261
262 #[test]
263 fn test_valid_minimal_config() {
264 let config = r#"
265domain = "example.com"
266postmaster = "postmaster@example.com"
267
268[smtp]
269host = "0.0.0.0"
270port = 25
271max_message_size = "50MB"
272
273[storage]
274backend = "filesystem"
275path = "./data/mailboxes"
276
277[[processors]]
278name = "root"
279state = "root"
280mailets = []
281"#;
282
283 let mut file = Builder::new().suffix(".toml").tempfile().unwrap();
284 file.write_all(config.as_bytes()).unwrap();
285 let path = file.path().to_str().unwrap().to_string();
286
287 let result = run(&path);
288 assert!(result.is_ok());
289 }
290
291 #[test]
292 fn test_missing_config_file() {
293 let result = run("/nonexistent/path/to/config.toml");
294 assert!(result.is_err());
295 }
296
297 #[test]
298 fn test_config_with_all_backends() {
299 let config = r#"
300domain = "example.com"
301postmaster = "postmaster@example.com"
302
303[smtp]
304host = "0.0.0.0"
305port = 25
306max_message_size = "50MB"
307
308[storage]
309backend = "postgres"
310connection_string = "postgresql://localhost/rusmes"
311
312[auth]
313backend = "file"
314path = "./data/users.db"
315
316[[processors]]
317name = "root"
318state = "root"
319mailets = []
320"#;
321
322 let mut file = Builder::new().suffix(".toml").tempfile().unwrap();
323 file.write_all(config.as_bytes()).unwrap();
324 let path = file.path().to_str().unwrap().to_string();
325
326 let result = run(&path);
327 assert!(result.is_ok());
328 }
329}