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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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).
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).
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).
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.
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.
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.
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.
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.
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.
Fixes: CSRF bypass on Server Actions, HTTP request smuggling in
rewrites, unbounded image disk cache, postponed resume DoS, dev
HMR websocket CSRF bypass.
* 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