Offer the message's own format when replying #408

Closed
opened 2026-09-19 21:39:23 +00:00 by jcoffey-dev · 0 comments
Owner

Summary

A reply opened in the format from Settings → General → Composing, whatever the message being answered was written in. Replying in plain text to a rich text message throws away the formatting; replying in rich text to a plain-text one overrides what the sender chose. The per-draft switch existed, buried in the composer's ⋮ menu, which you have to know about and remember before you start writing.

  • A line above the editor when the two disagree: This message is rich text (or plain text), a Switch button, and a dismiss. The draft still opens in the format the settings ask for — the offer changes nothing until it's pressed, and never touches the setting.
  • Switching converts that draft only, keeping the quoted message and the signature, exactly as the ⋮ entry always did. Switching from that menu answers the offer too, so the bar doesn't linger after the question has been settled.
  • Forwards as well, where the formatting being passed on belongs to somebody else.
  • What counts as rich text is hasHtmlAlternative(), which reads the body part's own type. htmlBody is derived (RFC 8621 §4.1.4), so a plain-text message has one too, holding its text/plain part; trusting the list would offer rich text on every plain-text message and never offer plain text where it's wanted.

The mock disagreed with the server here, which is the sort of gap this exact feature would have fallen into. It returned an empty htmlBody for a plain-text message, where Stalwart 0.16.21 returns the text/plain part in both lists. Both mock builders now answer the way the server does.

Related issues

Fixes #407

Translations

Adds two keys, "This message is rich text" and "This message is plain text", translated in all nine catalogs. The buttons reuse the menu's existing "Switch to plain text" / "Switch to rich text" and the existing "Dismiss". The count falling back to English is 16 per language before and after.

Testing

  • reply-format-offer.test.ts: both directions, no offer when the formats agree, the derived-htmlBody trap, reply all and forward, both bodies present to switch between, and no offer on a message started from scratch.
  • format-offer-bar.test.tsx: the bar names the right format, Switch converts the draft and puts the bar away, Dismiss leaves the format alone.
  • npm test (1413 web, 251 server) and npm run typecheck pass.
  • Driven in Chrome against npm run dev:mock: plain-text default replying to a rich message offered rich text, and switching moved the quoted reply into the rich editor with its toolbar; rich default replying to a plain-text message offered plain text; a forward of a plain-text message offered it too; Dismiss left the format alone; and a rich reply to a rich message showed no bar.

Screenshots

┌─ Re: Your order is on its way ───────────────┐
│ From  Demo User <[email protected]>           │
│ To    [Shop Updates ×]                       │
│ Re: Your order is on its way                 │
├──────────────────────────────────────────────┤
│ This message is rich text  [Switch to rich…] ✕│
├──────────────────────────────────────────────┤

🤖 Generated with Claude Code

Merged 2026-09-19 as coffey-labs/ihasmail@d992442b81

Rebuilt from: git history, session transcript.

## Summary A reply opened in the format from **Settings → General → Composing**, whatever the message being answered was written in. Replying in plain text to a rich text message throws away the formatting; replying in rich text to a plain-text one overrides what the sender chose. The per-draft switch existed, buried in the composer's ⋮ menu, which you have to know about and remember before you start writing. - **A line above the editor when the two disagree:** *This message is rich text* (or *plain text*), a **Switch** button, and a dismiss. The draft still opens in the format the settings ask for — the offer changes nothing until it's pressed, and never touches the setting. - **Switching converts that draft only,** keeping the quoted message and the signature, exactly as the ⋮ entry always did. Switching from that menu answers the offer too, so the bar doesn't linger after the question has been settled. - **Forwards as well,** where the formatting being passed on belongs to somebody else. - **What counts as rich text is `hasHtmlAlternative()`,** which reads the body part's own type. `htmlBody` is derived (RFC 8621 §4.1.4), so a plain-text message has one too, holding its text/plain part; trusting the list would offer rich text on every plain-text message and never offer plain text where it's wanted. **The mock disagreed with the server here,** which is the sort of gap this exact feature would have fallen into. It returned an empty `htmlBody` for a plain-text message, where Stalwart 0.16.21 returns the text/plain part in both lists. Both mock builders now answer the way the server does. ## Related issues Fixes #407 ## Translations Adds two keys, "This message is rich text" and "This message is plain text", translated in all nine catalogs. The buttons reuse the menu's existing "Switch to plain text" / "Switch to rich text" and the existing "Dismiss". The count falling back to English is 16 per language before and after. ## Testing - `reply-format-offer.test.ts`: both directions, no offer when the formats agree, the derived-`htmlBody` trap, reply all and forward, both bodies present to switch between, and no offer on a message started from scratch. - `format-offer-bar.test.tsx`: the bar names the right format, Switch converts the draft and puts the bar away, Dismiss leaves the format alone. - `npm test` (1413 web, 251 server) and `npm run typecheck` pass. - Driven in Chrome against `npm run dev:mock`: plain-text default replying to a rich message offered rich text, and switching moved the quoted reply into the rich editor with its toolbar; rich default replying to a plain-text message offered plain text; a forward of a plain-text message offered it too; Dismiss left the format alone; and a rich reply to a rich message showed no bar. ## Screenshots ┌─ Re: Your order is on its way ───────────────┐ │ From Demo User <[email protected]> │ │ To [Shop Updates ×] │ │ Re: Your order is on its way │ ├──────────────────────────────────────────────┤ │ This message is rich text [Switch to rich…] ✕│ ├──────────────────────────────────────────────┤ 🤖 Generated with [Claude Code](https://claude.com/claude-code) **Merged** 2026-09-19 as coffey-labs/ihasmail@d992442b8194 <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.