Render plain-text mail with its line breaks #326

Closed
opened 2026-09-10 20:32:58 +00:00 by jcoffey-dev · 0 comments
Owner

Every plain-text message has been rendering as one continuous paragraph — line breaks collapsed, signature and quoted reply run into the prose. Reported from the inbox; reproduced and diagnosed against the live instance.

Cause

MessageView decided which renderer to use with showHtml = Boolean(htmlRaw).

But htmlBody is a derived list, not a filter. RFC 8621 §4.1.4 gives a message with no HTML alternative one anyway, holding the text/plain part. So htmlRaw is populated for plain-text mail, showHtml went true, and the message was routed to HtmlBody.ihm-email-root, which is white-space: normal. Every newline collapsed.

Confirmed live against Stalwart 0.16.21 on 2026-09-10, reading the same thread back through Email/get with bodyProperties: ["partId", "type"]:

from htmlBody textBody same part
plain-text sender ["text/plain"] ["text/plain"] yes
real alternative ["text/html"] ["text/plain"] no

The distinction was cleanly available — type was already in BODY_PROPS, and nothing looked at it.

Fix

Ask what the part actually is, not whether a value came back. The decision moves into hasHtmlAlternative() in lib/html.ts so it can be tested and so the reasoning has somewhere to live.

TextBody was written for exactly these messages and was simply unreachable. So this also switches on everything it does and that has never fired on plain-text mail:

  • white-space: pre-wrap — the line breaks the sender wrote
  • quote-depth colouring (.q1/.q2/.q3)
  • the collapsible ••• quoted block

Scope

Affects every plain-text-only message — mailing lists, git send-email, most automated notification mail. Not a corner case.

No user-facing strings added or changed, so no translation work; the nine catalogues are untouched.

Checks

  • npm run typecheck — clean, both packages
  • npm test — 1160 web tests / 114 files, 168 server tests, all passing
  • npm run build — clean
  • 7 new tests pinning the rule, built from the part shapes the live server actually returned

Not included

After this, hard-wrapped mail sets its own measure through the sender's line breaks and reads correctly. Mail with very long unwrapped lines will run the full width of the reading pane; a max-width in ch on .ihm-text-root would cap that, but it is a separate judgement about typography rather than a bug, so it is left out.

Merged 2026-09-10 as coffey-labs/ihasmail@9b497af756

Rebuilt from: git history, session transcript.

Every plain-text message has been rendering as one continuous paragraph — line breaks collapsed, signature and quoted reply run into the prose. Reported from the inbox; reproduced and diagnosed against the live instance. ## Cause `MessageView` decided which renderer to use with `showHtml = Boolean(htmlRaw)`. But `htmlBody` is a **derived list, not a filter**. RFC 8621 §4.1.4 gives a message with no HTML alternative one anyway, holding the `text/plain` part. So `htmlRaw` is populated for plain-text mail, `showHtml` went true, and the message was routed to `HtmlBody` → `.ihm-email-root`, which is `white-space: normal`. Every newline collapsed. Confirmed live against Stalwart 0.16.21 on 2026-09-10, reading the same thread back through `Email/get` with `bodyProperties: ["partId", "type"]`: | from | `htmlBody` | `textBody` | same part | |---|---|---|---| | plain-text sender | `["text/plain"]` | `["text/plain"]` | **yes** | | real alternative | `["text/html"]` | `["text/plain"]` | no | The distinction was cleanly available — `type` was already in `BODY_PROPS`, and nothing looked at it. ## Fix Ask what the part actually is, not whether a value came back. The decision moves into `hasHtmlAlternative()` in `lib/html.ts` so it can be tested and so the reasoning has somewhere to live. `TextBody` was written for exactly these messages and was simply unreachable. So this also switches on everything it does and that has never fired on plain-text mail: - `white-space: pre-wrap` — the line breaks the sender wrote - quote-depth colouring (`.q1`/`.q2`/`.q3`) - the collapsible `•••` quoted block ## Scope Affects every plain-text-only message — mailing lists, `git send-email`, most automated notification mail. Not a corner case. No user-facing strings added or changed, so **no translation work**; the nine catalogues are untouched. ## Checks - `npm run typecheck` — clean, both packages - `npm test` — 1160 web tests / 114 files, 168 server tests, all passing - `npm run build` — clean - 7 new tests pinning the rule, built from the part shapes the live server actually returned ## Not included After this, hard-wrapped mail sets its own measure through the sender's line breaks and reads correctly. Mail with very long unwrapped lines will run the full width of the reading pane; a `max-width` in `ch` on `.ihm-text-root` would cap that, but it is a separate judgement about typography rather than a bug, so it is left out. **Merged** 2026-09-10 as coffey-labs/ihasmail@9b497af756d6 <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.