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
69mod env_overrides;
70mod listeners;
71pub mod logging;
72mod parse;
73pub mod performance;
74mod runtime;
75pub mod tls;
76mod unknown_keys;
77mod validation;
78
79use rusmes_proto::MailAddress;
80use serde::{Deserialize, Serialize};
81use std::path::Path;
82use unknown_keys::collect_unknown_toml_keys;
83use validation::{
84 validate_domain, validate_email, validate_port, validate_processors, validate_storage_path,
85};
86
87// Re-export all public types from sub-modules so downstream crates see no change.
88pub use listeners::{
89 ConnectionLimitsConfig, ImapServerConfig, JmapPushConfig, JmapServerConfig, Pop3ServerConfig,
90 RateLimitConfig, RelayConfig, SmtpOutboundConfig, SmtpServerConfig,
91};
92pub use performance::PerformanceConfig;
93pub use runtime::{
94 AuthConfig, DomainsConfig, FileAuthConfig, LdapAuthConfig, LogFileConfig, LoggingConfig,
95 MailetConfig, MetricsBasicAuthConfig, MetricsConfig, OAuth2AuthConfig, OtlpProtocol,
96 ProcessorConfig, QueueConfig, SecurityConfig, SqlAuthConfig, StorageConfig, TracingConfig,
97};
98pub use tls::{ClientAuthMode, ProtocolKind, TlsConfig, TlsEndpointConfig};
99
100/// Main server configuration.
101///
102/// Loaded from a TOML or YAML file via [`ServerConfig::from_file`].
103/// All optional sections default to `None`; required fields (`domain`,
104/// `postmaster`, `smtp`, `storage`, `processors`) must be present.
105#[derive(Debug, Clone, Deserialize, Serialize)]
106pub struct ServerConfig {
107 /// Required. Primary mail domain served by this RusMES installation
108 /// (e.g. `"mail.example.com"`). Must be a syntactically valid domain name.
109 pub domain: String,
110
111 /// Required. RFC 5321 postmaster email address (e.g. `"postmaster@example.com"`).
112 /// Used as the envelope sender for system-generated bounce messages.
113 pub postmaster: String,
114
115 /// Required. SMTP listener configuration (host, port, TLS port, size limits).
116 pub smtp: SmtpServerConfig,
117
118 /// Default: `None`. IMAP4rev1 listener configuration. When absent the IMAP
119 /// service is not started.
120 pub imap: Option<ImapServerConfig>,
121
122 /// Default: `None`. JMAP HTTP listener configuration. When absent the JMAP
123 /// service is not started.
124 pub jmap: Option<JmapServerConfig>,
125
126 /// Default: `None`. POP3 listener configuration. When absent the POP3
127 /// service is not started.
128 pub pop3: Option<Pop3ServerConfig>,
129
130 /// Required. Mail storage backend (filesystem, PostgreSQL, or AmateRS).
131 pub storage: StorageConfig,
132
133 /// Required. Ordered list of processor chains. At least one processor
134 /// named `"root"` must be present.
135 pub processors: Vec<ProcessorConfig>,
136
137 /// Default: `"/var/run/rusmes"`. Per-process runtime directory used for
138 /// the PID file, the rate-limiter snapshot, and any other ephemeral state
139 /// files. Must be writable by the user running `rusmes-server`.
140 #[serde(default = "default_runtime_dir")]
141 pub runtime_dir: String,
142
143 /// Default: `None`. Outbound SMTP relay configuration. When absent,
144 /// rusmes delivers directly via DNS MX lookup.
145 #[serde(default)]
146 pub relay: Option<RelayConfig>,
147
148 /// Default: `None`. Authentication backend configuration (file, LDAP,
149 /// SQL, or OAuth2). When absent the server falls back to no-auth mode.
150 #[serde(default)]
151 pub auth: Option<AuthConfig>,
152
153 /// Default: `None`. Logging configuration (level, format, output, file
154 /// rotation). When absent the server logs `info`-level messages to stdout
155 /// in text format.
156 #[serde(default)]
157 pub logging: Option<LoggingConfig>,
158
159 /// Default: `None`. Outbound queue configuration (retry delays, back-off).
160 /// When absent, reasonable built-in defaults are used.
161 #[serde(default)]
162 pub queue: Option<QueueConfig>,
163
164 /// Default: `None`. Security configuration (relay networks, blocked IPs,
165 /// recipient validation). When absent all security checks are disabled.
166 #[serde(default)]
167 pub security: Option<SecurityConfig>,
168
169 /// Default: `None`. Local domain and alias mapping configuration.
170 /// When absent only the primary `domain` is considered local.
171 #[serde(default)]
172 pub domains: Option<DomainsConfig>,
173
174 /// Default: `None`. Prometheus metrics endpoint configuration.
175 /// When absent the `/metrics` endpoint is not exposed.
176 #[serde(default)]
177 pub metrics: Option<MetricsConfig>,
178
179 /// Default: `None`. OpenTelemetry OTLP tracing configuration.
180 /// When absent distributed tracing is disabled.
181 #[serde(default)]
182 pub tracing: Option<TracingConfig>,
183
184 /// Default: `None`. Per-IP and global connection limit configuration.
185 /// When absent no connection caps are enforced.
186 #[serde(default)]
187 pub connection_limits: Option<ConnectionLimitsConfig>,
188
189 /// Default: `PerformanceConfig::default()`. Runtime performance tuning:
190 /// Tokio worker threads, connection pool sizes, and per-connection buffer
191 /// sizes. Omitting `[performance]` uses conservative built-in defaults.
192 #[serde(default)]
193 pub performance: PerformanceConfig,
194
195 /// Default: `None`. TLS certificate and key paths. Supports a shared
196 /// `[tls.default]` endpoint and optional per-protocol overrides
197 /// (`[tls.smtp]`, `[tls.imap]`, `[tls.pop3]`, `[tls.jmap]`).
198 #[serde(default)]
199 pub tls: Option<TlsConfig>,
200
201 /// Default: `false`. When `true`, call `chroot(runtime_dir)` after binding
202 /// all sockets and loading TLS material, before dropping privileges.
203 /// Has effect only on Linux; on other platforms a `tracing::warn!` is
204 /// emitted and this field is otherwise ignored.
205 #[serde(default)]
206 pub chroot: bool,
207
208 /// Default: `""` (no-op). System user name to `setuid` to after binding
209 /// all sockets. The empty string means "do not change UID". Only
210 /// effective on Linux; ignored on other platforms (with a warning).
211 #[serde(default)]
212 pub run_as_user: String,
213
214 /// Default: `""` (no-op). System group name to `setgid` to after binding
215 /// all sockets. The empty string means "do not change GID". Only
216 /// effective on Linux; ignored on other platforms (with a warning).
217 #[serde(default)]
218 pub run_as_group: String,
219
220 /// Unknown TOML/YAML keys captured for diagnostic warnings.
221 ///
222 /// Not serialized to output. Populated by [`ServerConfig::from_file`]
223 /// via a two-phase parse (raw `toml::Value` → known-key diff) so that
224 /// `warn_unknown_keys` can emit [`tracing::warn!`] for each entry.
225 /// Exposed as `pub` so tests can assert on which keys were captured
226 /// without relying on subscriber interception.
227 #[serde(skip)]
228 pub extra: Vec<String>,
229}
230
231/// Default runtime directory used when `runtime_dir` is omitted from the
232/// configuration file. This path is conventionally writable by the user
233/// running `rusmes-server`.
234fn default_runtime_dir() -> String {
235 "/var/run/rusmes".to_string()
236}
237
238impl ServerConfig {
239 /// Load configuration from a TOML or YAML file.
240 ///
241 /// The format is auto-detected based on file extension:
242 /// - `.toml` files are parsed as TOML
243 /// - `.yaml` or `.yml` files are parsed as YAML
244 pub fn from_file(path: impl AsRef<Path>) -> anyhow::Result<Self> {
245 let path = path.as_ref();
246 let content = std::fs::read_to_string(path)?;
247
248 // Auto-detect format based on file extension
249 let mut config: ServerConfig = match path.extension().and_then(|ext| ext.to_str()) {
250 Some("yaml") | Some("yml") => serde_yaml::from_str(&content)?,
251 Some("toml") => {
252 // Two-phase: first parse to raw Value so we can detect
253 // unknown top-level keys, then deserialize into the struct.
254 let raw: toml::Value = toml::from_str(&content)?;
255 let unknown = collect_unknown_toml_keys(&raw);
256 let mut cfg: ServerConfig = toml::from_str(&content)?;
257 cfg.extra = unknown;
258 cfg
259 }
260 Some(ext) => {
261 return Err(anyhow::anyhow!(
262 "Unsupported configuration file extension: .{}. Use .toml, .yaml, or .yml",
263 ext
264 ));
265 }
266 None => {
267 return Err(anyhow::anyhow!(
268 "Configuration file must have a .toml, .yaml, or .yml extension"
269 ));
270 }
271 };
272
273 // Apply environment variable overrides
274 config.apply_env_overrides();
275
276 // Warn about unknown keys before validation so operators get actionable
277 // output even when validation subsequently fails.
278 config.warn_unknown_keys();
279
280 // Validate configuration
281 config.validate()?;
282
283 Ok(config)
284 }
285
286 /// Validate the entire configuration.
287 ///
288 /// This method is called automatically when loading configuration from a file.
289 /// It validates:
290 /// - Domain name format
291 /// - Postmaster email address
292 /// - Port numbers for SMTP, IMAP, JMAP
293 /// - Storage path accessibility
294 /// - Processor uniqueness
295 /// - Local domain names (if configured)
296 pub fn validate(&self) -> anyhow::Result<()> {
297 // Validate main domain
298 validate_domain(&self.domain)
299 .map_err(|e| anyhow::anyhow!("Invalid server domain: {}", e))?;
300
301 // Validate postmaster email
302 validate_email(&self.postmaster)
303 .map_err(|e| anyhow::anyhow!("Invalid postmaster email: {}", e))?;
304
305 // Validate SMTP configuration
306 validate_port(self.smtp.port, "SMTP port")?;
307 if let Some(tls_port) = self.smtp.tls_port {
308 validate_port(tls_port, "SMTP TLS port")?;
309 }
310
311 // Validate IMAP configuration
312 if let Some(ref imap) = self.imap {
313 validate_port(imap.port, "IMAP port")?;
314 if let Some(tls_port) = imap.tls_port {
315 validate_port(tls_port, "IMAP TLS port")?;
316 }
317 }
318
319 // Validate JMAP configuration
320 if let Some(ref jmap) = self.jmap {
321 validate_port(jmap.port, "JMAP port")?;
322 }
323
324 // Validate POP3 configuration
325 if let Some(ref pop3) = self.pop3 {
326 validate_port(pop3.port, "POP3 port")?;
327 if let Some(tls_port) = pop3.tls_port {
328 validate_port(tls_port, "POP3 TLS port")?;
329 }
330 }
331
332 // Validate storage path
333 match &self.storage {
334 StorageConfig::Filesystem { path } => {
335 validate_storage_path(path)?;
336 }
337 StorageConfig::Postgres { connection_string } => {
338 if connection_string.is_empty() {
339 anyhow::bail!("Postgres connection string cannot be empty");
340 }
341 }
342 StorageConfig::AmateRS {
343 endpoints,
344 replication_factor,
345 } => {
346 if endpoints.is_empty() {
347 anyhow::bail!("AmateRS endpoints cannot be empty");
348 }
349 if *replication_factor == 0 {
350 anyhow::bail!("AmateRS replication factor must be greater than 0");
351 }
352 }
353 }
354
355 // Validate processors
356 validate_processors(&self.processors)?;
357
358 // Validate local domains if configured
359 if let Some(ref domains) = self.domains {
360 for domain in &domains.local_domains {
361 validate_domain(domain)
362 .map_err(|e| anyhow::anyhow!("Invalid local domain '{}': {}", domain, e))?;
363 }
364
365 // Validate aliases
366 for (from, to) in &domains.aliases {
367 validate_email(from)
368 .map_err(|e| anyhow::anyhow!("Invalid alias source '{}': {}", from, e))?;
369 validate_email(to)
370 .map_err(|e| anyhow::anyhow!("Invalid alias destination '{}': {}", to, e))?;
371 }
372 }
373
374 // Validate logging configuration
375 if let Some(ref logging) = self.logging {
376 logging.validate_level()?;
377 logging.validate_format()?;
378 }
379
380 // Validate queue configuration
381 if let Some(ref queue) = self.queue {
382 queue.validate_backoff_multiplier()?;
383 queue.validate_worker_threads()?;
384 }
385
386 // Validate security configuration
387 if let Some(ref security) = self.security {
388 security.validate_relay_networks()?;
389 security.validate_blocked_ips()?;
390 }
391
392 // Validate metrics configuration
393 if let Some(ref metrics) = self.metrics {
394 metrics.validate_bind_address()?;
395 metrics.validate_path()?;
396 }
397
398 // Validate performance configuration
399 self.performance.validate()?;
400
401 // Validate TLS configuration (if present)
402 if let Some(ref tls) = self.tls {
403 tls.validate()?;
404 }
405
406 Ok(())
407 }
408
409 /// Get postmaster address.
410 pub fn postmaster_address(&self) -> anyhow::Result<MailAddress> {
411 self.postmaster
412 .parse()
413 .map_err(|e| anyhow::anyhow!("Invalid postmaster address: {}", e))
414 }
415
416 /// Return the [`TlsEndpointConfig`] for `proto`, or `None` if no TLS is
417 /// configured.
418 ///
419 /// Delegates to [`TlsConfig::tls_for_protocol`] which returns the
420 /// per-protocol override when present and falls back to `tls.default`.
421 pub fn tls_for_protocol(&self, proto: ProtocolKind) -> Option<&TlsEndpointConfig> {
422 self.tls.as_ref().map(|t| t.tls_for_protocol(proto))
423 }
424
425 /// Emit `tracing::warn!` for every unknown top-level configuration key.
426 ///
427 /// Called automatically by [`ServerConfig::from_file`] after
428 /// deserialization. Operators can use the warnings to detect typos or
429 /// stale keys without causing a hard failure.
430 pub fn warn_unknown_keys(&self) {
431 for key in &self.extra {
432 tracing::warn!(
433 "unknown configuration key '{}' will be ignored; check your config file for typos",
434 key
435 );
436 }
437 }
438}