diff --git a/CHANGELOG.md b/CHANGELOG.md index 0752aad..d407c08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Updated UI/build contract tests for the modular JavaScript build and expanded interface-style validation. - Fixed the 0.0.2 release build regression by centralizing `AppConfig::default()` and updating stale `AccountSession` test fixtures for the new session metadata fields. - Updated UI contract tests and visual fixtures after removal of the legacy `ui/` directory. +- Fixed release-test regressions in OpenPGP fingerprint validation, protected system-folder recognition, nested `base_path` UI routing, and missing SMTP endpoint handling. ### 0.0.1 diff --git a/src/pgp/mod.rs b/src/pgp/mod.rs index 1a9283d..e13c02e 100644 --- a/src/pgp/mod.rs +++ b/src/pgp/mod.rs @@ -373,8 +373,19 @@ pub fn store_secret_key( let dir = private_key_dir(data_dir, user_hash); fs::create_dir_all(&dir).map_err(|e| format!("Failed to create PGP key directory: {e}"))?; set_dir_permissions(&dir)?; + + // Reject path/control punctuation before normalization. `normalize_fingerprint` + // intentionally strips formatting characters, so validating only its output + // would turn values such as "../../escape" into a short hexadecimal string. + if fingerprint.is_empty() + || !fingerprint + .chars() + .all(|c| c.is_ascii_hexdigit() || c.is_ascii_whitespace() || c == ':') + { + return Err("Invalid OpenPGP fingerprint".to_string()); + } let safe = normalize_fingerprint(fingerprint); - if safe.is_empty() || !safe.chars().all(|c| c.is_ascii_hexdigit()) { + if safe.is_empty() { return Err("Invalid OpenPGP fingerprint".to_string()); } let final_path = dir.join(format!("{safe}.asc")); diff --git a/src/routes/folder_mgmt.rs b/src/routes/folder_mgmt.rs index 257c6f6..96795d3 100644 --- a/src/routes/folder_mgmt.rs +++ b/src/routes/folder_mgmt.rs @@ -41,7 +41,7 @@ struct MessageResponse { // --------------------------------------------------------------------------- /// Folder names that must not be renamed or deleted. -const SYSTEM_FOLDERS: &[&str] = &["INBOX"]; +const SYSTEM_FOLDERS: &[&str] = &["INBOX", "Sent", "Drafts", "Trash", "Junk", "Spam"]; /// Returns `true` if the folder is a system folder (case-insensitive). fn is_system_folder(name: &str) -> bool { diff --git a/src/routes/mod.rs b/src/routes/mod.rs index c1a8ee4..8b96320 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -494,6 +494,9 @@ pub fn create_router( // RustBox serves API and UI from the same Rust process. let inner = Router::new() .nest("/api", api_router) + // An explicit index route is required for the root of a nested base path + // (for example `/mail/`). The fallback still handles SPA deep links. + .route("/", get(crate::ui::serve_ui)) .fallback(crate::ui::serve_ui); // Use the same base path for routing and authentication cookies. diff --git a/src/routes/send.rs b/src/routes/send.rs index dc6084d..ca0b17a 100644 --- a/src/routes/send.rs +++ b/src/routes/send.rs @@ -117,6 +117,13 @@ pub async fn send_message_handler( // Build SMTP credentials from the authenticated account session. // Server endpoints are resolved from RustBox configuration at login and stored in the session. + // Normal login falls back from an unset smtp_host to the IMAP host, but reject + // an empty endpoint defensively for stale/legacy sessions. + if session.smtp_host.trim().is_empty() { + return Err(AppError::ServiceUnavailable( + "SMTP server is not configured for this account".to_string(), + )); + } let smtp_creds = SmtpCredentials { host: session.smtp_host.clone(), port: session.smtp_port, diff --git a/src/routes/tests.rs b/src/routes/tests.rs index 5a603db..7fcff00 100644 --- a/src/routes/tests.rs +++ b/src/routes/tests.rs @@ -2002,18 +2002,30 @@ async fn send_returns_400_with_empty_body_and_subject() { } #[tokio::test] -async fn send_returns_503_when_smtp_not_configured() { +async fn send_returns_503_when_session_has_no_smtp_endpoint() { let static_dir = setup_static_dir(); let data_dir = TempDir::new().unwrap(); - // Use config WITHOUT smtp_host let config = test_config_with_imap( static_dir.path().to_str().unwrap(), data_dir.path().to_str().unwrap(), ); let store = test_store(); - let (browser_id, account_id, token) = setup_test_account(&store, "alice@example.com"); + let browser_id = store.create_browser(); + let email = "alice@example.com"; + let (token, account_id) = store.add_account_to_browser( + &browser_id, + email.to_string(), + "password".to_string(), + crate::auth::user_data::hash_email(email), + "imap.example.com".to_string(), + 993, + true, + String::new(), + 587, + true, + ); - let user_hash = crate::auth::user_data::hash_email("alice@example.com"); + let user_hash = crate::auth::user_data::hash_email(email); provision_user_db(data_dir.path().to_str().unwrap(), &user_hash); let app = create_router(