JMAP Webmail built for the 21st Century. A modern, self-hosted email client for Stalwart Mail Server powered by the JMAP protocol. Email, calendar, contacts and files. Fast, private, and open source. https://bulwarkmail.org
  • TypeScript 98.5%
  • Shell 0.9%
  • CSS 0.3%
  • JavaScript 0.2%
Find a file
2026-07-14 16:20:52 +02:00
.github fix: show git commit in About instead of "unknown" 2026-05-02 00:28:08 +02:00
.husky feat: implement client-side recurrence expansion for calendar events 2026-03-29 17:12:30 +02:00
app Feature: contact groups as single expandable recipient chips 2026-07-09 17:46:06 +02:00
components fix: enable thread expansion in focused list 2026-07-14 16:20:52 +02:00
contexts Initial release: JMAP Webmail Client 2025-12-10 17:54:22 +01:00
e2e feat: add contacts phase 2, advanced search, vacation responder, Docker & TOTP 2FA 2026-02-16 18:51:30 +01:00
hooks feat: add Jalali (Persian/Shamsi) calendar support with Saturday as week start (#490) 2026-07-08 15:42:42 +02:00
i18n feat(i18n): Hebrew locale + full RTL support 2026-07-08 00:07:34 +02:00
lib fix: Generate Message-ID client-side using the sender's domain 2026-07-14 16:20:31 +02:00
locales feat: add plugin ui.rerenderEmail API and restyle read-receipt banner 2026-07-09 16:04:33 +02:00
public feat(pwa): add Apple Touch icons for iOS home screen 2026-07-05 19:51:06 +02:00
screenshots chore: update version to 1.5.0 2026-04-22 23:49:44 +02:00
stores fix(identity): sync default sender identity per account (#507) 2026-07-13 21:21:34 +02:00
.dockerignore fix: remove claude directory from .dockerignore and .gitignore 2026-03-19 19:06:31 +01:00
.env.dev.example docs: document DOMAIN_BRANDING env var in examples and README #332 2026-05-28 20:13:11 +02:00
.env.example feat(oauth): add OAUTH_AUTHORIZE_URL to override authorize endpoint 2026-06-19 12:30:23 +02:00
.gitignore fix: remove benchmark directory from .gitignore 2026-05-09 13:13:08 +02:00
CHANGELOG.md chore: update version to 1.7.7 2026-07-09 16:25:12 +02:00
CONTRIBUTING.md fix: standardize punctuation in tooltips and comments across multiple locales and code files 2026-05-14 21:44:24 +02:00
docker-compose.yml feat: web setup wizard + admin config/state dir split (#226) 2026-05-09 17:37:41 +02:00
Dockerfile feat: add NEXT_PUBLIC_DEFAULT_LOCALE for fallback UI locale #243 2026-05-25 17:01:52 +02:00
eslint.config.mjs chore: unblock pre-commit lint hook 2026-05-13 00:34:35 +02:00
FEATURES.md feat: cross-account "All accounts" views + full group/shared-account support 2026-06-23 19:16:22 +02:00
instrumentation.node.ts feat: web setup wizard + admin config/state dir split (#226) 2026-05-09 17:37:41 +02:00
instrumentation.ts fix: details toggle UX, Edge Runtime warnings, minimatch CVE 2026-02-28 21:03:02 +01:00
LICENSE feat: update LICENSE to include fork notice and improve formatting 2026-03-16 17:42:55 +01:00
next.config.ts fix: add outputFileTracingExcludes to optimize Turbopack memory tracing 2026-05-16 16:35:06 +02:00
NOTICE feat: update licensing information to AGPL-3.0-only and add NOTICE file for fork lineage 2026-03-16 16:46:45 +01:00
package-lock.json chore: update version to 1.7.7 2026-07-09 16:25:12 +02:00
package.json chore: update version to 1.7.7 2026-07-09 16:25:12 +02:00
playwright.config.ts feat: add contacts phase 2, advanced search, vacation responder, Docker & TOTP 2FA 2026-02-16 18:51:30 +01:00
postcss.config.mjs Initial release: JMAP Webmail Client 2025-12-10 17:54:22 +01:00
proxy.ts fix(csp): allow external/data fonts so email webfonts render 2026-07-05 21:28:15 +02:00
README.md chore: update version to 1.7.7 2026-07-09 16:25:12 +02:00
setup.sh fix: clone source when setup.sh runs detached from a checkout #518 2026-06-28 23:31:32 +02:00
tailwind.config.ts feat: implement email keyword management with color tagging 2026-03-11 18:40:09 +01:00
tsconfig.json feat: enforce forced/managed plugins and policy. Split user upload permission 2026-03-27 16:33:17 +01:00
VERSION chore: update version to 1.7.7 2026-07-09 16:25:12 +02:00
vitest.config.ts fix: resolve test failures across translations, contact form, and vitest config 2026-03-16 22:51:49 +01:00
vitest.setup.ts fix: repair pre-existing failing vitest suite 2026-06-19 23:53:20 +02:00

Bulwark Webmail

Bulwark Webmail

A modern, self-hosted webmail client for Stalwart Mail Server, built with Next.js and the JMAP protocol.

License: AGPL v3 Discord Version Docker


Installer

New in 1.6.4: a web-based setup wizard runs on first launch no .env.local editing, no shelling into the container.

Setup wizard

Point a browser at the running container and the wizard guides you through:

  • Server probe one or more JMAP endpoints, optional auto-pick by email domain, Stalwart feature toggle
  • Auth OAuth2 / OIDC discovery and validation, or basic-auth fallback
  • Security generate or paste a SESSION_SECRET, opt into settings sync
  • Logging text or JSON, level
  • Branding upload favicon, app logos, login logos, and company / legal URLs
  • Review grouped summary with an advanced toggle for the full config
  • Admin set the initial admin password and optionally drop a .config-locked marker so the config volume can be remounted read-only

The wizard writes to ADMIN_CONFIG_DIR (./data/admin by default). Setting JMAP_SERVER_URL in the environment skips the wizard and uses env-managed configuration instead.


Screenshots

Mail view
Calendar 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.
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.
Light mode Settings
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:

  • Mail threading, unified inbox, cross-account "All accounts" views, 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: a web setup wizard, OAuth2 / OIDC SSO, TOTP 2FA, multi-account with HTTP/2 connection pooling, 18 languages, PWA install, dark/light themes, a plugin system with an extension marketplace, and an admin dashboard.

Full feature list: FEATURES.md.


Quick Start

Docker

docker run -d -p 3000:3000 ghcr.io/bulwarkmail/webmail:latest

Or with Docker Compose:

docker compose up -d

On first launch, open http://localhost:3000 the web setup wizard walks you through JMAP server, OAuth, branding, and the admin password. No .env.local editing required. Existing installs that already define JMAP_SERVER_URL in their environment skip the wizard and keep the env-managed flow described under Configuration.

From Source

git clone https://github.com/bulwarkmail/webmail.git
cd webmail
npm install
npm run build && npm start
# Then open http://localhost:3000 to run the setup wizard

Development

npm run dev        # Dev server with a mock JMAP server
npm run typecheck
npm run lint

Configuration

Most deployments are configured through the setup wizard (on first launch) and the admin dashboard thereafter; values are written to the admin config directory rather than .env.local. Environment variables remain supported for operators who prefer file-driven configuration or read-only / immutable infrastructure. When an environment variable is set, it takes precedence over the corresponding admin-managed value, so setting JMAP_SERVER_URL will hide that field from the wizard and lock it in the admin UI.

All variables are evaluated at runtime, so Docker deployments can be reconfigured without rebuilding. Edit .env.local:

# Optional  overrides whatever the wizard writes
JMAP_SERVER_URL=https://mail.example.com
APP_NAME=My Webmail
Server listen address
HOSTNAME=0.0.0.0    # Default; use "::" for IPv6
PORT=3000
OAuth2 / OIDC
OAUTH_ENABLED=true
OAUTH_CLIENT_ID=webmail
OAUTH_CLIENT_SECRET=              # optional, for confidential clients
OAUTH_CLIENT_SECRET_FILE=         # path to a file containing the secret
OAUTH_ISSUER_URL=                 # optional, for external IdPs

Endpoints are auto-discovered via .well-known/oauth-authorization-server or .well-known/openid-configuration.

Session & settings sync
SESSION_SECRET=                      # openssl rand -base64 32
SESSION_SECRET_FILE=/session-secret  # path to a file containing the secret

SETTINGS_SYNC_ENABLED=true
SETTINGS_DATA_DIR=./data/settings    # mount as a volume in Docker

Credentials are encrypted with AES-256-GCM and stored in an httpOnly cookie (30-day expiry). Settings sync stores per-account preferences encrypted at rest and requires SESSION_SECRET.

Custom JMAP endpoint
ALLOW_CUSTOM_JMAP_ENDPOINT=true

Shows a "JMAP Server" field on the login form. External servers must CORS-allow the webmail origin.

Branding & PWA
APP_NAME=My Webmail
APP_SHORT_NAME=Webmail
APP_DESCRIPTION=Your personal mail

FAVICON_URL=/branding/favicon.svg
PWA_ICON_URL=/branding/icon.svg      # falls back to FAVICON_URL
PWA_THEME_COLOR=#3b82f6
PWA_BACKGROUND_COLOR=#ffffff

APP_LOGO_LIGHT_URL=/branding/logo-light.svg
APP_LOGO_DARK_URL=/branding/logo-dark.svg
LOGIN_LOGO_LIGHT_URL=/branding/login-light.svg
LOGIN_LOGO_DARK_URL=/branding/login-dark.svg

LOGIN_COMPANY_NAME=My Company
LOGIN_WEBSITE_URL=https://example.com
LOGIN_IMPRINT_URL=https://example.com/imprint
LOGIN_PRIVACY_POLICY_URL=https://example.com/privacy

# Per-domain overrides (optional). When the webmail is served on multiple
# hostnames, each host can override any subset of the branding fields above.
# Match is on the request Host (or X-Forwarded-Host). Use "*.example.com" to
# match any subdomain. Unset fields fall back to the global values.
DOMAIN_BRANDING=[{"host":"maildomain1.com","loginCompanyName":"Company One","loginLogoLightUrl":"/branding/one.svg"},{"host":"maildomain2.com","loginCompanyName":"Company Two"}]
Extension directory
EXTENSION_DIRECTORY_URL=https://extensions.bulwarkmail.org

Enables the admin marketplace for browsing and installing plugins and themes.

Stalwart integration & logging
STALWART_FEATURES=true               # password change, Sieve filters, etc.

LOG_FORMAT=text                      # "text" or "json"
LOG_LEVEL=info                       # error | warn | info | debug
Admin data directories
ADMIN_CONFIG_DIR=./data/admin        # operator-authored: config.json, policy.json, plugins/, themes/
ADMIN_STATE_DIR=./data/admin-state   # runtime: audit log, login timestamps, setup token
ADMIN_CONFIG_READONLY=true           # enforce read-only mode at the app layer

The split lets you mount the config volume read-only after the setup wizard completes. Legacy installs that pre-date the split keep working through ADMIN_DATA_DIR.

Subpath / reverse proxy mount

To serve the webmail at a subpath (e.g. https://example.com/webmail):

NEXT_PUBLIC_BASE_PATH=/webmail
NEXT_PUBLIC_LOCALE_PREFIX=always     # avoids next-intl rewrite loops

Unlike most other variables, NEXT_PUBLIC_BASE_PATH is read at build time because Next.js bakes it into emitted asset URLs. To use it with the published Docker image, build your own image with the variable set:

docker build --build-arg NEXT_PUBLIC_BASE_PATH=/webmail -t bulwark-webmail .

Then point your reverse proxy at the container without stripping the prefix - the app expects to receive requests under /webmail/... and serves all routes (/webmail/api/..., /webmail/_next/static/..., /webmail/sw.js, etc.) accordingly.

Keyboard Shortcuts

Key Action
j / k Navigate between emails
Enter / o Open email
Esc Close / deselect
c Compose
r / R Reply / Reply all
f Forward
s Star
e Archive
# Delete
/ Search
? Show all shortcuts

Tech Stack

Framework Next.js 16 with App Router
Language TypeScript
Styling Tailwind CSS v4
State Zustand
Protocol Custom JMAP client (RFC 8620)
i18n next-intl
Icons Lucide React

Why Stalwart?

Stalwart 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

See CONTRIBUTING.md.

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.