// Builds the self-contained HTML document for the RichTextEditor WebView. // Kept as a pure module (no react-native imports) so tests can assert the // embedded page script stays syntactically valid JavaScript. // // IMPORTANT: the page script lives inside a JS template literal. Backslash // escapes are "cooked" by the template (`\/` becomes `/`, `\s` becomes `s`), // which once shipped an invalid regex that killed the whole inline script // with a SyntaxError (issue #11 — Send stayed disabled because the editor // never posted `change` messages). Never rely on a backslash reaching the // page verbatim; write escape-free code or double the backslash. import type { ThemePalette } from '../theme/tokens'; // Minimum visible editor height (px). The editor auto-grows beyond this as the // user types, and the parent ScrollView handles overflow. export const MIN_EDITOR_HEIGHT = 220; // Shortest gap between two `change` posts while typing (see the page script). // Readers that need the current body - send, save, format switch - ask the // page with `getHtml` instead of waiting for the next post. export const CHANGE_THROTTLE_MS = 400; /** * The editor page's Content-Security-Policy, mirroring the viewer's * (`src/lib/email-html.ts`). Nothing but images, styles and our own inline * script may load. With `blockRemoteImages` images and fonts are limited to * what the composer produces itself - `data:` (inline and hydrated `cid:` * images) and `blob:` (pasted images) - so a quoted original's tracking * pixels and remote images don't fire just because it is replied to. */ export function buildEditorCsp(blockRemoteImages: boolean): string { const resources = blockRemoteImages ? ['img-src data: blob:', 'font-src data:', "media-src 'none'"] : ['img-src data: blob: https: http:', 'font-src data: https: http:', 'media-src data: https: http:']; return [ "default-src 'none'", "script-src 'unsafe-inline'", "style-src 'unsafe-inline'", ...resources, "base-uri 'none'", "form-action 'none'", ].join('; '); } /** * Whether the composer must open the editor with remote images blocked: it * is seeded with someone else's HTML (the quoted original of a reply or * forward, or a reopened draft that carries a quote) and the viewer would * have shown that message with its remote content blocked too. A reopened * draft doesn't say whose message it quotes, so its quote counts as * untrusted. */ export function shouldBlockEditorRemoteImages(opts: { /** The HTML the editor is seeded with from a message, if any. */ seedHtml?: string | null; isDraft: boolean; externalContentPolicy: 'allow' | 'block' | 'ask'; /** The quoted message's sender is trusted (ignored for drafts). */ senderTrusted: boolean; }): boolean { const { seedHtml, isDraft, externalContentPolicy, senderTrusted } = opts; if (!seedHtml || externalContentPolicy === 'allow') return false; if (isDraft) return /\sdata-quoted-html=|
`; }