Render plain-text mail with its line breaks

`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. Testing
`Boolean(htmlRaw)` therefore answered "this is HTML" for every plain-text
mail, sending it to HtmlBody and `.ihm-email-root`, which is
`white-space: normal` and collapses every line break. Hard-wrapped mail
arrived as a single paragraph with the signature and the quoted reply run
into the prose.

Confirmed live against Stalwart 0.16.21 (2026-09-10): a plain-text message
comes back with `htmlBody` and `textBody` naming the same part, typed
text/plain, while a real multipart/alternative names two different parts.
`type` was already in BODY_PROPS; nothing looked at it.

TextBody was written for exactly these messages and was simply unreachable,
so this also restores what it does -- pre-wrap, quote-depth colouring and the
collapsible quoted block, none of which had ever fired on plain-text mail.
This commit is contained in:
2026-09-10 13:32:37 -07:00
parent a9923c48d9
commit 73a1bad29f
3 changed files with 78 additions and 2 deletions
+20
View File
@@ -350,6 +350,26 @@ export function markKeptSurfaces(root: ParentNode): number {
return kept;
}
/**
* Whether a message really has an HTML alternative to render.
*
* `htmlBody` is a *derived* list, not a filter: RFC 8621 §4.1.4 says a message
* with no HTML alternative still gets one, and it holds the text/plain part.
* Confirmed live against Stalwart 0.16.21 (2026-09-10) -- a plain-text mail
* comes back with `htmlBody` and `textBody` naming the same part, typed
* `text/plain`, while a real multipart/alternative names two different parts.
*
* So "is there a body value under htmlBody" is not the question; the part's own
* type is. Answering the first one sent every plain-text message down the HTML
* path, where the body is placed in `.ihm-email-root` under
* `white-space: normal` and every line break collapses -- hard-wrapped mail
* arrived as a single paragraph with the signature and the quoted reply run
* into the prose.
*/
export function hasHtmlAlternative(part: { type?: string } | undefined, value: string | undefined): boolean {
return /^text\/html\b/i.test(part?.type ?? "") && Boolean(value);
}
export const TEXT_EMAIL_CSS = `
:host { display:block; }
.ihm-text-root { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-size: 13.5px; line-height:1.55; white-space: pre-wrap; overflow-wrap: anywhere; color: inherit; }