1use std::sync::Arc;
18
19pub use request_id::RequestId;
20
21pub mod api;
22pub mod auth;
23pub mod config;
24pub mod error;
25pub mod logging;
26pub mod mail;
27pub mod policy;
28pub mod rate_limit;
29pub mod sanitize;
30pub mod security;
31pub mod smtp;
32pub mod metrics;
33pub mod request_id;
34pub mod status;
35pub mod status_memory;
36#[cfg(feature = "sqlite")]
37pub mod status_sqlite;
38#[cfg(feature = "redis")]
39pub mod status_redis;
40pub mod validation;
41
42#[cfg(test)]
43mod tests;
44
45pub struct AppState {
51 config_store: arc_swap::ArcSwap<config::AppConfig>,
52 pub smtp: smtp::SmtpTransport,
53 pub rate_limiter: Arc<rate_limit::RateLimiter>,
54 pub metrics: Arc<metrics::Metrics>,
55 pub status_store: Arc<dyn status::StatusStore>,
57}
58
59impl AppState {
60 pub fn new(config: config::AppConfig) -> Arc<Self> {
62 let smtp = smtp::build_transport(&config.smtp)
63 .expect("SMTP transport construction failed after config validation");
64 let rate_limiter = Arc::new(rate_limit::RateLimiter::new(&config.rate_limit));
65 let m = Arc::new(metrics::Metrics::new());
66 let status_store: Arc<dyn status::StatusStore> = if !config.status.enabled {
67 Arc::new(status_memory::NoopStatusStore)
68 } else {
69 match config.status.store.as_str() {
70 #[cfg(feature = "sqlite")]
71 "sqlite" => {
72 let db_path = config.status.db_path.as_deref()
73 .expect("db_path validated in config::validate_config");
74 status_sqlite::SqliteStatusStore::open(db_path, &config.status, Arc::clone(&m))
75 .unwrap_or_else(|e| {
76 eprintln!("fatal: {e}");
77 std::process::exit(1);
78 })
79 }
80 #[cfg(feature = "redis")]
81 "redis" => {
82 let url = config.status.redis_url.as_deref()
83 .expect("redis_url validated in config::validate_config");
84 status_redis::RedisStatusStore::open(url, &config.status, Arc::clone(&m))
85 .unwrap_or_else(|e| {
86 eprintln!("fatal: {e}");
87 std::process::exit(1);
88 })
89 }
90 _ => status_memory::InMemoryStatusStore::new(&config.status, Arc::clone(&m)),
91 }
92 };
93 Arc::new(Self {
94 config_store: arc_swap::ArcSwap::from_pointee(config),
95 smtp,
96 rate_limiter,
97 metrics: m,
98 status_store,
99 })
100 }
101
102 pub fn config(&self) -> Arc<config::AppConfig> {
107 self.config_store.load_full()
108 }
109
110 pub fn new_with_store(config: config::AppConfig, store: Arc<dyn status::StatusStore>) -> Arc<Self> {
112 let smtp = smtp::build_transport(&config.smtp)
113 .expect("build_transport failed in new_with_store");
114 let rate_limiter = Arc::new(rate_limit::RateLimiter::new(&config.rate_limit));
115 let m = Arc::new(metrics::Metrics::new());
116 Arc::new(Self {
117 config_store: arc_swap::ArcSwap::from_pointee(config),
118 smtp,
119 rate_limiter,
120 metrics: m,
121 status_store: store,
122 })
123 }
124
125 pub fn reload_config(&self, new_config: config::AppConfig) {
131 let current = self.config();
132
133 let restart_fields = config::restart_required_changes(¤t, &new_config);
135 if !restart_fields.is_empty() {
136 tracing::warn!(
137 event = "sighup_restart_required",
138 fields = %restart_fields.join(", "),
139 "SIGHUP reload rejected for these fields — restart required"
140 );
141 }
143
144 let merged = config::merge_reloadable(¤t, &new_config);
146 self.status_store.reload_config(&merged.status);
147 self.config_store.store(Arc::new(merged));
148 tracing::info!(event = "config_reloaded");
149 }
150}