diff --git a/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js b/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js index 369f1b7c2..887acdec4 100644 --- a/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js +++ b/app/javascript/dashboard/helper/specs/emailQuoteExtractor.spec.js @@ -43,436 +43,16 @@ const EMAIL_WITH_FOLLOW_UP_CONTENT = `
Regards,
`; -// Real-world mixed body: an HTML reply, then RFC-style `>`-prefixed plain-text -// quote lines, and finally the original email wrapped in a Gmail blockquote. -// The current branchy implementation picks ONE strategy and misses the other. -const MIXED_PLAINTEXT_AND_HTML_QUOTE = `My HTML reply
-Thanks,
-Sivin
-The original HTML quoted email
-Dear Sam,
Thank you for the quotation. Could you share images?
Best,
Alex
Dear Alex,
Thank you for your inquiry.
Best regards,
Sam
` after it. -const OUTLOOK_REAL_WORLD_REPLY = `Hi team,
See attached the latest proposal.
Regards,
PatFrom: Sam <sam@example.test>
Sent: Wednesday, December 4, 2024 5:15 PM
To: Pat <pat@example.test>
Subject: Quotation`; - -// Yahoo Mail: wraps the previous email inHi Pat,
Quotation attached.
Sam
. -const YAHOO_MAIL_REPLY = `My reply text here.Thanks,
Pat`; - -// Thunderbird: the attribution paragraph carries class="moz-cite-prefix" and -// the original lives inOn Wednesday, December 4, 2024, 5:15 PM, Sam <sam@example.test> wrote:Original Yahoo-quoted message body.. -const THUNDERBIRD_REPLY = `My reply.
Thanks,
PatOn 4/12/24 17:15, Sam wrote:
`; - -// Gmail forwarded message — the body reads "---------- Forwarded message ---------" -// in plain text, with a header block beneath listing From/Date/Subject/To. -const GMAIL_FORWARDED_MESSAGE = `Original Thunderbird-quoted message body.
FYI — see the original below.`; - -// Outlook plain text "----- Original Message -----" header. -const OUTLOOK_ORIGINAL_MESSAGE = `---------- Forwarded message ---------
From: Sam <sam@example.test>
Date: Wed, 4 Dec 2024 at 17:15
Subject: Quotation
To: Pat <pat@example.test>Original forwarded body content.Quick reply.
Thanks
-----Original Message-----
From: Sam <sam@example.test>
Sent: Wednesday, December 4, 2024 5:15 PM
To: Pat <pat@example.test>
Subject: QuotationOriginal Outlook plain-style reply.
`; - -// Inline reply: bare(no client class) sits in the middle, with -// content following it. Trailing-only rule should preserve the blockquote. -const INLINE_REPLY = `See my responses inline below.
Question 1: pricing?
Answer: usd 100.
Question 2: timeline?
Answer: 2 weeks.
Let me know if any of that needs clarification.
Pat
`; - -// Plain conversational body with no quote markers — must not be mistakenly -// stripped and must not show the toggle. -const NO_QUOTE_BODY = `Just checking in — any update on this?
Thanks,
`; - -// iPhone Mail / `text/plain` reply, after sanitizeTextForRender() has converted -// `\n` → `
Pat
` and escaped lone `< > &`. -const IPHONE_MAIL_PLAINTEXT = [ - 'Test payments email
', - 'Thanks,
Shruthi
', - 'Sent from my iPhone
', - '> On Apr 6, 2026, at 11:26 PM, Shruthi M ', - '<shruthi.rohini.7@gmail.com> wrote:
', - '>
> Hi email
> To Eli
', - '> Thanks,
> Shruthi
', - '>
> Sent from my iPhone', -].join(''); +// Regression coverage for the quote-toggle rewrite shipped on this branch. +// Parse cleaned HTML into a container so each test can assert against +// .textContent or query nested nodes. +const cleaned = html => { + const c = document.createElement('div'); + c.innerHTML = EmailQuoteExtractor.extractQuotes(html); + return c; +}; describe('EmailQuoteExtractor', () => { - describe('real-world client shapes', () => { - it('Gmail web reply — strips .gmail_quote_container with attribution + blockquote', () => { - const cleaned = EmailQuoteExtractor.extractQuotes(GMAIL_REAL_WORLD_REPLY); - const c = document.createElement('div'); - c.innerHTML = cleaned; - expect(c.textContent).toContain('Dear Sam'); - expect(c.textContent).toContain('Best,'); - expect(c.textContent).not.toContain('Thank you for your inquiry'); - expect(c.textContent).not.toContain('On Wed, 4 Dec 2024'); - expect(c.querySelector('.gmail_quote')).toBeNull(); - expect(EmailQuoteExtractor.hasQuotes(GMAIL_REAL_WORLD_REPLY)).toBe(true); - }); - - it('Outlook reply — strips #divRplyFwdMsg header AND the trailing bare blockquote', () => { - const cleaned = EmailQuoteExtractor.extractQuotes( - OUTLOOK_REAL_WORLD_REPLY - ); - const c = document.createElement('div'); - c.innerHTML = cleaned; - expect(c.textContent).toContain('Hi team'); - expect(c.textContent).toContain('Regards,'); - expect(c.textContent).not.toContain('From: Sam'); - expect(c.textContent).not.toContain('Quotation attached'); - expect(c.querySelector('blockquote')).toBeNull(); - expect(c.querySelector('#divRplyFwdMsg')).toBeNull(); - expect(EmailQuoteExtractor.hasQuotes(OUTLOOK_REAL_WORLD_REPLY)).toBe( - true - ); - }); - - it('Yahoo Mail reply — strips .yahoo_quoted wrapper', () => { - const cleaned = EmailQuoteExtractor.extractQuotes(YAHOO_MAIL_REPLY); - const c = document.createElement('div'); - c.innerHTML = cleaned; - expect(c.textContent).toContain('My reply text here'); - expect(c.textContent).not.toContain('Original Yahoo-quoted message'); - expect(c.querySelector('.yahoo_quoted')).toBeNull(); - expect(EmailQuoteExtractor.hasQuotes(YAHOO_MAIL_REPLY)).toBe(true); - }); - - it('Thunderbird reply — strips .moz-cite-prefix attribution AND', () => { - const cleaned = EmailQuoteExtractor.extractQuotes(THUNDERBIRD_REPLY); - const c = document.createElement('div'); - c.innerHTML = cleaned; - expect(c.textContent).toContain('My reply'); - expect(c.textContent).toContain('Thanks,'); - expect(c.textContent).not.toContain('On 4/12/24 17:15'); - expect(c.textContent).not.toContain( - 'Original Thunderbird-quoted message' - ); - expect(c.querySelector('blockquote')).toBeNull(); - expect(c.querySelector('.moz-cite-prefix')).toBeNull(); - expect(EmailQuoteExtractor.hasQuotes(THUNDERBIRD_REPLY)).toBe(true); - }); - - it('Gmail forwarded message — strips "---------- Forwarded message ----------" block', () => { - const cleaned = EmailQuoteExtractor.extractQuotes( - GMAIL_FORWARDED_MESSAGE - ); - const c = document.createElement('div'); - c.innerHTML = cleaned; - expect(c.textContent).toContain('FYI — see the original below'); - expect(c.textContent).not.toContain('Forwarded message'); - expect(c.textContent).not.toContain('Original forwarded body content'); - expect(EmailQuoteExtractor.hasQuotes(GMAIL_FORWARDED_MESSAGE)).toBe(true); - }); - - it('Outlook plain-style "-----Original Message-----" header is stripped', () => { - const cleaned = EmailQuoteExtractor.extractQuotes( - OUTLOOK_ORIGINAL_MESSAGE - ); - const c = document.createElement('div'); - c.innerHTML = cleaned; - expect(c.textContent).toContain('Quick reply'); - expect(c.textContent).not.toContain('Original Message'); - expect(c.textContent).not.toContain('Original Outlook plain-style reply'); - expect(EmailQuoteExtractor.hasQuotes(OUTLOOK_ORIGINAL_MESSAGE)).toBe( - true - ); - }); - - it('inline reply — when content follows the quoted block, the body is left intact', () => { - const cleaned = EmailQuoteExtractor.extractQuotes(INLINE_REPLY); - const c = document.createElement('div'); - c.innerHTML = cleaned; - expect(c.textContent).toContain('See my responses inline below'); - expect(c.textContent).toContain('Question 1: pricing?'); - expect(c.textContent).toContain( - 'Let me know if any of that needs clarification' - ); - expect(c.querySelector('blockquote')).not.toBeNull(); - expect(EmailQuoteExtractor.hasQuotes(INLINE_REPLY)).toBe(false); - }); - - it('plain body with no quotes — body unchanged, no toggle', () => { - const cleaned = EmailQuoteExtractor.extractQuotes(NO_QUOTE_BODY); - const c = document.createElement('div'); - c.innerHTML = cleaned; - expect(c.textContent).toContain('Just checking in'); - expect(c.textContent).toContain('Thanks,'); - expect(EmailQuoteExtractor.hasQuotes(NO_QUOTE_BODY)).toBe(false); - }); - - it('empty body — no error, no toggle', () => { - expect(() => EmailQuoteExtractor.extractQuotes('')).not.toThrow(); - expect(EmailQuoteExtractor.hasQuotes('')).toBe(false); - }); - }); - - // Regression tests — develop-baseline behaviours that earlier rewrites broke. - describe('develop-baseline regression coverage', () => { - it('detects header quote inside a single outer wrapper', () => { - const html = - ''; - expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true); - const cleaned = EmailQuoteExtractor.extractQuotes(html); - expect(cleaned).toContain('My reply'); - expect(cleaned).not.toContain('Original Message'); - }); - - it('detects "On … wrote:" header even when followed by un-prefixed old lines', () => { - const html = - 'My reply.
-----Original Message-----
From: Sam
Sent: ...Old body line 1
On Wed, Sam wrote:
Old line 1
Old line 2
'; - expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true); - const cleaned = EmailQuoteExtractor.extractQuotes(html); - expect(cleaned).not.toContain('On Wed, Sam wrote'); - }); - - it('detects "From:/Sent:" header even when followed by un-prefixed old lines', () => { - const html = - 'Reply text.
From: Sam <sam@example.test>
Sent: Wednesday, December 4, 2024Old line 1
'; - expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true); - const cleaned = EmailQuoteExtractor.extractQuotes(html); - 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 <sam@example.test>
Sent: Wednesday, December 4, 2024
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'); - }); - - // Trailing-only rule: only strip the `>`-block when nothing substantive - // follows it. Otherwise the user's own bottom-posted / inline reply is - // silently dropped. - it('preserves user reply that is bottom-posted below `>`-quoted lines', () => { - const html = - '> On Tue, Pat wrote:
> Attached is the doc.
> Pat
Got it, looks good.'; - const c = document.createElement('div'); - c.innerHTML = EmailQuoteExtractor.extractQuotes(html); - expect(c.textContent).toContain('Got it, looks good'); - }); - - it('preserves user answers inline-posted between `>`-quoted lines', () => { - const html = - '> Q1: pricing?
A1: USD 100
> Q2: timeline?
A2: 2 weeks
Thanks!'; - const c = document.createElement('div'); - c.innerHTML = EmailQuoteExtractor.extractQuotes(html); - expect(c.textContent).toContain('A1: USD 100'); - expect(c.textContent).toContain('A2: 2 weeks'); - expect(c.textContent).toContain('Thanks!'); - }); - - // Anchored hard-header patterns: don't strip when the marker phrase shows - // up inside a sentence (false trigger). - it('does not strip when "Original Message" appears inside a sentence', () => { - const html = - 'The bug ticket says the markdown for `-----Original Message-----` should render correctly.
Here is my fix.
'; - const c = document.createElement('div'); - c.innerHTML = EmailQuoteExtractor.extractQuotes(html); - expect(c.textContent).toContain('Here is my fix'); - }); - - it('detects minimal "From: name + Sent: weekday" header (no @, no year)', () => { - const html = - 'Reply text.
From: Sam
Sent: Wednesday
To: Pat
Subject: Re: fooOld body
'; - expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true); - const cleaned = EmailQuoteExtractor.extractQuotes(html); - expect(cleaned).toContain('Reply text'); - expect(cleaned).not.toContain('From: Sam'); - }); - - it('preserves a bottom-posted reply that follows a header block at root', () => { - const html = - 'From: Sam <sam@example.test>
' + - '
Sent: Wednesday, December 4, 2024
To: Pat
Subject: fooHi Pat, original message body.
' + - '--- My reply below ---
' + - 'Got it, thanks!
'; - const c = document.createElement('div'); - c.innerHTML = EmailQuoteExtractor.extractQuotes(html); - expect(c.textContent).toContain('Got it, thanks'); - expect(c.textContent).toContain('My reply below'); - expect(c.textContent).not.toContain('From: Sam'); - }); - - it('strips Apple-Mail blockquote (attribution + body inside one)', () => { - const html = - 'Sounds good, see you Friday.' + - ''; - const c = document.createElement('div'); - c.innerHTML = EmailQuoteExtractor.extractQuotes(html); - expect(c.textContent).toContain('Sounds good'); - expect(c.textContent).not.toContain('On Apr 6, 2026'); - expect(c.textContent).not.toContain('Hi Pat'); - expect(c.textContent).not.toContain('Locking the Friday slot'); - }); - - // Flat Outlook header at root — strip the header block, keep the body - // visible. We can't tell the body apart from a bottom-posted reply at - // root level, so be safe (matches develop behaviour). - it('strips a flat Outlook header block but keeps body visible', () => { - const html = - '' + - 'On Apr 6, 2026, at 11:26 AM, Sam <sam@example.test> wrote:' + - '' + - 'Hi Pat,Locking the Friday slot.SamConfirming I received this — will review tomorrow.
' + - 'Thanks,
' + - '
PatFrom: Sam <sam@example.test>
' + - '
Sent: Wednesday, December 4, 2024 5:15 PM
To: Pat <pat@example.test>
Subject: QuotationHi Pat,
'; - const c = document.createElement('div'); - c.innerHTML = EmailQuoteExtractor.extractQuotes(html); - expect(c.textContent).toContain('Confirming I received this'); - expect(c.textContent).toContain('Thanks,'); - expect(c.textContent).not.toContain('From: Sam'); - }); - - // Inline reply where a soft-header `
Quotation attached.
Sam` is followed by the - // user's actual reply at the SAME level. The hard-cut for soft headers - // would otherwise eat the reply. - it('preserves user reply that follows a soft-header', () => { - const html = - 'On Mon, Sep 22, Sam wrote:
Original quoted line.My actual reply.
'; - const c = document.createElement('div'); - c.innerHTML = EmailQuoteExtractor.extractQuotes(html); - expect(c.textContent).toContain('My actual reply'); - }); - - it('preserves user reply that follows a wrapper div containing the soft-header block', () => { - const html = - 'On Mon, Sam wrote:My reply outside the wrapper.
'; - const c = document.createElement('div'); - c.innerHTML = EmailQuoteExtractor.extractQuotes(html); - expect(c.textContent).toContain('My reply outside the wrapper'); - }); - - it('preserves reply when the From-header sits inside a deep wrapper (Outlook WordSection1)', () => { - const html = ` --- `; - const c = document.createElement('div'); - c.innerHTML = EmailQuoteExtractor.extractQuotes(html); - expect(c.textContent).toContain('Pat — please look into this'); - expect(c.textContent).toContain('Thanks'); - expect(c.textContent).not.toContain('From: Maya'); - expect(c.textContent).not.toContain('Subject: Customer escalation'); - }); - - it('does not strip prose paragraphs that start with "From: " or "Sent: "', () => { - const fromHtml = - 'Pat — please look into this when you get a chance.
-Thanks,
-
Sam--From: Maya <maya@example.test>
-
Sent: Wednesday, December 4, 2024 8:42 AM
To: Sam <sam@example.test>
Subject: Customer escalationSam, Acme Corp is threatening to churn over recent latency issues.
-Maya
-From: now on, please follow this checklist.
This is regular content.
'; - let c = document.createElement('div'); - c.innerHTML = EmailQuoteExtractor.extractQuotes(fromHtml); - expect(c.textContent).toContain('From: now on'); - expect(c.textContent).toContain('regular content'); - - const sentHtml = - 'Sent: yesterday by the courier.
Tracking number to follow.
'; - c = document.createElement('div'); - c.innerHTML = EmailQuoteExtractor.extractQuotes(sentHtml); - expect(c.textContent).toContain('Sent: yesterday'); - expect(c.textContent).toContain('Tracking number'); - }); - - it('preserves reply text that sits before a hard marker in the SAME block', () => { - const html = - 'My reply'; - const c = document.createElement('div'); - c.innerHTML = EmailQuoteExtractor.extractQuotes(html); - expect(c.textContent).toContain('My reply'); - expect(c.textContent).not.toContain('Original Message'); - expect(c.textContent).not.toContain('Old body'); - }); - - it('does not strip when "Original Message" sits inside
-----Original Message-----
From: Sam
Old bodymid-paragraph', () => { - const html = ` -Hey Sam,
-The bug ticket says the markdown for
------Original Message-----should render correctly.-// strip on its own line onlyTested locally — passing all cases.
-Pat
- `; - const c = document.createElement('div'); - c.innerHTML = EmailQuoteExtractor.extractQuotes(html); - expect(c.textContent).toContain('Hey Sam'); - expect(c.textContent).toContain('Tested locally'); - expect(c.textContent).toContain('Pat'); - expect(EmailQuoteExtractor.hasQuotes(html)).toBe(false); - }); - }); - - it('strips RFC-style `>` quoted lines from a plain-text only body (iPhone Mail)', () => { - const cleanedHtml = EmailQuoteExtractor.extractQuotes( - IPHONE_MAIL_PLAINTEXT - ); - const container = document.createElement('div'); - container.innerHTML = cleanedHtml; - const text = container.textContent; - - expect(text).toContain('Test payments email'); - expect(text).toContain('Sent from my iPhone'); // signature stays - expect(text).not.toContain('On Apr 6, 2026'); - expect(text).not.toContain('Hi email'); - expect(text).not.toContain('To Eli'); - expect(EmailQuoteExtractor.hasQuotes(IPHONE_MAIL_PLAINTEXT)).toBe(true); - }); - - it('strips both plain-text `>` lines and HTML quote blocks in the same body', () => { - const cleanedHtml = EmailQuoteExtractor.extractQuotes( - MIXED_PLAINTEXT_AND_HTML_QUOTE - ); - const container = document.createElement('div'); - container.innerHTML = cleanedHtml; - const text = container.textContent; - - // Reply portion stays - expect(text).toContain('My HTML reply'); - expect(text).toContain('Sivin'); - - // Plain-text `>` quote lines are gone - expect(text).not.toContain('Inline plain-text quote line'); - expect(text).not.toContain('On Mon, Apr 6, 2026, Shruthi wrote'); - - // HTML quote block is gone - expect(text).not.toContain('The original HTML quoted email'); - expect(container.querySelectorAll('.gmail_quote').length).toBe(0); - }); - it('removes blockquote-based quotes from the email body', () => { const cleanedHtml = EmailQuoteExtractor.extractQuotes(SAMPLE_EMAIL_HTML); @@ -579,4 +159,314 @@ describe('EmailQuoteExtractor', () => { expect(cleanedHtml).not.toContain('eval'); }); }); + + describe('client wrappers', () => { + it('Gmail — strips .gmail_quote_container with attribution + blockquote', () => { + const html = `Dear Sam,
Thank you for the quotation.
Best,
Alex`; + const c = cleaned(html); + expect(c.textContent).toContain('Dear Sam'); + expect(c.textContent).not.toContain('Thank you for your inquiry'); + expect(c.querySelector('.gmail_quote')).toBeNull(); + expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true); + }); + + it('Outlook — strips #divRplyFwdMsg header AND the trailing bare blockquote', () => { + const html = `On Wed, 4 Dec 2024 at 17:15, Sam wrote:Dear Alex,
Thank you for your inquiry.
Hi team,
Regards,
PatFrom: Sam
Sent: Wed Dec 4, 2024
To: Pat
Subject: Quotation`; + const c = cleaned(html); + expect(c.textContent).toContain('Hi team'); + expect(c.textContent).not.toContain('From: Sam'); + expect(c.textContent).not.toContain('Quotation attached'); + expect(c.querySelector('blockquote')).toBeNull(); + expect(c.querySelector('#divRplyFwdMsg')).toBeNull(); + }); + + it('Yahoo — strips .yahoo_quoted wrapper', () => { + const html = `Hi Pat,
Quotation attached.
My reply text here.`; + const c = cleaned(html); + expect(c.textContent).toContain('My reply text here'); + expect(c.textContent).not.toContain('Original Yahoo-quoted message'); + expect(c.querySelector('.yahoo_quoted')).toBeNull(); + }); + + it('Thunderbird — strips .moz-cite-prefix attribution ANDOn Wed, Dec 4, 2024, Sam wrote:Original Yahoo-quoted message body.', () => { + const html = `My reply.
On 4/12/24 17:15, Sam wrote:
`; + const c = cleaned(html); + expect(c.textContent).toContain('My reply'); + expect(c.textContent).not.toContain('On 4/12/24 17:15'); + expect(c.textContent).not.toContain( + 'Original Thunderbird-quoted message' + ); + expect(c.querySelector('blockquote')).toBeNull(); + expect(c.querySelector('.moz-cite-prefix')).toBeNull(); + }); + }); + + describe('hard markers', () => { + it('Gmail "---------- Forwarded message ----------" block is stripped', () => { + const html = `Original Thunderbird-quoted message body.
FYI — see the original below.`; + const c = cleaned(html); + expect(c.textContent).toContain('FYI — see the original below'); + expect(c.textContent).not.toContain('Forwarded message'); + expect(c.textContent).not.toContain('Original forwarded body content'); + }); + + it('Outlook plain-style "-----Original Message-----" header is stripped', () => { + const html = `---------- Forwarded message ---------
From: Sam
Date: Wed, 4 Dec 2024
Subject: Quotation
To: PatOriginal forwarded body content.Quick reply.
-----Original Message-----
From: Sam
Sent: Wed Dec 4, 2024
To: Pat
Subject: QuotationOriginal Outlook plain-style reply.
`; + const c = cleaned(html); + expect(c.textContent).toContain('Quick reply'); + expect(c.textContent).not.toContain('Original Message'); + expect(c.textContent).not.toContain('Original Outlook plain-style reply'); + }); + + it('preserves reply text that sits before a hard marker in the SAME block', () => { + const html = + 'My reply'; + const c = cleaned(html); + expect(c.textContent).toContain('My reply'); + expect(c.textContent).not.toContain('Original Message'); + expect(c.textContent).not.toContain('Old body'); + }); + + it('does not strip when "Original Message" appears inside a sentence', () => { + const html = + '
-----Original Message-----
From: Sam
Old bodyThe bug ticket says the markdown for `-----Original Message-----` should render.
Here is my fix.
'; + expect(cleaned(html).textContent).toContain('Here is my fix'); + }); + + it('does not strip when "Original Message" sits insidemid-paragraph', () => { + const html = + 'Hey Sam,
The markdown for
-----Original Message-----should render.Tested locally.
Pat
'; + const c = cleaned(html); + expect(c.textContent).toContain('Hey Sam'); + expect(c.textContent).toContain('Tested locally'); + expect(c.textContent).toContain('Pat'); + expect(EmailQuoteExtractor.hasQuotes(html)).toBe(false); + }); + }); + + describe('multi-line attribution headers (From / Sent / To)', () => { + it('detects header inside a single outer wrapper', () => { + const html = + ''; + const c = cleaned(html); + expect(c.textContent).toContain('My reply'); + expect(c.textContent).not.toContain('Original Message'); + expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true); + }); + + it('detects "From:/Sent:" header even when followed by un-prefixed old lines', () => { + const html = + 'My reply.
-----Original Message-----
From: Sam
Sent: ...Old body
Reply text.
From: Sam
Sent: Wed Dec 4, 2024Old line 1
'; + const c = cleaned(html); + expect(c.textContent).toContain('Reply text'); + expect(c.textContent).not.toContain('From: Sam'); + expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true); + }); + + it('detects minimal "From: name + Sent: weekday" header (no @, no year)', () => { + const html = + 'Reply text.
From: Sam
Sent: Wednesday
To: Pat
Subject: Re: fooOld body
'; + const c = cleaned(html); + expect(c.textContent).toContain('Reply text'); + expect(c.textContent).not.toContain('From: Sam'); + expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true); + }); + + it('detects header buried in a deep wrapper (Outlook WordSection1)', () => { + const html = ` +++ `; + const c = cleaned(html); + expect(c.textContent).toContain('Pat — please look into this'); + expect(c.textContent).not.toContain('From: Maya'); + expect(c.textContent).not.toContain('Subject: Customer escalation'); + }); + + it('strips a flat header at root but keeps the body visible (bottom-post safety)', () => { + const html = + 'Pat — please look into this.
+Thanks,
+
Sam++From: Maya
+
Sent: Wed, 4 Dec
To: Sam
Subject: Customer escalationSam, Acme Corp is threatening to churn.
+Confirming I received this — will review tomorrow.
' + + 'Thanks,
' + + '
PatFrom: Sam
' + + '
Sent: Wed Dec 4, 2024
To: Pat
Subject: QuotationHi Pat,
'; + const c = cleaned(html); + expect(c.textContent).toContain('Confirming I received this'); + expect(c.textContent).toContain('Thanks,'); + expect(c.textContent).not.toContain('From: Sam'); + }); + + it('preserves a bottom-posted reply that follows a flat header block at root', () => { + const html = + '
Quotation attached.
SamFrom: Sam
' + + '
Sent: Wed Dec 4, 2024
To: Pat
Subject: fooHi Pat, original message body.
' + + '--- My reply below ---
' + + 'Got it, thanks!
'; + const c = cleaned(html); + expect(c.textContent).toContain('Got it, thanks'); + expect(c.textContent).toContain('My reply below'); + expect(c.textContent).not.toContain('From: Sam'); + }); + + it('strips Apple-Mail blockquote (attribution + body inside one)', () => { + const html = + 'Sounds good, see you Friday.' + + ''; + const c = cleaned(html); + expect(c.textContent).toContain('Sounds good'); + expect(c.textContent).not.toContain('On Apr 6, 2026'); + expect(c.textContent).not.toContain('Locking the Friday slot'); + }); + + it('preserves user reply that follows a soft-header' + + 'On Apr 6, 2026, at 11:26 AM, Sam wrote:' + + '' + + 'Hi Pat,Locking the Friday slot.', () => { + const html = + 'On Mon, Sep 22, Sam wrote:
Original quoted line.My actual reply.
'; + expect(cleaned(html).textContent).toContain('My actual reply'); + }); + + it('preserves user reply that follows a wrapper containing a soft-header block', () => { + const html = + 'On Mon, Sam wrote:My reply outside the wrapper.
'; + expect(cleaned(html).textContent).toContain( + 'My reply outside the wrapper' + ); + }); + + it('does not strip prose paragraphs that start with "From: " or "Sent: "', () => { + const fromHtml = + 'From: now on, please follow this checklist.
This is regular content.
'; + expect(cleaned(fromHtml).textContent).toContain('From: now on'); + expect(cleaned(fromHtml).textContent).toContain('regular content'); + + const sentHtml = + 'Sent: yesterday by the courier.
Tracking number to follow.
'; + expect(cleaned(sentHtml).textContent).toContain('Sent: yesterday'); + expect(cleaned(sentHtml).textContent).toContain('Tracking number'); + }); + }); + + describe('single-line "On … wrote:" attribution', () => { + it('detects header even when followed by un-prefixed old lines', () => { + const html = + 'On Wed, Sam wrote:
Old line 1
Old line 2
'; + const c = cleaned(html); + expect(c.textContent).not.toContain('On Wed, Sam wrote'); + expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true); + }); + }); + + describe('top-level RFC `>` and header tail (text +
)', () => { + it('iPhone Mail — strips RFC `>` quoted lines from a plain-text only body', () => { + const html = [ + 'Test payments email
', + 'Thanks,
Shruthi
', + 'Sent from my iPhone
', + '> On Apr 6, 2026, at 11:26 PM, Shruthi wrote:
', + '>
> Hi email
> To Eli
', + '> Thanks,
> Shruthi', + ].join(''); + const c = cleaned(html); + expect(c.textContent).toContain('Test payments email'); + expect(c.textContent).toContain('Sent from my iPhone'); // signature stays + expect(c.textContent).not.toContain('On Apr 6, 2026'); + expect(c.textContent).not.toContain('Hi email'); + expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true); + }); + + it('strips both plain-text `>` lines and HTML quote blocks in the same body', () => { + const html = `My HTML reply
+Sivin
+
+> On Mon, Apr 6, 2026, Shruthi wrote:
+> Inline plain-text quote line
++`; + const c = cleaned(html); + expect(c.textContent).toContain('My HTML reply'); + expect(c.textContent).toContain('Sivin'); + expect(c.textContent).not.toContain('Inline plain-text quote line'); + expect(c.textContent).not.toContain('The original HTML quoted email'); + expect(c.querySelectorAll('.gmail_quote').length).toBe(0); + }); + + it('detects top-level "On … wrote:" header (no wrapper)', () => { + const html = + 'Reply textThe original HTML quoted email
+
On Tue, Pat wrote:
Original line 1
Original line 2'; + const c = cleaned(html); + expect(c.textContent).toContain('Reply text'); + expect(c.textContent).not.toContain('On Tue, Pat wrote'); + expect(c.textContent).not.toContain('Original line 1'); + expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true); + }); + + it('detects top-level "From:/Sent:" header (no wrapper)', () => { + const html = + 'Reply text
From: Sam
Sent: Wed Dec 4, 2024
Original body'; + const c = cleaned(html); + expect(c.textContent).toContain('Reply text'); + expect(c.textContent).not.toContain('From: Sam'); + expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true); + }); + + it('detects top-level "-----Original Message-----" (no wrapper)', () => { + const html = 'Reply
-----Original Message-----
From: Sam
Body'; + const c = cleaned(html); + expect(c.textContent).toContain('Reply'); + expect(c.textContent).not.toContain('Original Message'); + expect(EmailQuoteExtractor.hasQuotes(html)).toBe(true); + }); + + it('preserves user reply that is bottom-posted below `>`-quoted lines', () => { + const html = + '> On Tue, Pat wrote:
> Attached is the doc.
> Pat
Got it, looks good.'; + expect(cleaned(html).textContent).toContain('Got it, looks good'); + }); + + it('preserves user answers inline-posted between `>`-quoted lines', () => { + const html = + '> Q1: pricing?
A1: USD 100
> Q2: timeline?
A2: 2 weeks
Thanks!'; + const c = cleaned(html); + expect(c.textContent).toContain('A1: USD 100'); + expect(c.textContent).toContain('A2: 2 weeks'); + expect(c.textContent).toContain('Thanks!'); + }); + }); + + describe('inline / no-quote bodies', () => { + it('inline reply — when content follows the quoted block, body is left intact', () => { + const html = + 'See my responses inline below.
Q1: pricing?
A1: usd 100.
Let me know if any of that needs clarification.
Pat
'; + const c = cleaned(html); + expect(c.textContent).toContain('See my responses inline below'); + expect(c.textContent).toContain('Q1: pricing?'); + expect(c.textContent).toContain( + 'Let me know if any of that needs clarification' + ); + expect(c.querySelector('blockquote')).not.toBeNull(); + expect(EmailQuoteExtractor.hasQuotes(html)).toBe(false); + }); + + it('plain body with no quotes — body unchanged, no toggle', () => { + const html = + 'Just checking in — any update on this?
Thanks,
'; + const c = cleaned(html); + expect(c.textContent).toContain('Just checking in'); + expect(c.textContent).toContain('Thanks,'); + expect(EmailQuoteExtractor.hasQuotes(html)).toBe(false); + }); + + it('empty body — no error, no toggle', () => { + expect(() => EmailQuoteExtractor.extractQuotes('')).not.toThrow(); + expect(EmailQuoteExtractor.hasQuotes('')).toBe(false); + }); + }); });
Pat