@aglyn/shared-util-email 1.0.0-beta.202 → 1.0.0-beta.204

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/package.json +3 -3
  2. package/src/index.d.ts +3 -0
  3. package/src/index.js +9 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/email-design-chrome.d.ts +61 -0
  6. package/src/lib/email-design-chrome.js +99 -0
  7. package/src/lib/email-design-chrome.js.map +1 -0
  8. package/src/lib/email-linkify.d.ts +3 -1
  9. package/src/lib/email-linkify.js +2 -2
  10. package/src/lib/email-linkify.js.map +1 -1
  11. package/src/lib/email-palette.d.ts +137 -0
  12. package/src/lib/email-palette.js +756 -0
  13. package/src/lib/email-palette.js.map +1 -0
  14. package/src/lib/email-render.d.ts +25 -0
  15. package/src/lib/email-render.js +27 -9
  16. package/src/lib/email-render.js.map +1 -1
  17. package/src/lib/held-page.d.ts +117 -0
  18. package/src/lib/held-page.js +172 -0
  19. package/src/lib/held-page.js.map +1 -0
  20. package/src/lib/host-email-render.d.ts +34 -7
  21. package/src/lib/host-email-render.js +69 -25
  22. package/src/lib/host-email-render.js.map +1 -1
  23. package/src/lib/hosted-page-screen.js +8 -6
  24. package/src/lib/hosted-page-screen.js.map +1 -1
  25. package/src/lib/outbound-phishing-screen.d.ts +24 -0
  26. package/src/lib/outbound-phishing-screen.js +81 -2
  27. package/src/lib/outbound-phishing-screen.js.map +1 -1
  28. package/src/lib/risk-notice-catalog.d.ts +204 -0
  29. package/src/lib/risk-notice-catalog.js +1702 -0
  30. package/src/lib/risk-notice-catalog.js.map +1 -0
  31. package/src/lib/risk-notice-emails.d.ts +25 -0
  32. package/src/lib/risk-notice-emails.js +182 -0
  33. package/src/lib/risk-notice-emails.js.map +1 -0
  34. package/src/lib/sender-readiness.d.ts +155 -0
  35. package/src/lib/sender-readiness.js +432 -0
  36. package/src/lib/sender-readiness.js.map +1 -0
  37. package/src/lib/system-email-catalog.js +7 -1
  38. package/src/lib/system-email-catalog.js.map +1 -1
  39. package/src/lib/tenant-email-catalog.d.ts +7 -0
  40. package/src/lib/tenant-email-catalog.js +183 -13
  41. package/src/lib/tenant-email-catalog.js.map +1 -1
  42. package/src/lib/text-email-html.d.ts +26 -0
  43. package/src/lib/text-email-html.js +56 -1
  44. package/src/lib/text-email-html.js.map +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/email-palette.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The palette an email is painted with (AGL-3370).\n *\n * A color an author picks in the Besigner is stored as a palette TOKEN PATH\n * (`primary.main`, `grey.600`), which MUI resolves on a web page and a mail\n * client never will. So an email needs the same palette the site renders\n * with, as plain data, before it writes a single `style` attribute.\n *\n * That palette is built by `@aglyn/shared-ui-theme` — MUI's `createTheme`,\n * then `createResponsiveTheme`'s shade and accessibility passes — and this\n * lib may not import it: a `type:util` package cannot stand on a `type:ui`\n * one, and a server rendering mail has no business loading MUI to do it. What\n * follows is that derivation ported, light scheme only, with the INPUT colors\n * of the two bases copied and everything else computed the way MUI computes\n * it. `email-palette-parity.spec.ts` in `shared-ui-theme` builds both and\n * holds them equal, so the copy cannot drift without a red.\n */\n\n/**\n * A light-scheme MUI palette as plain data: `primary`, `secondary`,\n * `tertiary`, `surface`, `error`, `warning`, `info`, `success` (each\n * `{ main, light, dark, contrastText }`), `background`, `text`, `tint`,\n * `divider`, `inputOutline`, `grey` and `common`.\n */\nexport type EmailPalette = Readonly<Record<string, unknown>>\n\n/** One authored palette color: a `main`, and any shade the author also set. */\nexport interface EmailPaletteColorInput {\n main?: string\n light?: string\n dark?: string\n contrastText?: string\n}\n\n/**\n * The shape of `HostThemeSchemeColors` (`@aglyn/shared-data-types`), spelled\n * out so this lib needs no dependency on the types package for one shape. An\n * interface has no index signature, so without this a site's typed colors\n * would not be assignable to a plain record.\n */\nexport interface EmailPaletteSchemeColors {\n primary?: EmailPaletteColorInput\n secondary?: EmailPaletteColorInput\n tertiary?: EmailPaletteColorInput\n surface?: EmailPaletteColorInput\n error?: EmailPaletteColorInput\n warning?: EmailPaletteColorInput\n info?: EmailPaletteColorInput\n success?: EmailPaletteColorInput\n background?: { default?: string; paper?: string }\n text?: { primary?: string; secondary?: string; disabled?: string }\n tint?: { primary?: string; secondary?: string; tertiary?: string }\n divider?: string\n}\n\nexport interface EmailPaletteInput {\n /**\n * The base a site or sender layers onto: `'platform'` is the console brand\n * (`consoleOptions`), `'tenant'` the neutral default a customer site wears\n * (`tenantOptions`). {@link emailPaletteBaseForHost} picks it for a site.\n */\n base: 'platform' | 'tenant'\n /**\n * A site's authored light-scheme colors — `host.theme.colorSchemes.light`.\n * Stored data, so every leaf is checked as it is read; a plain record is\n * accepted as readily as the typed shape.\n */\n colors?: EmailPaletteSchemeColors | Readonly<Record<string, unknown>> | null\n /**\n * A white-label org's brand color. It becomes `primary.main` and the whole\n * `primary` record is re-derived from it, exactly as a theme given\n * `palette.primary = { main }` would. Ignored unless it parses as a color.\n */\n primaryColor?: string | null\n}\n\ntype ColorRecord = Record<string, unknown>\ntype PaletteDraft = Record<string, unknown>\n\n// ── The two bases' authored inputs ─────────────────────────────────────────\n//\n// Copied from `console.theme.ts` (`colorScheme.light`) and `tenant.theme.ts`\n// (`tenantColorScheme.light`), minus the `svg*` records no email reads. Only\n// what those files AUTHOR is here; every shade they leave to MUI is derived\n// below, so the copy stays as short as the thing it mirrors.\n\nconst BRAND_GREY = {\n 50: '#FAFAFA',\n 100: '#F5F5F5',\n 200: '#EEEEEE',\n 300: '#E0E0E0',\n 400: '#BDBDBD',\n 500: '#9E9E9E',\n 600: '#757575',\n 700: '#616161',\n 800: '#424242',\n 900: '#212121',\n A100: '#D5D5D5',\n A200: '#AAAAAA',\n A400: '#303030',\n A700: '#616161',\n}\n\nconst PLATFORM_LIGHT_INPUT: PaletteDraft = {\n mode: 'light',\n primary: { main: '#00b0ff', dark: '#0077ad', contrastText: '#FFFFFF' },\n secondary: { main: '#e040fb', dark: '#9d2db0', contrastText: '#FFFFFF' },\n tertiary: { main: '#404C5C', contrastText: '#FFFFFF' },\n surface: { main: '#F8F9FA', contrastText: '#000000' },\n tint: { primary: '#E6F5FF', secondary: '#FBE6FE', tertiary: '#EEF0F2' },\n inputOutline: 'rgba(0, 0, 0, 0.23)',\n background: { default: '#F5F5F5', paper: '#FFFFFF' },\n info: { main: '#1878cd', contrastText: '#FFFFFF' },\n error: { main: '#e32c27', contrastText: '#FFFFFF' },\n success: { main: '#4CAF50', contrastText: '#000000DE' },\n warning: { main: '#FFAB40', contrastText: '#000000DE' },\n grey: BRAND_GREY,\n}\n\nconst TENANT_LIGHT_INPUT: PaletteDraft = {\n mode: 'light',\n primary: { main: '#1976d2', dark: '#125393', contrastText: '#FFFFFF' },\n secondary: { main: '#9c27b0', dark: '#6d1b7b', contrastText: '#FFFFFF' },\n tertiary: { main: '#4a5568', dark: '#343b49', contrastText: '#FFFFFF' },\n surface: { main: '#f1f3f5', contrastText: '#000000DE' },\n tint: { primary: '#e8f1fb', secondary: '#f5e9f7', tertiary: '#eef0f3' },\n inputOutline: 'rgba(0, 0, 0, 0.23)',\n background: { default: '#fafafa', paper: '#ffffff' },\n info: { main: '#0288d1', dark: '#015f92', contrastText: '#000000DE' },\n error: { main: '#d32f2f', dark: '#942121', contrastText: '#FFFFFF' },\n success: { main: '#2e7d32', dark: '#205823', contrastText: '#FFFFFF' },\n warning: { main: '#ed6c02', dark: '#a64c01', contrastText: '#000000DE' },\n grey: BRAND_GREY,\n}\n\n// ── MUI's light-mode constants (`createPalette`, `colors/*`) ───────────────\n\nconst MUI_COMMON = { black: '#000', white: '#fff' }\n\nconst MUI_GREY = {\n 50: '#fafafa',\n 100: '#f5f5f5',\n 200: '#eeeeee',\n 300: '#e0e0e0',\n 400: '#bdbdbd',\n 500: '#9e9e9e',\n 600: '#757575',\n 700: '#616161',\n 800: '#424242',\n 900: '#212121',\n A100: '#f5f5f5',\n A200: '#eeeeee',\n A400: '#bdbdbd',\n A700: '#616161',\n}\n\n/** `getLight()` — the mode-hydrated slots, minus `action` (no email reads it). */\nfunction muiLightDefaults(): PaletteDraft {\n return {\n text: {\n primary: 'rgba(0, 0, 0, 0.87)',\n secondary: 'rgba(0, 0, 0, 0.6)',\n disabled: 'rgba(0, 0, 0, 0.38)',\n },\n divider: 'rgba(0, 0, 0, 0.12)',\n background: { paper: MUI_COMMON.white, default: MUI_COMMON.white },\n }\n}\n\n/** MUI's `light.text.primary`, the dark ink `getContrastText` falls back to. */\nconst MUI_DARK_INK = 'rgba(0, 0, 0, 0.87)'\nconst MUI_CONTRAST_THRESHOLD = 3\n/** MUI's default `tonalOffset` of 0.2, split the way `addLightOrDark` splits it. */\nconst TONAL_OFFSET_LIGHT = 0.2\nconst TONAL_OFFSET_DARK = 0.2 * 1.5\n\n/** The six colors `createPalette` augments. Both bases author every one. */\nconst MUI_AUGMENTED_KEYS = [\n 'primary',\n 'secondary',\n 'error',\n 'warning',\n 'info',\n 'success',\n] as const\n\n/** `createResponsiveTheme`'s FOREGROUND_COLOR_KEYS: `dark` is accent text. */\nconst FOREGROUND_COLOR_KEYS: ReadonlyArray<string> = [\n 'primary',\n 'secondary',\n 'tertiary',\n 'error',\n 'warning',\n 'info',\n 'success',\n]\n/** …and `contrastText` is text wherever it lives, so `surface` joins. */\nconst CONTRAST_TEXT_COLOR_KEYS: ReadonlyArray<string> = [\n ...FOREGROUND_COLOR_KEYS,\n 'surface',\n]\n\n// ── MUI's colorManipulator, ported ─────────────────────────────────────────\n//\n// Byte-for-byte in behavior, including the parts that look odd: `recompose`\n// truncates rgb channels with `parseInt`, and `getLuminance` rounds to three\n// places. The palette's derived shades are strings, so a port that rounded\n// \"better\" would be a port that disagrees.\n\ninterface DecomposedColor {\n type: string\n values: Array<number>\n colorSpace?: string\n}\n\nfunction hexToRgb(color: string): string {\n const hex = color.slice(1)\n const pattern = new RegExp(`.{1,${hex.length >= 6 ? 2 : 1}}`, 'g')\n let parts: Array<string> | null = hex.match(pattern)\n if (parts && parts[0].length === 1) parts = parts.map((part) => part + part)\n return parts\n ? `rgb${parts.length === 4 ? 'a' : ''}(${parts\n .map((part, index) =>\n index < 3\n ? parseInt(part, 16)\n : Math.round((parseInt(part, 16) / 255) * 1000) / 1000,\n )\n .join(', ')})`\n : ''\n}\n\nfunction decomposeColor(color: string): DecomposedColor {\n if (color.charAt(0) === '#') return decomposeColor(hexToRgb(color))\n const marker = color.indexOf('(')\n const type = color.substring(0, marker)\n if (!['rgb', 'rgba', 'hsl', 'hsla', 'color'].includes(type)) {\n throw new Error(`email-palette: unsupported color \"${color}\"`)\n }\n const body = color.substring(marker + 1, color.length - 1)\n let raw: Array<string>\n let colorSpace: string | undefined\n if (type === 'color') {\n raw = body.split(' ')\n colorSpace = raw.shift()\n if (raw.length === 4 && raw[3].charAt(0) === '/') raw[3] = raw[3].slice(1)\n if (\n !['srgb', 'display-p3', 'a98-rgb', 'prophoto-rgb', 'rec-2020'].includes(\n colorSpace ?? '',\n )\n ) {\n throw new Error(`email-palette: unsupported color space \"${colorSpace}\"`)\n }\n } else {\n raw = body.split(',')\n }\n return { type, values: raw.map((value) => parseFloat(value)), colorSpace }\n}\n\nfunction recomposeColor({ type, colorSpace, values }: DecomposedColor): string {\n let parts: Array<string | number> = values\n if (type.includes('rgb')) {\n parts = values.map((value, index) =>\n index < 3 ? parseInt(String(value), 10) : value,\n )\n } else if (type.includes('hsl')) {\n parts = [...values]\n parts[1] = `${values[1]}%`\n parts[2] = `${values[2]}%`\n }\n const body = type.includes('color')\n ? `${colorSpace} ${parts.join(' ')}`\n : parts.join(', ')\n return `${type}(${body})`\n}\n\nfunction intToHex(value: number): string {\n const hex = value.toString(16)\n return hex.length === 1 ? `0${hex}` : hex\n}\n\nfunction rgbToHex(color: string): string {\n if (color.startsWith('#')) return color\n const { values } = decomposeColor(color)\n return `#${values\n .map((value, index) => intToHex(index === 3 ? Math.round(255 * value) : value))\n .join('')}`\n}\n\nfunction hslToRgb(color: string | DecomposedColor): string {\n const decomposed = typeof color === 'string' ? decomposeColor(color) : color\n const { values } = decomposed\n const hue = values[0]\n const saturation = values[1] / 100\n const lightness = values[2] / 100\n const chroma = saturation * Math.min(lightness, 1 - lightness)\n const channel = (n: number, k = (n + hue / 30) % 12) =>\n lightness - chroma * Math.max(Math.min(k - 3, 9 - k, 1), -1)\n const rgb = [\n Math.round(channel(0) * 255),\n Math.round(channel(8) * 255),\n Math.round(channel(4) * 255),\n ]\n let type = 'rgb'\n if (decomposed.type === 'hsla') {\n type += 'a'\n rgb.push(values[3])\n }\n return recomposeColor({ type, values: rgb })\n}\n\nfunction clampUnit(value: number): number {\n return Math.min(1, Math.max(0, value))\n}\n\nfunction lighten(color: string, coefficient: number): string {\n const decomposed = decomposeColor(color)\n const amount = clampUnit(coefficient)\n const { type, values } = decomposed\n if (type.includes('hsl')) {\n values[2] += (100 - values[2]) * amount\n } else if (type.includes('rgb')) {\n for (let i = 0; i < 3; i += 1) values[i] += (255 - values[i]) * amount\n } else if (type.includes('color')) {\n for (let i = 0; i < 3; i += 1) values[i] += (1 - values[i]) * amount\n }\n return recomposeColor(decomposed)\n}\n\nfunction darken(color: string, coefficient: number): string {\n const decomposed = decomposeColor(color)\n const amount = clampUnit(coefficient)\n const { type, values } = decomposed\n if (type.includes('hsl')) {\n values[2] *= 1 - amount\n } else if (type.includes('rgb') || type.includes('color')) {\n for (let i = 0; i < 3; i += 1) values[i] *= 1 - amount\n }\n return recomposeColor(decomposed)\n}\n\nfunction srgbChannelToLinear(value: number): number {\n return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4\n}\n\n/** MUI's `getLuminance`: WCAG luminance, rounded to three places. */\nfunction muiLuminance(color: string): number {\n const decomposed = decomposeColor(color)\n const rgb =\n decomposed.type === 'hsl' || decomposed.type === 'hsla'\n ? decomposeColor(hslToRgb(decomposed)).values\n : decomposed.values\n const linear = rgb.map((value) =>\n srgbChannelToLinear(decomposed.type !== 'color' ? value / 255 : value),\n )\n return Number(\n (0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]).toFixed(3),\n )\n}\n\n/** MUI's `getContrastRatio`, on the rounded luminance. */\nfunction muiContrastRatio(foreground: string, background: string): number {\n const a = muiLuminance(foreground)\n const b = muiLuminance(background)\n return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05)\n}\n\n// ── `accessible-shade.ts`, ported ──────────────────────────────────────────\n\nconst AA_TEXT_CONTRAST = 4.5\n\n/** Full-precision WCAG luminance, as `accessible-shade.ts` measures it. */\nfunction relativeLuminance(color: string): number {\n const decomposed = decomposeColor(color)\n if (decomposed.type === 'color') {\n throw new Error(`email-palette: unsupported color format \"${color}\"`)\n }\n const values = decomposed.type.startsWith('hsl')\n ? decomposeColor(hslToRgb(color)).values\n : decomposed.values\n return (\n 0.2126 * srgbChannelToLinear(values[0] / 255) +\n 0.7152 * srgbChannelToLinear(values[1] / 255) +\n 0.0722 * srgbChannelToLinear(values[2] / 255)\n )\n}\n\nfunction contrastRatio(colorA: string, colorB: string): number {\n const a = relativeLuminance(colorA)\n const b = relativeLuminance(colorB)\n return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05)\n}\n\nfunction meetsContrast(\n foreground: string,\n backgrounds: ReadonlyArray<string>,\n): boolean {\n return backgrounds.every(\n (background) => contrastRatio(foreground, background) >= AA_TEXT_CONTRAST,\n )\n}\n\nfunction toHslValues(color: string): [number, number, number] {\n const decomposed = decomposeColor(color)\n if (decomposed.type.startsWith('hsl')) {\n const [h, s, l] = decomposed.values\n return [h, s / 100, l / 100]\n }\n const r = decomposed.values[0] / 255\n const g = decomposed.values[1] / 255\n const b = decomposed.values[2] / 255\n const max = Math.max(r, g, b)\n const min = Math.min(r, g, b)\n const lightness = (max + min) / 2\n if (max === min) return [0, 0, lightness]\n const delta = max - min\n const saturation =\n lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min)\n let hue: number\n if (max === r) hue = (g - b) / delta + (g < b ? 6 : 0)\n else if (max === g) hue = (b - r) / delta + 2\n else hue = (r - g) / delta + 4\n return [hue * 60, saturation, lightness]\n}\n\nfunction hslToHex(hue: number, saturation: number, lightness: number): string {\n return rgbToHex(\n hslToRgb(`hsl(${hue}, ${saturation * 100}%, ${lightness * 100}%)`),\n )\n}\n\n/** The nearest shade of `color`, walking lightness one way, that clears AA. */\nfunction accessibleShade(\n color: string,\n backgrounds: ReadonlyArray<string>,\n direction: 'darken' | 'lighten',\n): string {\n if (!backgrounds.length || meetsContrast(color, backgrounds)) return color\n const [hue, saturation, initialLightness] = toHslValues(color)\n const delta = direction === 'lighten' ? 0.01 : -0.01\n let lightness = initialLightness\n let candidate = color\n for (let i = 0; i < 120; i += 1) {\n lightness = Math.min(1, Math.max(0, lightness + delta))\n candidate = hslToHex(hue, saturation, lightness)\n if (meetsContrast(candidate, backgrounds)) return candidate\n if (lightness === 0 || lightness === 1) return candidate\n }\n return candidate\n}\n\n// ── Palette assembly ───────────────────────────────────────────────────────\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** MUI's `deepmerge` with `clone: true`, for plain data. */\nfunction deepMerge(target: PaletteDraft, source: PaletteDraft): PaletteDraft {\n const output: PaletteDraft = { ...target }\n for (const key of Object.keys(source)) {\n const next = source[key]\n const current = target[key]\n if (\n isRecord(next) &&\n Object.prototype.hasOwnProperty.call(target, key) &&\n isRecord(current)\n ) {\n output[key] = deepMerge(current, next)\n } else {\n output[key] = isRecord(next) ? deepMerge({}, next) : next\n }\n }\n return output\n}\n\n/**\n * `createPalette`'s `augmentColor` for an object with a `main`: fill `light`\n * and `dark` from the tonal offset, and `contrastText` by MUI's threshold.\n */\nfunction augmentColor(color: ColorRecord): ColorRecord {\n const augmented = { ...color }\n const main = augmented['main']\n if (typeof main !== 'string') {\n throw new Error('email-palette: a palette color needs a string `main`')\n }\n if (!augmented['light']) augmented['light'] = lighten(main, TONAL_OFFSET_LIGHT)\n if (!augmented['dark']) augmented['dark'] = darken(main, TONAL_OFFSET_DARK)\n if (!augmented['contrastText']) {\n augmented['contrastText'] =\n muiContrastRatio(main, MUI_COMMON.white) >= MUI_CONTRAST_THRESHOLD\n ? MUI_COMMON.white\n : MUI_DARK_INK\n }\n return augmented\n}\n\n/**\n * `createResponsiveTheme`'s `addShadeVariants`, for the two colors MUI does\n * not know about. Its contrast pick is handed the DARK tonal offset (0.3) as\n * the threshold, so it lands on white for any color at all; the port keeps\n * that, because the parity it owes is to what the site renders, and any\n * pairing below AA is repaired by the accessibility pass that follows.\n */\nfunction addShadeVariants(color: unknown): void {\n if (!isRecord(color) || !color['main']) return\n const main = color['main'] as string\n if (!color['dark']) color['dark'] = darken(main, TONAL_OFFSET_DARK)\n if (!color['light']) color['light'] = lighten(main, TONAL_OFFSET_LIGHT)\n if (!color['contrastText']) {\n color['contrastText'] =\n muiContrastRatio(main, '#FFFFFF') >= TONAL_OFFSET_DARK\n ? '#FFFFFF'\n : 'rgba(0, 0, 0, 0.87)'\n }\n}\n\n/**\n * `ensureAccessibleShades` for the light scheme: a DERIVED `dark` that misses\n * 4.5:1 on either background is walked darker, and a derived `contrastText`\n * that misses 4.5:1 on its `main` is walked toward the pole with more room.\n * Whatever the input authored passes through untouched.\n */\nfunction ensureAccessibleShades(\n palette: PaletteDraft,\n input: PaletteDraft,\n): void {\n const background = isRecord(palette['background']) ? palette['background'] : {}\n const backgrounds = [background['default'], background['paper']].filter(\n (value): value is string => typeof value === 'string',\n )\n if (!backgrounds.length) return\n\n for (const key of CONTRAST_TEXT_COLOR_KEYS) {\n const color = palette[key]\n if (!isRecord(color) || typeof color['main'] !== 'string') continue\n const provided = input[key]\n if (isRecord(provided) && !('main' in provided)) continue\n const explicit = isRecord(provided) ? provided : {}\n const main = color['main']\n\n try {\n if (\n FOREGROUND_COLOR_KEYS.includes(key) &&\n !explicit['dark'] &&\n typeof color['dark'] === 'string' &&\n !meetsContrast(color['dark'], backgrounds)\n ) {\n color['dark'] = accessibleShade(color['dark'], backgrounds, 'darken')\n }\n if (\n !explicit['contrastText'] &&\n typeof color['contrastText'] === 'string' &&\n !meetsContrast(color['contrastText'], [main])\n ) {\n const direction =\n contrastRatio('#fff', main) >= contrastRatio('#000', main)\n ? 'lighten'\n : 'darken'\n color['contrastText'] = accessibleShade(\n color['contrastText'],\n [main],\n direction,\n )\n }\n } catch {\n // An unparseable color is left as derived, as the theme leaves it.\n }\n }\n}\n\n/** `createTheme` + `createResponsiveTheme`'s passes, palette only. */\nfunction derivePalette(input: PaletteDraft): PaletteDraft {\n const rest: PaletteDraft = { ...input }\n delete rest['mode']\n delete rest['contrastThreshold']\n delete rest['tonalOffset']\n\n const augmented: PaletteDraft = {}\n for (const key of MUI_AUGMENTED_KEYS) {\n const color = input[key]\n if (!isRecord(color)) {\n throw new Error(`email-palette: the base palette is missing \\`${key}\\``)\n }\n augmented[key] = augmentColor(color)\n }\n\n const palette = deepMerge(\n {\n common: { ...MUI_COMMON },\n mode: 'light',\n ...augmented,\n grey: MUI_GREY,\n ...muiLightDefaults(),\n },\n rest,\n )\n addShadeVariants(palette['tertiary'])\n addShadeVariants(palette['surface'])\n ensureAccessibleShades(palette, input)\n return palette\n}\n\n// ── A site's colors, as `hostThemeToThemeOptions` reads them ───────────────\n\nconst SCHEME_COLOR_KEYS = [\n 'primary',\n 'secondary',\n 'tertiary',\n 'surface',\n 'error',\n 'warning',\n 'info',\n 'success',\n] as const\n\n/** Truthy string, or nothing: the \"copy what was set\" test the theme uses. */\nfunction setString(value: unknown): string | undefined {\n return typeof value === 'string' && value ? value : undefined\n}\n\n/** `pickPaletteColor`: a color needs a `main`; its shades ride along if set. */\nfunction pickPaletteColor(color: unknown): ColorRecord | undefined {\n if (!isRecord(color)) return undefined\n const main = setString(color['main'])\n if (!main) return undefined\n const picked: ColorRecord = { main }\n for (const shade of ['light', 'dark', 'contrastText'] as const) {\n const value = setString(color[shade])\n if (value) picked[shade] = value\n }\n return picked\n}\n\n/** Picks the listed string leaves of a group that were set, or nothing. */\nfunction pickLeaves(\n group: unknown,\n keys: ReadonlyArray<string>,\n): Record<string, string> | undefined {\n if (!isRecord(group)) return undefined\n const picked: Record<string, string> = {}\n for (const key of keys) {\n const value = setString(group[key])\n if (value) picked[key] = value\n }\n return Object.keys(picked).length ? picked : undefined\n}\n\n/** `schemeColorsToPaletteOptions` for the light scheme. */\nfunction siteColorOverrides(\n input: EmailPaletteInput['colors'],\n): PaletteDraft {\n const overrides: PaletteDraft = { mode: 'light' }\n if (!isRecord(input)) return overrides\n const colors: Readonly<Record<string, unknown>> = input\n for (const key of SCHEME_COLOR_KEYS) {\n const color = pickPaletteColor(colors[key])\n if (color) overrides[key] = color\n }\n const background = pickLeaves(colors['background'], ['default', 'paper'])\n if (background) overrides['background'] = background\n const text = pickLeaves(colors['text'], ['primary', 'secondary', 'disabled'])\n if (text) overrides['text'] = text\n const tint = pickLeaves(colors['tint'], ['primary', 'secondary', 'tertiary'])\n if (tint) overrides['tint'] = tint\n const divider = setString(colors['divider'])\n if (divider) overrides['divider'] = divider\n return overrides\n}\n\n// ── Literal CSS colors ─────────────────────────────────────────────────────\n\nconst HEX_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i\nconst NUMBER = String.raw`[-+]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[-+]?\\d+)?(?:%|deg|grad|rad|turn)?`\n/**\n * `rgb()`, `rgba()`, `hsl()`, `hsla()` holding numbers and nothing else —\n * comma or space separated, with an optional `/ alpha`. Anything that could\n * close the declaration or open another (`;`, `\"`, `(`, `url`) cannot match.\n */\nconst FUNCTION_COLOR = new RegExp(\n String.raw`^(?:rgba?|hsla?)\\(\\s*${NUMBER}(?:\\s*,\\s*|\\s+)${NUMBER}(?:\\s*,\\s*|\\s+)${NUMBER}(?:(?:\\s*,\\s*|\\s*\\/\\s*)${NUMBER})?\\s*\\)$`,\n 'i',\n)\n/** A color MUI's own manipulators parse: hex, or a comma-separated function. */\nconst MUI_PARSEABLE_FUNCTION = new RegExp(\n String.raw`^(?:rgba?|hsla?)\\(\\s*${NUMBER}\\s*,\\s*${NUMBER}\\s*,\\s*${NUMBER}(?:\\s*,\\s*${NUMBER})?\\s*\\)$`,\n 'i',\n)\n\n/** CSS Color Module Level 4 named colors, plus `transparent`. */\nconst NAMED_COLORS: ReadonlySet<string> = new Set(\n (\n 'aliceblue antiquewhite aqua aquamarine azure beige bisque black ' +\n 'blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse ' +\n 'chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan ' +\n 'darkgoldenrod darkgray darkgreen darkgrey darkkhaki darkmagenta ' +\n 'darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen ' +\n 'darkslateblue darkslategray darkslategrey darkturquoise darkviolet ' +\n 'deeppink deepskyblue dimgray dimgrey dodgerblue firebrick floralwhite ' +\n 'forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green ' +\n 'greenyellow grey honeydew hotpink indianred indigo ivory khaki lavender ' +\n 'lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan ' +\n 'lightgoldenrodyellow lightgray lightgreen lightgrey lightpink ' +\n 'lightsalmon lightseagreen lightskyblue lightslategray lightslategrey ' +\n 'lightsteelblue lightyellow lime limegreen linen magenta maroon ' +\n 'mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen ' +\n 'mediumslateblue mediumspringgreen mediumturquoise mediumvioletred ' +\n 'midnightblue mintcream mistyrose moccasin navajowhite navy oldlace ' +\n 'olive olivedrab orange orangered orchid palegoldenrod palegreen ' +\n 'paleturquoise palevioletred papayawhip peachpuff peru pink plum ' +\n 'powderblue purple rebeccapurple red rosybrown royalblue saddlebrown ' +\n 'salmon sandybrown seagreen seashell sienna silver skyblue slateblue ' +\n 'slategray slategrey snow springgreen steelblue tan teal thistle tomato ' +\n 'turquoise violet wheat white whitesmoke yellow yellowgreen transparent'\n ).split(' '),\n)\n\n/** True for a literal CSS color that is safe to write into a style attribute. */\nfunction isLiteralColor(value: string): boolean {\n return (\n HEX_COLOR.test(value) ||\n FUNCTION_COLOR.test(value) ||\n NAMED_COLORS.has(value.toLowerCase())\n )\n}\n\n/** True for a color MUI can derive shades from without throwing. */\nfunction isDerivableColor(value: string): boolean {\n return HEX_COLOR.test(value) || MUI_PARSEABLE_FUNCTION.test(value)\n}\n\n// ── Public API ─────────────────────────────────────────────────────────────\n\n/**\n * The fully derived light palette a site or sender renders with: the\n * chosen base, the site's authored colors layered one level deep (a color's\n * whole record replaced, so MUI re-derives its shades from the new `main`),\n * then a white-label `primaryColor` over that.\n *\n * Equal, for every token the Besigner's color picker offers, to the palette\n * of `createResponsiveTheme` over `mergeThemeOptions(siteBaseOptions(…))`\n * and the host theme's light `hostThemeToThemeOptions`.\n *\n * Where the site's colors cannot be derived at all — a `main` MUI cannot\n * parse, on which `createTheme` itself throws — the email falls back to the\n * base rather than failing the send: there is no rendered page to agree with.\n */\nexport function buildEmailPalette(input: EmailPaletteInput): EmailPalette {\n const base =\n input.base === 'platform' ? PLATFORM_LIGHT_INPUT : TENANT_LIGHT_INPUT\n const primaryColor =\n typeof input.primaryColor === 'string' &&\n isDerivableColor(input.primaryColor.trim())\n ? input.primaryColor.trim()\n : undefined\n const brand: PaletteDraft = primaryColor\n ? { primary: { main: primaryColor } }\n : {}\n\n const layered = { ...base, ...siteColorOverrides(input.colors), ...brand }\n try {\n return derivePalette(layered)\n } catch {\n return derivePalette({ ...base, ...brand })\n }\n}\n\n/** The platform's own palette — the console brand, light scheme. */\nexport const PLATFORM_EMAIL_PALETTE: EmailPalette = buildEmailPalette({\n base: 'platform',\n})\n\n/** Palette token paths: a key, optionally one dot and a leaf (`grey.600`). */\nconst TOKEN_PATH = /^[a-zA-Z][a-zA-Z0-9]*(?:\\.[a-zA-Z0-9]+)?$/\n\nfunction lookupToken(\n palette: EmailPalette,\n path: string,\n): string | undefined {\n let node: unknown = palette\n for (const key of path.split('.')) {\n if (!isRecord(node) || !Object.prototype.hasOwnProperty.call(node, key)) {\n return undefined\n }\n node = node[key]\n }\n return typeof node === 'string' ? node : undefined\n}\n\n/**\n * A palette value as an opaque `#rrggbb`. Translucent values (`text.primary`\n * is `rgba(0, 0, 0, 0.87)`, some `contrastText`s are `#000000DE`) are\n * composited over the palette's paper, the ground an email body sits on:\n * Outlook's Word engine drops `rgba()` and eight-digit hex outright, and the\n * flattened color is what a reader sees on that ground anyway.\n */\nfunction toOpaqueHex(value: string, palette: EmailPalette): string | undefined {\n const channels = rgbaChannels(value)\n if (!channels) return undefined\n const [r, g, b, alpha] = channels\n if (alpha >= 1) return channelsToHex(r, g, b)\n const background = palette['background']\n const paper = isRecord(background) ? background['paper'] : undefined\n const ground =\n (typeof paper === 'string' ? rgbaChannels(paper) : undefined) ?? [\n 255, 255, 255, 1,\n ]\n const over = (fore: number, back: number) =>\n fore * alpha + back * (1 - alpha)\n return channelsToHex(over(r, ground[0]), over(g, ground[1]), over(b, ground[2]))\n}\n\nfunction rgbaChannels(\n value: string,\n): [number, number, number, number] | undefined {\n if (!isDerivableColor(value)) return undefined\n try {\n const decomposed = decomposeColor(value)\n const rgb = decomposed.type.startsWith('hsl')\n ? decomposeColor(hslToRgb(decomposed)).values\n : decomposed.values\n const alpha = decomposed.values.length > 3 ? decomposed.values[3] : 1\n const channels: [number, number, number, number] = [\n rgb[0],\n rgb[1],\n rgb[2],\n alpha,\n ]\n return channels.every(Number.isFinite) ? channels : undefined\n } catch {\n return undefined\n }\n}\n\nfunction channelsToHex(r: number, g: number, b: number): string {\n return `#${[r, g, b]\n .map((value) => intToHex(Math.round(Math.min(255, Math.max(0, value)))))\n .join('')}`\n}\n\n/**\n * Resolves a stored color prop for an email's markup.\n *\n * - A palette token path (`primary.main`, `grey.600`, `divider`) resolves\n * against `palette` to an opaque hex — or to nothing without a palette.\n * - A literal CSS color (hex, `rgb()`/`rgba()`/`hsl()`/`hsla()`,\n * `transparent`, a named color) passes through as written.\n * - Anything else — an unknown token, garbage, an empty value, and anything\n * that could step outside a `style` attribute — is `undefined`, so the\n * caller uses its own default.\n */\nexport function resolveEmailColor(\n value: unknown,\n palette: EmailPalette | undefined,\n): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n if (!trimmed) return undefined\n\n if (palette && TOKEN_PATH.test(trimmed)) {\n const resolved = lookupToken(palette, trimmed)\n if (resolved !== undefined) {\n const hex = toOpaqueHex(resolved, palette)\n if (hex) return hex\n if (isLiteralColor(resolved)) return resolved\n return undefined\n }\n }\n return isLiteralColor(trimmed) ? trimmed : undefined\n}\n\n// ── Which base a site wears ────────────────────────────────────────────────\n\n/**\n * The prefix the tenant middleware puts on a custom domain before it becomes\n * the `[host]` route segment — mirrored from `tenant.theme.ts`.\n */\nconst CNAME_PREFIX = 'cname--'\n\n/**\n * The hosts whose brand is the operator's own, mirrored from\n * `PLATFORM_BRAND_HOSTS` in `tenant.theme.ts` and read from the same\n * variable. Dot notation for the same reason it has there: Next substitutes\n * `process.env.NAME` textually and never the bracket form.\n */\nconst PLATFORM_BRAND_HOSTS: ReadonlySet<string> = new Set(\n (process.env.NEXT_PUBLIC_PLATFORM_BRAND_HOSTS ?? 'aglyn.com,aglyn.io')\n .split(',')\n .map((host) => host.trim().toLowerCase())\n .filter(Boolean),\n)\n\n/**\n * Which base a site's mail layers onto: `'platform'` for the operator's own\n * hosts, `'tenant'` for every customer site — the answer\n * `wearsPlatformBrand(hostBrandKey(host))` gives the page itself, so a site's\n * email and its published page start from the same palette.\n */\nexport function emailPaletteBaseForHost(\n host: { cname?: string | null; subdomain?: string | null } | null | undefined,\n): 'platform' | 'tenant' {\n const key = host?.cname || host?.subdomain || undefined\n if (!key) return 'tenant'\n const normalized = key.trim().toLowerCase()\n const bare = normalized.startsWith(CNAME_PREFIX)\n ? normalized.slice(CNAME_PREFIX.length)\n : normalized\n return PLATFORM_BRAND_HOSTS.has(bare) ? 'platform' : 'tenant'\n}\n"],"names":["process","BRAND_GREY","A100","A200","A400","A700","PLATFORM_LIGHT_INPUT","mode","primary","main","dark","contrastText","secondary","tertiary","surface","tint","inputOutline","background","default","paper","info","error","success","warning","grey","TENANT_LIGHT_INPUT","MUI_COMMON","black","white","MUI_GREY","muiLightDefaults","text","disabled","divider","MUI_DARK_INK","MUI_CONTRAST_THRESHOLD","TONAL_OFFSET_LIGHT","TONAL_OFFSET_DARK","MUI_AUGMENTED_KEYS","FOREGROUND_COLOR_KEYS","CONTRAST_TEXT_COLOR_KEYS","hexToRgb","color","hex","slice","pattern","RegExp","length","parts","match","map","part","index","parseInt","Math","round","join","decomposeColor","charAt","marker","indexOf","type","substring","includes","Error","body","raw","colorSpace","split","shift","values","value","parseFloat","recomposeColor","String","intToHex","toString","rgbToHex","startsWith","hslToRgb","decomposed","hue","saturation","lightness","chroma","min","channel","n","k","max","rgb","push","clampUnit","lighten","coefficient","amount","i","darken","srgbChannelToLinear","muiLuminance","linear","Number","toFixed","muiContrastRatio","foreground","a","b","AA_TEXT_CONTRAST","relativeLuminance","contrastRatio","colorA","colorB","meetsContrast","backgrounds","every","toHslValues","h","s","l","r","g","delta","hslToHex","accessibleShade","direction","initialLightness","candidate","isRecord","Array","isArray","deepMerge","target","source","output","key","Object","keys","next","current","prototype","hasOwnProperty","call","augmentColor","augmented","addShadeVariants","ensureAccessibleShades","palette","input","filter","provided","explicit","derivePalette","rest","common","SCHEME_COLOR_KEYS","setString","undefined","pickPaletteColor","picked","shade","pickLeaves","group","siteColorOverrides","overrides","colors","HEX_COLOR","NUMBER","FUNCTION_COLOR","MUI_PARSEABLE_FUNCTION","NAMED_COLORS","Set","isLiteralColor","test","has","toLowerCase","isDerivableColor","buildEmailPalette","base","primaryColor","trim","brand","layered","PLATFORM_EMAIL_PALETTE","TOKEN_PATH","lookupToken","path","node","toOpaqueHex","channels","rgbaChannels","alpha","channelsToHex","ground","over","fore","back","isFinite","resolveEmailColor","trimmed","resolved","CNAME_PREFIX","PLATFORM_BRAND_HOSTS","env","NEXT_PUBLIC_PLATFORM_BRAND_HOSTS","host","Boolean","emailPaletteBaseForHost","cname","subdomain","normalized","bare"],"mappings":"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"}
@@ -14,6 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
+ import { type EmailPalette } from './email-palette';
17
18
  export interface EmailRenderProduct {
18
19
  name: string;
19
20
  priceLabel?: string;
@@ -103,6 +104,24 @@ export interface EmailRenderOptions {
103
104
  * {@link EmailChrome}. Absent draws neither.
104
105
  */
105
106
  chrome?: EmailChrome;
107
+ /**
108
+ * The theme the email is sent under (AGL-3370): the console brand for the
109
+ * platform's own mail, a white-label org's color over it, or the site's
110
+ * theme for a site's mail. See {@link buildEmailPalette}.
111
+ *
112
+ * It does two things. A color an author picked in the Besigner is stored as
113
+ * a palette token (`primary.main`), which a mail client cannot resolve, so
114
+ * every color prop is resolved against this before it is written. And
115
+ * what nobody picked (a button's fill and label, a link) takes the theme's
116
+ * accent rather than a color of the renderer's own. Absent keeps the
117
+ * renderer's neutral defaults and drops an unresolvable token.
118
+ */
119
+ palette?: EmailPalette;
120
+ /**
121
+ * A button's corner radius in px: the theme's `shape.borderRadius`, which
122
+ * the Besigner canvas draws the button with. Absent is MUI's default, 4.
123
+ */
124
+ buttonRadius?: number;
106
125
  }
107
126
  /**
108
127
  * The row above the body that says who sent the mail.
@@ -155,6 +174,12 @@ export interface EmailChrome {
155
174
  header?: EmailChromeHeader;
156
175
  footer?: EmailChromeFooter;
157
176
  }
177
+ /**
178
+ * The theme an email is drawn in (AGL-3370): its palette, which resolves
179
+ * every color an author picked and colors what they did not, and its button
180
+ * radius. Spread into {@link EmailRenderOptions}.
181
+ */
182
+ export type EmailTheme = Pick<EmailRenderOptions, 'palette' | 'buttonRadius'>;
158
183
  export interface RenderedEmail {
159
184
  html: string;
160
185
  text: string;
@@ -43,6 +43,7 @@
43
43
  * page is the ordinary way that happens.
44
44
  */ import { hasSafeLinkScheme, hasSafeMediaScheme } from "@aglyn/shared-util-http/safe-url-scheme";
45
45
  import { linkifyEscapedText } from "./email-linkify.js";
46
+ import { resolveEmailColor } from "./email-palette.js";
46
47
  import { resolveEmailMediaSrc } from "./email-media-src.js";
47
48
  const FONT = 'Helvetica, Arial, sans-serif';
48
49
  /**
@@ -86,7 +87,24 @@ const TEXT_STYLES = {
86
87
  export function renderEmailHtml(options) {
87
88
  var _imageSrc, _ref;
88
89
  var _nodes_rootId;
89
- const { nodes, rootId = 'root', subject = '', preheader = '', merge, products, sanitize, mediaOrigin, mediaHostId, brandLogoUrl, chrome } = options;
90
+ const { nodes, rootId = 'root', subject = '', preheader = '', merge, products, sanitize, mediaOrigin, mediaHostId, brandLogoUrl, chrome, palette, buttonRadius } = options;
91
+ const radius = Number.isFinite(buttonRadius) && Number(buttonRadius) >= 0 ? Math.min(Number(buttonRadius), 48) : 4;
92
+ /**
93
+ * A stored color, resolved: a palette token against the theme, a literal
94
+ * as written, anything else to the fallback. The only way a color prop
95
+ * reaches the markup, so nothing unresolvable or unsafe is ever written
96
+ * into a style attribute.
97
+ */ const colorOf = (value, fallback)=>{
98
+ var _resolveEmailColor;
99
+ return (_resolveEmailColor = resolveEmailColor(value, palette)) != null ? _resolveEmailColor : fallback;
100
+ };
101
+ /** The theme's accent, for what the author left unset. */ const accent = {
102
+ fill: colorOf('primary.main', '#1a73e8'),
103
+ label: colorOf('primary.contrastText', '#ffffff'),
104
+ // The accent AS TEXT: `primary.dark` is the theme's AA-checked text
105
+ // shade, where the fill color itself may be too pale to read as a link.
106
+ link: colorOf('primary.dark', '#1a73e8')
107
+ };
90
108
  const textParts = [];
91
109
  const sub = (value)=>substituteMergeTokens(String(value != null ? value : ''), merge);
92
110
  /**
@@ -176,7 +194,7 @@ export function renderEmailHtml(options) {
176
194
  switch(node.componentId){
177
195
  case 'emailSection':
178
196
  {
179
- const background = props.backgroundColor || '#ffffff';
197
+ const background = colorOf(props.backgroundColor, '#ffffff');
180
198
  const padding = Number.isFinite(Number(props.padding)) ? Number(props.padding) : 24;
181
199
  const align = props.align || 'left';
182
200
  // A ROW, like every other block, with the band's table inside its
@@ -198,9 +216,9 @@ export function renderEmailHtml(options) {
198
216
  if (!text.trim()) return '';
199
217
  textParts.push(text);
200
218
  const style = (_TEXT_STYLES_props_variant = TEXT_STYLES[props.variant]) != null ? _TEXT_STYLES_props_variant : TEXT_STYLES['body'];
201
- const color = props.color || '#1a1a1a';
219
+ const color = colorOf(props.color, '#1a1a1a');
202
220
  const align = props.align || 'left';
203
- return row(`<div style="font-family:${FONT};${style};color:${color};text-align:${align};">` + linkifyEscapedText(escapeEmailHtml(text)).replace(/\n/g, '<br />') + `</div>`, 'padding:4px 0;');
221
+ return row(`<div style="font-family:${FONT};${style};color:${color};text-align:${align};">` + linkifyEscapedText(escapeEmailHtml(text), accent.link).replace(/\n/g, '<br />') + `</div>`, 'padding:4px 0;');
204
222
  }
205
223
  case 'emailRichtext':
206
224
  case 'emailHtml':
@@ -236,16 +254,16 @@ export function renderEmailHtml(options) {
236
254
  // does — a text/plain body is inert, but it is still delivered mail
237
255
  // and a refused URL does not belong in it either.
238
256
  const href = (_linkHref = linkHref((_props_href = props.href) != null ? _props_href : '#')) != null ? _linkHref : '#';
239
- const background = props.backgroundColor || '#1a73e8';
240
- const color = props.color || '#ffffff';
257
+ const background = colorOf(props.backgroundColor, accent.fill);
258
+ const color = colorOf(props.color, accent.label);
241
259
  const align = props.align || 'center';
242
260
  textParts.push(`${label}: ${href}`);
243
261
  // Bulletproof-ish button: padded anchor, table-aligned.
244
- return row(`<a href="${escapeEmailHtml(href)}" target="_blank" ` + `style="display:inline-block;padding:12px 28px;border-radius:6px;` + `background-color:${background};color:${color};font-family:${FONT};` + `font-size:15px;font-weight:600;text-decoration:none;">` + escapeEmailHtml(label) + `</a>`, `padding:12px 0;text-align:${align};`);
262
+ return row(`<a href="${escapeEmailHtml(href)}" target="_blank" ` + `style="display:inline-block;padding:12px 28px;border-radius:${radius}px;` + `background-color:${background};color:${color};font-family:${FONT};` + `font-size:15px;font-weight:600;text-decoration:none;">` + escapeEmailHtml(label) + `</a>`, `padding:12px 0;text-align:${align};`);
245
263
  }
246
264
  case 'emailDivider':
247
265
  {
248
- const color = props.color || '#e0e0e0';
266
+ const color = colorOf(props.color, '#e0e0e0');
249
267
  return row(`<div style="border-top:1px solid ${color};font-size:0;line-height:0;">&nbsp;</div>`, 'padding:12px 0;');
250
268
  }
251
269
  case 'emailSpacer':
@@ -270,7 +288,7 @@ export function renderEmailHtml(options) {
270
288
  // asset, so it carries the same reference/relative forms (AGL-1224).
271
289
  const productImage = imageSrc(product.imageUrl);
272
290
  const image = productImage ? `<img src="${escapeEmailHtml(productImage)}" alt="${escapeEmailHtml(product.name)}" width="280" style="max-width:100%;height:auto;border:0;border-radius:6px;" /><br />` : '';
273
- const button = productUrl ? `<a href="${escapeEmailHtml(productUrl)}" target="_blank" style="display:inline-block;margin-top:8px;padding:10px 24px;border-radius:6px;background-color:#1a73e8;color:#ffffff;font-family:${FONT};font-size:14px;font-weight:600;text-decoration:none;">${escapeEmailHtml(label)}</a>` : '';
291
+ const button = productUrl ? `<a href="${escapeEmailHtml(productUrl)}" target="_blank" style="display:inline-block;margin-top:8px;padding:10px 24px;border-radius:${radius}px;background-color:${accent.fill};color:${accent.label};font-family:${FONT};font-size:14px;font-weight:600;text-decoration:none;">${escapeEmailHtml(label)}</a>` : '';
274
292
  return row(`<div style="border:1px solid #e0e0e0;border-radius:8px;padding:16px;text-align:center;font-family:${FONT};">` + image + `<div style="font-size:16px;font-weight:600;margin-top:8px;">${escapeEmailHtml(product.name)}</div>` + (product.priceLabel ? `<div style="font-size:14px;color:#555555;margin-top:2px;">${escapeEmailHtml(product.priceLabel)}</div>` : '') + button + `</div>`, 'padding:8px 0;');
275
293
  }
276
294
  default:
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/email-render.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Email render pipeline (AGL-348): converts a designed email screen's\n * node map into email-client-compatible HTML (600px table layout,\n * inlined styles, bulletproof buttons) plus a plain-text alternative.\n * Pure — no I/O; callers resolve products/merge data first.\n *\n * Merge tokens ({{contact.firstName}}, {{unsubscribeUrl}}, …) substitute\n * from the provided map; unknown tokens are left in place so a missing\n * field is visible in test sends instead of silently blank.\n *\n * ## The safety contract, in one place\n *\n * Everything this emits is author-controlled, and it is emitted into mail\n * that has left the building. Three rules, all applied here rather than\n * left to a caller:\n *\n * 1. Block HTML goes through {@link EmailRenderOptions.sanitize}, which is\n * REQUIRED. There is no identity default to fall back into.\n * 2. Every URL is scheme-checked against `@aglyn/shared-util-http` before it\n * is escaped into an attribute — escaping stops an attribute breakout and\n * says nothing at all about `javascript:`.\n * 3. Merge substitution into a URL cannot draw from the `contact.` namespace,\n * so no template can route a recipient's own address into a query string.\n *\n * Mail clients strip most of this themselves, so none of the three is the\n * last line of defense in an inbox. They are the only line the moment one of\n * these documents renders outside one — a \"view this email in your browser\"\n * page is the ordinary way that happens.\n */\n\nimport {\n hasSafeLinkScheme,\n hasSafeMediaScheme,\n} from '@aglyn/shared-util-http/safe-url-scheme'\n\nimport { linkifyEscapedText } from './email-linkify'\nimport { resolveEmailMediaSrc } from './email-media-src'\n\nexport interface EmailRenderProduct {\n name: string\n priceLabel?: string\n imageUrl?: string\n url?: string\n}\n\nexport interface EmailRenderNode {\n componentId?: string\n props?: Record<string, any>\n nodes?: string[]\n}\n\nexport interface EmailRenderOptions {\n /** Flat node map (screen version `nodes`). */\n nodes: Record<string, EmailRenderNode | undefined>\n /** Root node id (defaults to 'root'). */\n rootId?: string\n subject?: string\n /** Hidden preview line shown next to the subject in inboxes. */\n preheader?: string\n /** Merge values keyed by token body, e.g. 'contact.firstName'. */\n merge?: Record<string, string>\n /** Product data by id for emailProduct blocks. */\n products?: Record<string, EmailRenderProduct | undefined>\n /**\n * The HTML policy applied to every `emailRichtext` / `emailHtml` block.\n *\n * REQUIRED, and required is the point. A policy a caller may omit is a\n * policy that gets omitted, and an omitted one here is not a rendering\n * bug: the console renders these same nodes through `sanitizeCustomHtml`\n * before an author ever sees them, so an unpoliced mail render means two\n * renderers of one document disagreeing about safety, with the unsafe one\n * being the one that sends.\n *\n * This module cannot pick the policy for itself — it is `scope:shared` and\n * the sanitizer is aglyn-scoped, which nx forbids it to import — so the\n * choice belongs to the caller, and the type is what makes the caller make\n * it. Every send path passes `sanitizeAuthorHtml`, which is exactly what\n * `sanitizeCustomHtml` delegates to, so the mailed copy and the previewed\n * copy are the same function of the same string.\n */\n sanitize: (html: string) => string\n /**\n * Absolute origin serving this email's media, e.g. `https://acme.com`\n * (AGL-1224).\n *\n * An image an author picked with \"Browse media\" is stored as a\n * `media:{scope}/{mediaId}` reference, which resolves to the SITE-RELATIVE\n * CDN path `/api/media/cdn/…`. A browser has a page to resolve that\n * against; an inbox has nothing. Without this, every picked image is a\n * broken-image box in the delivered mail.\n *\n * It is an input rather than a lookup because this module is pure, and\n * because the CDN route is mounted in BOTH the console and the tenant app\n * — which origin is correct depends on whose email this is.\n */\n mediaOrigin?: string\n /**\n * Host doc id of the site sending, so an `org:`-scoped reference is\n * host-qualified. The CDN is unauthenticated and decides from the URL\n * alone, so an org asset restricted to particular sites is only served\n * through the qualified form.\n */\n mediaHostId?: string\n /**\n * Absolute URL of the sender's EMAIL logo, prepended as a centred row at\n * the top of the 600px table (AGL-2139).\n *\n * `emailLogoUrl` was a first-class field of `OrgBrandingProfile`, resolved,\n * collected in the branding editor, https-validated and persisted — and read\n * at ZERO render sites. An agency admin on the tier that costs the most\n * filled it in, the form saved, the value round-tripped, and it appeared in\n * no email ever. This is the consumer that makes it a live capability\n * rather than a stored string.\n *\n * Absent or blank emits NOTHING — not an empty row, not a spacer. An email\n * with a gap where a logo should be reads as broken; one without a logo\n * reads as plain, which is the correct appearance for an org that has not\n * set one.\n *\n * Ignored when {@link EmailRenderOptions.chrome} carries a header: that\n * header is the logo row, and a caller that has one passes the org's logo\n * through it, so a message never opens with two.\n */\n brandLogoUrl?: string\n /**\n * The sender's header and footer, drawn around the body (AGL-3322). See\n * {@link EmailChrome}. Absent draws neither.\n */\n chrome?: EmailChrome\n}\n\n/**\n * The row above the body that says who sent the mail.\n *\n * `logoAlt` is required because it does two jobs. It is the logo's `alt`,\n * which is what most inboxes show, since they block images by default. And\n * it is the header itself when there is no logo to draw — none was given, or\n * the one given cannot be fetched from an inbox — so the sender is named in\n * bold text rather than by a broken-image box.\n */\nexport interface EmailChromeHeader {\n /** The logo. Resolved and scheme-checked like every other image here. */\n logoUrl?: string\n /** The sender's name: the logo's alt text, or the header when no logo draws. */\n logoAlt: string\n /** Where the logo or name links. A refused scheme drops the link, not the header. */\n href?: string\n}\n\n/**\n * The rows under the body, in the order they draw. Each is optional, and a\n * footer with none of them draws nothing.\n */\nexport interface EmailChromeFooter {\n /** Why the recipient is getting this mail. */\n reason?: string\n /** Where to get help. Drawn as a link; dropped whole when the href is refused. */\n support?: { label: string; href: string }\n /** Who sent it, and from where: a copyright and postal line. */\n legal?: string\n}\n\n/**\n * A sender's header and footer, drawn by the renderer rather than placed by a\n * designer (AGL-3322).\n *\n * For mail whose body carries no header or footer of its own: a system\n * email's built-in copy, or a design sent under a brand whose own blocks it\n * may not carry. The caller decides WHICH brand; this only draws.\n *\n * Every string goes through the same merge substitution as the body, so\n * `{{org.name}}` in a footer line reads as the organization it names, and is\n * escaped like body text. Every URL goes through the same scheme checks as a\n * body link or image. The footer lines are appended to the plain-text part\n * too, the support link as its bare address, because a reader of that part is\n * owed the same reason and the same way to get help.\n */\nexport interface EmailChrome {\n header?: EmailChromeHeader\n footer?: EmailChromeFooter\n}\n\nexport interface RenderedEmail {\n html: string\n text: string\n}\n\nconst FONT = 'Helvetica, Arial, sans-serif'\n\n/**\n * The colours the chrome is drawn in (AGL-3322), named once.\n *\n * Literal because this is email HTML: mail clients strip `<style>` and read no\n * CSS variables, so no theme token reaches the wire. They are the values\n * already in the mail, not new ones — the ink and card of a default body\n * block, and the band and caption grey of the email footer block the\n * platform's own mail wears — so the chrome and the body it frames agree.\n */\nconst CHROME_PALETTE = {\n /** The sender's name in the header: body text's ink. */\n ink: '#1a1a1a',\n /** The header band: a default section's card. */\n card: '#ffffff',\n /** The footer band. */\n band: '#F8F9FA',\n /** Footer lines and the support link. */\n caption: '#757575',\n} as const\n\nexport function escapeEmailHtml(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n}\n\n/** Substitutes {{token}} occurrences; unknown tokens stay visible. */\nexport function substituteMergeTokens(\n value: string,\n merge: Record<string, string> | undefined,\n): string {\n return value.replace(/\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g, (match, token: string) => {\n const replacement = merge?.[token]\n return replacement !== undefined ? replacement : match\n })\n}\n\nconst TEXT_STYLES: Record<string, string> = {\n heading: `font-size:28px;font-weight:700;line-height:1.25`,\n subheading: `font-size:20px;font-weight:600;line-height:1.3`,\n body: `font-size:15px;font-weight:400;line-height:1.55`,\n caption: `font-size:12px;font-weight:400;line-height:1.4`,\n}\n\n/** One full-width table row wrapping arbitrary cell HTML. */\nconst row = (cellHtml: string, cellStyle = ''): string =>\n `<tr><td style=\"${cellStyle}\">${cellHtml}</td></tr>`\n\n/**\n * The id the besigner roots every stored node map at — `CANVAS_ROOT_ELEMENT_ID`\n * in `@aglyn/aglyn`. `renderEmailHtml` still defaults `rootId` to `'root'` for\n * ad-hoc callers, but anything rendering a real besigner document MUST pass\n * this: rendering a besigner map as `'root'` finds no root and emits nothing\n * (AGL-765). Kept here so server code need not import the heavy `@aglyn/aglyn`\n * barrel; a drift guard in the console specs asserts the two stay equal.\n */\nexport const EMAIL_NODE_ROOT_ID = '_@_'\n\nexport function renderEmailHtml(options: EmailRenderOptions): RenderedEmail {\n const {\n nodes,\n rootId = 'root',\n subject = '',\n preheader = '',\n merge,\n products,\n sanitize,\n mediaOrigin,\n mediaHostId,\n brandLogoUrl,\n chrome,\n } = options\n\n const textParts: string[] = []\n const sub = (value: unknown): string =>\n substituteMergeTokens(String(value ?? ''), merge)\n\n /**\n * The merge map as it applies inside a URL: everything except the\n * `contact.` namespace.\n *\n * That namespace is the recipient — `contact.email`, `contact.name`,\n * `contact.firstName` are what campaign-send puts in the map. Substituting\n * them into an `href` or a `src` is how a template like\n * `https://example.test/?e={{contact.email}}` turns every send into the\n * recipient's own address on somebody's query string, and putting personal\n * data in a URL is a line this codebase does not cross. The refusal has to\n * live here because it is the SUBSTITUTION that creates the value: no\n * scheme check and no sanitizer can see the address, since the template\n * the author wrote and the editor reviewed contains only the token.\n *\n * Nothing else narrows: `{{unsubscribeUrl}}` and `{{site.url}}` ARE URLs\n * and are the reason a token appears in this position at all.\n *\n * A refused token is left standing rather than blanked, which is this\n * module's existing convention for a token it will not fill — a test send\n * shows `?e={{contact.email}}` in the link, which names the problem to the\n * author instead of silently shipping a subtly different URL.\n */\n const urlMerge = merge\n ? Object.fromEntries(\n Object.entries(merge).filter(\n ([token]) => !token.startsWith('contact.'),\n ),\n )\n : undefined\n\n /** {@link sub} for a value that lands in a URL — see {@link urlMerge}. */\n const subUrl = (value: unknown): string =>\n substituteMergeTokens(String(value ?? ''), urlMerge)\n\n /**\n * A link target safe to emit, or undefined.\n *\n * Escaping the value into the attribute stops a breakout and says nothing\n * about the scheme, so `escapeEmailHtml` on its own passes a stored\n * `javascript:` or `data:` href through verbatim. The check runs on the\n * SUBSTITUTED string, because a merge value is part of the final URL and a\n * template that reads clean can resolve to one that does not.\n */\n const linkHref = (value: unknown): string | undefined => {\n const href = subUrl(value)\n if (!href) return undefined\n return hasSafeLinkScheme(href) ? href : undefined\n }\n\n const origin = mediaOrigin?.replace(/\\/+$/, '')\n\n /**\n * A stored image value turned into something an inbox can actually fetch,\n * or undefined (AGL-1224).\n *\n * `resolveEmailMediaSrc` handles the three stored generations — a `media:`\n * reference, the AGL-175 relative CDN path, and a plain absolute URL an\n * author typed — but the first two come back site-relative, which is the\n * whole bug: only a browser has a page to resolve them against.\n *\n * With no origin to absolutize against, the image is DROPPED rather than\n * emitted relative. Both are a missing picture; a dropped one leaves a gap,\n * while `src=\"/api/media/cdn/…\"` renders as a broken-image box, which reads\n * to a recipient as a broken email rather than a plain one.\n *\n * ⚠️ This comment used to claim \"neither send path relies on this — both\n * supply an origin\", and treated reaching the drop as hypothetical. It was\n * wrong, and it was wrong in the direction that hides the bug: campaign-send\n * passed no `mediaOrigin`/`mediaHostId` at all, so every author-picked image\n * was silently dropped from every marketing campaign until AGL-1394. The\n * quiet failure IS still the right one for a caller that genuinely has no\n * origin — but \"no caller reaches this\" is a claim about the whole repo, and\n * it decays the moment someone adds a third send path. Verify it before\n * relying on it; do not restore a count here.\n *\n * A protocol-relative `//host/x.png` is passed through untouched: it is\n * already absolute enough to name a host, and prefixing an origin would\n * corrupt it.\n *\n * The scheme is checked on the way out. `resolveEmailMediaSrc` recognizes\n * the two stored reference forms and passes anything else through as an\n * author-typed URL, so it is this check and nothing else that stands\n * between what an author types and the `src` attribute. A refusal drops\n * the image on the same reasoning as everything else here that cannot be\n * made fetchable: a gap reads as a plain email, and a `src` we would not\n * stand behind is not made safer by shipping it.\n */\n const imageSrc = (value: unknown): string | undefined => {\n const resolved = resolveEmailMediaSrc(subUrl(value), mediaHostId)\n if (!resolved || !hasSafeMediaScheme(resolved)) return undefined\n if (!resolved.startsWith('/') || resolved.startsWith('//')) return resolved\n return origin ? `${origin}${resolved}` : undefined\n }\n\n const renderChildren = (ids: string[] | undefined): string =>\n (ids ?? [])\n .map((id) => renderNode(id))\n .filter(Boolean)\n .join('')\n\n const renderNode = (id: string): string => {\n const node = nodes[id]\n if (!node) return ''\n const props = node.props ?? {}\n switch (node.componentId) {\n case 'emailSection': {\n const background = props.backgroundColor || '#ffffff'\n const padding = Number.isFinite(Number(props.padding))\n ? Number(props.padding)\n : 24\n const align = props.align || 'left'\n // A ROW, like every other block, with the band's table inside its\n // cell. Whatever encloses a section is a table — the 600px column, or\n // another section's inner table — and a `<table>` placed straight into\n // a table's rows is not a child an HTML parser keeps there: it closes\n // the enclosing table and lands after it. Emitted bare, a section would\n // draw outside the column, as wide as the viewport rather than the\n // 600px the editor shows, and take every row after it out too.\n return row(\n `<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" ` +\n `style=\"background-color:${background};\">` +\n row(\n `<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">` +\n renderChildren(node.nodes) +\n `</table>`,\n `padding:${padding}px;text-align:${align};`,\n ) +\n `</table>`,\n )\n }\n case 'emailText': {\n const text = sub(props.children)\n // A block whose copy is only a token the send left empty (a\n // notification with no detail line, AGL-3367) draws nothing, rather\n // than an empty row and a blank paragraph in the text part.\n if (!text.trim()) return ''\n textParts.push(text)\n const style = TEXT_STYLES[props.variant as string] ?? TEXT_STYLES['body']\n const color = props.color || '#1a1a1a'\n const align = props.align || 'left'\n return row(\n `<div style=\"font-family:${FONT};${style};color:${color};text-align:${align};\">` +\n linkifyEscapedText(escapeEmailHtml(text)).replace(/\\n/g, '<br />') +\n `</div>`,\n 'padding:4px 0;',\n )\n }\n case 'emailRichtext':\n case 'emailHtml': {\n const html = sanitize(sub(props.html))\n if (!html.trim()) return ''\n textParts.push(html.replace(/<[^>]+>/g, ' ').trim())\n return row(\n `<div style=\"font-family:${FONT};font-size:15px;line-height:1.55;\">${html}</div>`,\n 'padding:4px 0;',\n )\n }\n case 'emailImage': {\n const src = imageSrc(props.src)\n if (!src) return ''\n const width = Number(props.width) > 0 ? Number(props.width) : 600\n const alt = escapeEmailHtml(String(props.alt ?? ''))\n const align = props.align || 'center'\n const img =\n `<img src=\"${escapeEmailHtml(src)}\" alt=\"${alt}\" width=\"${Math.min(width, 600)}\" ` +\n `style=\"display:inline-block;max-width:100%;height:auto;border:0;\" />`\n // A refused href drops the WRAPPER, not the picture: the image is the\n // content and the link is decoration on it.\n const href = linkHref(props.href)\n return row(\n href\n ? `<a href=\"${escapeEmailHtml(href)}\" target=\"_blank\">${img}</a>`\n : img,\n `padding:8px 0;text-align:${align};`,\n )\n }\n case 'emailButton': {\n const label = sub(props.children ?? 'Call to action')\n // A refused href falls back to the placeholder the empty case already\n // uses, so the button keeps its place in the layout and goes nowhere.\n // Dropping the row instead would take the author's copy with it, and\n // an inert button reads as an unfinished email rather than a broken\n // one. The plain-text alternative gets the same value the anchor\n // does — a text/plain body is inert, but it is still delivered mail\n // and a refused URL does not belong in it either.\n const href = linkHref(props.href ?? '#') ?? '#'\n const background = props.backgroundColor || '#1a73e8'\n const color = props.color || '#ffffff'\n const align = props.align || 'center'\n textParts.push(`${label}: ${href}`)\n // Bulletproof-ish button: padded anchor, table-aligned.\n return row(\n `<a href=\"${escapeEmailHtml(href)}\" target=\"_blank\" ` +\n `style=\"display:inline-block;padding:12px 28px;border-radius:6px;` +\n `background-color:${background};color:${color};font-family:${FONT};` +\n `font-size:15px;font-weight:600;text-decoration:none;\">` +\n escapeEmailHtml(label) +\n `</a>`,\n `padding:12px 0;text-align:${align};`,\n )\n }\n case 'emailDivider': {\n const color = props.color || '#e0e0e0'\n return row(\n `<div style=\"border-top:1px solid ${color};font-size:0;line-height:0;\">&nbsp;</div>`,\n 'padding:12px 0;',\n )\n }\n case 'emailSpacer': {\n const height = Number(props.height) > 0 ? Number(props.height) : 24\n return row(\n `<div style=\"height:${height}px;font-size:0;line-height:0;\">&nbsp;</div>`,\n )\n }\n case 'emailProduct': {\n const product = props.productId\n ? products?.[String(props.productId)]\n : undefined\n if (!product) return ''\n const label = sub(props.buttonLabel ?? 'Shop now')\n // Catalog data rather than author markup, and the caller absolutizes\n // it before it arrives — but \"it came from a trusted table\" is a\n // claim about every writer of that table, so the same rule applies\n // here as to an author-typed href. Refused means no button and no\n // line in the text part; the product card itself still renders.\n const productUrl =\n product.url && hasSafeLinkScheme(product.url)\n ? product.url\n : undefined\n textParts.push(\n `${product.name}${product.priceLabel ? ` — ${product.priceLabel}` : ''}` +\n (productUrl ? `: ${productUrl}` : ''),\n )\n // Same treatment as emailImage: a catalog image can be a picked\n // asset, so it carries the same reference/relative forms (AGL-1224).\n const productImage = imageSrc(product.imageUrl)\n const image = productImage\n ? `<img src=\"${escapeEmailHtml(productImage)}\" alt=\"${escapeEmailHtml(product.name)}\" width=\"280\" style=\"max-width:100%;height:auto;border:0;border-radius:6px;\" /><br />`\n : ''\n const button = productUrl\n ? `<a href=\"${escapeEmailHtml(productUrl)}\" target=\"_blank\" style=\"display:inline-block;margin-top:8px;padding:10px 24px;border-radius:6px;background-color:#1a73e8;color:#ffffff;font-family:${FONT};font-size:14px;font-weight:600;text-decoration:none;\">${escapeEmailHtml(label)}</a>`\n : ''\n return row(\n `<div style=\"border:1px solid #e0e0e0;border-radius:8px;padding:16px;text-align:center;font-family:${FONT};\">` +\n image +\n `<div style=\"font-size:16px;font-weight:600;margin-top:8px;\">${escapeEmailHtml(product.name)}</div>` +\n (product.priceLabel\n ? `<div style=\"font-size:14px;color:#555555;margin-top:2px;\">${escapeEmailHtml(product.priceLabel)}</div>`\n : '') +\n button +\n `</div>`,\n 'padding:8px 0;',\n )\n }\n default: {\n // Unknown/web components: render their children so mixed documents\n // degrade gracefully instead of dropping content.\n return renderChildren(node.nodes)\n }\n }\n }\n\n const body = renderNode(rootId) || renderChildren(nodes[rootId]?.nodes)\n const preheaderHtml = preheader\n ? `<div style=\"display:none;max-height:0;overflow:hidden;mso-hide:all;\">` +\n escapeEmailHtml(sub(preheader)) +\n `</div>`\n : ''\n\n // INSIDE the 600px table, above the designed body, so it inherits the\n // column's width on a phone instead of being centred against the viewport.\n // `alt` is the product name where one is known, because a logo blocked by\n // the client (which is the default in most inboxes) must still say who sent\n // the mail. Height is capped rather than set, so a wordmark and a square\n // mark both land at a sane size without the sender supplying dimensions.\n //\n // The src goes through `imageSrc` like every other image in the message\n // (AGL-2230), and NOT into the tag raw. `emailLogoUrl` is collected by\n // `MediaUrlField`, whose \"Browse\" button writes the picked asset's\n // site-relative `cdnPath` — and the org DAM's other generation stores a\n // `media:{scope}/{mediaId}` reference. Neither is a URL an inbox can\n // resolve: there is no page to resolve it against, so a raw pass-through\n // put `src=\"media:h1/med9\"` at the top of every transactional email a\n // white-label org sent. `imageSrc` absolutizes both forms against\n // `mediaOrigin`, passes a real https URL through untouched, and returns\n // undefined when it cannot produce something fetchable — in which case the\n // row is dropped, on the same reasoning as AGL-1224: a gap reads as a plain\n // email, a broken-image box reads as a broken one.\n const brandLogo = imageSrc(String(brandLogoUrl ?? '').trim()) ?? ''\n // The product name from the merge map, because most inboxes block images by\n // default — a logo with no `alt` is a blank box where the sender's identity\n // should be. Falls back to nothing rather than to a literal, which would\n // reintroduce the hard-coded brand this whole change removes.\n const brandLogoAlt = merge?.['brand.productName'] ?? ''\n const brandLogoHtml = brandLogo\n ? row(\n `<img src=\"${escapeEmailHtml(brandLogo)}\" alt=\"${escapeEmailHtml(brandLogoAlt)}\" ` +\n `style=\"max-height:48px;max-width:200px;display:block;margin:0 auto;border:0;\" />`,\n 'padding:24px 24px 0;text-align:center;',\n )\n : ''\n\n /**\n * The chrome header row, or '' when it has nothing to draw.\n *\n * Same placement and the same `imageSrc` as the logo row above, which it\n * replaces. Height is capped at 40px rather than set, so a wordmark and a\n * square mark both land at a sane size; the `height` attribute is for the\n * desktop clients that ignore CSS `max-height` and would otherwise draw the\n * file at its own size. A logo that cannot be fetched from an inbox falls\n * back to the name in bold, never to an `<img>` that draws a broken box.\n * White, so the header and a default body read as one card.\n */\n const renderChromeHeader = (header: EmailChromeHeader): string => {\n const logo = imageSrc(String(header.logoUrl ?? '').trim())\n const name = sub(header.logoAlt).trim()\n const href = linkHref(header.href)\n const link = (inner: string, style: string): string =>\n href\n ? `<a href=\"${escapeEmailHtml(href)}\" target=\"_blank\" style=\"${style}\">${inner}</a>`\n : inner\n let mark: string\n if (logo) {\n mark = link(\n `<img src=\"${escapeEmailHtml(logo)}\" alt=\"${escapeEmailHtml(name)}\" height=\"40\" ` +\n `style=\"display:block;margin:0 auto;border:0;height:auto;width:auto;max-height:40px;max-width:200px;\" />`,\n 'text-decoration:none;',\n )\n } else if (name) {\n mark =\n `<div style=\"font-family:${FONT};font-size:20px;font-weight:700;line-height:1.3;color:${CHROME_PALETTE.ink};text-align:center;\">` +\n link(escapeEmailHtml(name), `color:${CHROME_PALETTE.ink};text-decoration:none;`) +\n `</div>`\n } else {\n return ''\n }\n return row(\n mark,\n `padding:24px 24px 0;text-align:center;background-color:${CHROME_PALETTE.card};`,\n )\n }\n\n const headerHtml = chrome?.header\n ? renderChromeHeader(chrome.header)\n : brandLogoHtml\n\n /**\n * The chrome footer: rows under the body in the look of the email footer\n * block the platform's own mail wears (a `#F8F9FA` section, padded 24,\n * centered captions), and the same lines appended to the plain-text part.\n *\n * Rendered AFTER the body on purpose: the body's blocks push their text as\n * they draw, and the footer's lines belong after all of it. A line that\n * resolves to nothing is left out rather than drawn as an empty row.\n */\n const renderChromeFooter = (footer: EmailChromeFooter): string => {\n const htmlLines: string[] = []\n const textLines: string[] = []\n const reason = sub(footer.reason).trim()\n if (reason) {\n htmlLines.push(escapeEmailHtml(reason))\n textLines.push(reason)\n }\n const supportLabel = sub(footer.support?.label).trim()\n const supportHref = linkHref(footer.support?.href)\n if (supportLabel && supportHref) {\n htmlLines.push(\n `<a href=\"${escapeEmailHtml(supportHref)}\" target=\"_blank\" ` +\n `style=\"color:${CHROME_PALETTE.caption};text-decoration:underline;\">${escapeEmailHtml(supportLabel)}</a>`,\n )\n // A plain-text reader gets the address itself, bare: a mailto's\n // mailbox, or the URL. \"Get help: https://…\" names where it goes; a\n // label that already IS the mailbox is not repeated.\n const bare = /^mailto:/i.test(supportHref)\n ? supportHref.slice('mailto:'.length).split('?')[0]\n : supportHref\n textLines.push(supportLabel === bare ? bare : `${supportLabel}: ${bare}`)\n }\n const legal = sub(footer.legal).trim()\n if (legal) {\n htmlLines.push(escapeEmailHtml(legal))\n textLines.push(legal)\n }\n if (!htmlLines.length) return ''\n textParts.push(textLines.join('\\n'))\n return row(\n htmlLines\n .map(\n (line, index) =>\n `<div style=\"font-family:${FONT};font-size:12px;font-weight:400;line-height:1.4;color:${CHROME_PALETTE.caption};` +\n `${index ? 'margin-top:8px;' : ''}\">${line.replace(/\\n/g, '<br />')}</div>`,\n )\n .join(''),\n `padding:24px;text-align:center;background-color:${CHROME_PALETTE.band};`,\n )\n }\n\n const footerHtml = chrome?.footer ? renderChromeFooter(chrome.footer) : ''\n\n const html =\n `<!DOCTYPE html><html><head><meta charset=\"utf-8\" />` +\n `<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />` +\n `<title>${escapeEmailHtml(sub(subject))}</title></head>` +\n `<body style=\"margin:0;padding:0;background-color:#f4f4f4;\">` +\n preheaderHtml +\n `<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color:#f4f4f4;\">` +\n row(\n `<table role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" align=\"center\" style=\"max-width:600px;width:100%;margin:0 auto;\">` +\n headerHtml +\n body +\n footerHtml +\n `</table>`,\n 'padding:24px 8px;',\n ) +\n `</table></body></html>`\n\n return { html, text: textParts.join('\\n\\n') }\n}\n"],"names":["hasSafeLinkScheme","hasSafeMediaScheme","linkifyEscapedText","resolveEmailMediaSrc","FONT","CHROME_PALETTE","ink","card","band","caption","escapeEmailHtml","value","replace","substituteMergeTokens","merge","match","token","replacement","undefined","TEXT_STYLES","heading","subheading","body","row","cellHtml","cellStyle","EMAIL_NODE_ROOT_ID","renderEmailHtml","options","imageSrc","nodes","rootId","subject","preheader","products","sanitize","mediaOrigin","mediaHostId","brandLogoUrl","chrome","textParts","sub","String","urlMerge","Object","fromEntries","entries","filter","startsWith","subUrl","linkHref","href","origin","resolved","renderChildren","ids","map","id","renderNode","Boolean","join","node","props","componentId","background","backgroundColor","padding","Number","isFinite","align","text","children","trim","push","style","variant","color","html","src","width","alt","img","Math","min","label","height","product","productId","buttonLabel","productUrl","url","name","priceLabel","productImage","imageUrl","image","button","preheaderHtml","brandLogo","brandLogoAlt","brandLogoHtml","renderChromeHeader","header","logo","logoUrl","logoAlt","link","inner","mark","headerHtml","renderChromeFooter","footer","htmlLines","textLines","reason","supportLabel","support","supportHref","bare","test","slice","length","split","legal","line","index","footerHtml"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/email-render.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Email render pipeline (AGL-348): converts a designed email screen's\n * node map into email-client-compatible HTML (600px table layout,\n * inlined styles, bulletproof buttons) plus a plain-text alternative.\n * Pure — no I/O; callers resolve products/merge data first.\n *\n * Merge tokens ({{contact.firstName}}, {{unsubscribeUrl}}, …) substitute\n * from the provided map; unknown tokens are left in place so a missing\n * field is visible in test sends instead of silently blank.\n *\n * ## The safety contract, in one place\n *\n * Everything this emits is author-controlled, and it is emitted into mail\n * that has left the building. Three rules, all applied here rather than\n * left to a caller:\n *\n * 1. Block HTML goes through {@link EmailRenderOptions.sanitize}, which is\n * REQUIRED. There is no identity default to fall back into.\n * 2. Every URL is scheme-checked against `@aglyn/shared-util-http` before it\n * is escaped into an attribute — escaping stops an attribute breakout and\n * says nothing at all about `javascript:`.\n * 3. Merge substitution into a URL cannot draw from the `contact.` namespace,\n * so no template can route a recipient's own address into a query string.\n *\n * Mail clients strip most of this themselves, so none of the three is the\n * last line of defense in an inbox. They are the only line the moment one of\n * these documents renders outside one — a \"view this email in your browser\"\n * page is the ordinary way that happens.\n */\n\nimport {\n hasSafeLinkScheme,\n hasSafeMediaScheme,\n} from '@aglyn/shared-util-http/safe-url-scheme'\n\nimport { linkifyEscapedText } from './email-linkify'\nimport { resolveEmailColor, type EmailPalette } from './email-palette'\nimport { resolveEmailMediaSrc } from './email-media-src'\n\nexport interface EmailRenderProduct {\n name: string\n priceLabel?: string\n imageUrl?: string\n url?: string\n}\n\nexport interface EmailRenderNode {\n componentId?: string\n props?: Record<string, any>\n nodes?: string[]\n}\n\nexport interface EmailRenderOptions {\n /** Flat node map (screen version `nodes`). */\n nodes: Record<string, EmailRenderNode | undefined>\n /** Root node id (defaults to 'root'). */\n rootId?: string\n subject?: string\n /** Hidden preview line shown next to the subject in inboxes. */\n preheader?: string\n /** Merge values keyed by token body, e.g. 'contact.firstName'. */\n merge?: Record<string, string>\n /** Product data by id for emailProduct blocks. */\n products?: Record<string, EmailRenderProduct | undefined>\n /**\n * The HTML policy applied to every `emailRichtext` / `emailHtml` block.\n *\n * REQUIRED, and required is the point. A policy a caller may omit is a\n * policy that gets omitted, and an omitted one here is not a rendering\n * bug: the console renders these same nodes through `sanitizeCustomHtml`\n * before an author ever sees them, so an unpoliced mail render means two\n * renderers of one document disagreeing about safety, with the unsafe one\n * being the one that sends.\n *\n * This module cannot pick the policy for itself — it is `scope:shared` and\n * the sanitizer is aglyn-scoped, which nx forbids it to import — so the\n * choice belongs to the caller, and the type is what makes the caller make\n * it. Every send path passes `sanitizeAuthorHtml`, which is exactly what\n * `sanitizeCustomHtml` delegates to, so the mailed copy and the previewed\n * copy are the same function of the same string.\n */\n sanitize: (html: string) => string\n /**\n * Absolute origin serving this email's media, e.g. `https://acme.com`\n * (AGL-1224).\n *\n * An image an author picked with \"Browse media\" is stored as a\n * `media:{scope}/{mediaId}` reference, which resolves to the SITE-RELATIVE\n * CDN path `/api/media/cdn/…`. A browser has a page to resolve that\n * against; an inbox has nothing. Without this, every picked image is a\n * broken-image box in the delivered mail.\n *\n * It is an input rather than a lookup because this module is pure, and\n * because the CDN route is mounted in BOTH the console and the tenant app\n * — which origin is correct depends on whose email this is.\n */\n mediaOrigin?: string\n /**\n * Host doc id of the site sending, so an `org:`-scoped reference is\n * host-qualified. The CDN is unauthenticated and decides from the URL\n * alone, so an org asset restricted to particular sites is only served\n * through the qualified form.\n */\n mediaHostId?: string\n /**\n * Absolute URL of the sender's EMAIL logo, prepended as a centred row at\n * the top of the 600px table (AGL-2139).\n *\n * `emailLogoUrl` was a first-class field of `OrgBrandingProfile`, resolved,\n * collected in the branding editor, https-validated and persisted — and read\n * at ZERO render sites. An agency admin on the tier that costs the most\n * filled it in, the form saved, the value round-tripped, and it appeared in\n * no email ever. This is the consumer that makes it a live capability\n * rather than a stored string.\n *\n * Absent or blank emits NOTHING — not an empty row, not a spacer. An email\n * with a gap where a logo should be reads as broken; one without a logo\n * reads as plain, which is the correct appearance for an org that has not\n * set one.\n *\n * Ignored when {@link EmailRenderOptions.chrome} carries a header: that\n * header is the logo row, and a caller that has one passes the org's logo\n * through it, so a message never opens with two.\n */\n brandLogoUrl?: string\n /**\n * The sender's header and footer, drawn around the body (AGL-3322). See\n * {@link EmailChrome}. Absent draws neither.\n */\n chrome?: EmailChrome\n /**\n * The theme the email is sent under (AGL-3370): the console brand for the\n * platform's own mail, a white-label org's color over it, or the site's\n * theme for a site's mail. See {@link buildEmailPalette}.\n *\n * It does two things. A color an author picked in the Besigner is stored as\n * a palette token (`primary.main`), which a mail client cannot resolve, so\n * every color prop is resolved against this before it is written. And\n * what nobody picked (a button's fill and label, a link) takes the theme's\n * accent rather than a color of the renderer's own. Absent keeps the\n * renderer's neutral defaults and drops an unresolvable token.\n */\n palette?: EmailPalette\n /**\n * A button's corner radius in px: the theme's `shape.borderRadius`, which\n * the Besigner canvas draws the button with. Absent is MUI's default, 4.\n */\n buttonRadius?: number\n}\n\n/**\n * The row above the body that says who sent the mail.\n *\n * `logoAlt` is required because it does two jobs. It is the logo's `alt`,\n * which is what most inboxes show, since they block images by default. And\n * it is the header itself when there is no logo to draw — none was given, or\n * the one given cannot be fetched from an inbox — so the sender is named in\n * bold text rather than by a broken-image box.\n */\nexport interface EmailChromeHeader {\n /** The logo. Resolved and scheme-checked like every other image here. */\n logoUrl?: string\n /** The sender's name: the logo's alt text, or the header when no logo draws. */\n logoAlt: string\n /** Where the logo or name links. A refused scheme drops the link, not the header. */\n href?: string\n}\n\n/**\n * The rows under the body, in the order they draw. Each is optional, and a\n * footer with none of them draws nothing.\n */\nexport interface EmailChromeFooter {\n /** Why the recipient is getting this mail. */\n reason?: string\n /** Where to get help. Drawn as a link; dropped whole when the href is refused. */\n support?: { label: string; href: string }\n /** Who sent it, and from where: a copyright and postal line. */\n legal?: string\n}\n\n/**\n * A sender's header and footer, drawn by the renderer rather than placed by a\n * designer (AGL-3322).\n *\n * For mail whose body carries no header or footer of its own: a system\n * email's built-in copy, or a design sent under a brand whose own blocks it\n * may not carry. The caller decides WHICH brand; this only draws.\n *\n * Every string goes through the same merge substitution as the body, so\n * `{{org.name}}` in a footer line reads as the organization it names, and is\n * escaped like body text. Every URL goes through the same scheme checks as a\n * body link or image. The footer lines are appended to the plain-text part\n * too, the support link as its bare address, because a reader of that part is\n * owed the same reason and the same way to get help.\n */\nexport interface EmailChrome {\n header?: EmailChromeHeader\n footer?: EmailChromeFooter\n}\n\n/**\n * The theme an email is drawn in (AGL-3370): its palette, which resolves\n * every color an author picked and colors what they did not, and its button\n * radius. Spread into {@link EmailRenderOptions}.\n */\nexport type EmailTheme = Pick<EmailRenderOptions, 'palette' | 'buttonRadius'>\n\nexport interface RenderedEmail {\n html: string\n text: string\n}\n\nconst FONT = 'Helvetica, Arial, sans-serif'\n\n/**\n * The colours the chrome is drawn in (AGL-3322), named once.\n *\n * Literal because this is email HTML: mail clients strip `<style>` and read no\n * CSS variables, so no theme token reaches the wire. They are the values\n * already in the mail, not new ones — the ink and card of a default body\n * block, and the band and caption grey of the email footer block the\n * platform's own mail wears — so the chrome and the body it frames agree.\n */\nconst CHROME_PALETTE = {\n /** The sender's name in the header: body text's ink. */\n ink: '#1a1a1a',\n /** The header band: a default section's card. */\n card: '#ffffff',\n /** The footer band. */\n band: '#F8F9FA',\n /** Footer lines and the support link. */\n caption: '#757575',\n} as const\n\nexport function escapeEmailHtml(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n}\n\n/** Substitutes {{token}} occurrences; unknown tokens stay visible. */\nexport function substituteMergeTokens(\n value: string,\n merge: Record<string, string> | undefined,\n): string {\n return value.replace(/\\{\\{\\s*([\\w.]+)\\s*\\}\\}/g, (match, token: string) => {\n const replacement = merge?.[token]\n return replacement !== undefined ? replacement : match\n })\n}\n\nconst TEXT_STYLES: Record<string, string> = {\n heading: `font-size:28px;font-weight:700;line-height:1.25`,\n subheading: `font-size:20px;font-weight:600;line-height:1.3`,\n body: `font-size:15px;font-weight:400;line-height:1.55`,\n caption: `font-size:12px;font-weight:400;line-height:1.4`,\n}\n\n/** One full-width table row wrapping arbitrary cell HTML. */\nconst row = (cellHtml: string, cellStyle = ''): string =>\n `<tr><td style=\"${cellStyle}\">${cellHtml}</td></tr>`\n\n/**\n * The id the besigner roots every stored node map at — `CANVAS_ROOT_ELEMENT_ID`\n * in `@aglyn/aglyn`. `renderEmailHtml` still defaults `rootId` to `'root'` for\n * ad-hoc callers, but anything rendering a real besigner document MUST pass\n * this: rendering a besigner map as `'root'` finds no root and emits nothing\n * (AGL-765). Kept here so server code need not import the heavy `@aglyn/aglyn`\n * barrel; a drift guard in the console specs asserts the two stay equal.\n */\nexport const EMAIL_NODE_ROOT_ID = '_@_'\n\nexport function renderEmailHtml(options: EmailRenderOptions): RenderedEmail {\n const {\n nodes,\n rootId = 'root',\n subject = '',\n preheader = '',\n merge,\n products,\n sanitize,\n mediaOrigin,\n mediaHostId,\n brandLogoUrl,\n chrome,\n palette,\n buttonRadius,\n } = options\n const radius =\n Number.isFinite(buttonRadius) && Number(buttonRadius) >= 0\n ? Math.min(Number(buttonRadius), 48)\n : 4\n\n /**\n * A stored color, resolved: a palette token against the theme, a literal\n * as written, anything else to the fallback. The only way a color prop\n * reaches the markup, so nothing unresolvable or unsafe is ever written\n * into a style attribute.\n */\n const colorOf = (value: unknown, fallback: string): string =>\n resolveEmailColor(value, palette) ?? fallback\n /** The theme's accent, for what the author left unset. */\n const accent = {\n fill: colorOf('primary.main', '#1a73e8'),\n label: colorOf('primary.contrastText', '#ffffff'),\n // The accent AS TEXT: `primary.dark` is the theme's AA-checked text\n // shade, where the fill color itself may be too pale to read as a link.\n link: colorOf('primary.dark', '#1a73e8'),\n }\n\n const textParts: string[] = []\n const sub = (value: unknown): string =>\n substituteMergeTokens(String(value ?? ''), merge)\n\n /**\n * The merge map as it applies inside a URL: everything except the\n * `contact.` namespace.\n *\n * That namespace is the recipient — `contact.email`, `contact.name`,\n * `contact.firstName` are what campaign-send puts in the map. Substituting\n * them into an `href` or a `src` is how a template like\n * `https://example.test/?e={{contact.email}}` turns every send into the\n * recipient's own address on somebody's query string, and putting personal\n * data in a URL is a line this codebase does not cross. The refusal has to\n * live here because it is the SUBSTITUTION that creates the value: no\n * scheme check and no sanitizer can see the address, since the template\n * the author wrote and the editor reviewed contains only the token.\n *\n * Nothing else narrows: `{{unsubscribeUrl}}` and `{{site.url}}` ARE URLs\n * and are the reason a token appears in this position at all.\n *\n * A refused token is left standing rather than blanked, which is this\n * module's existing convention for a token it will not fill — a test send\n * shows `?e={{contact.email}}` in the link, which names the problem to the\n * author instead of silently shipping a subtly different URL.\n */\n const urlMerge = merge\n ? Object.fromEntries(\n Object.entries(merge).filter(\n ([token]) => !token.startsWith('contact.'),\n ),\n )\n : undefined\n\n /** {@link sub} for a value that lands in a URL — see {@link urlMerge}. */\n const subUrl = (value: unknown): string =>\n substituteMergeTokens(String(value ?? ''), urlMerge)\n\n /**\n * A link target safe to emit, or undefined.\n *\n * Escaping the value into the attribute stops a breakout and says nothing\n * about the scheme, so `escapeEmailHtml` on its own passes a stored\n * `javascript:` or `data:` href through verbatim. The check runs on the\n * SUBSTITUTED string, because a merge value is part of the final URL and a\n * template that reads clean can resolve to one that does not.\n */\n const linkHref = (value: unknown): string | undefined => {\n const href = subUrl(value)\n if (!href) return undefined\n return hasSafeLinkScheme(href) ? href : undefined\n }\n\n const origin = mediaOrigin?.replace(/\\/+$/, '')\n\n /**\n * A stored image value turned into something an inbox can actually fetch,\n * or undefined (AGL-1224).\n *\n * `resolveEmailMediaSrc` handles the three stored generations — a `media:`\n * reference, the AGL-175 relative CDN path, and a plain absolute URL an\n * author typed — but the first two come back site-relative, which is the\n * whole bug: only a browser has a page to resolve them against.\n *\n * With no origin to absolutize against, the image is DROPPED rather than\n * emitted relative. Both are a missing picture; a dropped one leaves a gap,\n * while `src=\"/api/media/cdn/…\"` renders as a broken-image box, which reads\n * to a recipient as a broken email rather than a plain one.\n *\n * ⚠️ This comment used to claim \"neither send path relies on this — both\n * supply an origin\", and treated reaching the drop as hypothetical. It was\n * wrong, and it was wrong in the direction that hides the bug: campaign-send\n * passed no `mediaOrigin`/`mediaHostId` at all, so every author-picked image\n * was silently dropped from every marketing campaign until AGL-1394. The\n * quiet failure IS still the right one for a caller that genuinely has no\n * origin — but \"no caller reaches this\" is a claim about the whole repo, and\n * it decays the moment someone adds a third send path. Verify it before\n * relying on it; do not restore a count here.\n *\n * A protocol-relative `//host/x.png` is passed through untouched: it is\n * already absolute enough to name a host, and prefixing an origin would\n * corrupt it.\n *\n * The scheme is checked on the way out. `resolveEmailMediaSrc` recognizes\n * the two stored reference forms and passes anything else through as an\n * author-typed URL, so it is this check and nothing else that stands\n * between what an author types and the `src` attribute. A refusal drops\n * the image on the same reasoning as everything else here that cannot be\n * made fetchable: a gap reads as a plain email, and a `src` we would not\n * stand behind is not made safer by shipping it.\n */\n const imageSrc = (value: unknown): string | undefined => {\n const resolved = resolveEmailMediaSrc(subUrl(value), mediaHostId)\n if (!resolved || !hasSafeMediaScheme(resolved)) return undefined\n if (!resolved.startsWith('/') || resolved.startsWith('//')) return resolved\n return origin ? `${origin}${resolved}` : undefined\n }\n\n const renderChildren = (ids: string[] | undefined): string =>\n (ids ?? [])\n .map((id) => renderNode(id))\n .filter(Boolean)\n .join('')\n\n const renderNode = (id: string): string => {\n const node = nodes[id]\n if (!node) return ''\n const props = node.props ?? {}\n switch (node.componentId) {\n case 'emailSection': {\n const background = colorOf(props.backgroundColor, '#ffffff')\n const padding = Number.isFinite(Number(props.padding))\n ? Number(props.padding)\n : 24\n const align = props.align || 'left'\n // A ROW, like every other block, with the band's table inside its\n // cell. Whatever encloses a section is a table — the 600px column, or\n // another section's inner table — and a `<table>` placed straight into\n // a table's rows is not a child an HTML parser keeps there: it closes\n // the enclosing table and lands after it. Emitted bare, a section would\n // draw outside the column, as wide as the viewport rather than the\n // 600px the editor shows, and take every row after it out too.\n return row(\n `<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" ` +\n `style=\"background-color:${background};\">` +\n row(\n `<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">` +\n renderChildren(node.nodes) +\n `</table>`,\n `padding:${padding}px;text-align:${align};`,\n ) +\n `</table>`,\n )\n }\n case 'emailText': {\n const text = sub(props.children)\n // A block whose copy is only a token the send left empty (a\n // notification with no detail line, AGL-3367) draws nothing, rather\n // than an empty row and a blank paragraph in the text part.\n if (!text.trim()) return ''\n textParts.push(text)\n const style = TEXT_STYLES[props.variant as string] ?? TEXT_STYLES['body']\n const color = colorOf(props.color, '#1a1a1a')\n const align = props.align || 'left'\n return row(\n `<div style=\"font-family:${FONT};${style};color:${color};text-align:${align};\">` +\n linkifyEscapedText(escapeEmailHtml(text), accent.link).replace(/\\n/g, '<br />') +\n `</div>`,\n 'padding:4px 0;',\n )\n }\n case 'emailRichtext':\n case 'emailHtml': {\n const html = sanitize(sub(props.html))\n if (!html.trim()) return ''\n textParts.push(html.replace(/<[^>]+>/g, ' ').trim())\n return row(\n `<div style=\"font-family:${FONT};font-size:15px;line-height:1.55;\">${html}</div>`,\n 'padding:4px 0;',\n )\n }\n case 'emailImage': {\n const src = imageSrc(props.src)\n if (!src) return ''\n const width = Number(props.width) > 0 ? Number(props.width) : 600\n const alt = escapeEmailHtml(String(props.alt ?? ''))\n const align = props.align || 'center'\n const img =\n `<img src=\"${escapeEmailHtml(src)}\" alt=\"${alt}\" width=\"${Math.min(width, 600)}\" ` +\n `style=\"display:inline-block;max-width:100%;height:auto;border:0;\" />`\n // A refused href drops the WRAPPER, not the picture: the image is the\n // content and the link is decoration on it.\n const href = linkHref(props.href)\n return row(\n href\n ? `<a href=\"${escapeEmailHtml(href)}\" target=\"_blank\">${img}</a>`\n : img,\n `padding:8px 0;text-align:${align};`,\n )\n }\n case 'emailButton': {\n const label = sub(props.children ?? 'Call to action')\n // A refused href falls back to the placeholder the empty case already\n // uses, so the button keeps its place in the layout and goes nowhere.\n // Dropping the row instead would take the author's copy with it, and\n // an inert button reads as an unfinished email rather than a broken\n // one. The plain-text alternative gets the same value the anchor\n // does — a text/plain body is inert, but it is still delivered mail\n // and a refused URL does not belong in it either.\n const href = linkHref(props.href ?? '#') ?? '#'\n const background = colorOf(props.backgroundColor, accent.fill)\n const color = colorOf(props.color, accent.label)\n const align = props.align || 'center'\n textParts.push(`${label}: ${href}`)\n // Bulletproof-ish button: padded anchor, table-aligned.\n return row(\n `<a href=\"${escapeEmailHtml(href)}\" target=\"_blank\" ` +\n `style=\"display:inline-block;padding:12px 28px;border-radius:${radius}px;` +\n `background-color:${background};color:${color};font-family:${FONT};` +\n `font-size:15px;font-weight:600;text-decoration:none;\">` +\n escapeEmailHtml(label) +\n `</a>`,\n `padding:12px 0;text-align:${align};`,\n )\n }\n case 'emailDivider': {\n const color = colorOf(props.color, '#e0e0e0')\n return row(\n `<div style=\"border-top:1px solid ${color};font-size:0;line-height:0;\">&nbsp;</div>`,\n 'padding:12px 0;',\n )\n }\n case 'emailSpacer': {\n const height = Number(props.height) > 0 ? Number(props.height) : 24\n return row(\n `<div style=\"height:${height}px;font-size:0;line-height:0;\">&nbsp;</div>`,\n )\n }\n case 'emailProduct': {\n const product = props.productId\n ? products?.[String(props.productId)]\n : undefined\n if (!product) return ''\n const label = sub(props.buttonLabel ?? 'Shop now')\n // Catalog data rather than author markup, and the caller absolutizes\n // it before it arrives — but \"it came from a trusted table\" is a\n // claim about every writer of that table, so the same rule applies\n // here as to an author-typed href. Refused means no button and no\n // line in the text part; the product card itself still renders.\n const productUrl =\n product.url && hasSafeLinkScheme(product.url)\n ? product.url\n : undefined\n textParts.push(\n `${product.name}${product.priceLabel ? ` — ${product.priceLabel}` : ''}` +\n (productUrl ? `: ${productUrl}` : ''),\n )\n // Same treatment as emailImage: a catalog image can be a picked\n // asset, so it carries the same reference/relative forms (AGL-1224).\n const productImage = imageSrc(product.imageUrl)\n const image = productImage\n ? `<img src=\"${escapeEmailHtml(productImage)}\" alt=\"${escapeEmailHtml(product.name)}\" width=\"280\" style=\"max-width:100%;height:auto;border:0;border-radius:6px;\" /><br />`\n : ''\n const button = productUrl\n ? `<a href=\"${escapeEmailHtml(productUrl)}\" target=\"_blank\" style=\"display:inline-block;margin-top:8px;padding:10px 24px;border-radius:${radius}px;background-color:${accent.fill};color:${accent.label};font-family:${FONT};font-size:14px;font-weight:600;text-decoration:none;\">${escapeEmailHtml(label)}</a>`\n : ''\n return row(\n `<div style=\"border:1px solid #e0e0e0;border-radius:8px;padding:16px;text-align:center;font-family:${FONT};\">` +\n image +\n `<div style=\"font-size:16px;font-weight:600;margin-top:8px;\">${escapeEmailHtml(product.name)}</div>` +\n (product.priceLabel\n ? `<div style=\"font-size:14px;color:#555555;margin-top:2px;\">${escapeEmailHtml(product.priceLabel)}</div>`\n : '') +\n button +\n `</div>`,\n 'padding:8px 0;',\n )\n }\n default: {\n // Unknown/web components: render their children so mixed documents\n // degrade gracefully instead of dropping content.\n return renderChildren(node.nodes)\n }\n }\n }\n\n const body = renderNode(rootId) || renderChildren(nodes[rootId]?.nodes)\n const preheaderHtml = preheader\n ? `<div style=\"display:none;max-height:0;overflow:hidden;mso-hide:all;\">` +\n escapeEmailHtml(sub(preheader)) +\n `</div>`\n : ''\n\n // INSIDE the 600px table, above the designed body, so it inherits the\n // column's width on a phone instead of being centred against the viewport.\n // `alt` is the product name where one is known, because a logo blocked by\n // the client (which is the default in most inboxes) must still say who sent\n // the mail. Height is capped rather than set, so a wordmark and a square\n // mark both land at a sane size without the sender supplying dimensions.\n //\n // The src goes through `imageSrc` like every other image in the message\n // (AGL-2230), and NOT into the tag raw. `emailLogoUrl` is collected by\n // `MediaUrlField`, whose \"Browse\" button writes the picked asset's\n // site-relative `cdnPath` — and the org DAM's other generation stores a\n // `media:{scope}/{mediaId}` reference. Neither is a URL an inbox can\n // resolve: there is no page to resolve it against, so a raw pass-through\n // put `src=\"media:h1/med9\"` at the top of every transactional email a\n // white-label org sent. `imageSrc` absolutizes both forms against\n // `mediaOrigin`, passes a real https URL through untouched, and returns\n // undefined when it cannot produce something fetchable — in which case the\n // row is dropped, on the same reasoning as AGL-1224: a gap reads as a plain\n // email, a broken-image box reads as a broken one.\n const brandLogo = imageSrc(String(brandLogoUrl ?? '').trim()) ?? ''\n // The product name from the merge map, because most inboxes block images by\n // default — a logo with no `alt` is a blank box where the sender's identity\n // should be. Falls back to nothing rather than to a literal, which would\n // reintroduce the hard-coded brand this whole change removes.\n const brandLogoAlt = merge?.['brand.productName'] ?? ''\n const brandLogoHtml = brandLogo\n ? row(\n `<img src=\"${escapeEmailHtml(brandLogo)}\" alt=\"${escapeEmailHtml(brandLogoAlt)}\" ` +\n `style=\"max-height:48px;max-width:200px;display:block;margin:0 auto;border:0;\" />`,\n 'padding:24px 24px 0;text-align:center;',\n )\n : ''\n\n /**\n * The chrome header row, or '' when it has nothing to draw.\n *\n * Same placement and the same `imageSrc` as the logo row above, which it\n * replaces. Height is capped at 40px rather than set, so a wordmark and a\n * square mark both land at a sane size; the `height` attribute is for the\n * desktop clients that ignore CSS `max-height` and would otherwise draw the\n * file at its own size. A logo that cannot be fetched from an inbox falls\n * back to the name in bold, never to an `<img>` that draws a broken box.\n * White, so the header and a default body read as one card.\n */\n const renderChromeHeader = (header: EmailChromeHeader): string => {\n const logo = imageSrc(String(header.logoUrl ?? '').trim())\n const name = sub(header.logoAlt).trim()\n const href = linkHref(header.href)\n const link = (inner: string, style: string): string =>\n href\n ? `<a href=\"${escapeEmailHtml(href)}\" target=\"_blank\" style=\"${style}\">${inner}</a>`\n : inner\n let mark: string\n if (logo) {\n mark = link(\n `<img src=\"${escapeEmailHtml(logo)}\" alt=\"${escapeEmailHtml(name)}\" height=\"40\" ` +\n `style=\"display:block;margin:0 auto;border:0;height:auto;width:auto;max-height:40px;max-width:200px;\" />`,\n 'text-decoration:none;',\n )\n } else if (name) {\n mark =\n `<div style=\"font-family:${FONT};font-size:20px;font-weight:700;line-height:1.3;color:${CHROME_PALETTE.ink};text-align:center;\">` +\n link(escapeEmailHtml(name), `color:${CHROME_PALETTE.ink};text-decoration:none;`) +\n `</div>`\n } else {\n return ''\n }\n return row(\n mark,\n `padding:24px 24px 0;text-align:center;background-color:${CHROME_PALETTE.card};`,\n )\n }\n\n const headerHtml = chrome?.header\n ? renderChromeHeader(chrome.header)\n : brandLogoHtml\n\n /**\n * The chrome footer: rows under the body in the look of the email footer\n * block the platform's own mail wears (a `#F8F9FA` section, padded 24,\n * centered captions), and the same lines appended to the plain-text part.\n *\n * Rendered AFTER the body on purpose: the body's blocks push their text as\n * they draw, and the footer's lines belong after all of it. A line that\n * resolves to nothing is left out rather than drawn as an empty row.\n */\n const renderChromeFooter = (footer: EmailChromeFooter): string => {\n const htmlLines: string[] = []\n const textLines: string[] = []\n const reason = sub(footer.reason).trim()\n if (reason) {\n htmlLines.push(escapeEmailHtml(reason))\n textLines.push(reason)\n }\n const supportLabel = sub(footer.support?.label).trim()\n const supportHref = linkHref(footer.support?.href)\n if (supportLabel && supportHref) {\n htmlLines.push(\n `<a href=\"${escapeEmailHtml(supportHref)}\" target=\"_blank\" ` +\n `style=\"color:${CHROME_PALETTE.caption};text-decoration:underline;\">${escapeEmailHtml(supportLabel)}</a>`,\n )\n // A plain-text reader gets the address itself, bare: a mailto's\n // mailbox, or the URL. \"Get help: https://…\" names where it goes; a\n // label that already IS the mailbox is not repeated.\n const bare = /^mailto:/i.test(supportHref)\n ? supportHref.slice('mailto:'.length).split('?')[0]\n : supportHref\n textLines.push(supportLabel === bare ? bare : `${supportLabel}: ${bare}`)\n }\n const legal = sub(footer.legal).trim()\n if (legal) {\n htmlLines.push(escapeEmailHtml(legal))\n textLines.push(legal)\n }\n if (!htmlLines.length) return ''\n textParts.push(textLines.join('\\n'))\n return row(\n htmlLines\n .map(\n (line, index) =>\n `<div style=\"font-family:${FONT};font-size:12px;font-weight:400;line-height:1.4;color:${CHROME_PALETTE.caption};` +\n `${index ? 'margin-top:8px;' : ''}\">${line.replace(/\\n/g, '<br />')}</div>`,\n )\n .join(''),\n `padding:24px;text-align:center;background-color:${CHROME_PALETTE.band};`,\n )\n }\n\n const footerHtml = chrome?.footer ? renderChromeFooter(chrome.footer) : ''\n\n const html =\n `<!DOCTYPE html><html><head><meta charset=\"utf-8\" />` +\n `<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />` +\n `<title>${escapeEmailHtml(sub(subject))}</title></head>` +\n `<body style=\"margin:0;padding:0;background-color:#f4f4f4;\">` +\n preheaderHtml +\n `<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color:#f4f4f4;\">` +\n row(\n `<table role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" align=\"center\" style=\"max-width:600px;width:100%;margin:0 auto;\">` +\n headerHtml +\n body +\n footerHtml +\n `</table>`,\n 'padding:24px 8px;',\n ) +\n `</table></body></html>`\n\n return { html, text: textParts.join('\\n\\n') }\n}\n"],"names":["hasSafeLinkScheme","hasSafeMediaScheme","linkifyEscapedText","resolveEmailColor","resolveEmailMediaSrc","FONT","CHROME_PALETTE","ink","card","band","caption","escapeEmailHtml","value","replace","substituteMergeTokens","merge","match","token","replacement","undefined","TEXT_STYLES","heading","subheading","body","row","cellHtml","cellStyle","EMAIL_NODE_ROOT_ID","renderEmailHtml","options","imageSrc","nodes","rootId","subject","preheader","products","sanitize","mediaOrigin","mediaHostId","brandLogoUrl","chrome","palette","buttonRadius","radius","Number","isFinite","Math","min","colorOf","fallback","accent","fill","label","link","textParts","sub","String","urlMerge","Object","fromEntries","entries","filter","startsWith","subUrl","linkHref","href","origin","resolved","renderChildren","ids","map","id","renderNode","Boolean","join","node","props","componentId","background","backgroundColor","padding","align","text","children","trim","push","style","variant","color","html","src","width","alt","img","height","product","productId","buttonLabel","productUrl","url","name","priceLabel","productImage","imageUrl","image","button","preheaderHtml","brandLogo","brandLogoAlt","brandLogoHtml","renderChromeHeader","header","logo","logoUrl","logoAlt","inner","mark","headerHtml","renderChromeFooter","footer","htmlLines","textLines","reason","supportLabel","support","supportHref","bare","test","slice","length","split","legal","line","index","footerHtml"],"mappings":"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"}