gmail-worker.jsā¢8.1 kB
// gmail-worker.js
// Gmail worker for fetching and classifying emails
import 'dotenv/config';
import fs from 'fs';
import { google } from 'googleapis';
import { nanoid } from 'nanoid';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import timezone from 'dayjs/plugin/timezone.js';
import * as chrono from 'chrono-node';
dayjs.extend(utc);
dayjs.extend(timezone);
const TZ = process.env.TIMEZONE || 'America/Detroit';
// Gmail OAuth setup
const oAuth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
process.env.GOOGLE_REDIRECT || 'http://localhost:3000/oauth2/callback'
);
// Load tokens from file
const TOKEN_PATH = './data/gmail_token.json';
if (fs.existsSync(TOKEN_PATH)) {
const tokens = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf8'));
oAuth2Client.setCredentials(tokens);
}
const gmail = google.gmail({ version: 'v1', auth: oAuth2Client });
// Query definitions
const QUERIES = {
hotschedules_12h: {
query: 'newer_than:12h (from:(*@hotschedules.com) OR subject:("Shift Notes" OR "called off" OR "no-show" OR "coverage needed"))',
window: 12,
type: '911'
},
brinker_24h: {
query: 'newer_than:24h from:(allen.woods@brinker.com OR *@brinker.com OR *@chilis.com OR c00605mgr@chilis.com) subject:(schedule OR due OR deadline OR submit OR report OR EOP OR Fusion OR Oracle)',
window: 24,
type: 'deliverable'
},
brinker_backstop: {
query: 'newer_than:7d from:(allen.woods@brinker.com OR *@brinker.com OR *@chilis.com OR c00605mgr@chilis.com) subject:(schedule OR due OR deadline OR submit OR report OR EOP OR Fusion OR Oracle)',
window: 168,
type: 'deliverable'
}
};
// Search messages
async function searchMessages(query, maxResults = 50) {
try {
const response = await gmail.users.messages.list({
userId: 'me',
q: query,
maxResults
});
return response.data.messages || [];
} catch (error) {
console.error('Gmail search error:', error.message);
return [];
}
}
// Get full message
async function getMessage(messageId) {
try {
const msg = await gmail.users.messages.get({
userId: 'me',
id: messageId,
format: 'full'
});
const payload = msg.data.payload || {};
const headers = payload.headers || [];
const getHeader = (name) => headers.find(h => h.name.toLowerCase() === name.toLowerCase())?.value || '';
let body = '';
const parts = [payload];
while (parts.length) {
const part = parts.shift();
if (part.parts) parts.push(...part.parts);
if (part.body?.data) {
body += Buffer.from(part.body.data, 'base64').toString('utf8');
}
}
return {
id: msg.data.id,
threadId: msg.data.threadId,
subject: getHeader('Subject'),
from: getHeader('From'),
date: getHeader('Date'),
body: body.substring(0, 5000),
snippet: msg.data.snippet,
link: `https://mail.google.com/mail/u/0/#inbox/${msg.data.threadId}`
};
} catch (error) {
console.error('Error getting message:', error.message);
return null;
}
}
// Classify message
function classifyMessage(message) {
if (!message) return null;
const text = `${message.subject} ${message.body}`.toLowerCase();
const is911 = /\b(call[\s-]?off|called out|no[\s-]?show|coverage needed|left early)\b/.test(text);
const isDeliverable = /\b(due|deadline|submit|by friday|eop|end of period|schedule ready)\b/.test(text);
let dueDate = null;
try {
const parsed = chrono.parseDate(text);
if (parsed) {
dueDate = dayjs(parsed).tz(TZ).format('YYYY-MM-DD');
}
} catch (e) {
// Date parsing failed
}
return {
id: nanoid(),
messageId: message.id,
threadId: message.threadId,
type: is911 ? '911' : (isDeliverable ? 'deliverable' : 'info'),
priority: is911 ? 'high' : 'normal',
subject: message.subject,
from: message.from,
date: message.date,
dueDate: dueDate,
snippet: message.snippet,
link: message.link,
status: 'open',
created: new Date().toISOString()
};
}
// Build report
function buildReport(results) {
const lines = [];
lines.push('# Talked to Gmail\n');
lines.push(`Generated: ${dayjs().tz(TZ).format('MMM D, YYYY h:mm A z')}\n`);
if (results.hs911.length > 0) {
lines.push('## šØ 911 - HotSchedules Alerts');
results.hs911.forEach(item => {
lines.push(`- **${item.subject}**`);
lines.push(` From: ${item.from}`);
lines.push(` [Open in Gmail](${item.link})`);
});
} else {
lines.push('## ā
No 911 Issues');
lines.push('No call-offs or coverage issues in the last 12 hours');
}
lines.push('');
if (results.deliverables.length > 0) {
lines.push('## š
Deadlines & Deliverables');
results.deliverables.forEach(item => {
const due = item.dueDate ? `Due: ${dayjs(item.dueDate).format('MMM D')}` : '';
lines.push(`- **${item.subject}** ${due}`);
lines.push(` From: ${item.from}`);
lines.push(` [Open in Gmail](${item.link})`);
});
} else {
lines.push('## No New Deadlines');
}
lines.push('');
lines.push(`**Summary:** ${results.hs911.length} urgent, ${results.deliverables.length} deadlines, ${results.all.length} total`);
return lines.join('\n');
}
// Main worker
async function runGmailWorker() {
console.log('š Starting Gmail worker...');
console.log(` Timezone: ${TZ}`);
console.log(` Store: ${process.env.STORE_NAME || 'Not configured'}`);
const results = {
hs911: [],
deliverables: [],
all: []
};
for (const [key, config] of Object.entries(QUERIES)) {
console.log(`\nš§ Running query: ${key}`);
const messages = await searchMessages(config.query);
console.log(` Found ${messages.length} messages`);
for (const msg of messages) {
const fullMessage = await getMessage(msg.id);
if (fullMessage) {
const classified = classifyMessage(fullMessage);
if (classified) {
results.all.push(classified);
if (classified.type === '911') {
results.hs911.push(classified);
} else if (classified.type === 'deliverable') {
results.deliverables.push(classified);
}
}
}
}
}
console.log(`\nš Results:`);
console.log(` 911 items: ${results.hs911.length}`);
console.log(` Deliverables: ${results.deliverables.length}`);
console.log(` Total messages: ${results.all.length}`);
const report = buildReport(results);
// Save results
const payload = {
ok: true,
report: report,
counts: {
hs: results.all.filter(r => r.from?.includes('hotschedules')).length,
brinker: results.all.filter(r => r.from?.includes('brinker')).length,
hs911: results.hs911.length,
deliverables: results.deliverables.length
},
tasks: results.all,
timestamp: new Date().toISOString()
};
if (!fs.existsSync('./data')) {
fs.mkdirSync('./data', { recursive: true });
}
const dataPath = './data/latest_report.json';
fs.writeFileSync(dataPath, JSON.stringify(payload, null, 2));
console.log('ā
Saved for MCP pickup');
if (!fs.existsSync('./data/reports')) {
fs.mkdirSync('./data/reports', { recursive: true });
}
const localPath = './data/reports/' + dayjs().format('YYYY-MM-DD-HHmmss') + '.json';
fs.writeFileSync(localPath, JSON.stringify(payload, null, 2));
console.log('ā
Saved report:', localPath);
return { report, results };
}
// Run
console.log('Gmail Worker - Standalone Mode');
runGmailWorker()
.then(result => {
console.log('\nš Report Preview:');
console.log('================');
console.log(result.report);
console.log('\nā
Gmail worker completed successfully');
process.exit(0);
})
.catch(error => {
console.error('\nā Error:', error.message);
if (error.message.includes('invalid_grant')) {
console.error('Token expired. Please run: npm run setup');
}
process.exit(1);
});