From 02808fa4c3129547bd61328eb8984f4dfa3f0f3d Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Fri, 13 Mar 2026 14:12:20 +0100
Subject: [PATCH] docs: update contributing guidelines and enhance README
formatting
---
CONTRIBUTING.md | 11 +-
README.md | 44 ++--
ROADMAP.md | 22 ++
docker-compose.yml | 10 +-
specifications/addon-plugin-theme-concept.md | 257 ++++++++++---------
5 files changed, 193 insertions(+), 151 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1fe024a4..83208b18 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -7,17 +7,20 @@ Thank you for your interest in contributing to Bulwark Webmail! This document pr
### Development Setup
1. **Fork and clone** the repository:
+
```bash
git clone https://github.com/bulwarkmail/webmail.git
cd webmail
```
2. **Install dependencies**:
+
```bash
npm install
```
3. **Set up environment**:
+
```bash
cp .env.example .env.local
# Edit .env.local with your JMAP server URL
@@ -75,9 +78,10 @@ This project uses **next-intl** for internationalization. Please follow these gu
### Key Rules
1. **Never hardcode user-facing text** - Always use translations:
+
```tsx
- const t = useTranslations('namespace');
- return
{t('key')}
;
+ const t = useTranslations("namespace");
+ return {t("key")}
;
```
2. **Translation file locations**:
@@ -109,6 +113,7 @@ This project uses **next-intl** for internationalization. Please follow these gu
### Before Submitting
1. **Create a feature branch**:
+
```bash
git checkout -b feature/your-feature-name
```
@@ -147,6 +152,7 @@ Follow the conventional commits format:
- `chore:` - Maintenance tasks
Examples:
+
```
feat: add email threading support
fix: resolve attachment download issue
@@ -184,6 +190,7 @@ webmail/
## Questions?
If you have questions about contributing, feel free to:
+
- Open an issue for discussion
- Check existing issues and pull requests
diff --git a/README.md b/README.md
index 3618c254..0e546c3e 100644
--- a/README.md
+++ b/README.md
@@ -200,6 +200,7 @@ OAUTH_ISSUER_URL= # optional, for external IdPs (Keycloak, Authe
```
Endpoints are auto-discovered via `.well-known/oauth-authorization-server` or `.well-known/openid-configuration`.
+
@@ -210,35 +211,36 @@ SESSION_SECRET=your-secret-key # Generate with: openssl rand -base64 32
```
Credentials encrypted with AES-256-GCM, stored in an httpOnly cookie (30-day expiry).
+
## 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 |
+| `c` | Compose |
+| `r` / `R` | Reply / Reply all |
+| `f` | Forward |
+| `s` | Star |
+| `e` | Archive |
+| `#` | Delete |
+| `/` | Search |
+| `?` | Show all shortcuts |
## Tech Stack
-| | |
-|---|---|
+| | |
+| ------------- | ------------------------------------------------- |
| **Framework** | [Next.js 16](https://nextjs.org/) with App Router |
-| **Language** | TypeScript |
-| **Styling** | [Tailwind CSS v4](https://tailwindcss.com/) |
-| **State** | [Zustand](https://zustand-demo.pmnd.rs/) |
-| **Protocol** | Custom JMAP client (RFC 8620) |
-| **i18n** | [next-intl](https://next-intl-docs.vercel.app/) |
-| **Icons** | [Lucide React](https://lucide.dev/) |
+| **Language** | TypeScript |
+| **Styling** | [Tailwind CSS v4](https://tailwindcss.com/) |
+| **State** | [Zustand](https://zustand-demo.pmnd.rs/) |
+| **Protocol** | Custom JMAP client (RFC 8620) |
+| **i18n** | [next-intl](https://next-intl-docs.vercel.app/) |
+| **Icons** | [Lucide React](https://lucide.dev/) |
## Why Stalwart?
diff --git a/ROADMAP.md b/ROADMAP.md
index bdd2fbec..675ea31a 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -5,12 +5,14 @@ This document tracks the development status and planned features for Bulwark Web
## Completed Features
### Core Infrastructure
+
- [x] Next.js 16 with TypeScript and App Router
- [x] Tailwind CSS v4 with Oxide engine
- [x] Zustand state management
- [x] Custom JMAP client implementation (RFC 8620)
### Authentication
+
- [x] Login with JMAP server authentication
- [x] Session management (no password storage for security)
- [x] Username autocomplete with history
@@ -23,6 +25,7 @@ This document tracks the development status and planned features for Bulwark Web
- [x] "Remember me" session persistence for Basic Auth (AES-256-GCM encrypted httpOnly cookie)
### JMAP Server Connection
+
- [x] Session establishment and keep-alive
- [x] Connection error handling and retries
- [x] CORS error detection with actionable user guidance
@@ -32,6 +35,7 @@ This document tracks the development status and planned features for Bulwark Web
- [x] Shared folders support (multi-account access)
### Email Operations
+
- [x] Email fetching and display
- [x] Full HTML email rendering
- [x] Compose, reply, reply-all, forward
@@ -48,6 +52,7 @@ This document tracks the development status and planned features for Bulwark Web
- [x] Email threading (Gmail-style inline expansion)
### Real-time Updates
+
- [x] EventSource for JMAP push notifications
- [x] State synchronization
- [x] Email arrival notifications
@@ -55,6 +60,7 @@ This document tracks the development status and planned features for Bulwark Web
- [x] Mailbox change handling
### User Interface
+
- [x] Three-pane layout (sidebar, list, viewer)
- [x] Minimalist design system
- [x] Dark and light theme support
@@ -86,6 +92,7 @@ This document tracks the development status and planned features for Bulwark Web
- [x] Screen reader live region announcements (sr-only)
### Internationalization
+
- [x] English language support
- [x] French language support
- [x] Japanese language support
@@ -98,6 +105,7 @@ This document tracks the development status and planned features for Bulwark Web
- [x] Language preference persistence
### Security & Accessibility
+
- [x] External content blocked by default
- [x] HTML sanitization with DOMPurify
- [x] User control for loading external content
@@ -114,6 +122,7 @@ This document tracks the development status and planned features for Bulwark Web
- [x] Screen reader sr-only live region for dynamic announcements
### Identity Management
+
- [x] Multiple sender identities (name, email, signature)
- [x] Sub-addressing support (user+tag@domain.com)
- [x] Per-identity signatures
@@ -123,6 +132,7 @@ This document tracks the development status and planned features for Bulwark Web
- [x] Primary identity (matching login) selected by default in composer
### Address Book & Contacts
+
- [x] Contact store with JMAP sync and local fallback
- [x] Contact CRUD operations (create, read, update, delete)
- [x] Contacts list view with search/filter
@@ -136,12 +146,14 @@ This document tracks the development status and planned features for Bulwark Web
- [x] i18n support for contacts (all 8 languages)
### Vacation Responder
+
- [x] JMAP VacationResponse singleton management
- [x] Settings tab with date range and message configuration
- [x] Sidebar indicator when vacation auto-reply is active
- [x] i18n support (all 8 languages)
### Calendar Integration
+
- [x] JMAP Calendar types (RFC 8984) and client methods
- [x] Calendar capability detection (urn:ietf:params:jmap:calendars)
- [x] Calendar store with Zustand (persist middleware)
@@ -177,6 +189,7 @@ This document tracks the development status and planned features for Bulwark Web
- [x] Event duplication button in modal (clones event +1 day, opens for editing)
### Email Filters
+
- [x] JMAP Sieve Scripts (RFC 9661) with capability detection
- [x] Visual rule builder (conditions: From/To/Cc/Subject/Header/Size/Body, actions: Move/Copy/Forward/Mark read/Star/Label/Discard/Reject/Keep/Stop)
- [x] Raw Sieve script editor with syntax validation
@@ -189,6 +202,7 @@ This document tracks the development status and planned features for Bulwark Web
- [x] i18n support (all 8 languages)
### Email Templates
+
- [x] Reusable email templates with local storage persistence
- [x] Category organization (General, Business, Personal, Support, Follow-up, custom)
- [x] Dynamic placeholder variables with auto-fill from composer context
@@ -200,10 +214,12 @@ This document tracks the development status and planned features for Bulwark Web
- [x] i18n support (all 8 languages)
### Email Display
+
- [x] Proper email layout without horizontal scroll or clipping
- [x] Blocked image container collapsing (no empty spaces in newsletters)
### Testing
+
- [x] Unit tests for validation utilities (57 tests)
- [x] Unit tests for email sanitization (27 tests)
- [x] Unit tests for color transformation (40 tests)
@@ -226,6 +242,7 @@ This document tracks the development status and planned features for Bulwark Web
- [x] Playwright E2E framework setup
### Deployment
+
- [x] Runtime environment variables (Docker-friendly configuration)
- [x] Health check endpoint
- [x] Docker support (multi-stage build, docker-compose, standalone output)
@@ -238,26 +255,31 @@ This document tracks the development status and planned features for Bulwark Web
## Planned Features
### Advanced Features
+
- [ ] Free/busy queries (Principal/getAvailability)
- [ ] Calendar sharing UI (JMAP Sharing RFC 9670)
- [ ] Email encryption (PGP/GPG)
### Performance Optimizations
+
- [ ] Email content caching
- [ ] Bundle size optimization
- [ ] Service worker for offline support
- [ ] Lazy loading for attachments
### Testing (Remaining)
+
- [ ] E2E tests with real JMAP server
- [ ] Accessibility testing
- [ ] Performance testing
### Deployment
+
- [ ] Production build optimizations
- [ ] Monitoring and logging
### Security Enhancements
+
- [ ] Rate limiting
## Known Issues
diff --git a/docker-compose.yml b/docker-compose.yml
index 5de56760..733c3645 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,7 +6,15 @@ services:
env_file:
- .env.local
healthcheck:
- test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/api/health"]
+ test:
+ [
+ "CMD",
+ "wget",
+ "--no-verbose",
+ "--tries=1",
+ "--spider",
+ "http://127.0.0.1:3000/api/health",
+ ]
interval: 30s
timeout: 5s
retries: 3
diff --git a/specifications/addon-plugin-theme-concept.md b/specifications/addon-plugin-theme-concept.md
index b67e21ee..18099da2 100644
--- a/specifications/addon-plugin-theme-concept.md
+++ b/specifications/addon-plugin-theme-concept.md
@@ -36,13 +36,13 @@ This document describes a system that allows the Bulwark Webmail application to
### Rebuild Requirements
-| Addon Type | Source | Rebuild Needed? |
-|-----------|--------|----------------|
-| Theme | Bundled (`/addons/themes/`) | **Yes** — included at build time |
-| Theme | URL (remote) | **No** — CSS loaded via `` at runtime |
-| Plugin | Bundled (`/addons/plugins/`) | **Yes** — included at build time |
-| Plugin | URL (remote) | **No** — JS loaded at runtime (see §6.3) |
-| Plugin | Local file (dev mode) | **No** — loaded at runtime |
+| Addon Type | Source | Rebuild Needed? |
+| ---------- | ---------------------------- | ------------------------------------------- |
+| Theme | Bundled (`/addons/themes/`) | **Yes** — included at build time |
+| Theme | URL (remote) | **No** — CSS loaded via `` at runtime |
+| Plugin | Bundled (`/addons/plugins/`) | **Yes** — included at build time |
+| Plugin | URL (remote) | **No** — JS loaded at runtime (see §6.3) |
+| Plugin | Local file (dev mode) | **No** — loaded at runtime |
Bundled addons are compiled into the app's static assets during `next build`. Adding, removing, or updating a bundled addon requires a rebuild and redeploy. URL-based and local addons are fully runtime-loaded — users can install, enable, disable, and uninstall them without any rebuild.
@@ -56,14 +56,14 @@ Bundled addons are compiled into the app's static assets during `next build`. Ad
## 2. Terminology
-| Term | Definition |
-|------|-----------|
-| **Addon** | Any installable extension — umbrella term for themes and plugins. |
-| **Theme** | An addon that only customizes visual appearance (colors, fonts, spacing, density). Ships as CSS + a manifest. Contains no executable code. |
-| **Plugin** | An addon that adds or modifies functionality. Ships as a JS/TS module + a manifest. May include a theme. |
-| **Slot** | A named insertion point in the UI where plugins can render components. |
-| **Hook Point** | A named event or state transition where plugins can run logic. |
-| **Manifest** | A `addon.json` file that declares metadata, permissions, and extension points. |
+| Term | Definition |
+| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
+| **Addon** | Any installable extension — umbrella term for themes and plugins. |
+| **Theme** | An addon that only customizes visual appearance (colors, fonts, spacing, density). Ships as CSS + a manifest. Contains no executable code. |
+| **Plugin** | An addon that adds or modifies functionality. Ships as a JS/TS module + a manifest. May include a theme. |
+| **Slot** | A named insertion point in the UI where plugins can render components. |
+| **Hook Point** | A named event or state transition where plugins can run logic. |
+| **Manifest** | A `addon.json` file that declares metadata, permissions, and extension points. |
---
@@ -74,65 +74,59 @@ Every addon has an `addon.json` at its root:
```jsonc
{
// ── Identity ──
- "id": "com.example.my-addon", // Reverse-domain unique ID
+ "id": "com.example.my-addon", // Reverse-domain unique ID
"name": "My Addon",
- "version": "1.0.0", // Semver
+ "version": "1.0.0", // Semver
"description": "A brief description.",
"author": {
"name": "Jane Doe",
- "url": "https://example.com"
+ "url": "https://example.com",
},
"license": "MIT",
"homepage": "https://example.com/my-addon",
// ── Compatibility ──
"engine": {
- "webmail": ">=1.0.0" // Required host app version range
+ "webmail": ">=1.0.0", // Required host app version range
},
// ── Type ──
- "type": "plugin", // "theme" | "plugin"
+ "type": "plugin", // "theme" | "plugin"
// ── Entry Points (plugins only) ──
- "main": "dist/index.js", // Plugin entry module
- "styles": "dist/styles.css", // Optional supplementary CSS
+ "main": "dist/index.js", // Plugin entry module
+ "styles": "dist/styles.css", // Optional supplementary CSS
// ── Theme Definition (themes, or plugins that include a theme) ──
"theme": {
- "variables": "theme.css", // CSS file with variable overrides
- "presets": ["light", "dark"], // Which base modes it provides
- "preview": "preview.png" // Screenshot for settings UI
+ "variables": "theme.css", // CSS file with variable overrides
+ "presets": ["light", "dark"], // Which base modes it provides
+ "preview": "preview.png", // Screenshot for settings UI
},
// ── Permissions (plugins only) ──
"permissions": [
- "emails:read", // Read email data from store
- "emails:write", // Modify email data (move, flag, etc.)
+ "emails:read", // Read email data from store
+ "emails:write", // Modify email data (move, flag, etc.)
"contacts:read",
"calendar:read",
"settings:read",
"settings:write",
- "notifications", // Show toasts / browser notifications
- "compose:toolbar", // Add buttons to composer toolbar
- "sidebar:section", // Add sections to the sidebar
- "viewer:action", // Add actions to email viewer toolbar
- "navigation:tab", // Add a top-level navigation tab
- "context-menu:email", // Extend email context menu
- "keyboard-shortcuts", // Register keyboard shortcuts
- "external-fetch" // Fetch external URLs (declared origins)
+ "notifications", // Show toasts / browser notifications
+ "compose:toolbar", // Add buttons to composer toolbar
+ "sidebar:section", // Add sections to the sidebar
+ "viewer:action", // Add actions to email viewer toolbar
+ "navigation:tab", // Add a top-level navigation tab
+ "context-menu:email", // Extend email context menu
+ "keyboard-shortcuts", // Register keyboard shortcuts
+ "external-fetch", // Fetch external URLs (declared origins)
],
// ── External Origins (if external-fetch permission is declared) ──
- "allowedOrigins": [
- "https://api.example.com"
- ],
+ "allowedOrigins": ["https://api.example.com"],
// ── Slots (declares which UI slots the plugin uses) ──
- "slots": [
- "sidebar.bottom",
- "compose.toolbar",
- "viewer.actions"
- ],
+ "slots": ["sidebar.bottom", "compose.toolbar", "viewer.actions"],
// ── Settings Schema (plugin-specific preferences) ──
"settings": {
@@ -140,17 +134,17 @@ Every addon has an `addon.json` at its root:
"type": "string",
"label": "API Key",
"description": "Your API key for the service.",
- "secret": true
+ "secret": true,
},
"enabled": {
"type": "boolean",
"label": "Enable integration",
- "default": true
- }
+ "default": true,
+ },
},
// ── i18n ──
- "locales": "locales/" // Directory with {locale}.json files
+ "locales": "locales/", // Directory with {locale}.json files
}
```
@@ -183,8 +177,8 @@ The app already uses CSS custom properties (variables) for all colors, defined i
--font-family-base: "Inter", sans-serif;
--font-family-mono: "JetBrains Mono", monospace;
--radius-base: 8px;
- --spacing-density: 1; /* 0.8 = compact, 1 = normal, 1.2 = comfortable */
- --shadow-elevation-1: 0 1px 3px rgba(0,0,0,0.08);
+ --spacing-density: 1; /* 0.8 = compact, 1 = normal, 1.2 = comfortable */
+ --shadow-elevation-1: 0 1px 3px rgba(0, 0, 0, 0.08);
}
:root[data-theme="com.example.nord"].dark {
@@ -209,16 +203,16 @@ User selects theme in Settings → Appearance
### 4.3 Theme Capabilities
-| Capability | Mechanism |
-|-----------|-----------|
-| Colors | Override `--color-*` CSS variables |
-| Typography | Override `--font-family-*` variables |
-| Spacing/density | Override `--spacing-density` multiplier |
-| Border radius | Override `--radius-*` variables |
-| Shadows | Override `--shadow-*` variables |
-| Dark mode variant | Provide `.dark` overrides |
-| Tag/label colors | Override `--tag-color-*` palette |
-| Custom CSS | Additional rules scoped under `[data-theme="..."]` |
+| Capability | Mechanism |
+| ----------------- | -------------------------------------------------- |
+| Colors | Override `--color-*` CSS variables |
+| Typography | Override `--font-family-*` variables |
+| Spacing/density | Override `--spacing-density` multiplier |
+| Border radius | Override `--radius-*` variables |
+| Shadows | Override `--shadow-*` variables |
+| Dark mode variant | Provide `.dark` overrides |
+| Tag/label colors | Override `--tag-color-*` palette |
+| Custom CSS | Additional rules scoped under `[data-theme="..."]` |
### 4.4 Theme Constraints
@@ -271,7 +265,9 @@ export function activate(ctx: PluginContext) {
ctx.contextMenu.register("email", {
label: ctx.i18n.t("translateEmail"),
icon: "Languages",
- action: (emailId) => { /* ... */ },
+ action: (emailId) => {
+ /* ... */
+ },
});
}
@@ -305,10 +301,10 @@ interface PluginContext {
/** Store access (read-only or read-write based on permissions) */
stores: {
- emails: PluginEmailStore; // If emails:read or emails:write
- contacts: PluginContactStore; // If contacts:read
- calendar: PluginCalendarStore; // If calendar:read
- settings: PluginSettingsStore; // If settings:read or settings:write
+ emails: PluginEmailStore; // If emails:read or emails:write
+ contacts: PluginContactStore; // If contacts:read
+ calendar: PluginCalendarStore; // If calendar:read
+ settings: PluginSettingsStore; // If settings:read or settings:write
};
/** Plugin-specific settings (defined in manifest "settings" schema) */
@@ -411,7 +407,8 @@ Next.js `import()` only resolves modules known at build time. To load plugins fr
async function loadRemotePlugin(url: string): Promise {
// 1. Fetch the plugin's JS bundle as text
const response = await fetch(url);
- if (!response.ok) throw new Error(`Failed to fetch plugin: ${response.status}`);
+ if (!response.ok)
+ throw new Error(`Failed to fetch plugin: ${response.status}`);
const code = await response.text();
// 2. Validate size limit (500 KB default)
@@ -437,11 +434,11 @@ async function loadRemotePlugin(url: string): Promise {
// Shared dependencies exposed to plugins — avoids bundling duplicates
const SHARED_DEPS: Record = {
- "react": React,
+ react: React,
"react/jsx-runtime": jsxRuntime,
"lucide-react": lucideIcons,
"date-fns": dateFns,
- "sonner": sonner,
+ sonner: sonner,
};
```
@@ -474,16 +471,16 @@ interface AddonManagerState {
interface InstalledAddon {
manifest: AddonManifest;
enabled: boolean;
- installedAt: string; // ISO timestamp
- source: AddonSource; // Where it was loaded from
+ installedAt: string; // ISO timestamp
+ source: AddonSource; // Where it was loaded from
runtimeState: "inactive" | "active" | "error";
- error?: string; // Last activation error
+ error?: string; // Last activation error
}
type AddonSource =
- | { type: "bundled" } // Shipped with the app
- | { type: "url"; url: string } // Loaded from a URL
- | { type: "local"; path: string }; // Development: local file
+ | { type: "bundled" } // Shipped with the app
+ | { type: "url"; url: string } // Loaded from a URL
+ | { type: "local"; path: string }; // Development: local file
```
---
@@ -530,20 +527,20 @@ function Slot({ name }: { name: string }) {
#### Available Slots
-| Slot Name | Location | Use Case |
-|-----------|----------|----------|
-| `sidebar.top` | Top of sidebar, below compose button | Quick-access widgets |
-| `sidebar.bottom` | Bottom of sidebar, above storage quota | Extra navigation, widgets |
-| `navigation.tabs` | Navigation rail, below contacts icon | New top-level views |
-| `compose.toolbar` | Composer toolbar (formatting bar) | Encrypt, translate, AI assist buttons |
-| `compose.footer` | Below composer body, above send button | Send-time options (delay, schedule) |
-| `viewer.actions` | Email viewer toolbar | Custom actions (translate, summarize) |
-| `viewer.header` | Above email body in viewer | Banners, warnings, metadata |
-| `viewer.footer` | Below email body in viewer | Related content, suggestions |
-| `list.toolbar` | Above email list | Additional filters, bulk actions |
-| `settings.sections` | Settings page, below existing sections | Plugin settings panels |
-| `calendar.toolbar` | Calendar view toolbar | Calendar-specific actions |
-| `contacts.toolbar` | Contacts view toolbar | Contact-specific actions |
+| Slot Name | Location | Use Case |
+| ------------------- | -------------------------------------- | ------------------------------------- |
+| `sidebar.top` | Top of sidebar, below compose button | Quick-access widgets |
+| `sidebar.bottom` | Bottom of sidebar, above storage quota | Extra navigation, widgets |
+| `navigation.tabs` | Navigation rail, below contacts icon | New top-level views |
+| `compose.toolbar` | Composer toolbar (formatting bar) | Encrypt, translate, AI assist buttons |
+| `compose.footer` | Below composer body, above send button | Send-time options (delay, schedule) |
+| `viewer.actions` | Email viewer toolbar | Custom actions (translate, summarize) |
+| `viewer.header` | Above email body in viewer | Banners, warnings, metadata |
+| `viewer.footer` | Below email body in viewer | Related content, suggestions |
+| `list.toolbar` | Above email list | Additional filters, bulk actions |
+| `settings.sections` | Settings page, below existing sections | Plugin settings panels |
+| `calendar.toolbar` | Calendar view toolbar | Calendar-specific actions |
+| `contacts.toolbar` | Contacts view toolbar | Contact-specific actions |
### 7.2 Hook Events
@@ -551,42 +548,42 @@ Plugins can listen to app events and state transitions:
#### Email Hooks
-| Event | Payload | Description |
-|-------|---------|-------------|
-| `email:selected` | `{ emailId, email }` | User selected an email |
-| `email:opened` | `{ emailId, email }` | Email viewer rendered |
-| `email:compose:open` | `{ mode, replyTo? }` | Composer opened |
-| `email:compose:before-send` | `{ draft }` | Before sending — can modify draft |
-| `email:compose:sent` | `{ emailId }` | Email sent successfully |
-| `email:moved` | `{ emailId, from, to }` | Email moved between mailboxes |
-| `email:deleted` | `{ emailId }` | Email deleted |
-| `email:flagged` | `{ emailId, flags }` | Email flags changed |
+| Event | Payload | Description |
+| --------------------------- | ----------------------- | --------------------------------- |
+| `email:selected` | `{ emailId, email }` | User selected an email |
+| `email:opened` | `{ emailId, email }` | Email viewer rendered |
+| `email:compose:open` | `{ mode, replyTo? }` | Composer opened |
+| `email:compose:before-send` | `{ draft }` | Before sending — can modify draft |
+| `email:compose:sent` | `{ emailId }` | Email sent successfully |
+| `email:moved` | `{ emailId, from, to }` | Email moved between mailboxes |
+| `email:deleted` | `{ emailId }` | Email deleted |
+| `email:flagged` | `{ emailId, flags }` | Email flags changed |
#### Calendar Hooks
-| Event | Payload | Description |
-|-------|---------|-------------|
-| `calendar:event:created` | `{ event }` | New event created |
-| `calendar:event:updated` | `{ event, changes }` | Event modified |
-| `calendar:event:deleted` | `{ eventId }` | Event deleted |
-| `calendar:view:changed` | `{ view, date }` | Calendar view switched |
+| Event | Payload | Description |
+| ------------------------ | -------------------- | ---------------------- |
+| `calendar:event:created` | `{ event }` | New event created |
+| `calendar:event:updated` | `{ event, changes }` | Event modified |
+| `calendar:event:deleted` | `{ eventId }` | Event deleted |
+| `calendar:view:changed` | `{ view, date }` | Calendar view switched |
#### Contact Hooks
-| Event | Payload | Description |
-|-------|---------|-------------|
-| `contact:selected` | `{ contactId }` | Contact selected |
-| `contact:created` | `{ contact }` | New contact created |
-| `contact:updated` | `{ contact }` | Contact modified |
+| Event | Payload | Description |
+| ------------------ | --------------- | ------------------- |
+| `contact:selected` | `{ contactId }` | Contact selected |
+| `contact:created` | `{ contact }` | New contact created |
+| `contact:updated` | `{ contact }` | Contact modified |
#### App Hooks
-| Event | Payload | Description |
-|-------|---------|-------------|
-| `app:ready` | `{}` | App fully loaded |
-| `app:theme:changed` | `{ theme }` | Theme switched |
-| `app:locale:changed` | `{ locale }` | Language changed |
-| `app:navigation` | `{ from, to }` | User navigated between views |
+| Event | Payload | Description |
+| -------------------- | -------------- | ---------------------------- |
+| `app:ready` | `{}` | App fully loaded |
+| `app:theme:changed` | `{ theme }` | Theme switched |
+| `app:locale:changed` | `{ locale }` | Language changed |
+| `app:navigation` | `{ from, to }` | User navigated between views |
---
@@ -609,18 +606,19 @@ Permissions are enforced at the `PluginContext` level — if a plugin didn't dec
### 8.2 Sandboxing Strategy
-| Layer | Mechanism |
-|-------|-----------|
-| **Store access** | `PluginContext` exposes only permitted store slices. Write access returns proxied objects — mutations are validated before applying. |
-| **DOM access** | Plugin components render inside an ``. They receive a scoped React tree — no direct `document` manipulation. |
-| **Network** | `ctx.fetch()` is a controlled wrapper. Requests are only allowed to origins listed in `allowedOrigins`. All other `fetch` / `XMLHttpRequest` calls from plugin code are blocked via CSP headers. |
-| **Storage** | Plugins use `ctx.config` (backed by a namespaced key in `localStorage`). No direct `localStorage` / `sessionStorage` access. |
-| **Error isolation** | Each plugin slot is wrapped in an `AddonErrorBoundary`. A crashing plugin is caught and disabled without affecting the rest of the app. |
-| **Resource limits** | Plugin CSS is limited to 100 KB. Plugin JS bundles are limited to 500 KB (configurable). |
+| Layer | Mechanism |
+| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| **Store access** | `PluginContext` exposes only permitted store slices. Write access returns proxied objects — mutations are validated before applying. |
+| **DOM access** | Plugin components render inside an ``. They receive a scoped React tree — no direct `document` manipulation. |
+| **Network** | `ctx.fetch()` is a controlled wrapper. Requests are only allowed to origins listed in `allowedOrigins`. All other `fetch` / `XMLHttpRequest` calls from plugin code are blocked via CSP headers. |
+| **Storage** | Plugins use `ctx.config` (backed by a namespaced key in `localStorage`). No direct `localStorage` / `sessionStorage` access. |
+| **Error isolation** | Each plugin slot is wrapped in an `AddonErrorBoundary`. A crashing plugin is caught and disabled without affecting the rest of the app. |
+| **Resource limits** | Plugin CSS is limited to 100 KB. Plugin JS bundles are limited to 500 KB (configurable). |
### 8.3 Content Security Policy
Theme CSS is sanitized to disallow:
+
- `url()` references to external domains (only data URIs and same-origin).
- `@import` statements.
- `expression()` or `behavior:` (legacy IE attack vectors).
@@ -650,11 +648,11 @@ function AddonErrorBoundary({ addonId, children }) {
### 9.1 Addon Formats
-| Format | Description | Use Case |
-|--------|-------------|----------|
-| **Bundled** | Shipped inside the app's `/addons/` directory | Default themes, first-party plugins |
-| **URL** | Loaded from a remote URL at runtime | Self-hosted or third-party addons |
-| **Local file** | Loaded from a local path (dev mode only) | Plugin development |
+| Format | Description | Use Case |
+| -------------- | --------------------------------------------- | ----------------------------------- |
+| **Bundled** | Shipped inside the app's `/addons/` directory | Default themes, first-party plugins |
+| **URL** | Loaded from a remote URL at runtime | Self-hosted or third-party addons |
+| **Local file** | Loaded from a local path (dev mode only) | Plugin development |
### 9.2 Addon Bundle Structure
@@ -692,12 +690,14 @@ addons/
### 9.4 Installation Flow
**From URL:**
+
1. User pastes addon URL into Settings → Addons → "Install from URL".
2. App fetches `{url}/addon.json`, validates schema and compatibility.
3. Manifest is stored in addon registry (`localStorage`).
4. On next activation, the addon's assets are fetched and cached.
**Bundled:**
+
1. Addons in `/addons/` are auto-discovered at build time.
2. A generated `addon-registry.json` maps addon IDs to their local paths.
3. Bundled addons appear pre-installed (but can be disabled).
@@ -779,6 +779,7 @@ Appearance
```
When a theme is selected, the app:
+
1. Sets `data-theme` attribute on ``.
2. Loads the theme's CSS file.
3. Persists the choice in `ThemeStore`.
@@ -894,6 +895,7 @@ nord-theme/
```
`addon.json`:
+
```json
{
"id": "org.nordtheme.bulwark-webmail",
@@ -925,6 +927,7 @@ email-translator/
```
`addon.json`:
+
```json
{
"id": "com.example.email-translator",