use std::fmt;
use std::path::Path;
use lettre::Address;
use serde::Deserialize;
use thiserror::Error;
#[derive(Clone, Deserialize)]
#[serde(transparent)]
pub struct SecretString(String);
impl SecretString {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn expose(&self) -> &str {
&self.0
}
}
impl fmt::Debug for SecretString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("[REDACTED]")
}
}
impl fmt::Display for SecretString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("[REDACTED]")
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct StatusConfig {
#[serde(default = "default_status_enabled")]
pub enabled: bool,
#[serde(default = "default_status_store")]
pub store: String,
#[serde(default = "default_status_ttl_seconds")]
pub ttl_seconds: u64,
#[serde(default = "default_status_max_records")]
pub max_records: usize,
#[serde(default = "default_status_cleanup_interval_seconds")]
pub cleanup_interval_seconds: u64,
pub db_path: Option<std::path::PathBuf>,
pub redis_url: Option<String>,
}
fn default_status_enabled() -> bool { true }
fn default_status_store() -> String { "memory".into() }
fn default_status_ttl_seconds() -> u64 { 3600 }
fn default_status_max_records() -> usize { 10_000 }
fn default_status_cleanup_interval_seconds() -> u64 { 60 }
#[derive(Debug, Clone, Deserialize)]
pub struct AppConfig {
pub server: ServerConfig,
pub security: SecurityConfig,
pub mail: MailConfig,
pub smtp: SmtpConfig,
#[serde(default)]
pub rate_limit: RateLimitConfig,
#[serde(default)]
pub logging: LoggingConfig,
#[serde(default)]
pub status: StatusConfig,
}
impl Default for StatusConfig {
fn default() -> Self {
Self {
enabled: default_status_enabled(),
store: default_status_store(),
ttl_seconds: default_status_ttl_seconds(),
max_records: default_status_max_records(),
cleanup_interval_seconds: default_status_cleanup_interval_seconds(),
db_path: None,
redis_url: None,
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ServerConfig {
pub bind_address: String,
#[serde(default = "default_max_request_body_bytes")]
pub max_request_body_bytes: usize,
#[serde(default = "default_request_timeout_seconds")]
pub request_timeout_seconds: u64,
#[serde(default = "default_shutdown_timeout_seconds")]
pub shutdown_timeout_seconds: u64,
#[serde(default)]
pub concurrency_limit: usize,
pub tls_cert: Option<std::path::PathBuf>,
pub tls_key: Option<std::path::PathBuf>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SecurityConfig {
#[serde(default = "default_true")]
pub require_auth: bool,
#[serde(default)]
pub trust_proxy_headers: bool,
#[serde(default)]
pub trusted_source_cidrs: Vec<String>,
#[serde(default)]
pub allowed_source_cidrs: Vec<String>,
#[serde(default)]
pub api_keys: Vec<ApiKeyConfig>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ApiKeyConfig {
pub id: String,
pub secret: SecretString,
#[serde(default = "default_true")]
pub enabled: bool,
pub description: Option<String>,
#[serde(default)]
pub allowed_recipient_domains: Vec<String>,
#[serde(default)]
pub allowed_recipients: Vec<String>,
pub rate_limit_per_min: Option<u32>,
#[serde(default)]
pub burst: u32,
pub mask_recipient: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MailConfig {
pub default_from: String,
pub default_from_name: Option<String>,
#[serde(default)]
pub allowed_recipient_domains: Vec<String>,
#[serde(default = "default_max_subject_chars")]
pub max_subject_chars: usize,
#[serde(default = "default_max_body_bytes")]
pub max_body_bytes: usize,
#[serde(default = "default_max_recipients")]
pub max_recipients: usize,
#[serde(default = "default_max_attachments")]
pub max_attachments: usize,
#[serde(default = "default_max_attachment_bytes")]
pub max_attachment_bytes: usize,
#[serde(default = "default_max_bulk_messages")]
pub max_bulk_messages: usize,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SmtpConfig {
#[serde(default = "default_smtp_mode")]
pub mode: String,
#[serde(default = "default_smtp_tls")]
pub tls: String,
#[serde(default = "default_smtp_host")]
pub host: String,
#[serde(default = "default_smtp_port")]
pub port: u16,
#[serde(default = "default_connect_timeout_seconds")]
pub connect_timeout_seconds: u64,
#[serde(default = "default_submission_timeout_seconds")]
pub submission_timeout_seconds: u64,
pub auth_user: Option<String>,
pub auth_password: Option<SecretString>,
#[serde(default = "default_pipe_command")]
pub pipe_command: String,
#[serde(default = "default_bulk_concurrency")]
pub bulk_concurrency: usize,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RateLimitConfig {
#[serde(default = "default_global_per_min")]
pub global_per_min: u32,
#[serde(default = "default_per_ip_per_min")]
pub per_ip_per_min: u32,
#[serde(default = "default_per_key_per_min")]
pub per_key_per_min: u32,
#[serde(default = "default_global_burst")]
pub global_burst: u32,
#[serde(default = "default_per_ip_burst")]
pub per_ip_burst: u32,
#[serde(default = "default_per_key_burst")]
pub per_key_burst: u32,
#[serde(default)]
pub burst_size: u32,
#[serde(default = "default_ip_table_size")]
pub ip_table_size: usize,
}
impl RateLimitConfig {
pub fn effective_global_burst(&self) -> u32 {
if self.global_burst > 0 { self.global_burst }
else if self.burst_size > 0 { self.burst_size }
else { default_global_burst() }
}
pub fn effective_per_ip_burst(&self) -> u32 {
if self.per_ip_burst > 0 { self.per_ip_burst }
else if self.burst_size > 0 { self.burst_size }
else { default_per_ip_burst() }
}
pub fn effective_per_key_burst(&self) -> u32 {
if self.per_key_burst > 0 { self.per_key_burst }
else if self.burst_size > 0 { self.burst_size }
else { default_per_key_burst() }
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct LoggingConfig {
#[serde(default = "default_log_format")]
pub format: String,
#[serde(default = "default_log_level")]
pub level: String,
#[serde(default)]
pub mask_recipient: bool,
}
fn default_max_request_body_bytes() -> usize { 1_048_576 }
fn default_request_timeout_seconds() -> u64 { 30 }
fn default_shutdown_timeout_seconds() -> u64 { 30 }
fn default_true() -> bool { true }
fn default_max_subject_chars() -> usize { 255 }
fn default_max_body_bytes() -> usize { 65_536 }
fn default_smtp_mode() -> String { "smtp".into() }
fn default_smtp_host() -> String { "127.0.0.1".into() }
fn default_smtp_port() -> u16 { 25 }
fn default_connect_timeout_seconds() -> u64 { 5 }
fn default_submission_timeout_seconds() -> u64 { 30 }
fn default_bulk_concurrency() -> usize { 5 }
fn default_global_per_min() -> u32 { 60 }
fn default_per_ip_per_min() -> u32 { 20 }
#[allow(dead_code)]
fn default_burst_size() -> u32 { 5 }
fn default_max_recipients() -> usize { 10 }
fn default_max_attachments() -> usize { 5 }
fn default_max_attachment_bytes() -> usize { 10 * 1024 * 1024 } fn default_pipe_command() -> String { "/usr/sbin/sendmail".into() }
fn default_smtp_tls() -> String { "none".into() }
fn default_global_burst() -> u32 { 10 }
fn default_per_ip_burst() -> u32 { 5 }
fn default_per_key_burst() -> u32 { 5 }
fn default_per_key_per_min() -> u32 { 30 }
fn default_ip_table_size() -> usize { 10_000 }
fn default_log_format() -> String { "text".into() }
fn default_log_level() -> String { "info".into() }
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("cannot read config file: {0}")]
Io(#[from] std::io::Error),
#[error("config parse error: {0}")]
Parse(#[from] toml::de::Error),
#[error("invalid server.bind_address: must be host:port (e.g. 127.0.0.1:8080)")]
InvalidBindAddress,
#[error("invalid mail.default_from: must be a valid email address")]
InvalidDefaultFrom,
#[error("security.require_auth is true but no api_keys are defined")]
NoApiKeys,
#[error("no api_keys entries have enabled = true")]
NoEnabledApiKeys,
#[error("invalid CIDR: {0}")]
InvalidCidr(String),
#[error("configuration error: {0}")]
Validation(String),
#[error("invalid smtp.port: must be 1-65535")]
InvalidSmtpPort,
#[error("invalid rate_limit values: all per_min values must be > 0")]
InvalidRateLimit,
#[error("invalid logging.level: must be trace, debug, info, warn, or error")]
InvalidLogLevel,
#[error("invalid logging.format: must be 'text' or 'json'")]
InvalidLogFormat,
}
pub mod validate;
pub use validate::{load, load_from_str, validate_config};
fn default_max_bulk_messages() -> usize { 10 }