From 52d5f1745a31977bbcd15139deb78c56335cf7c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 30 Oct 2026 12:52:00 +0100 Subject: [PATCH] v0.0.5_repoready --- CHANGELOG.md | 2 ++ README.md | 4 --- config.example.toml | 1 + packaging/logrotate/rustbox | 11 +++++++ packaging/rustbox.toml | 31 ++++++++++++++++++ packaging/systemd/rustbox.service | 7 ++++ scripts/install-systemd.sh | 10 ++++-- src/config.rs | 16 +++++++++ src/main.rs | 54 +++++++++++++++++++++++++------ 9 files changed, 120 insertions(+), 16 deletions(-) create mode 100644 packaging/logrotate/rustbox create mode 100644 packaging/rustbox.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index fee0a33..98493eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # RustBox changelog ### 0.0.5 +- Added native file logging through `log_dir`; the packaged Linux profile writes JSON logs to `/var/log/rustbox/rustbox.log` and ships 7-day logrotate policy. +- Updated the packaged systemd profile for `/var/lib/rustbox`, `/var/log/rustbox`, hardened service settings, and user-owned configuration. - Fixed the mobile reader toolbar regression so compact controls keep their original height and Message details / Move-copy menus open next to the triggering control instead of at the bottom of the viewport. - Full-screen message reading now overrides the normal reader width limit so the message workspace uses the available screen width. - Fixed mobile reader actions, attachment layout, long subjects, and touch targets. diff --git a/README.md b/README.md index be8f1de..6c411af 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,3 @@ See `docs/UPSTREAM.md` for details. ## Attribution See `NOTICE.md`. The supplied Oxi snapshot did not contain an authoritative `LICENSE` file; verify the exact upstream licensing terms before public redistribution. - -## Internal roadmap scope - -The authoritative internal feature plan is `docs/RUSTBOX_PLAN.md`. Current implementation status is tracked in `docs/IMPLEMENTATION_STATUS.md`. The historical baseline plan remains in `docs/RUSTBOX_PLAN.md`; current release deltas are documented in `CHANGELOG.md` and `docs/releases/`. diff --git a/config.example.toml b/config.example.toml index 30172e4..2b3991d 100644 --- a/config.example.toml +++ b/config.example.toml @@ -9,6 +9,7 @@ default_theme = "system" # apple-mail, yahoo, hey, nord, minimal and Roundcube variants. default_interface_skin = "gmail" data_dir = "/var/lib/rustbox" +log_dir = "/var/log/rustbox" session_timeout_hours = 24 remember_me_enabled = true remember_me_days = 30 diff --git a/packaging/logrotate/rustbox b/packaging/logrotate/rustbox new file mode 100644 index 0000000..d561088 --- /dev/null +++ b/packaging/logrotate/rustbox @@ -0,0 +1,11 @@ +/var/log/rustbox/rustbox.log { + daily + rotate 7 + missingok + notifempty + compress + delaycompress + copytruncate + create 0600 rustbox rustbox + su rustbox rustbox +} diff --git a/packaging/rustbox.toml b/packaging/rustbox.toml new file mode 100644 index 0000000..0e7de7a --- /dev/null +++ b/packaging/rustbox.toml @@ -0,0 +1,31 @@ +host = "0.0.0.0" +port = 3001 +environment = "production" +data_dir = "/var/lib/rustbox" +log_dir = "/var/log/rustbox" +session_timeout_hours = 72 +remember_me_enabled = true +remember_me_days = 30 +imap_host = "localhost" +imap_port = 993 +smtp_host = "localhost" +smtp_port = 587 +tls_enabled = true +tls_verify = false +max_attachment_size_mb = 50 +max_message_size_mb = 100 +sieve_port = 4190 +sieve_tls_mode = "none" +sieve_allow_insecure_auth = true +sieve_timeout_secs = 10 +default_junk_folder = "Spam" +default_theme = "system" +default_interface_skin = "apple-mail" +trusted_proxies = "127.0.0.1,::1" +outgoing_headers = [] +pgp_wkd_enabled = true +pgp_hkp_enabled = false +pgp_hkp_url = "https://keys.openpgp.org" +pgp_network_timeout_secs = 10 +pgp_max_key_bytes = 1048576 +pgp_allow_private_export = true diff --git a/packaging/systemd/rustbox.service b/packaging/systemd/rustbox.service index 388b2a1..6425b25 100644 --- a/packaging/systemd/rustbox.service +++ b/packaging/systemd/rustbox.service @@ -8,10 +8,15 @@ Type=simple User=rustbox Group=rustbox Environment=RUSTBOX_CONFIG=/etc/rustbox/rustbox.toml +Environment=RUST_LOG=info +WorkingDirectory=/var/lib/rustbox ExecStart=/usr/local/bin/rustbox Restart=on-failure RestartSec=3 StateDirectory=rustbox +StateDirectoryMode=0700 +LogsDirectory=rustbox +LogsDirectoryMode=0750 UMask=0077 NoNewPrivileges=true PrivateTmp=true @@ -25,6 +30,8 @@ RestrictSUIDSGID=true RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 LockPersonality=true MemoryDenyWriteExecute=true +StandardOutput=null +StandardError=journal [Install] WantedBy=multi-user.target diff --git a/scripts/install-systemd.sh b/scripts/install-systemd.sh index cccb6c8..1d19bf0 100755 --- a/scripts/install-systemd.sh +++ b/scripts/install-systemd.sh @@ -15,11 +15,17 @@ fi getent group rustbox >/dev/null || groupadd --system rustbox id rustbox >/dev/null 2>&1 || useradd --system --gid rustbox --home-dir /var/lib/rustbox --shell /usr/sbin/nologin rustbox -install -d -o root -g rustbox -m 0750 /etc/rustbox +install -d -o rustbox -g rustbox -m 0750 /etc/rustbox +install -d -o rustbox -g rustbox -m 0700 /var/lib/rustbox +install -d -o rustbox -g rustbox -m 0750 /var/log/rustbox install -m 0755 "$BIN" /usr/local/bin/rustbox install -m 0644 "$ROOT/packaging/systemd/rustbox.service" /etc/systemd/system/rustbox.service +install -m 0644 "$ROOT/packaging/logrotate/rustbox" /etc/logrotate.d/rustbox if [ ! -f /etc/rustbox/rustbox.toml ]; then - install -o root -g rustbox -m 0640 "$ROOT/config.example.toml" /etc/rustbox/rustbox.toml + install -o rustbox -g rustbox -m 0640 "$ROOT/packaging/rustbox.toml" /etc/rustbox/rustbox.toml +else + chown rustbox:rustbox /etc/rustbox/rustbox.toml + chmod 0640 /etc/rustbox/rustbox.toml fi RUSTBOX_CONFIG=/etc/rustbox/rustbox.toml /usr/local/bin/rustbox --check-config systemctl daemon-reload diff --git a/src/config.rs b/src/config.rs index 46daef8..065411c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -43,6 +43,9 @@ pub struct AppConfig { #[serde(default = "default_data_dir")] pub data_dir: String, + #[serde(default)] + pub log_dir: Option, + #[serde(default = "default_session_timeout_hours")] pub session_timeout_hours: u64, @@ -228,6 +231,7 @@ impl Default for AppConfig { max_attachment_size_mb: default_max_attachment_size_mb(), max_message_size_mb: default_max_message_size_mb(), data_dir: default_data_dir(), + log_dir: None, session_timeout_hours: default_session_timeout_hours(), remember_me_enabled: default_remember_me_enabled(), remember_me_days: default_remember_me_days(), @@ -385,6 +389,15 @@ 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 let Some(log_dir) = self.log_dir.as_deref() { + let log_dir = log_dir.trim(); + if log_dir.is_empty() { + return Err("LOG_DIR / log_dir cannot be empty".to_string()); + } + if !std::path::Path::new(log_dir).is_absolute() { + return Err("LOG_DIR / log_dir must be an absolute path".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()); } @@ -475,6 +488,7 @@ mod tests { assert_eq!(config.max_attachment_size_mb, 10); assert_eq!(config.max_message_size_mb, 50); assert_eq!(config.data_dir, "/data"); + assert!(config.log_dir.is_none()); assert_eq!(config.session_timeout_hours, 24); assert!(config.remember_me_enabled); assert_eq!(config.remember_me_days, 30); @@ -510,6 +524,7 @@ mod tests { .merge(("max_attachment_size_mb", 40usize)) .merge(("max_message_size_mb", 80usize)) .merge(("data_dir", "/var/rustbox")) + .merge(("log_dir", "/var/log/rustbox")) .merge(("session_timeout_hours", 48u64)) .merge(("remember_me_enabled", false)) .merge(("remember_me_days", 14u64)) @@ -536,6 +551,7 @@ mod tests { 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.log_dir.as_deref(), Some("/var/log/rustbox")); assert_eq!(config.session_timeout_hours, 48); assert!(!config.remember_me_enabled); assert_eq!(config.remember_me_days, 14); diff --git a/src/main.rs b/src/main.rs index 35c7083..685d079 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,8 @@ mod sieve; mod smtp; mod ui; -use std::sync::Arc; +use std::fs::OpenOptions; +use std::sync::{Arc, Mutex}; use std::time::Duration; use config::AppConfig; @@ -32,22 +33,25 @@ async fn main() -> Result<(), Box> { return Ok(()); } - tracing_subscriber::registry() - .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) - .with(fmt::layer().json()) - .init(); - let loaded_config = AppConfig::load()?; + if args.iter().any(|arg| arg == "--check-config") { + loaded_config.validate().map_err(|message| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("invalid RustBox configuration: {message}"), + ) + })?; + println!("RustBox configuration OK"); + return Ok(()); + } + + init_tracing(loaded_config.log_dir.as_deref())?; loaded_config.validate().map_err(|message| { std::io::Error::new( std::io::ErrorKind::InvalidInput, format!("invalid RustBox configuration: {message}"), ) })?; - if args.iter().any(|arg| arg == "--check-config") { - println!("RustBox configuration OK"); - return Ok(()); - } let config = Arc::new(loaded_config); let store = Arc::new(SessionStore::new_with_tls_verify( @@ -107,6 +111,36 @@ async fn main() -> Result<(), Box> { Ok(()) } +fn init_tracing(log_dir: Option<&str>) -> Result<(), Box> { + if let Some(log_dir) = log_dir.map(str::trim).filter(|value| !value.is_empty()) { + std::fs::create_dir_all(log_dir)?; + let log_path = std::path::Path::new(log_dir).join("rustbox.log"); + let mut options = OpenOptions::new(); + options.create(true).append(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let file = options.open(log_path)?; + tracing_subscriber::registry() + .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) + .with( + fmt::layer() + .json() + .with_ansi(false) + .with_writer(Mutex::new(file)), + ) + .init(); + } else { + tracing_subscriber::registry() + .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) + .with(fmt::layer().json().with_ansi(false)) + .init(); + } + Ok(()) +} + async fn shutdown_signal(idle_manager: Arc) { let ctrl_c = async { if let Err(error) = tokio::signal::ctrl_c().await {