🐛(frontend) fix outlook web handler in unquote logic (#754)

About the unquote-message logic, we encount a bug with a thread
implying Outlook Desktop quotes. Actually, for Outlook web we were looking for
a hr tag as quote separtor element. But sometimes this one can be wrap into a div
and we missed it.
This commit is contained in:
Jean-Baptiste PENRATH
2026-07-20 10:55:10 +02:00
committed by GitHub
parent 39f2d9ca98
commit 1eb68eeb61
2 changed files with 111 additions and 9 deletions
@@ -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 <hr>
// or as an <hr> 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;
@@ -375,6 +375,85 @@ describe("UnquoteMessage", () => {
);
});
it("should remove Microsoft Outlook Desktop quotes when the hr separator is wrapped in a div", () => {
const html = `
<div>My reply to the message</div>
<div class="MsoNormal" align="center" style="text-align:center"><hr size="3" width="98%" align="center"></div>
<div id="divRplyFwdMsg">
<b>De :</b> sender@example.com<br>
<b>Envoyé :</b> lundi 6 juillet 2026 15:45<br>
<b>À :</b> recipient@example.com<br>
</div>
<div>Original message body</div>
`;
const result = new UnquoteMessage(html).getHtml();
expect(result.hadQuotes).toBe(true);
expect(result.detectionMethod).toBe("handlers");
expect(result.content).toMatchInlineSnapshot(
`"<div>My reply to the message</div>"`
);
});
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 = `
<div>My reply</div>
<div>
<div id="content_out_sender_example.com"></div>
<div style="border:none;border-top:solid #E1E1E1 1.0pt;padding:3.0pt 0cm 0cm 0cm">
<p class="MsoNormal"><b>De :</b> sender@example.com</p>
</div>
</div>
<div>Original message body</div>
`;
const result = new UnquoteMessage(html).getHtml();
expect(result.hadQuotes).toBe(true);
expect(result.detectionMethod).toBe("handlers");
expect(result.content).toMatchInlineSnapshot(
`"<div>My reply</div>"`
);
});
it("should not swallow a plain-text reply preceding the Microsoft Outlook Web header inside a wrapper", () => {
const html = `
<div>
My plain-text reply
<div style="border:none;border-top:solid #E1E1E1 1.0pt;padding:3.0pt 0cm 0cm 0cm">
<p class="MsoNormal"><b>De :</b> sender@example.com</p>
</div>
</div>
`;
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 = `
<div>My reply</div>
<div>
<div style="border:none;border-top:solid #E1E1E1 1.0pt;padding:3.0pt 0cm 0cm 0cm">
<p class="MsoNormal"><b>De :</b> sender@example.com</p>
</div>
Quoted text node
</div>
<div>Original message body</div>
`;
const result = new UnquoteMessage(html).getHtml();
expect(result.hadQuotes).toBe(true);
expect(result.content).toMatchInlineSnapshot(
`"<div>My reply</div>"`
);
});
it("should remove ZMail quotes with zmail_extra", () => {
const html = `
<div>New message</div>