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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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
MessageViewdecided which renderer to use withshowHtml = Boolean(htmlRaw).But
htmlBodyis a derived list, not a filter. RFC 8621 §4.1.4 gives a message with no HTML alternative one anyway, holding thetext/plainpart. SohtmlRawis populated for plain-text mail,showHtmlwent true, and the message was routed toHtmlBody→.ihm-email-root, which iswhite-space: normal. Every newline collapsed.Confirmed live against Stalwart 0.16.21 on 2026-09-10, reading the same thread back through
Email/getwithbodyProperties: ["partId", "type"]:htmlBodytextBody["text/plain"]["text/plain"]["text/html"]["text/plain"]The distinction was cleanly available —
typewas already inBODY_PROPS, and nothing looked at it.Fix
Ask what the part actually is, not whether a value came back. The decision moves into
hasHtmlAlternative()inlib/html.tsso it can be tested and so the reasoning has somewhere to live.TextBodywas 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.q1/.q2/.q3)•••quoted blockScope
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 packagesnpm test— 1160 web tests / 114 files, 168 server tests, all passingnpm run build— cleanNot 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-widthinchon.ihm-text-rootwould 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.