v0.2.25
RustBox CI / rustbox (push) Canceled after 0s

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-05 08:33:13 +01:00
1 parent 648556f762
commit 99ee3385ac
69 files changed
+1423 -1365

No files matched your search

+1 -2
View File
@@ -127,8 +127,7 @@ html[data-font-family="mono"] {
--font-sans: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
}
/* Not derived from var(--line): a custom property referring to itself is a
cycle, which made --line empty (no borders) wherever no palette overrode it. */
/* Not var(--line) itself: a self-reference is invalid CSS. */
html[data-contrast="soft"] {
--line: color-mix(in srgb, var(--text) 11%, var(--panel));
--line-strong: color-mix(in srgb, var(--text) 20%, var(--panel));
+1 -2
View File
@@ -1416,8 +1416,7 @@ body.reader-fullscreen-open {
}
}
/* Optional wide-pane single-line message rows. The preference is on by default,
but the layout only activates once the message pane has enough horizontal room. */
/* Single-line rows when the message pane is wide enough. */
@container message-pane (min-width: 520px) {
html[data-compact-wide-rows="true"] .message-row {
min-height: min(var(--row-height), 36px);
+70 -7
View File
@@ -192,9 +192,74 @@
color: var(--muted);
}
.about-list code {
font: 10px var(--font-mono);
overflow-wrap: anywhere;
.about-list a,
.about-card a {
color: var(--accent);
}
.about-hero {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 16px;
}
.about-hero h2 {
margin: 0 0 2px;
}
.about-hero p {
margin: 0;
}
.about-logo {
flex: none;
width: 44px;
height: 44px;
padding: 9px;
border-radius: 12px;
background: color-mix(in srgb, var(--accent) 16%, var(--panel));
color: var(--accent);
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.about-version {
margin-left: 6px;
padding: 2px 8px;
border: 1px solid var(--line);
border-radius: 999px;
color: var(--muted);
font-size: 11px;
font-weight: 600;
vertical-align: middle;
}
.about-card {
display: grid;
gap: 6px;
margin-top: 16px;
padding: 12px 14px;
border: 1px solid var(--line);
border-radius: var(--radius-panel);
background: var(--panel-2);
}
.about-card p {
margin: 0;
line-height: 1.5;
}
.about-origin {
margin: 14px 0 0;
}
.about-copyright {
margin: 4px 0 0;
font-size: 11px;
}
.toast-stack {
@@ -2635,8 +2700,7 @@ kbd {
}
/* 0.2.24: theme schedule, account colour, contrast hint, list skeleton and
a short cross-fade when the theme or interface style changes. */
/* Theme schedule, account colour, contrast hint, list skeleton, theme cross-fade. */
.theme-schedule-row {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -2684,8 +2748,7 @@ kbd {
grid-column: 1 / -1;
margin: 0;
padding: 6px 9px;
/* Fixed colours on purpose: the warning must stay readable precisely when
the user's own text/panel colours are not. */
/* Fixed colours: must stay readable with any custom palette. */
border: 1px solid #d9a93a;
border-radius: var(--radius-control);
background: #fff4d6;
+2 -2
View File
@@ -1096,7 +1096,7 @@ html[data-interface-skin="notion"][data-theme="dark"] {
}
/* 0.2.23 predefined styles: light and dark palettes. */
/* Editor and atmospheric styles: light and dark palettes. */
html[data-interface-skin="dracula"][data-theme="light"] {
color-scheme: light;
--bg: #f4f1e6;
@@ -1855,7 +1855,7 @@ html[data-interface-skin="retro"][data-theme="dark"] {
/* 0.2.24 predefined styles: light and dark palettes. */
/* Retro styles: light and dark palettes. */
html[data-interface-skin="winxp"][data-theme="light"] {
color-scheme: light;
--bg: #ece9d8;
+2 -14
View File
@@ -1,17 +1,5 @@
/*
* RustBox interface-style standard (0.2.23).
*
* Every predefined style ships three layers:
* 1. light and dark palettes (56-interface-styles.css / 55-roundcube-skins.css);
* 2. a fixed layout profile (interfaceStyleProfiles in 20-preferences-auth.js);
* 3. the structural tokens below, consumed only when the user selects
* "Immersive" style depth (58-skin-immersive.css).
*
* Tokens here are always flat. Gradients live in 59-skin-gradients.css and
* only apply with the separate Appearance switch (skin_gradients).
*
* Generated from one table; keep every style listed here.
*/
/* Structural tokens per style, used at "Immersive" depth (58-skin-immersive.css).
Palettes: 55/56; layout profiles: 20-preferences-auth.js; gradients: 59. */
:root {
--skin-font: var(--font-sans);
+2 -11
View File
@@ -1,12 +1,4 @@
/*
* "Immersive" style depth: applies the structural tokens from 57-skin-tokens.css.
*
* Every rule is scoped to :root[data-skin-depth="immersive"][data-theme] so it
* outranks the per-style chrome rules (html[data-interface-skin=…][data-theme=…] …)
* without !important, and has no effect at the "colors" or "standard" depth.
* Inline custom properties written by applyPreferences() live on <html>, so the
* shared radius/font variables are re-pointed on <body> where they inherit down.
*/
/* "Immersive" depth: applies the tokens from 57-skin-tokens.css. */
:root[data-skin-depth="immersive"][data-theme] body {
--font-sans: var(--skin-font);
@@ -100,8 +92,7 @@
backdrop-filter: var(--skin-glass);
}
/* Floating panes only where the three-pane desktop layout is shown; the
phone/tablet drawer layout in 60-responsive.css stays untouched. */
/* Floating panes only in the desktop three-pane layout. */
@media (min-width: 821px) {
:root[data-skin-depth="immersive"][data-theme] .mail-shell {
gap: var(--skin-shell-gap);
+1 -8
View File
@@ -1,11 +1,4 @@
/*
* Style gradients (0.2.24): generated from one table.
*
* Active only with Appearance -> "Style gradients" on, at every style depth,
* for every predefined style except the RustBox custom style. Gradients are
* background-image layers over the colours chosen by the palette and depth;
* the defaults are translucent overlays, signature styles override them.
*/
/* Style gradients: only with the "Style gradients" switch on; not for the RustBox style. */
:root[data-skin-gradients="true"] {
--grad-backdrop: radial-gradient(1100px 560px at 92% -12%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 62%);
+9 -6
View File
@@ -350,11 +350,6 @@
"Active sessions": "Aktywne sesje",
"Live RustBox sessions for this account, including other browsers while the server is running.": "Aktywne sesje RustBox dla tego konta, także w innych przeglądarkach, gdy serwer działa.",
"Revoke other sessions": "Unieważnij pozostałe sesje",
"Webmail based on the Oxi codebase, with a RustBox-owned interface compiled into the Rust executable.": "Webmail oparty na kodzie Oxi, z interfejsem RustBox kompilowanym bezpośrednio do programu Rust.",
"Upstream": "Projekt bazowy",
"Oxi base": "Baza Oxi",
"Frontend": "Frontend",
"HTML, CSS and vanilla JavaScript embedded at Cargo build time": "HTML, CSS i czysty JavaScript osadzane podczas kompilacji Cargo",
"Author": "Autor",
"Keyboard shortcuts": "Skróty klawiaturowe",
"Identity & signature": "Tożsamość i podpis",
@@ -788,7 +783,15 @@
"because the copied colours belong to it.": "bo skopiowane kolory należą do niego.",
"Shapes, fonts, app bar and gradients specific to the style are not part of the copy.": "Kształty, czcionki, pasek aplikacji i gradienty charakterystyczne dla stylu nie wchodzą do kopii.",
"Create a custom copy?": "Utworzyć własną kopię?",
"Create custom copy": "Utwórz własną kopię"
"Create custom copy": "Utwórz własną kopię",
"Fast, self-hosted webmail written in Rust.": "Szybki webmail do samodzielnego hostowania, napisany w Rust.",
"License": "Licencja",
"Technology": "Technologia",
"GNU AGPL v3 or commercial license": "GNU AGPL v3 lub licencja komercyjna",
"Rust server with an embedded HTML, CSS and JavaScript interface": "Serwer w Rust z wbudowanym interfejsem HTML, CSS i JavaScript",
"RustBox is free software under the GNU Affero General Public License v3: you may use, modify and share it, also in a company. If you run a modified version as a network service, you must offer its source code to its users.": "RustBox jest wolnym oprogramowaniem na licencji GNU Affero General Public License v3: możesz go używać, zmieniać i udostępniać, także w firmie. Jeśli uruchamiasz zmodyfikowaną wersję jako usługę sieciową, musisz udostępnić jej kod źródłowy jej użytkownikom.",
"Companies that prefer different terms — for example to keep their changes private — can obtain a paid commercial license. Contacting the author is voluntary: it is never required for use that follows the AGPL.": "Firmy, które wolą inne warunki — na przykład zachować swoje zmiany dla siebie — mogą uzyskać płatną licencję komercyjną. Kontakt z autorem jest dobrowolny: nie jest wymagany przy korzystaniu zgodnym z AGPL.",
"The first version of RustBox was created as a fork of the Oxi project (MIT License).": "Pierwsza wersja RustBox powstała jako fork projektu Oxi (licencja MIT)."
},
"fixed_interface_style_note": "gotowy styl interfejsu. Włącz edycję poniżej, aby dopasować jego układ i typografię.",
"custom_interface_style_note": "RustBox można w pełni dostosować: kolory, odstępy, panele, typografię i zaawansowane ustawienia interfejsu.",
+28 -9
View File
@@ -1307,21 +1307,40 @@
</div>
</section>
<section class="settings-panel hidden" data-panel="about">
<h2>RustBox __RUSTBOX_VERSION__</h2>
<p>Webmail based on the Oxi codebase, with a RustBox-owned interface compiled into the Rust executable.</p>
<div class="about-hero">
<svg class="about-logo" aria-hidden="true">
<use href="__RUSTBOX_ASSET_PREFIX__/assets/icons.svg#icon-mail"></use>
</svg>
<div>
<h2>RustBox <span class="about-version">__RUSTBOX_VERSION__</span></h2>
<p class="muted">Fast, self-hosted webmail written in Rust.</p>
</div>
</div>
<dl class="about-list">
<dt>Upstream</dt>
<dd>c0h1b4/oxi</dd>
<dt>Oxi base</dt>
<dd><code>__RUSTBOX_OXI_BASE__</code></dd>
<dt>Frontend</dt>
<dd>HTML, CSS and vanilla JavaScript embedded at Cargo build time</dd>
<dt>Author</dt>
<dd>@linuxiarz.pl</dd>
<dd>Mateusz Gruszczyński · <a href="https://linuxiarz.pl" target="_blank"
rel="noopener noreferrer">linuxiarz.pl</a></dd>
<dt>Repository</dt>
<dd><a href="https://git.linuxiarz.pl/gru/RustBox" target="_blank"
rel="noopener noreferrer">git.linuxiarz.pl/gru/RustBox</a></dd>
<dt>License</dt>
<dd>GNU AGPL v3 or commercial license</dd>
<dt>Technology</dt>
<dd>Rust server with an embedded HTML, CSS and JavaScript interface</dd>
</dl>
<div class="about-card">
<strong>License</strong>
<p>RustBox is free software under the GNU Affero General Public License v3: you may use, modify and
share it, also in a company. If you run a modified version as a network service, you must offer its
source code to its users.</p>
<p>Companies that prefer different terms — for example to keep their changes private — can obtain a
paid commercial license. Contacting the author is voluntary: it is never required for use that
follows the AGPL.</p>
<p><a href="mailto:linuxiarz@linuxiarz.pl">linuxiarz@linuxiarz.pl</a></p>
</div>
<p class="about-origin muted">The first version of RustBox was created as a fork of the Oxi project (MIT
License).</p>
<p class="about-copyright muted">© 2026 Mateusz Gruszczyński</p>
</section>
</div>
</div>
+7 -17
View File
@@ -231,9 +231,7 @@ function resolvedSkinDepth(prefs = state.prefs) {
return skinDepths.includes(prefs.skinDepth) ? prefs.skinDepth : "standard";
}
// "colors" keeps only a predefined style's palette; "standard" and "immersive"
// also apply its fixed layout profile ("immersive" adds the structural tokens
// from 57-skin-tokens.css on top, purely in CSS).
// Style depth: "colors" = palette only; "standard" adds the layout profile; "immersive" adds the structural tokens.
function layoutInterfaceStyle(prefs = state.prefs) {
return resolvedSkinDepth(prefs) === "colors" ? null : activeInterfaceStyle(prefs);
}
@@ -310,9 +308,7 @@ function applyPreferences() {
const fixedStyle = layoutInterfaceStyle(p);
const skinDepth = resolvedSkinDepth(p);
const previousLook = `${root.dataset.theme}|${root.dataset.interfaceSkin}|${root.dataset.skinDepth}|${root.dataset.skinGradients}`;
// A predefined style fixes the layout unless editing is allowed; then the
// user's own values (seeded from the style when editing was enabled) win,
// while the style's palette, tokens and gradients stay in place.
// The style fixes the layout unless editing is allowed.
const visual = fixedStyle && !p.presetScaleUnlocked
? { ...p, ...fixedStyle.preferences }
: p;
@@ -487,9 +483,7 @@ function contrastRatio(first, second) {
return (a + 0.05) / (b + 0.05);
}
// Warns while the user's own colours make text hard to read (WCAG AA, 4.5:1
// for text, 3:1 for the accent against panels). Predefined palettes are
// checked at release time, so the hint is only shown for custom colours.
// Live WCAG contrast hint for custom colours.
function updateContrastWarning(usingPredefinedPalette) {
const warning = $("#pref-contrast-warning");
if (!warning) return;
@@ -535,6 +529,7 @@ async function loadPreferences() {
catch (error) { console.warn("Could not load display preferences", error); }
applyPreferences();
await loadTranslations(state.prefs.language);
applyPreferences();
}
async function loadMailPreferences() {
@@ -724,9 +719,7 @@ function schedulePromotedAppearanceSave() {
const presetLayoutPreferenceKeys = [...presetEditablePreferenceKeys].filter((key) => !presetColorPreferenceKeys.has(key));
// "Allow editing predefined style": seed the user's own layout values from the
// style, so editing starts from exactly what is on screen and never mixes in
// values saved earlier for another style.
// Seeds the user's layout from the style so editing starts from what is on screen.
async function setPresetEditing(enabled) {
const layoutStyle = layoutInterfaceStyle(state.prefs);
const payload = { preset_scale_unlocked: enabled };
@@ -746,10 +739,7 @@ async function setPresetEditing(enabled) {
}
}
// Colours of a predefined style come from its light and dark palettes and
// cannot be overridden one by one without mixing two palettes. Changing one
// therefore turns the style into a RustBox custom copy of the palette that is
// on screen now, pinned to the current light/dark mode so it stays coherent.
// A colour change on a predefined style creates a RustBox custom copy of the current palette.
async function promotePresetToCustom(key, value) {
const paletteStyle = activeInterfaceStyle(state.prefs);
if (!paletteStyle || !presetColorPreferenceKeys.has(key) || !state.prefs.presetScaleUnlocked) return false;
@@ -815,7 +805,7 @@ function savePreference(key, value) {
}
if (["accentColor", "backgroundColor", "panelColor", "textColor"].includes(key)) state.prefs.colorPreset = "custom";
applyPreferences();
if (key === "language") loadTranslations(value);
if (key === "language") loadTranslations(value).then(applyPreferences);
const previousTimer = preferenceTimers.get(key);
if (previousTimer) clearTimeout(previousTimer);
const timer = setTimeout(async () => {
+24 -5
View File
@@ -11,12 +11,31 @@ function folderIconName(folder) {
return "folder";
}
function folderPathParts(folder) {
const delimiter = folder.delimiter || "/";
return delimiter ? folder.name.split(delimiter) : [folder.name];
}
// Tree order: children under their parent, INBOX first, siblings alphabetical.
function compareFolderPaths(a, b) {
const left = folderPathParts(a);
const right = folderPathParts(b);
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
if (left[index] === undefined) return -1;
if (right[index] === undefined) return 1;
if (left[index] === right[index]) continue;
if (index === 0) {
const leftInbox = left[0].toUpperCase() === "INBOX";
const rightInbox = right[0].toUpperCase() === "INBOX";
if (leftInbox !== rightInbox) return leftInbox ? -1 : 1;
}
return left[index].localeCompare(right[index]) || (left[index] < right[index] ? -1 : 1);
}
return 0;
}
function orderedFolders() {
return [...state.folders].sort((a, b) => {
if (a.name.toUpperCase() === "INBOX") return -1;
if (b.name.toUpperCase() === "INBOX") return 1;
return a.name.localeCompare(b.name);
});
return [...state.folders].sort(compareFolderPaths);
}
function folderDepth(folder) {
+2 -8
View File
@@ -312,11 +312,7 @@ async function quickUpdateMessageFlag(message, flag, add, successMessage) {
}
}
// Hides rows for messages that are currently snoozed in this folder. This is
// a purely visual, best-effort filter on top of the already-rendered list:
// it never changes state.messages/state.total, so pagination/counts/caching
// above are completely unaffected. A message simply stops being hidden once
// its wake time passes and the folder is reloaded or reopened.
// Hides rows of snoozed messages (visual only; counts and paging unchanged).
async function applySnoozeHiding() {
if (state.searchContext) return;
const folder = state.folder;
@@ -508,9 +504,7 @@ function readerFullscreenUrl(detail = state.currentDetail) {
return new URL(location.href);
}
// Gmail-style modifier clicks on a message row, active only outside selection
// mode (inside selection mode the same modifiers keep doing range/toggle
// selection, see the row click handler below).
// Ctrl/Cmd/Shift-click on a row outside selection mode.
function openMessageInNewTab(folder, uid) {
window.open(messageDeepLinkUrl(folder, uid), "_blank", "noopener");
}
+1 -4
View File
@@ -1788,10 +1788,7 @@ function buildReferences(detail) {
return values.length ? values.join(" ") : null;
}
// "Undo Send": the server held the just-sent message back for a few seconds
// instead of dispatching it immediately. Show a toast with an Undo action
// for the whole window, then poll once for the final result so a send that
// actually failed in the background is still reported.
// Undo Send: offer Undo during the hold window, then report the final result.
function notifyUndoSendStaged(pendingId, windowSecs) {
const seconds = Math.max(0, windowSecs);
toast("Sending\u2026", "info", seconds * 1000 || 4000, {
+1 -3
View File
@@ -181,9 +181,7 @@ async function copyCurrentTo(targetFolder) {
} catch (error) { toast(error.message, "error"); }
}
// Snoozing hides a message from its folder listing until a chosen wake time.
// It is purely local client/server cache state: nothing moves on the real
// IMAP server, so a snooze never risks the actual mailbox.
// Snooze hides a message until its wake time (local only, nothing moves on IMAP).
function snoozePresetOptions() {
const now = new Date();
const laterToday = new Date(now.getTime() + 3 * 60 * 60 * 1000);
+2 -7
View File
@@ -409,9 +409,7 @@ function calendarReminderStorageKey(event, minutes) {
return `rustbox.calendarReminder.${state.accountId || "account"}.${event.id}.${event.start_time}.${minutes}`;
}
// Timed events: start minus the reminder. All-day events: the reminder counts
// from the start of the working day (calendar settings, default 09:00) of the
// event's date, so "1 day before" fires the previous morning, not at midnight.
// All-day events are reminded relative to the start of the working day.
function calendarReminderTrigger(event, minutes) {
const offset = Math.max(0, Number(minutes) || 0) * 60000;
if (event.all_day) {
@@ -476,10 +474,7 @@ function isDuplicateRealtimeEvent(payload) {
const now = Date.now();
for (const [key, seenAt] of state.realtimeSeenEvents) if (now - seenAt > 10000) state.realtimeSeenEvents.delete(key);
// Server websocket envelopes carry a unique eventId. When present it is the
// only safe deduplication key: state can legitimately cycle A -> B -> A in
// under a second (for example unread 1 -> 0 -> 1). Fingerprint-based
// deduplication would incorrectly drop the second A and leave stale UI.
// Deduplicate by eventId when present: the same state can legitimately repeat.
if (payload.eventId) {
const idKey = `id:${payload.eventId}`;
const duplicate = state.realtimeSeenEvents.has(idKey);
+1 -3
View File
@@ -1,6 +1,4 @@
// Filter conveniences on top of the visual Sieve editor: templates, creating
// missing target folders, and "Apply now" — running a rule's actions on mail
// that is already in a folder (Sieve itself only sees newly delivered mail).
// Filter templates, missing-folder creation and "Apply now" for existing mail.
function fmtText(template, values = {}) {
return trText(template).replace(/\{(\w+)\}/g, (_, key) => String(values[key] ?? ""));
+1 -3
View File
@@ -507,9 +507,7 @@ function bindEvents() {
$$("[data-signature-command]").forEach((button) => button.addEventListener("click", () => {
const range = selectedSignatureRange();
if (!range) {
// execCommand on a collapsed caret enables a sticky typing style (for
// example every subsequently typed character becomes bold). Signature
// formatting is intentionally selection-only.
// Formatting applies to the selection only (a collapsed caret would make it sticky).
signatureEditor.focus();
return;
}