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>
This commit is contained in:
2026-09-13 22:58:47 +07:00
co-authored by Cursor
parent 8fa9f4350e
commit 46a2e2a7b0
67 changed files with 12153 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
function decodeBase64Url(data) {
if (!data) return "";
const pad = data.length % 4 === 0 ? "" : "=".repeat(4 - (data.length % 4));
return Buffer.from(
data.replace(/-/g, "+").replace(/_/g, "/") + pad,
"base64",
).toString("utf8");
}
function stripHtml(html) {
return html
.replace(/<style[\s\S]*?<\/style>/gi, "")
.replace(/<script[\s\S]*?<\/script>/gi, "")
.replace(/<br\s*\/?>/gi, "\n")
.replace(/<\/p>/gi, "\n\n")
.replace(/<\/div>/gi, "\n")
.replace(/<\/tr>/gi, "\n")
.replace(/<[^>]+>/g, "")
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&#x27;/gi, "'")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
function walkParts(payload, acc = { text: "", html: "" }) {
if (!payload) return acc;
const mime = payload.mimeType || "";
const data = payload.body?.data;
if (data) {
if (mime === "text/plain") acc.text += decodeBase64Url(data);
else if (mime === "text/html") acc.html += decodeBase64Url(data);
}
for (const part of payload.parts || []) {
walkParts(part, acc);
}
return acc;
}
export function extractBody(payload) {
const { text, html } = walkParts(payload);
if (text.trim()) return text.trim();
if (html.trim()) return stripHtml(html);
return "";
}
export function header(payload, name) {
const match = payload?.headers?.find(
(h) => h.name?.toLowerCase() === name.toLowerCase(),
);
return match?.value || "";
}
export function summarizeMessage(message) {
const payload = message.payload || {};
return {
id: message.id,
threadId: message.threadId,
from: header(payload, "From"),
to: header(payload, "To"),
subject: header(payload, "Subject") || "(no subject)",
date: header(payload, "Date"),
snippet: message.snippet || "",
};
}