Add multiple account support to gmail-agent

This commit is contained in:
Antigravity 2026-06-13 16:04:03 -04:00
parent 4fbfdf13e7
commit 91d19afa7b
3 changed files with 76 additions and 44 deletions

View File

@ -50,9 +50,8 @@ spec:
mountPath: /app/gmail-agent/credentials.json
subPath: credentials.json
readOnly: true
- name: token-volume
mountPath: /app/gmail-agent/token.json
subPath: token.json
- name: tokens-volume
mountPath: /app/gmail-agent/tokens
readOnly: true
resources:
requests:
@ -70,9 +69,6 @@ spec:
items:
- key: credentials.json
path: credentials.json
- name: token-volume
- name: tokens-volume
secret:
secretName: gmail-agent-auth
items:
- key: token.json
path: token.json
secretName: gmail-agent-tokens

View File

@ -1,10 +1,24 @@
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const { google } = require('googleapis');
// If modifying these scopes, delete token.json.
const SCOPES = ['https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/gmail.modify'];
const TOKEN_PATH = 'token.json';
const accountName = process.argv[2];
if (!accountName) {
console.log("Usage: node auth.js <account-name>");
console.log("Example: node auth.js work");
process.exit(1);
}
const TOKENS_DIR = path.join(__dirname, 'tokens');
if (!fs.existsSync(TOKENS_DIR)) {
fs.mkdirSync(TOKENS_DIR);
}
const TOKEN_PATH = path.join(TOKENS_DIR, `${accountName}.json`);
fs.readFile('credentials.json', (err, content) => {
if (err) return console.log('Error loading credentials.json file. Make sure you downloaded it from Google Cloud Console:', err);
@ -18,7 +32,7 @@ function authorize(credentials, callback) {
fs.readFile(TOKEN_PATH, (err, token) => {
if (err) return callback(oAuth2Client);
oAuth2Client.setCredentials(JSON.parse(token));
console.log("Token already exists and is valid. You don't need to authenticate again.");
console.log(`Token for account '${accountName}' already exists and is valid. You don't need to authenticate again.`);
});
}
@ -27,19 +41,20 @@ function getNewToken(oAuth2Client) {
access_type: 'offline',
scope: SCOPES,
});
console.log('Authorize this app by visiting this url:', authUrl);
console.log(`\n=== Authorizing Account: ${accountName} ===\n`);
console.log('Authorize this app by visiting this url:\n', authUrl);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question('Enter the code from that page here: ', (code) => {
rl.question('\nEnter the code from that page here: ', (code) => {
rl.close();
oAuth2Client.getToken(code, (err, token) => {
if (err) return console.error('Error retrieving access token', err);
oAuth2Client.setCredentials(token);
fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
if (err) return console.error(err);
console.log('Token stored to', TOKEN_PATH);
console.log(`Token stored successfully to ${TOKEN_PATH}`);
});
});
});

View File

@ -5,7 +5,7 @@ const cron = require('node-cron');
const axios = require('axios');
const { google } = require('googleapis');
const TOKEN_PATH = path.join(__dirname, 'token.json');
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';
@ -13,25 +13,43 @@ const WHATSAPP_GATEWAY_URL = process.env.WHATSAPP_GATEWAY_URL || 'http://whatsap
const WHATSAPP_TO = process.env.WHATSAPP_TO || '+14085505485';
const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'qwen2.5:0.5b';
let oAuth2Client;
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(TOKEN_PATH)) {
console.error('Missing token.json. Please run "node auth.js" to generate it.');
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;
oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0] || 'urn:ietf:wg:oauth:2.0:oob');
oAuth2Client.setCredentials(JSON.parse(fs.readFileSync(TOKEN_PATH)));
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() {
const gmail = google.gmail({ version: 'v1', auth: oAuth2Client });
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',
@ -57,16 +75,16 @@ async function getUnreadEmails() {
body = Buffer.from(fullMsg.data.payload.body.data, 'base64').toString('utf8');
}
emails.push({ id: msg.id, subject, from, body: body.substring(0, 1000) });
emails.push({ id: msg.id, subject, from, body: body.substring(0, 1000), accountName: account.name, client: account.client });
}
return emails;
}
async function markAsRead(msgId) {
const gmail = google.gmail({ version: 'v1', auth: oAuth2Client });
async function markAsRead(email) {
const gmail = google.gmail({ version: 'v1', auth: email.client });
await gmail.users.messages.modify({
userId: 'me',
id: msgId,
id: email.id,
requestBody: { removeLabelIds: ['UNREAD'] }
});
}
@ -108,7 +126,7 @@ SUMMARY: <your summary if yes, otherwise leave blank>
}
async function sendWhatsAppNotification(email, summary) {
const message = `📧 *Important Email*\n*From:* ${email.from}\n*Subject:* ${email.subject}\n\n🤖 *Summary:* ${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, {
@ -117,32 +135,35 @@ async function sendWhatsAppNotification(email, summary) {
}, {
headers: { 'x-app-name': 'Gmail Agent' }
});
console.log(`Notification sent for: ${email.subject}`);
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...`);
console.log(`[${new Date().toISOString()}] Checking for new emails across ${accounts.length} accounts...`);
try {
const emails = await getUnreadEmails();
if (emails.length === 0) {
console.log('No new emails.');
return;
}
console.log(`Found ${emails.length} unread emails.`);
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}`);
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);
}
// Mark as read so we don't process it again (or add a label, but reading it is safest to prevent loops)
await markAsRead(email.id);
}
} catch (err) {
console.error('Error processing emails:', err.message);