1 parent
a238ff5e23
commit
93762fe9ef
65 files changed
+5420
-2792
No files matched your search
+150
-4
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user