diff --git a/app/javascript/dashboard/helper/emailQuoteExtractor.js b/app/javascript/dashboard/helper/emailQuoteExtractor.js
index 965a91f66..0bd68243c 100644
--- a/app/javascript/dashboard/helper/emailQuoteExtractor.js
+++ b/app/javascript/dashboard/helper/emailQuoteExtractor.js
@@ -126,11 +126,12 @@ export class EmailQuoteExtractor {
return this.findBlocksContainingText(root, HEADER_PATTERNS);
}
- // ---------- 5. Plain-text RFC `>` tail ----------
- // For text/plain emails (after sanitizeTextForRender converts \n →
),
- // find the earliest top-level text node whose visible lines all begin with
- // `>` and strip from there to the end (collapsing leading
/whitespace
- // separators back into the tail).
+ // ---------- 5. Top-level quote tail ----------
+ // For replies that arrive as text +
with no block wrapper (text/plain
+ // bodies after sanitizeTextForRender). Find the earliest top-level text
+ // node that begins a quote tail — either every visible line starts with `>`
+ // (RFC quote prefix) or the text contains a header marker — and strip from
+ // there, collapsing leading
/whitespace separators into the tail.
static removePlainTextTail(root) {
const start = this.findPlainTextTailStart(root);
@@ -142,7 +143,7 @@ export class EmailQuoteExtractor {
static findPlainTextTailStart(root) {
const children = Array.from(root.childNodes);
const tailIdx = children.findIndex(node =>
- this.isQuotePrefixedTextNode(node)
+ this.isQuoteTailStartTextNode(node)
);
if (tailIdx === -1) return -1;
let start = tailIdx;
@@ -152,14 +153,18 @@ export class EmailQuoteExtractor {
return start;
}
- static isQuotePrefixedTextNode(node) {
+ static isQuoteTailStartTextNode(node) {
if (node.nodeType !== Node.TEXT_NODE) return false;
const text = node.textContent;
if (!text.trim()) return false;
- return text
- .split('\n')
- .filter(line => line.trim() !== '')
- .every(line => line.trim().startsWith('>'));
+ const lines = text.split('\n').filter(line => line.trim() !== '');
+ if (lines.length > 0 && lines.every(l => l.trim().startsWith('>'))) {
+ return true;
+ }
+ return (
+ HEADER_PATTERNS.some(p => p.test(text)) ||
+ HARD_HEADER_PATTERNS.some(p => p.test(text))
+ );
}
static isNeutralNode(node) {
diff --git a/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js b/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js
index f3d338da9..963c83263 100644
--- a/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js
+++ b/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js
@@ -1,4 +1,4 @@
-import { describe, it, expect } from 'vitest';
+import { describe, expect, it } from 'vitest';
import { EmailQuoteExtractor } from '../emailQuoteExtractor.js';
const SAMPLE_EMAIL_HTML = `
@@ -237,6 +237,38 @@ describe('EmailQuoteExtractor', () => {
expect(cleaned).toContain('Reply text');
expect(cleaned).not.toContain('From: Sam');
});
+
+ // Header markers at the TOP LEVEL — text +
shape with no block
+ // wrapper. The marker's nearest block ancestor is root itself.
+ it('detects top-level "On … wrote:" header (no wrapper)', () => {
+ const html =
+ 'Reply text
On Tue, Pat wrote:
Original line 1
Original line 2';
+ expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true);
+ const c = document.createElement('div');
+ c.innerHTML = EmailQuoteExtractor.extractQuotes(html);
+ expect(c.textContent).toContain('Reply text');
+ expect(c.textContent).not.toContain('On Tue, Pat wrote');
+ expect(c.textContent).not.toContain('Original line 1');
+ });
+
+ it('detects top-level "From:/Sent:" header (no wrapper)', () => {
+ const html =
+ 'Reply text
From: Sam
Sent: Wednesday
Original body';
+ expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true);
+ const c = document.createElement('div');
+ c.innerHTML = EmailQuoteExtractor.extractQuotes(html);
+ expect(c.textContent).toContain('Reply text');
+ expect(c.textContent).not.toContain('From: Sam');
+ });
+
+ it('detects top-level "-----Original Message-----" (no wrapper)', () => {
+ const html = 'Reply
-----Original Message-----
From: Sam
Body';
+ expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true);
+ const c = document.createElement('div');
+ c.innerHTML = EmailQuoteExtractor.extractQuotes(html);
+ expect(c.textContent).toContain('Reply');
+ expect(c.textContent).not.toContain('Original Message');
+ });
});
it('strips RFC-style `>` quoted lines from a plain-text only body (iPhone Mail)', () => {