import { describe, it, expect } from 'vitest'; import { stripDangerousTags, isExternalResourceUrl, decodeCssEscapes, stripExternalStyleSheetCss, blockExternalResources, hasRemoteContent, prepareEmailHtml, wrapPlainTextEmail, wrapEmailHtml, hasNativeDarkMode, sniffImageMime, TRANSPARENT_BLOCKED_PIXEL, } from '../email-html'; describe('stripDangerousTags', () => { it('keeps
x
'); expect(out).toContain(''); }); it('still removes scripts, iframes, svg, link and event handlers', () => { const out = stripDangerousTags( '' + 'l', ); expect(out).not.toMatch(/ { expect(stripDangerousTags('')).toContain('data:image/png'); expect(stripDangerousTags('')).toContain('#blocked'); expect(stripDangerousTags('x')).toContain('#blocked'); }); it('scheme-checks every srcset candidate', () => { const out = stripDangerousTags(''); expect(out).toContain('srcset="https://a/1.png 1x"'); const svg = stripDangerousTags(''); expect(svg).toContain('srcset=""'); }); it('blocks entity-obfuscated javascript: URLs', () => { expect(stripDangerousTags('x')).toContain('#blocked'); expect(stripDangerousTags('x')).toContain('#blocked'); }); }); describe('isExternalResourceUrl', () => { it('detects http(s) and protocol-relative URLs including whitespace tricks', () => { expect(isExternalResourceUrl('https://x/p.gif')).toBe(true); expect(isExternalResourceUrl('\n\nhttps://x/p.gif')).toBe(true); expect(isExternalResourceUrl('ht\ttps://x')).toBe(true); expect(isExternalResourceUrl('//x/p.gif')).toBe(true); expect(isExternalResourceUrl('data:image/png;base64,AA')).toBe(false); expect(isExternalResourceUrl('cid:abc')).toBe(false); expect(isExternalResourceUrl(undefined)).toBe(false); }); }); describe('decodeCssEscapes / stripExternalStyleSheetCss', () => { it('decodes hex and char escapes', () => { expect(decodeCssEscapes('\\68ttp://x')).toBe('http://x'); expect(decodeCssEscapes('\\000068ttp://x')).toBe('http://x'); expect(decodeCssEscapes('\\75\\72\\6C(')).toBe('url('); }); it('neutralises external url(), @import and @font-face sources', () => { const css = '.a{background:url("https://t/x.png")} @import "https://t/a.css"; @font-face{src:url(https://t/f.woff)} .b{color:red}'; const out = stripExternalStyleSheetCss(css); expect(out).not.toContain('https://t'); expect(out).toContain('.b{color:red}'); }); it('catches escaped url( keywords', () => { const css = '.a{background:\\75\\72\\6C(https://t/x.png)}'; expect(stripExternalStyleSheetCss(css)).not.toContain('https://t'); }); it('returns the identical string when nothing is external', () => { const css = '.a{background:url(data:image/png;base64,AA)}'; expect(stripExternalStyleSheetCss(css)).toBe(css); }); }); describe('blockExternalResources', () => { it('swaps external img src for a transparent pixel and hides it', () => { const { html, blocked } = blockExternalResources('logo'); expect(blocked).toBe(true); expect(html).toContain(`src="${TRANSPARENT_BLOCKED_PIXEL}"`); expect(html).toContain('data-blocked-src="https://t/p.gif"'); expect(html).toContain('alt=""'); expect(html).toContain('display:none'); }); it('covers srcset, , poster, media src, background= and inline url()', () => { const input = [ '', '', '', '
x
', '
y
', ].join(''); const { html, blocked } = blockExternalResources(input); expect(blocked).toBe(true); // Only the data-blocked-* stashes may still carry the URLs. expect(html.replace(/data-blocked-[a-z]+="[^"]*"/g, '')).not.toContain('https://t/'); expect(html).toContain('data-blocked-srcset'); expect(html).toContain('data-blocked-poster'); expect(html).toContain('data-blocked-background'); expect(html).toContain('color:red'); }); it('catches whitespace-obfuscated src and leaves inline images alone', () => { const { html, blocked } = blockExternalResources(''); expect(blocked).toBe(true); expect(html).toContain('src="cid:a"'); expect(html).toContain('data:image/png;base64,AA'); }); it('scrubs '); expect(blocked).toBe(true); expect(html).not.toContain('https://t'); }); it('reports nothing blocked for a local-only body', () => { const { html, blocked } = blockExternalResources('

hi

'); expect(blocked).toBe(false); expect(html).toContain('

hi

'); expect(hasRemoteContent('

hi

')).toBe(false); expect(hasRemoteContent('')).toBe(true); }); }); describe('prepareEmailHtml', () => { it('uses the strict CSP only when blocking', () => { const strict = prepareEmailHtml('

x

', { blockRemoteImages: true }); expect(strict.html).toContain('img-src data: cid:;'); expect(strict.html).toContain("media-src 'none'"); const open = prepareEmailHtml('

x

', { blockRemoteImages: false }); expect(open.html).toContain('img-src data: cid: https: http:'); }); it('evaluates native dark mode once and reports the inversion decision', () => { const native = prepareEmailHtml('

x

', { isDark: true }); expect(native.hasNativeDark).toBe(true); expect(native.applyInversion).toBe(false); expect(native.html).toContain('color-scheme: light dark'); expect(native.html).not.toContain('invert(1)'); const plain = prepareEmailHtml('

x

', { isDark: true }); expect(plain.applyInversion).toBe(true); expect(plain.html).toContain('body { filter: invert(1) hue-rotate(180deg)'); // Current webmail revision: background-image containers re-invert without the :has guard. expect(plain.html).toMatch(/\[style\*="background-image"\],\s*\[background\]\s*\{\s*filter: invert\(1\)/); const light = prepareEmailHtml('

x

', { isDark: false }); expect(light.applyInversion).toBe(false); }); it('reports blocked external content', () => { const res = prepareEmailHtml('', { blockRemoteImages: true }); expect(res.blockedExternal).toBe(true); const none = prepareEmailHtml('

hi

', { blockRemoteImages: true }); expect(none.blockedExternal).toBe(false); }); it('inlines cid images from the map and keeps a transparent placeholder otherwise', () => { const res = prepareEmailHtml('', { cidMap: { a: 'data:image/png;base64,AA' } }); expect(res.html).toContain('src="data:image/png;base64,AA"'); expect(res.html).toContain(`src="${TRANSPARENT_BLOCKED_PIXEL}"`); }); it('tightens Word/Outlook HTML and drops the gutter for full-bleed canvases in auto mode', () => { const word = prepareEmailHtml('

x

'); expect(word.html).toContain('p.MsoNormal'); const bleed = prepareEmailHtml('
x
', { messageSpacing: 'auto' }); expect(bleed.html).toContain('padding: 0;'); const gutter = prepareEmailHtml('

x

', { messageSpacing: 'always' }); expect(gutter.html).toContain('padding: 16px;'); const edge = prepareEmailHtml('

x

', { messageSpacing: 'edge' }); expect(edge.html).toContain('padding: 0;'); }); it('neutralises height:100% wrappers', () => { expect(prepareEmailHtml('

x

').html).toContain('[style*="height:100%"]'); }); it('caps images and tables at the pane without overriding a sender max-width (#790)', () => { const { html } = prepareEmailHtml( '' + '
x
', ); // An unconditional !important cap would beat the inline max-width. expect(html).not.toMatch(/(^|[\s}])img\s*\{[^}]*max-width:\s*100%\s*!important/); expect(html).not.toMatch(/(^|[\s}])table\s*\{[^}]*max-width:\s*100%\s*!important/); expect(html).toContain('img:not([style*="max-width"]) { max-width: 100% !important; }'); expect(html).toContain('img[style*="max-width"] { max-width: 100%; }'); expect(html).toContain('img { height: auto !important; }'); expect(html).toContain('table:not([style*="max-width"]) { max-width: 100% !important; }'); expect(html).toContain('table[style*="max-width"] { max-width: 100%; }'); // The sender's sizing reaches the document untouched. expect(html).toContain('max-height:134px; max-width:200px; width:100%;'); expect(html).toContain('style="max-width:300px; width:100%"'); }); it('lets the content pick its text direction', () => { expect(prepareEmailHtml('

مرحبا بالعالم.

').html).toContain(''); }); it('keeps a direction the email declares on its own html or body tag', () => { const body = prepareEmailHtml('

Newsletter مرحبا

').html; expect(body).not.toContain('dir="auto"'); expect(body).toContain(''); const root = prepareEmailHtml('

x

').html; expect(root).not.toContain('dir="auto"'); // A dir on an inner element is not a document direction. expect(prepareEmailHtml('

x

').html).toContain(''); }); }); describe('wrapPlainTextEmail', () => { it('uses the app font by default and monospace on request', () => { expect(wrapPlainTextEmail('x', { font: 'sans' })).toContain('-apple-system'); expect(wrapPlainTextEmail('x', { font: 'mono' })).toContain('ui-monospace'); expect(wrapPlainTextEmail('x')).not.toContain('ui-monospace'); }); it('lets the content pick its text direction', () => { expect(wrapPlainTextEmail('مرحبا بالعالم.')).toContain(''); }); }); describe('hasNativeDarkMode', () => { it('detects prefers-color-scheme rules', () => { expect(hasNativeDarkMode('@media (prefers-color-scheme:dark){}')).toBe(true); expect(hasNativeDarkMode('

x

')).toBe(false); }); }); describe('sniffImageMime', () => { it('recognises PNG/JPEG/GIF/WebP magic bytes', () => { expect(sniffImageMime(new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), 'application/octet-stream')).toBe('image/png'); expect(sniffImageMime(new Uint8Array([0xff, 0xd8, 0xff, 0xe0]))).toBe('image/jpeg'); expect(sniffImageMime(new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]))).toBe('image/gif'); expect(sniffImageMime(new Uint8Array([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50]))).toBe('image/webp'); }); it('falls back to a declared image type, else octet-stream', () => { expect(sniffImageMime(new Uint8Array([1, 2, 3]), 'image/tiff')).toBe('image/tiff'); expect(sniffImageMime(new Uint8Array([1, 2, 3]), 'application/octet-stream')).toBe('application/octet-stream'); }); }); describe('reading width on a wide pane', () => { it('caps HTML and plain-text bodies only above the tablet breakpoint', () => { const html = wrapEmailHtml('

Hello

'); expect(html).toMatch(/@media \(min-width: 840px\) \{\s*body \{ max-width: 720px;/); const plain = wrapPlainTextEmail('Hello'); expect(plain).toContain('@media (min-width: 840px) { body { max-width: 80ch; } }'); }); });