/**
* 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}