From 440a2e7ac7a8cf929cc0b2fd3bdb3fa6d62597b7 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Thu, 29 Oct 2026 04:29:23 +0100
Subject: [PATCH] fix: scrub only real stylesheets and keep their css escapes
when blocking remote content
---
src/lib/__tests__/email-html.test.ts | 33 +++++
src/lib/email-html.ts | 172 +++++++++++++++++----------
2 files changed, 143 insertions(+), 62 deletions(-)
diff --git a/src/lib/__tests__/email-html.test.ts b/src/lib/__tests__/email-html.test.ts
index 268dd00c..415ac4ec 100644
--- a/src/lib/__tests__/email-html.test.ts
+++ b/src/lib/__tests__/email-html.test.ts
@@ -187,6 +187,39 @@ describe('blockExternalResources', () => {
expect(html).not.toMatch(/<\/style>\s*` in
+ // one attribute and a `` in a later one made it "scrub" the
+ // attributes between them, decoding the CSS escapes into real quotes.
+ it('scrubs only real stylesheets, not text inside attribute values', () => {
+ const input = String.raw`click
z
`; + const { html } = prepareEmailHtml(input, { blockRemoteImages: true, isDark: false }); + expect(html).not.toMatch(/href="javascript:/i); + expect(html).toContain(String.raw`click`); + }); + + it('cuts external URLs out of a sheet and leaves its escapes as written', () => { + const css = String.raw`a{background:url(https://x/)} b{background:\5c 75rl(https://y/)} c{background:\75rl(https://z/)} d{content:"\"\3c /style\3e"}`; + const { html, blocked } = blockExternalResources(``); + expect(blocked).toBe(true); + // `\5c 75rl(` is the text `\75rl(`, not a url(): written decoded it + // became one. The escaped quote stays escaped. + expect(html).toBe(String.raw``); + }); + + it('reads an escape past the last code point as U+FFFD instead of throwing', () => { + expect(decodeCssEscapes('\\FFFFFF')).toBe('\ufffd'); + expect(() => blockExternalResources('')).not.toThrow(); + }); + + it('finds images the parser reads as
');
expect(blocked).toBe(true);
diff --git a/src/lib/email-html.ts b/src/lib/email-html.ts
index f7e9cf9b..032f19c7 100644
--- a/src/lib/email-html.ts
+++ b/src/lib/email-html.ts
@@ -200,10 +200,15 @@ function serializeTag(name: string, attrs: ParsedAttr[], selfClosing: boolean):
* the browser keeps them) and returns the list to write, `null` to keep the
* attributes unchanged, or `false` to drop the tag. End tags are written
* without attributes; comments, doctypes and bogus comments are dropped.
+ * `rawText`, when given, rewrites the text of each kept raw-text element
+ * (a `