agentic-os/gmail-agent/index.js

258 lines
9.8 KiB
JavaScript

require('dotenv').config();
const fs = require('fs');
const path = require('path');
const cron = require('node-cron');
const axios = require('axios');
const { google } = require('googleapis');
const TOKENS_DIR = path.join(__dirname, 'tokens');
const CREDENTIALS_PATH = path.join(__dirname, 'credentials.json');
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 || '+14085505485';
const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'qwen2.5:0.5b';
let baseOAuth2Client;
let accounts = [];
function loadAuth() {
if (!fs.existsSync(CREDENTIALS_PATH)) {
console.error('Missing credentials.json. Please download it from Google Cloud Console and place it in the project directory.');
process.exit(1);
}
if (!fs.existsSync(TOKENS_DIR)) {
console.error('Missing tokens directory. Please run "node auth.js <account-name>" to generate an account token.');
process.exit(1);
}
const creds = JSON.parse(fs.readFileSync(CREDENTIALS_PATH));
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.error('No tokens found in tokens/. Please run "node auth.js <account-name>" to generate one.');
process.exit(1);
}
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}`);
}
}
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',
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 markAsRead(email) {
const gmail = google.gmail({ version: 'v1', auth: email.client });
await gmail.users.messages.modify({
userId: 'me',
id: email.id,
requestBody: { removeLabelIds: ['UNREAD'] }
});
}
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.
Newsletters, marketing, promotions, and generic updates are NOT important.
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 axios.post(OLLAMA_URL, {
model: OLLAMA_MODEL,
prompt: prompt,
stream: false
});
const responseText = res.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 axios.post(WHATSAPP_GATEWAY_URL, {
to: WHATSAPP_TO,
message: message
}, {
headers: { 'x-app-name': 'Gmail Agent' }
});
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}...`);
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 read so we don't process it again
await markAsRead(email);
}
}
} 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 = JSON.parse(fs.readFileSync(CREDENTIALS_PATH));
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',
scope: SCOPES,
});
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 = JSON.parse(fs.readFileSync(CREDENTIALS_PATH));
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, () => {
console.log(`Express server listening on port ${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);
}