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

-
-> On Mon, Apr 6, 2026, Shruthi wrote:
-> Inline plain-text quote line
-> that I made
-
-

The original HTML quoted email

-
`; - -// Real-world body: Gmail web reply (matches the structure the Chatwoot -// fixture in components-next/message/fixtures/emailConversation.js produces). -const GMAIL_REAL_WORLD_REPLY = `

Dear Sam,

Thank you for the quotation. Could you share images?

Best,
Alex


On Wed, 4 Dec 2024 at 17:15, Sam from CottonMart <sam@cottonmart.test> wrote:

Dear Alex,

Thank you for your inquiry.

Best regards,
Sam

`; - -// Outlook web/desktop: header div carries id="divRplyFwdMsg" (no quote class) -// and the original is wrapped in a bare `
` after it. -const OUTLOOK_REAL_WORLD_REPLY = `

Hi team,

See attached the latest proposal.

Regards,
Pat

From: Sam <sam@example.test>
Sent: Wednesday, December 4, 2024 5:15 PM
To: Pat <pat@example.test>
Subject: Quotation

Hi Pat,

Quotation attached.

Sam

`; - -// Yahoo Mail: wraps the previous email in
. -const YAHOO_MAIL_REPLY = `
My reply text here.
Thanks,
Pat
On Wednesday, December 4, 2024, 5:15 PM, Sam <sam@example.test> wrote:
Original Yahoo-quoted message body.
`; - -// Thunderbird: the attribution paragraph carries class="moz-cite-prefix" and -// the original lives in
. -const THUNDERBIRD_REPLY = `

My reply.

Thanks,
Pat

On 4/12/24 17:15, Sam wrote:

Original Thunderbird-quoted message body.

`; - -// 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 = `
FYI — see the original below.

---------- 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.
`; - -// Outlook plain text "----- Original Message -----" header. -const OUTLOOK_ORIGINAL_MESSAGE = `

Quick reply.

Thanks

-----Original Message-----
From: Sam <sam@example.test>
Sent: Wednesday, December 4, 2024 5:15 PM
To: Pat <pat@example.test>
Subject: Quotation

Original 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,
Pat

`; - -// iPhone Mail / `text/plain` reply, after sanitizeTextForRender() has converted -// `\n` → `
` 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 = - '

My reply.

-----Original Message-----
From: Sam
Sent: ...

Old body line 1

'; - 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 = - '

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, 2024

Old 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: foo

Old 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: foo

' + - '

Hi 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.
' + - '

' + - '
On Apr 6, 2026, at 11:26 AM, Sam <sam@example.test> wrote:
' + - '
Hi Pat,
Locking the Friday slot.
Sam
' + - '
'; - 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 = - '

Confirming I received this — will review tomorrow.

' + - '

Thanks,
Pat

' + - '

From: Sam <sam@example.test>
Sent: Wednesday, December 4, 2024 5:15 PM
To: Pat <pat@example.test>
Subject: Quotation

' + - '

Hi Pat,
Quotation attached.
Sam

'; - 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 `
` 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 = ` -
-

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 escalation

-
-

Sam, Acme Corp is threatening to churn over recent latency issues.

-

Maya

-
- `; - 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 = - '

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

-----Original Message-----
From: Sam
Old body
'; - 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 mid-paragraph', () => { - const html = ` -

Hey Sam,

-

The bug ticket says the markdown for -----Original Message----- should render correctly.

-
// strip on its own line only
-

Tested 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


On Wed, 4 Dec 2024 at 17:15, Sam wrote:

Dear Alex,

Thank you for your inquiry.

`; + 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 = `

Hi team,

Regards,
Pat

From: Sam
Sent: Wed Dec 4, 2024
To: Pat
Subject: Quotation

Hi Pat,

Quotation attached.

`; + 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 = `
My reply text here.
On Wed, Dec 4, 2024, Sam wrote:
Original Yahoo-quoted message body.
`; + 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 AND
', () => { + const html = `

My reply.

On 4/12/24 17:15, Sam wrote:

Original Thunderbird-quoted message body.

`; + 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 = `
FYI — see the original below.
---------- Forwarded message ---------
From: Sam
Date: Wed, 4 Dec 2024
Subject: Quotation
To: Pat
Original forwarded body content.
`; + 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 = `

Quick reply.

-----Original Message-----
From: Sam
Sent: Wed Dec 4, 2024
To: Pat
Subject: Quotation

Original 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

-----Original Message-----
From: Sam
Old body
'; + 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 = + '

The 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 inside mid-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 = + '

My reply.

-----Original Message-----
From: Sam
Sent: ...

Old body

'; + 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 = + '

Reply text.

From: Sam
Sent: Wed Dec 4, 2024

Old 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: foo

Old 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 = ` +
+

Pat — please look into this.

+

Thanks,
Sam

+
+

From: Maya
Sent: Wed, 4 Dec
To: Sam
Subject: Customer escalation

+
+

Sam, Acme Corp is threatening to churn.

+
+ `; + 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 = + '

Confirming I received this — will review tomorrow.

' + + '

Thanks,
Pat

' + + '

From: Sam
Sent: Wed Dec 4, 2024
To: Pat
Subject: Quotation

' + + '

Hi Pat,
Quotation attached.
Sam

'; + 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 = + '

From: Sam
Sent: Wed Dec 4, 2024
To: Pat
Subject: foo

' + + '

Hi 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.
' + + '
' + + '
On Apr 6, 2026, at 11:26 AM, Sam wrote:
' + + '
Hi Pat,
Locking the Friday slot.
' + + '
'; + 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
', () => { + 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
+
+

The original HTML quoted email

+
`; + 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 text

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,
Pat

'; + 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); + }); + }); });