1 parent
2c98b8f925
commit
e4aa787e06
27 files changed
+179
-110
No files matched your search
@@ -1,5 +1,14 @@
|
|||||||
# RustBox changelog
|
# RustBox changelog
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
- Updated the pinned Rust toolchain/MSRV to Rust 1.99.1.
|
||||||
|
- Updated RustBox Docker build/runtime images from Debian 12 Bookworm to Debian 13 Trixie (`rust:1.99.1-trixie`, `debian:trixie-slim`) and switched the runtime OpenSSL package to Trixie's `libssl3t64`.
|
||||||
|
- Updated PostgreSQL 17 Compose images from Bookworm to the supported Trixie variant.
|
||||||
|
- No schema migration is required; runtime schema remains V047 and V048 remains the next migration number.
|
||||||
|
- Bumped the application/source release to 0.2.4.
|
||||||
|
|
||||||
### 0.2.3
|
### 0.2.3
|
||||||
- Fixed API fallback routing so administratively disabled or otherwise unknown `/api/*` endpoints return HTTP 404 instead of being caught by the SPA fallback and returning the UI with HTTP 200.
|
- Fixed API fallback routing so administratively disabled or otherwise unknown `/api/*` endpoints return HTTP 404 instead of being caught by the SPA fallback and returning the UI with HTTP 200.
|
||||||
- Added administrator feature switches for ManageSieve, diagnostics and About; disabled Sieve/diagnostics routes are not registered, and the matching settings UI is removed while keyboard shortcuts remain available.
|
- Added administrator feature switches for ManageSieve, diagnostics and About; disabled Sieve/diagnostics routes are not registered, and the matching settings UI is removed while keyboard shortcuts remain available.
|
||||||
|
|||||||
Generated
+5
-7
@@ -699,13 +699,11 @@ checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc-fast"
|
name = "crc-fast"
|
||||||
version = "1.8.2"
|
version = "1.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85d9be5297a59f1b7651fd2711a1f4461929f53b182b394df0df15b3a387ef51"
|
checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc",
|
|
||||||
"digest 0.10.7",
|
"digest 0.10.7",
|
||||||
"rustversion",
|
|
||||||
"spin 0.10.1",
|
"spin 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3508,7 +3506,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustbox"
|
name = "rustbox"
|
||||||
version = "0.2.3"
|
version = "0.2.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"async-imap",
|
"async-imap",
|
||||||
@@ -4979,9 +4977,9 @@ checksum = "3a3b425286980a367278453f3b6d28cdcb134b574e5a35824a6ff5eebad50bb6"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.26.1"
|
version = "1.29.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
|
checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustbox"
|
name = "rustbox"
|
||||||
version = "0.2.3"
|
version = "0.2.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.88"
|
rust-version = "1.99.1"
|
||||||
default-run = "rustbox"
|
default-run = "rustbox"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# RustBox attribution / notice
|
# RustBox attribution / notice
|
||||||
|
|
||||||
RustBox 0.2.3 is derived from the Oxi webmail project:
|
RustBox 0.2.4 is derived from the Oxi webmail project:
|
||||||
|
|
||||||
- Upstream: `c0h1b4/oxi`
|
- Upstream: `c0h1b4/oxi`
|
||||||
- Base snapshot: `7f1507c486b78cf33e54beb701dd460d3bd2bd5d`
|
- Base snapshot: `7f1507c486b78cf33e54beb701dd460d3bd2bd5d`
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# RustBox 0.2.3
|
# RustBox 0.2.4
|
||||||
|
|
||||||
RustBox is a webmail derived from `c0h1b4/oxi`. The consumed Oxi revision is recorded in `UPSTREAM_BASE`.
|
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`.
|
`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.3
|
## Storage and search in 0.2.4
|
||||||
|
|
||||||
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.
|
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
|
```bash
|
||||||
git init
|
git init
|
||||||
git add .
|
git add .
|
||||||
git commit -m "RustBox 0.2.3 release"
|
git commit -m "RustBox 0.2.4 release"
|
||||||
./scripts/setup-upstream.sh
|
./scripts/setup-upstream.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+10
-5
@@ -1,6 +1,11 @@
|
|||||||
RustBox 0.2.3 release manifest
|
RustBox 0.2.4 release manifest
|
||||||
|
|
||||||
0.2.3 feature/configuration changes:
|
0.2.4 feature/configuration changes:
|
||||||
|
- 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
|
||||||
|
- pins Rust 1.99.1 and uses rust:1.99.1-trixie / debian:trixie-slim with libssl3t64
|
||||||
|
- moves PostgreSQL 17 Compose services to postgres:17-trixie
|
||||||
- optional Sieve, diagnostics and About feature switches
|
- optional Sieve, diagnostics and About feature switches
|
||||||
- API namespace has an explicit 404 fallback so disabled/unknown API routes cannot be served by the SPA fallback
|
- API namespace has an explicit 404 fallback so disabled/unknown API routes cannot be served by the SPA fallback
|
||||||
- opt-in security response headers (disabled by default)
|
- opt-in security response headers (disabled by default)
|
||||||
@@ -66,14 +71,14 @@ Docker examples:
|
|||||||
- docker/compose-mysql-nfs-opensearch.yml (MySQL + real NFS volume + OpenSearch)
|
- docker/compose-mysql-nfs-opensearch.yml (MySQL + real NFS volume + OpenSearch)
|
||||||
|
|
||||||
Operations/docs:
|
Operations/docs:
|
||||||
- README.md documents 0.2.3 storage/search configuration
|
- README.md documents 0.2.4 storage/search configuration
|
||||||
- docker/README.md documents Compose variants
|
- docker/README.md documents Compose variants
|
||||||
- docs/STORAGE_SEARCH_OPERATIONS.md documents failure modes, shared-storage requirements and migration behavior
|
- 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
|
- TODO.md leaves live SILO/MinIO/AWS/Ceph and real multi-node acceptance validation explicitly pending
|
||||||
|
|
||||||
Build/release metadata:
|
Build/release metadata:
|
||||||
- `scripts/package-release.sh` runs the full Rust release gate before creating ZIP/SHA256 artifacts
|
- `scripts/package-release.sh` runs the full Rust release gate before creating ZIP/SHA256 artifacts
|
||||||
- Cargo.toml version = 0.2.3
|
- Cargo.toml version = 0.2.4
|
||||||
- rust-toolchain.toml pins Rust 1.88.0
|
- rust-toolchain.toml pins Rust 1.99.1
|
||||||
- Cargo.lock is intentionally not shipped; the release gate regenerates it before locked checks
|
- 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
|
- CI and scripts/build-release.sh generate Cargo.lock, then run locked tests/Clippy/release build
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
RustBox 0.2.3 - final source validation
|
RustBox 0.2.4 - final source validation
|
||||||
|
|
||||||
0.2.3 scope:
|
0.2.4 scope:
|
||||||
|
- 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
|
||||||
|
- Debian Trixie RustBox images and PostgreSQL 17 Trixie Compose variants
|
||||||
- feature gates for Sieve/diagnostics/About and UI policy exposure
|
- feature gates for Sieve/diagnostics/About and UI policy exposure
|
||||||
- API fallback isolation: unknown/disabled `/api/*` routes return 404 and never fall through to the SPA UI
|
- API fallback isolation: unknown/disabled `/api/*` routes return 404 and never fall through to the SPA UI
|
||||||
- reset-all-settings preserving account/mail data
|
- reset-all-settings preserving account/mail data
|
||||||
- optional browser security headers
|
- optional browser security headers
|
||||||
- Docker two-node application topology with Envoy/Valkey/OpenSearch/PostgreSQL/Garage
|
- Docker two-node application topology with Envoy/Valkey/OpenSearch/PostgreSQL/Garage
|
||||||
- version metadata updated to 0.2.3
|
- version metadata updated to 0.2.4
|
||||||
|
|
||||||
Completed in this environment:
|
Completed in this environment:
|
||||||
- offline release validator: scripts/validate-release-static.py (196/196 checks passed)
|
- offline release validator: scripts/validate-release-static.py (207/207 checks passed)
|
||||||
- all maintained TOML examples parse successfully
|
- all maintained TOML examples parse successfully
|
||||||
- Docker Compose topology/config references are validated by the offline release validator
|
- Docker Compose topology/config references are validated by the offline release validator
|
||||||
- JSON files parse successfully
|
- JSON files parse successfully
|
||||||
@@ -35,7 +39,7 @@ Completed in this environment:
|
|||||||
- 0.2.1 OpenPGP visibility, OpenSearch backpressure and HTTP/dev cookie fixes remain in place
|
- 0.2.1 OpenPGP visibility, OpenSearch backpressure and HTTP/dev cookie fixes remain in place
|
||||||
|
|
||||||
Not executed in this packaging environment:
|
Not executed in this packaging environment:
|
||||||
- cargo fmt/test/clippy/build with Rust 1.88, because Rust/Cargo is not installed here and outbound DNS is unavailable
|
- cargo fmt/test/clippy/build with Rust 1.99.1, because Rust/Cargo is not installed in this validation environment
|
||||||
- live PostgreSQL/MySQL/S3/NFS/OpenSearch integration tests
|
- live PostgreSQL/MySQL/S3/NFS/OpenSearch integration tests
|
||||||
|
|
||||||
The final release gate on a Rust host is:
|
The final release gate on a Rust host is:
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# RustBox 0.2.3
|
# RustBox 0.2.4
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
port = 3001
|
port = 3001
|
||||||
environment = "production"
|
environment = "production"
|
||||||
|
|||||||
+3
-3
@@ -1,11 +1,11 @@
|
|||||||
FROM rust:1.88-bookworm AS builder
|
FROM rust:1.99.1-trixie AS builder
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y --no-install-recommends pkg-config libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN cargo generate-lockfile && cargo build --release --locked
|
RUN cargo generate-lockfile && cargo build --release --locked
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:trixie-slim
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3 && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3t64 && rm -rf /var/lib/apt/lists/*
|
||||||
RUN useradd --system --uid 10001 --create-home --home-dir /var/lib/rustbox rustbox
|
RUN useradd --system --uid 10001 --create-home --home-dir /var/lib/rustbox rustbox
|
||||||
COPY --from=builder /build/target/release/rustbox /usr/local/bin/rustbox
|
COPY --from=builder /build/target/release/rustbox /usr/local/bin/rustbox
|
||||||
USER rustbox
|
USER rustbox
|
||||||
|
|||||||
+9
-9
@@ -10,8 +10,8 @@ Two RustBox nodes + Envoy + Valkey + PostgreSQL + OpenSearch + Garage S3:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
PORT=3009 \
|
PORT=3009 \
|
||||||
IMAP_HOST=mail.linuxiarz.pl \
|
IMAP_HOST=mail.example.com \
|
||||||
SMTP_HOST=mail.linuxiarz.pl \
|
SMTP_HOST=mail.example.com \
|
||||||
docker compose -p rustbox-ha -f compose-ha-garage.yml up --build
|
docker compose -p rustbox-ha -f compose-ha-garage.yml up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -26,16 +26,16 @@ PostgreSQL + S3/SILO + OpenSearch:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
PORT=3009 \
|
PORT=3009 \
|
||||||
IMAP_HOST=mail.linuxiarz.pl \
|
IMAP_HOST=mail.example.com \
|
||||||
SMTP_HOST=mail.linuxiarz.pl \
|
SMTP_HOST=mail.example.com \
|
||||||
docker compose -p rustbox -f compose-postgres-s3-opensearch.yml up --build
|
docker compose -p rustbox -f compose-postgres-s3-opensearch.yml up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
SQLite, local blobs and local Tantivy:
|
SQLite, local blobs and local Tantivy:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
IMAP_HOST=mail.linuxiarz.pl \
|
IMAP_HOST=mail.example.com \
|
||||||
SMTP_HOST=mail.linuxiarz.pl \
|
SMTP_HOST=mail.example.com \
|
||||||
docker compose -p rustbox -f compose-single-sqlite.yml up --build
|
docker compose -p rustbox -f compose-single-sqlite.yml up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -44,8 +44,8 @@ MySQL + NFS + OpenSearch:
|
|||||||
```bash
|
```bash
|
||||||
NFS_SERVER=10.0.0.10 \
|
NFS_SERVER=10.0.0.10 \
|
||||||
NFS_EXPORT=/exports/rustbox \
|
NFS_EXPORT=/exports/rustbox \
|
||||||
IMAP_HOST=mail.linuxiarz.pl \
|
IMAP_HOST=mail.example.com \
|
||||||
SMTP_HOST=mail.linuxiarz.pl \
|
SMTP_HOST=mail.example.com \
|
||||||
docker compose -p rustbox -f compose-mysql-nfs-opensearch.yml up --build
|
docker compose -p rustbox -f compose-mysql-nfs-opensearch.yml up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -201,4 +201,4 @@ docker compose -p rustbox -f compose-postgres-s3-opensearch.yml exec opensearch
|
|||||||
-d '{"index.blocks.read_only_allow_delete":null}'
|
-d '{"index.blocks.read_only_allow_delete":null}'
|
||||||
```
|
```
|
||||||
|
|
||||||
RustBox 0.2.3 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.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.
|
||||||
@@ -42,7 +42,7 @@ x-rustbox-common: &rustbox-common
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:17-bookworm
|
image: postgres:17-trixie
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: rustbox
|
POSTGRES_DB: rustbox
|
||||||
POSTGRES_USER: rustbox
|
POSTGRES_USER: rustbox
|
||||||
@@ -103,7 +103,7 @@ services:
|
|||||||
- garage-data:/var/lib/garage/data
|
- garage-data:/var/lib/garage/data
|
||||||
networks:
|
networks:
|
||||||
rustbox-ha:
|
rustbox-ha:
|
||||||
ipv4_address: 172.28.80.10
|
ipv4_address: 10.240.6.10
|
||||||
|
|
||||||
garage-init:
|
garage-init:
|
||||||
build:
|
build:
|
||||||
@@ -154,4 +154,4 @@ networks:
|
|||||||
rustbox-ha:
|
rustbox-ha:
|
||||||
ipam:
|
ipam:
|
||||||
config:
|
config:
|
||||||
- subnet: 172.28.80.0/24
|
- subnet: 10.240.6.0/24
|
||||||
@@ -2,7 +2,7 @@ name: rustbox
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:17-bookworm
|
image: postgres:17-trixie
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: rustbox
|
POSTGRES_DB: rustbox
|
||||||
POSTGRES_USER: rustbox
|
POSTGRES_USER: rustbox
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ sieve_enabled = false
|
|||||||
diagnostics_enabled = true
|
diagnostics_enabled = true
|
||||||
about_enabled = true
|
about_enabled = true
|
||||||
security_headers_enabled = false
|
security_headers_enabled = false
|
||||||
trusted_proxies = "172.28.80.0/24"
|
trusted_proxies = "10.240.6.0/24"
|
||||||
|
|
||||||
[storage]
|
[storage]
|
||||||
backend = "s3"
|
backend = "s3"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ data_dir = "/var/lib/garage/data"
|
|||||||
db_engine = "sqlite"
|
db_engine = "sqlite"
|
||||||
replication_factor = 1
|
replication_factor = 1
|
||||||
rpc_bind_addr = "0.0.0.0:3901"
|
rpc_bind_addr = "0.0.0.0:3901"
|
||||||
rpc_public_addr = "172.28.80.10:3901"
|
rpc_public_addr = "10.240.6.10:3901"
|
||||||
rpc_secret = "8f2ec1b94f2ff5fc39391d8e1666929f8d6adc4f2270d5c950866a0a65e5f4b3"
|
rpc_secret = "8f2ec1b94f2ff5fc39391d8e1666929f8d6adc4f2270d5c950866a0a65e5f4b3"
|
||||||
|
|
||||||
[s3_api]
|
[s3_api]
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# RustBox 0.2.3 - implementation status
|
# RustBox 0.2.4 - implementation status
|
||||||
|
|
||||||
This file tracks the `0.2.3` 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.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.
|
||||||
|
|
||||||
## 0.2.3 storage/search architecture
|
## 0.2.4 storage/search architecture
|
||||||
|
|
||||||
- SQL backend selection (`sqlite`/`mysql`/`pgsql`) is independent from blob storage and search backend selection.
|
- 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.
|
- 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.
|
||||||
|
|||||||
+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.
|
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.3
|
## Updating an existing checkout to 0.2.4
|
||||||
|
|
||||||
Do not overlay the release with `cp *` or another command that skips dotfiles. That can retain stale `.github`, `.env.example`, or `Cargo.lock` files.
|
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.3 storage/search operations
|
# RustBox 0.2.4 storage/search operations
|
||||||
|
|
||||||
RustBox 0.2.3 treats SQL, blob storage and search as independent capabilities. The database engine does not decide whether `data_dir` is allowed.
|
RustBox 0.2.4 treats SQL, blob storage and search as independent capabilities. The database engine does not decide whether `data_dir` is allowed.
|
||||||
|
|
||||||
## Deployment matrix
|
## Deployment matrix
|
||||||
|
|
||||||
@@ -83,4 +83,4 @@ Cross-node realtime event fan-out remains separate from blob/search storage and
|
|||||||
|
|
||||||
## Validation status
|
## Validation status
|
||||||
|
|
||||||
The source package includes `scripts/validate-release-static.py` for offline structural checks. Full release validation still requires a Rust 1.88 build host and live service tests against the exact PostgreSQL/MySQL, S3/NFS and OpenSearch infrastructure used in production. Live SILO/MinIO/AWS/Ceph RGW and multi-node acceptance items remain intentionally unchecked in `TODO.md` until executed.
|
The source package includes `scripts/validate-release-static.py` for offline structural checks. Full release validation still requires a Rust 1.99.1 build host and live service tests against the exact PostgreSQL/MySQL, S3/NFS and OpenSearch infrastructure used in production. Live SILO/MinIO/AWS/Ceph RGW and multi-node acceptance items remain intentionally unchecked in `TODO.md` until executed.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Database schemas and migrations
|
# Database schemas and migrations
|
||||||
|
|
||||||
RustBox 0.2.3 keeps SQL, blob storage and search as independent concerns:
|
RustBox 0.2.4 keeps SQL, blob storage and search as independent concerns:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
database_backend = sqlite | mysql | pgsql
|
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.
|
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.3 is **V048**.
|
The next migration number after RustBox 0.2.4 is **V048**.
|
||||||
|
|
||||||
Redis and Redis Sentinel do not use SQL migrations.
|
Redis and Redis Sentinel do not use SQL migrations.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# RustBox 0.2.3 - packaged production configuration (DEB/RPM)
|
# RustBox 0.2.4 - packaged production configuration (DEB/RPM)
|
||||||
#
|
#
|
||||||
# This file contains all supported RustBox configuration options.
|
# This file contains all supported RustBox configuration options.
|
||||||
# Commented options are optional and do not change the packaged defaults.
|
# Commented options are optional and do not change the packaged defaults.
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.88.0"
|
channel = "1.99.1"
|
||||||
components = ["rustfmt", "clippy"]
|
components = ["rustfmt", "clippy"]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Static/offline release checks for RustBox 0.2.3.
|
"""Static/offline release checks for RustBox 0.2.4.
|
||||||
|
|
||||||
These checks intentionally avoid pretending to replace `cargo test`: they validate
|
These checks intentionally avoid pretending to replace `cargo test`: they validate
|
||||||
configuration, SQL migrations, Docker examples, query-catalog parity and release
|
configuration, SQL migrations, Docker examples, query-catalog parity and release
|
||||||
@@ -49,10 +49,10 @@ def extract_section(text: str, begin: str, end: str) -> str:
|
|||||||
|
|
||||||
# Release/toolchain metadata.
|
# Release/toolchain metadata.
|
||||||
cargo = tomllib.loads(read("Cargo.toml"))
|
cargo = tomllib.loads(read("Cargo.toml"))
|
||||||
require(cargo["package"]["version"] == "0.2.3", "Cargo package version is 0.2.3")
|
require(cargo["package"]["version"] == "0.2.4", "Cargo package version is 0.2.4")
|
||||||
require(cargo["package"]["rust-version"] == "1.88", "Cargo rust-version is 1.88")
|
require(cargo["package"]["rust-version"] == "1.99.1", "Cargo rust-version is 1.99.1")
|
||||||
toolchain = tomllib.loads(read("rust-toolchain.toml"))
|
toolchain = tomllib.loads(read("rust-toolchain.toml"))
|
||||||
require(toolchain["toolchain"]["channel"] == "1.88.0", "rust-toolchain pins Rust 1.88.0")
|
require(toolchain["toolchain"]["channel"] == "1.99.1", "rust-toolchain pins Rust 1.99.1")
|
||||||
require(not (ROOT / "Cargo.lock").exists(), "stale Cargo.lock is not shipped")
|
require(not (ROOT / "Cargo.lock").exists(), "stale Cargo.lock is not shipped")
|
||||||
require("cargo generate-lockfile" in read("scripts/build-release.sh"), "release script regenerates Cargo.lock")
|
require("cargo generate-lockfile" in read("scripts/build-release.sh"), "release script regenerates Cargo.lock")
|
||||||
release_script = read("scripts/build-release.sh")
|
release_script = read("scripts/build-release.sh")
|
||||||
@@ -65,15 +65,28 @@ 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("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("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("validate-release-static.py" in read(".github/workflows/ci.yml"), "CI runs offline/static release gate")
|
||||||
require(read("README.md").startswith("# RustBox 0.2.3"), "README identifies RustBox 0.2.3")
|
require(read("README.md").startswith("# RustBox 0.2.4"), "README identifies RustBox 0.2.4")
|
||||||
require(read("RELEASE-MANIFEST.txt").startswith("RustBox 0.2.3"), "release manifest identifies RustBox 0.2.3")
|
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.3"), "release validation identifies RustBox 0.2.3")
|
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.3"), "implementation status identifies RustBox 0.2.3")
|
require(read("docs/IMPLEMENTATION_STATUS.md").startswith("# RustBox 0.2.4"), "implementation status identifies RustBox 0.2.4")
|
||||||
require((ROOT / "docs/STORAGE_SEARCH_OPERATIONS.md").is_file(), "storage/search operations guide exists")
|
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('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")
|
require(not re.search(r"(?m)^IMAP_HOST=\s*$", read(".env.example")), "environment example does not set an invalid empty IMAP host")
|
||||||
require("pgp_enabled: bool" in read("src/routes/auth.rs"), "auth policy exposes OpenPGP availability")
|
require("pgp_enabled: bool" in read("src/routes/auth.rs"), "auth policy exposes OpenPGP availability")
|
||||||
require("setOpenPgpAvailability" in read("src/web/js/72-openpgp-identities.js"), "UI hides administratively disabled OpenPGP")
|
require("setOpenPgpAvailability" in read("src/web/js/72-openpgp-identities.js"), "UI hides administratively disabled OpenPGP")
|
||||||
|
storage_rs = read("src/storage/mod.rs")
|
||||||
|
require(
|
||||||
|
".with_allow_http(storage.allow_http)" in storage_rs.split("let client_options = ClientOptions::new()", 1)[1].split("let retry =", 1)[0],
|
||||||
|
"S3 allow_http is applied to the final ClientOptions",
|
||||||
|
)
|
||||||
|
require(
|
||||||
|
".with_allow_http(storage.allow_http)" not in storage_rs.split("let mut builder = AmazonS3Builder::from_env()", 1)[1].split("if let Some(endpoint)", 1)[0],
|
||||||
|
"S3 builder does not overwrite allow_http with replacement client options",
|
||||||
|
)
|
||||||
|
require(
|
||||||
|
"s3_http_endpoint_works_when_explicitly_allowed" in storage_rs,
|
||||||
|
"S3 HTTP regression test is present",
|
||||||
|
)
|
||||||
config_rs = read("src/config.rs")
|
config_rs = read("src/config.rs")
|
||||||
routes_mod = read("src/routes/mod.rs")
|
routes_mod = read("src/routes/mod.rs")
|
||||||
require("pub sieve_enabled: bool" in config_rs and "pub diagnostics_enabled: bool" in config_rs and "pub about_enabled: bool" in config_rs, "configuration exposes Sieve, diagnostics and About feature gates")
|
require("pub sieve_enabled: bool" in config_rs and "pub diagnostics_enabled: bool" in config_rs and "pub about_enabled: bool" in config_rs, "configuration exposes Sieve, diagnostics and About feature gates")
|
||||||
@@ -215,7 +228,16 @@ require(ha["cluster_mode"] is True and ha["session_backend"] == "redis", "HA/Gar
|
|||||||
require(ha["redis_url"] == "redis://valkey:6379/", "HA/Garage config targets Valkey for shared sessions")
|
require(ha["redis_url"] == "redis://valkey:6379/", "HA/Garage config targets Valkey for shared sessions")
|
||||||
require(ha["storage"]["backend"] == "s3" and ha["storage"]["endpoint"] == "http://garage:3900", "HA/Garage config stores blobs in Garage S3")
|
require(ha["storage"]["backend"] == "s3" and ha["storage"]["endpoint"] == "http://garage:3900", "HA/Garage config stores blobs in Garage S3")
|
||||||
require(ha["search"]["backend"] == "opensearch", "HA/Garage config uses OpenSearch")
|
require(ha["search"]["backend"] == "opensearch", "HA/Garage config uses OpenSearch")
|
||||||
|
dockerfile = read("docker/Dockerfile")
|
||||||
|
require("FROM rust:1.99.1-trixie AS builder" in dockerfile, "Docker builder pins Rust 1.99.1 on Debian Trixie")
|
||||||
|
require("FROM debian:trixie-slim" in dockerfile, "Docker runtime uses Debian Trixie slim")
|
||||||
|
require("libssl3t64" in dockerfile and "libssl3 " not in dockerfile, "Docker runtime uses the Debian Trixie OpenSSL runtime package")
|
||||||
|
require("bookworm" not in dockerfile.lower(), "RustBox Dockerfile no longer uses Debian Bookworm")
|
||||||
|
require("postgres:17-trixie" in read("docker/compose-postgres-s3-opensearch.yml"), "PostgreSQL/S3 Compose uses PostgreSQL 17 Trixie")
|
||||||
|
require("bookworm" not in read("docker/compose-postgres-s3-opensearch.yml").lower(), "PostgreSQL/S3 Compose no longer uses Bookworm")
|
||||||
ha_compose = read("docker/compose-ha-garage.yml")
|
ha_compose = read("docker/compose-ha-garage.yml")
|
||||||
|
require("postgres:17-trixie" in ha_compose, "HA Compose uses PostgreSQL 17 Trixie")
|
||||||
|
require("bookworm" not in ha_compose.lower(), "HA Compose no longer uses Bookworm")
|
||||||
require("rustbox-1:" in ha_compose and "rustbox-2:" in ha_compose, "HA Compose defines two RustBox application nodes")
|
require("rustbox-1:" in ha_compose and "rustbox-2:" in ha_compose, "HA Compose defines two RustBox application nodes")
|
||||||
for service in ["postgres:", "valkey:", "opensearch:", "garage:", "envoy:"]:
|
for service in ["postgres:", "valkey:", "opensearch:", "garage:", "envoy:"]:
|
||||||
require(service in ha_compose, f"HA Compose includes {service.removesuffix(':')}")
|
require(service in ha_compose, f"HA Compose includes {service.removesuffix(':')}")
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ pub fn provision_user_data_with_search(
|
|||||||
.map_err(|e| format!("failed to create search directory: {e}"))?;
|
.map_err(|e| format!("failed to create search directory: {e}"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure this account has its isolated tables in the configured RustBox database.
|
|
||||||
// The same database backend stores sessions unless session_backend=redis.
|
|
||||||
let conn = crate::db::pool::open_user_db(data_dir, user_hash)
|
let conn = crate::db::pool::open_user_db(data_dir, user_hash)
|
||||||
.map_err(|e| format!("failed to initialize user database: {e}"))?;
|
.map_err(|e| format!("failed to initialize user database: {e}"))?;
|
||||||
drop(conn);
|
drop(conn);
|
||||||
|
|||||||
+2
-35
@@ -20,8 +20,6 @@ const SQLITE_SCHEMA: &str = include_str!("../../migrations/sqlite/schema.sql");
|
|||||||
const MYSQL_SCHEMA: &str = include_str!("../../migrations/mysql/schema.sql");
|
const MYSQL_SCHEMA: &str = include_str!("../../migrations/mysql/schema.sql");
|
||||||
const PGSQL_SCHEMA: &str = include_str!("../../migrations/pgsql/schema.sql");
|
const PGSQL_SCHEMA: &str = include_str!("../../migrations/pgsql/schema.sql");
|
||||||
|
|
||||||
// The squashed legacy baseline is V046. Runtime migrations start at V047 and
|
|
||||||
// are stored as real files for every supported SQL engine.
|
|
||||||
const SQLITE_MIGRATIONS: &[(u32, &str)] = &[(
|
const SQLITE_MIGRATIONS: &[(u32, &str)] = &[(
|
||||||
47,
|
47,
|
||||||
include_str!("../../migrations/sqlite/V047__blob_keys_and_search_outbox.sql"),
|
include_str!("../../migrations/sqlite/V047__blob_keys_and_search_outbox.sql"),
|
||||||
@@ -65,7 +63,6 @@ const TENANT_TABLES: &[&str] = &[
|
|||||||
"user_meta",
|
"user_meta",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Parent tables precede children so legacy SQLite data can be copied with foreign keys enabled.
|
|
||||||
const IMPORT_TABLES: &[&str] = &[
|
const IMPORT_TABLES: &[&str] = &[
|
||||||
"calendar_events",
|
"calendar_events",
|
||||||
"calendar_settings",
|
"calendar_settings",
|
||||||
@@ -229,12 +226,6 @@ pub async fn configure(config: &AppConfig) -> Result<(), String> {
|
|||||||
conn.execute_batch(&global_schema(SQLITE_SCHEMA)?)
|
conn.execute_batch(&global_schema(SQLITE_SCHEMA)?)
|
||||||
.map_err(|e| format!("SQLite global schema initialization failed: {e}"))?;
|
.map_err(|e| format!("SQLite global schema initialization failed: {e}"))?;
|
||||||
|
|
||||||
// Unit tests create many independent temporary data directories in one
|
|
||||||
// process. Pinning the first SQLite path in the process-wide OnceLock
|
|
||||||
// makes later tests reuse a TempDir that may already have been deleted,
|
|
||||||
// causing a cascade of "unable to open database file" failures.
|
|
||||||
// Initialize the requested test database, but let open_user_db() resolve
|
|
||||||
// each test's own data_dir instead of publishing this path globally.
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
{
|
{
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -287,9 +278,6 @@ pub async fn configure(config: &AppConfig) -> Result<(), String> {
|
|||||||
let schema = global_schema(PGSQL_SCHEMA)?;
|
let schema = global_schema(PGSQL_SCHEMA)?;
|
||||||
let init_pool = pool.clone();
|
let init_pool = pool.clone();
|
||||||
runtime_wait(&runtime, async move {
|
runtime_wait(&runtime, async move {
|
||||||
// PostgreSQL emits NOTICE for every CREATE ... IF NOT EXISTS that
|
|
||||||
// already exists. Keep expected idempotent startup DDL quiet without
|
|
||||||
// changing application-wide logging or server settings.
|
|
||||||
let mut tx = init_pool
|
let mut tx = init_pool
|
||||||
.begin()
|
.begin()
|
||||||
.await
|
.await
|
||||||
@@ -298,11 +286,6 @@ pub async fn configure(config: &AppConfig) -> Result<(), String> {
|
|||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("failed to quiet PostgreSQL schema notices: {e}"))?;
|
.map_err(|e| format!("failed to quiet PostgreSQL schema notices: {e}"))?;
|
||||||
// `raw_sql(...).execute(&mut PgConnection)` can trigger SQLx's
|
|
||||||
// "Executor is not general enough" lifetime error when this
|
|
||||||
// future is spawned onto the dedicated database runtime. Execute
|
|
||||||
// the unprepared multi-statement DDL directly through `Executor`
|
|
||||||
// instead; this is SQLx's supported path for raw DDL batches.
|
|
||||||
(&mut *tx)
|
(&mut *tx)
|
||||||
.execute(schema.as_str())
|
.execute(schema.as_str())
|
||||||
.await
|
.await
|
||||||
@@ -460,7 +443,6 @@ pub trait ToDbValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kept explicit rather than clever: the macro above intentionally stays simple for rustfmt/parser compatibility.
|
|
||||||
impl ToDbValue for i64 {
|
impl ToDbValue for i64 {
|
||||||
fn to_db_value(&self) -> DbValue {
|
fn to_db_value(&self) -> DbValue {
|
||||||
DbValue::I64(Some(*self))
|
DbValue::I64(Some(*self))
|
||||||
@@ -601,10 +583,6 @@ impl IntoDbParams for [(); 0] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not use a const-generic array implementation here. For an empty array (`[]`)
|
|
||||||
// Rust cannot infer the element type and sees both `[(); 0]` and `[T; 0]` as valid
|
|
||||||
// `IntoDbParams` candidates. Implement only non-empty array lengths explicitly so
|
|
||||||
// legacy/rusqlite-style `[]` call sites remain unambiguous.
|
|
||||||
macro_rules! impl_into_db_params_array {
|
macro_rules! impl_into_db_params_array {
|
||||||
($($n:expr),+ $(,)?) => {
|
($($n:expr),+ $(,)?) => {
|
||||||
$(
|
$(
|
||||||
@@ -1457,8 +1435,6 @@ fn adapt_sql_params(
|
|||||||
return (sql, params);
|
return (sql, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic query builders in the application use SQLite-style ?N placeholders.
|
|
||||||
// Normalize them here so the same higher-level logic can target all engines.
|
|
||||||
let chars: Vec<char> = sql.chars().collect();
|
let chars: Vec<char> = sql.chars().collect();
|
||||||
let mut output = String::with_capacity(sql.len() + 16);
|
let mut output = String::with_capacity(sql.len() + 16);
|
||||||
let mut reordered = Vec::new();
|
let mut reordered = Vec::new();
|
||||||
@@ -1522,8 +1498,6 @@ fn adapt_sql_params(
|
|||||||
|
|
||||||
sql = output;
|
sql = output;
|
||||||
if backend == DatabaseBackend::Mysql && !reordered.is_empty() {
|
if backend == DatabaseBackend::Mysql && !reordered.is_empty() {
|
||||||
// If numbered placeholders were expanded, bind in occurrence order.
|
|
||||||
// Plain MySQL catalog queries already use '?' and are handled the same way.
|
|
||||||
return (adapt_dialect_sql(backend, sql), reordered);
|
return (adapt_dialect_sql(backend, sql), reordered);
|
||||||
}
|
}
|
||||||
(adapt_dialect_sql(backend, sql), params)
|
(adapt_dialect_sql(backend, sql), params)
|
||||||
@@ -1855,9 +1829,6 @@ fn tenant_prefix(user_hash: &str) -> Result<String, String> {
|
|||||||
return Err("empty user hash for database isolation".to_string());
|
return Err("empty user hash for database isolation".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Production user hashes are SHA-256 hex strings. Tests and legacy callers may
|
|
||||||
// use symbolic identifiers; hash those first so table prefixes remain safe,
|
|
||||||
// deterministic and restricted to ASCII hex without weakening isolation.
|
|
||||||
let normalized = if user_hash.len() >= 24 && user_hash.chars().all(|c| c.is_ascii_hexdigit()) {
|
let normalized = if user_hash.len() >= 24 && user_hash.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||||
user_hash.to_ascii_lowercase()
|
user_hash.to_ascii_lowercase()
|
||||||
} else {
|
} else {
|
||||||
@@ -2060,8 +2031,6 @@ fn ensure_server_tenant(
|
|||||||
let tenant = tenant_schema(MYSQL_SCHEMA, &prefix)?;
|
let tenant = tenant_schema(MYSQL_SCHEMA, &prefix)?;
|
||||||
let legacy_path = legacy_path.clone();
|
let legacy_path = legacy_path.clone();
|
||||||
runtime_wait(runtime, async move {
|
runtime_wait(runtime, async move {
|
||||||
// Serialize first-time provisioning per account across cluster nodes.
|
|
||||||
// MySQL named locks are session-scoped and are released automatically if the connection dies.
|
|
||||||
let lock_name = format!("rustbox:{prefix}");
|
let lock_name = format!("rustbox:{prefix}");
|
||||||
let mut lock_conn = pool
|
let mut lock_conn = pool
|
||||||
.acquire()
|
.acquire()
|
||||||
@@ -2210,10 +2179,8 @@ fn ensure_server_tenant(
|
|||||||
ConfiguredDatabase::Sqlite { .. } => false,
|
ConfiguredDatabase::Sqlite { .. } => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if imported_legacy {
|
if imported_legacy && let Some(path) = legacy_path.as_ref() {
|
||||||
if let Some(path) = legacy_path.as_ref() {
|
backup_legacy_sqlite(path);
|
||||||
backup_legacy_sqlite(path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// Query catalogs intentionally expose parallel SQL dialects. Not every catalog
|
|
||||||
// is used in a SQLite-only process, so dead-code suppression is scoped to the
|
|
||||||
// dialect modules instead of the rest of the crate.
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub mod mysql;
|
pub mod mysql;
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|||||||
@@ -194,8 +194,6 @@ pub async fn delete_attachment(
|
|||||||
return Err(AppError::NotFound("Attachment not found".to_string()));
|
return Err(AppError::NotFound("Attachment not found".to_string()));
|
||||||
}
|
}
|
||||||
if let Err(error) = storage.delete(&storage_key).await {
|
if let Err(error) = storage.delete(&storage_key).await {
|
||||||
// SQL is authoritative. A failed physical delete leaves an orphaned blob,
|
|
||||||
// which is safer than leaving a DB row pointing at a missing attachment.
|
|
||||||
tracing::warn!(error = %error, storage_key = %storage_key, "Failed to remove deleted attachment blob");
|
tracing::warn!(error = %error, storage_key = %storage_key, "Failed to remove deleted attachment blob");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+68
-2
@@ -223,6 +223,7 @@ fn s3_storage(storage: &StorageConfig) -> Result<Arc<dyn BlobStorage>, String> {
|
|||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.ok_or_else(|| "storage.bucket is required for S3".to_string())?;
|
.ok_or_else(|| "storage.bucket is required for S3".to_string())?;
|
||||||
let client_options = ClientOptions::new()
|
let client_options = ClientOptions::new()
|
||||||
|
.with_allow_http(storage.allow_http)
|
||||||
.with_connect_timeout(Duration::from_secs(storage.connect_timeout_secs))
|
.with_connect_timeout(Duration::from_secs(storage.connect_timeout_secs))
|
||||||
.with_timeout(Duration::from_secs(storage.request_timeout_secs));
|
.with_timeout(Duration::from_secs(storage.request_timeout_secs));
|
||||||
let retry = RetryConfig {
|
let retry = RetryConfig {
|
||||||
@@ -234,7 +235,6 @@ fn s3_storage(storage: &StorageConfig) -> Result<Arc<dyn BlobStorage>, String> {
|
|||||||
.with_bucket_name(bucket)
|
.with_bucket_name(bucket)
|
||||||
.with_region(storage.region.trim())
|
.with_region(storage.region.trim())
|
||||||
.with_virtual_hosted_style_request(!storage.path_style)
|
.with_virtual_hosted_style_request(!storage.path_style)
|
||||||
.with_allow_http(storage.allow_http)
|
|
||||||
.with_client_options(client_options)
|
.with_client_options(client_options)
|
||||||
.with_retry(retry)
|
.with_retry(retry)
|
||||||
.with_checksum_algorithm(Checksum::SHA256);
|
.with_checksum_algorithm(Checksum::SHA256);
|
||||||
@@ -470,7 +470,6 @@ pub async fn migrate_attachments(
|
|||||||
) {
|
) {
|
||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
// A logical DB key may already point at the configured destination after a previous run.
|
|
||||||
if attachment.storage_key == target_key
|
if attachment.storage_key == target_key
|
||||||
&& destination.exists(&target_key).await?
|
&& destination.exists(&target_key).await?
|
||||||
{
|
{
|
||||||
@@ -583,6 +582,13 @@ pub async fn migrate_attachments(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::AppConfig;
|
use crate::config::AppConfig;
|
||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
body::Body,
|
||||||
|
extract::State,
|
||||||
|
http::{Response, StatusCode, header},
|
||||||
|
};
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -621,6 +627,66 @@ mod tests {
|
|||||||
assert!(!storage.exists(&key).await.expect("missing"));
|
assert!(!storage.exists(&key).await.expect("missing"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn s3_http_probe(State(hits): State<Arc<AtomicUsize>>) -> Response<Body> {
|
||||||
|
hits.fetch_add(1, Ordering::SeqCst);
|
||||||
|
let mut response = Response::new(Body::empty());
|
||||||
|
*response.status_mut() = StatusCode::OK;
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.insert(header::CONTENT_LENGTH, "0".parse().expect("content length"));
|
||||||
|
response.headers_mut().insert(
|
||||||
|
header::LAST_MODIFIED,
|
||||||
|
"Sat, 31 Oct 2026 20:00:00 GMT"
|
||||||
|
.parse()
|
||||||
|
.expect("last modified"),
|
||||||
|
);
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.insert(header::ETAG, "\"rustbox-test\"".parse().expect("etag"));
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn s3_http_endpoint_works_when_explicitly_allowed() {
|
||||||
|
let hits = Arc::new(AtomicUsize::new(0));
|
||||||
|
let app = Router::new()
|
||||||
|
.fallback(s3_http_probe)
|
||||||
|
.with_state(hits.clone());
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
||||||
|
.await
|
||||||
|
.expect("bind test S3 endpoint");
|
||||||
|
let endpoint = format!(
|
||||||
|
"http://{}",
|
||||||
|
listener.local_addr().expect("test S3 endpoint address")
|
||||||
|
);
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
axum::serve(listener, app)
|
||||||
|
.await
|
||||||
|
.expect("serve test S3 endpoint");
|
||||||
|
});
|
||||||
|
|
||||||
|
let storage = StorageConfig {
|
||||||
|
backend: "s3".to_string(),
|
||||||
|
bucket: Some("rustbox-test".to_string()),
|
||||||
|
region: "us-east-1".to_string(),
|
||||||
|
endpoint: Some(endpoint),
|
||||||
|
path_style: true,
|
||||||
|
allow_http: true,
|
||||||
|
access_key_id: Some("test-access-key".to_string()),
|
||||||
|
secret_access_key: Some("test-secret-key".to_string()),
|
||||||
|
max_retries: 0,
|
||||||
|
request_timeout_secs: 2,
|
||||||
|
connect_timeout_secs: 2,
|
||||||
|
retry_timeout_secs: 2,
|
||||||
|
..StorageConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let backend = s3_storage(&storage).expect("S3 backend with HTTP enabled");
|
||||||
|
assert!(backend.exists("probe").await.expect("HTTP S3 HEAD request"));
|
||||||
|
assert_eq!(hits.load(Ordering::SeqCst), 1);
|
||||||
|
server.abort();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn attachment_keys_are_logical_and_path_safe() {
|
fn attachment_keys_are_logical_and_path_safe() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
+10
-5
@@ -567,7 +567,8 @@
|
|||||||
data-i18n="filters">Filters</button><button class="settings-tab" data-tab="diagnostics"
|
data-i18n="filters">Filters</button><button class="settings-tab" data-tab="diagnostics"
|
||||||
type="button">Diagnostics</button><button class="settings-tab" data-tab="account"
|
type="button">Diagnostics</button><button class="settings-tab" data-tab="account"
|
||||||
type="button">Account</button><button class="settings-tab" data-tab="about" type="button"
|
type="button">Account</button><button class="settings-tab" data-tab="about" type="button"
|
||||||
data-i18n="about">About</button><button id="shortcuts-open" class="button quiet settings-shortcuts" type="button">Keyboard shortcuts</button></nav>
|
data-i18n="about">About</button><button id="shortcuts-open" class="button quiet settings-shortcuts"
|
||||||
|
type="button">Keyboard shortcuts</button></nav>
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
<section class="settings-panel" data-panel="appearance">
|
<section class="settings-panel" data-panel="appearance">
|
||||||
<div class="settings-panel-heading">
|
<div class="settings-panel-heading">
|
||||||
@@ -943,7 +944,8 @@
|
|||||||
</div><button id="identity-new" class="button primary" type="button">New identity</button>
|
</div><button id="identity-new" class="button primary" type="button">New identity</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="identity-overview">
|
<div class="identity-overview">
|
||||||
<div class="identity-overview-hint"><strong>Configured identities</strong><span id="identity-overview-help" class="muted">Select an
|
<div class="identity-overview-hint"><strong>Configured identities</strong><span
|
||||||
|
id="identity-overview-help" class="muted">Select an
|
||||||
identity to edit its sender data, text/HTML footer, image and OpenPGP defaults.</span></div>
|
identity to edit its sender data, text/HTML footer, image and OpenPGP defaults.</span></div>
|
||||||
<div id="identity-list" class="identity-list"></div>
|
<div id="identity-list" class="identity-list"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1165,8 +1167,10 @@
|
|||||||
<div class="settings-panel-heading account-session-heading">
|
<div class="settings-panel-heading account-session-heading">
|
||||||
<div>
|
<div>
|
||||||
<h3>Reset settings</h3>
|
<h3>Reset settings</h3>
|
||||||
<p class="muted">Restore appearance, mailbox, notifications, folder, calendar and OpenPGP preferences. Messages, identities, keys and templates are kept.</p>
|
<p class="muted">Restore appearance, mailbox, notifications, folder, calendar and OpenPGP preferences.
|
||||||
</div><button id="reset-all-settings" class="button quiet danger" type="button">Reset all settings</button>
|
Messages, identities, keys and templates are kept.</p>
|
||||||
|
</div><button id="reset-all-settings" class="button quiet danger" type="button">Reset all
|
||||||
|
settings</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="settings-panel hidden" data-panel="about">
|
<section class="settings-panel hidden" data-panel="about">
|
||||||
@@ -1253,7 +1257,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="identity-modal-section identity-modal-options">
|
<section class="identity-modal-section identity-modal-options">
|
||||||
<div class="identity-modal-heading"><strong>Defaults</strong><span id="identity-defaults-help" class="muted">Choose where this signature
|
<div class="identity-modal-heading"><strong>Defaults</strong><span id="identity-defaults-help"
|
||||||
|
class="muted">Choose where this signature
|
||||||
is inserted and, when OpenPGP is configured, its default behavior.</span></div>
|
is inserted and, when OpenPGP is configured, its default behavior.</span></div>
|
||||||
<div class="identity-toggle-grid">
|
<div class="identity-toggle-grid">
|
||||||
<label class="checkbox-row"><input id="identity-signature-new" type="checkbox"><span>Signature on new
|
<label class="checkbox-row"><input id="identity-signature-new" type="checkbox"><span>Signature on new
|
||||||
|
|||||||
Reference in new issue
Block a user