Compute Email Roast Stats
Analyze Gmail data for roastable patterns: follow-ups ignored, late-night emails, meeting spam, desperate behavior
Source Code
import fs from "fs";
const [inputPath = "session/emails.json", outputPath = "session/roast_metrics.json"] =
process.argv.slice(2);
console.log(`Analyzing emails for roastable patterns: ${inputPath}`);
try {
if (!fs.existsSync(inputPath)) {
console.error(`File not found: ${inputPath}`);
process.exit(1);
}
const raw = fs.readFileSync(inputPath, "utf-8");
const data = JSON.parse(raw);
const messages = data.messages || [];
if (messages.length === 0) {
console.log("No messages to roast. Suspicious...");
const emptyStats = {
totalMessages: 0,
verdict: "Your inbox is suspiciously clean. What are you hiding?",
};
fs.writeFileSync(outputPath, JSON.stringify(emptyStats, null, 2));
console.log(JSON.stringify({ success: true, outputPath, stats: emptyStats }));
process.exit(0);
}
console.log(`Analyzing ${messages.length} messages for sins...`);
// Separate sent vs received
const sent = messages.filter((m) =>
m.labelIds?.includes("SENT") || m.from?.toLowerCase().includes("me")
);
const received = messages.filter((m) => !sent.includes(m));
// ===== FOLLOW-UP PATTERNS (THE CRINGE) =====
const followUpPatterns = [
/just following up/i,
/circling back/i,
/checking in/i,
/wanted to follow up/i,
/bumping this/i,
/any update/i,
/did you get a chance/i,
/touching base/i,
/looping back/i,
/gentle reminder/i,
/friendly reminder/i,
/per my last email/i,
];
const followUpEmails = sent.filter((m) =>
followUpPatterns.some((p) => p.test(m.subject) || p.test(m.snippet))
);
// Check which follow-ups got responses (by thread)
const followUpThreads = new Set(followUpEmails.map((m) => m.threadId));
const threadMessages = {};
for (const m of messages) {
if (!threadMessages[m.threadId]) threadMessages[m.threadId] = [];
threadMessages[m.threadId].push(m);
}
let followUpsIgnored = 0;
for (const threadId of followUpThreads) {
const thread = threadMessages[threadId] || [];
const followUpInThread = followUpEmails.find((f) => f.threadId === threadId);
if (!followUpInThread) continue;
const followUpDate = new Date(followUpInThread.date);
const repliesAfter = thread.filter((m) => {
const mDate = new Date(m.date);
return mDate > followUpDate && !sent.includes(m);
});
if (repliesAfter.length === 0) followUpsIgnored++;
}
// ===== LATE NIGHT / WEEKEND EMAILS =====
const lateNightEmails = [];
const weekendEmails = [];
const veryEarlyEmails = [];
for (const m of sent) {
if (!m.date) continue;
const d = new Date(m.date);
if (isNaN(d.getTime())) continue;
const hour = d.getHours();
const day = d.getDay();
if (hour >= 22 || hour < 5) lateNightEmails.push(m);
if (hour >= 5 && hour < 7) veryEarlyEmails.push(m);
if (day === 0 || day === 6) weekendEmails.push(m);
}
// ===== MEETING REQUEST SPAM =====
const meetingPatterns = [
/meeting/i,
/calendar invite/i,
/sync up/i,
/quick call/i,
/hop on a call/i,
/15 minutes/i,
/30 minutes/i,
/schedule/i,
/available for/i,
];
const meetingRequests = sent.filter((m) =>
meetingPatterns.some((p) => p.test(m.subject) || p.test(m.snippet))
);
// ===== RESPONSE TIME STATS =====
const responseTimes = [];
for (const threadId of Object.keys(threadMessages)) {
const thread = threadMessages[threadId].sort(
(a, b) => new Date(a.date) - new Date(b.date)
);
for (let i = 1; i < thread.length; i++) {
const prev = thread[i - 1];
const curr = thread[i];
const prevIsSent = sent.includes(prev);
const currIsSent = sent.includes(curr);
// Measure time to respond to received emails
if (!prevIsSent && currIsSent) {
const prevDate = new Date(prev.date);
const currDate = new Date(curr.date);
if (!isNaN(prevDate.getTime()) && !isNaN(currDate.getTime())) {
const diffMinutes = (currDate - prevDate) / (1000 * 60);
if (diffMinutes > 0 && diffMinutes < 43200) {
// Max 30 days
responseTimes.push(diffMinutes);
}
}
}
}
}
const avgResponseMinutes =
responseTimes.length > 0
? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length)
: null;
const fastestResponse = responseTimes.length > 0 ? Math.round(Math.min(...responseTimes)) : null;
const slowestResponse = responseTimes.length > 0 ? Math.round(Math.max(...responseTimes)) : null;
// ===== DESPERATE SENDER (single person obsession) =====
const recipientCounts = {};
for (const m of sent) {
const to = extractEmail(m.to);
if (to && to !== "unknown") {
recipientCounts[to] = (recipientCounts[to] || 0) + 1;
}
}
const topRecipients = Object.entries(recipientCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([email, count]) => ({
email,
count,
percentage: Math.round((count / sent.length) * 100),
}));
// ===== WHO IGNORES YOU MOST =====
const senderCounts = {};
for (const m of received) {
const from = extractEmail(m.from);
if (from && from !== "unknown") {
senderCounts[from] = (senderCounts[from] || 0) + 1;
}
}
const topSenders = Object.entries(senderCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([email, count]) => ({ email, count }));
// ===== EMAIL LENGTH (VERBOSITY) =====
const snippetLengths = sent.map((m) => (m.snippet || "").length);
const avgSnippetLength =
snippetLengths.length > 0
? Math.round(snippetLengths.reduce((a, b) => a + b, 0) / snippetLengths.length)
: 0;
// ===== THREAD ABANDONMENT =====
let abandonedThreads = 0;
let totalThreads = 0;
for (const threadId of Object.keys(threadMessages)) {
const thread = threadMessages[threadId];
if (thread.length < 2) continue;
totalThreads++;
const lastMessage = thread.sort((a, b) => new Date(b.date) - new Date(a.date))[0];
// If the last message in thread is FROM someone else (not you), you abandoned it
if (received.includes(lastMessage)) {
abandonedThreads++;
}
}
const abandonmentRate =
totalThreads > 0 ? Math.round((abandonedThreads / totalThreads) * 100) : 0;
// ===== HOUR OF DAY DISTRIBUTION =====
const hourDistribution = {};
for (let i = 0; i < 24; i++) hourDistribution[i] = 0;
for (const m of sent) {
if (!m.date) continue;
const d = new Date(m.date);
if (!isNaN(d.getTime())) {
hourDistribution[d.getHours()]++;
}
}
const peakHour = Object.entries(hourDistribution).reduce((a, b) =>
b[1] > a[1] ? b : a
);
// ===== COMPILE ROAST STATS =====
const stats = {
analyzed_at: new Date().toISOString(),
total_emails: messages.length,
sent_count: sent.length,
received_count: received.length,
// The Cringe Stats
follow_ups: {
total_sent: followUpEmails.length,
ignored: followUpsIgnored,
ignored_rate: followUpEmails.length > 0
? Math.round((followUpsIgnored / followUpEmails.length) * 100)
: 0,
sample_subjects: followUpEmails.slice(0, 3).map((m) => m.subject),
},
// Boundary Issues
late_night: {
count: lateNightEmails.length,
percentage: sent.length > 0
? Math.round((lateNightEmails.length / sent.length) * 100)
: 0,
},
weekend: {
count: weekendEmails.length,
percentage: sent.length > 0
? Math.round((weekendEmails.length / sent.length) * 100)
: 0,
},
early_bird: {
count: veryEarlyEmails.length,
percentage: sent.length > 0
? Math.round((veryEarlyEmails.length / sent.length) * 100)
: 0,
},
// Meeting Addict
meeting_requests: {
count: meetingRequests.length,
percentage: sent.length > 0
? Math.round((meetingRequests.length / sent.length) * 100)
: 0,
},
// Response Behavior
response_time: {
average_minutes: avgResponseMinutes,
fastest_minutes: fastestResponse,
slowest_minutes: slowestResponse,
average_human: formatDuration(avgResponseMinutes),
fastest_human: formatDuration(fastestResponse),
slowest_human: formatDuration(slowestResponse),
},
// Relationship Patterns
top_recipients: topRecipients,
top_senders: topSenders,
recipient_obsession: topRecipients[0] || null,
// Conversation Style
verbosity: {
avg_snippet_length: avgSnippetLength,
assessment:
avgSnippetLength > 150
? "novelist"
: avgSnippetLength > 80
? "normal"
: avgSnippetLength > 40
? "terse"
: "one-word-reply-king",
},
// Thread Behavior
thread_abandonment: {
rate: abandonmentRate,
abandoned: abandonedThreads,
total: totalThreads,
},
// Time Patterns
peak_hour: {
hour: parseInt(peakHour[0]),
count: peakHour[1],
description: formatHour(parseInt(peakHour[0])),
},
hour_distribution: hourDistribution,
};
// Write output
const dir = outputPath.includes("/") ? outputPath.split("/").slice(0, -1).join("/") : null;
if (dir) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(outputPath, JSON.stringify(stats, null, 2));
// Log summary
console.log(`\n✓ Roast analysis complete`);
console.log(` Total emails: ${stats.total_emails}`);
console.log(` Follow-ups sent: ${stats.follow_ups.total_sent}`);
console.log(` Follow-ups IGNORED: ${stats.follow_ups.ignored} (${stats.follow_ups.ignored_rate}%)`);
console.log(` Late night emails: ${stats.late_night.count}`);
console.log(` Weekend emails: ${stats.weekend.count}`);
console.log(` Meeting requests: ${stats.meeting_requests.count}`);
console.log(` Avg response time: ${stats.response_time.average_human || "N/A"}`);
console.log(` Thread abandonment: ${stats.thread_abandonment.rate}%`);
console.log(` Peak sending hour: ${stats.peak_hour.description}`);
console.log(` Written to: ${outputPath}`);
console.log(JSON.stringify({ success: true, outputPath, stats }));
} catch (error) {
console.error("Error analyzing emails:", error.message);
throw error;
}
function extractEmail(header) {
if (!header) return "unknown";
const match = header.match(/<([^>]+)>/);
return match ? match[1].toLowerCase() : header.toLowerCase().trim();
}
function formatDuration(minutes) {
if (minutes === null) return null;
if (minutes < 60) return `${minutes} minutes`;
if (minutes < 1440) return `${Math.round(minutes / 60)} hours`;
return `${Math.round(minutes / 1440)} days`;
}
function formatHour(hour) {
if (hour === 0) return "Midnight (12 AM)";
if (hour === 12) return "Noon (12 PM)";
if (hour < 12) return `${hour} AM`;
return `${hour - 12} PM`;
}