// Build the initial HTML body for a new compose / reply / forward, mirroring // what the webmail composer (`components/email/email-composer.tsx`) produces. // All values are HTML-escaped before insertion into the document. import { escapeHtml, stripDangerousTags } from './email-html'; import type { TimeFormat } from '../stores/settings-store'; export interface ReplyMeta { from?: { email?: string; name?: string }; to?: Array<{ email?: string; name?: string }>; cc?: Array<{ email?: string; name?: string }>; subject?: string; body?: string; // plain-text body (preferred when htmlBody absent) htmlBody?: string; receivedAt?: string; } export interface QuoteHeaderOptions { timeFormat?: TimeFormat; locale?: string; unknownLabel?: string; } function escapeForHtmlBody(text: string): string { return escapeHtml(text).replace(/\n/g, '
'); } // Webmail's default quote header labels the sender by display name when there // is one, falling back to the bare address — not "Name ". function senderName(from?: { email?: string; name?: string }, unknownLabel = 'Unknown'): string { if (!from) return unknownLabel; return from.name || from.email || unknownLabel; } // Mirror the webmail quote-header date: locale- and 12/24h-aware, with a short // weekday (lib/quote-header.ts → formatDateTime with weekday/year/month/day). function formatQuoteDate(iso: string | undefined, timeFormat: TimeFormat, locale?: string): string { if (!iso) return ''; const d = new Date(iso); if (Number.isNaN(d.getTime())) return ''; const intlLocale = !locale || locale === 'en' ? 'en-US' : locale; return d.toLocaleString(intlLocale, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h', }); } const BLOCKQUOTE_STYLE = 'margin:0 0 0 0.8ex;border-left:2px solid #ccc;padding-left:1ex'; /** * Build the seed HTML for a new compose / reply / forward. Returns an empty * paragraph for plain compose (so the contenteditable has a caret target). * * The reply/forward quote header mirrors the webmail default header * (`lib/quote-header.ts`): a reply gets "On , wrote:" above a * blockquoted body; a forward gets a From/Date/Subject block followed by the * original body (not blockquoted). */ export function buildInitialHtml( mode: 'compose' | 'reply' | 'replyAll' | 'forward', reply?: ReplyMeta | null, opts: QuoteHeaderOptions = {}, ): string { if (!reply || mode === 'compose') return '


'; const { timeFormat = '24h', locale, unknownLabel = 'Unknown' } = opts; const date = formatQuoteDate(reply.receivedAt, timeFormat, locale); const fromStr = senderName(reply.from, unknownLabel); const subject = reply.subject ?? ''; const quotedBody = reply.htmlBody ? stripDangerousTags(reply.htmlBody) : reply.body ? escapeForHtmlBody(reply.body) : ''; if (!quotedBody) return '


'; if (mode === 'forward') { // Forwarded message block (From/Date/Subject), body appended unquoted. return [ '


', '
---------- Forwarded message ----------
', `From: ${escapeHtml(fromStr)}
`, `Date: ${escapeHtml(date)}
`, `Subject: ${escapeHtml(subject)}

`, '
', quotedBody, ].join(''); } // reply / replyAll const headerLine = date ? `On ${escapeHtml(date)}, ${escapeHtml(fromStr)} wrote:` : `${escapeHtml(fromStr)} wrote:`; return [ '


', `
${headerLine}
`, `
${quotedBody}
`, ].join(''); } /** * Strip HTML tags and decode common entities for a plain-text fallback body. * Mirrors the webmail's `htmlToPlainText` (DOMParser-based) since we don't * have DOMParser in the RN runtime. */ export function htmlToPlainText(html: string): string { if (!html) return ''; let s = html; // Treat block-level boundaries and
as newlines before stripping tags. s = s.replace(//gi, '\n'); // Paragraph-level closes get a blank line; list/heading/cell closes a single // newline so they stack tightly. s = s.replace(/<\/(p|div|blockquote|pre)>/gi, '\n\n'); s = s.replace(/<\/(li|h[1-6]|tr)>/gi, '\n'); s = s.replace(/]*>/gi, '• '); s = s.replace(/<[^>]+>/g, ''); // Decode the most common entities; leave the rest as-is. s = s .replace(/ /gi, ' ') .replace(/&/gi, '&') .replace(/</gi, '<') .replace(/>/gi, '>') .replace(/"/gi, '"') .replace(/'/gi, "'"); // Collapse runs of >2 newlines and trim trailing whitespace per line. s = s.replace(/\n{3,}/g, '\n\n').replace(/[ \t]+\n/g, '\n'); return s.trim(); } /** * Walk the editor HTML, replacing `` with `` * (and dropping the data-cid attribute). Returns the rewritten HTML and the * set of CIDs actually referenced so the sender can include exactly the * needed inline parts. */ export function rewriteInlineImages(html: string): { html: string; usedCids: string[] } { if (!html || !/data-cid=/i.test(html)) return { html, usedCids: [] }; const used = new Set(); const out = html.replace( /]*?)\sdata-cid=("([^"]*)"|'([^']*)')([^>]*)>/gi, (_full, before: string, _quoted: string, dq: string | undefined, sq: string | undefined, after: string) => { const cid = dq ?? sq ?? ''; if (!cid) return _full; used.add(cid); // Drop any existing src= and replace with cid: form. const merged = (before + after) .replace(/\ssrc=("[^"]*"|'[^']*')/gi, '') .trim(); const space = merged ? ' ' + merged : ''; return ``; }, ); return { html: out, usedCids: Array.from(used) }; }