// The composer's per-message HTML <-> plain-text switch (#1022). Port of the // webmail's components/email/compose-format.ts. // // Both directions keep what the send, draft-save and identity-switch paths // rely on: whether (and where) the body carries the signature. An HTML body // carries it as a marker-bracketed range (`containsEmbeddedSignature`), a // plain-text body as the signature text after its separator. The converters // take the signature out in the source format and put it back, in the same // place, in the target format, so a switch never doubles or loses it. import { escapeHtml } from './email-html'; import { htmlToPlainText } from './compose-html'; import { buildEmbeddedSignatureHtml, containsEmbeddedSignature, getPlainTextSignature, plainTextBodyHasSignature, plainTextBodyWithoutSignature, spliceSignature, wantsSignatureSeparator, type SignatureSource, } from './signature-utils'; /** * The format a composer opens in. A reopened draft keeps the one it was * written in - an HTML draft used to be flattened by the "plain text only" * setting on its first autosave. An empty draft and every new message follow * the setting. */ export function initialPlainTextMode( draft: { htmlBody?: string; textBody?: string } | null | undefined, plainTextSetting: boolean, ): boolean { if (draft?.htmlBody) return false; if (draft?.textBody) return true; return plainTextSetting; } /** Plain text as editor HTML: a paragraph per blank line, `
` per newline. */ export function plainTextToComposerHtml(text: string): string { if (!text) return ''; return text .replace(/\r\n?/g, '\n') .split(/\n{2,}/) .map((paragraph) => `

${escapeHtml(paragraph).replace(/\n/g, '
')}

`) .join(''); } function separatorText(separator: boolean): string { return separator ? '\n\n-- \n' : '\n\n'; } // Stands in for the signature while the rest of the body is converted. Control // characters survive `htmlToPlainText`, which only strips tags and entities. const SIGNATURE_TOKEN = '\u0003signature\u0003'; /** * The rich-text body as a plain-text body. Formatting and inline images are * dropped (that is the point of plain text); an embedded signature comes back * as the identity's plain-text signature, where it was: at the end, or above * the quote. */ export function htmlComposeBodyToPlainText( html: string, identity: SignatureSource, options: { separator: boolean }, ): string { if (!containsEmbeddedSignature(html)) return htmlToPlainText(html); const text = htmlToPlainText(spliceSignature(html, `

${SIGNATURE_TOKEN}

`)); const at = text.indexOf(SIGNATURE_TOKEN); if (at === -1) return text; const head = text.slice(0, at).replace(/\s+$/, ''); const tail = text.slice(at + SIGNATURE_TOKEN.length).replace(/^\s+/, ''); const signature = getPlainTextSignature(identity); const withSignature = signature ? `${head}${separatorText(wantsSignatureSeparator(options.separator, identity))}${signature}` : head; return tail ? `${withSignature}\n\n${tail}` : withSignature; } /** * A plain-text body as editor HTML. The identity's signature - after its * separator above the quote, or at the end - becomes the embedded, * marker-bracketed signature, so identity switches and sending keep working * as in a body that started out as HTML. */ export function plainComposeBodyToHtml( text: string, identity: SignatureSource, options: { separator: boolean }, ): string { const empty = '


'; const signature = getPlainTextSignature(identity); const embedded = signature ? buildEmbeddedSignatureHtml(identity, options) : ''; if (embedded) { const marker = `${separatorText(wantsSignatureSeparator(options.separator, identity))}${signature}`; const at = text.indexOf(marker); if (at !== -1) { const before = plainTextToComposerHtml(text.slice(0, at).replace(/^\n+/, '')); const after = plainTextToComposerHtml(text.slice(at + marker.length).replace(/^\n+/, '')); return `${before || empty}${embedded}${after}`; } if (plainTextBodyHasSignature(text, identity)) { const before = plainTextToComposerHtml(plainTextBodyWithoutSignature(text, identity).replace(/^\n+/, '')); return `${before || empty}${embedded}`; } } return plainTextToComposerHtml(text) || empty; }