Skip to content

Commit 3bf7a9d

Browse files
authored
fix(email): strip plain-text quoted replies whose attribution line is quote-prefixed (#324)
Some mail clients quote-prefix the "On <date>, <name> wrote:" attribution line itself (e.g. "> On ... wrote:") instead of leaving it bare before the quoted body. The plain-text branch of stripQuotedReply only matched an unprefixed attribution line, so replies in this format kept the entire original message quoted underneath instead of trimming it. Broadened the boundary regex to tolerate an optional leading ">" quote marker, in both the Gmail and Outlook connectors (duplicated logic, kept in sync). Added regression tests for the quote-prefixed case to both connectors' test suites.
1 parent 8d47457 commit 3bf7a9d

4 files changed

Lines changed: 29 additions & 4 deletions

File tree

‎connectors/google/src/mail/gmail-api.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,20 @@ describe("forwarded email body extraction", () => {
298298
expect(reply).not.toContain("old message text");
299299
});
300300

301+
it("strips plain-text quoted replies whose 'On ... wrote:' attribution is itself quote-prefixed", () => {
302+
// Some clients (observed from a real reply) emit the attribution line
303+
// WITH a leading "> " rather than leaving it unquoted, e.g.:
304+
// Reply text.
305+
// > On Jun 1, 2026, at 9:00 AM, Bob <bob@example.com> wrote:
306+
// > old message text
307+
const reply = stripQuotedReply(
308+
"My answer is yes.\r\n> On Mon, Jun 1, 2026, at 9:00 AM, Bob <bob@example.com> wrote:\r\n> old message text",
309+
"text"
310+
);
311+
expect(reply).toContain("My answer is yes.");
312+
expect(reply).not.toContain("old message text");
313+
});
314+
301315
it("strips Apple Mail quoted replies (<blockquote type=\"cite\">)", () => {
302316
// Apple Mail (Mail.app / iPhone Mail) wraps the "On <date>, <name> wrote:"
303317
// attribution and the quoted history in <blockquote type="cite">. The

‎connectors/google/src/mail/gmail-api.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -862,8 +862,11 @@ export function stripQuotedReply(
862862
for (let i = 0; i < lines.length; i++) {
863863
const line = lines[i].trim();
864864

865-
// "On [date], [name] wrote:" or "On [date], [name] <email> wrote:"
866-
if (/^On .+ wrote:\s*$/.test(line)) {
865+
// "On [date], [name] wrote:" or "On [date], [name] <email> wrote:".
866+
// Some clients quote-prefix the attribution line itself (e.g. "> On ...
867+
// wrote:") instead of leaving it bare — allow an optional leading ">"
868+
// quote marker so that case is still recognized as the reply boundary.
869+
if (/^(?:>+\s*)*On .+ wrote:\s*$/.test(line)) {
867870
// Verify next non-empty line starts with ">" (actual quoted content)
868871
const nextContentLine = lines.slice(i + 1).find((l) => l.trim() !== "");
869872
if (nextContentLine && nextContentLine.trim().startsWith(">")) {

‎connectors/outlook/src/mail/email-parsing.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,9 @@ describe("stripQuotedReply", () => {
3838
const text = "Thanks!\nOn Tue, Jun 10, 2026, Bob wrote:\n> earlier";
3939
expect(stripQuotedReply(text, "text")).toBe("Thanks!");
4040
});
41+
42+
it("cuts plain-text quotes whose 'On ... wrote:' attribution is itself quote-prefixed", () => {
43+
const text = "Thanks!\n> On Tue, Jun 10, 2026, at 9:00 AM, Bob <bob@example.com> wrote:\n> earlier";
44+
expect(stripQuotedReply(text, "text")).toBe("Thanks!");
45+
});
4146
});

‎connectors/outlook/src/mail/email-parsing.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,11 @@ export function stripQuotedReply(
136136
for (let i = 0; i < lines.length; i++) {
137137
const line = lines[i].trim();
138138

139-
// "On [date], [name] wrote:" or "On [date], [name] <email> wrote:"
140-
if (/^On .+ wrote:\s*$/.test(line)) {
139+
// "On [date], [name] wrote:" or "On [date], [name] <email> wrote:".
140+
// Some clients quote-prefix the attribution line itself (e.g. "> On ...
141+
// wrote:") instead of leaving it bare — allow an optional leading ">"
142+
// quote marker so that case is still recognized as the reply boundary.
143+
if (/^(?:>+\s*)*On .+ wrote:\s*$/.test(line)) {
141144
// Verify next non-empty line starts with ">" (actual quoted content)
142145
const nextContentLine = lines.slice(i + 1).find((l) => l.trim() !== "");
143146
if (nextContentLine && nextContentLine.trim().startsWith(">")) {

0 commit comments

Comments
 (0)