Bulwark 1.18.0 works with Stalwart 1.0.

Read the upgrade notes
Docs / DevelopmentEdit on GitHub

Contributing

Bulwark is AGPL-licensed and developed in public on GitHub. Bug reports, feature requests, translations and patches are all welcome, and you don't need to be an expert to send one.

If your dev environment won't start, a bug is hard to pin down or a translation has you stuck, ask on the Discord server. That is also where the maintainers and other contributors talk.

What fits in Bulwark

Bulwark is a mail, calendar, contacts and files client for Stalwart. Read this section before you start on a feature. A pull request that doesn't fit is closed however good the code is, and that costs you more time than it costs us.

Stalwart first, standard JMAP welcome

Bulwark is built and tested against Stalwart. Changes that make Bulwark work with other standards-compliant JMAP servers are welcome when they cost Stalwart users nothing: no slower paths, no lost features, and no branches that only exist for one provider. Test against Stalwart as well, since that is what the maintainers run.

Core or plugin

Core is for what most people expect from a mail, calendar, contacts and files client. Everything narrower belongs in a plugin or a theme. That covers niche workflows, integrations with other services, and features that only a few deployments want. If a plugin can't do what you need because an API, slot or hook is missing, a pull request that adds that extension point is welcome.

These don't go in core:

  • Other protocols. IMAP, POP3 and other non-JMAP backends. Bridges such as the legacy proxy are separate projects.
  • Integrations with other services. CRMs, chat, cloud storage, AI assistants and the like. Write them as plugins.
  • Stalwart server administration. Domains, queues, DNS and server settings stay in Stalwart's own admin interface. Bulwark covers what users manage for their own account and what the webmail's admin dashboard needs.
  • Personalising the app's look beyond themes. Per-user fonts, colour pickers and alternative layouts. Changes to the look go through themes.

Settings

A new behaviour should have one good default. When deployments really do need different behaviour, make it an admin setting in the admin dashboard or the admin policy, not a per-user toggle. A pull request that adds a user setting so that its behaviour can be switched off will usually be asked to choose a default instead.

Both editions, every screen size

  • Lite. Features that run in the browser have to work in Lite. A feature that needs the Node server has to be hidden in Lite, not left broken.
  • Mobile. UI changes have to work at phone width and with touch, as well as in the light and dark theme.

The mobile app

The same scope applies to the mobile app. A feature that is out of scope for the webmail is out of scope for the app as well.

Before you write code

Bug fixes and translations can go straight to a pull request. Open an issue first, and wait until a maintainer agrees to the approach, when a change:

  • is larger than about 500 lines, not counting locale files and tests,
  • adds a runtime dependency to package.json, or
  • touches security-sensitive code, whatever its size: sign-in, OAuth and sessions, the email HTML sanitizer, the plugin sandbox, outgoing-request guards (url-guard), and the admin routes.

Agreement first saves you from building something we can't merge. Large features are then delivered as a series of pull requests, each of which works and can be reviewed on its own. Don't send one large pull request, and don't send a stack where every pull request depends on the next.

Development setup

git clone https://github.com/bulwarkmail/webmail.git
cd webmail
npm install
cp .env.dev.example .env.local
npm run dev

Then open http://localhost:3000. The .env.dev.example file turns on the built-in mock JMAP server (DEV_MOCK_JMAP=true), so you can develop without a mail server and sign in with any username and password. To work against a real server instead, copy .env.example and set JMAP_SERVER_URL. The environment reference lists every other variable.

For a tour of the code before you change it, read Architecture.

Checks and tests

npm run typecheck   # TypeScript
npm run lint        # ESLint
npm run lint:fix    # ESLint, fixing what it can

Husky runs these on every commit, and CI runs the unit tests on every push and pull request.

SuiteCommandWhat it covers
Unitnpx vitest runVitest with jsdom. Tests live in __tests__/ folders next to the code
Translationsnpm run test:translationsLocale files checked for structural drift against English
Integrationnpm run test:integrationPlaywright against a real Stalwart server in Docker
Lite smokenpm run test:lite-smokePlaywright against a demo Lite export

Run a single unit test file with npx vitest run lib/__tests__/<name>.test.ts, or npx vitest to watch.

The integration suite needs Docker and takes several minutes. It has its own setup notes and findings log in integration/README.md. New behaviour that touches mail or folder synchronization, or multi-account handling, belongs there.

Project structure

  • webmail/
    • app/Next.js App Router
      • (main)/[locale]/Locale-aware app pages (mail, calendar, contacts, files, settings)
      • (main)/admin/Admin dashboard
      • (main)/setup/First-launch setup wizard
      • (sandbox)/Isolated plugin sandbox routes
      • api/Route handlers (auth, admin, jmap, caldav, …)
    • components/React components, by feature
      • email/Email list, viewer, composer
      • calendar/ contacts/ files/ filters/ templates/
      • layout/Sidebar, shell, navigation
      • settings/Settings panels
      • plugins/Plugin host UI
      • ui/Reusable primitives
    • contexts/React contexts
    • hooks/Custom React hooks
    • i18n/next-intl routing, locale detection, RTL direction
    • lib/Utilities and libraries
      • jmap/JMAP client implementation
      • stalwart/Stalwart-specific admin/API helpers
      • admin/ auth/ oauth/Config, sessions, OAuth flows
      • plugin-sandbox/Plugin sandbox bridge and hardening
    • locales/Translation files, one directory per locale
    • stores/Zustand state stores
    • public/Static assets and branding
    • integration/Dockerized Stalwart + Playwright suite

Code style

  • TypeScript for all new code, with proper types and interfaces. Avoid any.
  • Functional components with hooks. Keep components focused, and pull reusable logic into a custom hook under hooks/.
  • Put "use client" only where it is needed.
  • Reusable UI primitives go in components/ui/; state lives in Zustand stores under stores/.
  • Style with Tailwind utility classes and the existing theme CSS variables, and check both the light and dark theme. Reach for custom CSS only when Tailwind can't express it.

Translations

Translation runs through next-intl, currently across 27 languages: Arabic, Catalan, Czech, Danish, Dutch, English, Farsi, French, German, Hebrew, Hungarian, Italian, Japanese, Korean, Latvian, Mongolian, Norwegian Bokmål, Polish, Portuguese, Romanian, Russian, Simplified Chinese, Slovak, Spanish, Traditional Chinese, Turkish and Ukrainian. English (locales/en/common.json) is the source of truth.

  1. Never hardcode user-facing text. Always go through a translation:

    const t = useTranslations("namespace");
    return <div>{t("key")}</div>;
    
  2. Add new keys to en/common.json first. Other locales can follow in the same pull request or a later one; missing keys fall back to English.

  3. Use the existing namespaces: login.*, sidebar.*, email_list.*, email_viewer.*, email_composer.*, settings.*, calendar.*, contacts.*, notifications.*, common.*.

  4. Keep navigation locale-aware: router.push(`/${params.locale}/settings`).

Translation pull requests don't need an issue first. Keep each one to a single locale.

Right-to-left layouts

Arabic, Farsi and Hebrew render right to left (see i18n/direction.ts). Use Tailwind's logical utilities (ms-*/me-*, ps-*/pe-*, start-*/end-*) rather than physical ones (ml-*, pl-*, left-*) so layouts flip correctly. Popovers positioned in JavaScript with getBoundingClientRect() don't pick up logical utilities, so check isDocumentRTL() there.

Adding a locale

A new locale takes edits in four places:

  1. locales/<code>/common.json: copy locales/en/common.json and translate it.
  2. i18n/routing.ts: add the code to SUPPORTED_LOCALES.
  3. i18n/request.ts: add a case to the static-import switch.
  4. components/ui/language-switcher.tsx: add { value, label } with the language's native name, plus a flag in components/ui/flag-icons.tsx.

For a right-to-left language, also add the code to rtlLocales in i18n/direction.ts. Run npm run test:translations afterwards.

Pull requests

  1. Fork the repository and create a branch: git checkout -b feature/my-feature.
  2. Make the change, with unit tests for new logic and translations for new text.
  3. Run everything: npm run typecheck && npm run lint && npx vitest run, and npm run build for changes that touch the build.
  4. Push to your fork and open a pull request.

In the pull request:

  • Keep it to one change.
  • Say what changed and why.
  • Add screenshots for UI changes, at desktop and phone width.
  • Reference the issues it relates to, including the one where the approach was agreed if it needed one.

If review feedback goes unanswered for 30 days, the pull request is closed. Reopen it when you pick the work up again.

Commit messages

Use Conventional Commits:

TypeFor
feat:New features
fix:Bug fixes
docs:Documentation
style:Formatting and other changes with no effect on behaviour
refactor:Restructuring without changing behaviour
test:Adding or updating tests
chore:Maintenance
feat: add email threading support
fix: resolve attachment download issue

AI-assisted contributions

AI tools are part of how Bulwark is developed, and you're welcome to use them for code, tests, translations and documentation. You don't need to say that you did. You are responsible for what you submit, the same as for anything you wrote by hand: understand it, run it, and be ready to explain it in review.

Use a capable model

Use a current, capable model, the strongest one your tool offers. Don't use small, heavily quantized or older-generation models. Bulwark is a large codebase built on a protocol that most models know little about, and weaker models fill the gaps with guesses that look right:

  • JMAP methods, properties and capabilities that don't exist, or that Stalwart doesn't support.
  • Code that works with one account and breaks with several. Email, mailbox and account ids repeat across accounts and servers, so every write has to name the account it belongs to.
  • Hardcoded English text instead of translations, and physical CSS utilities (ml-*, left-*) that break right-to-left layouts.
  • Email HTML that skips the sanitizer, or remote content loaded when it should stay blocked.

Reviewing a change like that takes longer than writing it from scratch, so pull requests that show these patterns may be closed without a line-by-line review.

Before you open the pull request

  • Read every line of the diff and remove anything you can't explain.
  • Keep it to the change you meant to make. Drop reformatting, renames and new abstractions the tool added along the way.
  • Run the checks and tests above, then try the change in a browser. If it touches mail sync or several accounts, try it against a real Stalwart server as well as the mock.
  • Check that new tests fail without your change. A test that only records what the code does today doesn't catch anything.
  • In translations, keep placeholders such as {count} and plural rules intact, and match the tone of the existing strings in that language.
  • Write the pull request description yourself, or cut it down to what changed and why. Long generated summaries slow the review down.
  • Answer review questions in your own words.

Issues and security reports

The same applies to issues and vulnerability reports. Reproduce the problem on a running instance before you send it, and include the steps. Reports that describe code or behaviour that doesn't exist in Bulwark are closed.

Documentation

The documentation lives in the website repository under docs/, one Markdown file per page. If your change alters behaviour a page describes, update that page too, or mention it in your pull request.

Reporting issues

Open an issue on GitHub and include:

  • Browser and version
  • Stalwart version
  • Steps to reproduce
  • Expected and actual behaviour
  • Screenshots, if they help

Security

  • Never commit secrets: API keys, passwords, tokens or .env* files.
  • Sanitize user input and email content.
  • Keep remote content in emails blocked by default.
  • Report vulnerabilities privately to dev@bulwarkmail.org or through a GitHub security advisory, never in a public issue.

License

By contributing, you agree that your contributions are licensed under the GNU Affero General Public License v3 (AGPL-3.0-only), together with the additional permission for app store distribution in the webmail and mobile app LICENSE files.