diff --git a/src/frontend/src/features/utils/unquote-message/handlers.ts b/src/frontend/src/features/utils/unquote-message/handlers.ts index 86d567b2..ffe1eee2 100644 --- a/src/frontend/src/features/utils/unquote-message/handlers.ts +++ b/src/frontend/src/features/utils/unquote-message/handlers.ts @@ -143,15 +143,31 @@ export const HANDLERS: CustomHandler[] = [ const normalizedStyle = style.replaceAll(/(cm|pt|mm)/g, "in"); if (normalizedStyle.endsWith(" 1.0in;padding:3.0in 0in 0in 0in")) { - // Check if parent has only one element child - if (msoRoot.parentElement) { - const elementChildren = Array.from( - msoRoot.parentElement.childNodes - ).filter((node) => node.nodeType === Node.ELEMENT_NODE); - - if (elementChildren.length === 1) { - msoRoot = msoRoot.parentElement as HTMLElement; + // The quoted content may live at an upper level: climb out of wrapper + // divs while the header has no following sibling, as long as nothing + // meaningful precedes it inside the wrapper (to avoid swallowing the + // new message content) + while ( + msoRoot.parentElement && + msoRoot.parentElement.tagName.toLowerCase() !== "body" && + !msoRoot.nextElementSibling + ) { + // Use childNodes to also catch plain-text nodes, but only those + // preceding the header: text nodes following it belong to the quote + // and must not halt the climb + const siblings = Array.from(msoRoot.parentElement.childNodes); + const previousSiblings = siblings.slice(0, siblings.indexOf(msoRoot)); + if ( + previousSiblings.some( + (node) => + (node.nodeType === Node.ELEMENT_NODE || + node.nodeType === Node.TEXT_NODE) && + node.textContent?.trim() + ) + ) { + break; } + msoRoot = msoRoot.parentElement; } // Collect all next siblings @@ -175,7 +191,14 @@ export const HANDLERS: CustomHandler[] = [ const detectedElements: HTMLElement[] = []; let prev = element.previousElementSibling; while (prev) { - if (prev.tagName.toLowerCase() === "hr") { + // Outlook renders the reply separator either as a bare
+ // or as an
alone inside a centering div + const isSeparator = + prev.tagName.toLowerCase() === "hr" || + (prev.children.length === 1 && + prev.children[0].tagName.toLowerCase() === "hr" && + !prev.textContent?.trim()); + if (isSeparator) { // It's a reply from Outlook! detectedElements.push(element, prev as HTMLElement); let sibling = element.nextElementSibling; diff --git a/src/frontend/src/features/utils/unquote-message/index.test.ts b/src/frontend/src/features/utils/unquote-message/index.test.ts index b251275d..d2c93244 100644 --- a/src/frontend/src/features/utils/unquote-message/index.test.ts +++ b/src/frontend/src/features/utils/unquote-message/index.test.ts @@ -375,6 +375,85 @@ describe("UnquoteMessage", () => { ); }); + it("should remove Microsoft Outlook Desktop quotes when the hr separator is wrapped in a div", () => { + const html = ` +
My reply to the message
+

+
+ De : sender@example.com
+ Envoyé : lundi 6 juillet 2026 15:45
+ À : recipient@example.com
+
+
Original message body
+ `; + + const result = new UnquoteMessage(html).getHtml(); + + expect(result.hadQuotes).toBe(true); + expect(result.detectionMethod).toBe("handlers"); + expect(result.content).toMatchInlineSnapshot( + `"
My reply to the message
"` + ); + }); + + it("should remove Microsoft Outlook Web quotes when the header is nested in a wrapper and the body is a sibling of the wrapper", () => { + const html = ` +
My reply
+
+
+
+

De : sender@example.com

+
+
+
Original message body
+ `; + + const result = new UnquoteMessage(html).getHtml(); + + expect(result.hadQuotes).toBe(true); + expect(result.detectionMethod).toBe("handlers"); + expect(result.content).toMatchInlineSnapshot( + `"
My reply
"` + ); + }); + + it("should not swallow a plain-text reply preceding the Microsoft Outlook Web header inside a wrapper", () => { + const html = ` +
+ My plain-text reply +
+

De : sender@example.com

+
+
+ `; + + const result = new UnquoteMessage(html).getHtml(); + + expect(result.hadQuotes).toBe(true); + expect(result.content).toContain("My plain-text reply"); + expect(result.content).not.toContain("sender@example.com"); + }); + + it("should keep climbing when only quoted text nodes follow the Microsoft Outlook Web header", () => { + const html = ` +
My reply
+
+
+

De : sender@example.com

+
+ Quoted text node +
+
Original message body
+ `; + + const result = new UnquoteMessage(html).getHtml(); + + expect(result.hadQuotes).toBe(true); + expect(result.content).toMatchInlineSnapshot( + `"
My reply
"` + ); + }); + it("should remove ZMail quotes with zmail_extra", () => { const html = `
New message