diff --git a/.env.example b/.env.example index 6036141b..2d47077f 100644 --- a/.env.example +++ b/.env.example @@ -274,7 +274,9 @@ LOGIN_WEBSITE_URL=https://bulwarkmail.org # # Fallback UI locale used when the visitor's Accept-Language header does not # match any supported locale. Defaults to "en". -# Supported: cs, da, de, en, es, fr, it, ja, ko, lv, nl, pl, pt, ru, tr, uk, zh +# Supported: ar, ca, cs, da, de, en, es, fa, fr, he, hu, it, ja, ko, lv, nl, pl, +# pt, ro, ru, sk, tr, uk, zh +# An unsupported value falls back to "en". # NEXT_PUBLIC_DEFAULT_LOCALE=tr # Locale prefix mode for URLs. Recommended "always" when proxying under a diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7d941e32..67f38b82 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,13 +10,13 @@ # Contributing to Bulwark Webmail -We're writing the webmail we wanted in 2026 and didn't find. Modern protocol, modern tooling, modern UI. Not a SaaS. Not a startup. Not for sale. +We're writing the webmail we wanted in 2026 and didn't find: a JMAP-native client with an interface built this decade. It's AGPL and self-hosted, run by the people who use it rather than sold to them. -If that resonates with you, we'd love your help. This guide covers how to get the project running, the conventions we follow, and how to land your first change. +If that sounds like your kind of project, we'd love the help. ## Join the Community -You don't need to be an expert to contribute. Whether you're setting up your dev environment for the first time, filing a bug, or translating a string, the Discord is the fastest way to get unstuck and meet the people working on this. +You don't need to be an expert to contribute. A dev environment that won't start, a bug you're not sure how to report, a translation you're stuck on: Discord is the fastest way to get unstuck and to meet the people working on this. - **Get support** - real-time help with development hurdles - **Share ideas** - feature suggestions, design feedback, doc improvements @@ -46,15 +46,21 @@ You don't need to be an expert to contribute. Whether you're setting up your dev 3. **Set up environment**: ```bash - cp .env.example .env.local - # Edit .env.local with your JMAP server URL + cp .env.dev.example .env.local ``` + This enables the built-in mock JMAP server (`DEV_MOCK_JMAP=true`), so you can + develop without a mail server. Log in with any username and password. To work + against a real server instead, copy `.env.example` and set `JMAP_SERVER_URL`. + 4. **Start development server**: + ```bash npm run dev ``` + Then open http://localhost:3000. + ### Code Quality Before submitting a pull request, ensure your code passes all checks: @@ -72,6 +78,19 @@ npm run lint:fix These checks run automatically on commit via Husky pre-commit hooks. +### Testing + +| Suite | Command | What it covers | +| ---------------- | -------------------------- | ------------------------------------------------------------------ | +| **Unit** | `npx vitest run` | Vitest + jsdom. Tests live in `__tests__/` folders next to the code | +| **Translations** | `npm run test:translations` | Locale files checked for structural drift against English | +| **Integration** | `npm run test:integration` | Playwright against a real Stalwart server in Docker | +| **E2E smoke** | `npx playwright test` | UI smoke tests against `npm run dev` | + +Run a single unit test file with `npx vitest run lib/__tests__/.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](integration/README.md). New behavior that touches mail/folder synchronization or multi-account handling belongs there. + ## Code Style Guidelines ### TypeScript @@ -97,7 +116,9 @@ These checks run automatically on commit via Husky pre-commit hooks. ## Internationalization (i18n) -This project uses **next-intl**. English (`/locales/en/common.json`) is the source of truth; we ship 15 additional locales (cs, de, es, fr, it, ja, ko, lv, nl, pl, pt, ru, tr, uk, zh). +This project uses **next-intl**. English (`/locales/en/common.json`) is the source of truth; we ship 23 additional locales (ar, ca, cs, da, de, es, fa, fr, he, hu, it, ja, ko, lv, nl, pl, pt, ro, ru, sk, tr, uk, zh). + +Arabic, Hebrew, and Persian 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. For popovers positioned in JS via `getBoundingClientRect()`, check `isDocumentRTL()`: inline `position: fixed` styles don't pick up logical utilities. ### Rules @@ -126,6 +147,19 @@ This project uses **next-intl**. English (`/locales/en/common.json`) is the sour router.push(`/${params.locale}/settings`); ``` +### Adding a new locale + +Registering a new locale takes edits in four places: + +1. `locales//common.json` - copy `locales/en/common.json` and translate +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 **native** language 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 - it checks the locale files for structural drift against English. + ## Pull Request Process ### Before Submitting @@ -138,13 +172,13 @@ This project uses **next-intl**. English (`/locales/en/common.json`) is the sour 2. **Make your changes** following the code style guidelines -3. **Test your changes** thoroughly +3. **Test your changes** thoroughly, and add unit tests for new logic 4. **Update translations** if you added user-facing text 5. **Run all checks**: ```bash - npm run typecheck && npm run lint + npm run typecheck && npm run lint && npx vitest run ``` ### Submitting @@ -181,21 +215,33 @@ docs: update README with keyboard shortcuts ``` webmail/ -├── app/ # Next.js App Router pages -│ └── [locale]/ # Locale-aware routing -├── components/ # React components -│ ├── email/ # Email-related components -│ ├── layout/ # Layout components -│ ├── settings/ # Settings components -│ └── ui/ # Reusable UI components -├── contexts/ # React contexts -├── hooks/ # Custom React hooks -├── lib/ # Utilities and libraries -│ └── jmap/ # JMAP client implementation -├── locales/ # Translation files -│ ├── en/ # English translations -│ └── fr/ # French translations -└── stores/ # Zustand state stores +├── 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 +│ ├── 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 +│ └── __tests__/ # Vitest unit tests +├── locales/ # Translation files, one directory per locale +├── stores/ # Zustand state stores +├── public/ # Static assets and branding +├── e2e/ # Playwright smoke tests (against `npm run dev`) +└── integration/ # Dockerized Stalwart + Playwright suite ``` ## Security diff --git a/FEATURES.md b/FEATURES.md index 1043c7c1..c2cd4c08 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -2,145 +2,147 @@ ## Mail -- Read, compose, reply, reply-all, and forward with a Tiptap rich text editor (inline images, drag-and-drop embedding, tables) -- Gmail-style threading with inline expansion and an optional conversation toggle -- Unified Mailbox – combined Inbox, Sent, Drafts, Junk, Archive, and Trash, scoped by default to the active account and its shared/group folders, with an optional admin-gated cross-account mode that spans every connected account -- Aggregated All mail / Unread / Starred entries in the Unified Mailbox – scoped by the same account boundary (or all accounts in cross-account mode) and narrowed by a per-account folder selection; each list labels the source folder of every message -- Search inside the Unified Mailbox – text search across every unified view (the per-role mailboxes and the folder-selected All mail / Unread / Starred lists); advanced filters are additionally available in the per-role unified mailboxes -- Three selectable mail layouts: split (three-pane), focused list, and reading pane at bottom -- Draft auto-save with identity preservation, persisted HTML body, and proper `In-Reply-To` / `References` headers on replies -- Attachment upload, download, drag-out to local file system, and inline preview – images, inline PDF on desktop and mobile, composer attachments (click to open), and `.eml` (`message/rfc822`) attachments rendered like an email; image thumbnails and forgotten-attachment warning -- Scheduled send and configurable send delay +- Read, compose, reply, reply-all, and forward in a Tiptap rich-text editor that handles inline images, drag-and-drop embedding, and tables +- Gmail-style threading, expanded inline, with a conversation toggle you can switch off +- The Unified Mailbox combines Inbox, Sent, Drafts, Junk, Archive, and Trash. By default it stays inside the active account and its shared/group folders; an admin can unlock a cross-account mode that spans every connected account. +- All mail, Unread, and Starred obey that same account boundary and can be narrowed to a per-account folder selection. Every row names the folder its message came from. +- Search runs across all unified views; the per-role mailboxes add the full filter panel on top +- Three mail layouts: split three-pane, focused list, or reading pane at the bottom +- Drafts auto-save, keeping the chosen identity, the HTML body, and correct `In-Reply-To` / `References` headers on replies +- Attachments upload, download, drag out to the file system, and preview inline. Images and PDFs render on desktop and mobile, composer attachments open on click, and `.eml` (`message/rfc822`) parts display as a nested email. There are list thumbnails, and a warning when you mention an attachment and forget it. +- Scheduled send, plus a configurable delay before anything leaves the outbox - Read receipts (MDN, RFC 8098) -- Editable, layout-preserving quote island when replying -- 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 -- 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 plus prefetching of initial email data on login -- Quick reply, hover actions, sender avatars (favicon-based), and recipient popovers -- Plain-text composer mode and Reply-To support -- Configurable signature position (above or below quoted text) per identity -- From-header override in the composer with optional catch-all auto-reply: replies to an alias on a domain you own auto-fill the alias as the sender even when it isn't a configured identity -- `.eml` file import via folder right-click menu +- Quoted text lands in an editable island that keeps the original layout +- Full-text search with a JMAP filter panel, search chips, wildcards, OR conditions, and cross-mailbox queries +- Multi-select for batch archive, delete, move, and tag +- Archive directly, by year, or by month +- Tags carry color labels, reorder by drag, and can be assigned by dropping a message onto them +- Star or unstar, with a configurable mark-as-read delay +- Large mailboxes scroll virtually, and the first page of mail prefetches at login +- Quick reply, hover actions, favicon-based sender avatars, recipient popovers +- Plain-text composer mode and Reply-To +- The signature sits above or below the quoted text, per identity +- Override the From header in the composer. Reply to an alias on a domain you own and it auto-fills as the sender, even when no identity exists for it. +- Import `.eml` files from the folder right-click menu - TNEF (`winmail.dat`) extraction and `message/rfc822` unwrapping -- Folder management with icon picker, subfolders, and sidebar counts -- Print directly from the viewer -- Browser history sync for back/forward navigation +- Folders take an icon, nest, and show counts in the sidebar +- Print from the viewer +- Browser back and forward move through mail history ## Calendar -- Month, week, day, and agenda views with a mini-calendar sidebar and task list -- 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 -- iCalendar import with preview, bulk create, and UID deduplication -- iCal / webcal subscriptions with editing and batch import -- Auto-generated birthday calendar from contacts -- Virtual locations (video conference URLs) as first-class event fields -- Task management with due dates, priority, and completion status -- Shared calendars with CalDAV discovery, multi-account home resolution, and per-viewer colors -- Week numbers, event hover preview, notifications with sound picker -- Real-time sync via JMAP push +- Month, week, day, and agenda views, with a mini-calendar and task list in the sidebar +- Drag an event to reschedule it, click-drag to create one, pull an edge to resize. Everything snaps to 15 minutes. +- Recurring events edit and delete by scope: this occurrence, this and following, or all +- iMIP invitations on create and update (RFC 5545 / 6047), an organizer/attendee panel, and RSVP with trust assessment +- `.ics` attachments are detected in the email viewer, so you can RSVP or import without leaving the message +- iCalendar import previews first, then bulk-creates, deduplicating on UID +- iCal / webcal subscriptions, editable, with batch import +- A birthday calendar generated from your contacts +- Virtual locations (video-conference URLs) are first-class event fields +- Tasks with due dates, priority, and completion status +- Shared calendars through CalDAV discovery, resolving homes across accounts, colored per viewer +- Week numbers, hover preview, notifications with a sound picker +- JMAP push keeps everything in sync ## Contacts -- JMAP sync (RFC 9553 / 9610) with local fallback -- Multiple address books with drag-and-drop between books -- Contact groups with member management -- vCard import/export (RFC 6350) with duplicate detection -- Trusted senders stored in a dedicated JMAP address book -- Autocomplete in the composer (To / Cc / Bcc) +- JMAP sync (RFC 9553 / 9610), falling back to local storage +- Several address books, with drag-and-drop between them +- Groups with member management +- vCard import/export (RFC 6350) that flags duplicates +- Trusted senders live in their own JMAP address book +- Autocomplete on To, Cc, and Bcc ## Filters & Templates -- Server-side filters via JMAP Sieve Scripts (RFC 9661) -- Visual rule builder with expanded view; conditions (From, To, Subject, Size, Body, Attachment…) with multi-value matching and actions (Move, Forward, Star, Discard…) -- Preserves rules authored in other clients +- Server-side filters as JMAP Sieve Scripts (RFC 9661) +- A visual rule builder: conditions on From, To, Subject, Size, Body, Attachment and more, each matching multiple values, with actions to move, forward, star, or discard +- Rules written in other clients survive the round-trip - Raw Sieve editor with syntax validation -- Vacation responder with date range scheduling -- Reusable email templates with placeholder auto-fill (`{{recipientName}}`, `{{date}}`, …) +- A vacation responder you can schedule to a date range +- Templates with placeholder auto-fill (`{{recipientName}}`, `{{date}}`, …) ## Files -- JMAP FileNode browser (Stalwart native cloud storage) with a real folder hierarchy; legacy flat-named files are migrated into nested `FileNode` folders automatically on load -- Streamed WebDAV PUT upload and folder upload with progress tracking -- Dynamic upload limits based on server configuration -- Grid and list views with sorting by name, size, or date -- Previews for images, text, audio, and video -- Clipboard operations (cut, copy, paste, duplicate), favorites, and recent files -- JMAP sharing (RFC 9670) for files and folders – share with users or groups at read, read/write, or manager levels via a principal picker, with share indicators and a "Shared with me" sidebar section for folders other principals have shared with you +- Browse Stalwart's native JMAP FileNode storage as a real folder tree. Legacy flat-named files migrate into nested `FileNode` folders on first load. +- Streamed WebDAV PUT upload, whole folders included, with progress +- Upload limits follow the server's own configuration +- Grid or list, sorted by name, size, or date +- Preview images, text, audio, and video +- Cut, copy, paste, duplicate; favorites; recent files +- JMAP sharing (RFC 9670) for files and folders. Pick a user or group from the principal picker and grant read, read/write, or manager. Shared items get an indicator, and anything other principals share with you appears under "Shared with me". ## Security & Privacy -- 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 -- SPF / DKIM / DMARC status indicators – surfaces the most severe SPF result and hides the "via" badge on spoofed mail -- OAuth2 / OIDC with PKCE (Keycloak, Authentik, or built-in), OAuth-only mode, OAuth app passwords, and non-interactive SSO for embedded deployments +- External content stays blocked until you say otherwise, and trusted senders are remembered +- HTML sanitized through DOMPurify +- S/MIME: manage certificates, then sign, encrypt, decrypt, and verify. Legacy 3DES / PBE is supported, and keys stay isolated per account. +- SPF / DKIM / DMARC indicators surface the most severe SPF result and drop the "via" badge on spoofed mail +- OAuth2 / OIDC with PKCE against Keycloak, Authentik, or the built-in provider, plus OAuth-only mode, OAuth app passwords, and non-interactive SSO for embedded deployments - TOTP two-factor authentication -- Account security panel for password and 2FA management via the Stalwart admin API -- Optional "Remember me" via AES-256-GCM encrypted httpOnly cookie -- Enforced CSP with per-request nonce, SSRF redirect validation, PDF iframe sandbox, and IP spoofing prevention -- Plugin hardening with dangerous-pattern detection and admin approval +- Password and 2FA management through the Stalwart admin API +- "Remember me" is optional and rides an AES-256-GCM encrypted httpOnly cookie +- CSP is enforced with a per-request nonce, alongside SSRF redirect validation, a sandboxed PDF iframe, and IP spoofing prevention +- Plugins are scanned for dangerous patterns and need admin approval - Newsletter unsubscribe (RFC 2369) ## Interface -- Selectable mail layouts (split three-pane, focused list, reading pane at bottom) with resizable columns -- Dark and light themes with intelligent email color transformation -- Bundled color themes including Aurora Glass and Elastic; theme cards render as a mini mailbox mockup built from the theme's own colors, with light/dark variant chips -- Responsive desktop, tablet, and mobile layouts +- Split three-pane, focused list, or bottom reading pane, columns resizable +- Dark and light themes. Email colors are remapped by luminance, so a mail hard-coded to dark-on-white stays readable on a dark background. +- Bundled themes such as Aurora Glass and Elastic. Each theme card renders as a miniature mailbox built from that theme's own colors, with chips for the light and dark variants. +- Layouts for desktop, tablet, and mobile - Full keyboard navigation -- Drag-and-drop email organization and tag assignment -- Interactive guided tour for new users -- Right-click context menus, toast notifications with undo -- Customizable toolbar position, favicon, and login branding -- Pinnable sidebar apps with drag-and-drop reordering -- Encrypted settings sync across devices +- Drag and drop to organize mail and assign tags +- A guided tour for first-time users +- Right-click menus, and toasts that offer an undo +- Toolbar position, favicon, and login branding are configurable +- Sidebar apps pin and reorder by drag +- Settings sync between devices, encrypted - Storage quota display -- WCAG AA contrast, reduced-motion support, focus trap, and screen reader live regions +- WCAG AA contrast, reduced-motion support, focus traps, and screen-reader live regions ## Internationalization -19 languages: Česky · Dansk · Deutsch · English · Español · Français · Italiano · Latviešu · Magyar · Nederlands · Polski · Português · Română · Türkçe · Русский · Українська · 한국어 · 日本語 · 简体中文 +24 languages: Català · Česky · Dansk · Deutsch · English · Español · Français · Italiano · Latviešu · Magyar · Nederlands · Polski · Português · Română · Slovenčina · Türkçe · Русский · Українська · עברית · العربية · فارسی · 한국어 · 日本語 · 简体中文 -Automatic browser detection with persistent preference. Configurable locale URL prefix via `NEXT_PUBLIC_LOCALE_PREFIX`. +- Arabic, Hebrew, and Persian render right-to-left; document direction and logical layout flip automatically +- The browser's `Accept-Language` picks the first language, and the choice persists per user +- `NEXT_PUBLIC_DEFAULT_LOCALE` sets the fallback, `NEXT_PUBLIC_LOCALE_PREFIX` the URL prefix ## Identity & Multi-Account -- Multiple simultaneous accounts with instant switching and per-account session persistence; the 5-account cap is lifted on HTTP/2 servers (limited by browser connection pooling on HTTP/1.1) -- Account switcher with connection status and default account selection -- Multiple sender identities with per-identity signatures, automatic sync, and badges in viewer/list -- Configurable signature position (above or below quoted text) -- Sub-addressing (`user+tag@domain.com`) with configurable delimiter and contextual tag suggestions +- Run several accounts at once and switch instantly, each keeping its own session. The 5-account cap lifts on HTTP/2 servers; on HTTP/1.1, browser connection pooling still sets the limit. +- An account switcher showing connection status, and a default account +- Multiple sender identities, each with its own signature, synced automatically and badged in the viewer and list +- Signature above or below the quoted text +- Sub-addressing (`user+tag@domain.com`), delimiter configurable, with tag suggestions drawn from context - Shared folders across accounts -- Shared / group (delegated) accounts: their folders appear alongside your own and can be merged into the Unified Mailbox ("Include group inboxes"); their messages are fully actionable there – open, mark read, spam / not-spam, move, delete, and archive – with folder unread counts kept in sync -- Multiple JMAP servers per deployment with optional auto-pick by email domain -- Optional custom JMAP endpoints on the login form (`ALLOW_CUSTOM_JMAP_ENDPOINT`) +- Shared and group (delegated) accounts put their folders next to your own, and "Include group inboxes" merges them into the Unified Mailbox. You can open, mark read, flag as spam or not-spam, move, delete, and archive their messages from there, and folder unread counts stay in step. +- Several JMAP servers per deployment, optionally auto-picked by email domain +- Custom JMAP endpoints on the login form, when `ALLOW_CUSTOM_JMAP_ENDPOINT` permits it ## Admin & Extensibility -- Web setup wizard for first launch – guides through JMAP server(s), OAuth/OIDC, session secret, logging, branding (with file upload), and admin password; persists to the admin config dir, no `.env.local` editing required -- Stalwart admin dashboard with dedicated policy sections, collapsed into a single tabbed page -- Admin policy gates for the Unified Mailbox – enable or disable the All mail / Unread / Starred entries org-wide, plus a cross-account capability gate (off by default; auto-enabled on upgrade for instances that already used the cross-account views); each gated view still respects the user's own toggle -- Split admin storage: `ADMIN_CONFIG_DIR` (operator-authored, mountable read-only after setup) and `ADMIN_STATE_DIR` (runtime audit log and login timestamps) -- File-based secrets for JSON config: `passwordHashFile` (admin password), `sessionSecretFile`, and `oauthClientSecretFile` for Docker/Kubernetes secret mounts -- Admin toggle for search-engine indexing (`robots.txt` / `noindex`) -- Plugin system – schema-driven config UI, render and intercept hooks, `onAvatarResolve`, `onBeforeEmailSend`, composer-sidebar and email-banner slots, calendar event slots, i18n APIs (localizable sandboxed plugins via manifest locales and `api.i18n.t`), an `/api/translate` proxy, email-body access, and managed policy enforcement -- Plugin hot-reload and dev-folder loading, on-demand `src/` bundling via esbuild, and `http:fetch` permission with `httpOrigins` -- 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`); install/uninstall restricted to the admin dashboard -- Bundled plugins including Jitsi Meet calendar integration +- A setup wizard runs on first launch and walks through JMAP servers, OAuth/OIDC, the session secret, logging, branding (uploads included), and the admin password. It writes to the admin config dir, so `.env.local` stays untouched. +- The Stalwart admin dashboard, its policy sections collapsed into one tabbed page +- Admin policy gates for the Unified Mailbox: turn All mail / Unread / Starred on or off org-wide, and gate cross-account capability separately (off by default, auto-enabled on upgrade for instances already using it). A gated view still respects the user's own toggle. +- Admin storage splits in two. `ADMIN_CONFIG_DIR` is operator-authored and can be mounted read-only once setup finishes; `ADMIN_STATE_DIR` holds the runtime audit log and login timestamps. +- JSON config can read secrets from files (`passwordHashFile`, `sessionSecretFile`, `oauthClientSecretFile`) for Docker and Kubernetes secret mounts +- An admin toggle controls search-engine indexing (`robots.txt` / `noindex`) +- Plugin system: a schema-driven config UI, render and intercept hooks, `onAvatarResolve`, `onBeforeEmailSend`, composer-sidebar and email-banner slots, calendar event slots, i18n APIs (sandboxed plugins localize through manifest locales and `api.i18n.t`), an `/api/translate` proxy, email-body access, and managed policy enforcement +- Plugins hot-reload, load from a dev folder, bundle `src/` on demand through esbuild, and can request `http:fetch` scoped by `httpOrigins` +- Themes upload as ZIP bundles, and admins can enforce one +- An extension marketplace browses and installs plugins and themes from a configurable directory (`EXTENSION_DIRECTORY_URL`). Installing and uninstalling stay in the admin dashboard. +- Bundled plugins, including Jitsi Meet for the calendar ## Operations -- Progressive Web App with service worker, install prompt, web push notifications for inbox mail, dynamic manifest, and configurable (per-domain) install screenshots -- Automatic update check with server-side logging of new releases and a non-dismissible update notice -- Structured logging (`text` or `json`) with category-based levels -- Anonymous instance telemetry (opt-in via admin UI, the installer, or `BULWARK_TELEMETRY=on`; off by default) – version, platform, bucketed account counts, feature toggles only -- Release (`main`) and development (`dev`) Docker images on GHCR -- Subpath deployment via `NEXT_PUBLIC_BASE_PATH` for mounting behind a reverse proxy -- Demo mode with fixture data – no mail server required +- Progressive Web App: service worker, install prompt, web push for new inbox mail, a dynamic manifest, and install screenshots configurable per domain +- Update checks run on their own, log new releases server-side, and raise a notice that can't be dismissed +- Structured logging (`text` or `json`) with per-category levels +- Anonymous instance telemetry, off unless you enable it through the admin UI, the installer, or `BULWARK_TELEMETRY=on`. It reports version, platform, bucketed account counts, and feature toggles. +- Docker images on GHCR, for release (`main`) and development (`dev`) +- `NEXT_PUBLIC_BASE_PATH` mounts the app at a subpath behind a reverse proxy +- Demo mode runs on fixture data, no mail server required diff --git a/README.md b/README.md index de76d879..63a4d73a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ # Bulwark Webmail -A modern, self-hosted webmail client for [Stalwart Mail Server](https://stalw.art/), built with Next.js and the JMAP protocol. +A self-hosted webmail client for [Stalwart Mail Server](https://stalw.art/), built with Next.js and the JMAP protocol. [![License: AGPL v3](https://img.shields.io/badge/license-AGPL%20v3-blue.svg?logo=gnu&logoColor=white)](LICENSE) [![Discord](https://img.shields.io/discord/1482128142939455674?color=7289da&label=discord&logo=discord&logoColor=white)](https://discord.gg/tYCujymGrT) @@ -20,12 +20,7 @@ A modern, self-hosted webmail client for [Stalwart Mail Server](https://stalw.ar ## 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 - +Since **1.6.4**, a web-based setup wizard runs on first launch – no `.env.local` editing, no shelling into the container. Point a browser at the running container and the wizard guides you through: @@ -70,21 +65,21 @@ The wizard writes to `ADMIN_CONFIG_DIR` (`./data/admin` by default). Setting `JM Settings -Light mode – full theme support with intelligent color transformation for HTML emails. +Light mode – full theme support, remapping HTML email colors by luminance so dark-on-dark text stays readable. 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. It bundles the four apps most self-hosters end up wanting: - **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. +They share one login, one settings store, and one admin dashboard. SSO, 2FA, multi-account, 24 languages, PWA install, themes, and plugins apply across all four. Full feature list: **[FEATURES.md](FEATURES.md)**. @@ -104,7 +99,7 @@ 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](#configuration). +On first launch, open `http://localhost:3000` and the setup wizard takes over. Installs that already define `JMAP_SERVER_URL` skip it and keep the env-managed flow under [Configuration](#configuration). ### From Source @@ -119,16 +114,20 @@ npm run build && npm start ### Development ```bash -npm run dev # Dev server with a mock JMAP server +cp .env.dev.example .env.local # Built-in mock JMAP server, no mail server needed + +npm run dev # Dev server npm run typecheck npm run lint +npx vitest run # Unit tests +npm run test:integration # Dockerized Stalwart + Playwright suite (see integration/README.md) ``` ## 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. +Most deployments are configured through the setup wizard on first launch, then the admin dashboard; those values live in the admin config directory rather than `.env.local`. Environment variables still work, and they suit read-only or immutable infrastructure better. An environment variable always wins over the admin-managed value, so setting `JMAP_SERVER_URL` hides that field from the wizard and locks it in the admin UI. -All variables are evaluated at runtime, so Docker deployments can be reconfigured without rebuilding. Edit `.env.local`: +Nearly all variables are evaluated at runtime, so Docker deployments can be reconfigured without rebuilding. The exceptions are the `NEXT_PUBLIC_*` ones noted below, which Next.js bakes in at build time. Edit `.env.local`: ```env # Optional – overrides whatever the wizard writes @@ -151,13 +150,28 @@ PORT=3000 ```env OAUTH_ENABLED=true +OAUTH_ONLY=true # hide the username/password form entirely 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 +OAUTH_AUTHORIZE_URL= # override only the user-facing authorize endpoint +OAUTH_ALLOW_PRIVATE_ENDPOINTS= # allow discovery to resolve to RFC-1918 addresses ``` -Endpoints are auto-discovered via `.well-known/oauth-authorization-server` or `.well-known/openid-configuration`. +Endpoints are auto-discovered via `.well-known/oauth-authorization-server` or `.well-known/openid-configuration`. `OAUTH_ALLOW_PRIVATE_ENDPOINTS` is off by default as an SSRF guard. Enable it only for split-DNS deployments where the issuer's public hostname resolves to an internal IP. + + + +
+Anonymous telemetry + +```env +BULWARK_TELEMETRY=on # opt-in; off by default +TELEMETRY_DATA_DIR=./data/telemetry # instance id and consent; mount a volume +``` + +Off unless you turn it on, in the admin UI, the installer, or here. Heartbeats carry version, platform, bucketed account counts, and feature toggles. No email addresses, hostnames, or IPs. Setting the variable (to either value) locks the choice and disables the admin toggle.
@@ -255,6 +269,25 @@ The split lets you mount the config volume read-only after the setup wizard comp +
+Default UI locale + +The UI language follows each visitor's `Accept-Language` header and their stored preference. `NEXT_PUBLIC_DEFAULT_LOCALE` sets the fallback used when neither matches a supported locale (default `en`): + +```env +NEXT_PUBLIC_DEFAULT_LOCALE=de +``` + +Supported: `ar`, `ca`, `cs`, `da`, `de`, `en`, `es`, `fa`, `fr`, `he`, `hu`, `it`, `ja`, `ko`, `lv`, `nl`, `pl`, `pt`, `ro`, `ru`, `sk`, `tr`, `uk`, `zh`. An unsupported value falls back to `en`. + +Like `NEXT_PUBLIC_BASE_PATH`, this is read at **build time**. To use it with the published Docker image, build your own: + +```bash +docker build --build-arg NEXT_PUBLIC_DEFAULT_LOCALE=de -t bulwark-webmail . +``` + +
+
Subpath / reverse proxy mount @@ -271,37 +304,46 @@ Unlike most other variables, `NEXT_PUBLIC_BASE_PATH` is read at **build time** b 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. +Then point your reverse proxy at the container without stripping the prefix. The app expects requests under `/webmail/...` and serves every route (`/webmail/api/...`, `/webmail/_next/static/...`, `/webmail/sw.js`, and so on) 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 | +| Key | Action | +| -------------------- | ----------------------- | +| `j` `↓` / `k` `↑` | Navigate between emails | +| `Enter` / `o` | Open email | +| `Esc` | Close / deselect | +| `x` | Expand / collapse thread | +| `c` | Compose | +| `r` / `R` `a` | Reply / Reply all | +| `f` | Forward | +| `s` | Star | +| `e` | Archive | +| `#` / `Del` | Delete | +| `u` / `Shift`+`I` | Mark unread / read | +| `!` | Toggle spam | +| `Ctrl`+`A` | Select all | +| `Shift`+`G` | Refresh | +| `/` | Search | +| `?` | Show all shortcuts | + +In the composer: `Ctrl/Cmd`+`Enter` sends, `Ctrl/Cmd`+`Shift`+`Enter` opens scheduled send, and `t` opens the template picker. ## Tech Stack | | | | ------------- | ------------------------------------------------- | -| **Framework** | [Next.js 16](https://nextjs.org/) with App Router | +| **Framework** | [Next.js 16](https://nextjs.org/) with App Router, React 19 | | **Language** | TypeScript | | **Styling** | [Tailwind CSS v4](https://tailwindcss.com/) | | **State** | [Zustand](https://zustand-demo.pmnd.rs/) | | **Protocol** | Custom JMAP client (RFC 8620) | +| **Editor** | [Tiptap](https://tiptap.dev/) | | **i18n** | [next-intl](https://next-intl-docs.vercel.app/) | | **Icons** | [Lucide React](https://lucide.dev/) | +| **Testing** | [Vitest](https://vitest.dev/) + [Playwright](https://playwright.dev/) | ## Why Stalwart? diff --git a/hooks/use-keyboard-shortcuts.ts b/hooks/use-keyboard-shortcuts.ts index 03134a72..a648a511 100644 --- a/hooks/use-keyboard-shortcuts.ts +++ b/hooks/use-keyboard-shortcuts.ts @@ -303,9 +303,8 @@ export const KEYBOARD_SHORTCUTS = { { key: "x", description: "shortcuts.threads.expand_collapse" }, ], composer: [ - { key: "Ctrl + Enter", description: "shortcuts.composer.send" }, - { key: "Ctrl + Shift + Enter", description: "shortcuts.composer.schedule_send" }, - { key: "t", description: "shortcuts.composer.template_picker" }, { key: "Ctrl/Cmd + Enter", description: "shortcuts.composer.send" }, + { key: "Ctrl/Cmd + Shift + Enter", description: "shortcuts.composer.schedule_send" }, + { key: "t", description: "shortcuts.composer.template_picker" }, ], } as const;