Scan Emails for Bingo Phrases
Fetch sent emails and count occurrences of 25 corporate email cliches
Source Code
import fs from "fs";
// Note: This code follows Gmail API patterns from skill:gmail.inbox.search
// but requires format=full for body text scanning (existing code:gmail.inbox.fetch
// only supports metadata format). The phrase counting logic is bingo-specific.
// The 25 bingo phrases with their search patterns (case-insensitive)
const BINGO_PHRASES = [
// B Column - Apologetic
{ id: "B1", phrase: "Sorry for the delay", patterns: ["sorry for the delay", "apologies for the delay", "sorry for my late"] },
{ id: "B2", phrase: "Apologies for the confusion", patterns: ["apologies for the confusion", "sorry for the confusion", "apologize for any confusion"] },
{ id: "B3", phrase: "Sorry to bother you", patterns: ["sorry to bother", "hate to bother", "apologies for bothering"] },
{ id: "B4", phrase: "I apologize for any inconvenience", patterns: ["apologize for any inconvenience", "sorry for any inconvenience", "apologies for the inconvenience"] },
{ id: "B5", phrase: "Sorry for the late reply", patterns: ["sorry for the late reply", "apologies for the late", "sorry for my delayed"] },
// I Column - Passive-Aggressive
{ id: "I1", phrase: "Per my last email", patterns: ["per my last email", "as per my last", "as i mentioned", "as mentioned in my previous"] },
{ id: "I2", phrase: "As previously mentioned", patterns: ["as previously mentioned", "as i previously", "mentioned previously", "as noted earlier"] },
{ id: "I3", phrase: "Just to clarify", patterns: ["just to clarify", "to clarify", "for clarification", "let me clarify"] },
{ id: "I4", phrase: "Going forward", patterns: ["going forward", "moving forward", "from now on"] },
{ id: "I5", phrase: "As per our conversation", patterns: ["as per our conversation", "per our conversation", "as we discussed", "as discussed"] },
// N Column - Corporate Jargon
{ id: "N1", phrase: "Circle back", patterns: ["circle back", "circling back", "circle around"] },
{ id: "N2", phrase: "Touch base", patterns: ["touch base", "touching base", "quick touch base"] },
{ id: "N3", phrase: "FREE SPACE", patterns: [] }, // Everyone gets this one
{ id: "N4", phrase: "Synergies", patterns: ["synergy", "synergies", "synergize", "synergistic"] },
{ id: "N5", phrase: "Low-hanging fruit", patterns: ["low-hanging fruit", "low hanging fruit", "quick wins"] },
// G Column - Filler Phrases
{ id: "G1", phrase: "Hope this email finds you well", patterns: ["hope this email finds you", "hope this finds you well", "hope you're doing well", "hope you are well"] },
{ id: "G2", phrase: "Let me know if you have any questions", patterns: ["let me know if you have any questions", "feel free to reach out", "don't hesitate to ask"] },
{ id: "G3", phrase: "Hope that makes sense", patterns: ["hope that makes sense", "hope this makes sense", "let me know if that makes sense", "does that make sense"] },
{ id: "G4", phrase: "Please advise", patterns: ["please advise", "kindly advise", "please let me know"] },
{ id: "G5", phrase: "Thanks in advance", patterns: ["thanks in advance", "thank you in advance", "appreciate your help in advance"] },
// O Column - Urgency Theater
{ id: "O1", phrase: "ASAP", patterns: ["asap", "as soon as possible", "at your earliest"] },
{ id: "O2", phrase: "Just following up", patterns: ["just following up", "following up on", "quick follow up", "wanted to follow up"] },
{ id: "O3", phrase: "Friendly reminder", patterns: ["friendly reminder", "gentle reminder", "quick reminder", "kind reminder"] },
{ id: "O4", phrase: "At your earliest convenience", patterns: ["at your earliest convenience", "when you get a chance", "whenever you have time"] },
{ id: "O5", phrase: "Wanted to flag", patterns: ["wanted to flag", "flagging this", "just flagging", "to flag"] },
];
const CONCURRENCY = 10;
const [outputPath, maxEmailsArg = "200"] = process.argv.slice(2);
const maxEmails = parseInt(maxEmailsArg, 10);
if (!outputPath) {
console.error("Error: outputPath is required");
process.exit(1);
}
/**
* Fetch message IDs for sent emails
*/
async function fetchMessageIds(maxResults) {
const ids = [];
let pageToken = null;
while (ids.length < maxResults) {
const remaining = maxResults - ids.length;
const pageSize = Math.min(remaining, 100);
const url = new URL("https://gmail.googleapis.com/gmail/v1/users/me/messages");
url.searchParams.set("maxResults", pageSize.toString());
url.searchParams.set("q", "in:sent newer_than:90d");
if (pageToken) url.searchParams.set("pageToken", pageToken);
const res = await fetch(url.toString(), {
headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" },
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Gmail API failed: ${res.status} - ${text}`);
}
const data = await res.json();
if (!data.messages || data.messages.length === 0) break;
ids.push(...data.messages.map((m) => m.id).slice(0, remaining));
pageToken = data.nextPageToken;
if (!pageToken) break;
}
return ids;
}
/**
* Fetch full message content
*/
async function fetchMessages(messageIds) {
const results = [];
for (let i = 0; i < messageIds.length; i += CONCURRENCY) {
const batch = messageIds.slice(i, i + CONCURRENCY);
const fetched = await Promise.all(
batch.map(async (id) => {
const url = `https://gmail.googleapis.com/gmail/v1/users/me/messages/${id}?format=full`;
const res = await fetch(url, {
headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" },
});
if (!res.ok) return null;
try {
return await res.json();
} catch {
return null;
}
})
);
results.push(...fetched.filter(Boolean));
console.log(` Fetched ${Math.min(i + CONCURRENCY, messageIds.length)}/${messageIds.length}...`);
}
return results;
}
/**
* Extract plain text body from Gmail message
*/
function extractBodyText(payload) {
if (!payload) return "";
// Direct body
if (payload.body?.data) {
return Buffer.from(payload.body.data, "base64").toString("utf-8");
}
// Multipart - look for text/plain
if (payload.parts) {
for (const part of payload.parts) {
if (part.mimeType === "text/plain" && part.body?.data) {
return Buffer.from(part.body.data, "base64").toString("utf-8");
}
// Nested parts
if (part.parts) {
const nested = extractBodyText(part);
if (nested) return nested;
}
}
// Fallback to text/html if no plain text
for (const part of payload.parts) {
if (part.mimeType === "text/html" && part.body?.data) {
const html = Buffer.from(part.body.data, "base64").toString("utf-8");
return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ");
}
}
}
return "";
}
/**
* Count phrase occurrences in text
*/
function countPhrases(text) {
const lowerText = text.toLowerCase();
const counts = {};
for (const { id, phrase, patterns } of BINGO_PHRASES) {
if (patterns.length === 0) {
// FREE SPACE
counts[id] = { phrase, count: 1 };
continue;
}
let count = 0;
for (const pattern of patterns) {
const regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
const matches = lowerText.match(regex);
if (matches) count += matches.length;
}
counts[id] = { phrase, count };
}
return counts;
}
async function main() {
console.log("๐ฏ Inbox Bingo - Scanning for corporate email crimes...\n");
// Fetch sent email IDs
console.log("Phase 1: Finding sent emails...");
const messageIds = await fetchMessageIds(maxEmails);
console.log(` Found ${messageIds.length} sent emails from the last 90 days\n`);
if (messageIds.length === 0) {
console.error("No sent emails found in the last 90 days.");
const emptyResult = {
success: false,
error: "no_emails_found",
emailCount: 0,
phrases: {},
};
fs.writeFileSync(outputPath, JSON.stringify(emptyResult, null, 2));
process.exit(1);
}
// Fetch full messages
console.log("Phase 2: Reading email content...");
const messages = await fetchMessages(messageIds);
console.log(` Retrieved ${messages.length} emails\n`);
// Extract and combine all text
console.log("Phase 3: Counting corporate crimes...");
let allText = "";
for (const msg of messages) {
const body = extractBodyText(msg.payload);
allText += " " + body;
}
// Count phrases
const phraseCounts = countPhrases(allText);
// Calculate stats
let totalDisasterScore = 0;
let filledSquares = 0;
const topPhrases = [];
for (const [id, { phrase, count }] of Object.entries(phraseCounts)) {
if (count > 0) {
filledSquares++;
totalDisasterScore += count;
if (id !== "N3") { // Exclude FREE SPACE from top phrases
topPhrases.push({ id, phrase, count });
}
}
}
// Sort by count descending
topPhrases.sort((a, b) => b.count - a.count);
// Calculate bingo lines
const grid = [
["B1", "I1", "N1", "G1", "O1"],
["B2", "I2", "N2", "G2", "O2"],
["B3", "I3", "N3", "G3", "O3"],
["B4", "I4", "N4", "G4", "O4"],
["B5", "I5", "N5", "G5", "O5"],
];
let bingoCount = 0;
const bingos = [];
// Check rows
for (let row = 0; row < 5; row++) {
if (grid[row].every((id) => phraseCounts[id]?.count > 0)) {
bingoCount++;
bingos.push(`Row ${row + 1}`);
}
}
// Check columns
const columns = ["B", "I", "N", "G", "O"];
for (let col = 0; col < 5; col++) {
const colIds = [1, 2, 3, 4, 5].map((n) => `${columns[col]}${n}`);
if (colIds.every((id) => phraseCounts[id]?.count > 0)) {
bingoCount++;
bingos.push(`${columns[col]} Column`);
}
}
// Check diagonals
const diag1 = ["B1", "I2", "N3", "G4", "O5"];
const diag2 = ["B5", "I4", "N3", "G2", "O1"];
if (diag1.every((id) => phraseCounts[id]?.count > 0)) {
bingoCount++;
bingos.push("Diagonal โ");
}
if (diag2.every((id) => phraseCounts[id]?.count > 0)) {
bingoCount++;
bingos.push("Diagonal โ");
}
// Build result
const result = {
success: true,
emailCount: messages.length,
filledSquares,
totalDisasterScore,
bingoCount,
bingos,
topPhrases: topPhrases.slice(0, 10),
phrases: phraseCounts,
scannedAt: new Date().toISOString(),
};
// Write results
const dir = outputPath.substring(0, outputPath.lastIndexOf("/"));
if (dir) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(result, null, 2));
// Log summary
console.log("\n๐ฏ BINGO RESULTS:");
console.log(` Emails scanned: ${messages.length}`);
console.log(` Squares filled: ${filledSquares}/25`);
console.log(` Total disaster score: ${totalDisasterScore}`);
console.log(` Bingos: ${bingoCount} (${bingos.join(", ") || "none"})`);
console.log("\n Top corporate crimes:");
for (const { phrase, count } of topPhrases.slice(0, 5)) {
console.log(` โข "${phrase}": ${count}x`);
}
console.log(`\nโ Results saved to: ${outputPath}`);
console.log(JSON.stringify({ success: true, outputPath, bingoCount, filledSquares, totalDisasterScore }, null, 2));
}
main().catch((err) => {
console.error("Error:", err.message);
process.exit(1);
});