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 || '+14085505485'; 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.'); } try { 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 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: `; 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: WHATSAPP_TO, 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}...`); 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 = 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', 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 = 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, () => { 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); }