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
 |
-| 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 — 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 — 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