v0.2.32
RustBox CI / rustbox (push) Waiting to run

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-09 13:30:22 +01:00
1 parent 2932578424
commit 167fdfcb04
9 files changed
+68 -25

No files matched your search

+4
View File
@@ -2,6 +2,10 @@
What changed in each RustBox release, written for the people who use and run it. Technical details are in the [documentation](docs/).
## 0.2.32
- Message composer formatting now preserves the selected text when the toolbar is clicked. Bold, italic, underline and the other formatting controls apply to the original selection instead of collapsing it; link, font, size and text colour use the same preserved range.
## 0.2.31
- Phones and tablets: press and hold a message in the list to open a bottom sheet with its actions (read, flag, archive, delete as large buttons, then reply, forward, labels, move, filters and the rest of the desktop menu). The hover buttons no longer cover the subject and date on touch screens.
Generated
+1 -1
View File
@@ -3534,7 +3534,7 @@ dependencies = [
[[package]]
name = "rustbox"
version = "0.2.31"
version = "0.2.32"
dependencies = [
"aes-gcm",
"async-imap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rustbox"
version = "0.2.31"
version = "0.2.32"
edition = "2024"
rust-version = "1.99.1"
license = "AGPL-3.0-only"
+1 -1
View File
@@ -1,4 +1,4 @@
# RustBox 0.2.31
# RustBox 0.2.32
**Fast, self-hosted webmail written in Rust.** RustBox connects to any IMAP/SMTP server and ships as one executable with the whole interface built in, so there is no Node.js, PHP or separate frontend to deploy.
+6 -3
View File
@@ -1,4 +1,7 @@
RustBox 0.2.31 release manifest
RustBox 0.2.32 release manifest
0.2.32 release changes:
- composer formatting toolbar preserves the active DOM selection before toolbar focus changes and restores it before applying formatting; bold/italic/underline and the remaining commands, links, font, size and colour now target the text the user selected
0.2.31 release changes:
- touch screens: long press on a message row opens a bottom action sheet (read/unread, flag, archive, delete quick actions plus the full desktop context menu with in-place submenus); it follows the interface style colours, shape, scale and density, closes via backdrop, drag-down, Escape and Android back, and is centred on tablets; the hover quick-action overlay is hidden on touch screens instead of covering the subject and date
@@ -140,14 +143,14 @@ Docker examples:
Operations/docs:
- standalone `scripts/migrate_roundcube.py` imports Roundcube contacts/identities using RustBox config.toml and supports SQLite/MySQL/PostgreSQL targets
- README.md documents 0.2.31 configuration
- README.md documents 0.2.32 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.31
- Cargo.toml version = 0.2.32
- rust-toolchain.toml pins Rust 1.99.1
- Cargo.lock is retained for reproducible application builds; the release gate generates it only when missing
- CI and scripts/build-release.sh use the retained Cargo.lock (generating it only if missing), then run locked tests/Clippy/release build
+7 -4
View File
@@ -1,4 +1,7 @@
RustBox 0.2.31 - final source validation
RustBox 0.2.32 - final source validation
0.2.32 scope:
- composer toolbar keeps the selected DOM Range stable across toolbar focus changes, so bold/italic/underline and the remaining formatting controls apply to the intended selection
0.2.31 scope:
- touch long-press action sheet on the message list, exercised in Playwright against the built server with a mocked API (iPhone 13, Pixel 7, iPad Pro 11 landscape; 8 interface styles at small/normal/large scale, compact/comfortable density, reduced motion; Android back, drag-down, Escape); desktop hover actions and right-click menu unchanged
@@ -74,10 +77,10 @@ RustBox 0.2.31 - final source validation
- Composing shows only compact attachment/message size-limit values on one line
- 0.2.9 realtime notification gating, browser shortcut preservation and 300-message defaults remain intact
- V047-V052 are consolidated into compatibility baseline V052; V053 adds `skin_depth` and `skin_gradients`; V054 adds the theme schedule and account colour; V055 adds the CalDAV/CardDAV columns (`dav_name`, `dav_data`) and a non-unique contacts e-mail index; V056 is next
- version metadata identifies 0.2.31
- version metadata identifies 0.2.32
Completed in this environment:
- offline release validator: scripts/validate-release-static.py (467/467 checks passed)
- offline release validator: scripts/validate-release-static.py (468/468 checks passed)
- browser JavaScript assembled in manifest order passes `node --check`
- mailbox thread grouping smoke test covers a three-message References chain, expand/collapse behavior and flat-mode fallback
- reply-recipient helper smoke test covers own-account, alias and normal incoming-message replies
@@ -85,7 +88,7 @@ Completed in this environment:
- all JSON files parse successfully
- Python helper scripts compile successfully
- shell scripts pass `bash -n`
- release metadata consistently identifies 0.2.31
- release metadata consistently identifies 0.2.32
- V052 baseline consolidation plus the V053 skin-depth migration are validated offline
- 0.2.25 filter UI (apply now, run now, always move from sender), context submenus and preset editing exercised in Playwright against a mocked API; the 0.2.24 styles rendered at standard and immersive depth with gradients on and off; earlier: all 37 interface styles rendered in light and dark at the immersive depth against a mocked API (Playwright), with no page errors
+6 -2
View File
@@ -1,7 +1,11 @@
# RustBox 0.2.31 - implementation status
# RustBox 0.2.32 - implementation status
- Draft saves are realtime-silent: Drafts folder/list updates continue, but saving a draft does not surface as a new-mail notification.
This file tracks the `0.2.31` 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.32` 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.32 composer selection formatting
- The compose toolbar captures the editor DOM Range before toolbar controls take focus and restores it before executing a formatting command. Bold, italic, underline, lists, alignment, quoting, clear formatting, links, font, size and text colour therefore apply to the intended selected text.
## 0.2.31 touch action sheet
+2 -1
View File
@@ -167,7 +167,8 @@ require("const attachments = mergeComposeAttachments(state.composeAttachments);"
require('if (state.composeUploading) {' in compose_js and 'scheduleUnusedInlineAttachmentCleanup();' in compose_js and 'staleSignatureIds' in compose_js, "signature CID cleanup waits for in-flight uploads before deleting apparently unreferenced signature images")
require('function preferredComposeIdentity()' in read("src/web/js/72-openpgp-identities.js") and 'activeAccountEmail().toLowerCase()' in read("src/web/js/72-openpgp-identities.js") and 'if (preferred) select.value = String(preferred.id);' in read("src/web/js/72-openpgp-identities.js") and 'return state.identities.find((identity) => identity.id === id) || preferredComposeIdentity();' in read("src/web/js/72-openpgp-identities.js"), "composer resolves a concrete active/default identity before inserting signatures")
require('const hasAccountIdentity = Boolean(normalizedAccountEmail)' in read("src/web/js/72-openpgp-identities.js") and 'if (!hasAccountIdentity) {' in read("src/web/js/72-openpgp-identities.js"), "composer does not duplicate the active account address when a matching identity exists")
require('const command = key === "z" ? (event.shiftKey ? "redo" : "undo")' in bindings_js and 'document.execCommand(command, false, null);' in bindings_js, "composer supports Ctrl/Cmd+Z undo and redo shortcuts")
require('const command = key === "z" ? (event.shiftKey ? "redo" : "undo")' in bindings_js and 'runComposeCommand(command);' in bindings_js, "composer supports Ctrl/Cmd+Z undo and redo shortcuts")
require('let composeFormattingRange = null;' in bindings_js and 'rememberComposeFormattingRange' in bindings_js and 'restoreEditorRange(range, composeEditor);' in bindings_js and '.editor-toolbar button, .editor-toolbar select, .editor-toolbar input[type=\'color\']' in bindings_js and 'runComposeCommand(button.dataset.command' in bindings_js, "composer formatting toolbar preserves the selected text while controls take focus")
require('.message-pagination {' in mail_css and 'grid-template-columns: 30px 30px auto 30px 30px;' in mail_css and '.pagination-arrow {' in mail_css and '.load-more {' in mail_css and 'width: 100%;' in mail_css.split('.load-more {', 1)[1].split('}', 1)[0], "message pagination uses compact first/previous/next/last controls while progressive-load controls span the message list width")
require('id="compose-fields-toggle"' in read("src/web/index.html") and 'function setComposeFieldsCollapsed' in compose_js and 'function toggleComposeFields' in compose_js and '.compose-dialog.compose-fields-collapsed .compose-addresses' in compose_css and 'setComposeFieldsCollapsed(false)' in bindings_js, "composer addressing fields can collapse to maximize editor space and reopen on invalid recipient input")
require('id="compose-save-draft"' in read("src/web/index.html") and '$("#compose-save-draft").addEventListener("click", saveComposeDraftToImap);' in bindings_js and 'saveCurrentDraft(false, true, true)' in compose_js, "composer exposes an explicit manual IMAP draft save before cancel")
+40 -12
View File
@@ -1,6 +1,38 @@
function bindEditor() {
const composeEditor = $("#compose-editor");
let composeFormattingRange = null;
const rememberComposeFormattingRange = () => {
const range = captureEditorRange(composeEditor);
if (range) composeFormattingRange = range;
return range;
};
const runComposeCommand = (command, value = null) => {
const range = composeFormattingRange || captureEditorRange(composeEditor);
if (range) restoreEditorRange(range, composeEditor);
else composeEditor.focus();
document.execCommand(command, false, value);
rememberComposeFormattingRange();
};
["keyup", "mouseup", "input"].forEach((eventName) => composeEditor?.addEventListener(eventName, rememberComposeFormattingRange));
composeEditor?.addEventListener("pointerdown", () => {
// A new click/touch in the editor starts a fresh caret or selection.
// Do not let an older toolbar selection become the next command target.
composeFormattingRange = null;
});
$$(".editor-toolbar button, .editor-toolbar select, .editor-toolbar input[type='color']").forEach((control) => {
control.addEventListener("pointerdown", rememberComposeFormattingRange);
if (control.matches("button")) {
control.addEventListener("mousedown", (event) => {
if (event.button !== 0) return;
// Keep the browser from moving focus to the toolbar before execCommand.
// The saved Range is still restored below for browsers that clear selection anyway.
event.preventDefault();
});
}
});
composeEditor?.addEventListener("keydown", (event) => {
if (!(event.ctrlKey || event.metaKey) || event.altKey) return;
const key = event.key.toLowerCase();
@@ -8,37 +40,33 @@ function bindEditor() {
if (!command) return;
if (typeof document.execCommand !== "function") return;
event.preventDefault();
composeEditor.focus();
document.execCommand(command, false, null);
runComposeCommand(command);
scheduleDraftSave();
scheduleUnusedInlineAttachmentCleanup();
updateComposeMessageSize();
});
$$(".editor-toolbar [data-command]").forEach((button) => button.addEventListener("click", () => {
$("#compose-editor").focus();
document.execCommand(button.dataset.command, false, button.dataset.commandValue || null);
runComposeCommand(button.dataset.command, button.dataset.commandValue || null);
}));
$("#editor-link").addEventListener("click", async () => {
const range = composeFormattingRange || captureEditorRange(composeEditor);
const url = await appPrompt("Insert link", { value: "https://", type: "url", confirmLabel: "Insert link", required: true });
if (!url) return;
$("#compose-editor").focus();
document.execCommand("createLink", false, url);
composeFormattingRange = range;
runComposeCommand("createLink", url);
});
$("#editor-font").addEventListener("change", (event) => {
if (!event.target.value) return;
$("#compose-editor").focus();
document.execCommand("fontName", false, event.target.value);
runComposeCommand("fontName", event.target.value);
scheduleDraftSave();
});
$("#editor-size").addEventListener("change", (event) => {
if (!event.target.value) return;
$("#compose-editor").focus();
document.execCommand("fontSize", false, event.target.value);
runComposeCommand("fontSize", event.target.value);
scheduleDraftSave();
});
$("#editor-color").addEventListener("input", (event) => {
$("#compose-editor").focus();
document.execCommand("foreColor", false, event.target.value);
runComposeCommand("foreColor", event.target.value);
scheduleDraftSave();
});
}