fix(gmail): stop wrapping draft/send HTML body in per-paragraph <p> tags (#5479)

* fix(gmail): stop wrapping draft/send HTML body in per-paragraph <p> tags

* fix(gmail): use <br> instead of CSS white-space for line breaks

white-space: pre-wrap has inconsistent email-client support (including
Gmail for non-Google accounts per caniemail.com); <br> is the
client-agnostic standard for plain-text-to-HTML line breaks.
This commit is contained in:
Waleed
2026-07-07 10:28:43 -07:00
committed by GitHub
parent 03462b9289
commit 954fdd8595
2 changed files with 13 additions and 12 deletions
+5 -5
View File
@@ -91,10 +91,10 @@ describe('escapeHtml', () => {
})
describe('plainTextToHtml', () => {
it('renders blank lines as paragraph breaks and single newlines as <br>', () => {
it('converts newlines to <br> without paragraph margins', () => {
const html = plainTextToHtml('Hi Janice,\n\nHope you are well.\nSecond line.')
expect(html).toContain('<p>Hi Janice,</p>')
expect(html).toContain('<p>Hope you are well.<br>Second line.</p>')
expect(html).not.toContain('<p>')
expect(html).toContain('Hi Janice,<br><br>Hope you are well.<br>Second line.')
})
it('escapes HTML in the source text', () => {
@@ -150,7 +150,7 @@ describe('buildSimpleEmailMessage', () => {
expect(plainIdx).toBeGreaterThan(-1)
expect(htmlIdx).toBeGreaterThan(plainIdx)
expect(decodePart(decoded, 'text/plain')).toBe('Hi Janice,\n\nQuick question.')
expect(decodePart(decoded, 'text/html')).toContain('<p>Hi Janice,</p>')
expect(decodePart(decoded, 'text/html')).toContain('Hi Janice,<br><br>Quick question.')
})
it('encodes bodies as base64 so UTF-8 (emoji, accents) round-trips cleanly', () => {
@@ -242,7 +242,7 @@ describe('buildMimeMessage', () => {
expect(message).toMatch(/Content-Type: multipart\/alternative; boundary="([^"]+)"/)
expect(message).toContain('Content-Disposition: attachment; filename="note.txt"')
expect(decodePart(message, 'text/plain')).toBe('Hello')
expect(decodePart(message, 'text/html')).toContain('<p>Hello</p>')
expect(decodePart(message, 'text/html')).toContain('Hello')
})
it('emits multipart/alternative without multipart/mixed when no attachments', () => {
+8 -7
View File
@@ -339,17 +339,18 @@ export function escapeHtml(value: string): string {
/**
* Convert a plain-text body to an HTML body that flows naturally in Gmail.
* Blank lines become paragraph breaks; single newlines become `<br>`.
* Newlines become `<br>` rather than per-paragraph `<p>` tags or a CSS
* `white-space` rule — `<p>` margins are what Gmail's "Remove formatting"
* button strips, and `white-space` has inconsistent support across email
* clients (including Gmail for non-Google accounts). Plain `<br>` line
* breaks are the standard, client-agnostic way to preserve plain-text
* formatting in an HTML alternative part.
* This avoids the narrow hard-wrapped rendering Gmail uses for `text/plain`.
*/
export function plainTextToHtml(body: string): string {
const normalized = body.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
const paragraphs = normalized.split(/\n{2,}/)
const htmlParagraphs = paragraphs.map((paragraph) => {
const escaped = escapeHtml(paragraph).replace(/\n/g, '<br>')
return `<p>${escaped}</p>`
})
return `<!DOCTYPE html><html><body>${htmlParagraphs.join('')}</body></html>`
const escaped = escapeHtml(normalized).replace(/\n/g, '<br>')
return `<!DOCTYPE html><html><body>${escaped}</body></html>`
}
/**