agentic-os/gmail-agent/index.js

308 lines
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

require('dotenv').config();
const fs = require('fs');
const path = require('path');
const cron = require('node-cron');
const { google } = require('googleapis');
const TOKENS_DIR = path.join(__dirname, 'tokens');
const CREDENTIALS_PATH = path.join(__dirname, 'credentials.json');
function getCredentials() {
if (!fs.existsSync(CREDENTIALS_PATH) || fs.statSync(CREDENTIALS_PATH).isDirectory()) {
throw new Error('Google OAuth credentials.json is missing. Please create the gmail-agent-auth Kubernetes Secret with your Google Cloud OAuth credentials.');
}
return JSON.parse(fs.readFileSync(CREDENTIALS_PATH));
}
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://ollama.ai-agents.svc.cluster.local:11434/api/generate';
const WHATSAPP_GATEWAY_URL = process.env.WHATSAPP_GATEWAY_URL || 'http://whatsapp-gateway.ai-agents.svc.cluster.local:5001/api/send-message';
const WHATSAPP_TO = process.env.WHATSAPP_TO;
const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'qwen2.5:0.5b';
let baseOAuth2Client;
let accounts = [];
function loadAuth() {
try {
if (!fs.existsSync(CREDENTIALS_PATH) || fs.statSync(CREDENTIALS_PATH).isDirectory()) {
console.warn('⚠️ Missing credentials.json. Gmail Agent will not be able to process emails or authenticate new accounts until this file is provided.');
return;
}
if (!fs.existsSync(TOKENS_DIR)) {
fs.mkdirSync(TOKENS_DIR, { recursive: true });
console.log('Created empty tokens directory.');
}
const creds = getCredentials();
const {client_secret, client_id, redirect_uris} = creds.installed || creds.web;
baseOAuth2Client = { client_id, client_secret, redirect_uri: redirect_uris[0] || 'urn:ietf:wg:oauth:2.0:oob' };
const tokenFiles = fs.readdirSync(TOKENS_DIR).filter(file => file.endsWith('.json'));
if (tokenFiles.length === 0) {
console.log(' No token files found in tokens/. Waiting for accounts to be added via UI.');
return;
}
for (const file of tokenFiles) {
const accountName = file.replace('.json', '');
const tokenContent = JSON.parse(fs.readFileSync(path.join(TOKENS_DIR, file)));
const client = new google.auth.OAuth2(baseOAuth2Client.client_id, baseOAuth2Client.client_secret, baseOAuth2Client.redirect_uri);
client.setCredentials(tokenContent);
accounts.push({ name: accountName, client });
console.log(`✅ Loaded credentials for account: ${accountName}`);
}
} catch (err) {
console.error('❌ Error loading auth files:', err.message);
}
}
async function ensureAgentReadLabel(account) {
const gmail = google.gmail({ version: 'v1', auth: account.client });
const res = await gmail.users.labels.list({ userId: 'me' });
const labels = res.data.labels || [];
let agentReadLabel = labels.find(l => l.name === 'AGENT_READ');
if (!agentReadLabel) {
console.log(`Creating AGENT_READ label for account ${account.name}...`);
const createRes = await gmail.users.labels.create({
userId: 'me',
requestBody: {
name: 'AGENT_READ',
labelListVisibility: 'labelShow',
messageListVisibility: 'show'
}
});
agentReadLabel = createRes.data;
}
return agentReadLabel.id;
}
async function getUnreadEmails(account) {
const gmail = google.gmail({ version: 'v1', auth: account.client });
const res = await gmail.users.messages.list({
userId: 'me',
q: 'is:unread -label:AGENT_READ',
maxResults: 10
});
const messages = res.data.messages || [];
const emails = [];
for (const msg of messages) {
const fullMsg = await gmail.users.messages.get({ userId: 'me', id: msg.id, format: 'full' });
const headers = fullMsg.data.payload.headers;
const subject = headers.find(h => h.name === 'Subject')?.value || 'No Subject';
const from = headers.find(h => h.name === 'From')?.value || 'Unknown';
let body = '';
if (fullMsg.data.payload.parts) {
const part = fullMsg.data.payload.parts.find(p => p.mimeType === 'text/plain');
if (part && part.body.data) {
body = Buffer.from(part.body.data, 'base64').toString('utf8');
}
} else if (fullMsg.data.payload.body && fullMsg.data.payload.body.data) {
body = Buffer.from(fullMsg.data.payload.body.data, 'base64').toString('utf8');
}
emails.push({ id: msg.id, subject, from, body: body.substring(0, 1000), accountName: account.name, client: account.client });
}
return emails;
}
async function markAsAgentRead(email, labelId) {
const gmail = google.gmail({ version: 'v1', auth: email.client });
await gmail.users.messages.modify({
userId: 'me',
id: email.id,
requestBody: { addLabelIds: [labelId] }
});
}
async function analyzeWithOllama(email) {
const prompt = `You are an AI assistant that determines if an email is important enough to notify the user immediately on WhatsApp.
CRITICAL INSTRUCTION: You MUST strictly IGNORE all promotional emails, newsletters, marketing, discounts, product updates, or any emails of low significance.
For these, you MUST output IMPORTANT: NO.
Important things: Bills due, flight/travel updates, urgent requests from people, security alerts, direct personal messages.
Email Subject: ${email.subject}
Email From: ${email.from}
Email Body Preview:
${email.body}
First, determine if this is important (YES or NO).
If YES, provide a 1-sentence summary of what the user needs to know.
Output format exactly like this:
IMPORTANT: YES/NO
SUMMARY: <your summary if yes, otherwise leave blank>
`;
try {
const res = await fetch(OLLAMA_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: OLLAMA_MODEL,
prompt: prompt,
stream: false
})
});
const data = await res.json();
const responseText = data.response || '';
const isImportant = responseText.includes('IMPORTANT: YES');
const summaryMatch = responseText.match(/SUMMARY:\s*(.*)/);
const summary = summaryMatch ? summaryMatch[1].trim() : '';
return { isImportant, summary };
} catch (err) {
console.error('Ollama Error:', err.message);
return { isImportant: false, summary: '' };
}
}
async function sendWhatsAppNotification(email, summary) {
const message = `📧 *[${email.accountName}] Important Email*\n*From:* ${email.from}\n*Subject:* ${email.subject}\n\n🤖 *Summary:* ${summary}`;
try {
await fetch(WHATSAPP_GATEWAY_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-app-name': 'Gmail Agent'
},
body: JSON.stringify({
to: 'default',
message: message
})
});
console.log(`Notification sent for: [${email.accountName}] ${email.subject}`);
} catch (err) {
console.error('WhatsApp Gateway Error:', err.message);
}
}
async function processEmails() {
console.log(`[${new Date().toISOString()}] Checking for new emails across ${accounts.length} accounts...`);
try {
for (const account of accounts) {
console.log(`Checking account: ${account.name}...`);
if (!account.agentReadLabelId) {
account.agentReadLabelId = await ensureAgentReadLabel(account);
}
const emails = await getUnreadEmails(account);
if (emails.length === 0) {
console.log(`No new emails for ${account.name}.`);
continue;
}
console.log(`Found ${emails.length} unread emails for ${account.name}.`);
for (const email of emails) {
const analysis = await analyzeWithOllama(email);
if (analysis.isImportant) {
await sendWhatsAppNotification(email, analysis.summary);
} else {
console.log(`Skipped non-important email: ${email.subject}`);
}
// Mark as processed using our custom label
await markAsAgentRead(email, account.agentReadLabelId);
}
}
} catch (err) {
console.error('Error processing emails:', err.message);
}
}
// ─── Express Server for UI Integration ─────────────────────────────────────────
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
// Get all connected accounts
app.get('/api/accounts', (req, res) => {
res.json({
success: true,
accounts: accounts.map(a => ({ name: a.name }))
});
});
// Generate Auth URL for a new account
app.post('/api/auth/url', (req, res) => {
const { accountName } = req.body;
if (!accountName) return res.status(400).json({ success: false, error: 'accountName is required' });
try {
const creds = getCredentials();
const {client_secret, client_id, redirect_uris} = creds.installed || creds.web;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0] || 'urn:ietf:wg:oauth:2.0:oob');
const SCOPES = ['https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/gmail.modify'];
const authUrl = oAuth2Client.generateAuthUrl({
access_type: 'offline',
prompt: 'consent',
scope: SCOPES,
state: accountName // pass the accountName in state so we don't lose it across redirects
});
res.json({ success: true, url: authUrl });
} catch (e) {
res.status(500).json({ success: false, error: e.message });
}
});
// Verify token and hot-reload account
app.post('/api/auth/token', (req, res) => {
const { accountName, code } = req.body;
if (!accountName || !code) return res.status(400).json({ success: false, error: 'accountName and code are required' });
try {
const creds = getCredentials();
const {client_secret, client_id, redirect_uris} = creds.installed || creds.web;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0] || 'urn:ietf:wg:oauth:2.0:oob');
oAuth2Client.getToken(code, (err, token) => {
if (err) return res.status(400).json({ success: false, error: err.message });
const tokenPath = path.join(TOKENS_DIR, `${accountName}.json`);
fs.writeFileSync(tokenPath, JSON.stringify(token));
// Reload accounts list
accounts = [];
loadAuth();
res.json({ success: true, message: `Account ${accountName} added successfully!` });
});
} catch (e) {
res.status(500).json({ success: false, error: e.message });
}
});
const PORT = process.env.PORT || 5002;
app.listen(PORT, '0.0.0.0', () => {
console.log(`Express server listening on 0.0.0.0:${PORT}`);
});
// Start Application
try {
loadAuth();
console.log('Gmail Agent started successfully.');
console.log(`Cron job scheduled to check every 15 minutes.`);
// Check immediately on startup
processEmails();
// Schedule every 15 minutes
cron.schedule('*/15 * * * *', () => {
processEmails();
});
} catch (e) {
console.error('Failed to start:', e.message);
}