Clicking an attachment chip in the composer now opens the same FilePreviewModal
the message viewer uses, instead of offering only download/remove. The chip
becomes clickable once the attachment has content (a local File, or an uploaded
blob for forwarded attachments) and the type is previewable.
- getFileContent prefers the in-memory File (no network round-trip) and falls
back to composerClient.fetchBlob for forwarded attachments (blobId only).
- Previewability (isFilePreviewable) and the open-in-new-tab safety gate are
handled inside the modal, so this adds no new egress/attack surface; the local
download path uses an <a download> (forces a save, never executes).
- No new dependencies and no new locale keys.
Some emails (Outlook / templated HTML) set `html, body { height: 100% }` in
their own <style>. Combined with the viewer srcDoc's `overflow: hidden` and the
scrollHeight-based iframe auto-resize, the measured height collapses to the
iframe's initial size, so everything below the first screenful (often just the
header/logo) is clipped and the rest of the message is invisible.
Force `height: auto !important` on html/body in the rendered srcDoc so the
document grows to its real content height before scrollHeight is measured.
Replying to / forwarding a layout-heavy HTML email (nested tables, MJML,
Outlook divs) destroyed its layout: ProseMirror re-parsed the quoted body
through its strict schema and discarded anything that didn't fit. The quoted
original is now held verbatim in a new atomic QuotedHtml node and never parsed
into the schema; its NodeView renders inside a shadow root so app CSS can't
cascade in and the in-editor view matches the sent mail 1:1.
- quoted-html.ts (new): QuotedHtml atom node + shadow-DOM NodeView (inner
contentEditable for redaction), serializeEditorContent(), buildQuotedHtmlBlock().
- rich-text-editor: register the node; emit via serializeEditorContent (not
getHTML) so the verbatim island survives.
- composer: both HTML reply/forward paths embed the original as an island
(sanitize -> cid-rewrite -> buildQuotedHtmlBlock); the signature-swap effect
serializes via serializeEditorContent and treats the island as a quote
boundary so the splice never cuts into the quoted body.
atom:true means Backspace at the boundary / Ctrl+A+Delete removes the whole
quote in one go.
Bulwark had no read-receipt support (JMAP/Stalwart have no native MDN).
End-to-end, client-side, in three parts:
- Request (compose): a toolbar toggle (MailCheck, green when on) sets
Disposition-Notification-To on the outgoing message via the JMAP
"header:<name>:asText" create property. Threaded composer -> page ->
email-store -> client.sendEmail. Default from requestReadReceiptDefault.
- Detect (viewer): reads Disposition-Notification-To case-insensitively from
the parsed headers and shows a banner (green Send / red Ignore) in the
unified notification bar. Hidden in Sent/Drafts/Trash/Junk and once handled.
message/disposition-notification + message/delivery-status report parts are
filtered out of the attachment list.
- Respond (MDN): lib/mdn.ts builds an RFC 8098 multipart/report (text/plain +
message/disposition-notification, UTF-8/base64, localized subject + body).
client.sendReadReceipt uploads the blob, imports it into Sent via
Email/import, then submits with an explicit envelope. Both Send and Ignore
set the $MDNSent keyword (RFC 3503) so no client re-prompts. Behaviour
configurable: ask / always / never.
New: lib/mdn.ts, read-receipt-banner.tsx. Settings (requestReadReceiptDefault,
readReceiptResponse) + UI. All 17 locales.
Replying to a reply produced "Re: Re: foo" (and German used the English
"Re:"/"Fwd:" instead of "AW:"/"WG:"). Four code paths built reply/forward
subjects and only one deduplicated - and only for the English prefix, so
cross-locale threads accumulated chains.
New lib/subject-prefix.ts strips any leading run of reply/forward markers
across ~35 tokens from all supported languages (plus Outlook Re[2]: and
Eudora Re*2: counters), then prepends the locale-appropriate prefix. All four
call sites (composer getInitialSubject, the two page.tsx sites, and the three
pro-tab handlers) now use buildReplySubject/buildForwardSubject. German prefix
corrected to AW:/WG:.
The reply/forward quote header was always emitted in English ("On {date},
{from} wrote:", "---------- Forwarded message ----------", From/Date/Subject)
regardless of UI language, in both the main path (lib/quote-header.ts) and the
composer's inline fallback. quote-header.ts now takes an optional localized
QuoteHeaderLabels set (English defaults preserved for back-compat); page.tsx
builds it from a new quote_header message namespace, and the composer fallback
uses the same keys. Added the quote_header namespace to all 17 locales.
Also folds in the forward-sender-address fix: the forward "From:" line now
shows the full "Name <email>" like every mail client (the reply line keeps the
bare name, which reads naturally in "On … wrote:").
The global rule
td, th { word-break: break-word; }
was breaking HTML-email tables one glyph per row whenever a column was
narrow, especially for Hebrew/Arabic/CJK headers and long English
strings. The non-standard `word-break: break-word` keyword behaves
like `break-all` in some engines, splitting words at arbitrary
character boundaries even when the word would fit if the column auto-
expanded.
`overflow-wrap: break-word` is already set on body/table, so the rule
only needs to add min-content relaxation for cells. `overflow-wrap:
anywhere` does exactly that without re-introducing break-all
behaviour.
Repro: any transactional Hebrew/RTL order-summary email — each header
(`מוצר`, `כמות`, `מחיר`) collapses to one glyph per row. After the fix
they render on a single line.
Closes#341.
Adds the universal "send with the platform modifier" shortcut every
mainstream mail client (Gmail, Outlook, Apple Mail, Proton, Tutanota,
Fastmail, Thunderbird) supports. Closes#343.
Behaviour:
* Window-level keydown listener registered while the composer is
mounted. Fires when focus is anywhere inside the composer — chip
inputs, subject, body textarea, or the rich-text contentEditable.
* Plain Enter is untouched; only Enter + Ctrl (Win/Linux) or Cmd
(macOS) triggers send. Shift/Alt modifiers are ignored so existing
autocomplete-confirm / chip-commit Enters are not hijacked.
* Routes through a ref so handleSend's per-render rebind doesn't
re-register the listener every render.
* All existing send-time validation, attachment-warning, draft-save
and undo-send flows still apply — the shortcut just calls the
same handleSend() as the toolbar button.
* Listed in the Keyboard Shortcuts dialog under the existing
Composer section.
Tested:
* Compose -> type body -> Ctrl+Enter -> Outbox.
* Cc/Bcc autocomplete suggestion + Enter still selects (alt-free
Enter without Ctrl, so the new listener bails).
* Subject input -> Ctrl+Enter -> sends.
* Body Enter without modifier -> newline.
Parallelize ICS parse with raw blob fetch, render the banner as soon
as parsing returns instead of awaiting the existing-event lookup, and
filter that lookup by UID server-side instead of fetching every event
on the calendar.