fix(x): stop clipping the last line of short email bodies

A message body whose first block has a top margin (a bare <p>, most
Outlook mail) rendered with its final line cut off. The margin collapses
through <body>, so body.scrollHeight under-reports by the margin height
while the content's real bottom sits lower — and that short height is
what we set on the iframe. Measured in Electron: a two-line reply
reported 63px against a 73px content bottom, losing 10px. display:
flow-root on <body> contains the margin (the existing rule only reset the
*last* child's bottom margin). Pre-existing; not caused by quote
collapsing.

Also restore <body> attributes when re-serializing a prepared body.
prepareEmailHtml joined head.innerHTML + body.innerHTML, dropping the
<body> tag — and with it the bgcolor/style that 431 of 584 local
messages rely on for their background (the host parser merges a nested
<body>'s attributes onto its own). Serialize documentElement.innerHTML
instead so the tag and its attributes survive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arjun 2026-07-11 10:35:24 +05:30
parent 992c1883a7
commit 0e3918f32a
3 changed files with 18 additions and 4 deletions

View file

@ -312,6 +312,10 @@ function buildEmailDocument(
overflow-y: hidden;
word-wrap: break-word;
padding-bottom: 4px;
/* Contain the first child's top margin. Without this it collapses through
<body>, shifting the box down while body.scrollHeight stays short so
the height we hand the iframe cuts the last line off. */
display: flow-root;
}
body > *:last-child { margin-bottom: 0; }
img { max-width: 100%; height: auto; }

View file

@ -79,6 +79,14 @@ describe('prepareEmailHtml — quote detection', () => {
expect(html).toContain('.a{color:red}')
})
it('preserves <body> attributes, which carry the email background', () => {
// Embedded in the host <body>, the parser merges these onto it. Dropping
// the tag would strip the background off every styled newsletter.
const { html } = prepareEmailHtml('<html><body bgcolor="#f4f4f4" style="margin:0"><p>Hi</p></body></html>')
expect(html).toContain('bgcolor="#f4f4f4"')
expect(html).toContain('margin:0')
})
it('ignores quoted content when deciding whether the email is styled', () => {
// The table lives only in the quote, so the body should still adapt to theme.
const { styled } = prepareEmailHtml(

View file

@ -143,10 +143,12 @@ export function prepareEmailHtml(rawHtml: string): PreparedEmail {
const doc = new DOMParser().parseFromString(rawHtml, 'text/html')
const hasQuote = markQuotedNodes(doc) && unmarkIfNothingRemains(doc)
const styled = isStyledDocument(doc)
// Emails ship <style> blocks that the parser hoists into <head>. Only the
// body gets re-embedded downstream, so carry the head along with it — a
// <style> is just as valid inside <body>.
const html = (doc.head?.innerHTML || '') + (doc.body?.innerHTML || '')
// Serialize <head> and <body> as *elements*, not just their contents. Most
// emails carry a <body bgcolor=… style=…>, and when this HTML is embedded
// inside the host <body>, the parser merges those attributes onto it — which
// is how backgrounds have always applied. Concatenating innerHTML would drop
// the tag and with it the attributes.
const html = doc.documentElement?.innerHTML || doc.body?.innerHTML || ''
return { html, hasQuote, styled }
}