Skip to main content

rusmes_config/
lib.rs

1//! # rusmes-config
2//!
3//! Configuration management for the RusMES mail server.
4//!
5//! ## Overview
6//!
7//! `rusmes-config` provides the [`ServerConfig`] struct and supporting types that model
8//! the complete runtime configuration of a RusMES installation.  Configuration is
9//! normally loaded from a TOML or YAML file on disk, with optional overrides from
10//! environment variables (prefix `RUSMES_`).
11//!
12//! ## File format auto-detection
13//!
14//! [`ServerConfig::from_file`] inspects the file extension:
15//!
16//! | Extension | Format |
17//! |-----------|--------|
18//! | `.toml`   | TOML   |
19//! | `.yaml` / `.yml` | YAML |
20//!
21//! Both formats expose identical semantics; see the crate tests for concrete examples.
22//!
23//! ## Environment variable overrides
24//!
25//! Every significant configuration key has a corresponding `RUSMES_*` environment
26//! variable that takes precedence over the file value.  A full list is documented on
27//! [`ServerConfig::apply_env_overrides`].  This enables twelve-factor-style deployments
28//! where the base config is baked into a container image and secrets are injected at
29//! runtime.
30//!
31//! ## Sections
32//!
33//! | Struct | Field | Description |
34//! |--------|-------|-------------|
35//! | [`SmtpServerConfig`] | `smtp` | Listening addresses, TLS ports, rate limits |
36//! | [`ImapServerConfig`] | `imap` | IMAP4rev1 listener |
37//! | [`JmapServerConfig`] | `jmap` | JMAP HTTP listener |
38//! | [`Pop3ServerConfig`] | `pop3` | POP3 listener |
39//! | [`StorageConfig`] | `storage` | Filesystem, Postgres, or AmateRS backend |
40//! | [`AuthConfig`] | `auth` | File, LDAP, SQL, or OAuth2 auth backend config |
41//! | [`QueueConfig`] | `queue` | Retry queue with exponential back-off |
42//! | [`SecurityConfig`] | `security` | Relay networks, blocked IPs |
43//! | [`DomainsConfig`] | `domains` | Local domains and address aliases |
44//! | [`MetricsConfig`] | `metrics` | Prometheus scrape endpoint |
45//! | [`TracingConfig`] | `tracing` | OpenTelemetry OTLP exporter |
46//! | [`ConnectionLimitsConfig`] | `connection_limits` | Per-IP and global connection caps |
47//! | [`LoggingConfig`] | `logging` | Log level / format / output routing |
48//!
49//! The [`logging`] module provides [`logging::init_logging`] for initialising the global
50//! `tracing` subscriber from a [`logging::LogConfig`], including file rotation and optional
51//! gzip compression of rotated files.
52//!
53//! ## Validation
54//!
55//! [`ServerConfig::validate`] is called automatically during [`ServerConfig::from_file`].
56//! It checks domain syntax, email addresses, port numbers, storage path accessibility,
57//! and processor uniqueness.
58//!
59//! ## Example
60//!
61//! ```rust,no_run
62//! use rusmes_config::ServerConfig;
63//!
64//! let cfg = ServerConfig::from_file("/etc/rusmes/rusmes.toml")?;
65//! println!("Serving domain {}", cfg.domain);
66//! # Ok::<(), anyhow::Error>(())
67//! ```
68
69pub mod logging;
70mod validation;
71
72use rusmes_proto::MailAddress;
73use serde::{Deserialize, Serialize};
74use std::collections::HashMap;
75use std::path::Path;
76use validation::{
77    validate_domain, validate_email, validate_port, validate_processors, validate_storage_path,
78};
79
80/// Main server configuration
81#[derive(Debug, Clone, Deserialize, Serialize)]
82pub struct ServerConfig {
83    pub domain: String,
84    pub postmaster: String,
85    pub smtp: SmtpServerConfig,
86    pub imap: Option<ImapServerConfig>,
87    pub jmap: Option<JmapServerConfig>,
88    pub pop3: Option<Pop3ServerConfig>,
89    pub storage: StorageConfig,
90    pub processors: Vec<ProcessorConfig>,
91    #[serde(default)]
92    pub relay: Option<RelayConfig>,
93    #[serde(default)]
94    pub auth: Option<AuthConfig>,
95    #[serde(default)]
96    pub logging: Option<LoggingConfig>,
97    #[serde(default)]
98    pub queue: Option<QueueConfig>,
99    #[serde(default)]
100    pub security: Option<SecurityConfig>,
101    #[serde(default)]
102    pub domains: Option<DomainsConfig>,
103    #[serde(default)]
104    pub metrics: Option<MetricsConfig>,
105    #[serde(default)]
106    pub tracing: Option<TracingConfig>,
107    #[serde(default)]
108    pub connection_limits: Option<ConnectionLimitsConfig>,
109}
110
111impl ServerConfig {
112    /// Load configuration from a TOML or YAML file
113    ///
114    /// The format is auto-detected based on file extension:
115    /// - `.toml` files are parsed as TOML
116    /// - `.yaml` or `.yml` files are parsed as YAML
117    pub fn from_file(path: impl AsRef<Path>) -> anyhow::Result<Self> {
118        let path = path.as_ref();
119        let content = std::fs::read_to_string(path)?;
120
121        // Auto-detect format based on file extension
122        let mut config: ServerConfig = match path.extension().and_then(|ext| ext.to_str()) {
123            Some("yaml") | Some("yml") => serde_yaml::from_str(&content)?,
124            Some("toml") => toml::from_str(&content)?,
125            Some(ext) => {
126                return Err(anyhow::anyhow!(
127                    "Unsupported configuration file extension: .{}. Use .toml, .yaml, or .yml",
128                    ext
129                ));
130            }
131            None => {
132                return Err(anyhow::anyhow!(
133                    "Configuration file must have a .toml, .yaml, or .yml extension"
134                ));
135            }
136        };
137
138        // Apply environment variable overrides
139        config.apply_env_overrides();
140
141        // Validate configuration
142        config.validate()?;
143
144        Ok(config)
145    }
146
147    /// Apply environment variable overrides to configuration
148    ///
149    /// Environment variables follow the convention RUSMES_SECTION_KEY.
150    /// Priority: env vars > config file > defaults
151    ///
152    /// Supported environment variables:
153    /// - RUSMES_DOMAIN
154    /// - RUSMES_POSTMASTER
155    /// - RUSMES_SMTP_HOST
156    /// - RUSMES_SMTP_PORT
157    /// - RUSMES_SMTP_TLS_PORT
158    /// - RUSMES_SMTP_MAX_MESSAGE_SIZE
159    /// - RUSMES_SMTP_REQUIRE_AUTH
160    /// - RUSMES_SMTP_ENABLE_STARTTLS
161    /// - RUSMES_SMTP_RATE_LIMIT_MAX_CONNECTIONS_PER_IP
162    /// - RUSMES_SMTP_RATE_LIMIT_MAX_MESSAGES_PER_HOUR
163    /// - RUSMES_SMTP_RATE_LIMIT_WINDOW_DURATION
164    /// - RUSMES_IMAP_HOST
165    /// - RUSMES_IMAP_PORT
166    /// - RUSMES_IMAP_TLS_PORT
167    /// - RUSMES_JMAP_HOST
168    /// - RUSMES_JMAP_PORT
169    /// - RUSMES_JMAP_BASE_URL
170    /// - RUSMES_STORAGE_PATH (for filesystem backend)
171    /// - RUSMES_LOG_LEVEL
172    /// - RUSMES_LOG_FORMAT
173    /// - RUSMES_LOG_OUTPUT
174    /// - RUSMES_QUEUE_INITIAL_DELAY
175    /// - RUSMES_QUEUE_MAX_DELAY
176    /// - RUSMES_QUEUE_BACKOFF_MULTIPLIER
177    /// - RUSMES_QUEUE_MAX_ATTEMPTS
178    /// - RUSMES_QUEUE_WORKER_THREADS
179    /// - RUSMES_QUEUE_BATCH_SIZE
180    /// - RUSMES_METRICS_ENABLED
181    /// - RUSMES_METRICS_BIND_ADDRESS
182    /// - RUSMES_METRICS_PATH
183    /// - RUSMES_TRACING_ENABLED
184    /// - RUSMES_TRACING_ENDPOINT
185    /// - RUSMES_TRACING_PROTOCOL (grpc or http)
186    /// - RUSMES_TRACING_SERVICE_NAME
187    /// - RUSMES_TRACING_SAMPLE_RATIO
188    /// - RUSMES_CONNECTION_LIMITS_MAX_CONNECTIONS_PER_IP
189    /// - RUSMES_CONNECTION_LIMITS_MAX_TOTAL_CONNECTIONS
190    /// - RUSMES_CONNECTION_LIMITS_IDLE_TIMEOUT
191    /// - RUSMES_CONNECTION_LIMITS_REAPER_INTERVAL
192    pub fn apply_env_overrides(&mut self) {
193        // Top-level fields
194        if let Ok(val) = std::env::var("RUSMES_DOMAIN") {
195            self.domain = val;
196        }
197        if let Ok(val) = std::env::var("RUSMES_POSTMASTER") {
198            self.postmaster = val;
199        }
200
201        // SMTP configuration
202        if let Ok(val) = std::env::var("RUSMES_SMTP_HOST") {
203            self.smtp.host = val;
204        }
205        if let Ok(val) = std::env::var("RUSMES_SMTP_PORT") {
206            if let Ok(port) = val.parse::<u16>() {
207                self.smtp.port = port;
208            }
209        }
210        if let Ok(val) = std::env::var("RUSMES_SMTP_TLS_PORT") {
211            if let Ok(port) = val.parse::<u16>() {
212                self.smtp.tls_port = Some(port);
213            }
214        }
215        if let Ok(val) = std::env::var("RUSMES_SMTP_MAX_MESSAGE_SIZE") {
216            self.smtp.max_message_size = val;
217        }
218        if let Ok(val) = std::env::var("RUSMES_SMTP_REQUIRE_AUTH") {
219            if let Ok(b) = val.parse::<bool>() {
220                self.smtp.require_auth = b;
221            }
222        }
223        if let Ok(val) = std::env::var("RUSMES_SMTP_ENABLE_STARTTLS") {
224            if let Ok(b) = val.parse::<bool>() {
225                self.smtp.enable_starttls = b;
226            }
227        }
228
229        // SMTP rate limit configuration
230        let has_rate_limit_max =
231            std::env::var("RUSMES_SMTP_RATE_LIMIT_MAX_MESSAGES_PER_HOUR").is_ok();
232        let has_rate_limit_window = std::env::var("RUSMES_SMTP_RATE_LIMIT_WINDOW_DURATION").is_ok();
233        let has_rate_limit_max_conn =
234            std::env::var("RUSMES_SMTP_RATE_LIMIT_MAX_CONNECTIONS_PER_IP").is_ok();
235
236        if has_rate_limit_max || has_rate_limit_window || has_rate_limit_max_conn {
237            // Create rate limit config if it doesn't exist
238            if self.smtp.rate_limit.is_none() {
239                self.smtp.rate_limit = Some(RateLimitConfig {
240                    max_connections_per_ip: 10,
241                    max_messages_per_hour: 100,
242                    window_duration: "1h".to_string(),
243                });
244            }
245
246            if let Some(ref mut rate_limit) = self.smtp.rate_limit {
247                if let Ok(val) = std::env::var("RUSMES_SMTP_RATE_LIMIT_MAX_CONNECTIONS_PER_IP") {
248                    if let Ok(n) = val.parse::<usize>() {
249                        rate_limit.max_connections_per_ip = n;
250                    }
251                }
252                if let Ok(val) = std::env::var("RUSMES_SMTP_RATE_LIMIT_MAX_MESSAGES_PER_HOUR") {
253                    if let Ok(n) = val.parse::<u32>() {
254                        rate_limit.max_messages_per_hour = n;
255                    }
256                }
257                if let Ok(val) = std::env::var("RUSMES_SMTP_RATE_LIMIT_WINDOW_DURATION") {
258                    rate_limit.window_duration = val;
259                }
260            }
261        }
262
263        // IMAP configuration
264        if let Ok(val) = std::env::var("RUSMES_IMAP_HOST") {
265            if self.imap.is_none() {
266                self.imap = Some(ImapServerConfig {
267                    host: "0.0.0.0".to_string(),
268                    port: 143,
269                    tls_port: None,
270                });
271            }
272            if let Some(ref mut imap) = self.imap {
273                imap.host = val;
274            }
275        }
276        if let Ok(val) = std::env::var("RUSMES_IMAP_PORT") {
277            if let Ok(port) = val.parse::<u16>() {
278                if self.imap.is_none() {
279                    self.imap = Some(ImapServerConfig {
280                        host: "0.0.0.0".to_string(),
281                        port,
282                        tls_port: None,
283                    });
284                } else if let Some(ref mut imap) = self.imap {
285                    imap.port = port;
286                }
287            }
288        }
289        if let Ok(val) = std::env::var("RUSMES_IMAP_TLS_PORT") {
290            if let Ok(port) = val.parse::<u16>() {
291                if self.imap.is_none() {
292                    self.imap = Some(ImapServerConfig {
293                        host: "0.0.0.0".to_string(),
294                        port: 143,
295                        tls_port: Some(port),
296                    });
297                } else if let Some(ref mut imap) = self.imap {
298                    imap.tls_port = Some(port);
299                }
300            }
301        }
302
303        // JMAP configuration
304        if let Ok(val) = std::env::var("RUSMES_JMAP_HOST") {
305            if self.jmap.is_none() {
306                self.jmap = Some(JmapServerConfig {
307                    host: "0.0.0.0".to_string(),
308                    port: 8080,
309                    base_url: "http://localhost:8080".to_string(),
310                });
311            }
312            if let Some(ref mut jmap) = self.jmap {
313                jmap.host = val;
314            }
315        }
316        if let Ok(val) = std::env::var("RUSMES_JMAP_PORT") {
317            if let Ok(port) = val.parse::<u16>() {
318                if self.jmap.is_none() {
319                    self.jmap = Some(JmapServerConfig {
320                        host: "0.0.0.0".to_string(),
321                        port,
322                        base_url: "http://localhost:8080".to_string(),
323                    });
324                } else if let Some(ref mut jmap) = self.jmap {
325                    jmap.port = port;
326                }
327            }
328        }
329        if let Ok(val) = std::env::var("RUSMES_JMAP_BASE_URL") {
330            if self.jmap.is_none() {
331                self.jmap = Some(JmapServerConfig {
332                    host: "0.0.0.0".to_string(),
333                    port: 8080,
334                    base_url: val,
335                });
336            } else if let Some(ref mut jmap) = self.jmap {
337                jmap.base_url = val;
338            }
339        }
340
341        // Storage configuration (only filesystem backend path)
342        if let Ok(val) = std::env::var("RUSMES_STORAGE_PATH") {
343            if let StorageConfig::Filesystem { ref mut path } = self.storage {
344                *path = val;
345            }
346        }
347
348        // Logging configuration
349        if let Ok(val) = std::env::var("RUSMES_LOG_LEVEL") {
350            if self.logging.is_none() {
351                self.logging = Some(LoggingConfig {
352                    level: val,
353                    format: "text".to_string(),
354                    output: "stdout".to_string(),
355                    file: None,
356                });
357            } else if let Some(ref mut logging) = self.logging {
358                logging.level = val;
359            }
360        }
361        if let Ok(val) = std::env::var("RUSMES_LOG_FORMAT") {
362            if self.logging.is_none() {
363                self.logging = Some(LoggingConfig {
364                    level: "info".to_string(),
365                    format: val,
366                    output: "stdout".to_string(),
367                    file: None,
368                });
369            } else if let Some(ref mut logging) = self.logging {
370                logging.format = val;
371            }
372        }
373        if let Ok(val) = std::env::var("RUSMES_LOG_OUTPUT") {
374            if self.logging.is_none() {
375                self.logging = Some(LoggingConfig {
376                    level: "info".to_string(),
377                    format: "text".to_string(),
378                    output: val,
379                    file: None,
380                });
381            } else if let Some(ref mut logging) = self.logging {
382                logging.output = val;
383            }
384        }
385
386        // Queue configuration
387        if let Ok(val) = std::env::var("RUSMES_QUEUE_INITIAL_DELAY") {
388            if self.queue.is_none() {
389                self.queue = Some(QueueConfig {
390                    initial_delay: val,
391                    max_delay: "3600s".to_string(),
392                    backoff_multiplier: 2.0,
393                    max_attempts: 5,
394                    worker_threads: 4,
395                    batch_size: 100,
396                });
397            } else if let Some(ref mut queue) = self.queue {
398                queue.initial_delay = val;
399            }
400        }
401        if let Ok(val) = std::env::var("RUSMES_QUEUE_MAX_DELAY") {
402            if self.queue.is_none() {
403                self.queue = Some(QueueConfig {
404                    initial_delay: "60s".to_string(),
405                    max_delay: val,
406                    backoff_multiplier: 2.0,
407                    max_attempts: 5,
408                    worker_threads: 4,
409                    batch_size: 100,
410                });
411            } else if let Some(ref mut queue) = self.queue {
412                queue.max_delay = val;
413            }
414        }
415        if let Ok(val) = std::env::var("RUSMES_QUEUE_BACKOFF_MULTIPLIER") {
416            if let Ok(multiplier) = val.parse::<f64>() {
417                if self.queue.is_none() {
418                    self.queue = Some(QueueConfig {
419                        initial_delay: "60s".to_string(),
420                        max_delay: "3600s".to_string(),
421                        backoff_multiplier: multiplier,
422                        max_attempts: 5,
423                        worker_threads: 4,
424                        batch_size: 100,
425                    });
426                } else if let Some(ref mut queue) = self.queue {
427                    queue.backoff_multiplier = multiplier;
428                }
429            }
430        }
431        if let Ok(val) = std::env::var("RUSMES_QUEUE_MAX_ATTEMPTS") {
432            if let Ok(attempts) = val.parse::<u32>() {
433                if self.queue.is_none() {
434                    self.queue = Some(QueueConfig {
435                        initial_delay: "60s".to_string(),
436                        max_delay: "3600s".to_string(),
437                        backoff_multiplier: 2.0,
438                        max_attempts: attempts,
439                        worker_threads: 4,
440                        batch_size: 100,
441                    });
442                } else if let Some(ref mut queue) = self.queue {
443                    queue.max_attempts = attempts;
444                }
445            }
446        }
447        if let Ok(val) = std::env::var("RUSMES_QUEUE_WORKER_THREADS") {
448            if let Ok(threads) = val.parse::<usize>() {
449                if self.queue.is_none() {
450                    self.queue = Some(QueueConfig {
451                        initial_delay: "60s".to_string(),
452                        max_delay: "3600s".to_string(),
453                        backoff_multiplier: 2.0,
454                        max_attempts: 5,
455                        worker_threads: threads,
456                        batch_size: 100,
457                    });
458                } else if let Some(ref mut queue) = self.queue {
459                    queue.worker_threads = threads;
460                }
461            }
462        }
463        if let Ok(val) = std::env::var("RUSMES_QUEUE_BATCH_SIZE") {
464            if let Ok(batch_size) = val.parse::<usize>() {
465                if self.queue.is_none() {
466                    self.queue = Some(QueueConfig {
467                        initial_delay: "60s".to_string(),
468                        max_delay: "3600s".to_string(),
469                        backoff_multiplier: 2.0,
470                        max_attempts: 5,
471                        worker_threads: 4,
472                        batch_size,
473                    });
474                } else if let Some(ref mut queue) = self.queue {
475                    queue.batch_size = batch_size;
476                }
477            }
478        }
479
480        // Metrics configuration
481        if let Ok(val) = std::env::var("RUSMES_METRICS_ENABLED") {
482            if let Ok(enabled) = val.parse::<bool>() {
483                if self.metrics.is_none() {
484                    self.metrics = Some(MetricsConfig {
485                        enabled,
486                        bind_address: "0.0.0.0:9090".to_string(),
487                        path: "/metrics".to_string(),
488                    });
489                } else if let Some(ref mut metrics) = self.metrics {
490                    metrics.enabled = enabled;
491                }
492            }
493        }
494        if let Ok(val) = std::env::var("RUSMES_METRICS_BIND_ADDRESS") {
495            if self.metrics.is_none() {
496                self.metrics = Some(MetricsConfig {
497                    enabled: true,
498                    bind_address: val,
499                    path: "/metrics".to_string(),
500                });
501            } else if let Some(ref mut metrics) = self.metrics {
502                metrics.bind_address = val;
503            }
504        }
505        if let Ok(val) = std::env::var("RUSMES_METRICS_PATH") {
506            if self.metrics.is_none() {
507                self.metrics = Some(MetricsConfig {
508                    enabled: true,
509                    bind_address: "0.0.0.0:9090".to_string(),
510                    path: val,
511                });
512            } else if let Some(ref mut metrics) = self.metrics {
513                metrics.path = val;
514            }
515        }
516
517        // Tracing configuration
518        if let Ok(val) = std::env::var("RUSMES_TRACING_ENABLED") {
519            if let Ok(enabled) = val.parse::<bool>() {
520                if self.tracing.is_none() {
521                    self.tracing = Some(TracingConfig {
522                        enabled,
523                        ..Default::default()
524                    });
525                } else if let Some(ref mut tracing) = self.tracing {
526                    tracing.enabled = enabled;
527                }
528            }
529        }
530        if let Ok(val) = std::env::var("RUSMES_TRACING_ENDPOINT") {
531            if self.tracing.is_none() {
532                self.tracing = Some(TracingConfig {
533                    enabled: true,
534                    endpoint: val,
535                    ..Default::default()
536                });
537            } else if let Some(ref mut tracing) = self.tracing {
538                tracing.endpoint = val;
539            }
540        }
541        if let Ok(val) = std::env::var("RUSMES_TRACING_PROTOCOL") {
542            let protocol = match val.to_lowercase().as_str() {
543                "grpc" => OtlpProtocol::Grpc,
544                "http" => OtlpProtocol::Http,
545                _ => OtlpProtocol::Grpc,
546            };
547            if self.tracing.is_none() {
548                self.tracing = Some(TracingConfig {
549                    enabled: true,
550                    protocol,
551                    ..Default::default()
552                });
553            } else if let Some(ref mut tracing) = self.tracing {
554                tracing.protocol = protocol;
555            }
556        }
557        if let Ok(val) = std::env::var("RUSMES_TRACING_SERVICE_NAME") {
558            if self.tracing.is_none() {
559                self.tracing = Some(TracingConfig {
560                    enabled: true,
561                    service_name: val,
562                    ..Default::default()
563                });
564            } else if let Some(ref mut tracing) = self.tracing {
565                tracing.service_name = val;
566            }
567        }
568        if let Ok(val) = std::env::var("RUSMES_TRACING_SAMPLE_RATIO") {
569            if let Ok(ratio) = val.parse::<f64>() {
570                if self.tracing.is_none() {
571                    self.tracing = Some(TracingConfig {
572                        enabled: true,
573                        sample_ratio: ratio,
574                        ..Default::default()
575                    });
576                } else if let Some(ref mut tracing) = self.tracing {
577                    tracing.sample_ratio = ratio;
578                }
579            }
580        }
581
582        // Connection limits configuration
583        if let Ok(val) = std::env::var("RUSMES_CONNECTION_LIMITS_MAX_CONNECTIONS_PER_IP") {
584            if let Ok(max) = val.parse::<usize>() {
585                if self.connection_limits.is_none() {
586                    self.connection_limits = Some(ConnectionLimitsConfig {
587                        max_connections_per_ip: max,
588                        max_total_connections: default_max_total_connections(),
589                        idle_timeout: default_idle_timeout(),
590                        reaper_interval: default_reaper_interval(),
591                    });
592                } else if let Some(ref mut limits) = self.connection_limits {
593                    limits.max_connections_per_ip = max;
594                }
595            }
596        }
597        if let Ok(val) = std::env::var("RUSMES_CONNECTION_LIMITS_MAX_TOTAL_CONNECTIONS") {
598            if let Ok(max) = val.parse::<usize>() {
599                if self.connection_limits.is_none() {
600                    self.connection_limits = Some(ConnectionLimitsConfig {
601                        max_connections_per_ip: default_max_connections_per_ip(),
602                        max_total_connections: max,
603                        idle_timeout: default_idle_timeout(),
604                        reaper_interval: default_reaper_interval(),
605                    });
606                } else if let Some(ref mut limits) = self.connection_limits {
607                    limits.max_total_connections = max;
608                }
609            }
610        }
611        if let Ok(val) = std::env::var("RUSMES_CONNECTION_LIMITS_IDLE_TIMEOUT") {
612            if self.connection_limits.is_none() {
613                self.connection_limits = Some(ConnectionLimitsConfig {
614                    max_connections_per_ip: default_max_connections_per_ip(),
615                    max_total_connections: default_max_total_connections(),
616                    idle_timeout: val,
617                    reaper_interval: default_reaper_interval(),
618                });
619            } else if let Some(ref mut limits) = self.connection_limits {
620                limits.idle_timeout = val;
621            }
622        }
623        if let Ok(val) = std::env::var("RUSMES_CONNECTION_LIMITS_REAPER_INTERVAL") {
624            if self.connection_limits.is_none() {
625                self.connection_limits = Some(ConnectionLimitsConfig {
626                    max_connections_per_ip: default_max_connections_per_ip(),
627                    max_total_connections: default_max_total_connections(),
628                    idle_timeout: default_idle_timeout(),
629                    reaper_interval: val,
630                });
631            } else if let Some(ref mut limits) = self.connection_limits {
632                limits.reaper_interval = val;
633            }
634        }
635    }
636
637    /// Validate the entire configuration
638    ///
639    /// This method is called automatically when loading configuration from a file.
640    /// It validates:
641    /// - Domain name format
642    /// - Postmaster email address
643    /// - Port numbers for SMTP, IMAP, JMAP
644    /// - Storage path accessibility
645    /// - Processor uniqueness
646    /// - Local domain names (if configured)
647    pub fn validate(&self) -> anyhow::Result<()> {
648        // Validate main domain
649        validate_domain(&self.domain)
650            .map_err(|e| anyhow::anyhow!("Invalid server domain: {}", e))?;
651
652        // Validate postmaster email
653        validate_email(&self.postmaster)
654            .map_err(|e| anyhow::anyhow!("Invalid postmaster email: {}", e))?;
655
656        // Validate SMTP configuration
657        validate_port(self.smtp.port, "SMTP port")?;
658        if let Some(tls_port) = self.smtp.tls_port {
659            validate_port(tls_port, "SMTP TLS port")?;
660        }
661
662        // Validate IMAP configuration
663        if let Some(ref imap) = self.imap {
664            validate_port(imap.port, "IMAP port")?;
665            if let Some(tls_port) = imap.tls_port {
666                validate_port(tls_port, "IMAP TLS port")?;
667            }
668        }
669
670        // Validate JMAP configuration
671        if let Some(ref jmap) = self.jmap {
672            validate_port(jmap.port, "JMAP port")?;
673        }
674
675        // Validate POP3 configuration
676        if let Some(ref pop3) = self.pop3 {
677            validate_port(pop3.port, "POP3 port")?;
678            if let Some(tls_port) = pop3.tls_port {
679                validate_port(tls_port, "POP3 TLS port")?;
680            }
681        }
682
683        // Validate storage path
684        match &self.storage {
685            StorageConfig::Filesystem { path } => {
686                validate_storage_path(path)?;
687            }
688            StorageConfig::Postgres { connection_string } => {
689                if connection_string.is_empty() {
690                    anyhow::bail!("Postgres connection string cannot be empty");
691                }
692            }
693            StorageConfig::AmateRS {
694                endpoints,
695                replication_factor,
696            } => {
697                if endpoints.is_empty() {
698                    anyhow::bail!("AmateRS endpoints cannot be empty");
699                }
700                if *replication_factor == 0 {
701                    anyhow::bail!("AmateRS replication factor must be greater than 0");
702                }
703            }
704        }
705
706        // Validate processors
707        validate_processors(&self.processors)?;
708
709        // Validate local domains if configured
710        if let Some(ref domains) = self.domains {
711            for domain in &domains.local_domains {
712                validate_domain(domain)
713                    .map_err(|e| anyhow::anyhow!("Invalid local domain '{}': {}", domain, e))?;
714            }
715
716            // Validate aliases
717            for (from, to) in &domains.aliases {
718                validate_email(from)
719                    .map_err(|e| anyhow::anyhow!("Invalid alias source '{}': {}", from, e))?;
720                validate_email(to)
721                    .map_err(|e| anyhow::anyhow!("Invalid alias destination '{}': {}", to, e))?;
722            }
723        }
724
725        // Validate logging configuration
726        if let Some(ref logging) = self.logging {
727            logging.validate_level()?;
728            logging.validate_format()?;
729        }
730
731        // Validate queue configuration
732        if let Some(ref queue) = self.queue {
733            queue.validate_backoff_multiplier()?;
734            queue.validate_worker_threads()?;
735        }
736
737        // Validate security configuration
738        if let Some(ref security) = self.security {
739            security.validate_relay_networks()?;
740            security.validate_blocked_ips()?;
741        }
742
743        // Validate metrics configuration
744        if let Some(ref metrics) = self.metrics {
745            metrics.validate_bind_address()?;
746            metrics.validate_path()?;
747        }
748
749        Ok(())
750    }
751
752    /// Get postmaster address
753    pub fn postmaster_address(&self) -> anyhow::Result<MailAddress> {
754        self.postmaster
755            .parse()
756            .map_err(|e| anyhow::anyhow!("Invalid postmaster address: {}", e))
757    }
758}
759
760/// SMTP server configuration
761#[derive(Debug, Clone, Deserialize, Serialize)]
762pub struct SmtpServerConfig {
763    pub host: String,
764    pub port: u16,
765    #[serde(default)]
766    pub tls_port: Option<u16>,
767    pub max_message_size: String, // e.g., "50MB"
768    #[serde(default)]
769    pub require_auth: bool,
770    #[serde(default)]
771    pub enable_starttls: bool,
772    #[serde(default)]
773    pub rate_limit: Option<RateLimitConfig>,
774}
775
776impl SmtpServerConfig {
777    /// Parse max message size to bytes
778    pub fn max_message_size_bytes(&self) -> anyhow::Result<usize> {
779        parse_size(&self.max_message_size)
780    }
781}
782
783/// IMAP server configuration
784#[derive(Debug, Clone, Deserialize, Serialize)]
785pub struct ImapServerConfig {
786    pub host: String,
787    pub port: u16,
788    #[serde(default)]
789    pub tls_port: Option<u16>,
790}
791
792/// JMAP server configuration
793#[derive(Debug, Clone, Deserialize, Serialize)]
794pub struct JmapServerConfig {
795    pub host: String,
796    pub port: u16,
797    pub base_url: String,
798}
799
800/// POP3 server configuration
801#[derive(Debug, Clone, Deserialize, Serialize)]
802pub struct Pop3ServerConfig {
803    pub host: String,
804    pub port: u16,
805    #[serde(default)]
806    pub tls_port: Option<u16>,
807    #[serde(default = "default_pop3_timeout")]
808    pub timeout_seconds: u64,
809    #[serde(default)]
810    pub enable_stls: bool,
811}
812
813fn default_pop3_timeout() -> u64 {
814    600
815}
816
817/// SMTP Relay configuration for outbound mail
818#[derive(Debug, Clone, Deserialize, Serialize)]
819pub struct RelayConfig {
820    pub host: String,
821    pub port: u16,
822    #[serde(default)]
823    pub username: Option<String>,
824    #[serde(default)]
825    pub password: Option<String>,
826    #[serde(default = "default_use_tls")]
827    pub use_tls: bool,
828}
829
830fn default_use_tls() -> bool {
831    true
832}
833
834/// Storage backend configuration
835#[derive(Debug, Clone, Deserialize, Serialize)]
836#[serde(tag = "backend")]
837pub enum StorageConfig {
838    #[serde(rename = "filesystem")]
839    Filesystem { path: String },
840    #[serde(rename = "postgres")]
841    Postgres { connection_string: String },
842    #[serde(rename = "amaters")]
843    AmateRS {
844        endpoints: Vec<String>,
845        replication_factor: usize,
846    },
847}
848
849/// Processor chain configuration
850#[derive(Debug, Clone, Deserialize, Serialize)]
851pub struct ProcessorConfig {
852    pub name: String,
853    pub state: String,
854    pub mailets: Vec<MailetConfig>,
855}
856
857/// Mailet configuration
858#[derive(Debug, Clone, Deserialize, Serialize)]
859pub struct MailetConfig {
860    pub matcher: String,
861    pub mailet: String,
862    #[serde(default)]
863    pub params: HashMap<String, String>,
864}
865
866/// Rate limiting configuration
867#[derive(Debug, Clone, Deserialize, Serialize)]
868pub struct RateLimitConfig {
869    #[serde(default = "default_max_connections_per_ip")]
870    pub max_connections_per_ip: usize,
871    pub max_messages_per_hour: u32,
872    pub window_duration: String, // e.g., "1h"
873}
874
875fn default_max_connections_per_ip() -> usize {
876    10
877}
878
879impl RateLimitConfig {
880    /// Parse window duration to seconds
881    pub fn window_duration_seconds(&self) -> anyhow::Result<u64> {
882        parse_duration(&self.window_duration)
883    }
884}
885
886/// Authentication backend configuration
887#[derive(Debug, Clone, Deserialize, Serialize)]
888#[serde(tag = "backend")]
889pub enum AuthConfig {
890    #[serde(rename = "file")]
891    File {
892        #[serde(flatten)]
893        config: FileAuthConfig,
894    },
895    #[serde(rename = "ldap")]
896    Ldap {
897        #[serde(flatten)]
898        config: LdapAuthConfig,
899    },
900    #[serde(rename = "sql")]
901    Sql {
902        #[serde(flatten)]
903        config: SqlAuthConfig,
904    },
905    #[serde(rename = "oauth2")]
906    OAuth2 {
907        #[serde(flatten)]
908        config: OAuth2AuthConfig,
909    },
910}
911
912/// File-based authentication configuration
913#[derive(Debug, Clone, Deserialize, Serialize)]
914pub struct FileAuthConfig {
915    pub path: String,
916}
917
918/// LDAP authentication configuration
919#[derive(Debug, Clone, Deserialize, Serialize)]
920pub struct LdapAuthConfig {
921    pub url: String,
922    pub base_dn: String,
923    pub bind_dn: String,
924    pub bind_password: String,
925    pub user_filter: String,
926}
927
928/// SQL authentication configuration
929#[derive(Debug, Clone, Deserialize, Serialize)]
930pub struct SqlAuthConfig {
931    pub connection_string: String,
932    pub query: String,
933}
934
935/// OAuth2 authentication configuration
936#[derive(Debug, Clone, Deserialize, Serialize)]
937pub struct OAuth2AuthConfig {
938    pub client_id: String,
939    pub client_secret: String,
940    pub token_url: String,
941    pub authorization_url: String,
942}
943
944/// Logging configuration
945#[derive(Debug, Clone, Deserialize, Serialize)]
946pub struct LoggingConfig {
947    pub level: String,  // trace, debug, info, warn, error
948    pub format: String, // json or text
949    pub output: String, // stdout, stderr, or file path
950    #[serde(default)]
951    pub file: Option<LogFileConfig>,
952}
953
954impl LoggingConfig {
955    /// Validate log level
956    pub fn validate_level(&self) -> anyhow::Result<()> {
957        match self.level.as_str() {
958            "trace" | "debug" | "info" | "warn" | "error" => Ok(()),
959            _ => Err(anyhow::anyhow!("Invalid log level: {}", self.level)),
960        }
961    }
962
963    /// Validate log format
964    pub fn validate_format(&self) -> anyhow::Result<()> {
965        match self.format.as_str() {
966            "json" | "text" => Ok(()),
967            _ => Err(anyhow::anyhow!("Invalid log format: {}", self.format)),
968        }
969    }
970}
971
972/// Log file configuration
973#[derive(Debug, Clone, Deserialize, Serialize)]
974pub struct LogFileConfig {
975    pub path: String,
976    pub max_size: String, // e.g., "100MB"
977    pub max_backups: u32,
978    pub compress: bool,
979}
980
981impl LogFileConfig {
982    /// Parse max file size to bytes
983    pub fn max_size_bytes(&self) -> anyhow::Result<usize> {
984        parse_size(&self.max_size)
985    }
986}
987
988/// Queue configuration
989#[derive(Debug, Clone, Deserialize, Serialize)]
990pub struct QueueConfig {
991    pub initial_delay: String, // e.g., "60s"
992    pub max_delay: String,     // e.g., "3600s"
993    pub backoff_multiplier: f64,
994    pub max_attempts: u32,
995    pub worker_threads: usize,
996    pub batch_size: usize,
997}
998
999impl QueueConfig {
1000    /// Parse initial delay to seconds
1001    pub fn initial_delay_seconds(&self) -> anyhow::Result<u64> {
1002        parse_duration(&self.initial_delay)
1003    }
1004
1005    /// Parse max delay to seconds
1006    pub fn max_delay_seconds(&self) -> anyhow::Result<u64> {
1007        parse_duration(&self.max_delay)
1008    }
1009
1010    /// Validate backoff multiplier
1011    pub fn validate_backoff_multiplier(&self) -> anyhow::Result<()> {
1012        if self.backoff_multiplier <= 0.0 {
1013            return Err(anyhow::anyhow!("backoff_multiplier must be positive"));
1014        }
1015        Ok(())
1016    }
1017
1018    /// Validate worker threads
1019    pub fn validate_worker_threads(&self) -> anyhow::Result<()> {
1020        if self.worker_threads == 0 {
1021            return Err(anyhow::anyhow!("worker_threads must be greater than 0"));
1022        }
1023        Ok(())
1024    }
1025}
1026
1027/// Security configuration
1028#[derive(Debug, Clone, Deserialize, Serialize)]
1029pub struct SecurityConfig {
1030    pub relay_networks: Vec<String>, // CIDR notation
1031    pub blocked_ips: Vec<String>,
1032    pub check_recipient_exists: bool,
1033    pub reject_unknown_recipients: bool,
1034}
1035
1036impl SecurityConfig {
1037    /// Validate CIDR notation for relay networks
1038    pub fn validate_relay_networks(&self) -> anyhow::Result<()> {
1039        for network in &self.relay_networks {
1040            // Basic validation - should contain a slash for CIDR notation
1041            if !network.contains('/') {
1042                return Err(anyhow::anyhow!("Invalid CIDR notation: {}", network));
1043            }
1044        }
1045        Ok(())
1046    }
1047
1048    /// Validate IP addresses in blocked list
1049    pub fn validate_blocked_ips(&self) -> anyhow::Result<()> {
1050        for ip in &self.blocked_ips {
1051            // Basic validation - should contain dots (IPv4) or colons (IPv6)
1052            if !ip.contains('.') && !ip.contains(':') {
1053                return Err(anyhow::anyhow!("Invalid IP address: {}", ip));
1054            }
1055        }
1056        Ok(())
1057    }
1058}
1059
1060/// Domains configuration
1061#[derive(Debug, Clone, Deserialize, Serialize)]
1062pub struct DomainsConfig {
1063    pub local_domains: Vec<String>,
1064    #[serde(default)]
1065    pub aliases: HashMap<String, String>,
1066}
1067
1068impl DomainsConfig {
1069    /// Validate domain names
1070    pub fn validate_local_domains(&self) -> anyhow::Result<()> {
1071        for domain in &self.local_domains {
1072            if domain.is_empty() {
1073                return Err(anyhow::anyhow!("Domain name cannot be empty"));
1074            }
1075            // Basic validation - should contain at least one dot
1076            if !domain.contains('.') {
1077                return Err(anyhow::anyhow!("Invalid domain name: {}", domain));
1078            }
1079        }
1080        Ok(())
1081    }
1082
1083    /// Validate alias email addresses
1084    pub fn validate_aliases(&self) -> anyhow::Result<()> {
1085        for (from, to) in &self.aliases {
1086            if !from.contains('@') {
1087                return Err(anyhow::anyhow!("Invalid alias source: {}", from));
1088            }
1089            if !to.contains('@') {
1090                return Err(anyhow::anyhow!("Invalid alias destination: {}", to));
1091            }
1092        }
1093        Ok(())
1094    }
1095}
1096
1097/// Metrics configuration
1098#[derive(Debug, Clone, Deserialize, Serialize)]
1099pub struct MetricsConfig {
1100    pub enabled: bool,
1101    pub bind_address: String, // e.g., "0.0.0.0:9090"
1102    pub path: String,         // e.g., "/metrics"
1103}
1104
1105impl MetricsConfig {
1106    /// Validate bind address format
1107    pub fn validate_bind_address(&self) -> anyhow::Result<()> {
1108        if !self.bind_address.contains(':') {
1109            return Err(anyhow::anyhow!(
1110                "Invalid bind address format: {}",
1111                self.bind_address
1112            ));
1113        }
1114        Ok(())
1115    }
1116
1117    /// Validate path format
1118    pub fn validate_path(&self) -> anyhow::Result<()> {
1119        if !self.path.starts_with('/') {
1120            return Err(anyhow::anyhow!(
1121                "Metrics path must start with '/': {}",
1122                self.path
1123            ));
1124        }
1125        Ok(())
1126    }
1127}
1128
1129/// OpenTelemetry tracing configuration
1130#[derive(Debug, Clone, Deserialize, Serialize)]
1131pub struct TracingConfig {
1132    pub enabled: bool,
1133    pub endpoint: String, // e.g., "http://localhost:4317" for gRPC or "http://localhost:4318" for HTTP
1134    pub protocol: OtlpProtocol,
1135    pub service_name: String,
1136    #[serde(default)]
1137    pub sample_ratio: f64, // 0.0 to 1.0, default 1.0 (trace everything)
1138}
1139
1140/// OTLP protocol type
1141#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
1142#[serde(rename_all = "lowercase")]
1143pub enum OtlpProtocol {
1144    Grpc,
1145    Http,
1146}
1147
1148impl Default for TracingConfig {
1149    fn default() -> Self {
1150        Self {
1151            enabled: false,
1152            endpoint: "http://localhost:4317".to_string(),
1153            protocol: OtlpProtocol::Grpc,
1154            service_name: "rusmes".to_string(),
1155            sample_ratio: 1.0,
1156        }
1157    }
1158}
1159
1160impl TracingConfig {
1161    /// Validate endpoint URL format
1162    pub fn validate_endpoint(&self) -> anyhow::Result<()> {
1163        if !self.endpoint.starts_with("http://") && !self.endpoint.starts_with("https://") {
1164            return Err(anyhow::anyhow!(
1165                "Endpoint must start with http:// or https://: {}",
1166                self.endpoint
1167            ));
1168        }
1169        Ok(())
1170    }
1171
1172    /// Validate sample ratio
1173    pub fn validate_sample_ratio(&self) -> anyhow::Result<()> {
1174        if !(0.0..=1.0).contains(&self.sample_ratio) {
1175            return Err(anyhow::anyhow!(
1176                "Sample ratio must be between 0.0 and 1.0: {}",
1177                self.sample_ratio
1178            ));
1179        }
1180        Ok(())
1181    }
1182
1183    /// Validate service name
1184    pub fn validate_service_name(&self) -> anyhow::Result<()> {
1185        if self.service_name.trim().is_empty() {
1186            return Err(anyhow::anyhow!("Service name cannot be empty"));
1187        }
1188        Ok(())
1189    }
1190}
1191
1192/// Connection limits configuration
1193#[derive(Debug, Clone, Deserialize, Serialize)]
1194pub struct ConnectionLimitsConfig {
1195    /// Maximum connections per IP address (0 = unlimited)
1196    #[serde(default = "default_max_connections_per_ip")]
1197    pub max_connections_per_ip: usize,
1198    /// Maximum total connections (0 = unlimited)
1199    #[serde(default = "default_max_total_connections")]
1200    pub max_total_connections: usize,
1201    /// Idle timeout for connections (e.g., "300s", "5m")
1202    #[serde(default = "default_idle_timeout")]
1203    pub idle_timeout: String,
1204    /// Reaper interval for cleaning up idle connections (e.g., "60s", "1m")
1205    #[serde(default = "default_reaper_interval")]
1206    pub reaper_interval: String,
1207}
1208
1209fn default_max_total_connections() -> usize {
1210    1000
1211}
1212
1213fn default_idle_timeout() -> String {
1214    "300s".to_string()
1215}
1216
1217fn default_reaper_interval() -> String {
1218    "60s".to_string()
1219}
1220
1221impl ConnectionLimitsConfig {
1222    /// Parse idle timeout to seconds
1223    pub fn idle_timeout_seconds(&self) -> anyhow::Result<u64> {
1224        parse_duration(&self.idle_timeout)
1225    }
1226
1227    /// Parse reaper interval to seconds
1228    pub fn reaper_interval_seconds(&self) -> anyhow::Result<u64> {
1229        parse_duration(&self.reaper_interval)
1230    }
1231}
1232
1233/// Parse size string like "50MB", "1GB", "1024KB"
1234fn parse_size(s: &str) -> anyhow::Result<usize> {
1235    let s = s.trim().to_uppercase();
1236
1237    if let Some(rest) = s.strip_suffix("GB") {
1238        let num: f64 = rest.trim().parse()?;
1239        Ok((num * 1024.0 * 1024.0 * 1024.0) as usize)
1240    } else if let Some(rest) = s.strip_suffix("MB") {
1241        let num: f64 = rest.trim().parse()?;
1242        Ok((num * 1024.0 * 1024.0) as usize)
1243    } else if let Some(rest) = s.strip_suffix("KB") {
1244        let num: f64 = rest.trim().parse()?;
1245        Ok((num * 1024.0) as usize)
1246    } else if let Some(rest) = s.strip_suffix("B") {
1247        let num: usize = rest.trim().parse()?;
1248        Ok(num)
1249    } else {
1250        // Assume bytes
1251        let num: usize = s.parse()?;
1252        Ok(num)
1253    }
1254}
1255
1256/// Parse duration string like "60s", "30m", "1h"
1257fn parse_duration(s: &str) -> anyhow::Result<u64> {
1258    let s = s.trim().to_lowercase();
1259
1260    if let Some(rest) = s.strip_suffix('h') {
1261        let num: u64 = rest.trim().parse()?;
1262        Ok(num * 3600)
1263    } else if let Some(rest) = s.strip_suffix('m') {
1264        let num: u64 = rest.trim().parse()?;
1265        Ok(num * 60)
1266    } else if let Some(rest) = s.strip_suffix('s') {
1267        let num: u64 = rest.trim().parse()?;
1268        Ok(num)
1269    } else {
1270        // Assume seconds
1271        let num: u64 = s.parse()?;
1272        Ok(num)
1273    }
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278    use super::*;
1279
1280    #[test]
1281    fn test_parse_size() {
1282        assert_eq!(parse_size("1024").unwrap(), 1024);
1283        assert_eq!(parse_size("1KB").unwrap(), 1024);
1284        assert_eq!(parse_size("1MB").unwrap(), 1024 * 1024);
1285        assert_eq!(parse_size("1GB").unwrap(), 1024 * 1024 * 1024);
1286        assert_eq!(parse_size("50MB").unwrap(), 50 * 1024 * 1024);
1287    }
1288
1289    #[test]
1290    fn test_parse_duration() {
1291        assert_eq!(parse_duration("60").unwrap(), 60);
1292        assert_eq!(parse_duration("60s").unwrap(), 60);
1293        assert_eq!(parse_duration("5m").unwrap(), 300);
1294        assert_eq!(parse_duration("1h").unwrap(), 3600);
1295        assert_eq!(parse_duration("2h").unwrap(), 7200);
1296    }
1297
1298    #[test]
1299    fn test_parse_toml_config() {
1300        let toml_str = r#"
1301            domain = "example.com"
1302            postmaster = "postmaster@example.com"
1303
1304            [smtp]
1305            host = "0.0.0.0"
1306            port = 25
1307            tls_port = 587
1308            max_message_size = "50MB"
1309
1310            [storage]
1311            backend = "filesystem"
1312            path = "/var/mail"
1313
1314            [[processors]]
1315            name = "root"
1316            state = "root"
1317
1318            [[processors.mailets]]
1319            matcher = "All"
1320            mailet = "LocalDelivery"
1321        "#;
1322
1323        let config: ServerConfig = toml::from_str(toml_str).unwrap();
1324        assert_eq!(config.domain, "example.com");
1325        assert_eq!(config.smtp.port, 25);
1326        assert_eq!(config.processors.len(), 1);
1327        assert_eq!(config.processors[0].mailets.len(), 1);
1328    }
1329
1330    #[test]
1331    fn test_parse_auth_config() {
1332        let toml_str = r#"
1333            backend = "file"
1334            path = "/etc/rusmes/users.db"
1335        "#;
1336
1337        let config: AuthConfig = toml::from_str(toml_str).unwrap();
1338        match config {
1339            AuthConfig::File { config } => {
1340                assert_eq!(config.path, "/etc/rusmes/users.db");
1341            }
1342            _ => panic!("Expected File auth backend"),
1343        }
1344    }
1345
1346    #[test]
1347    fn test_parse_logging_config() {
1348        let toml_str = r#"
1349            level = "info"
1350            format = "json"
1351            output = "stdout"
1352        "#;
1353
1354        let config: LoggingConfig = toml::from_str(toml_str).unwrap();
1355        assert_eq!(config.level, "info");
1356        assert_eq!(config.format, "json");
1357        assert_eq!(config.output, "stdout");
1358        config.validate_level().unwrap();
1359        config.validate_format().unwrap();
1360    }
1361
1362    #[test]
1363    fn test_parse_queue_config() {
1364        let toml_str = r#"
1365            initial_delay = "60s"
1366            max_delay = "3600s"
1367            backoff_multiplier = 2.0
1368            max_attempts = 5
1369            worker_threads = 5
1370            batch_size = 100
1371        "#;
1372
1373        let config: QueueConfig = toml::from_str(toml_str).unwrap();
1374        assert_eq!(config.initial_delay_seconds().unwrap(), 60);
1375        assert_eq!(config.max_delay_seconds().unwrap(), 3600);
1376        assert_eq!(config.backoff_multiplier, 2.0);
1377        assert_eq!(config.max_attempts, 5);
1378        assert_eq!(config.worker_threads, 5);
1379        assert_eq!(config.batch_size, 100);
1380        config.validate_backoff_multiplier().unwrap();
1381        config.validate_worker_threads().unwrap();
1382    }
1383
1384    #[test]
1385    fn test_parse_security_config() {
1386        let toml_str = r#"
1387            relay_networks = ["127.0.0.0/8", "10.0.0.0/8"]
1388            blocked_ips = ["192.0.2.1", "2001:db8::1"]
1389            check_recipient_exists = true
1390            reject_unknown_recipients = true
1391        "#;
1392
1393        let config: SecurityConfig = toml::from_str(toml_str).unwrap();
1394        assert_eq!(config.relay_networks.len(), 2);
1395        assert_eq!(config.blocked_ips.len(), 2);
1396        assert!(config.check_recipient_exists);
1397        assert!(config.reject_unknown_recipients);
1398        config.validate_relay_networks().unwrap();
1399        config.validate_blocked_ips().unwrap();
1400    }
1401
1402    #[test]
1403    fn test_parse_domains_config() {
1404        let toml_str = r#"
1405            local_domains = ["example.com", "mail.example.com"]
1406
1407            [aliases]
1408            "abuse@example.com" = "postmaster@example.com"
1409            "webmaster@example.com" = "admin@example.com"
1410        "#;
1411
1412        let config: DomainsConfig = toml::from_str(toml_str).unwrap();
1413        assert_eq!(config.local_domains.len(), 2);
1414        assert_eq!(config.aliases.len(), 2);
1415        assert_eq!(
1416            config.aliases.get("abuse@example.com"),
1417            Some(&"postmaster@example.com".to_string())
1418        );
1419        config.validate_local_domains().unwrap();
1420        config.validate_aliases().unwrap();
1421    }
1422
1423    #[test]
1424    fn test_parse_metrics_config() {
1425        let toml_str = r#"
1426            enabled = true
1427            bind_address = "0.0.0.0:9090"
1428            path = "/metrics"
1429        "#;
1430
1431        let config: MetricsConfig = toml::from_str(toml_str).unwrap();
1432        assert!(config.enabled);
1433        assert_eq!(config.bind_address, "0.0.0.0:9090");
1434        assert_eq!(config.path, "/metrics");
1435        config.validate_bind_address().unwrap();
1436        config.validate_path().unwrap();
1437    }
1438
1439    #[test]
1440    fn test_parse_rate_limit_config() {
1441        let toml_str = r#"
1442            max_messages_per_hour = 100
1443            window_duration = "1h"
1444        "#;
1445
1446        let config: RateLimitConfig = toml::from_str(toml_str).unwrap();
1447        assert_eq!(config.max_messages_per_hour, 100);
1448        assert_eq!(config.window_duration_seconds().unwrap(), 3600);
1449    }
1450
1451    #[test]
1452    fn test_parse_full_config_with_all_sections() {
1453        let toml_str = r#"
1454            domain = "mail.example.com"
1455            postmaster = "postmaster@example.com"
1456
1457            [smtp]
1458            host = "0.0.0.0"
1459            port = 25
1460            tls_port = 587
1461            max_message_size = "50MB"
1462
1463            [smtp.rate_limit]
1464            max_messages_per_hour = 100
1465            window_duration = "1h"
1466
1467            [storage]
1468            backend = "filesystem"
1469            path = "/var/mail"
1470
1471            [auth]
1472            backend = "file"
1473            path = "/etc/rusmes/users.db"
1474
1475            [logging]
1476            level = "info"
1477            format = "json"
1478            output = "stdout"
1479
1480            [queue]
1481            initial_delay = "60s"
1482            max_delay = "3600s"
1483            backoff_multiplier = 2.0
1484            max_attempts = 5
1485            worker_threads = 5
1486            batch_size = 100
1487
1488            [security]
1489            relay_networks = ["127.0.0.0/8"]
1490            blocked_ips = []
1491            check_recipient_exists = true
1492            reject_unknown_recipients = true
1493
1494            [domains]
1495            local_domains = ["example.com"]
1496
1497            [domains.aliases]
1498            "abuse@example.com" = "postmaster@example.com"
1499
1500            [metrics]
1501            enabled = true
1502            bind_address = "0.0.0.0:9090"
1503            path = "/metrics"
1504
1505            [[processors]]
1506            name = "root"
1507            state = "root"
1508
1509            [[processors.mailets]]
1510            matcher = "All"
1511            mailet = "LocalDelivery"
1512        "#;
1513
1514        let config: ServerConfig = toml::from_str(toml_str).unwrap();
1515        assert_eq!(config.domain, "mail.example.com");
1516        assert!(config.auth.is_some());
1517        assert!(config.logging.is_some());
1518        assert!(config.queue.is_some());
1519        assert!(config.security.is_some());
1520        assert!(config.domains.is_some());
1521        assert!(config.metrics.is_some());
1522        assert!(config.smtp.rate_limit.is_some());
1523
1524        // Validate sections
1525        if let Some(logging) = &config.logging {
1526            logging.validate_level().unwrap();
1527            logging.validate_format().unwrap();
1528        }
1529
1530        if let Some(queue) = &config.queue {
1531            queue.validate_backoff_multiplier().unwrap();
1532            queue.validate_worker_threads().unwrap();
1533        }
1534
1535        if let Some(security) = &config.security {
1536            security.validate_relay_networks().unwrap();
1537            security.validate_blocked_ips().unwrap();
1538        }
1539
1540        if let Some(domains) = &config.domains {
1541            domains.validate_local_domains().unwrap();
1542            domains.validate_aliases().unwrap();
1543        }
1544
1545        if let Some(metrics) = &config.metrics {
1546            metrics.validate_bind_address().unwrap();
1547            metrics.validate_path().unwrap();
1548        }
1549    }
1550
1551    #[test]
1552    fn test_parse_yaml_config() {
1553        let yaml_str = r#"
1554domain: example.com
1555postmaster: postmaster@example.com
1556
1557smtp:
1558  host: 0.0.0.0
1559  port: 25
1560  tls_port: 587
1561  max_message_size: 50MB
1562
1563storage:
1564  backend: filesystem
1565  path: /var/mail
1566
1567processors:
1568  - name: root
1569    state: root
1570    mailets:
1571      - matcher: All
1572        mailet: LocalDelivery
1573        params: {}
1574        "#;
1575
1576        let config: ServerConfig = serde_yaml::from_str(yaml_str).unwrap();
1577        assert_eq!(config.domain, "example.com");
1578        assert_eq!(config.smtp.port, 25);
1579        assert_eq!(config.processors.len(), 1);
1580        assert_eq!(config.processors[0].mailets.len(), 1);
1581    }
1582
1583    #[test]
1584    fn test_yaml_equivalence_to_toml() {
1585        // YAML version
1586        let yaml_str = r#"
1587domain: mail.example.com
1588postmaster: postmaster@example.com
1589
1590smtp:
1591  host: 0.0.0.0
1592  port: 25
1593  tls_port: 587
1594  max_message_size: 50MB
1595  require_auth: true
1596  enable_starttls: true
1597
1598storage:
1599  backend: filesystem
1600  path: /var/mail
1601
1602processors:
1603  - name: root
1604    state: root
1605    mailets:
1606      - matcher: All
1607        mailet: LocalDelivery
1608
1609auth:
1610  backend: file
1611  path: /etc/rusmes/users.db
1612
1613logging:
1614  level: info
1615  format: json
1616  output: stdout
1617
1618domains:
1619  local_domains:
1620    - example.com
1621    - mail.example.com
1622        "#;
1623
1624        // TOML version
1625        let toml_str = r#"
1626domain = "mail.example.com"
1627postmaster = "postmaster@example.com"
1628
1629[smtp]
1630host = "0.0.0.0"
1631port = 25
1632tls_port = 587
1633max_message_size = "50MB"
1634require_auth = true
1635enable_starttls = true
1636
1637[storage]
1638backend = "filesystem"
1639path = "/var/mail"
1640
1641[[processors]]
1642name = "root"
1643state = "root"
1644
1645[[processors.mailets]]
1646matcher = "All"
1647mailet = "LocalDelivery"
1648
1649[auth]
1650backend = "file"
1651path = "/etc/rusmes/users.db"
1652
1653[logging]
1654level = "info"
1655format = "json"
1656output = "stdout"
1657
1658[domains]
1659local_domains = ["example.com", "mail.example.com"]
1660        "#;
1661
1662        let yaml_config: ServerConfig = serde_yaml::from_str(yaml_str).unwrap();
1663        let toml_config: ServerConfig = toml::from_str(toml_str).unwrap();
1664
1665        // Verify both configs are equivalent
1666        assert_eq!(yaml_config.domain, toml_config.domain);
1667        assert_eq!(yaml_config.postmaster, toml_config.postmaster);
1668        assert_eq!(yaml_config.smtp.host, toml_config.smtp.host);
1669        assert_eq!(yaml_config.smtp.port, toml_config.smtp.port);
1670        assert_eq!(yaml_config.smtp.tls_port, toml_config.smtp.tls_port);
1671        assert_eq!(
1672            yaml_config.smtp.max_message_size,
1673            toml_config.smtp.max_message_size
1674        );
1675        assert_eq!(yaml_config.smtp.require_auth, toml_config.smtp.require_auth);
1676        assert_eq!(
1677            yaml_config.smtp.enable_starttls,
1678            toml_config.smtp.enable_starttls
1679        );
1680        assert_eq!(yaml_config.processors.len(), toml_config.processors.len());
1681
1682        // Check auth config
1683        assert!(yaml_config.auth.is_some());
1684        assert!(toml_config.auth.is_some());
1685
1686        // Check logging config
1687        if let (Some(yaml_log), Some(toml_log)) = (&yaml_config.logging, &toml_config.logging) {
1688            assert_eq!(yaml_log.level, toml_log.level);
1689            assert_eq!(yaml_log.format, toml_log.format);
1690            assert_eq!(yaml_log.output, toml_log.output);
1691        }
1692
1693        // Check domains config
1694        if let (Some(yaml_domains), Some(toml_domains)) =
1695            (&yaml_config.domains, &toml_config.domains)
1696        {
1697            assert_eq!(
1698                yaml_domains.local_domains.len(),
1699                toml_domains.local_domains.len()
1700            );
1701            assert_eq!(yaml_domains.local_domains, toml_domains.local_domains);
1702        }
1703    }
1704}