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