1 parent
e4aa787e06
commit
27a53c3c6a
16 files changed
+71
-5668
No files matched your search
@@ -1,5 +1,11 @@
|
||||
# RustBox changelog
|
||||
|
||||
### 0.2.5
|
||||
- After moving or deleting the currently opened message, RustBox now automatically opens the next message in the current list; if the removed message was last, it falls back to the previous message.
|
||||
- The behavior applies to Spam, Not spam, Archive, Move and Delete actions and keeps the reader open while advancing.
|
||||
- No schema migration is required; runtime schema remains V047 and V048 remains the next migration number.
|
||||
- Bumped the application/source release to 0.2.5.
|
||||
|
||||
### 0.2.4
|
||||
- Fixed S3-compatible storage over explicitly allowed plain HTTP endpoints by applying `storage.allow_http` to the final `ClientOptions` object instead of setting it before `with_client_options()` replaces those options. This fixes Garage/MinIO/Ceph-style internal HTTP endpoints without weakening the HTTPS default.
|
||||
- Added an HTTP S3 regression test that verifies a request reaches an explicitly allowed local S3-compatible endpoint.
|
||||
|
||||
Generated
-5623
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustbox"
|
||||
version = "0.2.4"
|
||||
version = "0.2.5"
|
||||
edition = "2024"
|
||||
rust-version = "1.99.1"
|
||||
default-run = "rustbox"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# RustBox attribution / notice
|
||||
|
||||
RustBox 0.2.4 is derived from the Oxi webmail project:
|
||||
RustBox 0.2.5 is derived from the Oxi webmail project:
|
||||
|
||||
- Upstream: `c0h1b4/oxi`
|
||||
- Base snapshot: `7f1507c486b78cf33e54beb701dd460d3bd2bd5d`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# RustBox 0.2.4
|
||||
# RustBox 0.2.5
|
||||
|
||||
RustBox is a webmail derived from `c0h1b4/oxi`. The consumed Oxi revision is recorded in `UPSTREAM_BASE`.
|
||||
|
||||
@@ -34,7 +34,7 @@ rustbox/
|
||||
|
||||
`build.rs` combines the CSS modules from `src/web/css/manifest.txt` and JavaScript modules from `src/web/js/manifest.txt`. Cargo embeds the generated CSS/JS bundles, HTML and SVG sprite in the RustBox executable. There is no standalone source `src/web/app.js`; `src/web/` is the single source of truth and Cargo generates the embedded bundles in `OUT_DIR`.
|
||||
|
||||
## Storage and search in 0.2.4
|
||||
## Storage and search in 0.2.5
|
||||
|
||||
RustBox separates three concerns: SQL metadata, blob storage and search. The SQL engine does **not** decide whether `data_dir` may be used. A single-node PostgreSQL/MySQL deployment may still use filesystem blobs and local Tantivy. In cluster mode, local node state is rejected unless the configured backend is explicitly shared.
|
||||
|
||||
@@ -224,7 +224,7 @@ Initialize Git after extracting the release archive:
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "RustBox 0.2.4 release"
|
||||
git commit -m "RustBox 0.2.5 release"
|
||||
./scripts/setup-upstream.sh
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
RustBox 0.2.4 release manifest
|
||||
RustBox 0.2.5 release manifest
|
||||
|
||||
0.2.4 feature/configuration changes:
|
||||
0.2.5 feature/configuration changes:
|
||||
- automatically opens the next message after moving/deleting the current message, including Spam, Not spam, Archive and Move actions
|
||||
- falls back to the previous message when the removed message was the last loaded item
|
||||
- keeps the reader open while advancing, including full-screen reader mode
|
||||
- fixes S3-compatible HTTP endpoints by carrying allow_http in the final object_store ClientOptions
|
||||
- keeps HTTPS-only behavior as the default; plain HTTP still requires storage.allow_http=true
|
||||
- adds a local HTTP S3 regression test for the request path
|
||||
@@ -71,14 +74,14 @@ Docker examples:
|
||||
- docker/compose-mysql-nfs-opensearch.yml (MySQL + real NFS volume + OpenSearch)
|
||||
|
||||
Operations/docs:
|
||||
- README.md documents 0.2.4 storage/search configuration
|
||||
- README.md documents 0.2.5 storage/search configuration
|
||||
- docker/README.md documents Compose variants
|
||||
- docs/STORAGE_SEARCH_OPERATIONS.md documents failure modes, shared-storage requirements and migration behavior
|
||||
- TODO.md leaves live SILO/MinIO/AWS/Ceph and real multi-node acceptance validation explicitly pending
|
||||
|
||||
Build/release metadata:
|
||||
- `scripts/package-release.sh` runs the full Rust release gate before creating ZIP/SHA256 artifacts
|
||||
- Cargo.toml version = 0.2.4
|
||||
- Cargo.toml version = 0.2.5
|
||||
- rust-toolchain.toml pins Rust 1.99.1
|
||||
- Cargo.lock is intentionally not shipped; the release gate regenerates it before locked checks
|
||||
- CI and scripts/build-release.sh generate Cargo.lock, then run locked tests/Clippy/release build
|
||||
@@ -1,6 +1,7 @@
|
||||
RustBox 0.2.4 - final source validation
|
||||
RustBox 0.2.5 - final source validation
|
||||
|
||||
0.2.4 scope:
|
||||
0.2.5 scope:
|
||||
- reader advances to the next list message after Spam/Not spam/Archive/Move/Delete actions, with previous-message fallback for the last item
|
||||
- S3-compatible plain-HTTP client-options fix for explicitly allowed internal endpoints
|
||||
- regression coverage for an HTTP S3-compatible endpoint
|
||||
- Rust 1.99.1 toolchain/MSRV
|
||||
@@ -10,10 +11,10 @@ RustBox 0.2.4 - final source validation
|
||||
- reset-all-settings preserving account/mail data
|
||||
- optional browser security headers
|
||||
- Docker two-node application topology with Envoy/Valkey/OpenSearch/PostgreSQL/Garage
|
||||
- version metadata updated to 0.2.4
|
||||
- version metadata updated to 0.2.5
|
||||
|
||||
Completed in this environment:
|
||||
- offline release validator: scripts/validate-release-static.py (207/207 checks passed)
|
||||
- offline release validator: scripts/validate-release-static.py (211/211 checks passed)
|
||||
- all maintained TOML examples parse successfully
|
||||
- Docker Compose topology/config references are validated by the offline release validator
|
||||
- JSON files parse successfully
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# RustBox 0.2.4
|
||||
# RustBox 0.2.5
|
||||
host = "127.0.0.1"
|
||||
port = 3001
|
||||
environment = "production"
|
||||
|
||||
+1
-1
@@ -201,4 +201,4 @@ docker compose -p rustbox -f compose-postgres-s3-opensearch.yml exec opensearch
|
||||
-d '{"index.blocks.read_only_allow_delete":null}'
|
||||
```
|
||||
|
||||
RustBox 0.2.4 retains the OpenSearch outbox backoff introduced in 0.2.2, so these 429/flood-stage responses no longer produce one warning per queued event on a full disk.
|
||||
RustBox 0.2.5 retains the OpenSearch outbox backoff introduced in 0.2.2, so these 429/flood-stage responses no longer produce one warning per queued event on a full disk.
|
||||
@@ -1,8 +1,8 @@
|
||||
# RustBox 0.2.4 - implementation status
|
||||
# RustBox 0.2.5 - implementation status
|
||||
|
||||
This file tracks the `0.2.4` source snapshot. Storage/search scope and outstanding acceptance work are tracked in `TODO.md`. A feature is only listed as implemented when working code exists in this source tree; partial items are called out explicitly.
|
||||
This file tracks the `0.2.5` source snapshot. Storage/search scope and outstanding acceptance work are tracked in `TODO.md`. A feature is only listed as implemented when working code exists in this source tree; partial items are called out explicitly.
|
||||
|
||||
## 0.2.4 storage/search architecture
|
||||
## 0.2.5 storage/search architecture
|
||||
|
||||
- SQL backend selection (`sqlite`/`mysql`/`pgsql`) is independent from blob storage and search backend selection.
|
||||
- Cluster mode clears an implicit/default `data_dir`; PostgreSQL/MySQL single-node deployments may still intentionally use `data_dir`-backed filesystem storage and local Tantivy.
|
||||
@@ -36,6 +36,7 @@ This file tracks the `0.2.4` source snapshot. Storage/search scope and outstandi
|
||||
- Multi-select and bulk read/unread, flag/unflag, archive, move, copy, label and delete actions, including explicit select-all across the complete cached search result set.
|
||||
- Drag-and-drop a message onto a folder to move it.
|
||||
- Previous/next message navigation plus keyboard actions for common reader operations, compose, selection mode and selecting the current message.
|
||||
- After moving/deleting the currently opened message (including Spam, Not spam, Archive and Move), the reader automatically opens the next list item and falls back to the previous item when the removed message was last.
|
||||
- Message reading, collapsible thread navigation, attachments, raw headers, full RFC822 `.eml` download via `BODY.PEEK[]` and `.eml` import into a selected IMAP folder via APPEND.
|
||||
- Message/folder context menus with common Roundcube-style actions, including folder properties, full message source, Message-ID/address copy and mailing-list unsubscribe; the reader sender line also exposes compose/contact/search/filter actions.
|
||||
- Mailbox quota display where the IMAP server exposes quota, including warning/danger states at 80%/95%.
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ RUSTBOX_CONFIG=./config.dev.toml ./target/release/rustbox --check-config
|
||||
|
||||
Browser JavaScript source lives in functional modules under `src/web/js/`, ordered by `src/web/js/manifest.txt`. `build.rs` concatenates those modules into the embedded `rustbox-ui.js` asset during every Cargo build. The repository has no duplicate `ui/` snapshot; `src/web/` is the only browser UI source.
|
||||
|
||||
## Updating an existing checkout to 0.2.4
|
||||
## Updating an existing checkout to 0.2.5
|
||||
|
||||
Do not overlay the release with `cp *` or another command that skips dotfiles. That can retain stale `.github`, `.env.example`, or `Cargo.lock` files.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# RustBox 0.2.4 storage/search operations
|
||||
# RustBox 0.2.5 storage/search operations
|
||||
|
||||
RustBox 0.2.4 treats SQL, blob storage and search as independent capabilities. The database engine does not decide whether `data_dir` is allowed.
|
||||
RustBox 0.2.5 treats SQL, blob storage and search as independent capabilities. The database engine does not decide whether `data_dir` is allowed.
|
||||
|
||||
## Deployment matrix
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Database schemas and migrations
|
||||
|
||||
RustBox 0.2.4 keeps SQL, blob storage and search as independent concerns:
|
||||
RustBox 0.2.5 keeps SQL, blob storage and search as independent concerns:
|
||||
|
||||
```text
|
||||
database_backend = sqlite | mysql | pgsql
|
||||
@@ -34,7 +34,7 @@ Account data is isolated inside the selected physical database with a determinis
|
||||
|
||||
Every schema-changing release must add the same logical migration for all supported SQL engines and register it in `src/db/pool.rs`. Do not update only `schema.sql`: fresh installs use the baseline while existing installations use the numbered migration path.
|
||||
|
||||
The next migration number after RustBox 0.2.4 is **V048**.
|
||||
The next migration number after RustBox 0.2.5 is **V048**.
|
||||
|
||||
Redis and Redis Sentinel do not use SQL migrations.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# RustBox 0.2.4 - packaged production configuration (DEB/RPM)
|
||||
# RustBox 0.2.5 - packaged production configuration (DEB/RPM)
|
||||
#
|
||||
# This file contains all supported RustBox configuration options.
|
||||
# Commented options are optional and do not change the packaged defaults.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Static/offline release checks for RustBox 0.2.4.
|
||||
"""Static/offline release checks for RustBox 0.2.5.
|
||||
|
||||
These checks intentionally avoid pretending to replace `cargo test`: they validate
|
||||
configuration, SQL migrations, Docker examples, query-catalog parity and release
|
||||
@@ -49,7 +49,7 @@ def extract_section(text: str, begin: str, end: str) -> str:
|
||||
|
||||
# Release/toolchain metadata.
|
||||
cargo = tomllib.loads(read("Cargo.toml"))
|
||||
require(cargo["package"]["version"] == "0.2.4", "Cargo package version is 0.2.4")
|
||||
require(cargo["package"]["version"] == "0.2.5", "Cargo package version is 0.2.5")
|
||||
require(cargo["package"]["rust-version"] == "1.99.1", "Cargo rust-version is 1.99.1")
|
||||
toolchain = tomllib.loads(read("rust-toolchain.toml"))
|
||||
require(toolchain["toolchain"]["channel"] == "1.99.1", "rust-toolchain pins Rust 1.99.1")
|
||||
@@ -65,10 +65,10 @@ require("make_zip.py" in package_script and "sha256sum" in package_script, "rele
|
||||
require("cargo generate-lockfile" in read(".github/workflows/ci.yml"), "CI regenerates Cargo.lock before --locked checks")
|
||||
require("validate-release-static.py" in read("scripts/build-release.sh"), "release script runs offline/static release gate")
|
||||
require("validate-release-static.py" in read(".github/workflows/ci.yml"), "CI runs offline/static release gate")
|
||||
require(read("README.md").startswith("# RustBox 0.2.4"), "README identifies RustBox 0.2.4")
|
||||
require(read("RELEASE-MANIFEST.txt").startswith("RustBox 0.2.4"), "release manifest identifies RustBox 0.2.4")
|
||||
require(read("RELEASE-VALIDATION.txt").startswith("RustBox 0.2.4"), "release validation identifies RustBox 0.2.4")
|
||||
require(read("docs/IMPLEMENTATION_STATUS.md").startswith("# RustBox 0.2.4"), "implementation status identifies RustBox 0.2.4")
|
||||
require(read("README.md").startswith("# RustBox 0.2.5"), "README identifies RustBox 0.2.5")
|
||||
require(read("RELEASE-MANIFEST.txt").startswith("RustBox 0.2.5"), "release manifest identifies RustBox 0.2.5")
|
||||
require(read("RELEASE-VALIDATION.txt").startswith("RustBox 0.2.5"), "release validation identifies RustBox 0.2.5")
|
||||
require(read("docs/IMPLEMENTATION_STATUS.md").startswith("# RustBox 0.2.5"), "implementation status identifies RustBox 0.2.5")
|
||||
require((ROOT / "docs/STORAGE_SEARCH_OPERATIONS.md").is_file(), "storage/search operations guide exists")
|
||||
require('imap_host = ""' not in read("config.dev.toml"), "development config does not override IMAP host with an invalid empty value")
|
||||
require(not re.search(r"(?m)^IMAP_HOST=\s*$", read(".env.example")), "environment example does not set an invalid empty IMAP host")
|
||||
@@ -98,6 +98,11 @@ require('.route("/settings/reset", post(settings::reset_all_settings))' in route
|
||||
require("db::settings::reset_all" in read("src/routes/settings.rs"), "settings reset route delegates preference reset to the DB layer")
|
||||
require('$("#quick-filter-btn")?.classList.toggle("hidden", state.authPolicy.sieveEnabled === false)' in read("src/web/js/20-preferences-auth.js"), "Sieve quick-filter UI disappears when Sieve is disabled")
|
||||
require("state.authPolicy.sieveEnabled === false ? []" in read("src/web/js/60-mail-actions-search.js"), "Sieve context-menu actions disappear when Sieve is disabled")
|
||||
mail_actions_js = read("src/web/js/60-mail-actions-search.js")
|
||||
require("function nextMessageTargetAfterRemoval(folder, uid)" in mail_actions_js, "reader computes the adjacent message before removing the current item")
|
||||
require("state.messages[index + 1] || state.messages[index - 1]" in mail_actions_js, "reader prefers the next message and falls back to the previous item")
|
||||
require(mail_actions_js.count("await removeCurrentMessageFromView(") >= 2, "move and permanent-delete flows share automatic reader advancement")
|
||||
require("if (nextTarget) await openMessage(nextTarget.folder, nextTarget.uid);" in mail_actions_js, "reader opens the adjacent message after current-message removal")
|
||||
require("is_opensearch_backpressure_error" in read("src/search/engine.rs"), "OpenSearch worker classifies backpressure failures")
|
||||
require("worker pass failed; backing off" in read("src/search/engine.rs"), "OpenSearch worker backs off after failures")
|
||||
require("ENVIRONMENT: ${ENVIRONMENT:-development}" in read("docker/compose-postgres-s3-opensearch.yml"), "PostgreSQL Compose defaults direct HTTP to development cookie mode")
|
||||
@@ -112,7 +117,8 @@ require(".execute(schema.as_str())" in db_pool, "PostgreSQL startup executes sch
|
||||
require(not re.search(r"raw_sql\(&schema\)\s*\.execute\(&mut \*tx\)", db_pool), "PostgreSQL startup avoids raw_sql mutable-connection lifetime regression")
|
||||
require("sqlite_test_configuration_does_not_pin_deleted_tempdir" in db_pool, "SQLite test isolation has a regression test for deleted TempDir reuse")
|
||||
require("let config = AppConfig {" in db_pool and "let mut config = AppConfig::default();" not in db_pool, "SQLite isolation regression test is Clippy-clean with struct initialization")
|
||||
require("each test's own data_dir" in db_pool and "#[cfg(test)]" in db_pool, "SQLite test configuration does not publish a temporary database path globally")
|
||||
sqlite_configure = db_pool.split("DatabaseBackend::Sqlite => {", 1)[1].split("DatabaseBackend::Mysql => {", 1)[0]
|
||||
require("#[cfg(test)]" in sqlite_configure and "return Ok(());" in sqlite_configure and "#[cfg(not(test))]" in sqlite_configure, "SQLite test configuration does not publish a temporary database path globally")
|
||||
require("RUST_LOG: ${RUST_LOG:-info,sqlx::postgres::notice=warn}" not in read("docker/compose-postgres-s3-opensearch.yml"), "PostgreSQL Compose no longer depends on a notice-filter workaround")
|
||||
pgsql_queries = read("src/queries/pgsql/queries.rs")
|
||||
require("ON CONFLICT(key) DO UPDATE SET value = CAST(user_meta.value AS INTEGER) + 1" in pgsql_queries, "PostgreSQL header-sync UPSERT qualifies the existing target value")
|
||||
|
||||
@@ -91,21 +91,35 @@ async function moveMessageByDrag(fromFolder, uid, targetFolder) {
|
||||
toast(`Moved to ${targetFolder}`, "success", 9000, moveUndoAction(result));
|
||||
}
|
||||
|
||||
function nextMessageTargetAfterRemoval(folder, uid) {
|
||||
const index = state.messages.findIndex((message) => message.uid === uid && (message.folder || state.folder) === folder);
|
||||
if (index < 0) return null;
|
||||
const message = state.messages[index + 1] || state.messages[index - 1];
|
||||
if (!message) return null;
|
||||
return { folder: message.folder || state.folder, uid: message.uid };
|
||||
}
|
||||
|
||||
async function removeCurrentMessageFromView(folder, uid, nextTarget) {
|
||||
state.messages = state.messages.filter((message) => !(message.uid === uid && (message.folder || state.folder) === folder));
|
||||
state.selectedMessages.delete(`${folder}\u0000${uid}`);
|
||||
state.total = Math.max(0, state.total - 1);
|
||||
state.selected = null;
|
||||
state.currentDetail = null;
|
||||
renderMessages();
|
||||
if (nextTarget) await openMessage(nextTarget.folder, nextTarget.uid);
|
||||
else closeReader();
|
||||
}
|
||||
|
||||
async function moveCurrentTo(targetFolder, successMessage = null) {
|
||||
if (!state.currentDetail || !targetFolder || targetFolder === state.currentDetail.folder) return;
|
||||
const detail = state.currentDetail;
|
||||
const nextTarget = nextMessageTargetAfterRemoval(detail.folder, detail.uid);
|
||||
try {
|
||||
const result = await request("/messages/move", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ from_folder: detail.folder, to_folder: targetFolder, uid: detail.uid }),
|
||||
});
|
||||
state.messages = state.messages.filter((msg) => !(msg.uid === detail.uid && (msg.folder || state.folder) === detail.folder));
|
||||
state.selectedMessages.delete(`${detail.folder}\u0000${detail.uid}`);
|
||||
state.total = Math.max(0, state.total - 1);
|
||||
state.selected = null;
|
||||
state.currentDetail = null;
|
||||
closeReader();
|
||||
renderMessages();
|
||||
await removeCurrentMessageFromView(detail.folder, detail.uid, nextTarget);
|
||||
await refreshFolderListOnly();
|
||||
toast(successMessage || `Moved to ${targetFolder}`, "success", 9000, moveUndoAction(result));
|
||||
} catch (error) { toast(error.message, "error"); }
|
||||
@@ -185,15 +199,10 @@ async function deleteCurrent() {
|
||||
return;
|
||||
}
|
||||
if (!await appConfirm("Permanently delete this message?", { title: "Delete message permanently", confirmLabel: "Delete permanently", danger: true })) return;
|
||||
const nextTarget = nextMessageTargetAfterRemoval(folder, uid);
|
||||
try {
|
||||
await request(`/messages/${encodeURIComponent(folder)}/${uid}`, { method: "DELETE" });
|
||||
state.messages = state.messages.filter((msg) => !(msg.uid === uid && (msg.folder || state.folder) === folder));
|
||||
state.selectedMessages.delete(`${folder}\u0000${uid}`);
|
||||
state.total = Math.max(0, state.total - 1);
|
||||
state.selected = null;
|
||||
state.currentDetail = null;
|
||||
closeReader();
|
||||
renderMessages();
|
||||
await removeCurrentMessageFromView(folder, uid, nextTarget);
|
||||
await refreshFolderListOnly();
|
||||
toast("Message deleted", "success");
|
||||
} catch (error) { toast(error.message, "error"); }
|
||||
|
||||
Reference in new issue
Block a user