v0.0.3
RustBox CI / rustbox (push) Waiting to run

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-30 00:12:09 +01:00
1 parent a238ff5e23
commit 93762fe9ef
65 files changed
+5420 -2792

No files matched your search

+150 -4
View File
@@ -24,8 +24,8 @@ pub struct AppConfig {
#[serde(default = "default_port")]
pub port: u16,
/// IMAP server hostname. Required at startup.
#[serde(default)]
/// IMAP server hostname. Defaults to localhost when omitted.
#[serde(default = "default_imap_host")]
pub imap_host: Option<String>,
/// IMAP server port.
@@ -44,6 +44,20 @@ pub struct AppConfig {
#[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,
@@ -64,6 +78,16 @@ pub struct AppConfig {
#[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>,
@@ -158,6 +182,10 @@ fn default_port() -> u16 {
3001
}
fn default_imap_host() -> Option<String> {
Some("localhost".to_string())
}
fn default_imap_port() -> u16 {
993
}
@@ -170,6 +198,14 @@ fn default_tls_enabled() -> bool {
true
}
fn default_max_attachment_size_mb() -> usize {
10
}
fn default_max_message_size_mb() -> usize {
50
}
fn default_data_dir() -> String {
"/data".to_string()
}
@@ -189,6 +225,14 @@ fn default_environment() -> String {
"development".to_string()
}
fn default_theme() -> String {
"system".to_string()
}
fn default_interface_skin() -> String {
"rustbox".to_string()
}
fn default_sieve_port() -> u16 {
4190
}
@@ -219,16 +263,21 @@ impl Default for AppConfig {
Self {
host: default_host(),
port: default_port(),
imap_host: None,
imap_host: default_imap_host(),
imap_port: default_imap_port(),
smtp_host: None,
smtp_port: default_smtp_port(),
tls_enabled: default_tls_enabled(),
tls_verify: None,
max_attachment_size_mb: default_max_attachment_size_mb(),
max_message_size_mb: default_max_message_size_mb(),
data_dir: default_data_dir(),
session_timeout_hours: default_session_timeout_hours(),
remember_me_enabled: default_remember_me_enabled(),
remember_me_days: default_remember_me_days(),
environment: default_environment(),
default_theme: default_theme(),
default_interface_skin: default_interface_skin(),
base_path: None,
sieve_host: None,
sieve_port: default_sieve_port(),
@@ -261,6 +310,27 @@ 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;
}
let normalized = host
.trim()
.trim_matches(|ch| ch == '[' || ch == ']')
.to_ascii_lowercase();
!matches!(normalized.as_str(), "localhost" | "127.0.0.1" | "::1")
}
pub fn max_attachment_size_bytes(&self) -> usize {
self.max_attachment_size_mb.saturating_mul(1024 * 1024)
}
pub fn max_message_size_bytes(&self) -> usize {
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> {
@@ -370,12 +440,46 @@ impl AppConfig {
if self.port == 0 || self.imap_port == 0 || self.smtp_port == 0 || self.sieve_port == 0 {
return Err("network ports must be greater than zero".to_string());
}
if self.max_attachment_size_mb == 0 || self.max_attachment_size_mb > 512 {
return Err("MAX_ATTACHMENT_SIZE_MB must be between 1 and 512".to_string());
}
if self.max_message_size_mb == 0 || self.max_message_size_mb > 1024 {
return Err("MAX_MESSAGE_SIZE_MB must be between 1 and 1024".to_string());
}
if self.max_attachment_size_mb > self.max_message_size_mb {
return Err("MAX_ATTACHMENT_SIZE_MB cannot exceed MAX_MESSAGE_SIZE_MB".to_string());
}
if self.session_timeout_hours == 0 || self.session_timeout_hours > 24 * 365 {
return Err("SESSION_TIMEOUT_HOURS must be between 1 and 8760".to_string());
}
if self.remember_me_days == 0 || self.remember_me_days > 365 {
return Err("REMEMBER_ME_DAYS must be between 1 and 365".to_string());
}
if !matches!(self.default_theme.as_str(), "system" | "dark" | "light") {
return Err("DEFAULT_THEME must be one of: system, dark, light".to_string());
}
if !matches!(
self.default_interface_skin.as_str(),
"rustbox"
| "roundcube-classic"
| "roundcube-larry"
| "roundcube-elastic"
| "clean"
| "gmail"
| "proton"
| "zoho"
| "outlook"
| "minimal"
| "graphite"
| "high-contrast"
| "fastmail"
| "apple-mail"
| "yahoo"
| "hey"
| "nord"
) {
return Err("DEFAULT_INTERFACE_SKIN must be one of: rustbox, roundcube-classic, roundcube-larry, roundcube-elastic, clean, gmail, proton, zoho, outlook, minimal, graphite, high-contrast, fastmail, apple-mail, yahoo, hey, nord".to_string());
}
if !matches!(
self.sieve_tls_mode.as_str(),
"starttls" | "implicit" | "none"
@@ -417,16 +521,23 @@ mod tests {
assert_eq!(config.host, "0.0.0.0");
assert_eq!(config.port, 3001);
assert!(config.imap_host.is_none());
assert_eq!(config.imap_host.as_deref(), Some("localhost"));
assert_eq!(config.imap_port, 993);
assert!(config.smtp_host.is_none());
assert_eq!(config.smtp_port, 587);
assert!(config.tls_enabled);
assert_eq!(config.tls_verify, None);
assert!(!config.tls_verify_for_host("localhost"));
assert!(config.tls_verify_for_host("mail.example.com"));
assert_eq!(config.max_attachment_size_mb, 10);
assert_eq!(config.max_message_size_mb, 50);
assert_eq!(config.data_dir, "/data");
assert_eq!(config.session_timeout_hours, 24);
assert!(config.remember_me_enabled);
assert_eq!(config.remember_me_days, 30);
assert_eq!(config.environment, "development");
assert_eq!(config.default_theme, "system");
assert_eq!(config.default_interface_skin, "rustbox");
assert!(config.sieve_host.is_none());
assert_eq!(config.sieve_port, 4190);
assert_eq!(config.sieve_tls_mode, "starttls");
@@ -454,11 +565,16 @@ mod tests {
.merge(("smtp_host", "smtp.example.com"))
.merge(("smtp_port", 465u16))
.merge(("tls_enabled", false))
.merge(("tls_verify", true))
.merge(("max_attachment_size_mb", 40usize))
.merge(("max_message_size_mb", 80usize))
.merge(("data_dir", "/var/rustbox"))
.merge(("session_timeout_hours", 48u64))
.merge(("remember_me_enabled", false))
.merge(("remember_me_days", 14u64))
.merge(("environment", "production"))
.merge(("default_theme", "light"))
.merge(("default_interface_skin", "gmail"))
.merge(("sieve_host", "sieve.example.com"))
.merge(("sieve_port", 4191u16))
.merge(("sieve_tls_mode", "implicit"))
@@ -475,11 +591,16 @@ mod tests {
assert_eq!(config.smtp_host.as_deref(), Some("smtp.example.com"));
assert_eq!(config.smtp_port, 465);
assert!(!config.tls_enabled);
assert_eq!(config.tls_verify, Some(true));
assert_eq!(config.max_attachment_size_mb, 40);
assert_eq!(config.max_message_size_mb, 80);
assert_eq!(config.data_dir, "/var/rustbox");
assert_eq!(config.session_timeout_hours, 48);
assert!(!config.remember_me_enabled);
assert_eq!(config.remember_me_days, 14);
assert_eq!(config.environment, "production");
assert_eq!(config.default_theme, "light");
assert_eq!(config.default_interface_skin, "gmail");
assert_eq!(config.sieve_host.as_deref(), Some("sieve.example.com"));
assert_eq!(config.sieve_port, 4191);
assert_eq!(config.sieve_tls_mode, "implicit");
@@ -511,6 +632,31 @@ mod tests {
}
}
#[test]
fn invalid_default_theme_is_rejected() {
let config = AppConfig {
imap_host: Some("imap.example.com".to_string()),
default_theme: "sepia".to_string(),
..AppConfig::default()
};
assert!(config.validate().unwrap_err().contains("DEFAULT_THEME"));
}
#[test]
fn invalid_default_interface_skin_is_rejected() {
let config = AppConfig {
imap_host: Some("imap.example.com".to_string()),
default_interface_skin: "hotmail-1999".to_string(),
..AppConfig::default()
};
assert!(
config
.validate()
.unwrap_err()
.contains("DEFAULT_INTERFACE_SKIN")
);
}
#[test]
fn outgoing_headers_are_parsed_and_structural_headers_rejected() {
let config = AppConfig {