// 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 { buildQuoteHeader, type QuoteHeaderLabels } from './quote-header';
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;
labels?: QuoteHeaderLabels;
}
function escapeForHtmlBody(text: string): string {
return escapeHtml(text).replace(/\n/g, '
');
}
const BLOCKQUOTE_STYLE = 'margin:0 0 0 0.8ex;border-left:2px solid #ccc;padding-left:1ex';
/**
* Marker attribute on the wrapper that holds the quoted original (header +
* body). The signature splice uses it to place the signature above the quote,
* and the attachment reminder strips it before keyword matching (#570).
*/
export const QUOTED_BLOCK_ATTR = 'data-quoted-html';
export const QUOTED_BLOCK_START = `
${quotedBody}` : `${header.html}${quotedBody}`; return `
` 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) }; } // Transparent 1x1 GIF used as a stand-in src while the real inline image is // being fetched from JMAP (webmail #163). export const INLINE_IMAGE_PLACEHOLDER = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; /** * Rewrites `
` references into * `
` so the editor can render the * quoted original (a `cid:` URL would show a broken image) while still * carrying the cid through edits. Returns the cids found. */ export function rewriteCidImagesForEditor(html: string): { html: string; cids: string[] } { if (!html || html.indexOf('cid:') === -1) return { html, cids: [] }; const cids = new Set
(); const out = html.replace( / ]*?)\ssrc=("cid:([^"]*)"|'cid:([^']*)')([^>]*)>/gi, (_full, before: string, _q: string, dq: string | undefined, sq: string | undefined, after: string) => { const cid = (dq ?? sq ?? '').trim(); if (!cid) return _full; cids.add(cid); const rest = (before + after).replace(/\sdata-cid=("[^"]*"|'[^']*')/gi, '').trim(); return `
`; }, ); return { html: out, cids: Array.from(cids) }; } /** * Replace the placeholder src on `
` elements with the * resolved data URL once the inline blob has been fetched. */ export function replaceInlineImagePlaceholders( html: string, cidToDataUrl: Map
, ): string { if (!html || cidToDataUrl.size === 0 || html.indexOf('data-cid') === -1) return html; return html.replace( / ]*?)\sdata-cid=("([^"]*)"|'([^']*)')([^>]*)>/gi, (full, before: string, _q: string, dq: string | undefined, sq: string | undefined, after: string) => { const cid = dq ?? sq ?? ''; const dataUrl = cidToDataUrl.get(cid); if (!dataUrl) return full; const rest = (before + after).replace(/\ssrc=("[^"]*"|'[^']*')/gi, '').trim(); return `
`; }, ); } /** * Sniffs the real image MIME type from a blob's leading magic bytes, returning * null when the bytes aren't a recognizable image (webmail #543). */ export function sniffImageMime(bytes: Uint8Array): string | null { if (!bytes || bytes.length < 4) return null; if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) return 'image/png'; if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return 'image/jpeg'; if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38) return 'image/gif'; if ( bytes.length >= 12 && bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 && bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50 ) return 'image/webp'; if (bytes[0] === 0x42 && bytes[1] === 0x4d) return 'image/bmp'; return null; }