Commit Graph
207 Commits
Author SHA1 Message Date
Matthieu MALVACHE a053c8a38d fix(print): hide reply/archive/delete action bar and tablet back button
The email viewer's quick-action row (Reply, ReplyAll, Forward, Archive,
Delete, Star and friends) and the tablet back button were visible in
the printed output. Apply Tailwind's print:hidden utility to both so
the printed page shows only the actual mail content — header, body,
and attachments — which is what users expect to archive on paper.

The More Actions dropdown and the Quick Reply section already had
print:hidden applied.
2026-04-17 13:39:53 +02:00
Matthieu MALVACHE ce3b34f535 fix(print): clone viewer to a body-level overlay before printing
The previous :has()-based print rules worked in Chromium's headless
PDF export but produced a blank page in Firefox. Rather than fight
browser-specific differences in @media print with :has(), flex, and
visibility on deeply-nested ancestors, clone the email viewer DOM
into a dedicated #print-overlay appended directly to document.body
before calling window.print(), and add a html.is-printing class.

Print CSS now targets a single well-known id (#print-overlay) at the
body root: everything else is display:none'd (no :has() needed), the
overlay flows naturally on the page, and the subtree is forced to
black text on a transparent backdrop with a white root background so
dark-mode rendering prints cleanly on paper with Firefox's default
"Print background" setting off. afterprint removes the overlay and
clears the class, so both successful prints and cancellations clean
up.
2026-04-17 13:38:09 +02:00
Matthieu MALVACHE c42a51438c fix(print): force light colours so dark-mode emails print visibly
Chrome defaults the "Background graphics" toggle off, so dark-mode
backgrounds don't print. The light-on-dark text of the viewer then
prints as white text on white paper — the page is blank.

Override the printed subtree with black text on a transparent
backdrop (and a single white backdrop on the root container), and
keep links in their conventional print blue. The dark mode of the
app no longer needs to leak into the printed document. Verified in a
headless print run with backgrounds off: a short message prints as
exactly one readable page.
2026-04-17 13:25:56 +02:00
Matthieu MALVACHE 52610ef782 fix(print): blank extra pages for short emails
The previous print rules kept the visibility: hidden + visibility:
visible pattern, which leaves the sidebar and email list in the layout
tree — they still contribute height. The outer flex h-screen wrapper
plus the long mailbox list made the printed document span several
pages even for a one-line email, showing the email on page one and
6–7 blank pages after.

Switch to a :has()-based display: none on every body descendant that
isn't on the ancestor chain of #email-viewer-container (and isn't the
viewer or its subtree). Those elements drop out of layout entirely,
so the document height reflects only the viewer's own content. The
ancestor chain keeps its flex styling but releases viewport-sized
height constraints. Verified in a headless print run: a short message
now renders as exactly one page.
2026-04-17 13:10:51 +02:00
Matthieu MALVACHE 43ba0aef8a fix(print): empty page when email viewer carries an enter animation
The print rules introduced with the print-layout change left the
#email-viewer-container's utility animation (animate-in fade-in) in
place. Chrome samples keyframes when it snapshots the page for print,
so it frequently captured the opacity: 0 frame — the page came out
completely empty. Reproduced in a headless print run: the broken rule
set produces an 874-byte blank PDF; this fix produces an 18 KB PDF
with the full header, body text, and iframe content.

The replacement print block resets the viewer subtree's animation,
transition, transform, filter and opacity so no keyframe state leaks
through; switches #email-viewer-container to position: static and
resets the ancestor overflow chain via :has() so a long email prints
across multiple pages instead of being clipped to the first viewport;
and pulls @page margin up to 12mm so the content has breathable
page margins instead of colliding with Chrome's default headers.
2026-04-17 13:01:03 +02:00
Matthieu MALVACHE 425fc971c4 feat(ui): optional domain favicon avatars
When a contact has no photo, fall back to the sender domain's favicon
instead of coloured initials. The lookup is opt-in (settings → email
behaviour) and off by default. The favicon is fetched from DuckDuckGo's
public icon service using only the domain name — the email address
never leaves the browser, and nothing is hashed or correlated back to
the user. Initials stay rendered behind the image so the avatar still
reads right if the favicon 404s or the user is offline.

Adds a settings toggle + translations across all 10 locales.

Closes #22.
2026-04-16 23:18:24 +02:00
Matthieu MALVACHE 08d12be925 feat(email): archive entire conversation, not just the selected message
Archive now follows the Gmail / Apple Mail convention: when the current
message belongs to a thread, all of its siblings move to Archive in a
single Email/set. Keeps the inbox free of half-archived conversations.

Introduces client.moveThreadToMailbox(threadId, dest) which resolves the
thread's emailIds via Thread/get then batches the mailboxIds update
into one JMAP request, and a matching store method that reconciles
local counters for the source and destination mailboxes without a full
refetch. Single-message mail (no threadId) falls back to the existing
per-message path.

Closes #49.
2026-04-16 23:16:09 +02:00
Matthieu MALVACHE f3bf6efedc fix(oauth): fail with a clear message when SSO is loaded over plain HTTP
OAuth2/OIDC with PKCE needs crypto.subtle.digest(), which browsers
only expose in secure contexts (HTTPS or localhost). Loading the
webmail over http://host without a TLS proxy in front surfaced as
"TypeError: can't access property 'digest', crypto.subtle is
undefined" when the user clicked "Sign in with SSO" — unhelpful.

Guard handleOAuthLogin with window.isSecureContext + a crypto.subtle
check and surface a translated message ("SSO requires a secure
connection…") in an amber warning banner instead. Key added to all 10
locales.

Closes #23.
2026-04-16 23:12:38 +02:00
Matthieu MALVACHE 24ad26e388 fix(sieve): use full hierarchical path in filter move/copy actions
Selecting a nested folder as a filter destination stored only the leaf
name, producing a Sieve fileinto action the server could not resolve
(e.g. "Foo" instead of "Inbox/Projects/Foo"). Add a getMailboxFullPath
helper that walks parentId and joins with "/", and route the filter
modal's move/copy dropdown through it so the emitted script references
the full path.

The helper guards against unknown ids, orphaned parents, and
self-referential cycles.

Closes #62.
2026-04-16 23:10:26 +02:00
Vsevolod Sauta 8a038595b4 feat: add russian and ukrainian locales 2026-04-16 23:07:36 +02:00
Jabi Infante 393fd12e8f feat: custom favicon with unread badge, fix stale mailbox counts
Replace the default Next.js favicon.ico with an SVG mail icon from
Lucide and add useFaviconBadge, a canvas-based hook that draws a red
unread-count badge on the tab favicon. Shows "+" when count > 9. On
Safari 15 and below (no Canvas roundRect), falls back to fillRect.

Also fixes a real JMAP push bug in the email store: Mailbox state
changes on push weren't always paired with Email changes, so unread
counts in the sidebar could go stale when a new message arrived. Now
Email changes refresh mailbox counts too, and the Mailbox branch
de-dups when both fire in the same push.

Credits #63 (@jabiinfante).
2026-04-16 23:06:26 +02:00
Roy Petter Dyrdahl Torgersen 76cebb7905 fix(contacts): batch ContactCard/get to respect maxObjectsInGet
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).
2026-04-16 23:04:57 +02:00
Prastowo aGung Widodo 3c1a4496f9 fix(print): restrict print area to the email viewer
The print output was capturing the entire window, so portrait pages
only showed the sidebar and message list. Scope the print rendering
with a @media print block that hides everything, then re-reveals the
#email-viewer-container subtree and positions it full-page. Add
print:hidden to the More Actions dropdown and the Quick Reply section
so they don't appear in the printed output.

Credits #55 (@prastowoagungwidodo).
2026-04-16 23:03:25 +02:00
Prastowo aGung Widodo e12bf45e84 fix(contacts): stop action buttons from wrapping on empty state
Apply whitespace-nowrap + overflow-hidden + text-ellipsis to the "New
Contact" / "Import vCard" buttons on the empty state, and add flex-wrap
+ min-w-0 on the parent so longer future translations wrap to a new
row instead of clipping behind the ellipsis. Icons get shrink-0 so
they stay rendered.

Credits #56 (@prastowoagungwidodo).
2026-04-16 23:02:23 +02:00
Joel Purra 7272466ffa ci: publish major version container image tag
Adds a `{{major}}` tag so users can pin to `jmap-webmail:1` and receive
non-breaking minor/patch updates without manually bumping the tag each
release.

Closes #57. Thanks @joelpurra.
2026-04-16 23:00:53 +02:00
Matthieu MALVACHE 09f72d3d4f fix(security): escape linkClassName in plainTextToSafeHtml
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.
2026-04-16 22:55:48 +02:00
Matthieu MALVACHE d33309a8b6 fix(security): escape quotes in plain-text linkifier, bump vulnerable deps
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.
2026-04-16 22:47:15 +02:00
Matthieu MALVACHE e0bdf99d0d chore: exclude superpowers dirs from public release, reinforce no-AI-reference policy 2026-04-16 22:46:55 +02:00
Prastowo Agung Widodo b8df1f3051 Fix: missing email_viewer.send translation 2026-03-23 22:58:06 +01:00
Matthieu MALVACHE c78b766f2a fix: add type text/plain to textBody for RFC 8621 compliance (#48)
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.
2026-03-23 22:57:39 +01:00
Matthieu MALVACHE 4bdf98525c chore: remove AI reference from spam header comment 2026-03-23 15:26:12 +01:00
Matthieu MALVACHE 3cd3e26825 chore: bump version to 1.4.0
New features: folder management (#44), mail multi-selection (#43).
Bug fixes: health endpoint (#41), identity deletion (#42), inline CID
images, email list flicker, dependency updates.

Thanks to @wrenix, @ClemaX, @freddij, @dlecourtaltimafr, and @capitanroy
for reporting issues and contributing to this release.
2026-03-23 15:21:20 +01:00
Matthieu MALVACHE 301386c709 chore: fix stale tests, rename spam label, update deps and fix flatted CVE
- Fix oauth-discovery tests expecting removed console.error
- Rename "AI Analysis" to "Spam Analysis" in email viewer
- Update next 16.2.1, tailwind 4.2.2, zustand 5.0.12, eslint 9.39.4,
  typescript-eslint 8.57.1, tanstack/react-virtual 3.13.23
- Fix flatted prototype pollution (GHSA-rf6f-7fwh-wjgh)
2026-03-23 15:20:53 +01:00
Matthieu MALVACHE d3ef46f760 fix: add copy-to-clipboard feedback and neutralize dark mode background tint
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.
2026-03-23 15:14:14 +01:00
Matthieu MALVACHE 3ba5cf16c3 fix(sidebar): fix context menu viewport clipping and delete dialog overflow (#44)
Render context menu and delete confirmation dialog via portal to escape
sidebar overflow-hidden. Clamp context menu position to viewport bounds.
2026-03-23 14:39:20 +01:00
Matthieu MALVACHE be6907f863 feat(sidebar): add folder drag-and-drop reparenting (#44) 2026-03-23 14:29:16 +01:00
Matthieu MALVACHE 01cb0f173f feat(sidebar): add folder management UI with context menu, inline editing, and move-to (#44) 2026-03-23 14:26:22 +01:00
Matthieu MALVACHE adbf46770d feat(drag-drop): handle mailbox reparenting on drop (#44) 2026-03-23 14:20:20 +01:00
Matthieu MALVACHE d14b8f6567 feat(i18n): add folder management translation keys for all 8 locales (#44) 2026-03-23 14:18:51 +01:00
Matthieu MALVACHE d9d6b02987 feat(drag-drop): extend context with dragType for mailbox drag support (#44) 2026-03-23 14:17:20 +01:00
Matthieu MALVACHE 0b952dcce4 feat(store): add mailbox CRUD methods with optimistic UI (#44) 2026-03-23 14:15:17 +01:00
Matthieu MALVACHE a030707cd6 feat(jmap): add Mailbox/set methods for create, update, destroy (#44) 2026-03-23 14:13:01 +01:00
Matthieu MALVACHE cb862994e3 fix: render inline CID images in email body instead of showing as attachments
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.
2026-03-23 14:11:25 +01:00
Matthieu MALVACHE 146921d5f0 fix: eliminate email list flicker during loading and after-action refreshes
Shared isLoading flag caused the list to dim on unrelated operations
(send, batch actions, mailbox refresh). After-action fetches also
triggered a full loading overlay unnecessarily. Now operations that
don't fetch emails no longer set isLoading, after-action refreshes
use silent refreshCurrentMailbox, and the loading overlay only appears
after a 300ms delay to skip fast fetches.
2026-03-23 14:11:09 +01:00
Matthieu MALVACHE c80a5a4d51 docs: add folder management implementation plan (#44)
8-task plan covering JMAP client methods, store layer, DragDropContext
extension, i18n, sidebar UI with inline editing and drag-and-drop.
2026-03-23 14:09:28 +01:00
Matthieu MALVACHE 7bed7ff7c7 docs: add folder management design spec (#44)
Spec for IMAP folder CRUD operations: create, rename, move, and delete
mailboxes via the sidebar context menu and drag-and-drop.
2026-03-23 13:59:28 +01:00
Matthieu MALVACHE f646badc55 fix: remove production console statements and explain eslint-disable comments
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.
2026-03-23 13:25:50 +01:00
Matthieu MALVACHE 4c97d2d41d fix(i18n): add missing delete_confirm_title and delete_confirm_message keys (#43) 2026-03-23 13:21:51 +01:00
Matthieu MALVACHE b685e37c4e fix: resolve lint errors in email store and move-to-popover (#43) 2026-03-23 13:07:24 +01:00
Matthieu MALVACHE ae92bd2cbc feat: complete multi-selection store methods, toolbar integration, and shift-click support (#43) 2026-03-23 13:02:58 +01:00
Matthieu MALVACHE 6fc7217185 feat(i18n): add translation keys for multi-selection batch actions (#43) 2026-03-23 12:56:00 +01:00
Matthieu MALVACHE 837ef722b6 feat: add MoveToPopover component with search and keyboard navigation (#43) 2026-03-23 12:55:43 +01:00
Matthieu MALVACHE 3835efb3fa feat(store): add range selection, filter selection, and lastSelectedIndex tracking (#43) 2026-03-23 12:54:23 +01:00
Matthieu MALVACHE 84c43f8b0e fix: add urn:ietf:params:jmap:submission to Identity and EmailSubmission ops (#42)
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).
2026-03-23 12:18:42 +01:00
Matthieu MALVACHE 0948108792 fix: use v8 heap_size_limit for health check instead of heapTotal (#41)
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.
2026-03-23 12:18:27 +01:00
Matthieu MALVACHE df53c31708 chore: update Next.js 16.1.6 -> 16.2.0 (5 security fixes)
Fixes: CSRF bypass on Server Actions, HTTP request smuggling in
rewrites, unbounded image disk cache, postponed resume DoS, dev
HMR websocket CSRF bypass.
2026-03-20 16:52:32 +01:00
Rens Reinders 0153f66340 feat: double-click to create event from month view with smart time suggestion (#37)
* feat: open event modal on month day cell click

* feat: suggest first available hour when creating from month view

* chore: document month view slot suggestion helpers

* fix(calendar): use double-click to create event in month view

* fix(calendar): use 9am fallback for non-today month slots
2026-03-18 21:13:52 +01:00
Rens Reinders 167014871d fix: participant/invitation handling for Stalwart JMAP and deduplicate self-attendees (#36)
* fix: align calendar event payload with stalwart scheduling

* fix: dedupe calendar participants by normalized address

* chore: document calendar uid generation

* chore: clarify organizer participant guard
2026-03-18 21:13:52 +01:00
WrenIX e9baac022c fix: hide vertical nav rail on tablet to avoid duplicate navigation (#40) 2026-03-18 11:09:35 +01:00
Rens Reinders 53ecfe5f69 fix: replace inline calendar delete confirms with centered modal dialog (#34)
* fix: more margin on the avatar to align with subject

* fix: fixed multi day events multiple items gap spacing

* fix: align sticky week headers with calendar grid

* fix: nice confirm modal

* fix: added locales

* fix: polish confirm dialog behavior and participant delete messaging

* fix: apply themed destructive colors without button shadow

* fix: restore destructive button theme styling
2026-03-18 11:09:35 +01:00