From c30c38a7afe02c176fdecd9e1d19524e3b23b2c2 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 23 Apr 2026 00:07:34 +0200 Subject: [PATCH] chore: update version to 1.5.0 --- FEATURES.md | 16 ++--- README.md | 28 ++++---- app/[locale]/calendar/page.tsx | 2 +- app/[locale]/contacts/page.tsx | 2 +- app/[locale]/files/page.tsx | 2 +- app/[locale]/page.tsx | 4 +- app/[locale]/settings/page.tsx | 2 +- lib/email-sanitization.ts | 2 +- lib/jmap/client.ts | 6 +- locales/ru/common.json | 2 +- locales/uk/common.json | 2 +- setup.sh | 117 +++++++++++++++++++++++++++++++-- stores/email-store.ts | 2 +- 13 files changed, 145 insertions(+), 42 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index a7ca6bb3..79a69120 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -8,8 +8,8 @@ - Draft auto-save with identity preservation - Attachment upload, download, and inline preview; forgotten-attachment warning - Full-text search with JMAP filter panel, search chips, wildcards, OR conditions, and cross-mailbox queries -- Batch operations — multi-select, archive, delete, move, tag -- Archive modes — direct, by year, or by month +- Batch operations – multi-select, archive, delete, move, tag +- Archive modes – direct, by year, or by month - Multi-tag support with color labels, reordering, and drag-and-drop assignment - Star/unstar with configurable mark-as-read delay - Virtual scrolling for large mailboxes @@ -26,7 +26,7 @@ - Drag-to-reschedule, click-drag creation, and edge-resize with 15-minute snap - Recurring events with scoped edit/delete (this / this and following / all) - iMIP invitations on create and update (RFC 5545 / 6047), organizer/attendee UI, and RSVP with trust assessment -- Inline calendar invitations in the email viewer — auto-detect `.ics`, RSVP, import +- Inline calendar invitations in the email viewer – auto-detect `.ics`, RSVP, import - iCalendar import with preview, bulk create, and UID deduplication - iCal / webcal subscriptions with editing and batch import - Auto-generated birthday calendar from contacts @@ -67,7 +67,7 @@ - External content blocked by default, with a trusted senders list - HTML sanitization via DOMPurify -- S/MIME — manage certificates, sign, encrypt, decrypt, and verify; legacy 3DES / PBE support; per-account key isolation +- S/MIME – manage certificates, sign, encrypt, decrypt, and verify; legacy 3DES / PBE support; per-account key isolation - SPF / DKIM / DMARC status indicators - OAuth2 / OIDC with PKCE (Keycloak, Authentik, or built-in), OAuth-only mode, OAuth app passwords, and non-interactive SSO for embedded deployments - TOTP two-factor authentication @@ -110,9 +110,9 @@ Automatic browser detection with persistent preference. Configurable locale URL ## Admin & Extensibility - Stalwart admin dashboard with dedicated policy sections -- Plugin system — schema-driven config UI, render and intercept hooks, `onAvatarResolve` and i18n APIs, calendar event slots, and managed policy enforcement -- Themes — upload, enforce, and manage admin-controlled themes as ZIP bundles -- Extension marketplace — browse and install plugins and themes from a configurable directory (`EXTENSION_DIRECTORY_URL`) +- Plugin system – schema-driven config UI, render and intercept hooks, `onAvatarResolve` and i18n APIs, calendar event slots, and managed policy enforcement +- Themes – upload, enforce, and manage admin-controlled themes as ZIP bundles +- Extension marketplace – browse and install plugins and themes from a configurable directory (`EXTENSION_DIRECTORY_URL`) - Bundled plugins including Jitsi Meet calendar integration ## Operations @@ -121,4 +121,4 @@ Automatic browser detection with persistent preference. Configurable locale URL - Automatic update check with server-side logging of new releases - Structured logging (`text` or `json`) with category-based levels - Release (`main`) and development (`dev`) Docker images on GHCR -- Demo mode with fixture data — no mail server required +- Demo mode with fixture data – no mail server required diff --git a/README.md b/README.md index 702b24df..c1352b3b 100644 --- a/README.md +++ b/README.md @@ -32,35 +32,35 @@ A modern, self-hosted webmail client for [Stalwart Mail Server](https://stalw.ar Contacts -Calendar — month, week, day, and agenda views with drag-to-reschedule, iMIP invitations, and CalDAV subscriptions. -Contacts — multiple address books, groups, vCard import/export, and autocomplete in the composer. +Calendar – month, week, day, and agenda views with drag-to-reschedule, iMIP invitations, and CalDAV subscriptions. +Contacts – multiple address books, groups, vCard import/export, and autocomplete in the composer. Themes Plugins -Themes — bundled color themes or upload your own as ZIP bundles; admins can enforce presets. -Plugins — extend the client with bundled or third-party plugins installed from a .zip file. +Themes – bundled color themes or upload your own as ZIP bundles; admins can enforce presets. +Plugins – extend the client with bundled or third-party plugins installed from a .zip file. Light mode Settings -Light mode — full theme support with intelligent color transformation for HTML emails. -Settings — appearance, identities, filters, templates, security, and more. +Light mode – full theme support with intelligent color transformation for HTML emails. +Settings – appearance, identities, filters, templates, security, and more. ## Overview -Bulwark is a full webmail suite — not just an inbox. It bundles the four apps most self-hosters end up wanting on the same login: +Bulwark is a full webmail suite – not just an inbox. It bundles the four apps most self-hosters end up wanting on the same login: -- **Mail** — threading, unified inbox, full-text search, Sieve filters, S/MIME, templates -- **Calendar** — month/week/day/agenda, recurring events, iMIP invitations, CalDAV subscriptions -- **Contacts** — multiple address books, groups, vCard import/export -- **Files** — Stalwart's JMAP FileNode storage with previews and folder upload +- **Mail** – threading, unified inbox, full-text search, Sieve filters, S/MIME, templates +- **Calendar** – month/week/day/agenda, recurring events, iMIP invitations, CalDAV subscriptions +- **Contacts** – multiple address books, groups, vCard import/export +- **Files** – Stalwart's JMAP FileNode storage with previews and folder upload Plus the infrastructure around them: OAuth2 / OIDC SSO, TOTP 2FA, multi-account (up to 5 at once), 14 languages, PWA install, dark/light themes, a plugin system with an extension marketplace, and a Stalwart admin dashboard. @@ -82,7 +82,7 @@ Or with Docker Compose: ```bash cp .env.example .env.local -# Edit .env.local — set JMAP_SERVER_URL +# Edit .env.local – set JMAP_SERVER_URL docker compose up -d ``` @@ -93,7 +93,7 @@ git clone https://github.com/bulwarkmail/webmail.git cd webmail npm install cp .env.example .env.local -# Edit .env.local — set JMAP_SERVER_URL +# Edit .env.local – set JMAP_SERVER_URL npm run build && npm start ``` @@ -247,7 +247,7 @@ LOG_LEVEL=info # error | warn | info | debug ## Why Stalwart? -[Stalwart](https://github.com/stalwartlabs/mail-server) is a Rust mail server with native JMAP support — not IMAP/SMTP with JMAP bolted on. It handles JMAP, IMAP, SMTP, and ManageSieve in a single self-hosted binary with no third-party dependencies. +[Stalwart](https://github.com/stalwartlabs/mail-server) is a Rust mail server with native JMAP support – not IMAP/SMTP with JMAP bolted on. It handles JMAP, IMAP, SMTP, and ManageSieve in a single self-hosted binary with no third-party dependencies. ## Contributing diff --git a/app/[locale]/calendar/page.tsx b/app/[locale]/calendar/page.tsx index 50069e64..1b67de9c 100644 --- a/app/[locale]/calendar/page.tsx +++ b/app/[locale]/calendar/page.tsx @@ -126,7 +126,7 @@ export default function CalendarPage() { } }, [events, detailEvent]); - // Check auth on mount — skip when already authenticated so that navigating + // Check auth on mount – skip when already authenticated so that navigating // between routes doesn't retrigger checkAuth's transient `{ client: null, // isLoading: true }` reset, which was flashing the spinner on every nav. useEffect(() => { diff --git a/app/[locale]/contacts/page.tsx b/app/[locale]/contacts/page.tsx index b6a1c87d..1190019f 100644 --- a/app/[locale]/contacts/page.tsx +++ b/app/[locale]/contacts/page.tsx @@ -103,7 +103,7 @@ export default function ContactsPage() { const [isListResizing, setIsListResizing] = useState(false); const listDragStartWidth = useRef(384); - // Check auth on mount — skip when already authenticated so that navigating + // Check auth on mount – skip when already authenticated so that navigating // between routes doesn't retrigger checkAuth's transient `{ client: null, // isLoading: true }` reset, which was flashing the spinner on every nav. useEffect(() => { diff --git a/app/[locale]/files/page.tsx b/app/[locale]/files/page.tsx index 6725d337..261e73d6 100644 --- a/app/[locale]/files/page.tsx +++ b/app/[locale]/files/page.tsx @@ -105,7 +105,7 @@ export default function FilesPage() { const detailResource = detailName ? resources.find(r => r.name === detailName) || null : null; - // Check auth on mount — skip when already authenticated so that navigating + // Check auth on mount – skip when already authenticated so that navigating // between routes doesn't retrigger checkAuth's transient `{ client: null, // isLoading: true }` reset, which was flashing the spinner on every nav. useEffect(() => { diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index a9b844b2..197a65a2 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -455,7 +455,7 @@ export default function Home() { document.title = title; }, [showComposer, composerMode, selectedEmail, selectedMailbox, mailboxes, t, appName]); - // Check auth on mount — skip when already authenticated so that navigating + // Check auth on mount – skip when already authenticated so that navigating // between routes doesn't retrigger checkAuth's transient `{ client: null, // isLoading: true }` reset, which was flashing the spinner on every nav. useEffect(() => { @@ -838,7 +838,7 @@ export default function Home() { const handleArchive = async (emailToArchive: Email | null = selectedEmail) => { if (!client || !emailToArchive) return; - // Read fresh mailboxes from the store — batch archive calls this in a loop, + // Read fresh mailboxes from the store – batch archive calls this in a loop, // and each iteration needs to see folders created by prior iterations. const currentMailboxes = useEmailStore.getState().mailboxes; diff --git a/app/[locale]/settings/page.tsx b/app/[locale]/settings/page.tsx index 34128ea2..0f49ab83 100644 --- a/app/[locale]/settings/page.tsx +++ b/app/[locale]/settings/page.tsx @@ -179,7 +179,7 @@ export default function SettingsPage() { const [isResizing, setIsResizing] = useState(false); const dragStartWidth = useRef(256); - // Check auth on mount — skip when already authenticated so that navigating + // Check auth on mount – skip when already authenticated so that navigating // between routes doesn't retrigger checkAuth's transient `{ client: null, // isLoading: true }` reset, which was flashing the spinner on every nav. useEffect(() => { diff --git a/lib/email-sanitization.ts b/lib/email-sanitization.ts index be5df398..331b3e0a 100644 --- a/lib/email-sanitization.ts +++ b/lib/email-sanitization.ts @@ -40,7 +40,7 @@ export function sanitizeEmailHtml(html: string): string { * Sanitize config for emails rendered inside a sandboxed iframe. * Allows