Files
nsrbandCursor 46a2e2a7b0 feat: add gmail-hook monorepo with watcher, api, and web
Ship the receipt pipeline (Livin/Grab/BRI), PocketBase spendings, PM2 ecosystem, and a proper .gitignore that excludes secrets, SQLite data, and media dumps.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 22:58:47 +07:00

328 lines
8.5 KiB
JavaScript

import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
expandWorkItems,
listRfc822Parts,
messageFromRfc822Part,
} from "../src/mime.js";
import { processFetchedMessage } from "../src/process-fetched.js";
import { matchRule } from "../src/handlers/match-rule.js";
import { parseMatchedMessage } from "../src/handlers/parse-matched-message.js";
const silentLog = {
info() {},
warn() {},
error() {},
};
function textPart(text, partId = "0.0") {
return {
partId,
mimeType: "text/plain",
body: {
data: Buffer.from(text, "utf8")
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, ""),
},
};
}
function htmlPart(html, partId = "0.1") {
return {
partId,
mimeType: "text/html",
body: {
data: Buffer.from(html, "utf8")
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, ""),
},
};
}
function rfc822Part({
partId,
filename,
from,
subject,
date,
bodyText,
bodyHtml,
}) {
return {
partId,
mimeType: "message/rfc822",
filename,
parts: [
{
partId: `${partId}.0`,
mimeType: "multipart/alternative",
headers: [
{ name: "From", value: from },
{ name: "Subject", value: subject },
{ name: "Date", value: date },
],
parts: [
textPart(bodyText, `${partId}.0.0`),
...(bodyHtml ? [htmlPart(bodyHtml, `${partId}.0.1`)] : []),
],
},
],
};
}
const grabRideBody = `Bike Hemat
Hope you enjoyed your ride!
Picked up on 13 September 2026
Booking ID: A-TESTBOOKING1
Total Paid
Rp16.600
Receipt issued by the driver Mahendra.
Passenger
Bintang
`;
const grabFoodBody = `Selamat menikmati makanan Anda!
TOTAL
IDR 49000 TANGGAL | WAKTU
11 Sep 26 18:24 +0700
Detail Pesanan
Jenis Kendaraan:
GrabFood
Diterbitkan untuk
Bintang
Pesanan ID
00112712674-TESTFOOD1
Pesanan Dari:
Doner Kebab - Kalibata City
Profil:
Personal
Detail Tagihan
Detail Pembayaran:
OVO Cash
TOTAL (INCL. TAX) IDR 49000
`;
describe("listRfc822Parts", () => {
it("finds message/rfc822 attachments and does not treat outer as eml", () => {
const payload = {
mimeType: "multipart/mixed",
parts: [
{
mimeType: "multipart/alternative",
parts: [textPart("wrapper")],
},
rfc822Part({
partId: "1",
filename: "Your Grab E-Receipt.eml",
from: "Grab <no-reply@grab.com>",
subject: "Your Grab E-Receipt",
date: "Wed, 9 Sep 2026 06:09:32 +0000",
bodyText: grabRideBody,
}),
rfc822Part({
partId: "2",
filename: "Your Grab E-Receipt.eml",
from: "Grab <no-reply@grab.com>",
subject: "Your Grab E-Receipt",
date: "Thu, 10 Sep 2026 11:28:24 +0000",
bodyText: grabFoodBody,
}),
],
};
const parts = listRfc822Parts(payload);
assert.equal(parts.length, 2);
assert.equal(parts[0].filename, "Your Grab E-Receipt.eml");
});
});
describe("messageFromRfc822Part", () => {
it("builds synthetic message with nested From/Subject/body", () => {
const part = rfc822Part({
partId: "1",
filename: "Your Grab E-Receipt.eml",
from: "Grab <no-reply@grab.com>",
subject: "Your Grab E-Receipt",
date: "Wed, 9 Sep 2026 06:09:32 +0000",
bodyText: grabRideBody,
});
const msg = messageFromRfc822Part(part, {
parentMessageId: "outer123",
index: 0,
});
assert.equal(msg.id, "outer123#1");
assert.equal(msg.from, "Grab <no-reply@grab.com>");
assert.equal(msg.subject, "Your Grab E-Receipt");
assert.match(msg.body, /Booking ID: A-TESTBOOKING1/);
assert.equal(msg.parentMessageId, "outer123");
assert.equal(msg.attachmentFilename, "Your Grab E-Receipt.eml");
});
});
describe("expandWorkItems", () => {
it("returns outer only when no eml", () => {
const outer = {
id: "outer",
from: "me@x.com",
subject: "hi",
body: "x",
html: "",
};
const payload = {
mimeType: "multipart/alternative",
parts: [textPart("hi")],
};
const items = expandWorkItems(outer, payload);
assert.equal(items.length, 1);
assert.equal(items[0], outer);
});
it("returns eml messages when attachments exist", () => {
const outer = {
id: "outer123",
from: "NSRB <nasyarobby@gmail.com>",
subject: "Test attachment",
body: "Regards",
html: "",
};
const payload = {
mimeType: "multipart/mixed",
parts: [
{ mimeType: "multipart/alternative", parts: [textPart("Regards")] },
rfc822Part({
partId: "1",
filename: "a.eml",
from: "Grab <no-reply@grab.com>",
subject: "Your Grab E-Receipt",
date: "Wed, 9 Sep 2026 06:09:32 +0000",
bodyText: grabRideBody,
}),
],
};
const items = expandWorkItems(outer, payload);
assert.equal(items.length, 1);
assert.equal(items[0].from, "Grab <no-reply@grab.com>");
assert.equal(items[0].id, "outer123#1");
});
});
describe("processFetchedMessage fan-out", () => {
it("processes each eml attachment and skips outer wrapper", async () => {
const fetched = {
notification: { emailAddress: "eleven16th@gmail.com" },
log: silentLog,
message: {
id: "outer123",
from: "NSRB <nasyarobby@gmail.com>",
subject: "Test attachment",
body: "Regards",
html: "",
},
emlMessages: [
{
id: "outer123#1",
from: "Grab <no-reply@grab.com>",
subject: "Your Grab E-Receipt",
date: "Wed, 9 Sep 2026 06:09:32 +0000",
body: grabRideBody,
html: "",
parentMessageId: "outer123",
},
{
id: "outer123#2",
from: "Grab <no-reply@grab.com>",
subject: "Your Grab E-Receipt",
date: "Thu, 10 Sep 2026 11:28:24 +0000",
body: grabFoodBody,
html: "",
parentMessageId: "outer123",
},
],
};
const results = await processFetchedMessage(fetched, [
matchRule,
parseMatchedMessage,
]);
assert.equal(results.length, 2);
assert.equal(results[0].parsed?.kind, "grab.ereceipt.ride");
assert.equal(results[0].parsed?.reference, "A-TESTBOOKING1");
assert.equal(results[1].parsed?.kind, "grab.ereceipt.food");
assert.equal(results[1].parsed?.reference, "00112712674-TESTFOOD1");
});
it("processes outer message when no eml attachments", async () => {
const fetched = {
notification: { emailAddress: "eleven16th@gmail.com" },
log: silentLog,
message: {
id: "direct1",
from: `"Livin'" <noreply.livin@bankmandiri.co.id>`,
subject: "Pembayaran Berhasil!",
date: "Sun, 13 Sep 2026 14:48:36 +0700",
body: `Halo TEST,
Penerima
Shop
JAKARTA SELATAN - ID
Tanggal 13 Sep 2026
Jam 10:00:00 WIB
Nominal Transaksi Rp 10.000,00
No. Referensi 9999999999999999999`,
html: "",
},
emlMessages: [],
};
const results = await processFetchedMessage(fetched, [
matchRule,
parseMatchedMessage,
]);
assert.equal(results.length, 1);
assert.equal(results[0].parsed?.kind, "livin.qr_payment.success");
});
});
describe("matchRule on synthetic eml", () => {
it("matches Grab from nested headers without Fwd", async () => {
const result = await matchRule({
messageId: "outer#1",
log: silentLog,
message: {
id: "outer#1",
from: "Grab <no-reply@grab.com>",
subject: "Your Grab E-Receipt",
body: grabRideBody,
html: "",
},
});
assert.equal(result.pass.rule.kind, "grab.ereceipt.ride");
assert.equal(result.pass.original.from, "Grab <no-reply@grab.com>");
});
});
describe("extractMimePartsFromRawEml", () => {
it("pulls text/html from a simple multipart eml", async () => {
const { extractMimePartsFromRawEml } = await import("../src/mime.js");
const raw = [
'Content-Type: multipart/alternative; boundary="bnd"',
"",
"--bnd",
"Content-Type: text/plain; charset=UTF-8",
"",
"Hello plain",
"--bnd",
"Content-Type: text/html; charset=UTF-8",
"",
"<b>Hello html</b>",
"--bnd--",
"",
].join("\r\n");
const parts = extractMimePartsFromRawEml(raw);
assert.match(parts.text, /Hello plain/);
assert.match(parts.html, /Hello html/);
});
});