import { EmailQuoteExtractor } from '../emailQuoteExtractor.js'; const SAMPLE_EMAIL_HTML = `

method

On Mon, Sep 29, 2025 at 5:18 PM John shivam@chatwoot.com wrote:

Hi

On Mon, Sep 29, 2025 at 5:17 PM Shivam Mishra shivam@chatwoot.com wrote:

Yes, it is.

On Mon, Sep 29, 2025 at 5:16 PM John from Shaneforwoot < shaneforwoot@gmail.com> wrote:

Hey

On Mon, Sep 29, 2025 at 4:59 PM John shivam@chatwoot.com wrote:

This is another quoted quoted text reply

This is nice

On Mon, Sep 29, 2025 at 4:21 PM John from Shaneforwoot < > shaneforwoot@gmail.com> wrote:

Hey there, this is a reply from Chatwoot, notice the quoted text

Hey there

This is an email text, enjoy reading this

-- Shivam Mishra, Chatwoot

`; const EMAIL_WITH_SIGNATURE = `

Latest reply here.

Thanks,

Jane Doe

On Mon, Sep 22, Someone wrote:

Previous reply content

`; const EMAIL_WITH_FOLLOW_UP_CONTENT = `

Inline quote that should stay

Internal note follows

Regards,

`; // 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', () => { it('removes blockquote-based quotes from the email body', () => { const c = cleaned(SAMPLE_EMAIL_HTML); expect(c.querySelectorAll('blockquote').length).toBe(0); expect(c.textContent?.trim()).toBe('method'); expect(c.textContent).not.toContain('On Mon, Sep 29, 2025 at 5:18 PM'); }); it('keeps blockquote fallback when it is not the last top-level element', () => { const c = cleaned(EMAIL_WITH_FOLLOW_UP_CONTENT); expect(c.querySelector('blockquote')).not.toBeNull(); expect(c.lastElementChild?.tagName).toBe('P'); }); it('detects quote indicators in nested blockquotes', () => { expect(EmailQuoteExtractor.hasQuotes(SAMPLE_EMAIL_HTML)).toBe(true); }); it('does not flag blockquotes that are followed by other elements', () => { expect(EmailQuoteExtractor.hasQuotes(EMAIL_WITH_FOLLOW_UP_CONTENT)).toBe( false ); }); it('returns false when no quote indicators are present', () => { expect(EmailQuoteExtractor.hasQuotes('

Plain content

')).toBe(false); }); it('removes trailing blockquotes while preserving trailing signatures', () => { const c = cleaned(EMAIL_WITH_SIGNATURE); expect(c.querySelector('blockquote')).toBeNull(); expect(c.textContent).toContain('Thanks,'); expect(c.textContent).toContain('Jane Doe'); }); it('detects quotes for trailing blockquotes even when signatures follow text', () => { expect(EmailQuoteExtractor.hasQuotes(EMAIL_WITH_SIGNATURE)).toBe(true); }); describe('HTML sanitization', () => { it('removes onerror handlers from img tags in extractQuotes', () => { const maliciousHtml = '

Hello

'; const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml); expect(cleanedHtml).not.toContain('onerror'); expect(cleanedHtml).toContain('

Hello

'); }); it('removes onerror handlers from img tags in hasQuotes', () => { const maliciousHtml = '

Hello

'; // Should not throw and should safely check for quotes const result = EmailQuoteExtractor.hasQuotes(maliciousHtml); expect(result).toBe(false); }); it('removes script tags in extractQuotes', () => { const maliciousHtml = '

Content

More

'; const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml); expect(cleanedHtml).not.toContain('Content

'); expect(cleanedHtml).toContain('

More

'); }); it('removes onclick handlers in extractQuotes', () => { const maliciousHtml = '

Click me

'; const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml); expect(cleanedHtml).not.toContain('onclick'); expect(cleanedHtml).toContain('Click me'); }); it('removes javascript: URLs in extractQuotes', () => { const maliciousHtml = 'Link'; const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml); // eslint-disable-next-line no-script-url expect(cleanedHtml).not.toContain('javascript:'); expect(cleanedHtml).toContain('Link'); }); it('removes encoded payloads with event handlers in extractQuotes', () => { const maliciousHtml = ''; const cleanedHtml = EmailQuoteExtractor.extractQuotes(maliciousHtml); expect(cleanedHtml).not.toContain('onerror'); 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); }); it('cuts wrapper-level siblings when the marker is in a nested child', () => { // Outlook nested shape: marker sits in an inner block, but the actual // forwarded body lives in later siblings of the outer wrapper. Strategy // 2 selects the outer match so the wrapper's later siblings strip too. 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('From: Sam'); expect(c.textContent).not.toContain('Old body'); }); it('treats hard markers as absolute boundaries — sibling tail is always quoted body', () => { // Hard markers (`-----Original Message-----` etc.) are explicit, // anchored boundaries: by convention everything after is the original. // Strategy 4 (soft headers) preserves following siblings because // `From:` can appear in prose; strategy 2 deliberately does not. Locks // down the asymmetry against future "preserve bottom-post" rewrites. const html = '

-----Original Message-----

' + '

From: Sam

' + '

Old quoted body

' + '

Reply pretending to be bottom-posted

'; const c = cleaned(html); expect(c.textContent).not.toContain('Original Message'); expect(c.textContent).not.toContain('From: Sam'); expect(c.textContent).not.toContain('Old quoted body'); expect(c.textContent).not.toContain( 'Reply pretending to be bottom-posted' ); }); }); 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); }); }); });