From 39eb1bce2f16991d581004eaf6caa0f5a927f986 Mon Sep 17 00:00:00 2001 From: Nasyarobby Putra Date: Mon, 14 Sep 2026 00:04:26 +0700 Subject: [PATCH] fix(watcher): handle wrapped Subject lines in forwarded emails Update the parseForwardedHeader function to correctly join wrapped Subject lines before the To: line, ensuring accurate parsing of Gmail forwarded messages. Enhance tests to validate this behavior with Tokopedia email examples. --- apps/watcher/src/lib/email.js | 7 +++++-- apps/watcher/test/fixtures.js | 3 ++- apps/watcher/test/match.test.js | 10 ++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/apps/watcher/src/lib/email.js b/apps/watcher/src/lib/email.js index 76f8be1..ab1e801 100644 --- a/apps/watcher/src/lib/email.js +++ b/apps/watcher/src/lib/email.js @@ -13,6 +13,9 @@ export function extractEmail(from) { /** * Parse a Gmail "Forwarded message" header block from the body. * Returns null if no forwarded header is present. + * + * Subject may wrap onto following lines without leading whitespace + * (Gmail plain-text fold), so we read until the To: line. */ export function parseForwardedHeader(body) { if (!body) return null; @@ -22,14 +25,14 @@ export function parseForwardedHeader(body) { const after = body.slice(idx); const headerMatch = after.match( - /From:\s*(.+)\r?\nDate:\s*(.+)\r?\nSubject:\s*(.+)\r?\nTo:\s*(.+)/i, + /From:\s*(.+)\r?\nDate:\s*(.+)\r?\nSubject:\s*([\s\S]+?)\r?\nTo:\s*(.+)/i, ); if (!headerMatch) return null; return { from: headerMatch[1].trim(), date: headerMatch[2].trim(), - subject: headerMatch[3].trim(), + subject: headerMatch[3].replace(/\s+/g, " ").trim(), to: headerMatch[4].trim(), }; } diff --git a/apps/watcher/test/fixtures.js b/apps/watcher/test/fixtures.js index 8e47df9..25c6275 100644 --- a/apps/watcher/test/fixtures.js +++ b/apps/watcher/test/fixtures.js @@ -359,7 +359,8 @@ Roby export const tokopediaMultiForwardedBody = `---------- Forwarded message --------- From: Tokopedia Date: Fri, Aug 21, 2026 at 12:39 AM -Subject: Pesanan Selesai: “sandal jepit ANDA pria casual...” dan 1 Item Lainnya +Subject: Pesanan Selesai: “sandal jepit ANDA pria casual...” dan 1 Item +Lainnya To: Hai *Roby,* diff --git a/apps/watcher/test/match.test.js b/apps/watcher/test/match.test.js index c932003..24adaed 100644 --- a/apps/watcher/test/match.test.js +++ b/apps/watcher/test/match.test.js @@ -43,6 +43,16 @@ describe("parseForwardedHeader", () => { assert.equal(h.subject, "Pembayaran Berhasil!"); }); + it("joins wrapped Subject lines before To", () => { + const h = parseForwardedHeader(tokopediaMultiForwardedBody); + assert.ok(h); + assert.match(h.from, /noreply@tokopedia\.com/); + assert.equal( + h.subject, + "Pesanan Selesai: “sandal jepit ANDA pria casual...” dan 1 Item Lainnya", + ); + }); + it("returns null when there is no Fwd block", () => { assert.equal(parseForwardedHeader(livinDirectBody), null); });