/** * Turns a message's sanitized HTML into something a frame can actually show. * * Two things were missing and both looked like "images are broken": a message * written by any normal mail client references its own pictures as * src="cid:...", which resolves against nothing in a browser; and the frame was * handed a bare fragment with no document around it, so mail written for a * white page rendered as dark text on a dark surface with no width limit. */ /** The parts the API returns alongside the body, keyed by Content-ID. */ export type InlineImages = Record; /** * Replaces src="cid:x" with the data: URI the server delivered for that part. * * A reference with no matching part loses its source entirely rather than * keeping a cid: the browser cannot fetch: an unresolvable src draws a * broken-image icon, while no src at all falls back to the alt text. */ export function resolveInlineImages(html: string, images: InlineImages): string { if (!html) return ""; return html.replace( /(]*?)\ssrc\s*=\s*("|')cid:([^"']*)\2/gi, (_match, head: string, _quote: string, reference: string) => { // Clients disagree about whether the reference keeps the angle brackets // the Content-ID header carries, so both spellings resolve. const key = decodeURIComponent(reference.trim()).replace(/^<|>$/g, ""); const resolved = images[key]; return resolved ? `${head} src="${resolved}"` : head; }, ); } /** * Whether a message was designed, as a newsletter or a receipt is, rather than * just written, as a letter between two people is. * * A designed message brings its own colours, tables and background images, and * they only make sense on the white sheet it was drawn for. A written one has * nothing of the kind and reads best when it takes the app's own colours - on a * dark ground, a plain note as a bright white slab is the loudest thing on the * screen and says nothing the words do not. * * Plain white is not design: Outlook paints it behind everything it sends. */ export function looksDesigned(html: string): boolean { if (!html) return false; const neutral = /^\s*(?:#f{3}(?:f{3})?|white|transparent|none|rgba?\(\s*255\s*,\s*255\s*,\s*255(?:\s*,\s*(?:1|1\.0|0?\.\d+))?\s*\)|rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\))\s*(?:!important)?\s*;?\s*$/i; // bgcolor="#336699" and background="hero.png" are always a statement. for (const m of html.matchAll(/\b(?:bgcolor|background)\s*=\s*(["']?)([^"'\s>]+)\1/gi)) { if (!neutral.test(m[2])) return true; } // background / background-color in a style attribute or a ${bodyHtml} `; }