1pub 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#[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 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 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 config.apply_env_overrides();
140
141 config.validate()?;
143
144 Ok(config)
145 }
146
147 pub fn apply_env_overrides(&mut self) {
193 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 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 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 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 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 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 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 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 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 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 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 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 pub fn validate(&self) -> anyhow::Result<()> {
648 validate_domain(&self.domain)
650 .map_err(|e| anyhow::anyhow!("Invalid server domain: {}", e))?;
651
652 validate_email(&self.postmaster)
654 .map_err(|e| anyhow::anyhow!("Invalid postmaster email: {}", e))?;
655
656 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 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 if let Some(ref jmap) = self.jmap {
672 validate_port(jmap.port, "JMAP port")?;
673 }
674
675 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 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(&self.processors)?;
708
709 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 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 if let Some(ref logging) = self.logging {
727 logging.validate_level()?;
728 logging.validate_format()?;
729 }
730
731 if let Some(ref queue) = self.queue {
733 queue.validate_backoff_multiplier()?;
734 queue.validate_worker_threads()?;
735 }
736
737 if let Some(ref security) = self.security {
739 security.validate_relay_networks()?;
740 security.validate_blocked_ips()?;
741 }
742
743 if let Some(ref metrics) = self.metrics {
745 metrics.validate_bind_address()?;
746 metrics.validate_path()?;
747 }
748
749 Ok(())
750 }
751
752 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#[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, #[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 pub fn max_message_size_bytes(&self) -> anyhow::Result<usize> {
779 parse_size(&self.max_message_size)
780 }
781}
782
783#[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#[derive(Debug, Clone, Deserialize, Serialize)]
794pub struct JmapServerConfig {
795 pub host: String,
796 pub port: u16,
797 pub base_url: String,
798}
799
800#[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#[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#[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#[derive(Debug, Clone, Deserialize, Serialize)]
851pub struct ProcessorConfig {
852 pub name: String,
853 pub state: String,
854 pub mailets: Vec<MailetConfig>,
855}
856
857#[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#[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, }
874
875fn default_max_connections_per_ip() -> usize {
876 10
877}
878
879impl RateLimitConfig {
880 pub fn window_duration_seconds(&self) -> anyhow::Result<u64> {
882 parse_duration(&self.window_duration)
883 }
884}
885
886#[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#[derive(Debug, Clone, Deserialize, Serialize)]
914pub struct FileAuthConfig {
915 pub path: String,
916}
917
918#[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#[derive(Debug, Clone, Deserialize, Serialize)]
930pub struct SqlAuthConfig {
931 pub connection_string: String,
932 pub query: String,
933}
934
935#[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#[derive(Debug, Clone, Deserialize, Serialize)]
946pub struct LoggingConfig {
947 pub level: String, pub format: String, pub output: String, #[serde(default)]
951 pub file: Option<LogFileConfig>,
952}
953
954impl LoggingConfig {
955 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 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#[derive(Debug, Clone, Deserialize, Serialize)]
974pub struct LogFileConfig {
975 pub path: String,
976 pub max_size: String, pub max_backups: u32,
978 pub compress: bool,
979}
980
981impl LogFileConfig {
982 pub fn max_size_bytes(&self) -> anyhow::Result<usize> {
984 parse_size(&self.max_size)
985 }
986}
987
988#[derive(Debug, Clone, Deserialize, Serialize)]
990pub struct QueueConfig {
991 pub initial_delay: String, pub max_delay: String, pub backoff_multiplier: f64,
994 pub max_attempts: u32,
995 pub worker_threads: usize,
996 pub batch_size: usize,
997}
998
999impl QueueConfig {
1000 pub fn initial_delay_seconds(&self) -> anyhow::Result<u64> {
1002 parse_duration(&self.initial_delay)
1003 }
1004
1005 pub fn max_delay_seconds(&self) -> anyhow::Result<u64> {
1007 parse_duration(&self.max_delay)
1008 }
1009
1010 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 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#[derive(Debug, Clone, Deserialize, Serialize)]
1029pub struct SecurityConfig {
1030 pub relay_networks: Vec<String>, pub blocked_ips: Vec<String>,
1032 pub check_recipient_exists: bool,
1033 pub reject_unknown_recipients: bool,
1034}
1035
1036impl SecurityConfig {
1037 pub fn validate_relay_networks(&self) -> anyhow::Result<()> {
1039 for network in &self.relay_networks {
1040 if !network.contains('/') {
1042 return Err(anyhow::anyhow!("Invalid CIDR notation: {}", network));
1043 }
1044 }
1045 Ok(())
1046 }
1047
1048 pub fn validate_blocked_ips(&self) -> anyhow::Result<()> {
1050 for ip in &self.blocked_ips {
1051 if !ip.contains('.') && !ip.contains(':') {
1053 return Err(anyhow::anyhow!("Invalid IP address: {}", ip));
1054 }
1055 }
1056 Ok(())
1057 }
1058}
1059
1060#[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 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 if !domain.contains('.') {
1077 return Err(anyhow::anyhow!("Invalid domain name: {}", domain));
1078 }
1079 }
1080 Ok(())
1081 }
1082
1083 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#[derive(Debug, Clone, Deserialize, Serialize)]
1099pub struct MetricsConfig {
1100 pub enabled: bool,
1101 pub bind_address: String, pub path: String, }
1104
1105impl MetricsConfig {
1106 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 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#[derive(Debug, Clone, Deserialize, Serialize)]
1131pub struct TracingConfig {
1132 pub enabled: bool,
1133 pub endpoint: String, pub protocol: OtlpProtocol,
1135 pub service_name: String,
1136 #[serde(default)]
1137 pub sample_ratio: f64, }
1139
1140#[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 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 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 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#[derive(Debug, Clone, Deserialize, Serialize)]
1194pub struct ConnectionLimitsConfig {
1195 #[serde(default = "default_max_connections_per_ip")]
1197 pub max_connections_per_ip: usize,
1198 #[serde(default = "default_max_total_connections")]
1200 pub max_total_connections: usize,
1201 #[serde(default = "default_idle_timeout")]
1203 pub idle_timeout: String,
1204 #[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 pub fn idle_timeout_seconds(&self) -> anyhow::Result<u64> {
1224 parse_duration(&self.idle_timeout)
1225 }
1226
1227 pub fn reaper_interval_seconds(&self) -> anyhow::Result<u64> {
1229 parse_duration(&self.reaper_interval)
1230 }
1231}
1232
1233fn 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 let num: usize = s.parse()?;
1252 Ok(num)
1253 }
1254}
1255
1256fn 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 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 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 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 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 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 assert!(yaml_config.auth.is_some());
1684 assert!(toml_config.auth.is_some());
1685
1686 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 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}