fix: keep an Outlook-style reply from loading the quoted message's remote images

This commit is contained in:
Linus Rath
2026-10-26 19:30:18 +01:00
parent 12ee24a81a
commit edba184bc0
2 changed files with 31 additions and 4 deletions
@@ -6,6 +6,30 @@ import { buildQuotedHtmlBlock, QuotedHtml, serializeEditorContent } from '../quo
const createEditor = (content: string) => new Editor({ extensions: [StarterKit, QuotedHtml], content });
describe('HTML quote borders', () => {
it('flattens quotes without parsing the original into the page, where its images would load', () => {
// A browser fetches an <img> as soon as it is parsed into an element of
// the live document, attached or not: that fires the tracker the remote
// content setting holds back.
const descriptor = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML')!;
const liveParses: string[] = [];
Object.defineProperty(Element.prototype, 'innerHTML', {
...descriptor,
set(this: Element, value: string) {
if (this.ownerDocument === document && this.tagName !== 'TEMPLATE') liveParses.push(value);
descriptor.set!.call(this, value);
},
});
let html = '';
try {
html = buildQuotedHtmlBlock('<blockquote><img src="https://tracker.example/p.png">Hi</blockquote>', { showQuoteBorder: false });
} finally {
Object.defineProperty(Element.prototype, 'innerHTML', descriptor);
}
expect(liveParses.filter((value) => value.includes('tracker.example'))).toEqual([]);
expect(html).toContain('<img src="https://tracker.example/p.png">');
expect(html).not.toContain('blockquote');
});
it('keeps normal quote styling by default', () => {
const original = '<blockquote>Original reply</blockquote>';
const editor = createEditor(buildQuotedHtmlBlock(original));
+7 -4
View File
@@ -303,14 +303,17 @@ export function buildQuotedHtmlBlock(
if (!showQuoteBorder) {
// Outlook-style replies flatten quote levels, including earlier replies.
// Work on sanitized DOM so tables, links and signatures remain intact.
const container = document.createElement("div");
// A template's content is inert: parsed into a plain div, the original's
// remote images would load here, past the parking the island does.
const container = document.createElement("template");
container.innerHTML = html;
container.querySelectorAll("blockquote").forEach((quote) => {
const block = document.createElement("div");
const content = container.content;
content.querySelectorAll("blockquote").forEach((quote) => {
const block = content.ownerDocument.createElement("div");
block.append(...Array.from(quote.childNodes));
quote.replaceWith(block);
});
container.querySelectorAll<HTMLElement>(`[${QUOTED_HTML_MARKER}]`).forEach((quote) => {
content.querySelectorAll<HTMLElement>(`[${QUOTED_HTML_MARKER}]`).forEach((quote) => {
quote.setAttribute("data-quote-border", "false");
quote.style.borderLeft = "none";
quote.style.paddingLeft = "0";