mod env_overrides;
mod listeners;
pub mod logging;
mod parse;
pub mod performance;
mod runtime;
pub mod tls;
mod unknown_keys;
mod validation;
use rusmes_proto::MailAddress;
use serde::{Deserialize, Serialize};
use std::path::Path;
use unknown_keys::collect_unknown_toml_keys;
use validation::{
validate_domain, validate_email, validate_port, validate_processors, validate_storage_path,
};
pub use listeners::{
ConnectionLimitsConfig, ImapServerConfig, JmapPushConfig, JmapServerConfig, Pop3ServerConfig,
RateLimitConfig, RelayConfig, SmtpOutboundConfig, SmtpServerConfig,
};
pub use performance::PerformanceConfig;
pub use runtime::{
AuthConfig, DomainsConfig, FileAuthConfig, LdapAuthConfig, LogFileConfig, LoggingConfig,
MailetConfig, MetricsBasicAuthConfig, MetricsConfig, OAuth2AuthConfig, OtlpProtocol,
ProcessorConfig, QueueConfig, SecurityConfig, SqlAuthConfig, StorageConfig, TracingConfig,
};
pub use tls::{ClientAuthMode, ProtocolKind, TlsConfig, TlsEndpointConfig};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerConfig {
pub domain: String,
pub postmaster: String,
pub smtp: SmtpServerConfig,
pub imap: Option<ImapServerConfig>,
pub jmap: Option<JmapServerConfig>,
pub pop3: Option<Pop3ServerConfig>,
pub storage: StorageConfig,
pub processors: Vec<ProcessorConfig>,
#[serde(default = "default_runtime_dir")]
pub runtime_dir: String,
#[serde(default)]
pub relay: Option<RelayConfig>,
#[serde(default)]
pub auth: Option<AuthConfig>,
#[serde(default)]
pub logging: Option<LoggingConfig>,
#[serde(default)]
pub queue: Option<QueueConfig>,
#[serde(default)]
pub security: Option<SecurityConfig>,
#[serde(default)]
pub domains: Option<DomainsConfig>,
#[serde(default)]
pub metrics: Option<MetricsConfig>,
#[serde(default)]
pub tracing: Option<TracingConfig>,
#[serde(default)]
pub connection_limits: Option<ConnectionLimitsConfig>,
#[serde(default)]
pub performance: PerformanceConfig,
#[serde(default)]
pub tls: Option<TlsConfig>,
#[serde(default)]
pub chroot: bool,
#[serde(default)]
pub run_as_user: String,
#[serde(default)]
pub run_as_group: String,
#[serde(skip)]
pub extra: Vec<String>,
}
fn default_runtime_dir() -> String {
"/var/run/rusmes".to_string()
}
impl ServerConfig {
pub fn from_file(path: impl AsRef<Path>) -> anyhow::Result<Self> {
let path = path.as_ref();
let content = std::fs::read_to_string(path)?;
let mut config: ServerConfig = match path.extension().and_then(|ext| ext.to_str()) {
Some("yaml") | Some("yml") => serde_yaml::from_str(&content)?,
Some("toml") => {
let raw: toml::Value = toml::from_str(&content)?;
let unknown = collect_unknown_toml_keys(&raw);
let mut cfg: ServerConfig = toml::from_str(&content)?;
cfg.extra = unknown;
cfg
}
Some(ext) => {
return Err(anyhow::anyhow!(
"Unsupported configuration file extension: .{}. Use .toml, .yaml, or .yml",
ext
));
}
None => {
return Err(anyhow::anyhow!(
"Configuration file must have a .toml, .yaml, or .yml extension"
));
}
};
config.apply_env_overrides();
config.warn_unknown_keys();
config.validate()?;
Ok(config)
}
pub fn validate(&self) -> anyhow::Result<()> {
validate_domain(&self.domain)
.map_err(|e| anyhow::anyhow!("Invalid server domain: {}", e))?;
validate_email(&self.postmaster)
.map_err(|e| anyhow::anyhow!("Invalid postmaster email: {}", e))?;
validate_port(self.smtp.port, "SMTP port")?;
if let Some(tls_port) = self.smtp.tls_port {
validate_port(tls_port, "SMTP TLS port")?;
}
if let Some(ref imap) = self.imap {
validate_port(imap.port, "IMAP port")?;
if let Some(tls_port) = imap.tls_port {
validate_port(tls_port, "IMAP TLS port")?;
}
}
if let Some(ref jmap) = self.jmap {
validate_port(jmap.port, "JMAP port")?;
}
if let Some(ref pop3) = self.pop3 {
validate_port(pop3.port, "POP3 port")?;
if let Some(tls_port) = pop3.tls_port {
validate_port(tls_port, "POP3 TLS port")?;
}
}
match &self.storage {
StorageConfig::Filesystem { path } => {
validate_storage_path(path)?;
}
StorageConfig::Postgres { connection_string } => {
if connection_string.is_empty() {
anyhow::bail!("Postgres connection string cannot be empty");
}
}
StorageConfig::AmateRS {
endpoints,
replication_factor,
} => {
if endpoints.is_empty() {
anyhow::bail!("AmateRS endpoints cannot be empty");
}
if *replication_factor == 0 {
anyhow::bail!("AmateRS replication factor must be greater than 0");
}
}
}
validate_processors(&self.processors)?;
if let Some(ref domains) = self.domains {
for domain in &domains.local_domains {
validate_domain(domain)
.map_err(|e| anyhow::anyhow!("Invalid local domain '{}': {}", domain, e))?;
}
for (from, to) in &domains.aliases {
validate_email(from)
.map_err(|e| anyhow::anyhow!("Invalid alias source '{}': {}", from, e))?;
validate_email(to)
.map_err(|e| anyhow::anyhow!("Invalid alias destination '{}': {}", to, e))?;
}
}
if let Some(ref logging) = self.logging {
logging.validate_level()?;
logging.validate_format()?;
}
if let Some(ref queue) = self.queue {
queue.validate_backoff_multiplier()?;
queue.validate_worker_threads()?;
}
if let Some(ref security) = self.security {
security.validate_relay_networks()?;
security.validate_blocked_ips()?;
}
if let Some(ref metrics) = self.metrics {
metrics.validate_bind_address()?;
metrics.validate_path()?;
}
self.performance.validate()?;
if let Some(ref tls) = self.tls {
tls.validate()?;
}
Ok(())
}
pub fn postmaster_address(&self) -> anyhow::Result<MailAddress> {
self.postmaster
.parse()
.map_err(|e| anyhow::anyhow!("Invalid postmaster address: {}", e))
}
pub fn tls_for_protocol(&self, proto: ProtocolKind) -> Option<&TlsEndpointConfig> {
self.tls.as_ref().map(|t| t.tls_for_protocol(proto))
}
pub fn warn_unknown_keys(&self) {
for key in &self.extra {
tracing::warn!(
"unknown configuration key '{}' will be ignored; check your config file for typos",
key
);
}
}
}