1 parent
96a32ea8fb
commit
a643e7e3ab
106 files changed
+2223
-4669
No files matched your search
@@ -7,146 +7,102 @@ use figment::{
|
||||
use ipnet::IpNet;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Application configuration loaded via figment.
|
||||
///
|
||||
/// Layers (lowest to highest priority):
|
||||
/// 1. Serde defaults
|
||||
/// 2. Optional TOML (`RUSTBOX_CONFIG`, default `/etc/rustbox/rustbox.toml`)
|
||||
/// 3. Environment variables (flat, e.g. `PORT`, `IMAP_HOST`)
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct AppConfig {
|
||||
/// Bind address for the HTTP server.
|
||||
#[serde(default = "default_host")]
|
||||
pub host: String,
|
||||
|
||||
/// Port for the HTTP server.
|
||||
#[serde(default = "default_port")]
|
||||
pub port: u16,
|
||||
|
||||
/// IMAP server hostname. Defaults to localhost when omitted.
|
||||
#[serde(default = "default_imap_host")]
|
||||
pub imap_host: Option<String>,
|
||||
|
||||
/// IMAP server port.
|
||||
#[serde(default = "default_imap_port")]
|
||||
pub imap_port: u16,
|
||||
|
||||
/// SMTP server hostname. When unset or blank, IMAP host is used.
|
||||
#[serde(default)]
|
||||
pub smtp_host: Option<String>,
|
||||
|
||||
/// SMTP server port.
|
||||
#[serde(default = "default_smtp_port")]
|
||||
pub smtp_port: u16,
|
||||
|
||||
/// Whether TLS is enabled for mail connections.
|
||||
#[serde(default = "default_tls_enabled")]
|
||||
pub tls_enabled: bool,
|
||||
|
||||
/// Optional certificate/hostname verification override for IMAP and SMTP.
|
||||
/// When omitted, verification is disabled only for localhost/loopback endpoints
|
||||
/// and enabled for remote hosts.
|
||||
#[serde(default)]
|
||||
pub tls_verify: Option<bool>,
|
||||
|
||||
/// Maximum size of one uploaded compose attachment, in MiB.
|
||||
#[serde(default = "default_max_attachment_size_mb")]
|
||||
pub max_attachment_size_mb: usize,
|
||||
|
||||
/// Maximum size of the complete RFC822/MIME message submitted to the MTA, in MiB.
|
||||
#[serde(default = "default_max_message_size_mb")]
|
||||
pub max_message_size_mb: usize,
|
||||
|
||||
/// Directory for persistent data storage.
|
||||
#[serde(default = "default_data_dir")]
|
||||
pub data_dir: String,
|
||||
|
||||
/// Session timeout in hours.
|
||||
#[serde(default = "default_session_timeout_hours")]
|
||||
pub session_timeout_hours: u64,
|
||||
|
||||
/// Whether users may request a persistent "remember me" session.
|
||||
#[serde(default = "default_remember_me_enabled")]
|
||||
pub remember_me_enabled: bool,
|
||||
|
||||
/// Lifetime of a remembered session in days.
|
||||
#[serde(default = "default_remember_me_days")]
|
||||
pub remember_me_days: u64,
|
||||
|
||||
/// Environment the application is running in (development, production)
|
||||
#[serde(default = "default_environment")]
|
||||
pub environment: String,
|
||||
|
||||
/// Server-wide light/dark theme used before authentication (including the login screen).
|
||||
/// Authenticated per-user display preferences still take precedence.
|
||||
#[serde(default = "default_theme")]
|
||||
pub default_theme: String,
|
||||
|
||||
/// Server-wide interface style used on the login screen and by users whose
|
||||
/// interface style is set to `default`. Examples: `gmail`, `proton`, `zoho`.
|
||||
#[serde(default = "default_interface_skin")]
|
||||
pub default_interface_skin: String,
|
||||
|
||||
/// Optional base path prefix (e.g. "/mail") for serving behind a reverse proxy subpath.
|
||||
#[serde(default)]
|
||||
pub base_path: Option<String>,
|
||||
|
||||
/// ManageSieve host. When unset, the authenticated account's IMAP host is used.
|
||||
#[serde(default)]
|
||||
pub sieve_host: Option<String>,
|
||||
|
||||
/// ManageSieve TCP port.
|
||||
#[serde(default = "default_sieve_port")]
|
||||
pub sieve_port: u16,
|
||||
|
||||
/// ManageSieve transport: `starttls`, `implicit` or `none`.
|
||||
#[serde(default = "default_sieve_tls_mode")]
|
||||
pub sieve_tls_mode: String,
|
||||
|
||||
/// Explicit opt-in for sending reusable credentials over a cleartext Sieve connection.
|
||||
#[serde(default)]
|
||||
pub sieve_allow_insecure_auth: bool,
|
||||
|
||||
/// Network timeout for ManageSieve connect/read/write operations.
|
||||
#[serde(default = "default_sieve_timeout_secs")]
|
||||
pub sieve_timeout_secs: u64,
|
||||
|
||||
/// Comma-separated list of trusted proxy IPs or CIDR ranges that are allowed to set X-Forwarded-For.
|
||||
#[serde(default)]
|
||||
pub trusted_proxies: Option<String>,
|
||||
|
||||
/// Administrator-defined extra headers added to every outgoing message.
|
||||
/// Each entry uses `Header-Name: value`. Structural/MIME headers are rejected.
|
||||
#[serde(default)]
|
||||
pub outgoing_headers: Vec<String>,
|
||||
|
||||
/// Enable Web Key Directory lookups for OpenPGP recipient keys.
|
||||
#[serde(default = "default_pgp_wkd_enabled")]
|
||||
pub pgp_wkd_enabled: bool,
|
||||
|
||||
/// Allow optional HKP-over-HTTPS fallback for OpenPGP discovery.
|
||||
#[serde(default)]
|
||||
pub pgp_hkp_enabled: bool,
|
||||
|
||||
/// HKP-compatible HTTPS endpoint used when the fallback is enabled.
|
||||
#[serde(default = "default_pgp_hkp_url")]
|
||||
pub pgp_hkp_url: String,
|
||||
|
||||
/// Timeout for WKD/HKP network operations.
|
||||
#[serde(default = "default_pgp_network_timeout_secs")]
|
||||
pub pgp_network_timeout_secs: u64,
|
||||
|
||||
/// Maximum accepted OpenPGP key response size.
|
||||
#[serde(default = "default_pgp_max_key_bytes")]
|
||||
pub pgp_max_key_bytes: usize,
|
||||
|
||||
/// Allow authenticated users to export private OpenPGP key material.
|
||||
/// Disabled by default; public-key export is always available.
|
||||
#[serde(default)]
|
||||
pub pgp_allow_private_export: bool,
|
||||
|
||||
/// Administrator defaults for special IMAP folders. Per-account settings override these.
|
||||
#[serde(default)]
|
||||
pub default_sent_folder: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -302,7 +258,6 @@ impl Default for AppConfig {
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
/// Use the root path when no reverse-proxy prefix is configured.
|
||||
pub fn normalized_base_path(&self) -> &str {
|
||||
self.base_path
|
||||
.as_deref()
|
||||
@@ -310,8 +265,6 @@ impl AppConfig {
|
||||
.unwrap_or("/")
|
||||
}
|
||||
|
||||
/// Certificate verification policy for a configured mail host. Explicit
|
||||
/// `tls_verify` wins. Without it, only localhost/loopback skips validation.
|
||||
pub fn tls_verify_for_host(&self, host: &str) -> bool {
|
||||
if let Some(value) = self.tls_verify {
|
||||
return value;
|
||||
@@ -331,8 +284,6 @@ impl AppConfig {
|
||||
self.max_message_size_mb.saturating_mul(1024 * 1024)
|
||||
}
|
||||
|
||||
/// Parse the `trusted_proxies` field into IP or CIDR entries.
|
||||
/// Logs warnings for unparseable entries.
|
||||
pub fn parsed_trusted_proxies(&self) -> Vec<TrustedProxy> {
|
||||
let Some(ref proxies) = self.trusted_proxies else {
|
||||
return vec![];
|
||||
@@ -409,10 +360,6 @@ impl AppConfig {
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
/// Load configuration by layering serde defaults, optional TOML, and environment variables.
|
||||
///
|
||||
/// Environment variables are read without a prefix and mapped directly to
|
||||
/// struct fields via case-insensitive matching (e.g. `IMAP_HOST` → `imap_host`).
|
||||
#[allow(clippy::result_large_err)]
|
||||
pub fn load() -> Result<Self, figment::Error> {
|
||||
let path = std::env::var("RUSTBOX_CONFIG")
|
||||
@@ -423,8 +370,6 @@ impl AppConfig {
|
||||
figment = figment.merge(Toml::file(path));
|
||||
}
|
||||
|
||||
// Keep Oxi-compatible flat environment names (IMAP_HOST, SMTP_HOST, ...).
|
||||
// Environment variables intentionally override the TOML file.
|
||||
figment.merge(Env::raw()).extract()
|
||||
}
|
||||
|
||||
@@ -515,8 +460,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn default_config_values_load_correctly() {
|
||||
// With no env vars set, all defaults should apply.
|
||||
// We use figment with no providers (only serde defaults).
|
||||
let config: AppConfig = Figment::new().extract().expect("defaults should load");
|
||||
|
||||
assert_eq!(config.host, "0.0.0.0");
|
||||
@@ -555,8 +498,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn env_var_overrides_work() {
|
||||
// Simulate env overrides via figment's tuple provider
|
||||
// to verify all fields are overridable.
|
||||
let config: AppConfig = Figment::new()
|
||||
.merge(("host", "127.0.0.1"))
|
||||
.merge(("port", 8080u16))
|
||||
@@ -611,10 +552,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn real_env_vars_override_multi_word_fields() {
|
||||
// Verify that actual environment variables with underscores
|
||||
// (e.g. IMAP_HOST) correctly map to struct fields (imap_host).
|
||||
// SAFETY: This test runs single-threaded (--test-threads=1)
|
||||
// and cleans up env vars after use.
|
||||
unsafe {
|
||||
std::env::set_var("IMAP_HOST", "test-imap.example.com");
|
||||
std::env::set_var("DATA_DIR", "/tmp/oxi-test");
|
||||
@@ -625,7 +562,6 @@ mod tests {
|
||||
assert_eq!(config.imap_host.as_deref(), Some("test-imap.example.com"));
|
||||
assert_eq!(config.data_dir, "/tmp/oxi-test");
|
||||
|
||||
// Clean up
|
||||
unsafe {
|
||||
std::env::remove_var("IMAP_HOST");
|
||||
std::env::remove_var("DATA_DIR");
|
||||
|
||||
Reference in new issue
Block a user