From 8bdadc7ba390424a9f8125a4b7d9024e6b8fee40 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Thu, 16 Apr 2026 19:07:42 +0200
Subject: [PATCH] fix: standardize punctuation
---
.env.dev.example | 6 +-
.env.example | 4 +-
CHANGELOG.md | 12 +-
README.md | 126 +++++++++---------
app/[locale]/auth/callback/page.tsx | 4 +-
app/[locale]/login/page.tsx | 4 +-
app/[locale]/page.tsx | 6 +-
app/admin/logs/page.tsx | 2 +-
app/admin/marketplace/page.tsx | 2 +-
app/admin/page.tsx | 8 +-
app/admin/plugins/[id]/page.tsx | 2 +-
app/api/admin/audit/route.ts | 2 +-
app/api/admin/auth/route.ts | 6 +-
app/api/admin/branding/[filename]/route.ts | 2 +-
app/api/admin/branding/route.ts | 4 +-
app/api/admin/change-password/route.ts | 2 +-
app/api/admin/config/route.ts | 6 +-
app/api/admin/marketplace/route.ts | 4 +-
app/api/admin/plugins/[id]/bundle/route.ts | 2 +-
app/api/admin/plugins/[id]/config/route.ts | 6 +-
app/api/admin/plugins/route.ts | 8 +-
app/api/admin/policy/route.ts | 4 +-
app/api/admin/stalwart-check/route.ts | 2 +-
app/api/admin/themes/[id]/css/route.ts | 2 +-
app/api/admin/themes/route.ts | 8 +-
app/api/auth/session/route.ts | 2 +-
app/api/auth/totp-token-exchange/route.ts | 2 +-
app/api/dev-jmap/[...path]/route.ts | 106 +++++++--------
app/api/plugins/route.ts | 4 +-
app/api/settings/route.ts | 2 +-
components/contacts/contact-detail.tsx | 8 +-
components/contacts/contact-form.tsx | 14 +-
components/contacts/contact-import-dialog.tsx | 2 +-
components/contacts/contact-list-item.tsx | 2 +-
.../email/calendar-invitation-banner.tsx | 2 +-
components/email/email-viewer.tsx | 36 ++---
components/email/recipient-popover.tsx | 2 +-
components/files/file-browser.tsx | 4 +-
components/files/folder-tree-sidebar.tsx | 2 +-
components/layout/sidebar-apps-modal.tsx | 2 +-
components/settings/appearance-settings.tsx | 2 +-
components/settings/email-settings.tsx | 2 +-
components/settings/files-settings.tsx | 2 +-
components/settings/folder-settings.tsx | 4 +-
.../settings/smime-certificate-modal.tsx | 2 +-
components/tour/tour-overlay.tsx | 4 +-
hooks/use-browser-navigation.ts | 4 +-
i18n/routing.ts | 4 +-
instrumentation.node.ts | 2 +-
lib/__tests__/dev-jmap-mock.test.ts | 16 +--
lib/__tests__/jmap-client-resilience.test.ts | 12 +-
lib/__tests__/mailbox-deep-nesting.test.ts | 18 +--
lib/__tests__/oauth-discovery.test.ts | 2 +-
lib/account-state-manager.ts | 2 +-
lib/admin/password.ts | 4 +-
lib/browser-navigation.ts | 2 +-
lib/demo/demo-client.ts | 2 +-
lib/demo/demo-utils.ts | 6 +-
lib/demo/fixtures/emails.ts | 10 +-
lib/jmap/client.ts | 38 +++---
lib/jmap/types.ts | 2 +-
lib/plugin-api.ts | 10 +-
lib/plugin-hooks.ts | 16 +--
lib/plugin-i18n.ts | 4 +-
lib/plugin-loader.ts | 2 +-
lib/plugin-types.ts | 4 +-
lib/plugin-validator.ts | 2 +-
lib/recurrence-expansion.ts | 8 +-
lib/sieve/__tests__/external-rules.test.ts | 8 +-
.../__tests__/fixtures/mixed-origins.sieve | 2 +-
lib/sieve/parser.ts | 8 +-
lib/smime/certificate-utils.ts | 2 +-
lib/smime/crypto-engine.ts | 4 +-
lib/smime/mime-builder.ts | 2 +-
lib/smime/pkcs12-export.ts | 2 +-
lib/smime/pkcs12-import.ts | 2 +-
lib/smime/smime-decrypt.ts | 10 +-
lib/smime/smime-detect.ts | 2 +-
lib/smime/smime-sign.ts | 2 +-
lib/smime/smime-verify.ts | 2 +-
lib/theme-loader.ts | 2 +-
lib/tnef.ts | 8 +-
lib/utils.ts | 2 +-
locales/de/common.json | 12 +-
locales/en/common.json | 16 +--
locales/es/common.json | 12 +-
locales/fr/common.json | 14 +-
locales/it/common.json | 14 +-
locales/ja/common.json | 8 +-
locales/lv/common.json | 12 +-
locales/nl/common.json | 14 +-
locales/pl/common.json | 14 +-
locales/pt/common.json | 14 +-
locales/ru/common.json | 16 +--
locales/uk/common.json | 14 +-
locales/zh/common.json | 4 +-
package.json | 2 +-
proxy.ts | 4 +-
stores/account-store.ts | 2 +-
stores/auth-store.ts | 22 +--
stores/calendar-store.ts | 8 +-
stores/contact-store.ts | 2 +-
stores/email-store.ts | 6 +-
stores/file-store.ts | 4 +-
stores/filter-store.ts | 4 +-
stores/plugin-store.ts | 10 +-
stores/smime-store.ts | 4 +-
107 files changed, 452 insertions(+), 448 deletions(-)
diff --git a/.env.dev.example b/.env.dev.example
index 4ab88223..6d5d44d6 100644
--- a/.env.dev.example
+++ b/.env.dev.example
@@ -1,11 +1,11 @@
-# Bulwark Webmail — Development Configuration
+# Bulwark Webmail - Development Configuration
# Copy this file to .env.local to run with the built-in mock JMAP server.
-# No external mail server required — great for UI development and testing.
+# No external mail server required - great for UI development and testing.
#
# Usage:
# cp .env.dev.example .env.local
# npm run dev
-# Open http://localhost:3000 — log in with any username/password.
+# Open http://localhost:3000 - log in with any username/password.
# =============================================================================
# Mock JMAP Server
diff --git a/.env.example b/.env.example
index d0b8874c..99f857fa 100644
--- a/.env.example
+++ b/.env.example
@@ -1,4 +1,4 @@
-# Bulwark Webmail — Production Configuration
+# Bulwark Webmail - Production Configuration
# Copy this file to .env.local and fill in your values.
# For development with the built-in mock server, see .env.dev.example instead.
@@ -76,7 +76,7 @@ JMAP_SERVER_URL=https://your-jmap-server.com
# Directory for storing encrypted settings files (default: ./data/settings).
# For Docker, the working directory is /app, so the default resolves to
-# /app/data/settings — mount a persistent volume there:
+# /app/data/settings - mount a persistent volume there:
# volumes:
# - bulwark-settings:/app/data/settings
# SETTINGS_DATA_DIR=./data/settings
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc768557..1aebdf4e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,14 +6,14 @@ Thank you for your donations:
- _You? [Become a sponsor!](https://github.com/sponsors/bulwarkmail)_
-
**One-time**
+
- [@mkorthaus-private](https://github.com/mkorthaus-private)
- [@boris22100](https://github.com/boris22100)
**Monthly**
-- [@pr0ton11](https://github.com/pr0ton11)
+- [@pr0ton11](https://github.com/pr0ton11)
### Features
@@ -45,10 +45,12 @@ Thank you for your donations:
Thank you for your donations:
**One-time**
+
- [@boris22100](https://github.com/boris22100)
- [@mkorthaus-private](https://github.com/mkorthaus-private)
**Monthly**
+
- _You? [Become a sponsor!](https://github.com/sponsors/bulwarkmail)_
### Features
@@ -78,9 +80,11 @@ Thank you for your donations:
Thank you for your donations:
**One-time**
+
- [@mkorthaus-private](https://github.com/mkorthaus-private)
**Monthly**
+
- _You? [Become a sponsor!](https://github.com/sponsors/bulwarkmail)_
### Features
@@ -476,7 +480,7 @@ Thank you for your donations:
### Fixes
- **Context menu**: Fix "Move to folder" submenu closing when scrolling the folder list or moving the mouse to the submenu (#19)
-- **Move to folder**: Fix emails not actually moving on the server — JMAP response errors were silently ignored and shared account IDs were not resolved correctly
+- **Move to folder**: Fix emails not actually moving on the server - JMAP response errors were silently ignored and shared account IDs were not resolved correctly
- **Dependencies**: Update tailwindcss, lucide-react, @tanstack/react-virtual, @typescript-eslint/\*, globals, @types/node
## 1.1.1 (2026-02-28)
@@ -486,7 +490,7 @@ Thank you for your donations:
- **Email viewer**: Show/hide details toggle now stays in place when expanded instead of jumping to the bottom of the details section (#18)
- **Email viewer**: Details toggle text is now properly translated (was hardcoded in English)
- **Instrumentation**: Resolve Edge Runtime warnings by splitting Node.js-only code into a separate module
-- **Security**: Patch minimatch ReDoS vulnerability (CVE-2026-27903) — upgrade 9.0.6→9.0.9 and 3.1.3→3.1.5
+- **Security**: Patch minimatch ReDoS vulnerability (CVE-2026-27903) - upgrade 9.0.6→9.0.9 and 3.1.3→3.1.5
## 1.1.0 (2026-02-28)
diff --git a/README.md b/README.md
index 97877c55..0feff814 100644
--- a/README.md
+++ b/README.md
@@ -26,16 +26,16 @@ Built with Next.js and the JMAP protocol.
-
+
-**Mail** — Three-pane layout with sidebar, email list, and viewer
+**Mail** - Three-pane layout with sidebar, email list, and viewer
-**Calendar** — Month, week, day, and agenda views with event management
+**Calendar** - Month, week, day, and agenda views with event management
@@ -44,14 +44,14 @@ Built with Next.js and the JMAP protocol.
-**Contacts** — Contact management with groups and vCard support
+**Contacts** - Contact management with groups and vCard support
-**Files** — Cloud file browser with upload, preview, and folder navigation
+**Files** - Cloud file browser with upload, preview, and folder navigation
@@ -63,16 +63,16 @@ Built with Next.js and the JMAP protocol.
-
+
-**Light mode** — Full theme support with intelligent color transformation
+**Light mode** - Full theme support with intelligent color transformation
-**Settings** — Appearance, identities, filters, templates, and more
+**Settings** - Appearance, identities, filters, templates, and more
@@ -81,7 +81,7 @@ Built with Next.js and the JMAP protocol.
-**Login** — Configurable branding with OAuth2/OIDC and 2FA support
+**Login** - Configurable branding with OAuth2/OIDC and 2FA support
@@ -95,29 +95,29 @@ Built with Next.js and the JMAP protocol.
### Mail
- **Read, compose, reply, reply-all, forward** with rich HTML rendering and a Tiptap-based rich text editor (inline image upload, drag-and-drop embedding)
-- **Threading** — Gmail-style inline expansion with thread navigation; optional conversation threading toggle
-- **Unified mailbox** — view emails across all accounts in a single list
+- **Threading** - Gmail-style inline expansion with thread navigation; optional conversation threading toggle
+- **Unified mailbox** - view emails across all accounts in a single list
- **Draft auto-save** with discard confirmation; full draft editing with identity preservation
-- **Attachments** — upload, download, and inline preview; attachment-keyword warning if a file is forgotten
-- **Search** — full-text with JMAP filter panel, search chips, cross-mailbox queries, wildcard support, and OR conditions
-- **Batch operations** — multi-select with checkboxes, archive, delete, move, tag
-- **Archive modes** — archive directly or organize archived mail by year or month
+- **Attachments** - upload, download, and inline preview; attachment-keyword warning if a file is forgotten
+- **Search** - full-text with JMAP filter panel, search chips, cross-mailbox queries, wildcard support, and OR conditions
+- **Batch operations** - multi-select with checkboxes, archive, delete, move, tag
+- **Archive modes** - archive directly or organize archived mail by year or month
- **Print** emails directly from the viewer
- **Answered/forwarded status icons** in email list and thread views
- **Multi-tag per email** with color labels, reorderable tags, and drag-and-drop tag assignment
- **Star/unstar** with configurable mark-as-read delay
- **Virtual scrolling** for large mailboxes
- **Quick reply** from the viewer
-- **Hover actions** — configurable quick-action buttons on email rows with customizable placement
-- **Sender avatars** — favicon-based with negative caching for performance
+- **Hover actions** - configurable quick-action buttons on email rows with customizable placement
+- **Sender avatars** - favicon-based with negative caching for performance
- **Recipient popover** for quick contact interaction
- **Plain text composer mode** and auto-select reply identity
- **Reply-to addresses** support in the composer
-- **TNEF support** — extract Outlook `winmail.dat` message bodies and attachments automatically
+- **TNEF support** - extract Outlook `winmail.dat` message bodies and attachments automatically
- **message/rfc822 unwrapping** for embedded messages
-- **Folder management** — create, rename, delete folders with icon picker and subfolder support
-- **Tag counts** — unread and total counts displayed in sidebar
-- **Browser history sync** — back/forward navigation mirrors mail view state
+- **Folder management** - create, rename, delete folders with icon picker and subfolder support
+- **Tag counts** - unread and total counts displayed in sidebar
+- **Browser history sync** - back/forward navigation mirrors mail view state
### Calendar
@@ -125,13 +125,13 @@ Built with Next.js and the JMAP protocol.
- **Event hover preview** popover with configurable details
- **Drag-and-drop rescheduling**, click-drag or double-click creation, edge-resize (15-min snap)
- **Recurring events** with edit/delete scope (this / this and following / all) and client-side recurrence expansion
-- **Participant scheduling** — iMIP invitations sent on create and update (RFC 5545/6047 compliant), organizer/attendee UI, RSVP with trust assessment
-- **Inline calendar invitations** in email viewer — auto-detect `.ics`, RSVP, import
+- **Participant scheduling** - iMIP invitations sent on create and update (RFC 5545/6047 compliant), organizer/attendee UI, RSVP with trust assessment
+- **Inline calendar invitations** in email viewer - auto-detect `.ics`, RSVP, import
- **iCalendar import** with preview, bulk create, and UID deduplication
- **iCal / webcal subscriptions** with editing and batch import
-- **Birthday calendar** — auto-generated from contacts
-- **Virtual locations** — video conference URLs as first-class event fields
-- **Task management** — create, edit, and track tasks with due dates, priority, and completion status; external CalDAV client detection (Thunderbird)
+- **Birthday calendar** - auto-generated from contacts
+- **Virtual locations** - video conference URLs as first-class event fields
+- **Task management** - create, edit, and track tasks with due dates, priority, and completion status; external CalDAV client detection (Thunderbird)
- **Shared calendars** with visual grouping in the sidebar
- **CalDAV discovery** with automatic calendar home resolution for multi-account setups
- **Week numbers** in mini-calendar sidebar
@@ -141,66 +141,66 @@ Built with Next.js and the JMAP protocol.
### Contacts
- **Contact management** with JMAP sync (RFC 9553/9610) and local fallback
-- **Multiple address books** — create, rename, drag-and-drop between books, with editor picker in contact form
+- **Multiple address books** - create, rename, drag-and-drop between books, with editor picker in contact form
- **Collapsible sidebar** with address book grouping and bulk operations
- **Contact groups** with group expansion and member management
- **vCard import/export** (RFC 6350) with duplicate detection
- **Trusted senders** stored in a dedicated JMAP address book
- **Autocomplete** in composer (To/Cc/Bcc)
-- **Bulk operations** — multi-select, delete, group add, export
+- **Bulk operations** - multi-select, delete, group add, export
### Filters & Automation
- **Server-side email filters** via JMAP Sieve Scripts (RFC 9661)
-- **Visual rule builder** — conditions (From, To, Subject, Size, Body…) and actions (Move, Forward, Star, Discard…) with an expanded visual view
-- **External rule preservation** — rules authored in other clients are displayed and preserved
+- **Visual rule builder** - conditions (From, To, Subject, Size, Body…) and actions (Move, Forward, Star, Discard…) with an expanded visual view
+- **External rule preservation** - rules authored in other clients are displayed and preserved
- **Raw Sieve editor** with syntax validation
- **Vacation responder** with date range scheduling and sidebar indicator
-- **Email templates** — reusable, categorized, with placeholder auto-fill (`{{recipientName}}`, `{{date}}`, etc.)
+- **Email templates** - reusable, categorized, with placeholder auto-fill (`{{recipientName}}`, `{{date}}`, etc.)
### Files
- **File browser** with JMAP FileNode cloud storage (Stalwart native)
- **Upload and download** files with progress tracking, folder upload, and streamed WebDAV PUT (no in-memory buffering)
-- **Dynamic upload limits** — respects the server-configured maximum upload size
+- **Dynamic upload limits** - respects the server-configured maximum upload size
- **Folder navigation** with breadcrumb path and tree sidebar
- **Grid and list views** with sorting by name, size, or date
-- **Clipboard operations** — cut, copy, paste, duplicate files
+- **Clipboard operations** - cut, copy, paste, duplicate files
- **File preview** for images, text, audio, video, and more
- **Favorites and recent files** for quick access
-- **Bulk operations** — multi-select, delete, move, download
+- **Bulk operations** - multi-select, delete, move, download
### Security & Privacy
-- **External content blocked** by default — trusted senders list for auto-load
+- **External content blocked** by default - trusted senders list for auto-load
- **HTML sanitization** via DOMPurify with XSS prevention
-- **S/MIME** — manage certificates, sign outgoing mail, encrypt to recipients, decrypt messages, and verify signatures; self-signed certificate detection; legacy 3DES / PBE support; per-account key isolation
+- **S/MIME** - manage certificates, sign outgoing mail, encrypt to recipients, decrypt messages, and verify signatures; self-signed certificate detection; legacy 3DES / PBE support; per-account key isolation
- **SPF/DKIM/DMARC** status indicators
- **OAuth2/OIDC with PKCE** for SSO (Keycloak, Authentik, or built-in), with OAuth-only mode, OAuth app passwords, configurable scopes, and non-interactive SSO for embedded/iframe deployments
- **TOTP two-factor authentication**
-- **Account security panel** — manage passwords and 2FA via Stalwart admin API
-- **"Remember me"** — AES-256-GCM encrypted httpOnly cookie (opt-in)
-- **Security headers** — enforced CSP with per-request nonce, X-Frame-Options, Referrer-Policy; SSRF redirect validation; PDF iframe sandbox; IP spoofing prevention
-- **Plugin hardening** — dangerous-pattern detection, admin approval required, secure HTTP proxy API (no auth-header exposure)
+- **Account security panel** - manage passwords and 2FA via Stalwart admin API
+- **"Remember me"** - AES-256-GCM encrypted httpOnly cookie (opt-in)
+- **Security headers** - enforced CSP with per-request nonce, X-Frame-Options, Referrer-Policy; SSRF redirect validation; PDF iframe sandbox; IP spoofing prevention
+- **Plugin hardening** - dangerous-pattern detection, admin approval required, secure HTTP proxy API (no auth-header exposure)
- **Newsletter unsubscribe** (RFC 2369)
### Interface
-- **Three-pane layout** — sidebar, email list, viewer with resizable columns
+- **Three-pane layout** - sidebar, email list, viewer with resizable columns
- **Dark and light themes** with intelligent email color transformation
- **Always-light email rendering** option for problematic HTML messages in dark theme
-- **Responsive** — desktop sidebar + mobile bottom tab bar with tablet support
-- **Keyboard shortcuts** — full navigation without a mouse
+- **Responsive** - desktop sidebar + mobile bottom tab bar with tablet support
+- **Keyboard shortcuts** - full navigation without a mouse
- **Drag-and-drop** email organization between mailboxes and tag assignment
-- **Interactive guided tour** — onboarding walkthrough for new users
+- **Interactive guided tour** - onboarding walkthrough for new users
- **Right-click context menus**, toast notifications with undo, form validation with shake feedback
- **Customizable toolbar** position, custom favicon, sidebar/login logos, and login page branding
-- **Sidebar apps** — pin custom tools to the navigation rail with drag-and-drop reordering, mobile visibility toggles, and inline or new-tab launch modes
-- **Settings sync** — preferences synchronized with the server (encrypted)
+- **Sidebar apps** - pin custom tools to the navigation rail with drag-and-drop reordering, mobile visibility toggles, and inline or new-tab launch modes
+- **Settings sync** - preferences synchronized with the server (encrypted)
- **Storage quota** display
- **Version badge** in settings
- **Focused mode** with proper viewport bounds
-- **Accessibility** — WCAG AA contrast, reduced-motion support, focus trap, screen reader live regions
+- **Accessibility** - WCAG AA contrast, reduced-motion support, focus trap, screen reader live regions
### Internationalization
@@ -212,32 +212,32 @@ Automatic browser detection with persistent preference. Configurable locale URL
- **Multiple sender identities** with per-identity signatures
- **Automatic identity synchronization** and refresh to keep the identity manager aligned with server-side changes
-- **Sub-addressing** — `user+tag@domain.com` with contextual tag suggestions
+- **Sub-addressing** - `user+tag@domain.com` with contextual tag suggestions
- **Identity badges** in viewer and email list
### Multi-Account
- **Up to 5 simultaneous accounts** with instant switching and per-account session persistence
- **Account switcher** with connection status, default account selection, and per-account logout
-- **Per-account settings** — encrypted settings storage with server-side sync
+- **Per-account settings** - encrypted settings storage with server-side sync
- **Shared folders** across accounts
-- **Custom JMAP server endpoints** — optionally let users connect to any JMAP server from the login form (`ALLOW_CUSTOM_JMAP_ENDPOINT`)
+- **Custom JMAP server endpoints** - optionally let users connect to any JMAP server from the login form (`ALLOW_CUSTOM_JMAP_ENDPOINT`)
### Admin & Extensibility
-- **Stalwart admin dashboard** — sidebar access with reorganized dashboard and dedicated policy sections
-- **Plugin system** — schema-driven admin config UI, render and intercept hooks, `onAvatarResolve` and i18n APIs, calendar event action slots, forced enable/disable and managed policy enforcement
-- **Themes** — upload, enforce, and manage admin-controlled themes with ZIP bundles
-- **Extension marketplace** — browse and install plugins/themes from a configurable directory (`EXTENSION_DIRECTORY_URL`)
-- **Bundled plugins** — Jitsi Meet calendar integration
+- **Stalwart admin dashboard** - sidebar access with reorganized dashboard and dedicated policy sections
+- **Plugin system** - schema-driven admin config UI, render and intercept hooks, `onAvatarResolve` and i18n APIs, calendar event action slots, forced enable/disable and managed policy enforcement
+- **Themes** - upload, enforce, and manage admin-controlled themes with ZIP bundles
+- **Extension marketplace** - browse and install plugins/themes from a configurable directory (`EXTENSION_DIRECTORY_URL`)
+- **Bundled plugins** - Jitsi Meet calendar integration
### Operations
-- **Progressive Web App (PWA)** — installable with service worker, install prompt, and dynamic manifest (app name, description, icons, theme and background colors)
-- **Automatic update check** — server logs when a newer release is available
+- **Progressive Web App (PWA)** - installable with service worker, install prompt, and dynamic manifest (app name, description, icons, theme and background colors)
+- **Automatic update check** - server logs when a newer release is available
- **Logging categories** with `text` or `json` formats for log aggregation
-- **Docker images** — release (`main`) and development (`dev`) channels on GHCR
-- **Demo mode** — try the webmail with fixture data for emails, calendars, contacts, files, filters, identities, and mailboxes — no mail server required
+- **Docker images** - release (`main`) and development (`dev`) channels on GHCR
+- **Demo mode** - try the webmail with fixture data for emails, calendars, contacts, files, filters, identities, and mailboxes - no mail server required
---
@@ -255,7 +255,7 @@ Or with Docker Compose:
```bash
cp .env.example .env.local
-# Edit .env.local — set JMAP_SERVER_URL
+# Edit .env.local - set JMAP_SERVER_URL
docker compose up -d
```
@@ -266,7 +266,7 @@ git clone https://github.com/bulwarkmail/webmail.git
cd webmail
npm install
cp .env.example .env.local
-# Edit .env.local — set JMAP_SERVER_URL
+# Edit .env.local - set JMAP_SERVER_URL
npm run build && npm start
```
@@ -290,7 +290,7 @@ JMAP_SERVER_URL=https://mail.example.com
APP_NAME=My Webmail
```
-All variables are **runtime** — Docker deployments can be configured without rebuilding.
+All variables are **runtime** - Docker deployments can be configured without rebuilding.
Server Listen Address
@@ -424,7 +424,7 @@ LOG_LEVEL=info # "error", "warn", "info", "debug"
## Why Stalwart?
-[Stalwart](https://github.com/stalwartlabs/mail-server) is a mail server written in Rust with **native JMAP support** — not IMAP/SMTP with JMAP bolted on. It handles JMAP, IMAP, SMTP, and ManageSieve in a single binary. Self-hosted, no third-party dependencies.
+[Stalwart](https://github.com/stalwartlabs/mail-server) is a mail server written in Rust with **native JMAP support** - not IMAP/SMTP with JMAP bolted on. It handles JMAP, IMAP, SMTP, and ManageSieve in a single binary. Self-hosted, no third-party dependencies.
## Contributing
diff --git a/app/[locale]/auth/callback/page.tsx b/app/[locale]/auth/callback/page.tsx
index e5c48f22..8032d75a 100644
--- a/app/[locale]/auth/callback/page.tsx
+++ b/app/[locale]/auth/callback/page.tsx
@@ -35,7 +35,7 @@ function OAuthCallbackInner() {
const savedState = sessionStorage.getItem("oauth_state");
if (savedState) {
- // Classic flow — sessionStorage has the PKCE state (same-tab OAuth)
+ // Classic flow - sessionStorage has the PKCE state (same-tab OAuth)
if (!state || state !== savedState) {
setError("invalid_state");
return;
@@ -76,7 +76,7 @@ function OAuthCallbackInner() {
setError("token_exchange_failed");
});
} else if (state) {
- // Server-side SSO flow — state was stored in encrypted httpOnly cookie
+ // Server-side SSO flow - state was stored in encrypted httpOnly cookie
const ssoPrefix = getPathPrefix(params.locale as string);
loginWithServerSso(code, state)
.then((success) => {
diff --git a/app/[locale]/login/page.tsx b/app/[locale]/login/page.tsx
index 5b79c442..1739b766 100644
--- a/app/[locale]/login/page.tsx
+++ b/app/[locale]/login/page.tsx
@@ -255,7 +255,7 @@ export default function LoginPage() {
try {
window.top!.location.href = authorize_url;
} catch {
- // Cross-origin restriction — fall back to current frame
+ // Cross-origin restriction - fall back to current frame
window.location.href = authorize_url;
}
} else {
@@ -759,7 +759,7 @@ export default function LoginPage() {
)}
- Dev mode — logging in as dev@localhost
+ Dev mode - logging in as dev@localhost
) : oauthOnly ? (
diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx
index af6e1f57..f389bc99 100644
--- a/app/[locale]/page.tsx
+++ b/app/[locale]/page.tsx
@@ -256,7 +256,7 @@ export default function Home() {
// Restore conversation thread (mobile only). We can clear it directly,
// but reopening requires the thread group; if the user pressed forward
- // to return to a thread, we silently skip — back navigation always works.
+ // to return to a thread, we silently skip - back navigation always works.
if ((state.threadId ?? null) !== ctx.conversationThreadId) {
if (state.threadId === null) {
setConversationThread(null);
@@ -826,7 +826,7 @@ export default function Home() {
if (archiveMode === 'year') {
await moveThreadToMailbox(client, emailToArchive.id, yearMailbox.id);
} else {
- // archiveMode === 'month' — find or create month subfolder under year
+ // archiveMode === 'month' - find or create month subfolder under year
const yearId = yearMailbox.originalId || yearMailbox.id;
let monthMailbox = mailboxes.find(
m => m.name === month && m.parentId === yearId
@@ -1255,7 +1255,7 @@ export default function Home() {
// Handle back navigation from viewer on mobile.
// Delegate to the browser history stack so this button is equivalent to
- // the OS back button / mouse back button — popstate then restores the
+ // the OS back button / mouse back button - popstate then restores the
// previous snapshot via handleNavRestore. The viewer is only reachable
// from a state that pushed history, so back() always lands on an app entry.
const handleMobileBack = () => {
diff --git a/app/admin/logs/page.tsx b/app/admin/logs/page.tsx
index 9a8d6462..4ff02b21 100644
--- a/app/admin/logs/page.tsx
+++ b/app/admin/logs/page.tsx
@@ -139,7 +139,7 @@ export default function AdminLogsPage() {
}
function formatDetail(detail: Record): string {
- if (!detail || Object.keys(detail).length === 0) return '—';
+ if (!detail || Object.keys(detail).length === 0) return '-';
if (detail.reason) return String(detail.reason);
if (detail.key) return `${detail.key}: ${JSON.stringify(detail.old)} → ${JSON.stringify(detail.new)}`;
if (detail.changes && Array.isArray(detail.changes)) {
diff --git a/app/admin/marketplace/page.tsx b/app/admin/marketplace/page.tsx
index 1a547953..5c1cc481 100644
--- a/app/admin/marketplace/page.tsx
+++ b/app/admin/marketplace/page.tsx
@@ -117,7 +117,7 @@ export default function AdminMarketplacePage() {
setMessage({ type: 'error', text: data.error || 'Installation failed' });
}
} catch {
- setMessage({ type: 'error', text: 'Installation failed — network error' });
+ setMessage({ type: 'error', text: 'Installation failed - network error' });
} finally {
setInstalling(null);
}
diff --git a/app/admin/page.tsx b/app/admin/page.tsx
index db5808e0..5fb6d75e 100644
--- a/app/admin/page.tsx
+++ b/app/admin/page.tsx
@@ -99,8 +99,8 @@ export default function AdminDashboardPage() {
setWarnings(w);
}
- const jmapUrl = config?.jmapServerUrl || '—';
- const jmapHostname = jmapUrl !== '—' ? (() => { try { return new URL(jmapUrl).hostname; } catch { return jmapUrl; } })() : '—';
+ const jmapUrl = config?.jmapServerUrl || '-';
+ const jmapHostname = jmapUrl !== '-' ? (() => { try { return new URL(jmapUrl).hostname; } catch { return jmapUrl; } })() : '-';
return (
@@ -127,9 +127,9 @@ export default function AdminDashboardPage() {
{/* Server Info */}
- {config?.appName || '—'}
+ {config?.appName || '-'}
-
+ {jmapHostname}
diff --git a/app/admin/plugins/[id]/page.tsx b/app/admin/plugins/[id]/page.tsx
index 1411b444..181f3fcb 100644
--- a/app/admin/plugins/[id]/page.tsx
+++ b/app/admin/plugins/[id]/page.tsx
@@ -231,7 +231,7 @@ export default function PluginConfigPage() {
onChange={(e) => setFormValues(prev => ({ ...prev, [key]: e.target.value }))}
className="w-full h-9 px-3 rounded-md border border-input bg-background text-sm focus:outline-none focus:ring-2 focus:ring-ring"
>
-
+
{field.options.map(opt => (
))}
diff --git a/app/api/admin/audit/route.ts b/app/api/admin/audit/route.ts
index f48e30c4..7e68903b 100644
--- a/app/api/admin/audit/route.ts
+++ b/app/api/admin/audit/route.ts
@@ -4,7 +4,7 @@ import { readAuditLog } from '@/lib/admin/audit';
import { logger } from '@/lib/logger';
/**
- * GET /api/admin/audit — Get paginated audit log entries (admin-protected)
+ * GET /api/admin/audit - Get paginated audit log entries (admin-protected)
*/
export async function GET(request: NextRequest) {
try {
diff --git a/app/api/admin/auth/route.ts b/app/api/admin/auth/route.ts
index 509fa15d..803edd08 100644
--- a/app/api/admin/auth/route.ts
+++ b/app/api/admin/auth/route.ts
@@ -30,7 +30,7 @@ async function checkStalwartAdmin(request: NextRequest): Promise {
}
/**
- * POST /api/admin/auth — Login
+ * POST /api/admin/auth - Login
*/
export async function POST(request: NextRequest) {
try {
@@ -92,7 +92,7 @@ export async function POST(request: NextRequest) {
}
/**
- * GET /api/admin/auth — Check session status
+ * GET /api/admin/auth - Check session status
* Also checks if the user is a Stalwart admin (admin panel enabled even without password).
*/
export async function GET(request: NextRequest) {
@@ -136,7 +136,7 @@ export async function GET(request: NextRequest) {
}
/**
- * DELETE /api/admin/auth — Logout
+ * DELETE /api/admin/auth - Logout
*/
export async function DELETE(request: NextRequest) {
try {
diff --git a/app/api/admin/branding/[filename]/route.ts b/app/api/admin/branding/[filename]/route.ts
index 62b72b92..af8ab53a 100644
--- a/app/api/admin/branding/[filename]/route.ts
+++ b/app/api/admin/branding/[filename]/route.ts
@@ -14,7 +14,7 @@ const MIME_TYPES: Record = {
};
/**
- * GET /api/admin/branding/[filename] — Serve uploaded branding images
+ * GET /api/admin/branding/[filename] - Serve uploaded branding images
*
* This endpoint is public (no admin auth) so browsers can load images.
* Only files in the branding directory are served; directory traversal is prevented.
diff --git a/app/api/admin/branding/route.ts b/app/api/admin/branding/route.ts
index e97b0ae0..a538e565 100644
--- a/app/api/admin/branding/route.ts
+++ b/app/api/admin/branding/route.ts
@@ -33,7 +33,7 @@ function sanitizeFilename(name: string): string {
}
/**
- * POST /api/admin/branding — Upload a branding image file
+ * POST /api/admin/branding - Upload a branding image file
*
* Expects multipart/form-data with:
* - file: the image file
@@ -105,7 +105,7 @@ export async function POST(request: NextRequest) {
}
/**
- * DELETE /api/admin/branding — Remove an uploaded branding file
+ * DELETE /api/admin/branding - Remove an uploaded branding file
*
* Expects JSON body: { slot: string }
*/
diff --git a/app/api/admin/change-password/route.ts b/app/api/admin/change-password/route.ts
index 2247a6c9..30b8b33e 100644
--- a/app/api/admin/change-password/route.ts
+++ b/app/api/admin/change-password/route.ts
@@ -5,7 +5,7 @@ import { auditLog } from '@/lib/admin/audit';
import { logger } from '@/lib/logger';
/**
- * POST /api/admin/change-password — Change admin password
+ * POST /api/admin/change-password - Change admin password
*/
export async function POST(request: NextRequest) {
try {
diff --git a/app/api/admin/config/route.ts b/app/api/admin/config/route.ts
index 16db5135..cd9a58bb 100644
--- a/app/api/admin/config/route.ts
+++ b/app/api/admin/config/route.ts
@@ -6,7 +6,7 @@ import { CONFIG_ENV_MAP } from '@/lib/admin/types';
import { logger } from '@/lib/logger';
/**
- * GET /api/admin/config — Get full config with sources (admin-protected)
+ * GET /api/admin/config - Get full config with sources (admin-protected)
*/
export async function GET() {
try {
@@ -26,7 +26,7 @@ export async function GET() {
}
/**
- * PATCH /api/admin/config — Update config overrides (admin-protected)
+ * PATCH /api/admin/config - Update config overrides (admin-protected)
*/
export async function PATCH(request: NextRequest) {
try {
@@ -64,7 +64,7 @@ export async function PATCH(request: NextRequest) {
}
/**
- * DELETE /api/admin/config — Remove admin override for a key (revert to env/default)
+ * DELETE /api/admin/config - Remove admin override for a key (revert to env/default)
*/
export async function DELETE(request: NextRequest) {
try {
diff --git a/app/api/admin/marketplace/route.ts b/app/api/admin/marketplace/route.ts
index e57b5868..341e9627 100644
--- a/app/api/admin/marketplace/route.ts
+++ b/app/api/admin/marketplace/route.ts
@@ -17,7 +17,7 @@ import { sanitizeThemeCSS, validateThemeCSSSafety } from '@/lib/theme-loader';
const DIRECTORY_URL = process.env.EXTENSION_DIRECTORY_URL || 'http://localhost:3001';
/**
- * GET /api/admin/marketplace — Search/browse the extension directory
+ * GET /api/admin/marketplace - Search/browse the extension directory
* Proxies to the extension directory API
*/
export async function GET(request: NextRequest) {
@@ -75,7 +75,7 @@ export async function GET(request: NextRequest) {
}
/**
- * POST /api/admin/marketplace — Install an extension from the directory
+ * POST /api/admin/marketplace - Install an extension from the directory
* Body: { slug: string, version: string, type: 'plugin' | 'theme' }
*/
export async function POST(request: NextRequest) {
diff --git a/app/api/admin/plugins/[id]/bundle/route.ts b/app/api/admin/plugins/[id]/bundle/route.ts
index e5d0c4b9..44923d20 100644
--- a/app/api/admin/plugins/[id]/bundle/route.ts
+++ b/app/api/admin/plugins/[id]/bundle/route.ts
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { getPluginBundle, getPlugin } from '@/lib/admin/plugin-registry';
/**
- * GET /api/admin/plugins/[id]/bundle — Serve plugin JS bundle
+ * GET /api/admin/plugins/[id]/bundle - Serve plugin JS bundle
*
* Public endpoint so the client-side plugin loader can fetch bundles.
* Only serves plugins that exist in the registry and are enabled.
diff --git a/app/api/admin/plugins/[id]/config/route.ts b/app/api/admin/plugins/[id]/config/route.ts
index bd6e5ef2..7260956f 100644
--- a/app/api/admin/plugins/[id]/config/route.ts
+++ b/app/api/admin/plugins/[id]/config/route.ts
@@ -4,7 +4,7 @@ import { getPluginConfig, setPluginConfig, deletePluginConfigKey } from '@/lib/a
import { requireAdminAuth } from '@/lib/admin/session';
/**
- * GET /api/admin/plugins/[id]/config — Read all config for a plugin
+ * GET /api/admin/plugins/[id]/config - Read all config for a plugin
*
* Returns the full config object for admin-configured plugin settings.
* This endpoint is accessible from the client-side plugin API.
@@ -35,7 +35,7 @@ export async function GET(
}
/**
- * PUT /api/admin/plugins/[id]/config — Set a config key
+ * PUT /api/admin/plugins/[id]/config - Set a config key
*
* Body: { key: string, value: unknown }
* Requires admin authentication (checked via admin session).
@@ -83,7 +83,7 @@ export async function PUT(
}
/**
- * DELETE /api/admin/plugins/[id]/config — Delete a config key
+ * DELETE /api/admin/plugins/[id]/config - Delete a config key
*
* Body: { key: string }
*/
diff --git a/app/api/admin/plugins/route.ts b/app/api/admin/plugins/route.ts
index b1137346..9f2f46ed 100644
--- a/app/api/admin/plugins/route.ts
+++ b/app/api/admin/plugins/route.ts
@@ -23,7 +23,7 @@ const SUSPICIOUS_JS_PATTERNS = [
];
/**
- * GET /api/admin/plugins — List all admin-managed plugins
+ * GET /api/admin/plugins - List all admin-managed plugins
*/
export async function GET() {
try {
@@ -41,7 +41,7 @@ export async function GET() {
}
/**
- * POST /api/admin/plugins — Upload and install a plugin ZIP
+ * POST /api/admin/plugins - Upload and install a plugin ZIP
*/
export async function POST(request: NextRequest) {
try {
@@ -181,7 +181,7 @@ export async function POST(request: NextRequest) {
}
/**
- * PATCH /api/admin/plugins — Update plugin metadata (enable/disable)
+ * PATCH /api/admin/plugins - Update plugin metadata (enable/disable)
* Body: { id: string, enabled: boolean }
*/
export async function PATCH(request: NextRequest) {
@@ -218,7 +218,7 @@ export async function PATCH(request: NextRequest) {
}
/**
- * DELETE /api/admin/plugins — Remove a plugin
+ * DELETE /api/admin/plugins - Remove a plugin
* Body: { id: string }
*/
export async function DELETE(request: NextRequest) {
diff --git a/app/api/admin/policy/route.ts b/app/api/admin/policy/route.ts
index 4a9513cc..4d2f0529 100644
--- a/app/api/admin/policy/route.ts
+++ b/app/api/admin/policy/route.ts
@@ -6,7 +6,7 @@ import { logger } from '@/lib/logger';
import type { SettingsPolicy } from '@/lib/admin/types';
/**
- * GET /api/admin/policy — Get settings policy (NOT admin-protected — users read this)
+ * GET /api/admin/policy - Get settings policy (NOT admin-protected - users read this)
*/
export async function GET() {
try {
@@ -22,7 +22,7 @@ export async function GET() {
}
/**
- * PUT /api/admin/policy — Update settings policy (admin-protected)
+ * PUT /api/admin/policy - Update settings policy (admin-protected)
*/
export async function PUT(request: NextRequest) {
try {
diff --git a/app/api/admin/stalwart-check/route.ts b/app/api/admin/stalwart-check/route.ts
index fd067e26..4c2c3d6a 100644
--- a/app/api/admin/stalwart-check/route.ts
+++ b/app/api/admin/stalwart-check/route.ts
@@ -5,7 +5,7 @@ import { getStalwartCredentials } from '@/lib/stalwart/credentials';
/**
* GET /api/admin/stalwart-check
* Check if the currently logged-in user is a Stalwart admin.
- * Probes the admin-only principal-list endpoint — if the user can access it, they're an admin.
+ * Probes the admin-only principal-list endpoint - if the user can access it, they're an admin.
*/
export async function GET(request: NextRequest) {
try {
diff --git a/app/api/admin/themes/[id]/css/route.ts b/app/api/admin/themes/[id]/css/route.ts
index cf435a67..347ccfaa 100644
--- a/app/api/admin/themes/[id]/css/route.ts
+++ b/app/api/admin/themes/[id]/css/route.ts
@@ -3,7 +3,7 @@ import { getThemeCSS, getThemeRegistry } from '@/lib/admin/plugin-registry';
import { logger } from '@/lib/logger';
/**
- * GET /api/admin/themes/[id]/css — Serve theme CSS to clients
+ * GET /api/admin/themes/[id]/css - Serve theme CSS to clients
*/
export async function GET(
_request: NextRequest,
diff --git a/app/api/admin/themes/route.ts b/app/api/admin/themes/route.ts
index a0db4731..d585ca89 100644
--- a/app/api/admin/themes/route.ts
+++ b/app/api/admin/themes/route.ts
@@ -14,7 +14,7 @@ import { MAX_THEME_SIZE } from '@/lib/plugin-types';
import { sanitizeThemeCSS, validateThemeCSSSafety } from '@/lib/theme-loader';
/**
- * GET /api/admin/themes — List all admin-managed themes
+ * GET /api/admin/themes - List all admin-managed themes
*/
export async function GET() {
try {
@@ -32,7 +32,7 @@ export async function GET() {
}
/**
- * POST /api/admin/themes — Upload and install a theme ZIP
+ * POST /api/admin/themes - Upload and install a theme ZIP
*/
export async function POST(request: NextRequest) {
try {
@@ -151,7 +151,7 @@ export async function POST(request: NextRequest) {
}
/**
- * PATCH /api/admin/themes — Update theme metadata (enable/disable)
+ * PATCH /api/admin/themes - Update theme metadata (enable/disable)
* Body: { id: string, enabled: boolean }
*/
export async function PATCH(request: NextRequest) {
@@ -188,7 +188,7 @@ export async function PATCH(request: NextRequest) {
}
/**
- * DELETE /api/admin/themes — Remove a theme
+ * DELETE /api/admin/themes - Remove a theme
* Body: { id: string }
*/
export async function DELETE(request: NextRequest) {
diff --git a/app/api/auth/session/route.ts b/app/api/auth/session/route.ts
index 273afaed..06d11230 100644
--- a/app/api/auth/session/route.ts
+++ b/app/api/auth/session/route.ts
@@ -95,7 +95,7 @@ export async function GET(request: NextRequest) {
}
/**
- * PUT — retrieve full credentials (including password) for session restoration.
+ * PUT - retrieve full credentials (including password) for session restoration.
* Protected by multiple Sec-Fetch-* headers to ensure only same-origin
* browser fetch() requests succeed. Non-browser clients cannot forge these.
*/
diff --git a/app/api/auth/totp-token-exchange/route.ts b/app/api/auth/totp-token-exchange/route.ts
index b16e6631..39ef5bac 100644
--- a/app/api/auth/totp-token-exchange/route.ts
+++ b/app/api/auth/totp-token-exchange/route.ts
@@ -66,7 +66,7 @@ async function findTokenEndpoint(serverUrl: string): Promise {
return url;
}
} catch {
- // Network error — endpoint not reachable
+ // Network error - endpoint not reachable
}
}
diff --git a/app/api/dev-jmap/[...path]/route.ts b/app/api/dev-jmap/[...path]/route.ts
index b55f05d9..4c2dc54e 100644
--- a/app/api/dev-jmap/[...path]/route.ts
+++ b/app/api/dev-jmap/[...path]/route.ts
@@ -6,7 +6,7 @@ import { NextRequest, NextResponse } from 'next/server';
* Enabled only when DEV_MOCK_JMAP=true. Provides realistic dummy data
* so the UI can be developed without a real JMAP mail server.
*
- * Accepts any username/password — no real authentication.
+ * Accepts any username/password - no real authentication.
*/
const ACCOUNT_ID = 'dev-account-001';
@@ -114,8 +114,8 @@ const emails: MockEmail[] = [
textBody: [{ partId: 'p1', blobId: 'blob-001', size: 280, type: 'text/plain' }],
htmlBody: [{ partId: 'p2', blobId: 'blob-002', size: 420, type: 'text/html' }],
bodyValues: {
- p1: { value: 'Hallo!\n\nThis is a sample email to help you get started with the Bulwark Webmail development environment.\n\nFeel free to explore the UI — all data here is mock data.\n\nBeste Grüße,\nSophie' },
- p2: { value: '
Hallo!
This is a sample email to help you get started with the Bulwark Webmail development environment.
Feel free to explore the UI — all data here is mock data.
Beste Grüße, Sophie
' },
+ p1: { value: 'Hallo!\n\nThis is a sample email to help you get started with the Bulwark Webmail development environment.\n\nFeel free to explore the UI - all data here is mock data.\n\nBeste Grüße,\nSophie' },
+ p2: { value: '
Hallo!
This is a sample email to help you get started with the Bulwark Webmail development environment.
Feel free to explore the UI - all data here is mock data.
Beste Grüße, Sophie
' },
},
},
{
@@ -123,7 +123,7 @@ const emails: MockEmail[] = [
from: [{ name: 'Pierre Dubois', email: 'pierre@dubois.example' }],
to: [{ name: 'Dev User', email: 'dev@localhost' }],
cc: [{ name: 'Karel de Vries', email: 'karel@devries.example' }],
- subject: 'Project Update — Q1 Review',
+ subject: 'Project Update - Q1 Review',
preview: 'Salut team, I wanted to share the latest project numbers. We are on track to meet our targets for Q1.',
hasAttachment: true,
textBody: [{ partId: 'p1', blobId: 'blob-003', size: 640, type: 'text/plain' }],
@@ -160,7 +160,7 @@ const emails: MockEmail[] = [
textBody: [{ partId: 'p1', blobId: 'blob-007', size: 350, type: 'text/plain' }],
htmlBody: [{ partId: 'p2', blobId: 'blob-008', size: 500, type: 'text/html' }],
bodyValues: {
- p1: { value: 'A new issue has been opened by @contributor.\n\nTitle: Add dark mode toggle\n\nIt would be great to have a dark mode toggle in the settings panel. Currently users have to rely on system preferences.\n\n—\nReply to this email directly or view it on GitHub.' },
+ p1: { value: 'A new issue has been opened by @contributor.\n\nTitle: Add dark mode toggle\n\nIt would be great to have a dark mode toggle in the settings panel. Currently users have to rely on system preferences.\n\n-\nReply to this email directly or view it on GitHub.' },
p2: { value: '
A new issue has been opened by @contributor.
Add dark mode toggle
It would be great to have a dark mode toggle in the settings panel. Currently users have to rely on system preferences.
Reply to this email directly or view it on GitHub.
' },
},
},
@@ -183,13 +183,13 @@ const emails: MockEmail[] = [
id: 'email-013', threadId: 'thread-012', mailboxIds: { 'mb-inbox': true }, keywords: { '$label:purple': true }, size: 18200, receivedAt: daysAgo(0),
from: [{ name: 'Launchpad Weekly', email: 'hello@launchpad.example' }],
to: [{ name: 'Dev User', email: 'dev@localhost' }], cc: [],
- subject: 'Launchpad Weekly #47 — The future of the open web',
+ subject: 'Launchpad Weekly #47 - The future of the open web',
preview: 'This week: WebAssembly Components hit 1.0, a deep dive into privacy-first analytics, and 5 tools we can\'t stop using.',
hasAttachment: false,
textBody: [{ partId: 'p1', blobId: 'blob-020', size: 1200, type: 'text/plain' }],
htmlBody: [{ partId: 'p2', blobId: 'blob-021', size: 16000, type: 'text/html' }],
bodyValues: {
- p1: { value: 'LAUNCHPAD WEEKLY #47\nThe future of the open web\n\nWebAssembly Components hit 1.0\nThe Component Model spec has reached 1.0, unlocking language-agnostic modules that run anywhere.\n\nDeep dive: Privacy-first analytics\nCookie banners are on their way out. We explore the next generation of analytics tools that respect user privacy by design.\n\n5 tools we can\'t stop using\n1. Vite 7 — lightning-fast builds\n2. Biome — unified lint + format\n3. Deno 4 — batteries included runtime\n4. TailwindCSS 4 — zero config styling\n5. Playwright — end-to-end testing\n\nYou received this because you subscribed at launchpad.example.\nUnsubscribe: https://launchpad.example/unsubscribe' },
+ p1: { value: 'LAUNCHPAD WEEKLY #47\nThe future of the open web\n\nWebAssembly Components hit 1.0\nThe Component Model spec has reached 1.0, unlocking language-agnostic modules that run anywhere.\n\nDeep dive: Privacy-first analytics\nCookie banners are on their way out. We explore the next generation of analytics tools that respect user privacy by design.\n\n5 tools we can\'t stop using\n1. Vite 7 - lightning-fast builds\n2. Biome - unified lint + format\n3. Deno 4 - batteries included runtime\n4. TailwindCSS 4 - zero config styling\n5. Playwright - end-to-end testing\n\nYou received this because you subscribed at launchpad.example.\nUnsubscribe: https://launchpad.example/unsubscribe' },
p2: { value: '
◆ LAUNCHPAD WEEKLY ISSUE #47 • MARCH 2026
The future of the open web
WebAssembly Components hit 1.0, privacy-first analytics take center stage, and 5 tools we can’t stop using.
FEATURED
WebAssembly Components hit 1.0
The Component Model specification has officially reached 1.0, unlocking language-agnostic modules that compose and run anywhere — from the browser to the edge. This is a watershed moment for portable computing.
Cookie banners are on their way out. We explore the next generation of analytics platforms that respect user privacy by design — no consent dialogs required. From server-side aggregation to differential privacy, the landscape is shifting fast.
' },
},
},
@@ -199,7 +199,7 @@ const emails: MockEmail[] = [
from: [{ name: 'Lars Johansson', email: 'lars.johansson@fjord-systems.example' }],
to: [{ name: 'Dev User', email: 'dev@localhost' }],
cc: [{ name: 'Sophie Müller', email: 'sophie@eurotech.example' }, { name: 'Élise Moreau', email: 'elise.moreau@fjord-systems.example' }],
- subject: 'Sprint planning — next week priorities',
+ subject: 'Sprint planning - next week priorities',
preview: 'Hej team, here are the priorities for next sprint. Please review before our planning meeting tomorrow.',
hasAttachment: false,
textBody: [{ partId: 'p1', blobId: 'blob-030', size: 450, type: 'text/plain' }],
@@ -213,7 +213,7 @@ const emails: MockEmail[] = [
id: 'email-015', threadId: 'thread-014', mailboxIds: { 'mb-inbox': true }, keywords: { $seen: true }, size: 5800, receivedAt: hoursAgo(5),
from: [{ name: 'Booking.com', email: 'automated@booking.example' }],
to: [{ name: 'Dev User', email: 'dev@localhost' }], cc: [],
- subject: 'Prenotazione Confermata — Lake Como, Mar 28–30',
+ subject: 'Prenotazione Confermata - Lake Como, Mar 28–30',
preview: 'Your reservation has been confirmed. Check-in: March 28, 2026. Check-out: March 30, 2026.',
hasAttachment: true,
textBody: [{ partId: 'p1', blobId: 'blob-032', size: 500, type: 'text/plain' }],
@@ -257,7 +257,7 @@ const emails: MockEmail[] = [
id: 'email-018', threadId: 'thread-017', mailboxIds: { 'mb-inbox': true }, keywords: { $seen: true, $flagged: true, '$label:orange': true }, size: 4700, receivedAt: daysAgo(1),
from: [{ name: 'Hetzner Cloud', email: 'billing@hetzner.example' }],
to: [{ name: 'Dev User', email: 'dev@localhost' }], cc: [],
- subject: 'Your Hetzner invoice is available — February 2026',
+ subject: 'Your Hetzner invoice is available - February 2026',
preview: 'Your Hetzner Cloud invoice for February 2026 is now available. Total: €1.337,42.',
hasAttachment: true,
textBody: [{ partId: 'p1', blobId: 'blob-037', size: 400, type: 'text/plain' }],
@@ -273,7 +273,7 @@ const emails: MockEmail[] = [
id: 'email-019', threadId: 'thread-018', mailboxIds: { 'mb-inbox': true }, keywords: { $seen: true }, size: 3600, receivedAt: daysAgo(2),
from: [{ name: 'Astrid van der Berg', email: 'astrid@berglabs.example' }],
to: [{ name: 'Dev User', email: 'dev@localhost' }, { name: 'Lars Johansson', email: 'lars.johansson@fjord-systems.example' }], cc: [],
- subject: 'Meeting notes — API design review',
+ subject: 'Meeting notes - API design review',
preview: 'Here are the notes from today\'s API design review session. Key decisions: REST for public API, gRPC for internal services.',
hasAttachment: false,
textBody: [{ partId: 'p1', blobId: 'blob-038', size: 600, type: 'text/plain' }],
@@ -287,13 +287,13 @@ const emails: MockEmail[] = [
id: 'email-020', threadId: 'thread-019', mailboxIds: { 'mb-inbox': true }, keywords: { $seen: true }, size: 5200, receivedAt: daysAgo(3),
from: [{ name: 'Jacques Lefèvre', email: 'jacques@lefevre-avocats.example' }],
to: [{ name: 'Dev User', email: 'dev@localhost' }], cc: [],
- subject: 'Re: Partnership agreement — feedback',
+ subject: 'Re: Partnership agreement - feedback',
preview: 'I reviewed the draft agreement. A few points need clarification around intellectual property clauses.',
hasAttachment: true,
textBody: [{ partId: 'p1', blobId: 'blob-040', size: 700, type: 'text/plain' }],
htmlBody: [],
bodyValues: {
- p1: { value: 'Bonjour,\n\nI reviewed the draft partnership agreement. Overall it looks good, but a few points need clarification:\n\n1. Article 4.2 — IP ownership clause is ambiguous. Should specify that pre-existing IP remains with original owner.\n2. Article 7.1 — Non-compete period of 24 months may be too restrictive under EU law. Suggest 12 months.\n3. Article 9.3 — Liability cap should be tied to contract value, not a fixed amount.\n\nI\'ve marked up the document with detailed comments (attached).\n\nLet me know when you\'d like to discuss.\n\nBien cordialement,\nJacques Lefèvre\nLefèvre & Associés' },
+ p1: { value: 'Bonjour,\n\nI reviewed the draft partnership agreement. Overall it looks good, but a few points need clarification:\n\n1. Article 4.2 - IP ownership clause is ambiguous. Should specify that pre-existing IP remains with original owner.\n2. Article 7.1 - Non-compete period of 24 months may be too restrictive under EU law. Suggest 12 months.\n3. Article 9.3 - Liability cap should be tied to contract value, not a fixed amount.\n\nI\'ve marked up the document with detailed comments (attached).\n\nLet me know when you\'d like to discuss.\n\nBien cordialement,\nJacques Lefèvre\nLefèvre & Associés' },
},
attachments: [
{ partId: 'att4', blobId: 'blob-att-004', size: 45000, name: 'Contrat-de-Partenariat-Annoté.docx', type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' },
@@ -304,13 +304,13 @@ const emails: MockEmail[] = [
from: [{ name: 'Katrin Bauer', email: 'katrin.bauer@charite.example' }],
to: [{ name: 'Dev User', email: 'dev@localhost' }],
cc: [{ name: 'Pierre Dubois', email: 'pierre@dubois.example' }, { name: 'Chiara Rossi', email: 'chiara@rossi.example' }],
- subject: 'Team outing — voting on activity',
+ subject: 'Team outing - voting on activity',
preview: 'Hey everyone! Time to vote on next month\'s team outing. Options: Biergarten, Eurovision watch party, or cooking class.',
hasAttachment: false,
textBody: [{ partId: 'p1', blobId: 'blob-041', size: 300, type: 'text/plain' }],
htmlBody: [],
bodyValues: {
- p1: { value: 'Hallo zusammen!\n\nTime to vote on next month\'s team outing. Please reply with your preference:\n\nA) Biergarten evening + Bretzel buffet\nB) Eurovision watch party (with scorecards!)\nC) Cooking class (Italian cuisine — pasta fresca)\n\nVoting closes Friday. Most votes wins!\n\nKatrin' },
+ p1: { value: 'Hallo zusammen!\n\nTime to vote on next month\'s team outing. Please reply with your preference:\n\nA) Biergarten evening + Bretzel buffet\nB) Eurovision watch party (with scorecards!)\nC) Cooking class (Italian cuisine - pasta fresca)\n\nVoting closes Friday. Most votes wins!\n\nKatrin' },
},
},
{
@@ -323,7 +323,7 @@ const emails: MockEmail[] = [
textBody: [{ partId: 'p1', blobId: 'blob-042', size: 280, type: 'text/plain' }],
htmlBody: [],
bodyValues: {
- p1: { value: 'Your pull request has been merged.\n\nRepository: vcard-parser\nPR #89: Add support for FBURL property\nMerged by: @maintainer\nBranch: feature/fburl → main\n\nCommits merged:\n- feat: parse FBURL property from vCard 4.0\n- test: add FBURL round-trip tests\n- docs: update README with FBURL example\n\n—\nReply to this email directly or view it on GitHub.' },
+ p1: { value: 'Your pull request has been merged.\n\nRepository: vcard-parser\nPR #89: Add support for FBURL property\nMerged by: @maintainer\nBranch: feature/fburl → main\n\nCommits merged:\n- feat: parse FBURL property from vCard 4.0\n- test: add FBURL round-trip tests\n- docs: update README with FBURL example\n\n-\nReply to this email directly or view it on GitHub.' },
},
},
{
@@ -343,14 +343,14 @@ const emails: MockEmail[] = [
id: 'email-024', threadId: 'thread-023', mailboxIds: { 'mb-inbox': true }, keywords: { $seen: true }, size: 7200, receivedAt: daysAgo(5),
from: [{ name: 'DEV Community', email: 'digest@dev.to.example' }],
to: [{ name: 'Dev User', email: 'dev@localhost' }], cc: [],
- subject: 'DEV Digest — Top posts this week',
+ subject: 'DEV Digest - Top posts this week',
preview: 'This week\'s top posts: "Why I switched from React to Solid", "Building a CLI tool in Rust", and more.',
hasAttachment: false,
textBody: [{ partId: 'p1', blobId: 'blob-044', size: 800, type: 'text/plain' }],
htmlBody: [{ partId: 'p2', blobId: 'blob-045', size: 1200, type: 'text/html' }],
bodyValues: {
- p1: { value: 'DEV Digest — Top posts this week\n\n1. "Why I switched from React to Solid" by @webdev — 342 reactions\n2. "Building a CLI tool in Rust from scratch" by @rustacean — 289 reactions\n3. "The state of CSS in 2026" by @cssmaster — 256 reactions\n4. "Microservices are dead, long live modular monoliths" by @architect — 234 reactions\n5. "A beginner\'s guide to WebAssembly Components" by @wasmdev — 198 reactions\n\nHappy coding!\nThe DEV Team' },
- p2: { value: '
DEV Digest
Top posts this week:
"Why I switched from React to Solid" — 342 reactions
"Building a CLI tool in Rust from scratch" — 289 reactions
"The state of CSS in 2026" — 256 reactions
"Microservices are dead, long live modular monoliths" — 234 reactions
"A beginner\'s guide to WebAssembly Components" — 198 reactions
Happy coding! The DEV Team
' },
+ p1: { value: 'DEV Digest - Top posts this week\n\n1. "Why I switched from React to Solid" by @webdev - 342 reactions\n2. "Building a CLI tool in Rust from scratch" by @rustacean - 289 reactions\n3. "The state of CSS in 2026" by @cssmaster - 256 reactions\n4. "Microservices are dead, long live modular monoliths" by @architect - 234 reactions\n5. "A beginner\'s guide to WebAssembly Components" by @wasmdev - 198 reactions\n\nHappy coding!\nThe DEV Team' },
+ p2: { value: '
DEV Digest
Top posts this week:
"Why I switched from React to Solid" - 342 reactions
"Building a CLI tool in Rust from scratch" - 289 reactions
"The state of CSS in 2026" - 256 reactions
"Microservices are dead, long live modular monoliths" - 234 reactions
"A beginner\'s guide to WebAssembly Components" - 198 reactions
Happy coding! The DEV Team
' },
},
},
{
@@ -371,7 +371,7 @@ const emails: MockEmail[] = [
from: [{ name: 'Sophie Müller', email: 'sophie@eurotech.example' }],
to: [{ name: 'Lars Johansson', email: 'lars.johansson@fjord-systems.example' }],
cc: [{ name: 'Dev User', email: 'dev@localhost' }, { name: 'Élise Moreau', email: 'elise.moreau@fjord-systems.example' }],
- subject: 'Re: Sprint planning — next week priorities',
+ subject: 'Re: Sprint planning - next week priorities',
preview: 'Looks good! I\'d also suggest we add the email signature editor to the list. I can take that one.',
hasAttachment: false,
textBody: [{ partId: 'p1', blobId: 'blob-047', size: 200, type: 'text/plain' }],
@@ -385,7 +385,7 @@ const emails: MockEmail[] = [
from: [{ name: 'Liam Ó Donaill', email: 'liam.odonaill@finanz.example' }],
to: [{ name: 'Dev User', email: 'dev@localhost' }],
cc: [{ name: 'Nils Andersson', email: 'nils@digitaal.example' }],
- subject: 'Q1 Budget Review — Meeting this Thursday',
+ subject: 'Q1 Budget Review - Meeting this Thursday',
preview: 'Hi, let\'s meet Thursday at 14:00 to review the Q1 engineering budget. Please bring your team\'s actuals.',
hasAttachment: true,
textBody: [{ partId: 'p1', blobId: 'blob-062', size: 350, type: 'text/plain' }],
@@ -423,14 +423,14 @@ const emails: MockEmail[] = [
textBody: [{ partId: 'p1', blobId: 'blob-064', size: 150, type: 'text/plain' }],
htmlBody: [],
bodyValues: {
- p1: { value: 'Hej!\n\nHaven\'t caught up in a while. Free for a fika next week? Tuesday or Wednesday work best for me.\n\nThere\'s a great new café on the Prinsengracht I\'ve been wanting to try — they do a wonderful kanelbulle.\n\nNils' },
+ p1: { value: 'Hej!\n\nHaven\'t caught up in a while. Free for a fika next week? Tuesday or Wednesday work best for me.\n\nThere\'s a great new café on the Prinsengracht I\'ve been wanting to try - they do a wonderful kanelbulle.\n\nNils' },
},
},
{
id: 'email-039', threadId: 'thread-034', mailboxIds: { 'mb-inbox': true }, keywords: {}, size: 3400, receivedAt: hoursAgo(0.25),
from: [{ name: 'CI/CD Pipeline', email: 'ci@github.example' }],
to: [{ name: 'Dev User', email: 'dev@localhost' }], cc: [],
- subject: '❌ Build failed: main — bulwark-webmail #1337',
+ subject: '❌ Build failed: main - bulwark-webmail #1337',
preview: 'Build #1337 on branch main failed. 2 test(s) failed in email-sanitization.test.ts.',
hasAttachment: false,
textBody: [{ partId: 'p1', blobId: 'blob-060', size: 450, type: 'text/plain' }],
@@ -453,14 +453,14 @@ const emails: MockEmail[] = [
textBody: [{ partId: 'p1', blobId: 'blob-011', size: 80, type: 'text/plain' }],
htmlBody: [],
bodyValues: {
- p1: { value: 'Perfetto! Let\'s meet at noon by the Herengracht.\n\n— Dev User' },
+ p1: { value: 'Perfetto! Let\'s meet at noon by the Herengracht.\n\n- Dev User' },
},
},
{
id: 'email-007', threadId: 'thread-006', mailboxIds: { 'mb-sent': true }, keywords: { $seen: true }, size: 2200, receivedAt: daysAgo(3),
from: [{ name: 'Dev User', email: 'dev@localhost' }],
to: [{ name: 'Pierre Dubois', email: 'pierre@dubois.example' }], cc: [],
- subject: 'Re: Project Update — Q1 Review',
+ subject: 'Re: Project Update - Q1 Review',
preview: 'Merci Pierre, the numbers look great. I\'ll prepare the board presentation.',
hasAttachment: false,
textBody: [{ partId: 'p1', blobId: 'blob-012', size: 150, type: 'text/plain' }],
@@ -487,13 +487,13 @@ const emails: MockEmail[] = [
from: [{ name: 'Dev User', email: 'dev@localhost' }],
to: [{ name: 'Lars Johansson', email: 'lars.johansson@fjord-systems.example' }],
cc: [{ name: 'Sophie Müller', email: 'sophie@eurotech.example' }, { name: 'Élise Moreau', email: 'elise.moreau@fjord-systems.example' }],
- subject: 'Re: Sprint planning — next week priorities',
+ subject: 'Re: Sprint planning - next week priorities',
preview: 'Great suggestions Sophie. 10:30 works for me. I\'ll update the calendar invite.',
hasAttachment: false,
textBody: [{ partId: 'p1', blobId: 'blob-048', size: 150, type: 'text/plain' }],
htmlBody: [],
bodyValues: {
- p1: { value: 'Great suggestions Sophie. 10:30 works for me. I\'ll update the calendar invite.\n\nLars — let\'s also add a stretch goal for the email template system if we finish early.\n\n— Dev User' },
+ p1: { value: 'Great suggestions Sophie. 10:30 works for me. I\'ll update the calendar invite.\n\nLars - let\'s also add a stretch goal for the email template system if we finish early.\n\n- Dev User' },
},
},
{
@@ -506,20 +506,20 @@ const emails: MockEmail[] = [
textBody: [{ partId: 'p1', blobId: 'blob-049', size: 280, type: 'text/plain' }],
htmlBody: [],
bodyValues: {
- p1: { value: 'Nice work on the vCard parser! Left a few comments on the PR.\n\nMain concern: memory usage for large imports (1000+ contacts). Consider using a streaming parser instead of loading the entire file.\n\nAlso, the duplicate detection logic looks solid. Approved with minor changes.\n\n— Dev User' },
+ p1: { value: 'Nice work on the vCard parser! Left a few comments on the PR.\n\nMain concern: memory usage for large imports (1000+ contacts). Consider using a streaming parser instead of loading the entire file.\n\nAlso, the duplicate detection logic looks solid. Approved with minor changes.\n\n- Dev User' },
},
},
{
id: 'email-029', threadId: 'thread-018', mailboxIds: { 'mb-sent': true }, keywords: { $seen: true }, size: 1600, receivedAt: daysAgo(2),
from: [{ name: 'Dev User', email: 'dev@localhost' }],
to: [{ name: 'Astrid van der Berg', email: 'astrid@berglabs.example' }], cc: [],
- subject: 'Re: Meeting notes — API design review',
+ subject: 'Re: Meeting notes - API design review',
preview: 'Thanks for the thorough notes Astrid. I\'ll have the OpenAPI spec draft ready by Friday.',
hasAttachment: false,
textBody: [{ partId: 'p1', blobId: 'blob-050', size: 120, type: 'text/plain' }],
htmlBody: [],
bodyValues: {
- p1: { value: 'Bedankt for the thorough notes Astrid. I\'ll have the OpenAPI spec draft ready by Friday.\n\n— Dev User' },
+ p1: { value: 'Bedankt for the thorough notes Astrid. I\'ll have the OpenAPI spec draft ready by Friday.\n\n- Dev User' },
},
},
{
@@ -532,7 +532,7 @@ const emails: MockEmail[] = [
textBody: [{ partId: 'p1', blobId: 'blob-051', size: 900, type: 'text/plain' }],
htmlBody: [],
bodyValues: {
- p1: { value: 'Hej team,\n\nI\'ve been researching JMAP (RFC 8620/8621) as a replacement for our current REST-based mail backend. Here\'s a summary:\n\nWhy JMAP?\n- Eliminates N+1 query problems with batch requests\n- Built-in push notifications via EventSource\n- Efficient delta sync reduces bandwidth by 60-80%\n- Standardized protocol with growing ecosystem\n\nProposed timeline:\n- Phase 1 (Mar): Proof of concept with mock server\n- Phase 2 (Apr): Core email operations\n- Phase 3 (May): Calendar & contacts integration\n- Phase 4 (Jun): Migration from legacy API\n\nFull proposal document attached.\n\n— Dev User' },
+ p1: { value: 'Hej team,\n\nI\'ve been researching JMAP (RFC 8620/8621) as a replacement for our current REST-based mail backend. Here\'s a summary:\n\nWhy JMAP?\n- Eliminates N+1 query problems with batch requests\n- Built-in push notifications via EventSource\n- Efficient delta sync reduces bandwidth by 60-80%\n- Standardized protocol with growing ecosystem\n\nProposed timeline:\n- Phase 1 (Mar): Proof of concept with mock server\n- Phase 2 (Apr): Core email operations\n- Phase 3 (May): Calendar & contacts integration\n- Phase 4 (Jun): Migration from legacy API\n\nFull proposal document attached.\n\n- Dev User' },
},
attachments: [
{ partId: 'att5', blobId: 'blob-att-005', size: 67000, name: 'JMAP-Migration-Proposal.pdf', type: 'application/pdf' },
@@ -572,12 +572,12 @@ const emails: MockEmail[] = [
from: [{ name: 'Dev User', email: 'dev@localhost' }],
to: [{ name: 'CFP Committee', email: 'cfp@fosdem.example' }], cc: [],
subject: 'Talk proposal: Modern email clients with JMAP',
- preview: 'Title: Modern Email Clients with JMAP — From Protocol to Production...',
+ preview: 'Title: Modern Email Clients with JMAP - From Protocol to Production...',
hasAttachment: false,
textBody: [{ partId: 'p1', blobId: 'blob-053', size: 400, type: 'text/plain' }],
htmlBody: [],
bodyValues: {
- p1: { value: 'Title: Modern Email Clients with JMAP — From Protocol to Production\n\nAbstract:\nThis talk explores building a full-featured webmail client using the JMAP protocol. We\'ll cover session negotiation, efficient data sync, real-time push notifications, and lessons learned.\n\nConference: FOSDEM 2027\nFormat: 30-minute talk\nLevel: Intermediate\n\n[TODO: Add speaker bio]\n[TODO: Complete outline]' },
+ p1: { value: 'Title: Modern Email Clients with JMAP - From Protocol to Production\n\nAbstract:\nThis talk explores building a full-featured webmail client using the JMAP protocol. We\'ll cover session negotiation, efficient data sync, real-time push notifications, and lessons learned.\n\nConference: FOSDEM 2027\nFormat: 30-minute talk\nLevel: Intermediate\n\n[TODO: Add speaker bio]\n[TODO: Complete outline]' },
},
},
// =====================================================================
@@ -600,7 +600,7 @@ const emails: MockEmail[] = [
id: 'email-033', threadId: 'thread-028', mailboxIds: { 'mb-junk': true }, keywords: {}, size: 2200, receivedAt: hoursAgo(8),
from: [{ name: 'HTCPCP Service', email: 'noreply@teapot.example' }],
to: [{ name: 'Dev User', email: 'dev@localhost' }], cc: [],
- subject: '418 I\'m a Teapot — Your coffee request was denied',
+ subject: '418 I\'m a Teapot - Your coffee request was denied',
preview: 'Per RFC 2324, this server is a teapot and cannot brew coffee. Please try a coffee pot instead.',
hasAttachment: false,
textBody: [{ partId: 'p1', blobId: 'blob-054', size: 250, type: 'text/plain' }],
@@ -629,13 +629,13 @@ const emails: MockEmail[] = [
id: 'email-011', threadId: 'thread-010', mailboxIds: { 'mb-archive': true }, keywords: { $seen: true }, size: 3800, receivedAt: daysAgo(14),
from: [{ name: 'HR Department', email: 'hr@fjord-systems.example' }],
to: [{ name: 'Dev User', email: 'dev@localhost' }], cc: [],
- subject: 'Updated Holiday Policy — EU Directive Compliance',
+ subject: 'Updated Holiday Policy - EU Directive Compliance',
preview: 'Please review the updated paid leave policy effective next month, now with 30 days minimum.',
hasAttachment: false,
textBody: [{ partId: 'p1', blobId: 'blob-016', size: 600, type: 'text/plain' }],
htmlBody: [],
bodyValues: {
- p1: { value: 'Hej team,\n\nPlease review the updated paid leave policy effective next month. Key changes include:\n\n- Minimum annual leave: 30 days (EU directive compliance)\n- New flexible Friday policy — Freitags um 14:00 Schluss\n- Simplified approval workflow\n- Fika breaks are now officially protected time\n\nFull details in the employee handbook.\n\nBästa hälsningar,\nHR Department' },
+ p1: { value: 'Hej team,\n\nPlease review the updated paid leave policy effective next month. Key changes include:\n\n- Minimum annual leave: 30 days (EU directive compliance)\n- New flexible Friday policy - Freitags um 14:00 Schluss\n- Simplified approval workflow\n- Fika breaks are now officially protected time\n\nFull details in the employee handbook.\n\nBästa hälsningar,\nHR Department' },
},
},
{
@@ -671,7 +671,7 @@ const emails: MockEmail[] = [
id: 'email-036', threadId: 'thread-031', mailboxIds: { 'mb-archive': true }, keywords: { $seen: true, $flagged: true }, size: 3100, receivedAt: daysAgo(45),
from: [{ name: 'ELSTER Online', email: 'noreply@elster.example' }],
to: [{ name: 'Dev User', email: 'dev@localhost' }], cc: [],
- subject: 'Ihre Steuererklärung 2025 — Dokumente bereit',
+ subject: 'Ihre Steuererklärung 2025 - Dokumente bereit',
preview: 'Ihre Lohnsteuerbescheinigung und Steuerbescheid sind zum Download bereit.',
hasAttachment: true,
textBody: [{ partId: 'p1', blobId: 'blob-057', size: 300, type: 'text/plain' }],
@@ -707,7 +707,7 @@ const emails: MockEmail[] = [
id: 'email-038', threadId: 'thread-033', mailboxIds: { 'mb-trash': true }, keywords: { $seen: true }, size: 3500, receivedAt: daysAgo(1),
from: [{ name: 'SaaS Product', email: 'marketing@saas-product.example' }],
to: [{ name: 'Dev User', email: 'dev@localhost' }], cc: [],
- subject: '🎉 50% off annual plans — limited time!',
+ subject: '🎉 50% off annual plans - limited time!',
preview: 'Upgrade to our annual plan and save 50%. Offer expires this Sunday.',
hasAttachment: false,
textBody: [{ partId: 'p1', blobId: 'blob-059', size: 400, type: 'text/plain' }],
@@ -775,7 +775,7 @@ const contacts = [
emails: { e1: { address: 'karel@devries.example' } },
phones: { p1: { number: '+31 20 555 0142' } },
addresses: { a1: { street: [{ value: 'Herengracht 142' }], locality: 'Amsterdam', country: 'Netherlands', postcode: '1015 BN' } },
- notes: { n1: { note: 'Backend developer. Cycles to work rain or shine — true Dutchman.' } },
+ notes: { n1: { note: 'Backend developer. Cycles to work rain or shine - true Dutchman.' } },
},
{ id: 'contact-005', uid: 'urn:uuid:c0000005-0000-0000-0000-000000000005', addressBookIds: { 'ab-1': true }, kind: 'individual',
name: { components: [{ kind: 'given', value: 'Lars' }, { kind: 'surname', value: 'Johansson' }] },
@@ -970,7 +970,7 @@ function participant(name: string, email: string, kind: string = 'attendee') {
}
const calendarEvents = [
- // ===== Work calendar (cal-2) — recurring & meetings =====
+ // ===== Work calendar (cal-2) - recurring & meetings =====
makeEvent('evt-001', 'cal-2', 'Daily Standup', localDateTime(0, 9, 0), 'PT15M', {
recurrence: [{ frequency: 'weekly', byDay: [{ day: 'mo' }, { day: 'tu' }, { day: 'we' }, { day: 'th' }, { day: 'fr' }] }],
virtualLocations: { vl1: { uri: 'https://meet.example/standup', name: 'Google Meet', description: 'Daily sync' } },
@@ -1000,7 +1000,7 @@ const calendarEvents = [
p1: participant('Dev User', 'dev@localhost', 'owner'),
p2: participant('Lars Johansson', 'lars.johansson@fjord-systems.example'),
},
- description: 'Weekly catch-up. Duration: exactly 42 minutes — the answer to everything.',
+ description: 'Weekly catch-up. Duration: exactly 42 minutes - the answer to everything.',
}),
makeEvent('evt-004', 'cal-2', 'Code Review Session', localDateTime(0, 16, 0), 'PT1H', {
location: 'Konferenzsaal B',
@@ -1020,8 +1020,8 @@ const calendarEvents = [
},
description: 'Review microservices → JMAP migration architecture.',
}),
- // Overlapping event — deliberate conflict with Architecture Review
- makeEvent('evt-006', 'cal-2', 'Customer Call — EuroTech', localDateTime(2, 11, 30), 'PT45M', {
+ // Overlapping event - deliberate conflict with Architecture Review
+ makeEvent('evt-006', 'cal-2', 'Customer Call - EuroTech', localDateTime(2, 11, 30), 'PT45M', {
virtualLocations: { vl1: { uri: 'https://meet.example/eurotech', name: 'Teams' } },
participants: {
p1: participant('Dev User', 'dev@localhost', 'owner'),
@@ -1076,7 +1076,7 @@ const calendarEvents = [
color: '#d50000',
}),
- // ===== Team calendar (cal-3) — social & team =====
+ // ===== Team calendar (cal-3) - social & team =====
makeEvent('evt-012', 'cal-3', 'Biergarten Abend 🍺', localDateTime(5, 18, 0), 'PT3H', {
location: 'Biergarten am Prinsengracht, Amsterdam',
description: 'Monthly team social. Bretzel buffet included.\nVegetarian options: Käsespätzle, Kartoffelsalat.\nBring your own Dirndl/Lederhosen (optional but encouraged).',
@@ -1118,7 +1118,7 @@ const calendarEvents = [
p6: participant('Nils Andersson', 'nils@digitaal.example'),
},
}),
- makeEvent('evt-016', 'cal-3', 'Cooking Class — Pasta Fresca', localDateTime(12, 18, 30), 'PT2H30M', {
+ makeEvent('evt-016', 'cal-3', 'Cooking Class - Pasta Fresca', localDateTime(12, 18, 30), 'PT2H30M', {
location: 'La Cucina Cooking School, Jordaan, Amsterdam',
description: 'Team cooking class: fresh pasta from scratch.\nMenu: tagliatelle al ragù, ravioli ricotta e spinaci.\nChef: Chiara Rossi (guest instructor)',
participants: {
@@ -1160,7 +1160,7 @@ const calendarEvents = [
makeEvent('evt-023', 'cal-1', 'Dutch Language Lesson', localDateTime(1, 19, 0), 'PT1H30M', {
location: 'Taleninstituut, Plantage Middenlaan, Amsterdam',
recurrence: [{ frequency: 'weekly', byDay: [{ day: 'tu' }] }],
- description: 'Semester 3 — past tense and separable verbs. Ik heb geprobeerd...',
+ description: 'Semester 3 - past tense and separable verbs. Ik heb geprobeerd...',
}),
makeEvent('evt-024', 'cal-1', 'Call with Mum', localDateTime(0, 18, 30), 'PT30M', {
recurrence: [{ frequency: 'weekly', byDay: [{ day: 'su' }] }],
@@ -1169,18 +1169,18 @@ const calendarEvents = [
// Overlapping personal events
makeEvent('evt-025', 'cal-1', 'Haircut', localDateTime(6, 14, 30), 'PT45M', {
location: 'Kapper de Luxe, Utrechtsestraat, Amsterdam',
- description: 'Overlaps with Vondelpark bike ride — need to reschedule one!',
+ description: 'Overlaps with Vondelpark bike ride - need to reschedule one!',
}),
- // ===== Holiday calendar (cal-4) — all-day events =====
+ // ===== Holiday calendar (cal-4) - all-day events =====
makeEvent('evt-026', 'cal-4', 'Koningsdag 🧡', localDateTime(42, 0, 0), 'P1D', {
showWithoutTime: true,
- description: 'King\'s Day — national holiday in the Netherlands.\nWear orange! Visit a vrijmarkt. Eat tompouce.',
+ description: 'King\'s Day - national holiday in the Netherlands.\nWear orange! Visit a vrijmarkt. Eat tompouce.',
color: '#ff6d00',
}),
makeEvent('evt-027', 'cal-4', 'Tag der Arbeit', localDateTime(48, 0, 0), 'P1D', {
showWithoutTime: true,
- description: 'Labour Day — public holiday in most EU countries.',
+ description: 'Labour Day - public holiday in most EU countries.',
}),
makeEvent('evt-028', 'cal-4', 'Europe Day 🇪🇺', localDateTime(55, 0, 0), 'P1D', {
showWithoutTime: true,
@@ -1189,7 +1189,7 @@ const calendarEvents = [
}),
makeEvent('evt-029', 'cal-4', 'Bevrijdingsdag', localDateTime(52, 0, 0), 'P1D', {
showWithoutTime: true,
- description: 'Liberation Day — Dutch national holiday commemorating the end of WWII occupation.',
+ description: 'Liberation Day - Dutch national holiday commemorating the end of WWII occupation.',
}),
// ===== Birthday calendar (cal-5) =====
@@ -1218,7 +1218,7 @@ const calendarEvents = [
makeEvent('evt-034', 'cal-2', 'JMAP Conf Amsterdam', localDateTime(28, 9, 0), 'P2D', {
location: 'RAI Amsterdam Convention Centre',
showWithoutTime: true,
- description: 'Your talk: "Building Modern Webmail with JMAP" — Day 1, 14:00, Main Hall.\nDon\'t forget slide deck!',
+ description: 'Your talk: "Building Modern Webmail with JMAP" - Day 1, 14:00, Main Hall.\nDon\'t forget slide deck!',
participants: {
p1: participant('Dev User', 'dev@localhost'),
p2: participant('Sophie Müller', 'sophie@eurotech.example'),
@@ -1227,7 +1227,7 @@ const calendarEvents = [
}),
makeEvent('evt-035', 'cal-2', 'FOSDEM Talk Prep', localDateTime(10, 13, 0), 'PT2H', {
virtualLocations: { vl1: { uri: 'https://meet.example/fosdem-prep', name: 'Meet' } },
- description: 'Rehearse FOSDEM 2027 talk proposal.\nTitle: "Modern Email Clients with JMAP — From Protocol to Production"',
+ description: 'Rehearse FOSDEM 2027 talk proposal.\nTitle: "Modern Email Clients with JMAP - From Protocol to Production"',
}),
];
diff --git a/app/api/plugins/route.ts b/app/api/plugins/route.ts
index e1a4d424..f0c96d9b 100644
--- a/app/api/plugins/route.ts
+++ b/app/api/plugins/route.ts
@@ -3,10 +3,10 @@ import { getPluginRegistry, getThemeRegistry } from '@/lib/admin/plugin-registry
import { logger } from '@/lib/logger';
/**
- * GET /api/plugins — Public endpoint for clients to discover server-managed plugins & themes
+ * GET /api/plugins - Public endpoint for clients to discover server-managed plugins & themes
*
* Returns all enabled plugins and themes so the client can sync them to IndexedDB.
- * No admin auth required — this is how regular users receive plugins/themes.
+ * No admin auth required - this is how regular users receive plugins/themes.
*/
export async function GET() {
try {
diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts
index 5636e439..d03a1780 100644
--- a/app/api/settings/route.ts
+++ b/app/api/settings/route.ts
@@ -135,7 +135,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Identity mismatch' }, { status: 403 });
}
- // Enforce admin policy — strip locked settings so users can't override them
+ // Enforce admin policy - strip locked settings so users can't override them
await configManager.ensureLoaded();
const policy = configManager.getPolicy();
const filteredSettings = { ...settings };
diff --git a/components/contacts/contact-detail.tsx b/components/contacts/contact-detail.tsx
index 6ac6b3cb..ffdd8e28 100644
--- a/components/contacts/contact-detail.tsx
+++ b/components/contacts/contact-detail.tsx
@@ -87,7 +87,7 @@ export function ContactDetail({ contact, onEdit, onDelete, isMobile, className }
try {
let derBytes: ArrayBuffer | string | null = null;
if (key.uri.startsWith('data:')) {
- // data URI — extract base64 content
+ // data URI - extract base64 content
const commaIdx = key.uri.indexOf(',');
if (commaIdx === -1) continue;
const b64 = key.uri.substring(commaIdx + 1);
@@ -178,7 +178,7 @@ export function ContactDetail({ contact, onEdit, onDelete, isMobile, className }
{folderTree.length === 0 ? (
@@ -516,7 +516,7 @@ export function FolderSettings() {
)}
- {/* Standard Folder Roles — advanced section */}
+ {/* Standard Folder Roles - advanced section */}
{STANDARD_ROLES.map((role) => {
// Disambiguate duplicate folder names by appending parent path
diff --git a/components/settings/smime-certificate-modal.tsx b/components/settings/smime-certificate-modal.tsx
index 140e95c5..5a08551c 100644
--- a/components/settings/smime-certificate-modal.tsx
+++ b/components/settings/smime-certificate-modal.tsx
@@ -40,7 +40,7 @@ export function SmimeCertificateModal({
{ label: t("cert_email"), value: record.email },
{
label: t("cert_validity"),
- value: `${new Date(record.notBefore).toLocaleDateString()} — ${new Date(record.notAfter).toLocaleDateString()}`,
+ value: `${new Date(record.notBefore).toLocaleDateString()} - ${new Date(record.notAfter).toLocaleDateString()}`,
},
{ label: t("cert_fingerprint"), value: record.fingerprint },
];
diff --git a/components/tour/tour-overlay.tsx b/components/tour/tour-overlay.tsx
index e3d5fff2..27674fd1 100644
--- a/components/tour/tour-overlay.tsx
+++ b/components/tour/tour-overlay.tsx
@@ -123,7 +123,7 @@ export function TourOverlay() {
// Wait for target element to appear, then show
useEffect(() => {
if (!step) return;
- console.log(`[Tour] Step ${currentStep + 1}/${totalSteps}: "${step.id}" — target: ${step.target}, placement: ${step.placement}, interactive: ${!!step.interactive}`);
+ console.log(`[Tour] Step ${currentStep + 1}/${totalSteps}: "${step.id}" - target: ${step.target}, placement: ${step.placement}, interactive: ${!!step.interactive}`);
setVisible(false);
// Keep old targetRect and tooltipPos so the cutout/tooltip animate to the new position
// instead of disappearing and reappearing
@@ -178,7 +178,7 @@ export function TourOverlay() {
clearInterval(interval);
if (attempts >= maxAttempts && !cancelled) {
// Skip this step if element never appears
- console.warn(`[Tour] Step ${currentStep + 1} "${step.id}": SKIPPED — element never appeared after ${maxAttempts} attempts`);
+ console.warn(`[Tour] Step ${currentStep + 1} "${step.id}": SKIPPED - element never appeared after ${maxAttempts} attempts`);
nextStepRef.current();
}
}
diff --git a/hooks/use-browser-navigation.ts b/hooks/use-browser-navigation.ts
index f4676f6f..56358fe9 100644
--- a/hooks/use-browser-navigation.ts
+++ b/hooks/use-browser-navigation.ts
@@ -85,8 +85,8 @@ export function useBrowserNavigation({
const state = raw ? (raw[STATE_KEY] as StoredNavState | undefined) : undefined;
if (!state) return;
- // Hold the "applying pop" flag for the entire restore — including any
- // async work like fetching email content — so the resulting state
+ // Hold the "applying pop" flag for the entire restore - including any
+ // async work like fetching email content - so the resulting state
// updates don't trigger a fresh history push that would undo the
// user's back / forward navigation.
popDepthRef.current += 1;
diff --git a/i18n/routing.ts b/i18n/routing.ts
index c3e8d439..bfca928a 100644
--- a/i18n/routing.ts
+++ b/i18n/routing.ts
@@ -1,8 +1,8 @@
import { defineRouting } from 'next-intl/routing';
// Locale prefix mode can be configured via NEXT_PUBLIC_LOCALE_PREFIX.
-// - "never" (default): /settings — locale from cookie/Accept-Language
-// - "always": /en/settings — locale always in the URL
+// - "never" (default): /settings - locale from cookie/Accept-Language
+// - "always": /en/settings - locale always in the URL
// - "as-needed": /settings for default locale, /fr/settings otherwise
// When proxying Bulwark under a sub-path (NEXT_PUBLIC_BASE_PATH), "always" is
// recommended to avoid next-intl rewrite loops caused by locale detection
diff --git a/instrumentation.node.ts b/instrumentation.node.ts
index 677e257e..1f9d5aa0 100644
--- a/instrumentation.node.ts
+++ b/instrumentation.node.ts
@@ -38,7 +38,7 @@ if (process.env.NODE_ENV === "production") {
if (!SEMVER_RE.test(remote)) return;
if (compareVersions(current, remote) > 0) {
console.info(
- `Update available: v${remote} — https://github.com/bulwarkmail/webmail`
+ `Update available: v${remote} - https://github.com/bulwarkmail/webmail`
);
}
})
diff --git a/lib/__tests__/dev-jmap-mock.test.ts b/lib/__tests__/dev-jmap-mock.test.ts
index 426e968d..75ba078b 100644
--- a/lib/__tests__/dev-jmap-mock.test.ts
+++ b/lib/__tests__/dev-jmap-mock.test.ts
@@ -69,7 +69,7 @@ describe('dev-jmap mock server', () => {
});
});
- describe('POST /api — Mailbox/get', () => {
+ describe('POST /api - Mailbox/get', () => {
it('should return list of mailboxes', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
@@ -88,7 +88,7 @@ describe('dev-jmap mock server', () => {
});
});
- describe('POST /api — Email/query', () => {
+ describe('POST /api - Email/query', () => {
it('should filter by mailbox', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
@@ -117,7 +117,7 @@ describe('dev-jmap mock server', () => {
});
});
- describe('POST /api — Email/get', () => {
+ describe('POST /api - Email/get', () => {
it('should return emails by ids', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
@@ -150,7 +150,7 @@ describe('dev-jmap mock server', () => {
});
});
- describe('POST /api — Email/set', () => {
+ describe('POST /api - Email/set', () => {
it('should update email keywords', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
@@ -166,7 +166,7 @@ describe('dev-jmap mock server', () => {
});
});
- describe('POST /api — Identity/get', () => {
+ describe('POST /api - Identity/get', () => {
it('should return identities', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
@@ -183,7 +183,7 @@ describe('dev-jmap mock server', () => {
});
});
- describe('POST /api — unknown method', () => {
+ describe('POST /api - unknown method', () => {
it('should return error for unknown methods', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
@@ -199,7 +199,7 @@ describe('dev-jmap mock server', () => {
});
});
- describe('POST /api — back-references', () => {
+ describe('POST /api - back-references', () => {
it('should resolve #ids from Email/query result', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
@@ -220,7 +220,7 @@ describe('dev-jmap mock server', () => {
});
});
- describe('POST /api — invalid request', () => {
+ describe('POST /api - invalid request', () => {
it('should return 400 for missing methodCalls', async () => {
const req = makeRequest('http://localhost:3000/api/dev-jmap/api', {
method: 'POST',
diff --git a/lib/__tests__/jmap-client-resilience.test.ts b/lib/__tests__/jmap-client-resilience.test.ts
index 8bf2427b..9d8c9388 100644
--- a/lib/__tests__/jmap-client-resilience.test.ts
+++ b/lib/__tests__/jmap-client-resilience.test.ts
@@ -68,7 +68,7 @@ describe('JMAPClient resilience', () => {
return client;
}
- describe('authenticatedFetch — network error retry', () => {
+ describe('authenticatedFetch - network error retry', () => {
it('retries once on transient network error', async () => {
const client = await createConnectedClient();
const echoResponse = { methodResponses: [['Core/echo', { ping: 'pong' }, '0']] };
@@ -95,7 +95,7 @@ describe('JMAPClient resilience', () => {
});
});
- describe('authenticatedFetch — basic auth 401 session refresh', () => {
+ describe('authenticatedFetch - basic auth 401 session refresh', () => {
it('refreshes session and retries on 401 for API requests', async () => {
const client = await createConnectedClient();
const refreshedSession = makeSession({ apiUrl: 'https://mail.example.com/jmap/api-v2' });
@@ -138,12 +138,12 @@ describe('JMAPClient resilience', () => {
// connect() should throw without trying to refresh session (would cause infinite recursion)
await expect(client.connect()).rejects.toThrow('Invalid username or password');
- // Only one fetch call — no refresh attempt
+ // Only one fetch call - no refresh attempt
expect(fetchSpy).toHaveBeenCalledTimes(1);
});
});
- describe('authenticatedFetch — bearer token refresh', () => {
+ describe('authenticatedFetch - bearer token refresh', () => {
it('refreshes token and retries on 401 for bearer mode', async () => {
const tokenRefresh = vi.fn().mockResolvedValue('new-token-456');
const session = makeSession();
@@ -172,7 +172,7 @@ describe('JMAPClient resilience', () => {
});
});
- describe('authenticatedFetch — 429 rate limiting', () => {
+ describe('authenticatedFetch - 429 rate limiting', () => {
it('stops sending authenticated requests until the retry window expires', async () => {
const client = await createConnectedClient();
@@ -258,7 +258,7 @@ describe('JMAPClient resilience', () => {
// So ping throws, keep-alive catches it, fires false
// Then reconnect → connect() → authenticatedFetch(sessionUrl) succeeds
fetchSpy
- // ping fails — network error, retry also fails
+ // ping fails - network error, retry also fails
.mockRejectedValueOnce(new TypeError('Failed to fetch'))
.mockRejectedValueOnce(new TypeError('Failed to fetch'))
// reconnect → connect() → session URL succeeds
diff --git a/lib/__tests__/mailbox-deep-nesting.test.ts b/lib/__tests__/mailbox-deep-nesting.test.ts
index 3a7eeeb0..6daa9936 100644
--- a/lib/__tests__/mailbox-deep-nesting.test.ts
+++ b/lib/__tests__/mailbox-deep-nesting.test.ts
@@ -267,13 +267,13 @@ describe('GitHub #118: duplicate subfolder names cause depth-4 orphaning', () =>
it('should keep nested folders when a subfolder has the same name as a role mailbox', () => {
// Reporter's exact scenario: two subfolders with the same name.
// The dedup uses substring matching and removes non-role folders whose name
- // matches a role folder — even if they're deep in the tree with children.
+ // matches a role folder - even if they're deep in the tree with children.
const mailboxes = [
makeMailbox({ id: 'inbox', name: 'Inbox', role: 'inbox' }),
makeMailbox({ id: 'sent-role', name: 'Sent', role: 'sent' }),
// User-created subfolder also named "Sent" nested under Inbox
makeMailbox({ id: 'sent-custom', name: 'Sent', parentId: 'inbox' }),
- // Child of the custom "Sent" folder — becomes orphaned if parent is deduped
+ // Child of the custom "Sent" folder - becomes orphaned if parent is deduped
makeMailbox({ id: 'sent-child', name: 'Archive', parentId: 'sent-custom' }),
];
@@ -281,7 +281,7 @@ describe('GitHub #118: duplicate subfolder names cause depth-4 orphaning', () =>
const flat = flattenMailboxTree(tree);
const rootIds = tree.map(n => n.id);
- // sent-custom MUST be kept because it has children — removing it orphans sent-child
+ // sent-custom MUST be kept because it has children - removing it orphans sent-child
const sentCustom = flat.find(n => n.id === 'sent-custom');
expect(sentCustom).toBeDefined();
expect(sentCustom!.depth).toBe(1); // nested under Inbox
@@ -293,7 +293,7 @@ describe('GitHub #118: duplicate subfolder names cause depth-4 orphaning', () =>
});
it('should keep nested folders when name is substring of a role name', () => {
- // "Draft" is a substring of "Drafts" — dedup removes it, orphaning children
+ // "Draft" is a substring of "Drafts" - dedup removes it, orphaning children
const mailboxes = [
makeMailbox({ id: 'inbox', name: 'Inbox', role: 'inbox' }),
makeMailbox({ id: 'drafts-role', name: 'Drafts', role: 'drafts' }),
@@ -343,22 +343,22 @@ describe('GitHub #118: duplicate subfolder names cause depth-4 orphaning', () =>
it('should only dedup root-level non-role mailboxes that duplicate role mailboxes', () => {
// Dedup should only remove mailboxes that are BOTH:
- // 1. At root level (no parentId) — same structural position as role mailbox
+ // 1. At root level (no parentId) - same structural position as role mailbox
// 2. Name-matching a role mailbox
// Nested mailboxes with matching names should always be kept.
const mailboxes = [
makeMailbox({ id: 'inbox', name: 'Inbox', role: 'inbox' }),
makeMailbox({ id: 'sent-role', name: 'Sent', role: 'sent' }),
- makeMailbox({ id: 'sent-dup', name: 'Sent Mail' }), // root-level duplicate — OK to remove
+ makeMailbox({ id: 'sent-dup', name: 'Sent Mail' }), // root-level duplicate - OK to remove
makeMailbox({ id: 'proj', name: 'Projects', parentId: 'inbox' }),
- makeMailbox({ id: 'sent-nested', name: 'Sent', parentId: 'proj' }), // nested — must keep
+ makeMailbox({ id: 'sent-nested', name: 'Sent', parentId: 'proj' }), // nested - must keep
makeMailbox({ id: 'report', name: 'Report', parentId: 'sent-nested' }),
];
const tree = buildMailboxTree(mailboxes);
const flat = flattenMailboxTree(tree);
- // "Sent Mail" at root (no parentId) can be deduped — that's fine
+ // "Sent Mail" at root (no parentId) can be deduped - that's fine
// But "Sent" nested under Projects must be kept
const sentNested = flat.find(n => n.id === 'sent-nested');
expect(sentNested).toBeDefined();
@@ -377,7 +377,7 @@ describe('mailbox orphan behavior (missing parent)', () => {
const mailboxes = [
makeMailbox({ id: 'inbox', name: 'INBOX', role: 'inbox' }),
makeMailbox({ id: 'privat', name: 'PRIVAT', parentId: 'inbox' }),
- // 'bookings' is MISSING — simulating truncated JMAP response
+ // 'bookings' is MISSING - simulating truncated JMAP response
makeMailbox({ id: 'hotel2', name: 'HOTEL2', parentId: 'bookings' }),
makeMailbox({ id: 'restaurant', name: 'RESTAURANT', parentId: 'hotel2' }),
];
diff --git a/lib/__tests__/oauth-discovery.test.ts b/lib/__tests__/oauth-discovery.test.ts
index 0d2f6d61..a330377d 100644
--- a/lib/__tests__/oauth-discovery.test.ts
+++ b/lib/__tests__/oauth-discovery.test.ts
@@ -92,7 +92,7 @@ describe('oauth/discovery', () => {
expect(consoleSpy).toHaveBeenCalled();
});
- it('caches results — second call for same server URL does not re-fetch', async () => {
+ it('caches results - second call for same server URL does not re-fetch', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(VALID_METADATA),
diff --git a/lib/account-state-manager.ts b/lib/account-state-manager.ts
index 86d89b26..7fba8a3d 100644
--- a/lib/account-state-manager.ts
+++ b/lib/account-state-manager.ts
@@ -13,7 +13,7 @@ import { useIdentityStore } from '@/stores/identity-store';
import { useVacationStore } from '@/stores/vacation-store';
import { useSmimeStore } from '@/stores/smime-store';
-// Minimal snapshot shapes — we only capture what we need
+// Minimal snapshot shapes - we only capture what we need
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type StoreSnapshot = Record;
diff --git a/lib/admin/password.ts b/lib/admin/password.ts
index 64fe191d..d1043df4 100644
--- a/lib/admin/password.ts
+++ b/lib/admin/password.ts
@@ -115,7 +115,7 @@ export async function initAdminPassword(): Promise {
}
if (isHashed(envPassword)) {
- // Already hashed in env — save to file
+ // Already hashed in env - save to file
const data: AdminData = {
passwordHash: envPassword,
createdAt: new Date().toISOString(),
@@ -129,7 +129,7 @@ export async function initAdminPassword(): Promise {
return true;
}
- // Cleartext — hash it
+ // Cleartext - hash it
const hash = await hashPassword(envPassword);
const data: AdminData = {
passwordHash: hash,
diff --git a/lib/browser-navigation.ts b/lib/browser-navigation.ts
index 93a9c487..f6502455 100644
--- a/lib/browser-navigation.ts
+++ b/lib/browser-navigation.ts
@@ -50,7 +50,7 @@ export function getPathPrefix(locale?: string): string {
* through unchanged.
*
* Server code (route handlers, layout files running at SSR) should keep using
- * the raw Fetch API — the mount prefix is a browser-only concept.
+ * the raw Fetch API - the mount prefix is a browser-only concept.
*
* @example
* await apiFetch('/api/jmap', { method: 'POST', body })
diff --git a/lib/demo/demo-client.ts b/lib/demo/demo-client.ts
index 2870a074..ab38623c 100644
--- a/lib/demo/demo-client.ts
+++ b/lib/demo/demo-client.ts
@@ -6,7 +6,7 @@ import { generateDemoId } from './demo-utils';
/**
* In-memory JMAP client for demo mode.
- * All data lives in memory — no network calls, no cookies.
+ * All data lives in memory - no network calls, no cookies.
*/
export class DemoJMAPClient implements IJMAPClient {
private data: DemoData;
diff --git a/lib/demo/demo-utils.ts b/lib/demo/demo-utils.ts
index 2c742330..f8a15b46 100644
--- a/lib/demo/demo-utils.ts
+++ b/lib/demo/demo-utils.ts
@@ -7,9 +7,9 @@ export function generateDemoId(prefix: string = 'demo'): string {
/**
* Generate an ISO date string relative to "now".
- * @param daysOffset — whole days from today
- * @param hoursOffset — additional hours offset (default 0)
- * @param minutesOffset — additional minutes offset (default 0)
+ * @param daysOffset - whole days from today
+ * @param hoursOffset - additional hours offset (default 0)
+ * @param minutesOffset - additional minutes offset (default 0)
*/
export function demoDate(daysOffset: number, hoursOffset: number = 0, minutesOffset: number = 0): string {
const d = new Date();
diff --git a/lib/demo/fixtures/emails.ts b/lib/demo/fixtures/emails.ts
index ce8fb21b..5a8771f6 100644
--- a/lib/demo/fixtures/emails.ts
+++ b/lib/demo/fixtures/emails.ts
@@ -63,8 +63,8 @@ export function createDemoEmails(): Email[] {
textBody: [{ partId: '1', blobId: 'blob-5', size: 450, type: 'text/plain' }],
htmlBody: [{ partId: '2', blobId: 'blob-6', size: 650, type: 'text/html' }],
bodyValues: {
- '1': { value: 'Hi team,\n\nI wanted to share the updated timeline for our Q4 deliverables:\n\n- Phase 1: Design review — Oct 15\n- Phase 2: Development — Nov 1-30\n- Phase 3: Testing — Dec 1-15\n- Phase 4: Launch — Dec 20\n\nPlease review and let me know if you see any conflicts.\n\nBest,\nAlice' },
- '2': { value: '
Hi team,
I wanted to share the updated timeline for our Q4 deliverables:
Phase 1: Design review — Oct 15
Phase 2: Development — Nov 1-30
Phase 3: Testing — Dec 1-15
Phase 4: Launch — Dec 20
Please review and let me know if you see any conflicts.
Best, Alice
' },
+ '1': { value: 'Hi team,\n\nI wanted to share the updated timeline for our Q4 deliverables:\n\n- Phase 1: Design review - Oct 15\n- Phase 2: Development - Nov 1-30\n- Phase 3: Testing - Dec 1-15\n- Phase 4: Launch - Dec 20\n\nPlease review and let me know if you see any conflicts.\n\nBest,\nAlice' },
+ '2': { value: '
Hi team,
I wanted to share the updated timeline for our Q4 deliverables:
Phase 1: Design review - Oct 15
Phase 2: Development - Nov 1-30
Phase 3: Testing - Dec 1-15
Phase 4: Launch - Dec 20
Please review and let me know if you see any conflicts.
Best, Alice
' },
},
messageId: '',
},
@@ -83,7 +83,7 @@ export function createDemoEmails(): Email[] {
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-7', size: 520, type: 'text/plain' }],
bodyValues: {
- '1': { value: 'Looks good to me! One concern: the testing window might be tight given the holidays. Could we start testing a few days earlier?\n\nAlso, should we set up a shared doc for tracking blockers?\n\n— Bob' },
+ '1': { value: 'Looks good to me! One concern: the testing window might be tight given the holidays. Could we start testing a few days earlier?\n\nAlso, should we set up a shared doc for tracking blockers?\n\n- Bob' },
},
messageId: '',
inReplyTo: [''],
@@ -104,7 +104,7 @@ export function createDemoEmails(): Email[] {
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-8', size: 400, type: 'text/plain' }],
bodyValues: {
- '1': { value: 'Great point Bob. Let\'s move testing to Nov 28. I\'ll create the shared doc today and share the link.\n\nUpdated timeline:\n- Design review: Oct 15\n- Development: Nov 1-27\n- Testing: Nov 28 - Dec 15\n- Launch: Dec 20\n\n— Alice' },
+ '1': { value: 'Great point Bob. Let\'s move testing to Nov 28. I\'ll create the shared doc today and share the link.\n\nUpdated timeline:\n- Design review: Oct 15\n- Development: Nov 1-27\n- Testing: Nov 28 - Dec 15\n- Launch: Dec 20\n\n- Alice' },
},
messageId: '',
inReplyTo: [''],
@@ -291,7 +291,7 @@ export function createDemoEmails(): Email[] {
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-17', size: 480, type: 'text/plain' }],
bodyValues: {
- '1': { value: 'Hey,\n\nI\'ve been thinking about our rate limiting approach and wanted to propose:\n\n1. Token bucket algorithm instead of fixed window\n2. Per-endpoint limits rather than global\n3. Graduated response (warn → throttle → block)\n\nThoughts? I can put together a more detailed RFC if we agree on the direction.\n\n— Bob' },
+ '1': { value: 'Hey,\n\nI\'ve been thinking about our rate limiting approach and wanted to propose:\n\n1. Token bucket algorithm instead of fixed window\n2. Per-endpoint limits rather than global\n3. Graduated response (warn → throttle → block)\n\nThoughts? I can put together a more detailed RFC if we agree on the direction.\n\n- Bob' },
},
messageId: '',
},
diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts
index a319553b..bf95fa4c 100644
--- a/lib/jmap/client.ts
+++ b/lib/jmap/client.ts
@@ -104,7 +104,7 @@ const EMAIL_LIST_PROPERTIES = [
function isTaskObject(obj: { '@type'?: string; progress?: unknown; due?: unknown; percentComplete?: unknown }): boolean {
const type = obj['@type'];
if (typeof type === 'string' && type.toLowerCase() === 'task') return true;
- // CalDAV-created tasks may lack @type='Task' — detect by task-specific fields
+ // CalDAV-created tasks may lack @type='Task' - detect by task-specific fields
if (type !== 'Event' && (
('progress' in obj && typeof obj.progress === 'string') ||
('due' in obj && obj.due != null) ||
@@ -189,7 +189,7 @@ const CALENDAR_TASK_PROPERTIES = [
'useDefaultAlerts',
'alerts',
'relatedTo',
- 'percentComplete', // Task-only per RFC 8984 §5.2.4 — used in detection heuristic
+ 'percentComplete', // Task-only per RFC 8984 §5.2.4 - used in detection heuristic
] as const;
/**
@@ -209,7 +209,7 @@ function cleanRecurrenceRules(event: Record): void {
if (rules === undefined) continue;
delete event[pluralKey];
if (!Array.isArray(rules)) {
- // null means "remove recurrence" — pass through with the correct key
+ // null means "remove recurrence" - pass through with the correct key
event[singularKey] = rules;
continue;
}
@@ -398,7 +398,7 @@ export class JMAPClient implements IJMAPClient {
response = await fetch(url, { ...init, headers });
}
- // Handle 429 rate limiting — stop immediately, do not retry
+ // Handle 429 rate limiting - stop immediately, do not retry
if (response.status === 429) {
const retryAfterMs = JMAPClient.parseRetryAfter(response);
this.setRateLimited(retryAfterMs);
@@ -414,7 +414,7 @@ export class JMAPClient implements IJMAPClient {
response = await fetch(url, { ...init, headers: retryHeaders });
}
} else if (this.authMode === 'basic' && !this.reconnecting && url !== `${this.serverUrl}/.well-known/jmap`) {
- // JMAP session may have expired — re-establish and retry once
+ // JMAP session may have expired - re-establish and retry once
this.reconnecting = true;
try {
await this.refreshSession();
@@ -422,7 +422,7 @@ export class JMAPClient implements IJMAPClient {
const retryHeaders = { ...init?.headers as Record, 'Authorization': this.authHeader };
response = await fetch(url, { ...init, headers: retryHeaders });
} catch {
- // Session refresh failed — if TOTP was used, try re-auth with fresh TOTP
+ // Session refresh failed - if TOTP was used, try re-auth with fresh TOTP
if (this.onTotpRequired && this.basePassword) {
try {
const newTotp = await this.onTotpRequired();
@@ -434,7 +434,7 @@ export class JMAPClient implements IJMAPClient {
response = await fetch(url, { ...init, headers: retryHeaders });
}
} catch {
- // TOTP re-auth also failed — return original 401
+ // TOTP re-auth also failed - return original 401
}
}
} finally {
@@ -697,7 +697,7 @@ export class JMAPClient implements IJMAPClient {
if (rawMailboxes.length >= maxObjects) {
debug.warn('jmap',
`[JMAP Mailbox] Response contains ${rawMailboxes.length} mailboxes which equals maxObjectsInGet (${maxObjects}). ` +
- `Some mailboxes may be missing — nested folders could appear orphaned at root level.`
+ `Some mailboxes may be missing - nested folders could appear orphaned at root level.`
);
}
@@ -850,7 +850,7 @@ export class JMAPClient implements IJMAPClient {
if (response.methodResponses?.[1]?.[0] === "Email/get" && getResponse) {
const emails = (getResponse.list || []) as Email[];
- // Sort client-side as safety net — some servers may not honour
+ // Sort client-side as safety net - some servers may not honour
// the query sort for large mailboxes without additional filters.
emails.sort((a: Email, b: Email) =>
new Date(b.receivedAt).getTime() - new Date(a.receivedAt).getTime()
@@ -2814,7 +2814,7 @@ export class JMAPClient implements IJMAPClient {
for (const [id, account] of Object.entries(this.accounts)) {
if (id === primaryId) continue;
// Include accounts that either advertise calendar capability
- // or are non-personal (shared/group) accounts — Stalwart doesn't
+ // or are non-personal (shared/group) accounts - Stalwart doesn't
// always advertise capabilities on group accounts even when they
// have calendar resources.
if (account.accountCapabilities?.["urn:ietf:params:jmap:calendars"] || !account.isPersonal) {
@@ -2830,7 +2830,7 @@ export class JMAPClient implements IJMAPClient {
for (const [id, account] of Object.entries(this.accounts)) {
if (id === primaryId) continue;
// Include accounts that either advertise contacts capability
- // or are non-personal (shared/group) accounts — Stalwart doesn't
+ // or are non-personal (shared/group) accounts - Stalwart doesn't
// always advertise capabilities on group accounts even when they
// have contact resources.
if (account.accountCapabilities?.["urn:ietf:params:jmap:contacts"] || !account.isPersonal) {
@@ -3976,7 +3976,7 @@ export class JMAPClient implements IJMAPClient {
// CalDAV-created tasks (e.g. Thunderbird) may lack @type or have @type
// set to something other than 'Event'. Detect them by the presence of
// task-specific keys (due, progress, percentComplete), which RFC 8984 §5.2
- // defines as Task-only — a VEVENT will never include them in the response.
+ // defines as Task-only - a VEVENT will never include them in the response.
// We check for key presence (even if null) because Stalwart may return null
// instead of the RFC defaults (e.g. progress default is "needs-action").
// @see https://www.rfc-editor.org/rfc/rfc8984#section-5.2
@@ -4060,7 +4060,7 @@ export class JMAPClient implements IJMAPClient {
if (!createdId) {
debug.warn('tasks', 'CalendarTask/create no id in server response');
debug.groupEnd();
- throw new Error("Failed to create task — no id returned");
+ throw new Error("Failed to create task - no id returned");
}
// Fetch back with task-specific properties
@@ -4230,7 +4230,7 @@ export class JMAPClient implements IJMAPClient {
async createFileDirectory(name: string, parentId: string | null): Promise {
const accountId = this.getFilesAccountId();
- // Stalwart requires a blobId even for directories — upload an empty blob
+ // Stalwart requires a blobId even for directories - upload an empty blob
const emptyBlob = new File([], name, { type: 'application/x-directory' });
const { blobId } = await this.uploadBlob(emptyBlob);
@@ -4489,7 +4489,7 @@ export class JMAPClient implements IJMAPClient {
this.stopSSEPingMonitor();
- // Stream ended — reconnect unless we were intentionally closed
+ // Stream ended - reconnect unless we were intentionally closed
if (this.sseAbortController && !this.sseAbortController.signal.aborted) {
this.scheduleSSEReconnect();
}
@@ -4512,7 +4512,7 @@ export class JMAPClient implements IJMAPClient {
const change = JSON.parse(dataLines.join('\n')) as StateChange;
this.stateChangeCallback?.(change);
} catch {
- // Malformed SSE data — ignore
+ // Malformed SSE data - ignore
}
}
}
@@ -4671,7 +4671,7 @@ export class JMAPClient implements IJMAPClient {
this.stopSSEPingMonitor();
this.ssePingTimer = setInterval(() => {
if (Date.now() - this.lastSSEActivity > JMAPClient.SSE_PING_TIMEOUT) {
- // SSE connection is stale — abort and reconnect
+ // SSE connection is stale - abort and reconnect
this.stopSSEPingMonitor();
if (this.sseAbortController) {
this.sseAbortController.abort();
@@ -4693,7 +4693,7 @@ export class JMAPClient implements IJMAPClient {
if (typeof document !== 'undefined') {
this.visibilityHandler = () => {
if (!document.hidden) {
- // Tab became visible — immediately check for state changes
+ // Tab became visible - immediately check for state changes
this.checkForStateChanges();
}
};
@@ -4702,7 +4702,7 @@ export class JMAPClient implements IJMAPClient {
if (typeof window !== 'undefined') {
this.onlineHandler = () => {
- // Network reconnected — reconnect SSE or force a poll
+ // Network reconnected - reconnect SSE or force a poll
const eventSourceUrl = this.getEventSourceUrl();
if (eventSourceUrl && !this.sseAbortController) {
this.connectSSE(eventSourceUrl);
diff --git a/lib/jmap/types.ts b/lib/jmap/types.ts
index 7201d9cb..0c2d1714 100644
--- a/lib/jmap/types.ts
+++ b/lib/jmap/types.ts
@@ -39,7 +39,7 @@ export interface Email {
// S/MIME support
blobId?: string;
bodyStructure?: EmailBodyPart;
- // Unified mailbox support — set when displaying emails from multiple accounts
+ // Unified mailbox support - set when displaying emails from multiple accounts
accountId?: string;
accountLabel?: string;
}
diff --git a/lib/plugin-api.ts b/lib/plugin-api.ts
index c49ca36a..73c2e72b 100644
--- a/lib/plugin-api.ts
+++ b/lib/plugin-api.ts
@@ -112,7 +112,7 @@ function createPluginLogger(pluginId: string) {
export interface PluginAPI {
plugin: { id: string; version: string; settings: Record };
- /** Localisation API — register translations and call t() to get strings */
+ /** Localisation API - register translations and call t() to get strings */
i18n: PluginI18n;
ui: {
registerToolbarAction: (action: ToolbarAction) => Disposable;
@@ -154,7 +154,7 @@ export interface PluginHooksAPI {
onEmailClose: (handler: () => void) => Disposable;
onEmailContentRender: (handler: (...args: unknown[]) => unknown) => Disposable;
onThreadExpand: (handler: (...args: unknown[]) => unknown) => Disposable;
- /** Intercept — receives ComposeOptions, may mutate fields, return false to cancel */
+ /** Intercept - receives ComposeOptions, may mutate fields, return false to cancel */
onBeforeCompose: (handler: (options: import('./plugin-types').ComposeOptions) => boolean | void | Promise) => Disposable;
onComposerOpen: (handler: (...args: unknown[]) => unknown) => Disposable;
onBeforeEmailSend: (handler: (...args: unknown[]) => unknown) => Disposable;
@@ -184,7 +184,7 @@ export interface PluginHooksAPI {
onNewEmailReceived: (handler: (...args: unknown[]) => unknown) => Disposable;
onPushConnectionChange: (handler: (...args: unknown[]) => unknown) => Disposable;
onQuotaChange: (handler: (...args: unknown[]) => unknown) => Disposable;
- /** Intercept — receives MailtoContext, return false to prevent the system mail client */
+ /** Intercept - receives MailtoContext, return false to prevent the system mail client */
onMailtoIntercept: (handler: (ctx: import('./plugin-types').MailtoContext) => boolean | void | Promise) => Disposable;
// Calendar
onCalendarEventOpen: (handler: (...args: unknown[]) => unknown) => Disposable;
@@ -228,7 +228,7 @@ export interface PluginHooksAPI {
onDirectoryCreate: (handler: (...args: unknown[]) => unknown) => Disposable;
onBeforeFileDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
onAfterFileDelete: (handler: (...args: unknown[]) => unknown) => Disposable;
- /** Intercept — receives { file: FileResourceView, newName: string }, return false to cancel */
+ /** Intercept - receives { file: FileResourceView, newName: string }, return false to cancel */
onBeforeFileRename: (handler: (ctx: { file: import('./plugin-types').FileResourceView; newName: string }) => boolean | void | Promise) => Disposable;
onFileRename: (handler: (...args: unknown[]) => unknown) => Disposable;
onFileMove: (handler: (...args: unknown[]) => unknown) => Disposable;
@@ -331,7 +331,7 @@ export interface PluginHooksAPI {
onSidebarAppChange: (handler: (...args: unknown[]) => unknown) => Disposable;
// Avatar
onAvatarResolve: (handler: (...args: unknown[]) => unknown) => Disposable;
- // Render — transform hook for email list row badges
+ // Render - transform hook for email list row badges
// Handler: (badges: EmailListBadge[], ctx: { emailId: string; email: EmailReadView }) => EmailListBadge[]
onEmailListItemRender: (handler: (...args: unknown[]) => unknown) => Disposable;
}
diff --git a/lib/plugin-hooks.ts b/lib/plugin-hooks.ts
index c3df1b10..f8692778 100644
--- a/lib/plugin-hooks.ts
+++ b/lib/plugin-hooks.ts
@@ -1,4 +1,4 @@
-// Plugin Hook Bus — event bus system for plugin lifecycle hooks
+// Plugin Hook Bus - event bus system for plugin lifecycle hooks
import type { Disposable } from './plugin-types';
@@ -108,7 +108,7 @@ export class HookBus any> {
return this.handlers.length;
}
- /** Fire all handlers (observer pattern — no return values used) */
+ /** Fire all handlers (observer pattern - no return values used) */
async emit(...args: Parameters): Promise {
for (const { pluginId, handler } of this.handlers) {
if (pluginErrorTracker.isDisabled(pluginId)) continue;
@@ -132,7 +132,7 @@ export class HookBus any> {
}
}
- /** Fire handlers as interceptors — any returning false cancels the operation */
+ /** Fire handlers as interceptors - any returning false cancels the operation */
async intercept(...args: Parameters): Promise {
for (const { pluginId, handler } of this.handlers) {
if (pluginErrorTracker.isDisabled(pluginId)) continue;
@@ -146,7 +146,7 @@ export class HookBus any> {
return true;
}
- /** Fire handlers as transforms — each receives the output of the previous */
+ /** Fire handlers as transforms - each receives the output of the previous */
async transform(initial: V, ...rest: unknown[]): Promise {
let value = initial;
for (const { pluginId, handler } of this.handlers) {
@@ -172,7 +172,7 @@ export const emailHooks = {
onEmailClose: new HookBus(),
onEmailContentRender: new HookBus(),
onThreadExpand: new HookBus(),
- // Intercept hook — fires before the composer opens.
+ // Intercept hook - fires before the composer opens.
// Handlers receive ComposeOptions and may mutate fields in place.
// Return false to cancel opening the composer.
onBeforeCompose: new HookBus(),
@@ -204,7 +204,7 @@ export const emailHooks = {
onNewEmailReceived: new HookBus(),
onPushConnectionChange: new HookBus(),
onQuotaChange: new HookBus(),
- // Intercept hook — fired when a mailto: link is clicked.
+ // Intercept hook - fired when a mailto: link is clicked.
// Return false to prevent the browser from opening the system mail client.
onMailtoIntercept: new HookBus(),
};
@@ -261,7 +261,7 @@ export const fileHooks = {
onDirectoryCreate: new HookBus(),
onBeforeFileDelete: new HookBus(),
onAfterFileDelete: new HookBus(),
- // Intercept hook — fires before a file is renamed.
+ // Intercept hook - fires before a file is renamed.
// Receives { file: FileResourceView, newName: string }.
// Return false to cancel the rename.
onBeforeFileRename: new HookBus(),
@@ -423,7 +423,7 @@ export const avatarHooks = {
// §7.22 Render Hooks
export const renderHooks = {
- // Transform hook — runs for each visible email list row.
+ // Transform hook - runs for each visible email list row.
// Initial value: EmailListBadge[] (always starts as [])
// Second argument: { emailId: string; email: EmailReadView }
// Handlers return a new (or extended) badges array.
diff --git a/lib/plugin-i18n.ts b/lib/plugin-i18n.ts
index b3ab7faf..0537dd17 100644
--- a/lib/plugin-i18n.ts
+++ b/lib/plugin-i18n.ts
@@ -1,4 +1,4 @@
-// Plugin i18n registry — manages per-plugin translation tables
+// Plugin i18n registry - manages per-plugin translation tables
//
// Each plugin gets its own namespace keyed by:
// pluginId → locale → { messageKey → translated string }
@@ -105,7 +105,7 @@ export function createPluginI18n(pluginId: string) {
t(key: string, params?: Record): string {
const template = resolve(pluginId, key);
if (template !== undefined) return interpolate(template, params);
- return key; // never throw — just return the key
+ return key; // never throw - just return the key
},
/** The current app locale (e.g. "en", "de", "fr") */
diff --git a/lib/plugin-loader.ts b/lib/plugin-loader.ts
index 80547fc8..b7f9d219 100644
--- a/lib/plugin-loader.ts
+++ b/lib/plugin-loader.ts
@@ -1,4 +1,4 @@
-// Plugin Loader — loads and activates plugins via blob URL dynamic import
+// Plugin Loader - loads and activates plugins via blob URL dynamic import
import type { InstalledPlugin, Disposable } from './plugin-types';
import { pluginStorage } from './plugin-storage';
diff --git a/lib/plugin-types.ts b/lib/plugin-types.ts
index f4721f3d..c036ed99 100644
--- a/lib/plugin-types.ts
+++ b/lib/plugin-types.ts
@@ -195,7 +195,7 @@ export interface KeyboardShortcut {
}
// ─── Read-Only View Types ────────────────────────────────────
-// Projected views exposed to plugins — no direct store references
+// Projected views exposed to plugins - no direct store references
export interface EmailReadView {
id: string;
@@ -415,7 +415,7 @@ export interface ComposeOptions {
* A small visual indicator injected into an email list row via onEmailListItemRender.
*/
export interface EmailListBadge {
- /** Stable unique key within the plugin — used as React key */
+ /** Stable unique key within the plugin - used as React key */
key: string;
/** Short label text displayed in the badge */
label: string;
diff --git a/lib/plugin-validator.ts b/lib/plugin-validator.ts
index fe71a5e6..b06006bb 100644
--- a/lib/plugin-validator.ts
+++ b/lib/plugin-validator.ts
@@ -116,7 +116,7 @@ function checkJSSecurity(code: string): string[] {
const warnings: string[] = [];
for (const { pattern, label } of SUSPICIOUS_JS_PATTERNS) {
if (pattern.test(code)) {
- warnings.push(`Contains ${label} — review for security`);
+ warnings.push(`Contains ${label} - review for security`);
}
pattern.lastIndex = 0; // reset regex
}
diff --git a/lib/recurrence-expansion.ts b/lib/recurrence-expansion.ts
index db14ae5c..76f9e8fb 100644
--- a/lib/recurrence-expansion.ts
+++ b/lib/recurrence-expansion.ts
@@ -24,7 +24,7 @@ const INDEX_TO_DAY: string[] = ['su', 'mo', 'tu', 'we', 'th', 'fr', 'sa'];
* store can use it for mutations.
*
* Non-recurring events are returned as-is. For recurring events the master is
- * **not** returned — only expanded instances within the range.
+ * **not** returned - only expanded instances within the range.
*/
export function expandRecurringEvents(
events: CalendarEvent[],
@@ -46,7 +46,7 @@ export function expandRecurringEvents(
}
for (const event of events) {
- // Skip override instances returned by the server — they belong to a
+ // Skip override instances returned by the server - they belong to a
// master recurring event and are already handled via recurrenceOverrides.
if (event.recurrenceId && event.uid && recurringUids.has(event.uid)) {
continue;
@@ -165,7 +165,7 @@ function createOccurrence(
}
// ---------------------------------------------------------------------------
-// §3.3.3.1 — Implicit byX property addition
+// §3.3.3.1 - Implicit byX property addition
// ---------------------------------------------------------------------------
function addImplicitByX(
rule: CalendarRecurrenceRule,
@@ -211,7 +211,7 @@ function addImplicitByX(
}
// ---------------------------------------------------------------------------
-// §3.3.3.1 — Generate occurrence dates
+// §3.3.3.1 - Generate occurrence dates
// ---------------------------------------------------------------------------
function generateDates(
eventStart: Date,
diff --git a/lib/sieve/__tests__/external-rules.test.ts b/lib/sieve/__tests__/external-rules.test.ts
index a5d3175f..45f48d2e 100644
--- a/lib/sieve/__tests__/external-rules.test.ts
+++ b/lib/sieve/__tests__/external-rules.test.ts
@@ -19,7 +19,7 @@ function makeBulwarkRule(overrides: Partial = {}): FilterRule {
}
describe('external rule preservation (issue #201)', () => {
- describe('parser — external rule recognition', () => {
+ describe('parser - external rule recognition', () => {
it('parses a Roundcube-style rule with "# rule:[Name]" comment', () => {
const script = `require ["fileinto"];\n\n# rule:[Archive Newsletters]\nif header :contains "List-Id" "news" {\n fileinto "Newsletters";\n}\n`;
const result = parseScript(script);
@@ -84,7 +84,7 @@ describe('external rule preservation (issue #201)', () => {
});
});
- describe('parser — mixed Bulwark + external', () => {
+ describe('parser - mixed Bulwark + external', () => {
it('returns Bulwark rules from metadata and external rules from the rest', () => {
const bulwark = [makeBulwarkRule({ name: 'Bulwark A' })];
const bulwarkScript = generateScript(bulwark);
@@ -113,7 +113,7 @@ describe('external rule preservation (issue #201)', () => {
});
});
- describe('generator — external splice', () => {
+ describe('generator - external splice', () => {
it('appends external rawBlocks verbatim after Bulwark-managed output', () => {
const externalRule: FilterRule = {
id: 'ext-0',
@@ -253,7 +253,7 @@ describe('external rule preservation (issue #201)', () => {
const externalBefore = parsed.rules.filter(r => r.origin === 'external');
expect(externalBefore).toHaveLength(1);
- // Simulate a user edit — update the Bulwark rule name
+ // Simulate a user edit - update the Bulwark rule name
const edited = parsed.rules.map(r => (r.origin === 'external' || r.origin === 'opaque' ? r : { ...r, name: 'Mine (edited)' }));
const regenerated = generateScript(edited, parsed.vacation, { externalRequires: parsed.externalRequires });
const reparsed = parseScript(regenerated);
diff --git a/lib/sieve/__tests__/fixtures/mixed-origins.sieve b/lib/sieve/__tests__/fixtures/mixed-origins.sieve
index 74195eaa..6b9bc50c 100644
--- a/lib/sieve/__tests__/fixtures/mixed-origins.sieve
+++ b/lib/sieve/__tests__/fixtures/mixed-origins.sieve
@@ -17,7 +17,7 @@ if anyof(header :is "From" "ceo@company.com", header :is "From" "board@company.c
# --- External rules (managed outside Bulwark) ---
-# rule:[Finance — auto-file invoices]
+# rule:[Finance - auto-file invoices]
if allof(header :contains "From" "billing@", header :contains "Subject" "invoice") {
fileinto :copy "Finance/Invoices";
keep;
diff --git a/lib/sieve/parser.ts b/lib/sieve/parser.ts
index a1845926..953a06c6 100644
--- a/lib/sieve/parser.ts
+++ b/lib/sieve/parser.ts
@@ -233,7 +233,7 @@ function extractRequireTokens(stmt: string): string[] {
/**
* Extract the last contiguous block of comments immediately preceding a
- * statement — comments separated from the statement by a blank line are not
+ * statement - comments separated from the statement by a blank line are not
* considered its leading commentary (they likely belong to the previous
* block, e.g. a trailing "# Nextcloud Mail - end" marker).
*/
@@ -341,7 +341,7 @@ function parseAtom(raw: string): FilterCondition | null {
} else if (tag === 'is') {
comparator = negated ? 'not_is' : 'is';
} else {
- // :matches — distinguish starts_with / ends_with / matches
+ // :matches - distinguish starts_with / ends_with / matches
const starPositions = [...value].reduce((acc, ch, idx) => (ch === '*' ? [...acc, idx] : acc), []);
if (starPositions.length === 1 && starPositions[0] === value.length - 1) {
comparator = 'starts_with';
@@ -591,7 +591,7 @@ export function parseScript(content: string): ParseResult {
};
}
- // No metadata — check vacation-only first
+ // No metadata - check vacation-only first
const vacationOnly = detectVacationOnlyScript(content);
if (vacationOnly) return vacationOnly;
@@ -599,7 +599,7 @@ export function parseScript(content: string): ParseResult {
const external = parseExternalRules(content, 'ext');
if (!external.hasContent) {
- // Entirely empty or whitespace/comments only — treat as empty, editable.
+ // Entirely empty or whitespace/comments only - treat as empty, editable.
return { rules: [], isOpaque: false, externalRequires: [] };
}
diff --git a/lib/smime/certificate-utils.ts b/lib/smime/certificate-utils.ts
index ddfc7baf..bef1dd95 100644
--- a/lib/smime/certificate-utils.ts
+++ b/lib/smime/certificate-utils.ts
@@ -187,7 +187,7 @@ function extractEmailAddresses(cert: pkijs.Certificate): string[] {
names = gn.names;
}
} catch {
- // Malformed SAN — skip gracefully
+ // Malformed SAN - skip gracefully
}
}
diff --git a/lib/smime/crypto-engine.ts b/lib/smime/crypto-engine.ts
index 00ff218f..0a37fe01 100644
--- a/lib/smime/crypto-engine.ts
+++ b/lib/smime/crypto-engine.ts
@@ -21,7 +21,7 @@ import * as pkijs from 'pkijs';
// webcrypto-liner exports a Crypto constructor at runtime that extends native
// Web Crypto with legacy algorithms (3DES, etc.). Its type declarations only
// expose the type alias, so we import the module dynamically and cast.
-// Import the ES module build directly — the package's "browser" field points
+// Import the ES module build directly - the package's "browser" field points
// to a shim-only build that has no named exports (no setCrypto, Crypto, etc.).
// eslint-disable-next-line @typescript-eslint/no-require-imports
const liner = require('webcrypto-liner/build/index.es.js') as {
@@ -55,7 +55,7 @@ function pbeConfig(oid: string): { keyLen: number; ivLen: number; algName: strin
}
/**
- * PKCS#12 key derivation — RFC 7292, Appendix B.
+ * PKCS#12 key derivation - RFC 7292, Appendix B.
*
* @param password BMP-encoded password (with trailing 0x00 0x00)
* @param salt raw salt bytes
diff --git a/lib/smime/mime-builder.ts b/lib/smime/mime-builder.ts
index 18e8cfba..7f30c791 100644
--- a/lib/smime/mime-builder.ts
+++ b/lib/smime/mime-builder.ts
@@ -275,7 +275,7 @@ function cmsToBase64Blob(data: Blob | ArrayBuffer | Uint8Array): Blob {
} else if (data instanceof ArrayBuffer) {
bytes = new Uint8Array(data);
} else {
- // Blob — we need sync; caller should have converted. Fallback to empty.
+ // Blob - we need sync; caller should have converted. Fallback to empty.
bytes = new Uint8Array(0);
}
const b64 = base64Encode(bytes.buffer as ArrayBuffer);
diff --git a/lib/smime/pkcs12-export.ts b/lib/smime/pkcs12-export.ts
index 78c9caf5..07263e74 100644
--- a/lib/smime/pkcs12-export.ts
+++ b/lib/smime/pkcs12-export.ts
@@ -106,7 +106,7 @@ export async function exportPkcs12(
parsedValue: {
safeContents: [
{
- privacyMode: 0, // no extra encryption — key bag is already shrouded
+ privacyMode: 0, // no extra encryption - key bag is already shrouded
value: new pkijs.SafeContents({
safeBags: [keyBagSafe],
}),
diff --git a/lib/smime/pkcs12-import.ts b/lib/smime/pkcs12-import.ts
index e4a16435..d9df58c4 100644
--- a/lib/smime/pkcs12-import.ts
+++ b/lib/smime/pkcs12-import.ts
@@ -89,7 +89,7 @@ export async function importPkcs12(
cert = certBag.parsedValue;
der = cert.toSchema(true).toBER(false);
} else if (certBag.certId === '1.2.840.113549.1.9.22.1' && certBag.certValue) {
- // x509Certificate — extract DER from the OCTET STRING
+ // x509Certificate - extract DER from the OCTET STRING
const certDerBytes = (certBag.certValue as asn1js.OctetString).valueBlock.valueHexView;
const certAsn1 = asn1js.fromBER(certDerBytes);
if (certAsn1.offset !== -1) {
diff --git a/lib/smime/smime-decrypt.ts b/lib/smime/smime-decrypt.ts
index 3e3b3300..a389991e 100644
--- a/lib/smime/smime-decrypt.ts
+++ b/lib/smime/smime-decrypt.ts
@@ -78,7 +78,7 @@ export async function smimeDecrypt(input: DecryptionInput): Promise= 4) {
return r.readBytes(4);
}
@@ -268,7 +268,7 @@ export function parseTnef(data: Uint8Array): TnefResult {
attrCount++;
if (attrLen > r.remaining - 2) {
- debug.warn('email', 'Attribute #' + attrCount + ': truncated data — need', attrLen, 'bytes but only', r.remaining - 2, 'available');
+ debug.warn('email', 'Attribute #' + attrCount + ': truncated data - need', attrLen, 'bytes but only', r.remaining - 2, 'available');
break;
}
@@ -316,7 +316,7 @@ export function parseTnef(data: Uint8Array): TnefResult {
}
} else if (level === LVL_ATTACHMENT) {
if (attrID === attAttachRenddata) {
- // Start of a new attachment — flush previous
+ // Start of a new attachment - flush previous
if (curAttach?.data) {
debug.log('email', ' → Flushing previous attachment:', curAttach.name, '(' + curAttach.mimeType + ',', curAttach.data.byteLength, 'bytes)');
result.attachments.push({
@@ -375,7 +375,7 @@ export function parseTnef(data: Uint8Array): TnefResult {
});
}
- debug.log('email', 'TNEF parsing complete — body:', !!result.body, ', htmlBody:', !!result.htmlBody, ', attachments:', result.attachments.length);
+ debug.log('email', 'TNEF parsing complete - body:', !!result.body, ', htmlBody:', !!result.htmlBody, ', attachments:', result.attachments.length);
if (result.attachments.length > 0) {
debug.table(result.attachments.map(a => ({ name: a.name, mimeType: a.mimeType, size: a.data.byteLength })), 'email');
}
diff --git a/lib/utils.ts b/lib/utils.ts
index 622ca9f1..7547fd1d 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -139,7 +139,7 @@ function deduplicateMailboxes(mailboxes: Mailbox[]): Mailbox[] {
return;
}
- // Never deduplicate nested mailboxes — only root-level folders can be
+ // Never deduplicate nested mailboxes - only root-level folders can be
// duplicates of role-based mailboxes. Removing a nested folder that happens
// to share a name with a role folder (e.g. a subfolder named "Sent") would
// orphan its children to root level. (GitHub #118)
diff --git a/locales/de/common.json b/locales/de/common.json
index e20c51e3..997cde01 100644
--- a/locales/de/common.json
+++ b/locales/de/common.json
@@ -45,11 +45,11 @@
"imprint": "Impressum",
"privacy_policy": "Datenschutz",
"try_demo": "Demo testen",
- "demo_description": "Erkunden Sie mit Beispieldaten — kein Konto nötig",
+ "demo_description": "Erkunden Sie mit Beispieldaten - kein Konto nötig",
"demo_launching": "Demo wird gestartet...",
"demo_login_button": "Demo starten",
"demo_tagline": "Erleben Sie einen voll ausgestatteten E-Mail-Client. Kein Konto erforderlich.",
- "demo_no_signup": "Keine Registrierung nötig — erkunden Sie frei mit Beispieldaten",
+ "demo_no_signup": "Keine Registrierung nötig - erkunden Sie frei mit Beispieldaten",
"oauth_completing": "Anmeldung wird abgeschlossen...",
"oauth_error": {
"title": "Authentifizierung fehlgeschlagen",
@@ -1166,7 +1166,7 @@
"warnings": {
"end_before_start": "Das Enddatum muss nach dem Startdatum liegen",
"start_in_past": "Das Startdatum liegt in der Vergangenheit",
- "empty_body": "Der Nachrichtentext ist leer — Empfänger erhalten eine leere Antwort"
+ "empty_body": "Der Nachrichtentext ist leer - Empfänger erhalten eine leere Antwort"
}
},
"folders": {
@@ -2306,7 +2306,7 @@
},
"demo_welcome": {
"title": "Willkommen bei Bulwark Mail",
- "description": "Entdecken Sie einen voll ausgestatteten Webmail-Client — direkt in Ihrem Browser. Alle Daten bleiben auf Ihrem Gerät, also testen Sie alles.",
+ "description": "Entdecken Sie einen voll ausgestatteten Webmail-Client - direkt in Ihrem Browser. Alle Daten bleiben auf Ihrem Gerät, also testen Sie alles.",
"feature_email": "E-Mails lesen & verfassen",
"feature_organize": "Tags, Sterne & Ordner",
"feature_shortcuts": "Tastenkürzel",
@@ -2533,7 +2533,7 @@
"create_event_title": "Ereignis erstellen",
"create_event_desc": "Klicken Sie auf diese Schaltfläche, um ein neues Kalenderereignis zu erstellen. Sie können Titel, Datum, Uhrzeit und Teilnehmer festlegen.",
"event_modal_title": "Ereignisdetails",
- "event_modal_desc": "Hier ist das Ereignisformular. Geben Sie den Titel ein, wählen Sie Datum und Uhrzeit, fügen Sie einen Ort oder Teilnehmer hinzu. Klicken Sie auf Speichern, wenn Sie fertig sind — oder schließen Sie es und fahren Sie fort.",
+ "event_modal_desc": "Hier ist das Ereignisformular. Geben Sie den Titel ein, wählen Sie Datum und Uhrzeit, fügen Sie einen Ort oder Teilnehmer hinzu. Klicken Sie auf Speichern, wenn Sie fertig sind - oder schließen Sie es und fahren Sie fort.",
"contacts_list_title": "Ihre Kontakte",
"contacts_list_desc": "Hier sind Ihre Kontakte. Klicken Sie auf einen Kontakt, um Details zu sehen. Sie können neue Kontakte erstellen oder vCards importieren.",
"files_title": "Dateispeicher",
@@ -2541,7 +2541,7 @@
"settings_tabs_desc": "Hier finden Sie alle Einstellungskategorien. Passen Sie das Erscheinungsbild an, verwalten Sie Identitäten, richten Sie E-Mail-Filter ein, konfigurieren Sie Ihren Kalender und vieles mehr.",
"files_desc": "Ihr Dateibrowser zum Hochladen, Organisieren und Teilen von Dateien.",
"demo_banner_title": "Demo-Steuerung",
- "demo_banner_desc": "Sie sind im Demo-Modus — alles bleibt in Ihrem Browser. Klicken Sie jederzeit auf 'Demo zurücksetzen'.",
+ "demo_banner_desc": "Sie sind im Demo-Modus - alles bleibt in Ihrem Browser. Klicken Sie jederzeit auf 'Demo zurücksetzen'.",
"quota_title": "Speichernutzung",
"quota_desc": "Verfolgen Sie Ihre Postfachgröße hier. Der Kreis füllt sich mit zunehmendem Verbrauch."
},
diff --git a/locales/en/common.json b/locales/en/common.json
index 9714f915..38a5cb00 100644
--- a/locales/en/common.json
+++ b/locales/en/common.json
@@ -45,11 +45,11 @@
"imprint": "Imprint",
"privacy_policy": "Privacy Policy",
"try_demo": "Try Demo",
- "demo_description": "Explore with sample data — no account needed",
+ "demo_description": "Explore with sample data - no account needed",
"demo_launching": "Launching demo...",
"demo_login_button": "Launch Demo",
"demo_tagline": "Experience a full-featured email client. No account required.",
- "demo_no_signup": "No signup needed — explore freely with sample data",
+ "demo_no_signup": "No signup needed - explore freely with sample data",
"oauth_completing": "Completing sign in...",
"oauth_error": {
"title": "Authentication Failed",
@@ -928,7 +928,7 @@
"close": "Close",
"invalid_email": "Please enter a valid email address",
"already_added": "This sender is already trusted",
- "save_error": "Failed to save — check the Contacts debug log for details",
+ "save_error": "Failed to save - check the Contacts debug log for details",
"use_address_book_label": "Sync with address book",
"use_address_book_description": "Store trusted senders in a dedicated \"Trusted Senders\" address book so they sync across all your devices"
},
@@ -1171,7 +1171,7 @@
"warnings": {
"end_before_start": "End date must be after start date",
"start_in_past": "Start date is in the past",
- "empty_body": "Message body is empty — recipients will receive a blank reply"
+ "empty_body": "Message body is empty - recipients will receive a blank reply"
}
},
"folders": {
@@ -2313,7 +2313,7 @@
},
"demo_welcome": {
"title": "Welcome to Bulwark Mail",
- "description": "Explore a fully-featured webmail client — right in your browser. All data stays on your device, so feel free to test everything.",
+ "description": "Explore a fully-featured webmail client - right in your browser. All data stays on your device, so feel free to test everything.",
"feature_email": "Read & compose email",
"feature_organize": "Tags, stars & folders",
"feature_shortcuts": "Keyboard shortcuts",
@@ -2540,15 +2540,15 @@
"create_event_title": "Create an event",
"create_event_desc": "Click this button to create a new calendar event. You can set a title, date, time, and add participants.",
"event_modal_title": "Event details",
- "event_modal_desc": "Here's the event form. Fill in the title, pick a date and time, add a location or participants. Hit save when you're done — or close it and move on.",
+ "event_modal_desc": "Here's the event form. Fill in the title, pick a date and time, add a location or participants. Hit save when you're done - or close it and move on.",
"contacts_list_title": "Your contacts",
"contacts_list_desc": "Here are your contacts. Click any contact to see their full details on the right. You can also create new contacts, import vCards, or organize contacts into groups.",
"settings_tabs_title": "Settings menu",
"settings_tabs_desc": "Here are all the settings categories. Customize your appearance, manage identities, set up email filters, configure your calendar, and much more.",
"files_title": "File storage",
- "files_desc": "Your file browser lets you upload, organize, and share files — like a personal cloud drive built into your mail.",
+ "files_desc": "Your file browser lets you upload, organize, and share files - like a personal cloud drive built into your mail.",
"demo_banner_title": "Demo controls",
- "demo_banner_desc": "You're in demo mode — everything stays in your browser. Hit 'Reset Demo' anytime to start fresh with clean sample data.",
+ "demo_banner_desc": "You're in demo mode - everything stays in your browser. Hit 'Reset Demo' anytime to start fresh with clean sample data.",
"quota_title": "Storage usage",
"quota_desc": "Track your mailbox size here. The circle fills up as you use more space."
},
diff --git a/locales/es/common.json b/locales/es/common.json
index 8b309c1f..1f037d25 100644
--- a/locales/es/common.json
+++ b/locales/es/common.json
@@ -45,11 +45,11 @@
"imprint": "Aviso legal",
"privacy_policy": "Política de privacidad",
"try_demo": "Probar demo",
- "demo_description": "Explora con datos de ejemplo — sin cuenta necesaria",
+ "demo_description": "Explora con datos de ejemplo - sin cuenta necesaria",
"demo_launching": "Iniciando demo...",
"demo_login_button": "Iniciar demo",
"demo_tagline": "Experimenta un cliente de correo completo. Sin necesidad de cuenta.",
- "demo_no_signup": "Sin registro — explora libremente con datos de ejemplo",
+ "demo_no_signup": "Sin registro - explora libremente con datos de ejemplo",
"oauth_completing": "Completando inicio de sesión...",
"oauth_error": {
"title": "Error de autenticación",
@@ -1166,7 +1166,7 @@
"warnings": {
"end_before_start": "La fecha de fin debe ser posterior a la fecha de inicio",
"start_in_past": "La fecha de inicio está en el pasado",
- "empty_body": "El cuerpo del mensaje está vacío — los destinatarios recibirán una respuesta en blanco"
+ "empty_body": "El cuerpo del mensaje está vacío - los destinatarios recibirán una respuesta en blanco"
}
},
"folders": {
@@ -2306,7 +2306,7 @@
},
"demo_welcome": {
"title": "Bienvenido a Bulwark Mail",
- "description": "Explora un cliente de correo web completo — directamente en tu navegador. Todos los datos quedan en tu dispositivo, así que prueba todo.",
+ "description": "Explora un cliente de correo web completo - directamente en tu navegador. Todos los datos quedan en tu dispositivo, así que prueba todo.",
"feature_email": "Leer y redactar correos",
"feature_organize": "Etiquetas, estrellas y carpetas",
"feature_shortcuts": "Atajos de teclado",
@@ -2533,7 +2533,7 @@
"create_event_title": "Crear un evento",
"create_event_desc": "Haz clic en este botón para crear un nuevo evento de calendario. Puedes establecer un título, fecha, hora y añadir participantes.",
"event_modal_title": "Detalles del evento",
- "event_modal_desc": "Aquí está el formulario del evento. Rellena el título, elige una fecha y hora, añade una ubicación o participantes. Pulsa guardar cuando hayas terminado — o ciérralo y continúa.",
+ "event_modal_desc": "Aquí está el formulario del evento. Rellena el título, elige una fecha y hora, añade una ubicación o participantes. Pulsa guardar cuando hayas terminado - o ciérralo y continúa.",
"contacts_list_title": "Tus contactos",
"contacts_list_desc": "Aquí están tus contactos. Haz clic en cualquier contacto para ver sus detalles. Puedes crear contactos nuevos o importar vCards.",
"files_title": "Almacenamiento de archivos",
@@ -2541,7 +2541,7 @@
"settings_tabs_desc": "Aquí están todas las categorías de ajustes. Personaliza la apariencia, gestiona identidades, configura filtros de correo, ajusta tu calendario y mucho más.",
"files_desc": "Tu explorador de archivos para subir, organizar y compartir archivos.",
"demo_banner_title": "Controles de demo",
- "demo_banner_desc": "Estás en modo demo — todo permanece en tu navegador. Haz clic en 'Restablecer demo' en cualquier momento.",
+ "demo_banner_desc": "Estás en modo demo - todo permanece en tu navegador. Haz clic en 'Restablecer demo' en cualquier momento.",
"quota_title": "Uso de almacenamiento",
"quota_desc": "Controla el tamaño de tu buzón aquí. El círculo se llena a medida que usas más espacio."
},
diff --git a/locales/fr/common.json b/locales/fr/common.json
index 6c9233ca..2ac69980 100644
--- a/locales/fr/common.json
+++ b/locales/fr/common.json
@@ -45,11 +45,11 @@
"imprint": "Mentions légales",
"privacy_policy": "Politique de confidentialité",
"try_demo": "Essayer la démo",
- "demo_description": "Explorez avec des données d'exemple — aucun compte nécessaire",
+ "demo_description": "Explorez avec des données d'exemple - aucun compte nécessaire",
"demo_launching": "Lancement de la démo...",
"demo_login_button": "Lancer la démo",
"demo_tagline": "Découvrez un client de messagerie complet. Aucun compte requis.",
- "demo_no_signup": "Aucune inscription — explorez librement avec des données d'exemple",
+ "demo_no_signup": "Aucune inscription - explorez librement avec des données d'exemple",
"oauth_completing": "Connexion en cours...",
"oauth_error": {
"title": "Échec de l'authentification",
@@ -1166,7 +1166,7 @@
"warnings": {
"end_before_start": "La date de fin doit être postérieure à la date de début",
"start_in_past": "La date de début est dans le passé",
- "empty_body": "Le corps du message est vide — les destinataires recevront une réponse vide"
+ "empty_body": "Le corps du message est vide - les destinataires recevront une réponse vide"
}
},
"folders": {
@@ -2306,7 +2306,7 @@
},
"demo_welcome": {
"title": "Bienvenue sur Bulwark Mail",
- "description": "Explorez un client webmail complet — directement dans votre navigateur. Toutes les données restent sur votre appareil, alors testez tout.",
+ "description": "Explorez un client webmail complet - directement dans votre navigateur. Toutes les données restent sur votre appareil, alors testez tout.",
"feature_email": "Lire et rédiger des e-mails",
"feature_organize": "Tags, étoiles et dossiers",
"feature_shortcuts": "Raccourcis clavier",
@@ -2533,15 +2533,15 @@
"create_event_title": "Créer un événement",
"create_event_desc": "Cliquez sur ce bouton pour créer un nouvel événement. Vous pouvez définir un titre, une date, une heure et ajouter des participants.",
"event_modal_title": "Détails de l'événement",
- "event_modal_desc": "Voici le formulaire de l'événement. Remplissez le titre, choisissez une date et une heure, ajoutez un lieu ou des participants. Cliquez sur enregistrer quand vous avez terminé — ou fermez-le et passez à autre chose.",
+ "event_modal_desc": "Voici le formulaire de l'événement. Remplissez le titre, choisissez une date et une heure, ajoutez un lieu ou des participants. Cliquez sur enregistrer quand vous avez terminé - ou fermez-le et passez à autre chose.",
"contacts_list_title": "Vos contacts",
"contacts_list_desc": "Voici vos contacts. Cliquez sur un contact pour voir ses détails à droite. Vous pouvez aussi créer de nouveaux contacts, importer des vCards ou organiser les contacts en groupes.",
"files_title": "Stockage de fichiers",
"settings_tabs_title": "Menu des paramètres",
"settings_tabs_desc": "Voici toutes les catégories de paramètres. Personnalisez l'apparence, gérez les identités, configurez les filtres de messagerie, paramétrez votre calendrier et bien plus encore.",
- "files_desc": "Votre gestionnaire de fichiers vous permet de téléverser, organiser et partager des fichiers — comme un cloud personnel intégré à votre messagerie.",
+ "files_desc": "Votre gestionnaire de fichiers vous permet de téléverser, organiser et partager des fichiers - comme un cloud personnel intégré à votre messagerie.",
"demo_banner_title": "Contrôles de démo",
- "demo_banner_desc": "Vous êtes en mode démo — tout reste dans votre navigateur. Cliquez sur 'Réinitialiser la démo' à tout moment pour repartir avec des données fraîches.",
+ "demo_banner_desc": "Vous êtes en mode démo - tout reste dans votre navigateur. Cliquez sur 'Réinitialiser la démo' à tout moment pour repartir avec des données fraîches.",
"quota_title": "Utilisation du stockage",
"quota_desc": "Suivez la taille de votre boîte mail ici. Le cercle se remplit au fur et à mesure que vous utilisez plus d'espace."
},
diff --git a/locales/it/common.json b/locales/it/common.json
index d0b39c8e..8186a3ec 100644
--- a/locales/it/common.json
+++ b/locales/it/common.json
@@ -45,11 +45,11 @@
"imprint": "Note legali",
"privacy_policy": "Informativa sulla privacy",
"try_demo": "Prova la demo",
- "demo_description": "Esplora con dati di esempio — nessun account necessario",
+ "demo_description": "Esplora con dati di esempio - nessun account necessario",
"demo_launching": "Avvio demo...",
"demo_login_button": "Avvia demo",
"demo_tagline": "Scopri un client di posta completo. Nessun account richiesto.",
- "demo_no_signup": "Nessuna registrazione — esplora liberamente con dati di esempio",
+ "demo_no_signup": "Nessuna registrazione - esplora liberamente con dati di esempio",
"oauth_completing": "Completamento dell'accesso...",
"oauth_error": {
"title": "Autenticazione non riuscita",
@@ -1166,7 +1166,7 @@
"warnings": {
"end_before_start": "La data di fine deve essere successiva alla data di inizio",
"start_in_past": "La data di inizio è nel passato",
- "empty_body": "Il corpo del messaggio è vuoto — i destinatari riceveranno una risposta vuota"
+ "empty_body": "Il corpo del messaggio è vuoto - i destinatari riceveranno una risposta vuota"
}
},
"folders": {
@@ -2306,7 +2306,7 @@
},
"demo_welcome": {
"title": "Benvenuto su Bulwark Mail",
- "description": "Esplora un client webmail completo — direttamente nel tuo browser. Tutti i dati restano sul tuo dispositivo, quindi prova tutto.",
+ "description": "Esplora un client webmail completo - direttamente nel tuo browser. Tutti i dati restano sul tuo dispositivo, quindi prova tutto.",
"feature_email": "Leggere e scrivere email",
"feature_organize": "Tag, stelle e cartelle",
"feature_shortcuts": "Scorciatoie da tastiera",
@@ -2533,15 +2533,15 @@
"create_event_title": "Crea un evento",
"create_event_desc": "Fai clic su questo pulsante per creare un nuovo evento del calendario. Puoi impostare un titolo, data, ora e aggiungere partecipanti.",
"event_modal_title": "Dettagli evento",
- "event_modal_desc": "Ecco il modulo dell'evento. Inserisci il titolo, scegli data e ora, aggiungi un luogo o partecipanti. Premi salva quando hai finito — o chiudilo e vai avanti.",
+ "event_modal_desc": "Ecco il modulo dell'evento. Inserisci il titolo, scegli data e ora, aggiungi un luogo o partecipanti. Premi salva quando hai finito - o chiudilo e vai avanti.",
"contacts_list_title": "I tuoi contatti",
"contacts_list_desc": "Ecco i tuoi contatti. Clicca su un contatto per vedere i suoi dettagli a destra. Puoi anche creare nuovi contatti, importare vCard o organizzare i contatti in gruppi.",
"files_title": "Archiviazione file",
"settings_tabs_title": "Menu impostazioni",
"settings_tabs_desc": "Ecco tutte le categorie di impostazioni. Personalizza l'aspetto, gestisci le identità, configura i filtri email, imposta il calendario e molto altro.",
- "files_desc": "Il tuo file browser ti permette di caricare, organizzare e condividere file — come un cloud personale integrato nella tua posta.",
+ "files_desc": "Il tuo file browser ti permette di caricare, organizzare e condividere file - come un cloud personale integrato nella tua posta.",
"demo_banner_title": "Controlli demo",
- "demo_banner_desc": "Sei in modalità demo — tutto rimane nel tuo browser. Premi 'Reimposta Demo' in qualsiasi momento per ricominciare con dati puliti.",
+ "demo_banner_desc": "Sei in modalità demo - tutto rimane nel tuo browser. Premi 'Reimposta Demo' in qualsiasi momento per ricominciare con dati puliti.",
"quota_title": "Utilizzo dello spazio",
"quota_desc": "Monitora le dimensioni della tua casella qui. Il cerchio si riempie man mano che utilizzi più spazio."
},
diff --git a/locales/ja/common.json b/locales/ja/common.json
index e074a494..d7c2b121 100644
--- a/locales/ja/common.json
+++ b/locales/ja/common.json
@@ -45,11 +45,11 @@
"imprint": "サイト運営者情報",
"privacy_policy": "プライバシーポリシー",
"try_demo": "デモを試す",
- "demo_description": "サンプルデータで探索 — アカウント不要",
+ "demo_description": "サンプルデータで探索 - アカウント不要",
"demo_launching": "デモを起動中...",
"demo_login_button": "デモを開始",
"demo_tagline": "フル機能のメールクライアントを体験。アカウント不要。",
- "demo_no_signup": "登録不要 — サンプルデータで自由に探索",
+ "demo_no_signup": "登録不要 - サンプルデータで自由に探索",
"oauth_completing": "サインイン処理中...",
"oauth_error": {
"title": "認証に失敗しました",
@@ -1166,7 +1166,7 @@
"warnings": {
"end_before_start": "終了日は開始日より後に設定してください",
"start_in_past": "開始日が過去の日付です",
- "empty_body": "メッセージ本文が空です — 受信者には空の返信が届きます"
+ "empty_body": "メッセージ本文が空です - 受信者には空の返信が届きます"
}
},
"folders": {
@@ -2533,7 +2533,7 @@
"create_event_title": "イベントを作成",
"create_event_desc": "このボタンをクリックして新しいカレンダーイベントを作成します。タイトル、日付、時間を設定し、参加者を追加できます。",
"event_modal_title": "イベント詳細",
- "event_modal_desc": "イベントフォームです。タイトルを入力し、日時を選択し、場所や参加者を追加してください。完了したら保存をクリック — または閉じて次に進みましょう。",
+ "event_modal_desc": "イベントフォームです。タイトルを入力し、日時を選択し、場所や参加者を追加してください。完了したら保存をクリック - または閉じて次に進みましょう。",
"contacts_list_title": "連絡先一覧",
"contacts_list_desc": "連絡先の一覧です。連絡先をクリックすると右側に詳細が表示されます。新しい連絡先の作成、vCardのインポート、グループへの整理もできます。",
"files_title": "ファイルストレージ",
diff --git a/locales/lv/common.json b/locales/lv/common.json
index 2644f429..ed3a3fb3 100644
--- a/locales/lv/common.json
+++ b/locales/lv/common.json
@@ -45,11 +45,11 @@
"imprint": "Rekvizīti",
"privacy_policy": "Privātuma politika",
"try_demo": "Izmēģināt demo",
- "demo_description": "Izpētiet ar paraugdatiem — konts nav nepieciešams",
+ "demo_description": "Izpētiet ar paraugdatiem - konts nav nepieciešams",
"demo_launching": "Demo palaišana...",
"demo_login_button": "Palaist demo",
"demo_tagline": "Izmēģiniet pilnvērtīgu e-pasta klientu bez konta izveides.",
- "demo_no_signup": "Reģistrācija nav nepieciešama — brīvi pētiet paraugdatus",
+ "demo_no_signup": "Reģistrācija nav nepieciešama - brīvi pētiet paraugdatus",
"oauth_completing": "Pabeidz pierakstīšanos...",
"oauth_error": {
"title": "Autentifikācijas kļūda",
@@ -497,7 +497,7 @@
"smime_sign_off": "Iespējot S/MIME parakstu",
"smime_encrypt_on": "S/MIME šifrēšana iespējota",
"smime_encrypt_off": "Iespējot S/MIME šifrēšanu",
- "smime_encrypt_unavailable": "S/MIME šifrēšana nav pieejama — trūkst saņēmēja sertifikāta",
+ "smime_encrypt_unavailable": "S/MIME šifrēšana nav pieejama - trūkst saņēmēja sertifikāta",
"smime_unlock_title": "Atbloķēt S/MIME atslēgu",
"smime_unlock_message": "Ievadiet paroli, lai atbloķētu savu S/MIME parakstīšanas atslēgu.",
"smime_unlock_button": "Atbloķēt",
@@ -1166,7 +1166,7 @@
"warnings": {
"end_before_start": "Beigu datumam jābūt vēlāk par sākuma datumu",
"start_in_past": "Sākuma datums ir pagātnē",
- "empty_body": "Ziņojuma teksts ir tukšs — saņēmēji saņems tukšu atbildi"
+ "empty_body": "Ziņojuma teksts ir tukšs - saņēmēji saņems tukšu atbildi"
}
},
"folders": {
@@ -2537,11 +2537,11 @@
"contacts_list_title": "Jūsu kontakti",
"contacts_list_desc": "Skatīt un rediģēt kontaktus, importēt vCard vai veidot grupas.",
"settings_tabs_title": "Iestatījumu izvēlne",
- "settings_tabs_desc": "Šeit atradīsiet visas iestatījumu kategorijas — no izskata līdz drošībai.",
+ "settings_tabs_desc": "Šeit atradīsiet visas iestatījumu kategorijas - no izskata līdz drošībai.",
"files_title": "Failu krātuve",
"files_desc": "Failu pārvaldnieks ļauj augšupielādēt un organizēt failus kā personīgo mākoņkrātuvi.",
"demo_banner_title": "Demo vadība",
- "demo_banner_desc": "Jūs esat demo režīmā — visi dati paliek jūsu pārlūkā. Nospiediet «Atiestatīt demo», lai sāktu no jauna.",
+ "demo_banner_desc": "Jūs esat demo režīmā - visi dati paliek jūsu pārlūkā. Nospiediet «Atiestatīt demo», lai sāktu no jauna.",
"quota_title": "Krātuves izmantošana",
"quota_desc": "Sekojiet līdzi savas pastkastes aizpildījumam šeit."
},
diff --git a/locales/nl/common.json b/locales/nl/common.json
index e8f72b84..7a65ce02 100644
--- a/locales/nl/common.json
+++ b/locales/nl/common.json
@@ -45,11 +45,11 @@
"imprint": "Colofon",
"privacy_policy": "Privacybeleid",
"try_demo": "Demo proberen",
- "demo_description": "Verken met voorbeeldgegevens — geen account nodig",
+ "demo_description": "Verken met voorbeeldgegevens - geen account nodig",
"demo_launching": "Demo starten...",
"demo_login_button": "Demo starten",
"demo_tagline": "Ervaar een complete e-mailclient. Geen account nodig.",
- "demo_no_signup": "Geen registratie nodig — verken vrij met voorbeeldgegevens",
+ "demo_no_signup": "Geen registratie nodig - verken vrij met voorbeeldgegevens",
"oauth_completing": "Aanmelding voltooien...",
"oauth_error": {
"title": "Authenticatie mislukt",
@@ -1166,7 +1166,7 @@
"warnings": {
"end_before_start": "De einddatum moet na de startdatum liggen",
"start_in_past": "De startdatum ligt in het verleden",
- "empty_body": "De berichttekst is leeg — ontvangers krijgen een leeg antwoord"
+ "empty_body": "De berichttekst is leeg - ontvangers krijgen een leeg antwoord"
}
},
"folders": {
@@ -2306,7 +2306,7 @@
},
"demo_welcome": {
"title": "Welkom bij Bulwark Mail",
- "description": "Ontdek een volwaardige webmail-client — rechtstreeks in je browser. Alle gegevens blijven op je apparaat, dus test gerust alles.",
+ "description": "Ontdek een volwaardige webmail-client - rechtstreeks in je browser. Alle gegevens blijven op je apparaat, dus test gerust alles.",
"feature_email": "E-mails lezen en schrijven",
"feature_organize": "Tags, sterren en mappen",
"feature_shortcuts": "Sneltoetsen",
@@ -2533,15 +2533,15 @@
"create_event_title": "Evenement aanmaken",
"create_event_desc": "Klik op deze knop om een nieuw agenda-evenement aan te maken. U kunt een titel, datum, tijd en deelnemers instellen.",
"event_modal_title": "Evenementdetails",
- "event_modal_desc": "Hier is het evenementformulier. Vul de titel in, kies een datum en tijd, voeg een locatie of deelnemers toe. Klik op opslaan als u klaar bent — of sluit het en ga verder.",
+ "event_modal_desc": "Hier is het evenementformulier. Vul de titel in, kies een datum en tijd, voeg een locatie of deelnemers toe. Klik op opslaan als u klaar bent - of sluit het en ga verder.",
"contacts_list_title": "Uw contacten",
"contacts_list_desc": "Hier zijn uw contacten. Klik op een contact om de details rechts te bekijken. U kunt ook nieuwe contacten aanmaken, vCards importeren of contacten in groepen organiseren.",
"files_title": "Bestandsopslag",
"settings_tabs_title": "Instellingenmenu",
"settings_tabs_desc": "Hier vindt u alle instellingscategorieën. Pas het uiterlijk aan, beheer identiteiten, stel e-mailfilters in, configureer uw agenda en nog veel meer.",
- "files_desc": "Uw bestandsbrowser laat u bestanden uploaden, organiseren en delen — als een persoonlijke cloud geïntegreerd in uw mail.",
+ "files_desc": "Uw bestandsbrowser laat u bestanden uploaden, organiseren en delen - als een persoonlijke cloud geïntegreerd in uw mail.",
"demo_banner_title": "Demo-bediening",
- "demo_banner_desc": "U bent in demomodus — alles blijft in uw browser. Klik op 'Demo resetten' om opnieuw te beginnen met schone voorbeeldgegevens.",
+ "demo_banner_desc": "U bent in demomodus - alles blijft in uw browser. Klik op 'Demo resetten' om opnieuw te beginnen met schone voorbeeldgegevens.",
"quota_title": "Opslaggebruik",
"quota_desc": "Volg de grootte van uw mailbox hier. De cirkel vult zich naarmate u meer ruimte gebruikt."
},
diff --git a/locales/pl/common.json b/locales/pl/common.json
index 7edc21b5..ca830dc6 100644
--- a/locales/pl/common.json
+++ b/locales/pl/common.json
@@ -45,11 +45,11 @@
"imprint": "Impressum",
"privacy_policy": "Polityka prywatności",
"try_demo": "Wypróbuj demo",
- "demo_description": "Poznaj aplikację z przykładowymi danymi — bez potrzeby zakładania konta",
+ "demo_description": "Poznaj aplikację z przykładowymi danymi - bez potrzeby zakładania konta",
"demo_launching": "Uruchamianie demo...",
"demo_login_button": "Uruchom demo",
"demo_tagline": "Wypróbuj w pełni funkcjonalnego klienta poczty. Konto nie jest wymagane.",
- "demo_no_signup": "Bez rejestracji — eksploruj swobodnie z przykładowymi danymi",
+ "demo_no_signup": "Bez rejestracji - eksploruj swobodnie z przykładowymi danymi",
"oauth_completing": "Kończenie logowania...",
"oauth_error": {
"title": "Uwierzytelnianie nie powiodło się",
@@ -1166,7 +1166,7 @@
"warnings": {
"end_before_start": "Data zakończenia musi być późniejsza niż data rozpoczęcia",
"start_in_past": "Data rozpoczęcia jest w przeszłości",
- "empty_body": "Treść wiadomości jest pusta — odbiorcy otrzymają pustą odpowiedź"
+ "empty_body": "Treść wiadomości jest pusta - odbiorcy otrzymają pustą odpowiedź"
}
},
"folders": {
@@ -2306,7 +2306,7 @@
},
"demo_welcome": {
"title": "Witaj w Bulwark Mail",
- "description": "Poznaj w pełni funkcjonalnego klienta poczty webowej — bezpośrednio w przeglądarce. Wszystkie dane pozostają na Twoim urządzeniu, więc możesz śmiało wszystko przetestować.",
+ "description": "Poznaj w pełni funkcjonalnego klienta poczty webowej - bezpośrednio w przeglądarce. Wszystkie dane pozostają na Twoim urządzeniu, więc możesz śmiało wszystko przetestować.",
"feature_email": "Czytaj i pisz wiadomości e-mail",
"feature_organize": "Etykiety, gwiazdki i foldery",
"feature_shortcuts": "Skróty klawiszowe",
@@ -2533,15 +2533,15 @@
"create_event_title": "Utwórz wydarzenie",
"create_event_desc": "Kliknij ten przycisk, aby utworzyć nowe wydarzenie w kalendarzu. Możesz ustawić tytuł, datę, godzinę i dodać uczestników.",
"event_modal_title": "Szczegóły wydarzenia",
- "event_modal_desc": "To jest formularz wydarzenia. Uzupełnij tytuł, wybierz datę i godzinę, dodaj lokalizację albo uczestników. Gdy skończysz, kliknij zapisz — albo zamknij okno i przejdź dalej.",
+ "event_modal_desc": "To jest formularz wydarzenia. Uzupełnij tytuł, wybierz datę i godzinę, dodaj lokalizację albo uczestników. Gdy skończysz, kliknij zapisz - albo zamknij okno i przejdź dalej.",
"contacts_list_title": "Twoje kontakty",
"contacts_list_desc": "Tutaj są Twoje kontakty. Kliknij dowolny kontakt, aby zobaczyć jego pełne dane po prawej stronie. Możesz też tworzyć nowe kontakty, importować vCardy i organizować kontakty w grupy.",
"settings_tabs_title": "Menu ustawień",
"settings_tabs_desc": "Tutaj znajdują się wszystkie kategorie ustawień. Dostosuj wygląd, zarządzaj tożsamościami, skonfiguruj filtry poczty, ustawienia kalendarza i wiele więcej.",
"files_title": "Przechowywanie plików",
- "files_desc": "Przeglądarka plików pozwala przesyłać, porządkować i udostępniać pliki — jak osobisty dysk w chmurze wbudowany w Twoją pocztę.",
+ "files_desc": "Przeglądarka plików pozwala przesyłać, porządkować i udostępniać pliki - jak osobisty dysk w chmurze wbudowany w Twoją pocztę.",
"demo_banner_title": "Sterowanie wersją demo",
- "demo_banner_desc": "Jesteś w trybie demo — wszystko pozostaje w Twojej przeglądarce. W każdej chwili kliknij „Reset Demo”, aby zacząć od nowa z czystymi danymi przykładowymi.",
+ "demo_banner_desc": "Jesteś w trybie demo - wszystko pozostaje w Twojej przeglądarce. W każdej chwili kliknij „Reset Demo”, aby zacząć od nowa z czystymi danymi przykładowymi.",
"quota_title": "Wykorzystanie miejsca",
"quota_desc": "Tutaj możesz śledzić rozmiar swojej skrzynki pocztowej. Okrąg wypełnia się wraz ze wzrostem użycia przestrzeni."
},
diff --git a/locales/pt/common.json b/locales/pt/common.json
index 4f0fd67e..63cd2c9a 100644
--- a/locales/pt/common.json
+++ b/locales/pt/common.json
@@ -45,11 +45,11 @@
"imprint": "Informações legais",
"privacy_policy": "Política de privacidade",
"try_demo": "Experimentar demo",
- "demo_description": "Explore com dados de exemplo — sem conta necessária",
+ "demo_description": "Explore com dados de exemplo - sem conta necessária",
"demo_launching": "Iniciando demo...",
"demo_login_button": "Iniciar demo",
"demo_tagline": "Experimente um cliente de e-mail completo. Sem necessidade de conta.",
- "demo_no_signup": "Sem registo — explore livremente com dados de exemplo",
+ "demo_no_signup": "Sem registo - explore livremente com dados de exemplo",
"oauth_completing": "Concluindo login...",
"oauth_error": {
"title": "Falha na autenticação",
@@ -1166,7 +1166,7 @@
"warnings": {
"end_before_start": "A data de fim deve ser posterior à data de início",
"start_in_past": "A data de início está no passado",
- "empty_body": "O corpo da mensagem está vazio — os destinatários receberão uma resposta em branco"
+ "empty_body": "O corpo da mensagem está vazio - os destinatários receberão uma resposta em branco"
}
},
"folders": {
@@ -2306,7 +2306,7 @@
},
"demo_welcome": {
"title": "Bem-vindo ao Bulwark Mail",
- "description": "Explore um cliente de webmail completo — diretamente no seu navegador. Todos os dados ficam no seu dispositivo, então teste tudo.",
+ "description": "Explore um cliente de webmail completo - diretamente no seu navegador. Todos os dados ficam no seu dispositivo, então teste tudo.",
"feature_email": "Ler e escrever e-mails",
"feature_organize": "Tags, estrelas e pastas",
"feature_shortcuts": "Atalhos de teclado",
@@ -2533,15 +2533,15 @@
"create_event_title": "Criar um evento",
"create_event_desc": "Clique neste botão para criar um novo evento no calendário. Você pode definir um título, data, hora e adicionar participantes.",
"event_modal_title": "Detalhes do evento",
- "event_modal_desc": "Aqui está o formulário do evento. Preencha o título, escolha uma data e hora, adicione um local ou participantes. Clique em salvar quando terminar — ou feche e siga em frente.",
+ "event_modal_desc": "Aqui está o formulário do evento. Preencha o título, escolha uma data e hora, adicione um local ou participantes. Clique em salvar quando terminar - ou feche e siga em frente.",
"contacts_list_title": "Seus contatos",
"contacts_list_desc": "Aqui estão seus contatos. Clique em qualquer contato para ver seus detalhes à direita. Você também pode criar novos contatos, importar vCards ou organizar contatos em grupos.",
"files_title": "Armazenamento de ficheiros",
"settings_tabs_title": "Menu de configurações",
"settings_tabs_desc": "Aqui estão todas as categorias de configurações. Personalize a aparência, gerencie identidades, configure filtros de e-mail, ajuste o calendário e muito mais.",
- "files_desc": "O navegador de ficheiros permite carregar, organizar e partilhar ficheiros — como uma nuvem pessoal integrada no seu e-mail.",
+ "files_desc": "O navegador de ficheiros permite carregar, organizar e partilhar ficheiros - como uma nuvem pessoal integrada no seu e-mail.",
"demo_banner_title": "Controlos de demonstração",
- "demo_banner_desc": "Está no modo de demonstração — tudo permanece no seu navegador. Clique em 'Repor Demonstração' a qualquer momento para recomeçar com dados limpos.",
+ "demo_banner_desc": "Está no modo de demonstração - tudo permanece no seu navegador. Clique em 'Repor Demonstração' a qualquer momento para recomeçar com dados limpos.",
"quota_title": "Utilização do armazenamento",
"quota_desc": "Acompanhe o tamanho da sua caixa de correio aqui. O círculo preenche-se à medida que utiliza mais espaço."
},
diff --git a/locales/ru/common.json b/locales/ru/common.json
index b6a3d51a..6e6c6e0f 100644
--- a/locales/ru/common.json
+++ b/locales/ru/common.json
@@ -45,11 +45,11 @@
"imprint": "Выходные данные",
"privacy_policy": "Политика конфиденциальности",
"try_demo": "Попробовать демо",
- "demo_description": "Исследуйте с примерными данными — аккаунт не нужен",
+ "demo_description": "Исследуйте с примерными данными - аккаунт не нужен",
"demo_launching": "Запуск демо...",
"demo_login_button": "Запустить демо",
"demo_tagline": "Попробуйте полнофункциональный почтовый клиент. Аккаунт не нужен.",
- "demo_no_signup": "Регистрация не требуется — исследуйте свободно с примерными данными",
+ "demo_no_signup": "Регистрация не требуется - исследуйте свободно с примерными данными",
"oauth_completing": "Завершение входа...",
"oauth_error": {
"title": "Ошибка аутентификации",
@@ -497,7 +497,7 @@
"smime_sign_off": "Включить подпись S/MIME",
"smime_encrypt_on": "Шифрование S/MIME включено",
"smime_encrypt_off": "Включить шифрование S/MIME",
- "smime_encrypt_unavailable": "Шифрование S/MIME недоступно — отсутствует сертификат получателя",
+ "smime_encrypt_unavailable": "Шифрование S/MIME недоступно - отсутствует сертификат получателя",
"smime_unlock_title": "Разблокировать ключ S/MIME",
"smime_unlock_message": "Введите парольную фразу для разблокировки вашего ключа подписи S/MIME.",
"smime_unlock_button": "Разблокировать",
@@ -1166,7 +1166,7 @@
"warnings": {
"end_before_start": "Дата окончания должна быть позже даты начала",
"start_in_past": "Дата начала в прошлом",
- "empty_body": "Текст сообщения пуст — получатели получат пустой ответ"
+ "empty_body": "Текст сообщения пуст - получатели получат пустой ответ"
}
},
"folders": {
@@ -2306,7 +2306,7 @@
},
"demo_welcome": {
"title": "Добро пожаловать в Bulwark Mail",
- "description": "Откройте для себя полнофункциональный веб-клиент электронной почты — прямо в браузере. Все данные хранятся на вашем устройстве, поэтому вы можете смело всё проверить.",
+ "description": "Откройте для себя полнофункциональный веб-клиент электронной почты - прямо в браузере. Все данные хранятся на вашем устройстве, поэтому вы можете смело всё проверить.",
"feature_email": "Чтение и написание писем",
"feature_organize": "Метки, звёздочки и папки",
"feature_shortcuts": "Сочетания клавиш",
@@ -2533,15 +2533,15 @@
"create_event_title": "Создать событие",
"create_event_desc": "Нажмите эту кнопку для создания нового события. Вы можете указать название, дату, время и добавить участников.",
"event_modal_title": "Детали события",
- "event_modal_desc": "Это форма события. Заполните название, выберите дату и время, добавьте место или участников. Нажмите «Сохранить» после завершения — или закройте и продолжайте.",
+ "event_modal_desc": "Это форма события. Заполните название, выберите дату и время, добавьте место или участников. Нажмите «Сохранить» после завершения - или закройте и продолжайте.",
"contacts_list_title": "Ваши контакты",
"contacts_list_desc": "Здесь ваши контакты. Нажмите на контакт для просмотра подробностей справа. Вы также можете создавать новые контакты, импортировать vCard или организовывать контакты в группы.",
"settings_tabs_title": "Меню настроек",
"settings_tabs_desc": "Здесь все категории настроек. Настройте внешний вид, управляйте идентификациями, настраивайте фильтры, конфигурируйте календарь и многое другое.",
"files_title": "Хранилище файлов",
- "files_desc": "Файловый менеджер позволяет загружать, организовывать и делиться файлами — как личное облачное хранилище в вашей почте.",
+ "files_desc": "Файловый менеджер позволяет загружать, организовывать и делиться файлами - как личное облачное хранилище в вашей почте.",
"demo_banner_title": "Элементы управления демо",
- "demo_banner_desc": "Вы в демо-режиме — всё остаётся в вашем браузере. Нажмите «Сбросить демо» в любое время, чтобы начать заново с чистыми данными.",
+ "demo_banner_desc": "Вы в демо-режиме - всё остаётся в вашем браузере. Нажмите «Сбросить демо» в любое время, чтобы начать заново с чистыми данными.",
"quota_title": "Использование хранилища",
"quota_desc": "Отслеживайте размер вашего почтового ящика здесь. Круг заполняется по мере использования пространства."
},
diff --git a/locales/uk/common.json b/locales/uk/common.json
index 1c693282..9717de14 100644
--- a/locales/uk/common.json
+++ b/locales/uk/common.json
@@ -45,11 +45,11 @@
"imprint": "Вихідні дані",
"privacy_policy": "Політика конфіденційності",
"try_demo": "Спробуйте демо",
- "demo_description": "Досліджуйте за зразками даних — обліковий запис не потрібен",
+ "demo_description": "Досліджуйте за зразками даних - обліковий запис не потрібен",
"demo_launching": "Запуск демо...",
"demo_login_button": "Запустити демонстрацію",
"demo_tagline": "Спробуйте повнофункціональний поштовий клієнт. Обліковий запис не потрібен.",
- "demo_no_signup": "Реєстрація не потрібна — вільно досліджуйте з зразками даних",
+ "demo_no_signup": "Реєстрація не потрібна - вільно досліджуйте з зразками даних",
"oauth_completing": "Завершення входу...",
"oauth_error": {
"title": "Помилка автентифікації",
@@ -923,7 +923,7 @@
"close": "Закрити",
"invalid_email": "Введіть дійсну електронну адресу",
"already_added": "Цей відправник уже є надійним",
- "save_error": "Не вдалося зберегти — подробиці перевірте в журналі налагодження Контактів",
+ "save_error": "Не вдалося зберегти - подробиці перевірте в журналі налагодження Контактів",
"use_address_book_label": "Синхронізація з адресною книгою",
"use_address_book_description": "Зберігайте довірених відправників у спеціальній адресній книзі «Надійні відправники», щоб вони синхронізувалися на всіх ваших пристроях"
},
@@ -1166,7 +1166,7 @@
"warnings": {
"end_before_start": "Дата завершення має бути після дати початку",
"start_in_past": "Дата початку в минулому",
- "empty_body": "Тіло повідомлення порожнє — одержувачі отримають порожню відповідь"
+ "empty_body": "Тіло повідомлення порожнє - одержувачі отримають порожню відповідь"
}
},
"folders": {
@@ -2306,7 +2306,7 @@
},
"demo_welcome": {
"title": "Ласкаво просимо до Bulwark Mail",
- "description": "Відкрийте для себе повнофункціональний клієнт веб-пошти — прямо у вашому браузері. Усі дані залишаються на вашому пристрої, тому сміливо тестуйте все.",
+ "description": "Відкрийте для себе повнофункціональний клієнт веб-пошти - прямо у вашому браузері. Усі дані залишаються на вашому пристрої, тому сміливо тестуйте все.",
"feature_email": "Читайте та створюйте електронний лист",
"feature_organize": "Теги, зірочки та папки",
"feature_shortcuts": "Комбінації клавіш",
@@ -2533,7 +2533,7 @@
"create_event_title": "Створіть подію",
"create_event_desc": "Натисніть цю кнопку, щоб створити нову подію календаря. Ви можете встановити назву, дату, час і додати учасників.",
"event_modal_title": "Деталі події",
- "event_modal_desc": "Ось форма події. Введіть назву, виберіть дату та час, додайте місце чи учасників. Натисніть «Зберегти», коли закінчите — або закрийте його та йдіть далі.",
+ "event_modal_desc": "Ось форма події. Введіть назву, виберіть дату та час, додайте місце чи учасників. Натисніть «Зберегти», коли закінчите - або закрийте його та йдіть далі.",
"contacts_list_title": "Ваші контакти",
"contacts_list_desc": "Ось ваші контакти. Клацніть будь-який контакт, щоб побачити його повну інформацію праворуч. Ви також можете створювати нові контакти, імпортувати vCards або організовувати контакти в групи.",
"settings_tabs_title": "Меню налаштувань",
@@ -2541,7 +2541,7 @@
"files_title": "Зберігання файлів",
"files_desc": "Ваш файловий браузер дозволяє завантажувати, упорядковувати та ділитися файлами, як особистий хмарний диск, вбудований у вашу пошту.",
"demo_banner_title": "Демонстраційні елементи керування",
- "demo_banner_desc": "Ви в демонстраційному режимі — все залишається у вашому браузері. Будь-коли натисніть «Скинути демонстрацію», щоб почати заново з чистими зразками даних.",
+ "demo_banner_desc": "Ви в демонстраційному режимі - все залишається у вашому браузері. Будь-коли натисніть «Скинути демонстрацію», щоб почати заново з чистими зразками даних.",
"quota_title": "Використання сховища",
"quota_desc": "Відстежуйте розмір своєї поштової скриньки тут. Коло заповнюється, коли ви використовуєте більше місця."
},
diff --git a/locales/zh/common.json b/locales/zh/common.json
index 78103790..d32140ce 100644
--- a/locales/zh/common.json
+++ b/locales/zh/common.json
@@ -1166,7 +1166,7 @@
"warnings": {
"end_before_start": "结束日期必须晚于开始日期",
"start_in_past": "开始日期已在过去",
- "empty_body": "邮件正文为空——收件人将收到空白回复"
+ "empty_body": "邮件正文为空--收件人将收到空白回复"
}
},
"folders": {
@@ -2306,7 +2306,7 @@
},
"demo_welcome": {
"title": "欢迎使用 Bulwark Mail",
- "description": "探索功能齐全的网页邮件客户端——就在您的浏览器中。所有数据都保留在您的设备上,请随意体验所有功能。",
+ "description": "探索功能齐全的网页邮件客户端--就在您的浏览器中。所有数据都保留在您的设备上,请随意体验所有功能。",
"feature_email": "阅读和撰写邮件",
"feature_organize": "标签、星标和文件夹",
"feature_shortcuts": "键盘快捷键",
diff --git a/package.json b/package.json
index 96deeddd..c54bcf00 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "bulwark-webmail",
"version": "1.4.14",
- "description": "Bulwark Webmail — a modern webmail client built for Stalwart Mail Server",
+ "description": "Bulwark Webmail - a modern webmail client built for Stalwart Mail Server",
"author": "Bulwark Webmail ",
"license": "AGPL-3.0-only",
"repository": {
diff --git a/proxy.ts b/proxy.ts
index a9ed124d..ba784c69 100644
--- a/proxy.ts
+++ b/proxy.ts
@@ -30,12 +30,12 @@ export function proxy(request: NextRequest) {
`frame-ancestors ${frameAncestors}`,
].join("; ");
- // Skip intl middleware for /admin routes — they have their own layout
+ // Skip intl middleware for /admin routes - they have their own layout
const pathname = request.nextUrl.pathname;
const isAdminRoute = pathname === '/admin' || pathname.startsWith('/admin/');
// When localePrefix is 'always', paths that already have a locale prefix
- // (e.g. /en/settings) should not be re-processed by the intl middleware —
+ // (e.g. /en/settings) should not be re-processed by the intl middleware -
// doing so can trigger rewrite loops when combined with a proxy basePath.
const locales = routing.locales as readonly string[];
const hasLocalePrefix = locales.some(
diff --git a/stores/account-store.ts b/stores/account-store.ts
index 74d618ad..2bb8759e 100644
--- a/stores/account-store.ts
+++ b/stores/account-store.ts
@@ -61,7 +61,7 @@ export const useAccountStore = create()(
const id = generateAccountId(entry.username, entry.serverUrl);
if (state.accounts.some((a) => a.id === id)) {
- // Already exists — update mutable fields and return existing id
+ // Already exists - update mutable fields and return existing id
set((s) => ({
accounts: s.accounts.map((a) =>
a.id === id
diff --git a/stores/auth-store.ts b/stores/auth-store.ts
index 9f7c3a00..0ca76acd 100644
--- a/stores/auth-store.ts
+++ b/stores/auth-store.ts
@@ -430,7 +430,7 @@ export const useAuthStore = create()(
if (!upgradedToOAuth) {
const { useTotpReauthStore } = await import('@/stores/totp-reauth-store');
client.enableTotpReauth(password, () => useTotpReauthStore.getState().requestTotp());
- debug.log('auth', 'TOTP re-auth enabled — user will be prompted for fresh codes on session expiry');
+ debug.log('auth', 'TOTP re-auth enabled - user will be prompted for fresh codes on session expiry');
}
}
@@ -920,7 +920,7 @@ export const useAuthStore = create()(
const remainingAccounts = accountStore.accounts;
if (remainingAccounts.length > 0 && !wasDemoMode) {
- // Switch to the next account — this is the one path that stays in-app
+ // Switch to the next account - this is the one path that stays in-app
const nextAccount = remainingAccounts[0];
clearAllStores();
@@ -955,7 +955,7 @@ export const useAuthStore = create()(
}).catch((err) => debug.error('Failed to load identities after switch:', err));
}
} else {
- // Client not in memory — clear everything and redirect.
+ // Client not in memory - clear everything and redirect.
// Trying to async-restore during logout caused the original bug.
debug.error(`Cannot restore next account ${nextAccount.id}, performing full logout`);
evictAccount(nextAccount.id);
@@ -971,12 +971,12 @@ export const useAuthStore = create()(
return;
}
- // No accounts remaining (or demo mode) — full logout + redirect
+ // No accounts remaining (or demo mode) - full logout + redirect
performFullLogout(set);
notifyParent('sso:logout');
- // Background cookie/token cleanup — keepalive ensures completion during navigation
+ // Background cookie/token cleanup - keepalive ensures completion during navigation
if (!wasDemoMode) {
apiFetch(`/api/auth/session?slot=${slot}`, { method: 'DELETE', keepalive: true }).catch(() => {});
if (wasOAuth) {
@@ -984,7 +984,7 @@ export const useAuthStore = create()(
}
}
- // Redirect to login — this is synchronous and happens AFTER all state is cleared
+ // Redirect to login - this is synchronous and happens AFTER all state is cleared
redirectToLogin();
},
@@ -1039,7 +1039,7 @@ export const useAuthStore = create()(
let targetRestoreRateLimited = false;
if (!targetClient) {
- // Client not connected — try to restore
+ // Client not connected - try to restore
try {
if (targetAccount.authMode === 'oauth') {
const res = await apiFetch(`/api/auth/token?slot=${targetAccount.cookieSlot}`, { method: 'PUT' });
@@ -1105,7 +1105,7 @@ export const useAuthStore = create()(
return;
}
- // Cannot restore — remove the stale account and redirect to login
+ // Cannot restore - remove the stale account and redirect to login
evictAccount(accountId);
accountStore.removeAccount(accountId);
apiFetch(`/api/auth/session?slot=${targetAccount.cookieSlot}`, { method: 'DELETE' }).catch(() => {});
@@ -1239,7 +1239,7 @@ export const useAuthStore = create()(
throw new Error(`Session cookie missing: ${res.status}`);
}
} else {
- // Basic auth without rememberMe — can't restore
+ // Basic auth without rememberMe - can't restore
throw new Error('No saved session');
}
} catch (err) {
@@ -1523,7 +1523,7 @@ export const useAuthStore = create()(
const { identities, primaryIdentity } = loadIdentities(rawIdentities, username);
set({ identities, primaryIdentity });
} catch {
- // Silently fail — background sync should not surface errors to the user
+ // Silently fail - background sync should not surface errors to the user
}
},
@@ -1538,7 +1538,7 @@ export const useAuthStore = create()(
{
name: 'auth-storage',
partialize: (state) => {
- // Don't persist unauthenticated state — prevents resurrecting stale sessions
+ // Don't persist unauthenticated state - prevents resurrecting stale sessions
if (!state.isAuthenticated) return {};
return {
serverUrl: state.serverUrl,
diff --git a/stores/calendar-store.ts b/stores/calendar-store.ts
index 301fc8e2..ab3e3f40 100644
--- a/stores/calendar-store.ts
+++ b/stores/calendar-store.ts
@@ -374,7 +374,7 @@ export const useCalendarStore = create()(
rsvpEvent: async (client, eventId, participantId, status, replyTo) => {
set({ error: null });
- // JMAP participant IDs are opaque strings — they can contain @, ., :, / etc.
+ // JMAP participant IDs are opaque strings - they can contain @, ., :, / etc.
// Only reject empty or obviously malicious values (path traversal).
if (!participantId || participantId.includes('..')) {
set({ error: 'Invalid participant ID' });
@@ -448,15 +448,15 @@ export const useCalendarStore = create()(
for (const e of eventsToProcess) {
if (!e.uid || !uidToEvent.has(e.uid)) {
- // UID doesn't exist on server — create it
+ // UID doesn't exist on server - create it
newEvents.push(e);
} else {
const existing = uidToEvent.get(e.uid)!;
if (existing.calendarIds[realCalendarId]) {
- // Already in target calendar — skip
+ // Already in target calendar - skip
continue;
}
- // Exists in another calendar — link to target calendar
+ // Exists in another calendar - link to target calendar
eventsToLink.push({
eventId: existing.id,
calendarIds: { ...existing.calendarIds, [realCalendarId]: true },
diff --git a/stores/contact-store.ts b/stores/contact-store.ts
index ee80767d..b388d2a9 100644
--- a/stores/contact-store.ts
+++ b/stores/contact-store.ts
@@ -14,7 +14,7 @@ export function getContactDisplayName(contact: ContactCard): string {
const full = [given, surname].filter(Boolean).join(' ');
if (full) return full;
}
- // Fall back to name.full (RFC 9553 — used by Stalwart and other JMAP servers)
+ // Fall back to name.full (RFC 9553 - used by Stalwart and other JMAP servers)
if (contact.name.full) return contact.name.full;
}
if (contact.nicknames) {
diff --git a/stores/email-store.ts b/stores/email-store.ts
index 572912ff..f124b282 100644
--- a/stores/email-store.ts
+++ b/stores/email-store.ts
@@ -1089,7 +1089,7 @@ export const useEmailStore = create((set, get) => ({
return mb.accountId === acctId;
});
if (!trashMailbox) {
- // No trash available for this account — fall back to destroy so the action isn't silently dropped.
+ // No trash available for this account - fall back to destroy so the action isn't silently dropped.
await acctClient.batchDeleteEmails(ids);
return;
}
@@ -1732,7 +1732,7 @@ export const useEmailStore = create((set, get) => ({
receivedAt: new Date(Date.now() - 3600000).toISOString(),
from: [{ name: "Emily Chen", email: "emily.chen@gmail.com" }],
to: [{ email: "you@example.com" }],
- subject: "Re: Dashboard Redesign v2 — feedback",
+ subject: "Re: Dashboard Redesign v2 - feedback",
preview: "Hey! I just pushed the updated mockups to Figma. I incorporated all the feedback from last week's meeting. Let me know what you think about the new nav...",
hasAttachment: true,
},
@@ -1784,7 +1784,7 @@ export const useEmailStore = create((set, get) => ({
receivedAt: new Date(Date.now() - 108000000).toISOString(),
from: [{ name: "Sarah Kim", email: "sarah.kim@proton.me" }],
to: [{ email: "you@example.com" }],
- subject: "Conference talk proposal — need your review",
+ subject: "Conference talk proposal - need your review",
preview: "I'm submitting a talk to ReactConf about our email client architecture. Could you take a look at my abstract before the deadline on Friday?...",
hasAttachment: true,
},
diff --git a/stores/file-store.ts b/stores/file-store.ts
index 5fbabeca..c757458b 100644
--- a/stores/file-store.ts
+++ b/stores/file-store.ts
@@ -224,7 +224,7 @@ export const useFileStore = create((set, get) => ({
try { localStorage.setItem('files-path-stack', JSON.stringify(newStack)); } catch { /* ignore */ }
try {
- // Always fetch all nodes from root — Stalwart doesn't support parentId nesting
+ // Always fetch all nodes from root - Stalwart doesn't support parentId nesting
const allNodes = await client.listFileNodes(null);
const prefix = getPathPrefix(newPath);
const filteredNodes = filterNodesByPrefix(allNodes, prefix);
@@ -389,7 +389,7 @@ export const useFileStore = create((set, get) => ({
try {
await client.createFileDirectory(fullDirName, null);
} catch {
- // Directory may already exist — ignore
+ // Directory may already exist - ignore
}
}
diff --git a/stores/filter-store.ts b/stores/filter-store.ts
index 7d48442b..50c312fc 100644
--- a/stores/filter-store.ts
+++ b/stores/filter-store.ts
@@ -57,7 +57,7 @@ export const useFilterStore = create()((set, get) => ({
const allScripts = await client.getSieveScripts();
debug.log('filters', 'Sieve scripts fetched:', allScripts.length);
- // Skip the server-managed 'vacation' script (RFC 9661 §4) — it can only
+ // Skip the server-managed 'vacation' script (RFC 9661 §4) - it can only
// be modified via VacationResponse/set, not SieveScript/set.
const scripts = allScripts.filter(s => s.name !== 'vacation');
@@ -224,7 +224,7 @@ export const useFilterStore = create()((set, get) => ({
const content = generateScript(rules, vacation.isEnabled ? vacation : undefined, { externalRequires });
if (activeScript) {
- // Preserve the script's current activation state — don't pass activate: true
+ // Preserve the script's current activation state - don't pass activate: true
// unconditionally, as that would deactivate the server-managed 'vacation'
// script and cause VacationResponse/get to return isEnabled: false.
await client.updateSieveScript(activeScript.id, content, activeScript.isActive);
diff --git a/stores/plugin-store.ts b/stores/plugin-store.ts
index 36686d62..55548872 100644
--- a/stores/plugin-store.ts
+++ b/stores/plugin-store.ts
@@ -1,4 +1,4 @@
-// Plugin store — manages installed plugins, slot registrations, and lifecycle
+// Plugin store - manages installed plugins, slot registrations, and lifecycle
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
@@ -274,7 +274,7 @@ export const usePluginStore = create()(
status: p.enabled ? 'enabled' : 'installed',
error: undefined,
})),
- // Don't persist slots — they are runtime-only, rebuilt on load
+ // Don't persist slots - they are runtime-only, rebuilt on load
}),
onRehydrateStorage: () => {
return (state) => {
@@ -381,7 +381,7 @@ async function syncServerPlugins(
const local = get().plugins.find(p => p.id === sp.id);
if (!local) {
- // New server plugin — download and install
+ // New server plugin - download and install
const code = await downloadPluginBundle(sp.id);
if (!code) continue;
@@ -411,7 +411,7 @@ async function syncServerPlugins(
return { plugins: [...state.plugins, plugin] };
});
} else if (local.version !== sp.version) {
- // Version changed — re-download bundle
+ // Version changed - re-download bundle
const code = await downloadPluginBundle(sp.id);
if (!code) continue;
@@ -478,7 +478,7 @@ async function syncServerPlugins(
// Persist current server plugin IDs for future cleanup
setServerManagedPluginIds(serverPluginIds);
} catch {
- // Sync failure is non-fatal — client continues with local plugins
+ // Sync failure is non-fatal - client continues with local plugins
console.warn('[plugin-store] Server plugin sync failed, using local plugins only');
}
}
diff --git a/stores/smime-store.ts b/stores/smime-store.ts
index 078a3adf..280eba61 100644
--- a/stores/smime-store.ts
+++ b/stores/smime-store.ts
@@ -149,7 +149,7 @@ interface SmimeStore extends SmimePersistedState {
// Loaded from IndexedDB
keyRecords: SmimeKeyRecord[];
publicCerts: SmimePublicCert[];
- // Runtime only — never persisted
+ // Runtime only - never persisted
unlockedKeys: Map;
unlockedDecryptionKeys: Map;
unlockedLegacyDecryptionKeys: Map;
@@ -282,7 +282,7 @@ export const useSmimeStore = create()(
set({ isLoading: true, error: null });
try {
const cert = parseCertificatePemOrDer(data);
- // Always re-encode to DER — input might be PEM text (string or ArrayBuffer)
+ // Always re-encode to DER - input might be PEM text (string or ArrayBuffer)
const der = cert.toSchema(true).toBER(false);
const info = await extractCertificateInfo(cert, der);
const email = info.emailAddresses[0] ?? '';