Edits of a recurring meeting from one of its occurrences went wrong in several ways (live on 1.0 and 0.16.23, attendees included): - A "this and following" split whose new series could not be created stayed half done: the calendar store answers most failures with null, and only a thrown error put the old series back. The series stayed ended, told so to the attendees, and the save said it had worked. A null now puts the series back as well and fails the save. The rollback writes the old overrides back as a whole map: a pointer into a series without overrides fails, and the split may just have removed its only one. - "This occurrence" on the series itself (a meeting opened from a link or a search result) went out in one write that reached no attendee who had answered. It now takes the same two steps as an occurrence of an expanded series: the override as it is without messages, then the change of it. - The new series of a split started at the opened occurrence's own time and in the user's zone: Mondays in New York, one occurrence moved to Tuesday, renamed from there by a user in Berlin, became a Tuesday series in Berlin. It now goes on from that occurrence's recurrence id, in the series' zone, moved only by what the editor moved; the occurrence keeps its own time through its override. It takes the old series' details with what the user changed, not the opened occurrence's own ones. - "All events" from an occurrence with an override of its own wrote that occurrence's own title, copied into its override, back to the series: a new room there renamed the series. Only the fields the user changed go to the series now; the opened occurrence's override takes them too (on 1.0 also where it holds them as pointers), and other overrides where they only held the series' old value. - The move of an "all" edit was measured on the user's clock: across a week in which only one of the two zones changes to or from summer time, the series moved an hour more or less than picked. It is now measured on the series' own clock. - 0.16 (0.16.23 checked) reads a rule's UNTIL as the series' wall clock taken for UTC, so in a zone behind UTC a split or "delete this and following" kept the occurrence it was to end before. The UNTIL is now earlier by the zone's offset, which ends the series there on 0.16 and 1.0 alike. - An applied counter proposal for one occurrence of a recurring meeting went to the first event with its uid. It now goes to the occurrence the proposal names; one for a whole series of which only occurrences are in view is not applied to any of them.
Website · Documentation · Live demo · Releases · Discord
Bulwark Webmail is a self-hosted webmail client for Stalwart Mail Server. It talks to Stalwart over JMAP and puts mail, calendar, contacts and files behind one login, with one set of settings and one admin dashboard.
Contents
- Screenshots
- Features
- Quick start
- Other ways to install
- Configuration
- Documentation
- Development
- Community and support
- License
Screenshots


Calendar week view · Contact details with recent mail and upcoming events

Composer with identities and formatting · Light and dark themes
Features
- Mail: threading, unified inbox, cross-account views, full-text search, Sieve filters, S/MIME, templates, scheduled send
- Calendar: month, week, day and agenda views, recurring events, iMIP invitations, CalDAV subscriptions
- Contacts: several address books, groups, vCard import and export
- Files: Stalwart's JMAP file storage, with previews, sharing and folder upload
All four share single sign-on and 2FA, multiple accounts, 27 languages, PWA install and web push, themes, plugins and keyboard shortcuts. The full list is on the All features page.
There are two editions. The full edition runs as a Node.js server and adds the admin dashboard, OAuth, plugins and settings sync. Bulwark Lite is the same client as static files, served by any web host or by Stalwart itself.
Quick start
docker run -d -p 3000:3000 ghcr.io/bulwarkmail/webmail:latest
Then open http://localhost:3000. A setup wizard asks for your Stalwart server and an admin password. The installation guide has the details, and Stalwart setup covers the mail server side.
Other ways to install
| Method | Guide |
|---|---|
| Docker Compose | Compose |
| Behind a reverse proxy or on a sub-path | Reverse proxy |
| From source, without Docker | Manual install |
| Lite on any static host | Static hosting |
| Lite as a container | Container image |
| Lite served by Stalwart | Install on Stalwart |
Updating explains how to move to a new version.
Configuration
Most installs are set up in the wizard on first launch and changed later in the admin dashboard. You can also use environment variables, which fit read-only or immutable deployments better. When both set the same key, the value saved in the admin config wins, so an environment variable only fills in what the admin config leaves unset.
JMAP_SERVER_URL=https://mail.example.com
APP_NAME=My Webmail
| Topic | Guide |
|---|---|
| Overview, config files and precedence | Configuration |
| Every variable | Environment reference |
| OAuth2 / OIDC and single sign-on | Authentication, Embedded SSO |
| Several JMAP servers, custom endpoints | Multi-server deployments, Custom endpoints |
| Branding, logos, per-domain branding | Customization |
| Anonymous telemetry (off by default) | Anonymous usage stats |
Documentation
All documentation is at bulwarkmail.org/docs:
- Getting started: Introduction, Installation, Editions, Bulwark Lite, Demo mode
- Deployment: Docker, Manual install, Static hosting, Install on Stalwart, Updating
- Guides: Admin dashboard, Account security, Impersonation, Plugins, Marketplace, Troubleshooting
- Extensions: Introduction, manifest.json, Publishing
- Development: Architecture, Contributing
- Legal: Privacy
The pages are Markdown files in the website repository. Send corrections there.
Development
git clone https://github.com/bulwarkmail/webmail.git
cd webmail
npm install
cp .env.dev.example .env.local # built-in mock JMAP server, no mail server needed
npm run dev
npm run typecheck
npm run lint
npx vitest run # unit tests
npm run test:integration # Stalwart in Docker + Playwright
The contributing guide covers tests, translations, code style and pull requests. Architecture explains how the code is organized.
The stack: Next.js 16 and React 19, TypeScript, Tailwind CSS v4, Zustand, Tiptap, next-intl, Tabler Icons, and our own JMAP client (RFC 8620). Tests run on Vitest and Playwright.
Community and support
- Questions: check Troubleshooting, then ask on Discord.
- Bugs and feature requests: open a GitHub issue.
- Security vulnerabilities: report them privately to dev@bulwarkmail.org or through a security advisory, never in a public issue.
- Release notes: CHANGELOG.md and the GitHub releases.
License
GNU AGPL v3. This repository preserves the original MIT attribution for the fork lineage in NOTICE.
Acknowledgments
Thanks to root-fr/jmap-webmail and @ma2t for the groundwork this project builds upon.

