1use std::fmt;
34use std::path::Path;
35
36use lettre::Address;
37use serde::Deserialize;
38use thiserror::Error;
39
40#[derive(Clone, Deserialize)]
49#[serde(transparent)]
50pub struct SecretString(String);
51
52impl SecretString {
53 pub fn new(s: impl Into<String>) -> Self {
54 Self(s.into())
55 }
56
57 pub fn expose(&self) -> &str {
61 &self.0
62 }
63}
64
65impl fmt::Debug for SecretString {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 f.write_str("[REDACTED]")
68 }
69}
70
71impl fmt::Display for SecretString {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 f.write_str("[REDACTED]")
74 }
75}
76
77#[derive(Debug, Clone, Deserialize)]
85pub struct StatusConfig {
86 #[serde(default = "default_status_enabled")]
89 pub enabled: bool,
90 #[serde(default = "default_status_store")]
92 pub store: String,
93 #[serde(default = "default_status_ttl_seconds")]
95 pub ttl_seconds: u64,
96 #[serde(default = "default_status_max_records")]
98 pub max_records: usize,
99 #[serde(default = "default_status_cleanup_interval_seconds")]
101 pub cleanup_interval_seconds: u64,
102 pub db_path: Option<std::path::PathBuf>,
105 pub redis_url: Option<String>,
108}
109
110fn default_status_enabled() -> bool { true }
111fn default_status_store() -> String { "memory".into() }
112fn default_status_ttl_seconds() -> u64 { 3600 }
113fn default_status_max_records() -> usize { 10_000 }
114fn default_status_cleanup_interval_seconds() -> u64 { 60 }
115
116#[derive(Debug, Clone, Deserialize)]
117pub struct AppConfig {
118 pub server: ServerConfig,
119 pub security: SecurityConfig,
120 pub mail: MailConfig,
121 pub smtp: SmtpConfig,
122 #[serde(default)]
123 pub rate_limit: RateLimitConfig,
124 #[serde(default)]
125 pub logging: LoggingConfig,
126 #[serde(default)]
127 pub status: StatusConfig,
128}
129
130impl Default for StatusConfig {
131 fn default() -> Self {
132 Self {
133 enabled: default_status_enabled(),
134 store: default_status_store(),
135 ttl_seconds: default_status_ttl_seconds(),
136 max_records: default_status_max_records(),
137 cleanup_interval_seconds: default_status_cleanup_interval_seconds(),
138 db_path: None,
139 redis_url: None,
140 }
141 }
142}
143
144#[derive(Debug, Clone, Deserialize)]
145pub struct ServerConfig {
146 pub bind_address: String,
147 #[serde(default = "default_max_request_body_bytes")]
148 pub max_request_body_bytes: usize,
149 #[serde(default = "default_request_timeout_seconds")]
150 pub request_timeout_seconds: u64,
151 #[serde(default = "default_shutdown_timeout_seconds")]
152 pub shutdown_timeout_seconds: u64,
153 #[serde(default)]
155 pub concurrency_limit: usize,
156 pub tls_cert: Option<std::path::PathBuf>,
158 pub tls_key: Option<std::path::PathBuf>,
160 #[serde(default = "default_monitoring_cidrs")]
163 pub monitoring_cidrs: Vec<String>,
164}
165
166#[derive(Debug, Clone, Deserialize)]
167pub struct SecurityConfig {
168 #[serde(default)]
171 pub trust_proxy_headers: bool,
172 #[serde(default)]
175 pub trusted_source_cidrs: Vec<String>,
176 #[serde(default)]
179 pub allowed_source_cidrs: Vec<String>,
180 #[serde(default)]
181 pub api_keys: Vec<ApiKeyConfig>,
182}
183
184#[derive(Debug, Clone, Deserialize)]
186pub struct ApiKeyConfig {
187 pub id: String,
188 pub secret: SecretString,
189 #[serde(default = "default_true")]
190 pub enabled: bool,
191 pub description: Option<String>,
192 #[serde(default)]
194 pub allowed_recipient_domains: Vec<String>,
195 #[serde(default)]
198 pub allowed_recipients: Vec<String>,
199 pub rate_limit_per_min: Option<u32>,
201 #[serde(default)]
203 pub burst: u32,
204 pub mask_recipient: Option<bool>,
207}
208
209#[derive(Debug, Clone, Deserialize)]
210pub struct MailConfig {
211 pub default_from: String,
212 pub default_from_name: Option<String>,
213 #[serde(default)]
215 pub allowed_recipient_domains: Vec<String>,
216 #[serde(default = "default_max_subject_chars")]
217 pub max_subject_chars: usize,
218 #[serde(default = "default_max_body_bytes")]
219 pub max_body_bytes: usize,
220 #[serde(default = "default_max_recipients")]
222 pub max_recipients: usize,
223 #[serde(default = "default_max_attachments")]
225 pub max_attachments: usize,
226 #[serde(default = "default_max_attachment_bytes")]
228 pub max_attachment_bytes: usize,
229 #[serde(default = "default_max_bulk_messages")]
231 pub max_bulk_messages: usize,
232 #[serde(default)]
234 pub allow_html_body: bool,
235 #[serde(default)]
237 pub allow_attachments: bool,
238 #[serde(default)]
240 pub allow_bulk_send: bool,
241 pub max_total_attachment_bytes: Option<usize>,
244}
245
246#[derive(Debug, Clone, Deserialize)]
247pub struct SmtpConfig {
248 #[serde(default = "default_smtp_mode")]
249 pub mode: String,
250 #[serde(default = "default_smtp_tls")]
252 pub tls: String,
253 #[serde(default = "default_smtp_host")]
254 pub host: String,
255 #[serde(default = "default_smtp_port")]
256 pub port: u16,
257 #[serde(default = "default_connect_timeout_seconds")]
258 pub connect_timeout_seconds: u64,
259 #[serde(default = "default_submission_timeout_seconds")]
260 pub submission_timeout_seconds: u64,
261 pub auth_user: Option<String>,
263 pub auth_password: Option<SecretString>,
265 #[serde(default = "default_pipe_command")]
267 pub pipe_command: String,
268 #[serde(default = "default_bulk_concurrency")]
271 pub bulk_concurrency: usize,
272}
273
274#[derive(Debug, Clone, Deserialize)]
275pub struct RateLimitConfig {
276 #[serde(default = "default_global_per_min")]
278 pub global_per_min: u32,
279 #[serde(default = "default_per_ip_per_min")]
280 pub per_ip_per_min: u32,
281 #[serde(default = "default_per_key_per_min")]
283 pub per_key_per_min: u32,
284
285 #[serde(default = "default_global_burst")]
287 pub global_burst: u32,
288 #[serde(default = "default_per_ip_burst")]
289 pub per_ip_burst: u32,
290 #[serde(default = "default_per_key_burst")]
292 pub per_key_burst: u32,
293
294 #[serde(default)]
297 pub burst_size: u32,
298
299 #[serde(default = "default_ip_table_size")]
302 pub ip_table_size: usize,
303}
304
305impl RateLimitConfig {
306 pub fn effective_global_burst(&self) -> u32 {
308 if self.global_burst > 0 { self.global_burst }
309 else if self.burst_size > 0 { self.burst_size }
310 else { default_global_burst() }
311 }
312 pub fn effective_per_ip_burst(&self) -> u32 {
313 if self.per_ip_burst > 0 { self.per_ip_burst }
314 else if self.burst_size > 0 { self.burst_size }
315 else { default_per_ip_burst() }
316 }
317 pub fn effective_per_key_burst(&self) -> u32 {
318 if self.per_key_burst > 0 { self.per_key_burst }
319 else if self.burst_size > 0 { self.burst_size }
320 else { default_per_key_burst() }
321 }
322}
323
324#[derive(Debug, Clone, Deserialize)]
325pub struct LoggingConfig {
326 #[serde(default = "default_log_format")]
328 pub format: String,
329 #[serde(default = "default_log_level")]
330 pub level: String,
331 #[serde(default)]
333 pub mask_recipient: bool,
334}
335
336fn default_max_request_body_bytes() -> usize { 1_048_576 }
341fn default_request_timeout_seconds() -> u64 { 30 }
342fn default_shutdown_timeout_seconds() -> u64 { 30 }
343fn default_true() -> bool { true }
344fn default_max_subject_chars() -> usize { 255 }
345fn default_max_body_bytes() -> usize { 65_536 }
346fn default_smtp_mode() -> String { "smtp".into() }
347fn default_smtp_host() -> String { "127.0.0.1".into() }
348fn default_smtp_port() -> u16 { 25 }
349fn default_connect_timeout_seconds() -> u64 { 5 }
350fn default_submission_timeout_seconds() -> u64 { 30 }
351fn default_bulk_concurrency() -> usize { 5 }
352fn default_global_per_min() -> u32 { 60 }
353fn default_per_ip_per_min() -> u32 { 20 }
354#[allow(dead_code)]
355fn default_burst_size() -> u32 { 5 }
356fn default_max_recipients() -> usize { 10 }
357fn default_max_attachments() -> usize { 5 }
358fn default_max_attachment_bytes() -> usize { 10 * 1024 * 1024 } fn default_pipe_command() -> String { "/usr/sbin/sendmail".into() }
360fn default_smtp_tls() -> String { "none".into() }
361fn default_global_burst() -> u32 { 10 }
362fn default_per_ip_burst() -> u32 { 5 }
363fn default_per_key_burst() -> u32 { 5 }
364fn default_per_key_per_min() -> u32 { 30 }
365fn default_ip_table_size() -> usize { 10_000 }
366fn default_log_format() -> String { "text".into() }
367fn default_log_level() -> String { "info".into() }
368
369#[derive(Debug, Error)]
374pub enum ConfigError {
375 #[error("cannot read config file: {0}")]
376 Io(#[from] std::io::Error),
377
378 #[error("config parse error: {0}")]
379 Parse(#[from] toml::de::Error),
380
381 #[error("invalid server.bind_address: must be host:port (e.g. 127.0.0.1:8080)")]
382 InvalidBindAddress,
383
384 #[error("invalid mail.default_from: must be a valid email address")]
385 InvalidDefaultFrom,
386 #[error("no api_keys are configured")]
387 NoApiKeys,
388
389 #[error("no api_keys entries have enabled = true")]
390 NoEnabledApiKeys,
391
392 #[error("invalid CIDR: {0}")]
393 InvalidCidr(String),
394
395 #[error("configuration error: {0}")]
396 Validation(String),
397
398 #[error("invalid smtp.port: must be 1-65535")]
399 InvalidSmtpPort,
400
401 #[error("invalid rate_limit values: all per_min values must be > 0")]
402 InvalidRateLimit,
403
404 #[error("invalid logging.level: must be trace, debug, info, warn, or error")]
405 InvalidLogLevel,
406
407 #[error("invalid logging.format: must be 'text' or 'json'")]
408 InvalidLogFormat,
409}
410
411pub mod validate;
416pub use validate::{load, load_from_str, validate_config};
417
418fn default_max_bulk_messages() -> usize { 10 }
419
420fn default_monitoring_cidrs() -> Vec<String> { vec!["127.0.0.1/32".into(), "::1/128".into()] }
421
422pub fn restart_required_changes(old: &AppConfig, new: &AppConfig) -> Vec<String> {
431 let mut changed = Vec::new();
432
433 macro_rules! check {
434 ($field:expr, $label:literal) => {
435 if $field { changed.push($label.into()); }
436 };
437 }
438
439 check!(old.server.bind_address != new.server.bind_address, "server.bind_address");
440 check!(old.server.request_timeout_seconds != new.server.request_timeout_seconds, "server.request_timeout_seconds");
441 check!(old.server.max_request_body_bytes != new.server.max_request_body_bytes, "server.max_request_body_bytes");
442 check!(old.server.concurrency_limit != new.server.concurrency_limit, "server.concurrency_limit");
443 check!(old.smtp.host != new.smtp.host, "smtp.host");
444 check!(old.smtp.port != new.smtp.port, "smtp.port");
445 check!(old.smtp.mode != new.smtp.mode, "smtp.mode");
446 check!(old.rate_limit.global_per_min != new.rate_limit.global_per_min, "rate_limit.global_per_min");
447 check!(old.rate_limit.per_ip_per_min != new.rate_limit.per_ip_per_min, "rate_limit.per_ip_per_min");
448 check!(old.rate_limit.per_key_per_min != new.rate_limit.per_key_per_min, "rate_limit.per_key_per_min");
449 check!(old.security.trust_proxy_headers != new.security.trust_proxy_headers, "security.trust_proxy_headers");
450 check!(old.security.trusted_source_cidrs != new.security.trusted_source_cidrs, "security.trusted_source_cidrs");
451 check!(old.security.allowed_source_cidrs != new.security.allowed_source_cidrs, "security.allowed_source_cidrs");
452 check!(old.status.enabled != new.status.enabled, "status.enabled");
453 check!(old.status.store != new.status.store, "status.store");
454 check!(old.status.db_path != new.status.db_path, "status.db_path");
455 check!(old.status.redis_url != new.status.redis_url, "status.redis_url");
456
457 changed
458}
459
460pub fn merge_reloadable(current: &AppConfig, new: &AppConfig) -> AppConfig {
463 AppConfig {
464 server: current.server.clone(),
466 smtp: current.smtp.clone(),
467 rate_limit: current.rate_limit.clone(),
468 security: new.security.clone(),
470 mail: new.mail.clone(),
471 logging: new.logging.clone(),
472 status: StatusConfig {
473 enabled: current.status.enabled,
475 store: current.status.store.clone(),
476 db_path: current.status.db_path.clone(),
477 redis_url: current.status.redis_url.clone(),
478 ttl_seconds: new.status.ttl_seconds,
480 max_records: new.status.max_records,
481 cleanup_interval_seconds: new.status.cleanup_interval_seconds,
482 },
483 }
484}