258 lines
9.8 KiB
JavaScript
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);
|
|
}
|