274 lines
9.1 KiB
JavaScript
274 lines
9.1 KiB
JavaScript
import { senderDisplayName } from "./senderAvatar";
|
||
|
||
const CATEGORY_ORDER = ["Primary", "Updates", "Promotions", "Social", "Finance"];
|
||
|
||
export function emailSearchText(email) {
|
||
const sender = typeof email.sender === "string"
|
||
? email.sender
|
||
: email.sender?.name || email.sender?.email || email.sender?.address || "";
|
||
return [
|
||
sender,
|
||
email.subject,
|
||
email.snippet,
|
||
email.preview,
|
||
email.body_text,
|
||
].filter(Boolean).join(" ").toLowerCase();
|
||
}
|
||
|
||
export function emailCategory(email) {
|
||
const text = emailSearchText(email);
|
||
if (/invoice|bill|payment|receipt|finance|bank|transfer|otp|wallet/.test(text)) return "Finance";
|
||
if (/linkedin|twitter|facebook|instagram|connection request|social/.test(text)) return "Social";
|
||
if (/unsubscribe|promo|offer|sale|newsletter|deal|discount/.test(text)) return "Promotions";
|
||
if (/notification|update|alert|noreply|no-reply|provisioned|activated|console|security/.test(text)) return "Updates";
|
||
return "Primary";
|
||
}
|
||
|
||
const PRIORITY_LABELS = new Set(["Important", "Clients", "Projects", "Finance", "HR"]);
|
||
const ACTION_LABELS = new Set(["Clients", "Important", "Projects", "Finance", "HR", "AI"]);
|
||
|
||
export function emailLabels(email) {
|
||
const text = emailSearchText(email);
|
||
const labels = [];
|
||
if (email.is_starred || /urgent|important|asap|priority/.test(text)) {
|
||
labels.push({ name: "Important", tone: "rose" });
|
||
}
|
||
if (/project|proposal|website|development|sow|contract/.test(text)) {
|
||
labels.push({ name: "Projects", tone: "violet" });
|
||
}
|
||
if (/client|customer|internship|candidate|revdau|desi clans/.test(text)) {
|
||
labels.push({ name: "Clients", tone: "blue" });
|
||
}
|
||
if (/hr|hiring|recruit|offer letter|onboarding/.test(text)) {
|
||
labels.push({ name: "HR", tone: "violet" });
|
||
}
|
||
if (/invoice|bill|payment|finance|receipt/.test(text)) {
|
||
labels.push({ name: "Finance", tone: "amber" });
|
||
}
|
||
if (/integration|api|webhook|connect/.test(text)) {
|
||
labels.push({ name: "Integrations", tone: "green" });
|
||
}
|
||
if (/ai|agent|llm|gpt|model/.test(text)) {
|
||
labels.push({ name: "AI", tone: "pink" });
|
||
}
|
||
if (/domain|dns|mx|spf|dkim|\.env|environment variable/.test(text)) {
|
||
labels.push({ name: "Domains", tone: "amber" });
|
||
}
|
||
if (/linkedin|twitter|facebook|instagram|social/.test(text)) {
|
||
labels.push({ name: "Social", tone: "green" });
|
||
}
|
||
if (/notification|update|alert|noreply|security/.test(text)) {
|
||
labels.push({ name: "Updates", tone: "blue" });
|
||
}
|
||
if (/personal|family/.test(text)) {
|
||
labels.push({ name: "Personal", tone: "green" });
|
||
}
|
||
const unique = [];
|
||
labels.forEach((label) => {
|
||
if (!unique.some((item) => item.name === label.name)) unique.push(label);
|
||
});
|
||
return unique.slice(0, 2);
|
||
}
|
||
|
||
export function matchesLens(email, lens) {
|
||
if (!lens || lens === "all") return true;
|
||
if (lens === "starred") return email.is_starred === true;
|
||
return emailLabels(email).some((label) => label.name === lens);
|
||
}
|
||
|
||
export function categoryCounts(emails) {
|
||
const counts = Object.fromEntries(CATEGORY_ORDER.map((name) => [name, 0]));
|
||
emails.forEach((email) => {
|
||
if (email.is_read === false) counts[emailCategory(email)] += 1;
|
||
});
|
||
return counts;
|
||
}
|
||
|
||
function hasPriorityLabel(email) {
|
||
return emailLabels(email).some((label) => PRIORITY_LABELS.has(label.name));
|
||
}
|
||
|
||
function hasActionLabel(email) {
|
||
return emailLabels(email).some((label) => ACTION_LABELS.has(label.name));
|
||
}
|
||
|
||
function looksActionable(email) {
|
||
const text = emailSearchText(email);
|
||
return /please|action|reply|review|attached|confirm|need|waiting|\?|follow up|response/.test(text)
|
||
|| attachmentCount(email) > 0
|
||
|| /https?:\/\//.test(text);
|
||
}
|
||
|
||
export function inboxIntelligence(emails) {
|
||
const unread = emails.filter((email) => email.is_read === false);
|
||
const important = emails.filter((email) =>
|
||
email.is_starred
|
||
|| email.is_read === false
|
||
|| hasPriorityLabel(email)
|
||
|| emailLabels(email).length > 0,
|
||
);
|
||
const actionItems = emails.filter((email) =>
|
||
looksActionable(email) || hasActionLabel(email) || email.is_read === false,
|
||
);
|
||
const meetings = emails.filter((email) =>
|
||
/meet|meeting|calendar|schedule|call|zoom|interview|sync|demo|standup/.test(emailSearchText(email)),
|
||
);
|
||
const invoices = emails.filter((email) =>
|
||
/invoice|bill|payment|receipt|purchase order|\bpo\b/.test(emailSearchText(email)),
|
||
);
|
||
return {
|
||
unread: unread.length,
|
||
important: important.length,
|
||
actionItems: actionItems.length,
|
||
meetings: meetings.length,
|
||
invoices: invoices.length,
|
||
importantFromClients: important.filter((email) =>
|
||
emailLabels(email).some((label) => label.name === "Clients"),
|
||
).length,
|
||
};
|
||
}
|
||
|
||
export function attachmentCount(email) {
|
||
if (Array.isArray(email.attachments)) return email.attachments.length;
|
||
return Number(email.attachment_count || 0);
|
||
}
|
||
|
||
function emailTimestamp(email) {
|
||
const value = email.received_at || email.sent_at || email.created_at || email.updated_at;
|
||
if (!value) return null;
|
||
const date = new Date(value);
|
||
return Number.isNaN(date.getTime()) ? null : date;
|
||
}
|
||
|
||
function formatRelativeAge(date) {
|
||
if (!date) return "";
|
||
const hours = Math.floor((Date.now() - date.getTime()) / 3600000);
|
||
if (hours < 1) return "<1h";
|
||
if (hours < 24) return `${hours}h`;
|
||
return `${Math.floor(hours / 24)}d`;
|
||
}
|
||
|
||
function extractMeetingTime(text) {
|
||
const match = text.match(/\b(\d{1,2}:\d{2})\s*(am|pm)?/i)
|
||
|| text.match(/\b(\d{1,2})\s*(am|pm)\b/i);
|
||
if (!match) return null;
|
||
const raw = match[0].replace(/\s+/g, " ").trim();
|
||
if (/am|pm/i.test(raw)) return raw.toUpperCase();
|
||
const [hours, minutes] = raw.split(":").map(Number);
|
||
if (Number.isNaN(hours)) return raw;
|
||
const period = hours >= 12 ? "PM" : "AM";
|
||
const hour12 = hours % 12 || 12;
|
||
return `${hour12}:${String(minutes || 0).padStart(2, "0")} ${period}`;
|
||
}
|
||
|
||
function extractVenue(text) {
|
||
if (/google meet|meet\.google/i.test(text)) return "Google Meet";
|
||
if (/zoom/i.test(text)) return "Zoom";
|
||
if (/teams|microsoft teams/i.test(text)) return "Teams";
|
||
if (/video call|video meeting/i.test(text)) return "Video call";
|
||
return "";
|
||
}
|
||
|
||
function extractReplyStatus(text) {
|
||
if (/confirm|confirmation|awaiting/.test(text)) return "Awaiting confirmation";
|
||
if (/proposal|quote/.test(text)) return "Proposal reply";
|
||
if (/invoice|payment|bill/.test(text)) return "Payment follow-up";
|
||
if (/review|feedback/.test(text)) return "Needs review";
|
||
if (/reply|respond/.test(text)) return "Needs reply";
|
||
return "Needs attention";
|
||
}
|
||
|
||
function extractDeadline(text) {
|
||
if (/tomorrow/i.test(text)) return "Tomorrow";
|
||
const weekday = text.match(/\b(monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/i);
|
||
if (weekday) {
|
||
return weekday[1].charAt(0).toUpperCase() + weekday[1].slice(1);
|
||
}
|
||
if (/today/i.test(text)) return "Today";
|
||
return null;
|
||
}
|
||
|
||
export function inboxContext(emails) {
|
||
const stats = inboxIntelligence(emails);
|
||
|
||
const needsReply = emails
|
||
.filter((email) =>
|
||
email.is_read === false
|
||
|| looksActionable(email)
|
||
|| hasActionLabel(email)
|
||
|| /please|reply|respond|confirm|review|attached|need your|waiting|action required/.test(
|
||
emailSearchText(email),
|
||
),
|
||
)
|
||
.sort((left, right) => {
|
||
const leftDate = emailTimestamp(left)?.getTime() || 0;
|
||
const rightDate = emailTimestamp(right)?.getTime() || 0;
|
||
return rightDate - leftDate;
|
||
})
|
||
.slice(0, 4)
|
||
.map((email) => ({
|
||
id: email.id,
|
||
name: senderDisplayName(email),
|
||
age: formatRelativeAge(emailTimestamp(email)),
|
||
status: extractReplyStatus(emailSearchText(email)),
|
||
}));
|
||
|
||
const today = [];
|
||
emails.forEach((email) => {
|
||
const text = emailSearchText(email);
|
||
if (!/meet|meeting|call|interview|sync|calendar/.test(text)) return;
|
||
const time = extractMeetingTime(text);
|
||
if (!time) return;
|
||
const label = (email.subject || "Meeting").trim().slice(0, 42);
|
||
const venue = extractVenue(text);
|
||
const tone = /invoice|payment|bill|purchase order|\bpo\b/.test(text)
|
||
? "amber"
|
||
: /sync|project/.test(text)
|
||
? "rose"
|
||
: /client|demo/.test(text)
|
||
? "blue"
|
||
: "green";
|
||
if (!today.some((item) => item.time === time && item.label === label)) {
|
||
today.push({ time, label, venue, tone });
|
||
}
|
||
});
|
||
|
||
const deadlines = [];
|
||
emails.forEach((email) => {
|
||
const text = emailSearchText(email);
|
||
if (!/invoice|proposal|payment|due|deadline|bill/.test(text)) return;
|
||
const due = extractDeadline(text);
|
||
if (!due) return;
|
||
const label = (email.subject || "Deadline")
|
||
.split(/[-–:|]/)
|
||
.map((part) => part.trim())
|
||
.find(Boolean)
|
||
?.slice(0, 28) || "Deadline";
|
||
if (!deadlines.some((item) => item.label === label && item.due === due)) {
|
||
deadlines.push({ label, due });
|
||
}
|
||
});
|
||
|
||
return {
|
||
stats,
|
||
needsReply,
|
||
today: today.slice(0, 4),
|
||
deadlines: deadlines.slice(0, 4),
|
||
};
|
||
}
|
||
|
||
export function labelCounts(emails) {
|
||
const counts = {
|
||
Important: 0,
|
||
Projects: 0,
|
||
Clients: 0,
|
||
Finance: 0,
|
||
Personal: 0,
|
||
};
|
||
emails.forEach((email) => {
|
||
emailLabels(email).forEach((label) => {
|
||
counts[label.name] = (counts[label.name] || 0) + 1;
|
||
});
|
||
});
|
||
return counts;
|
||
}
|