v0.0.5
RustBox CI / rustbox (push) Canceled after 0s

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-30 12:06:44 +01:00
1 parent 96a32ea8fb
commit a643e7e3ab
106 files changed
+2223 -4669

No files matched your search

-64
View File
@@ -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");