Calendar / Auth: Pin the resolved IP address at socket-connect time when fetching caller-supplied URLs (iCalendar subscriptions, JMAP login and TOTP token-exchange servers). The public-host check used to run before fetch() opened its socket, so an attacker who controlled DNS for a hostname could rebind it to loopback, RFC-1918 or cloud-metadata addresses between the check and the connect and read up to 10 MB of the internal response through the unauthenticated /api/fetch-ical endpoint. Redirect targets are now validated the same way (GHSA-24w9-8r42-8jwm, thanks @Tike00)
Features
Push: Re-sync existing push registrations in the background on app start, so registrations created before the delivery filter existed β or whose Junk mailbox id went stale β get repaired without re-enabling notifications
Fixes
Push: Stop sending notifications for spam β the push subscription now carries a JMAP emailPush delivery filter that excludes $junk and the Junk mailbox (needs a server advertising the emailPush capability, e.g. Stalwart β₯ 0.16.16; older servers keep the previous behaviour)
Navigation: Forward every request header through the proxy β Next 16.3's RSC header check rejected the stripped router headers and sent navigations into a 307 redirect loop (#919)
Calendar: Stop emitting RSCALE=GREGORIAN;SKIP=OMIT on plain Gregorian recurrence rules β DAVx5 rejected them as invalid and Android sync broke (#805, thanks @hildebrandttk)
Mail: Search suggestions with recent searches and contact autocomplete (#845)
Mail: Render plain-text emails in the app font by default, with a monospace option (#830)
Mail: Pull-to-refresh indicator while dragging the list down (#826)
Mail: Redesigned unread favicon badge β a compact keyline badge
Mobile: Swipe message rows left or right for quick actions β archive, delete, toggle read, toggle star, or spam, configurable per direction, RTL-aware
Pro: Reworked split-screen shell with per-pane tab strips, drag & drop between panes, and pane-scoped overlays
Pro: Folder tabs via drag & drop
Pro: The address bar follows the focused tab, and deep links are delivered live to already-mounted surfaces
Composer: Real byte progress for attachment uploads, stock and plugin-offloaded; cancel now aborts the transfer itself
Calendar: Moving an event's start moves the end with it, keeping the event's length
Contacts: Trusted Senders address book enabled by default on contacts-capable accounts
Settings: Custom time zone setting that overrides browser detection (#755)
Login: Server dropdown on the OAuth-only login screen (#799)
Push: New-mail notifications grouped per account with a "+N more messages" line instead of one notification per message
Push: Per-device revoke for push subscriptions (#841)
Admin: Configurable Stalwart admin access to the dashboard (#870)
Admin: Push relay picked from an admin-defined list instead of a free URL field
Branding: OpenGraph/Twitter link previews with a generated card image
Plugins: onBeforeComposeOpenToReply and sibling hooks let a plugin edit an email before it populates the composer for reply or forward
Plugins: getPublicKeyFromWKD
Plugins: Privileged plugins can fetch a byte range of a blob
Plugins: progressFileId on api.http.post so an offloaded upload reports byte progress to the composer chip
Plugins: isActive on AccountResponse
Plugins: Label settings and label reordering exposed to extensions
Plugins: JMAP keyword helpers and gateway keywords exposed to extensions
Plugins: Mailbox refresh hook
Dev: AddressBook/set in the dev mock JMAP server
Changes
Plugins: The PRF getOrCreate flow is split into separate get and create steps for better authenticator compatibility (#851)
Dependencies: Next 16.3.3, pdfjs-dist 6.2.108, DOMPurify 3.4.14 (npm audit)
Fixes
Send: Route scheduled sends to the account that owns the submission β mail scheduled from a shared address can now be listed, cancelled, and rescheduled instead of going out silently (#874)
Send: Split recipient lists whose angle brackets never close without dropping recipients
Send: generateMessageId crashed on insecure origins (crypto.randomUUID undefined), failing the send after the draft save
Composer: Keep attachments when re-opening a draft, also in the Pro draft tab, and destroy old draft versions only after a successful create or send (#849)
Composer: Keep the signature in saved drafts and embed it into re-opened drafts (#848)
Composer: Double-click unlocks the embedded signature for editing
Composer: Keep already written text when applying a template (#540)
Composer: Preselect the shared folder's identity for new messages
Composer: Namespace all accounts consistently in the Pro composer identity list
Composer: Opening a mailto: link runs the unsaved-draft dialog instead of replacing the draft outright
Composer: Keep the fresh-compose tab title clear of the selected email subject
Composer: Clear the viewer when sending destroys the displayed draft
Reply: Honour an external Reply-To even on a self-sent message
Mail: Flip $junk/$notjunk keywords on spam and not-spam (#850)
Mail: Remove keywords with null rather than false in Email/set, per RFC 8620
Mail: Escape the JSON Pointer in keyword patches so nested tags like work/clients patch the right keyword
Mail: Route keyword writes β tags, pins, flags β to the selected shared account so they persist
Mail: Route shared-folder management to the owner account, scoped to one server
Mail: Open the right conversations in a shared mailbox (#814)
Mail: Route multi-account email lookups and invitation parsing by source account (#847), and parse invitations in directly viewed shared folders against the folder owner (#867)
Mail: Folders containing a system folder name no longer disappear from the sidebar (#771)
Mail: Folder drag & drop can move folders into other parents (#855)
Mail: Keep the folder tree when a refresh burst hits maxConcurrentRequests (#780)
Mail: Stop All-Mail and cross-account views emptying on delete, star, or mark-read (#791)
Mail: Keep just-read or unstarred mail in the open Unread/Starred view
Mail: Search folder filter defaults to all folders and persists (#788)
Mail: Fixed-width read/unread toolbar button so buttons don't jump when a message opens (#864)
Mail: Transparent hover-action background on tagged rows
Mail: Enforce the external media preference on plugin-rendered bodies (#797)
Mail: Apply the data: URI allowlist to media tags and srcset candidates too
Mail: Make label reordering atomic
Mail: Pass PDF bytes to pdf.js instead of fetching the blob: URL that CSP connect-src blocks (#871)
Mail: Include email templates in cross-device settings sync (#825)
Mail: Toast store crashed on insecure origins, breaking every post-action acknowledgement
Mobile: Reach the tag and move submenus in the more-actions menu (#779)
Mobile: iOS Safari no longer zooms the viewport on every input focus (#838)
Calendar: Click and double-click create events at the clicked slot instead of near the current time
Calendar: Save and Cancel are available when an event is created, and the toolbar no longer overflows in edit mode
Calendar: Edit a single recurring occurrence via a one-shot override patch
Calendar: Hide tasks-only calendars from the event calendar
Calendar: Exclude subscription and read-only calendars from event creation (#762)
Calendar: Rights-first event editability, including alias organizers
Calendar: Pin supported-calendar-component-set on created calendars (#760)
Calendar: Gate first-touch calendar and contacts requests to stop duplicate default calendars (#907)
Calendar/Contacts: Namespace all accounts consistently on switch, so multi-account address-book aggregation and calendar selection survive an account switch
Contacts: Import vCard dates as RFC 9553 PartialDate and map common X- extensions (#224)
Files: Decode percent-encoded FileNode names from WebDAV-created nodes (#869)
Files: Reset the account-scoped Files drive on every account switch
Accounts: Refresh the account display name from the Stalwart principal on login, restore, and switch (#900)
Auth: Reuse the cached access token on session restore (#552)
Auth: Refresh TOTP-minted tokens with the default client id when no OAuth client is configured (#873)
Auth: Store the session cookie for relative JMAP server URLs
Security: Close IPv6 transition-address and redirect bypasses in the endpoint guard
Security: Add embedded custom app origins to CSP frame-src (#787)
Security: Per-account isolation for encryption at rest
JMAP: Cap live SSE streams per tab and keep exactly one stream per client, so many logins can't starve JMAP requests (#702)
JMAP: Check the specific capability a request declares (principals:owner), not a broader one
JMAP: Resolve relative session URLs without corrupting URI templates
Settings: Merge per-account maps on server load, fixing the compose identity switch
Settings: Leaving the Pro interface returns to the surface in use and keeps the settings scroll position
UI: Position portalled popovers before first paint to stop the layout flash on open
Push: Recreate the push subscription on re-register (#841)
i18n: Key parity across all 24 locales, Catalan and Mongolian registered in the client provider, and scoped translation hooks instead of relative namespace paths
Docs: Lengthen the example SESSION_SECRET so it meets the minimum length
A GitHub Actions incident left the 1.8.0 release build queued indefinitely, so no Docker image was ever published for that tag. 1.8.1 re-runs the release with the same code, plus one plugin slot that merged in the meantime.
Security: Manage S/MIME and PGP public keys and configure Stalwart encryption at rest from account security settings
Notifications: Background notification onboarding, sequenced after the PWA install prompt
Navigation: Deep links for mail, calendar, contacts, files, and settings, with screen-reader improvements
Settings: Always show the Unified Mailbox switch in Layout settings
i18n: Catalan translation
i18n: Localized editor toolbar across every locale
Plugins: Contact API β contact.get, contact.create, contact.update, contact.search
Plugins: contact-cryptokeys UI slot, behind the ui:contact-cryptokeys permission, so a plugin can render a contact's crypto keys in place of the built-in list
Plugins: user.getAccounts and user.getIdentities
Plugins: user.logout method and logout hook
Plugins: Crypto API β public-key management and encryption-at-rest control on the privileged tier
Plugins: onBeforeBlobUpload can offload an attachment to external storage
Plugins: Binary Blob/File bodies for api.http.post
Plugins: upfiles.get moved behind email:blob-read, off the privileged tier
Dev: Mock JMAP defaults now include nested tags
Changes
Mail: The "Reset to defaults" button is gone from tag settings β one stray click wiped a carefully built tag list, with no confirmation and no undo
Fixes
Send: Send through the identity's own account client so DKIM matches the From domain (#461)
Send: Split Name <addr> recipients into the JMAP name and email fields (#672)
Send: Time out stalled JMAP requests so a send can't hang forever (#702)
Mail: Keep inline images when replying to application/octet-stream cid parts (#543)
Mail: Reply on your own thread message no longer re-addresses the original recipients (#703)
Mail: Empty folder no longer stops after 500 emails (#711)
Mail: Move messages across accounts from the "Move to" context menu, preserving read state and deferring source removal to Stalwart
Mail: Stop resurrecting deleted rows in the mailbox refresh merge
Mail: Keep the message/rfc822 attachment visible after inline unwrapping
Mail: Strip sender and recipient names from forward-as-attachment filenames
Mail: Open mailto: links in the built-in composer
Mail: Spell out the full tag path in drag-and-drop toasts, so Personal/Receipts and Work/Receipts no longer read as the same tag
Mail: Act on current email state in the context menu's mark-as-read instead of a stale copy
Mail: Match the selected-row tint between dark and light mode
Mail: Restore lost animations after the Tailwind config move
Drafts: Restore the sender identity when reopening a draft
Calendar: Stop re-adding the organizer to the attendee list on every save (#731)
Calendar: Stop re-probing shared accounts that have no calendar access
Calendar: Route the parse dump through the debug logger
S/MIME: The built-in S/MIME implementation has been removed from core and re-delivered through the new generic crypto plugin hooks (privileged same-origin plugin tier). S/MIME signing, encryption, decryption, certificate management, and the related settings UI now live in a plugin rather than the main app. Deployments that relied on built-in S/MIME must install the S/MIME crypto plugin to retain those features.
Opt-in tabbed multi-pane interface for power users. Open multiple mail, calendar, contacts, and file views side-by-side, drag tabs to reorder or split panes at the edges, and work across all logged-in accounts in one shell, cross-account email moves, a unified inbox with search, account-split calendar/contacts/files sidebars, and a per-account "From" dropdown in the composer. Enable from Settings β Appearance; the proInterface preference is per-device and not synced.
Pro mode is experimental and we need your feedback to shape it. If something feels off, breaks, or is missing, please don't hesitate to open an issue or start a discussion on GitHub!
Breaking Changes
Plugins: Plugins now run inside a null-origin iframe sandbox and talk to the host over a postMessage RPC bridge. The in-process plugin runtime is gone; the bundled in-tree plugins have been migrated. Third-party plugins built against the old in-process API need to be ported to the sandboxed runtime.
Plugins: Server-managed bundles must be Ed25519-signed by the host and approved by an admin before they load. The host public key is served from /api/plugin-signing-pubkey and each bundle response carries the signature in the X-Bundle-Signature header. User-uploaded bundles still load unsigned, but managed marketplace and dev-folder bundles do not.
Plugins: bundleHash is now a full SHA-256 over the bundle. Legacy short hashes are migrated on first load; any out-of-band tooling that pinned the old hash format needs to be updated.
Features
Pro: Tabbed shell with drag-to-reorder, drag-to-edge to split, side-by-side panes, and pane-aware responsive layout with a scoped sidebar overlay
Pro: Auto-redirect to the Pro shell when Pro mode is on; proInterface is kept per-device instead of syncing
Pro: Multi-account mail sidebar with client routing and a per-account mailbox cache
Pro: Unified mailbox always visible, with full-text search
Pro: Cross-account email moves
Pro: Multi-account calendar sidebar split into owned vs shared per account
Pro: Multi-account contacts and a cross-account file picker
Pro: Composer From dropdown grouped by account
Plugins: Per-plugin admin approval workflow with Ed25519 bundle signing verified on load
Plugins: Marketplace update flow for installed plugins and themes
Setup: Allow the setup wizard over plain HTTP with a dismissable warning gate
Setup: Warn when the JMAP URL points at a local-only host
Account: List and reorder logged-in accounts from settings (#282)
Mail: Mobile handoff page with JMAP authentication verification for cross-device OAuth
First-launch web setup wizard. New installs no longer need to hand-edit .env.local - point a browser at the container and the wizard probes the JMAP server(s), configures OAuth/OIDC, generates the session secret, accepts branding uploads, and provisions the initial admin password. Admin storage is now split into ADMIN_CONFIG_DIR (operator-authored, mountable read-only after setup) and ADMIN_STATE_DIR (runtime audit log and login timestamps); the legacy ADMIN_DATA_DIR keeps working for existing installs.
Features
Setup: Web setup wizard with multi-step flow: Server, Auth, Security, Logging, Branding, Review, Admin
Setup: Admin config/state directory split with optional ADMIN_CONFIG_READONLY for immutable deployments (#226)
Setup: File uploads on the wizard branding step
Setup: Redesigned review step with grouped summary and an advanced toggle for the full config
Setup: Require explicit confirmation when JMAP probe finds no session
Mail: Drag attachments out of the viewer to the local file system (#267)
Mail: Configurable signature position β above or below quoted text (#266)
Mail: Signature position is now searchable from the email behavior settings
Mail: Show avatar in Focused list for compact density and above
Mail: Align Focused list preview with other layout previews
Compose: From-header override in the composer with catch-all auto-reply, replies to an alias on a domain you own pre-fill the alias as the sender even when it isn't a configured identity (#246)
Performance
Mail: Prefetch initial email data on login
Auth: Parallelize login round-trips and drop redundant JMAP re-verify
Fixes
Auth: Skip upstream JMAP reverify for trusted URLs (#237)
Auth: Show account identity in the switcher header instead of the sending alias
Compose: Fall back to the primary identity signature on reply
Setup: Drop redundant first-login banner about removing ADMIN_PASSWORD (#222)
UI: Consistent notice cards for server probe results
New: Help shape Bulwark Webmail. Each instance now sends a lightweight daily heartbeat (version, platform, bucketed account counts, feature toggles - never message data or PII) so we can see which platforms and features actually get used and prioritize fixes where they matter most. You're in control: opt out any time from Admin β Telemetry or by setting BULWARK_TELEMETRY=off. Full schema in the privacy notice.
Features
Telemetry: Anonymous instance telemetry, on by default. Reports schema version, platform, bucketed account counts, and feature toggles only - disable from the admin UI, with BULWARK_TELEMETRY=off, or by clearing the endpoint
Telemetry: Track unique logins (HMAC'd per instance, 90-day retention) so the heartbeat can report bucketed account totals without storing usernames
Plugins: Theme API v2 with token compiler and skin slot
Plugins: Extension preview page and detailed extension info API
Calendar: Right-click context menu on empty calendar space
Docker: Persistent named volume for telemetry data so the instance id and admin's consent choice survive container upgrades
Fixes
Security: Block telemetry endpoint from pointing at internal/loopback hosts (validation + DNS-rebind re-check at fetch time)
Plugins: New composer-sidebar slot and ui:composer-sidebar permission β plugins can now render a panel on either side of the New Message dialog. See repos/subway-surfers for an example
Plugins: Manifests can declare frameOrigins β a strictly-validated list of https://host origins the plugin needs to embed. The proxy reads the union from enabled plugins and merges it into the host CSP frame-src, so the host CSP no longer needs to know about specific embed providers
Calendar/Contacts: JMAP sharing for calendars and address books
i18n: Czech language support
Fixes
Security: Validate URLs before outbound fetch
Calendar: Prevent drag creation on touch events in the time grid
Contacts: Emit RFC 9553 name kinds and decode QUOTED-PRINTABLE in vCard import (#224, #187)
Mail: Hide preview line in compact density to match settings preview (#223)
Proxy: Inline matcher for Next.js proxy and drop unnecessary Node.js runtime config
i18n: Portuguese fixes for "ficheiro" and "contactos" variants
Self-service portal now needs Stalwart 0.16+: Stalwart dropped its self-service HTTP API in 0.16.0 and replaced it with JMAP. Bulwark Webmail only talks to the new JMAP endpoint, so the self-service portal (account settings, app passwords, API keys) requires Stalwart 0.16 or newer. STALWART_API_URL is deprecated, these actions go through the normal JMAP session.
Features
Stalwart: Migrate Stalwart management API to JMAP x: methods for Stalwart 0.16
Admin: Add API Keys management and IP allowlist for App Passwords
Contacts: Revamp contact detail view with filters, photo, print, and duplicate actions