Contact loading silently returned empty when the address book held
more entries than the server's maxObjectsInGet (Stalwart defaults to
500). Replace the single back-reference get with a two-step flow:
query IDs first, then pack a batched get into one JMAP request. All
batches ride a single HTTP roundtrip via multiple method calls.
Address books at or under the server cap still get a single method
call, so no behavior change for small lists.
Adds getMaxObjectsInGet() capability helper that mirrors the existing
getMaxCallsInRequest() / getMaxSizeUpload() shape. 83 new test cases
cover single-batch, exact-boundary, over-cap, and direct-ids paths.
Closes#45. Credits #46 (@capitanroy).
Defense-in-depth. All current callers pass hardcoded tailwind class
strings, so this is not exploitable today, but a future caller that
forwarded a user-controlled value would get HTML injection through the
class attribute. Run the value through escapeHtml() and add a test
covering the attribute-escape case.
Plain-text email bodies were escaped for <, >, & but not " or ', and the
URL linkifier regex captured every non-whitespace character up to the
next <. A URL containing a double or single quote broke out of the
href attribute in the rendered anchor, allowing arbitrary event handlers
to be injected into otherwise plain-text mail. Reported by @rathlinus.
Extract a shared plainTextToSafeHtml helper in lib/email-sanitization.ts
that escapes all five HTML-significant characters in the correct order
before linkification, and route both email-viewer and thread view
through it. Add tests that parse the output and assert no onmouseover
attribute lands on the anchor element.
Also bump dependencies flagged by npm audit: next 16.2.4 (DoS in Server
Components), next-intl 4.9.1 (open redirect), dompurify 3.4.0
(FORBID_TAGS bypass); picomatch/vite/brace-expansion resolve
transitively. npm audit is clean.
Bump version to 1.4.1.
Apache JAMES requires the type field on textBody parts per strict
RFC 8621, while Stalwart is lenient. This fixes compose failures
on non-Stalwart JMAP backends.
Copy source button now shows a checkmark and "Copied" text for 2s
after clicking. Dark mode email background blend target changed to
perfectly neutral gray (R=G=B) to eliminate perceived color cast on
certain displays.
Emails with embedded images (cid: references) displayed them as
downloadable attachments because the browser cannot resolve cid: URLs
and the attachments were not filtered. Added fetchBlobAsObjectUrl to
the JMAP client, pre-fetch inline images as object URLs, replace cid:
references in sanitized HTML, and filter CID attachments from the
download list in both the email viewer and thread conversation view.
Remove ~60 console.error/log/warn calls from application code that were
leaking implementation details to browser devtools. Errors are already
surfaced to users via toast notifications or state updates.
Add missing rationale to all 9 bare eslint-disable comments so future
readers understand why deps are intentionally excluded.
Per RFC 8621, Identity/get, Identity/set, and EmailSubmission/set require
the submission capability. Missing it caused Stalwart to reject identity
deletion (and potentially other identity operations).
heapTotal is V8's current allocation, not the max. A 35MB process with
37MB allocated reads as 95% when the real limit is ~2GB, causing
container restarts. Also fix stale calendar-participants test expectations.
- Remove isMobile/isTablet JS state from layout classes to eliminate
orientation-change blink (CSS breakpoints apply instantly)
- Extract dismissViewer() helper for return-to-list pattern (was duplicated 4x)
- Convert more-actions dropdown from hover-only to click-toggle (works on touch)
- Add Escape key and role attributes to more-actions menu
- Reset showMoreActions on email change to prevent stale menu
- Move overflow-x:auto to iframe inner body stylesheet (was inert on iframe element)
- Reset tabletListVisible when crossing to desktop breakpoint
P0 Core:
- Sandboxed iframe rendering for rich HTML emails (CSS isolation)
- API retry with exponential backoff for transient JMAP failures
- Mobile bottom action bar with touch-friendly email actions
- Long-press context menu + tap-to-expand submenus on touch devices
P1 Polish:
- Tag counts sidebar section with JMAP batch queries
- Empty folder option for Junk/Trash with batch delete
- Extra-compact density option (28px rows, 44px on touch)
- SPF/DKIM/DMARC security tooltips with plain-language explanations
- Resizable sidebars with drag, touch, and keyboard support
- Expandable sender info panel in email viewer
All 8 locales updated (en/fr/ja/es/it/de/nl/pt).
parseDuration() now handles undefined event.duration gracefully instead
of crashing on .match(). CalendarEvent.duration type updated to reflect
that JMAP servers may omit this field.
Sieve filter activation replaced isActive (server-set per RFC 9661) with
onSuccessActivateScript. Create/update calls merged with activation into
single JMAP roundtrips.
Closes#31, closes#21
The context menu's "Move to folder" submenu was practically unusable:
scroll events inside the folder list closed the entire menu, and moving
the mouse from the trigger to the submenu dismissed it instantly.
Additionally, the JMAP moveEmail call silently discarded server errors
and always used the primary account ID, causing moves to appear to
succeed locally while the server rejected them.
Closesroot-fr/jmap-webmail#19
Basic Auth sessions now persist across page refreshes via an encrypted
httpOnly cookie (AES-256-GCM). The feature is opt-in: admin sets
SESSION_SECRET env var, user checks "Remember me" at login.
Rework the 2FA input from a checkbox+animated panel to a discreet
"I have a 2FA code" text link that reveals the TOTP field on click.
Logout now revokes the refresh token and redirects to the IdP's
end_session_endpoint so the SSO session is fully terminated. The
end_session_url is validated server-side (HTTPS only) before being
returned to the client, preventing open redirect attacks.
Also adds 14 unit tests covering PKCE (including RFC 7636 test vector)
and OAuth discovery (fallback, caching, required field validation).
Support external IdPs (Keycloak, Authentik) that handle auth separately
from the JMAP server via OAUTH_ISSUER_URL. Also surfaces discovery
failures with logged errors and a visible warning banner instead of
silently hiding the SSO button.
Add OAuth2 authorization code flow with PKCE as an alternative to
basic auth. Supports Stalwart's built-in OAuth provider with automatic
token refresh, session persistence via httpOnly cookies, and graceful
fallback to basic auth when OAuth is not configured.
When Stalwart returns session URLs with explicit :443 port, the URL
parser normalizes it out but the original string keeps it, causing
string slicing to produce wrong offsets. Now extracts the path by
finding the first / after // instead of relying on origin length.
Sent emails only included the bare email address in the From field,
so recipients saw <user@example.com> with no display name. Now the
identity name from the JMAP server is included in the From header
for composed emails, quick replies, and saved drafts.
Also sorts identities so the one matching the login username is
selected by default in the composer dropdown.
JMAP servers (e.g. Stalwart behind Docker) may return internal hostnames
in their session response (https://jmap/ instead of the actual server URL).
Replace the origin of all session URLs (apiUrl, downloadUrl, uploadUrl,
eventSourceUrl) with the user-configured server origin, using string
slicing to preserve RFC 6570 template placeholders without encoding.
Fixes#16
When the JMAP server is reachable but lacks CORS headers, users saw
"Unable to reach the server" which is misleading. Now a no-cors probe
distinguishes CORS blocking from true network failure, surfacing a
specific message that points users to their server's CORS config.
Address GitHub issue #13 (events not spanning real duration, hour label alignment)
and add five standard calendar UX features:
- Click-drag on empty time slots to create events with pre-filled time range
- Resize events by dragging bottom edge handle (15-min snap, optimistic JMAP update)
- Recurring event edit/delete scope dialog (this/following/all occurrences)
- Double-click quick create with inline title input (PT1H default)
- Event duplication button in modal (clones +1 day, opens for editing)
Shared interaction logic extracted to hooks/use-time-grid-interactions.ts.
All features include i18n (8 locales), accessibility (ARIA, pointer events),
and proper error handling with toast feedback.
- Add ConfirmDialog component with promise-based useConfirmDialog hook
- Add WelcomeBanner onboarding component with localStorage persistence
- Add NavigationRail (desktop icon rail + mobile bottom tab bar)
- Polish login form (shake on error, TOTP slide, password visibility, 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)
- Improve toast system with undo action support and typed durations
- Add template settings tab to settings page
- Refactor sidebar (cleaner code, remove unused imports)
- Add WCAG AA reduced-motion, safe area insets, sr-only live region
- Add shake/slide animations in globals.css
- Update i18n for all 8 locales
Add organizer/attendee UI with RSVP buttons (accept/maybe/decline),
contact autocomplete in participant input, scheduling message support
via sendSchedulingMessages flag, and DnD notification for participant
events. Add inline calendar invitation banner in email viewer that
auto-detects .ics attachments, parses event details, and allows RSVP
or import to calendar. Includes cancellation display and 51 tests.
Dark mode color transform had 3 bugs preventing it from working:
- Hook only ran when external content was blocked, not for all emails
- Checked `theme` ('system') instead of `resolvedTheme` ('dark')
- Only transformed CSS style attrs, missing <font color> and bgcolor
Replaced invert+boost algorithm with blend-toward-white for text
(preserves hue, brighter results) and added background darkening
for light email backgrounds. Also handles color/bgcolor HTML attrs.
Fixed long email subjects pushing action buttons off-screen by adding
min-w-0 to the email viewer flex container in the page layout.
Evaluate JMAP CalendarEventAlert triggers client-side and display toast
notifications when alert times are reached. Extracts notification sound
into shared utility and mounts ToastContainer globally (fixes silent
toast failures across all components).
- Pure alert utilities: offset parsing, fire time computation (start/end),
effective alerts resolution with useDefaultAlerts, pending alert filtering
- Zustand persist store for acknowledged alert deduplication (24h retention)
- Global hook with 60s interval check, proactive 24h event fetch via ref
(isolated from calendar store), stale closure protection
- Settings: calendarNotificationsEnabled, calendarNotificationSound toggles
- Toast icon customization, CalendarAlertProvider in layout
- i18n: all 8 locales (EN/FR/JA/ES/IT/DE/NL/PT)
- 44 tests (36 alert utils + 8 notification store), 558 total passing
Contacts:
- Contact groups/lists with JMAP members map and composer expansion
- vCard import/export with RFC 6350 parser and duplicate detection
- Bulk operations (multi-select, delete, group add, export)
Search:
- Advanced search panel with JMAP filter fields
- Search chips for active filters visualization
- Debounced inputs with AbortController deduplication
Vacation:
- JMAP VacationResponse singleton management
- Settings tab with date range and message configuration
- Sidebar indicator when vacation responder is active
Auth:
- TOTP 2FA support with Stalwart-compatible password$totp format
Infrastructure:
- Docker multi-stage build with standalone Next.js output
- Structured server-side logger with text/JSON format
- CSP Report-Only and security headers via proxy middleware
- Layout refactoring (HTML structure in root layout)
- Playwright E2E framework setup
Testing: 450+ tests (identity, contacts, vCard, threads, headers, components)
i18n: All new strings added to all 8 locales
Remove width: max-content and display: inline-block from email content
CSS that caused horizontal scroll and left-side text clipping. Collapse
empty table cells/containers when external images are blocked to prevent
large blank areas in newsletter emails.
Implement Phase 1 of contacts support — the biggest missing feature
on the roadmap. Uses RFC 9553 (JSContact) data model and RFC 9610
(JMAP for Contacts) when the server advertises the capability,
falling back to localStorage for servers without contact support.
- JMAP types for ContactCard, AddressBook, NameComponent, etc.
- JMAP client methods: CRUD, search, address book listing
- Refactored request() to accept dynamic `using` capabilities
- Zustand contact store with dual-mode persistence
- Full contacts page with two-column layout (list + detail/edit)
- Contact form with multi-email/phone, context labels, validation
- Composer autocomplete on To/Cc/Bcc with keyboard nav and ARIA
- Sidebar navigation link to contacts
- Auth integration: fetch on login, clear on logout
- i18n: contacts.* namespace across all 8 languages
Emails containing <style> tags were injecting global CSS rules that
affected the entire application UI. For example, button:hover rules
from emails would override all button hover states app-wide.
Changes:
- Block <style> tags in email sanitization config (security fix)
- Keep inline style attributes for element-specific formatting
- Add explicit bg-transparent to banner buttons (defense in depth)
- Add CSS isolation to banner container
- Fix theme dependency in email content memoization
This prevents malicious or poorly-formatted emails from breaking the
UI or being used for phishing via CSS injection attacks.
Implement RFC 2369 List-Unsubscribe support with security validation (protocol whitelist, XSS prevention), two-step confirmation UI, and localStorage persistence. Add intelligent color transformation for dark mode to fix unreadable inline email styles using WCAG 2.0 luminance calculation. Complete batch spam undo operations for context menu multi-select. Update documentation with new features and comprehensive test coverage (97 tests).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implement comprehensive spam management allowing users to report spam emails and restore false positives. Emails are moved to/from the Junk folder with toast notifications and undo support.
Key features:
- Mark as spam: moves email to Junk folder with 5-second undo
- Mark as not spam: restores email from Junk to inbox
- Batch spam operations via context menu
- Smart toggle based on current folder (Junk vs other folders)
- Keyboard shortcut (Shift+!) for quick spam reporting
- Full i18n support (EN/FR)
- Shared mailbox compatibility
UX improvements:
- Toast notifications with undo action using sonner library
- Auto-deselect email after spam action
- Visual distinction with red destructive styling
- Desktop button + context menu + keyboard shortcuts
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Complete internationalization coverage for all identity-related UI elements,
including mobile navigation, batch actions, and recipient formatting. Improve
sub-address badge to detect and display tags for emails received at
user+tag@domain.com addresses, not just sent emails. Add contextual recipient
display showing "me" for current user and "Name and X others" for multiple
recipients. Update documentation to reflect completed identity management
implementation.
- Add useMemo for expensive tag suggestions computation
- Add useCallback for identity CRUD handlers
- Extract magic numbers to constants (MAX_RECENT_TAGS, MAX_TAG_LENGTH)
- Improve JMAP error messages with context
Performance: Reduces unnecessary re-renders in identity modal
Maintainability: Makes limits configurable and self-documenting
UX: Users get clearer error messages when operations fail
No functional changes. All tests passing.
Enable users to manage multiple sending identities and organize incoming mail with sub-addressing (user+tag@domain.com format). Provides better email organization, privacy through disposable addresses, and flexibility for users with multiple roles or accounts.
- Add getIdentities() method to JMAP client
- Fetch and store identities in auth store on login
- Add From selector in composer (dropdown for multiple identities)
- Pass identity email/ID through send flow instead of using username
- Add translations for From field (en/fr)
This allows proper sender address handling for LDAP and other auth
backends where username differs from email address.
Closes#5
- Add pagination support to searchEmails (was limited to 50 results)
- Search now scoped to current mailbox/folder
- Support shared mailbox folders in search
- Display total results count (e.g., "50 of 1400 conversations")
- Add clear (X) button to search input
- Re-run search when changing folders during active search
- Remove unused sidebar screenshot
- Implement thread grouping by threadId in email list
- Add ThreadListItem with collapsed/expanded states for desktop
- Create ThreadConversationView for full-screen mobile experience
- Add thread utility functions for grouping and sorting
- Extend JMAP client with getThread() and getThreadEmails() methods
- Add keyboard shortcut 'x' to expand/collapse threads
- Add thread expansion state management in email store
- Fix React hooks order in login page
- Add .env.example for environment configuration
- Add husky for git hooks
New features:
- Keyboard shortcuts (j/k navigation, r/R/f for reply/forward, s star, e archive, # delete)
- Drag-and-drop emails to mailbox folders with multi-select support
- Right-click context menus on email list items with all actions
- Mobile-responsive adaptive layout with hamburger menu navigation
New files:
- hooks/use-keyboard-shortcuts.ts - Global keyboard shortcut handling
- hooks/use-email-drag.ts, hooks/use-mailbox-drop.ts - Native HTML5 DnD API
- hooks/use-context-menu.ts, components/ui/context-menu.tsx - Context menu system
- hooks/use-media-query.ts, stores/ui-store.ts - Responsive breakpoint handling
- components/layout/mobile-header.tsx - Mobile navigation header
- contexts/drag-drop-context.tsx - Drag-drop state management
- components/keyboard-shortcuts-modal.tsx - Help modal (? key)
- components/email/email-context-menu.tsx - Email-specific context menu
Updated TODO.md with completed tasks and code audit summary