- New stores/signature-store.ts: Zustand persist with CRUD, default/reply
signature IDs, per-identity signature mapping
- New signature-settings.tsx: list management with add/edit/delete/duplicate
- New signature-editor-modal.tsx: TipTap rich text editor for signatures
- email-composer.tsx: auto-insert signature based on mode (compose/reply)
+ signature selector dropdown in toolbar
- identity-form.tsx: per-identity default/reply signature dropdowns
- settings/page.tsx: Signatures tab in Mail settings group
Popovers and dropdown menus across the app (sub-address helper, calendar
toolbar/color pickers, contact/template/attachment menus, rich text editor
color and table pickers, unsubscribe confirmation, composer send menu)
were anchored with physical `left-0`/`right-0`. In RTL locales those
don't flip with the trigger, so the menu detaches from the button that
opened it. Switched to Tailwind's logical `start-0`/`end-0` (and the
matching `rounded-s-*`/`rounded-e-*` corners on hover-action overlays)
so they mirror correctly for RTL locales (ar, he, fa) while staying
identical in LTR.
The default sender identity (`preferredPrimaryId`) lived only in the
browser-local `identity-storage` store and was never written to the synced
settings, so the choice was lost on clearing site data / switching browsers and
never appeared in exported settings.
Persist it in the synced settings store, keyed **per account**
(`preferredIdentityIds: Record<accountId, identityId>`), mirroring the existing
per-account `allMailFolderIds`. Per-account keying is required because JMAP
identity ids are account-scoped and would otherwise collide across accounts /
the unified mailbox.
This supersedes the earlier username-keyed fix that had landed on main: the
username-keyed map, `loadIdentities()` fallback write, and the
`applyPreferredIdentityOrdering` store action (plus its settings-store hook)
are removed so a single account-keyed mechanism remains.
- settings-store: `preferredIdentityIds` (accountId -> identityId) in state,
defaults, export, import (non-record guard), rehydrate coercion, v6 migration.
- auth-store: `applyPreferredIdentity(accountId?)` reorders the active account's
identities once synced settings load, and performs the one-time migration of
the pre-#507 browser-local default into the synced map (keyed by accountId).
Invoked from every `loadFromServer().finally()` (login / OAuth / SSO / switch
/ restore). `loadIdentities()` now only applies the local fallback ordering.
- identity-manager-modal: the star action writes the choice by `activeAccountId`.
- identity-store: `preferredPrimaryId` kept as a local (sync-off) fallback.
- tests: per-account independence, export/import round-trip, import guard, and
applyPreferredIdentity reorder / active-account gating / local-default
migration.
Signatures render into the main document - the identity form's live preview
and the composer's signature block - rather than the sandboxed iframe used for
message bodies. SIGNATURE_SANITIZE_CONFIG allows no target attribute, so those
anchors were live and target-less: one click navigated the whole app away,
discarding the unsent draft or the unsaved signature with it.
Add sanitizeSignatureHtmlForDisplay, which keeps the storage sanitizer's image
restrictions but forces target="_blank" rel="noopener noreferrer" on every
anchor, and use it at the two render sites. The composer's SignatureBlock
NodeView stamps the target on its rendered DOM instead, because attrs.html is
what serializeEditorContent emits into the sent message - storage and the
recipient's copy stay exactly as the user wrote them.
When authenticating with a local-part username (e.g. 'user' instead of
'user@domain.tld') on Stalwart 0.15.x, the default sender could resolve
to an alias identity instead of the canonical mailbox address.
- Add emailMatchesUsername() helper that matches local-part usernames
against full email addresses (e.g. 'user' matches 'user@domain.tld')
- Prefer canonical identities (mayDelete=false) over aliases as tiebreaker
- Add preferredPrimaryId to identity store (persisted to localStorage)
so users can explicitly set their default sender
- Add 'Set as Primary' star button in identity manager modal
- Fix sendEmail() fallback identity resolution for local-part usernames
- Add i18n strings for all 8 supported locales
Fixes#43
- Add syncIdentities() to auth store to propagate identity changes from
identity store, fixing stale data that caused save failures and duplicates
- Call syncIdentities() after every create, update, and delete in the
identity manager modal
- Switch email composer to read identities from identity store for
consistency with the rest of the app
- Append identity text signature (with RFC 3676 separator) to email body
when sending from the composer and quick reply paths
- Add tests for syncIdentities and signature appending logic
Resizable Columns
- Add ResizeHandle component with mouse drag, keyboard (Arrow keys),
and double-click to reset to default width
- Add sidebarWidth/emailListWidth to ui-store with clamping + persistence
- Wire resize handles between sidebar/email-list panels on desktop
Navigation Rail Overhaul
- Move StorageQuotaCircle, push-status, sign-out from Sidebar to NavigationRail
- Interactive SVG ring with popover breakdown (used/free/total)
- Sidebar collapse state lifted to ui-store
- Show total email count per mailbox alongside unread badge
Email Multi-Selection & Drag-and-Drop
- Ctrl+Click (toggle) and Shift+Click (range) on all list items
- Add selectRangeEmails and lastSelectedEmailId to email-store
- Enable drag-and-drop on thread items and thread headers
- useEmailDrag accepts optional threadEmails for full-thread drag
Email Viewer Layout
- Remove card wrapper for cleaner full-width reading
- Always render HTML body when available
- Adjust skeleton loader to match flat layout
Modal & UI Polish
- Standardise backdrops, close buttons, padding, border-radius, transitions
- Migrate template-string classNames to cn() in settings
- Unify focus-ring token to ring-ring on form controls
i18n
- Add storage_used/free/total keys to all 8 locales
Dev Mock JMAP Server (new, gated by DEV_MOCK_JMAP=true)
- Session, Mailbox/Email/Thread/Identity CRUD, back-references, upload
- GET /download with Content-Disposition, GET /eventsource SSE
Tests (46 new)
- ui-store (13), email-selection (10), resize-handle (9), mock-server (14)
- Add NavigationRail component (desktop vertical icon sidebar + mobile bottom tab bar)
- Add ConfirmDialog with promise-based useConfirmDialog hook for async confirmation flow
- Add WelcomeBanner onboarding component (one-time display, localStorage persistence)
- Polish login form UX (shake on error, TOTP slide animation, password visibility toggle, session expired banner)
- Add inline form validation with shake animation in email composer and contacts
- Add empty state patterns for contacts (no data vs no search results with contextual actions)
- Improve toast notification system with undo action support and typed durations
- Add WCAG AA prefers-reduced-motion media query, safe area insets, sr-only live regions
- Add template settings tab and keyboard shortcut integration
- Update all 8 locale translations
This release significantly expands internationalization support and adds comprehensive identity management features.
Internationalization (i18n):
- Add 5 new languages: Spanish, Italian, German, Dutch, Portuguese
- Expand from 3 to 8 total supported languages
- Redesign language switcher for better scalability (dropdown UI)
- Complete translations for all features across all languages
Identity Management:
- Multiple sender identities with per-identity signatures
- Sub-addressing support (user+tag@domain.com)
- Context-aware tag suggestions for sub-addresses
- Identity badges in email viewer and list
- Full CRUD operations for managing identities
Newsletter Management:
- RFC 2369 List-Unsubscribe support (one-click unsubscribe)
- HTTP and mailto unsubscribe methods
- Security validation prevents XSS attacks
- Two-step confirmation with persistent dismissal
Security & Accessibility:
- Dark mode email readability (intelligent color transformation)
- WCAG 2.0 Level AA color contrast compliance
- Comprehensive XSS prevention with validation utilities
- Unit test coverage for security-critical code (57 validation tests)
Testing:
- Add unit tests for validation utilities
- Add unit tests for email sanitization
- Add unit tests for color transformation
- Full test coverage for XSS attack vectors