#!/usr/bin/env node /* Google Workspace helper CLI Commands: whoami send --to --subject --body [--html] search-mail --query [--max 10] search-calendar --query [--max 10] [--timeMin ISO] [--timeMax ISO] [--calendar primary] create-event --summary --start --end [--timeZone America/Chicago] [--description ] [--location ] [--calendar primary] */ const fs = require('fs'); const path = require('path'); const { google } = require('googleapis'); const DEFAULT_SUBJECT = process.env.GW_IMPERSONATE || 'stefano@fiorinis.com'; const DEFAULT_KEY_CANDIDATES = [ process.env.GW_KEY_PATH, path.join(process.env.HOME || '', '.openclaw/workspace/.clawdbot/credentials/google-workspace/service-account.json'), path.join(process.env.HOME || '', '.clawdbot/credentials/google-workspace/service-account.json'), ].filter(Boolean); const SCOPES = [ 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/gmail.compose', 'https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/calendar', 'https://www.googleapis.com/auth/calendar.events', ]; function resolveKeyPath() { for (const p of DEFAULT_KEY_CANDIDATES) { if (p && fs.existsSync(p)) return p; } return null; } function parseArgs(argv) { const out = { _: [] }; for (let i = 0; i < argv.length; i++) { const token = argv[i]; if (token.startsWith('--')) { const key = token.slice(2); const next = argv[i + 1]; if (!next || next.startsWith('--')) { out[key] = true; } else { out[key] = next; i++; } } else { out._.push(token); } } return out; } function usage() { console.log(`Google Workspace CLI\n Env (optional): GW_IMPERSONATE user to impersonate (default: ${DEFAULT_SUBJECT}) GW_KEY_PATH service-account key path Commands: whoami send --to --subject --body [--html] search-mail --query [--max 10] search-calendar --query [--max 10] [--timeMin ISO] [--timeMax ISO] [--calendar primary] create-event --summary --start --end [--timeZone America/Chicago] [--description ] [--location ] [--calendar primary] `); } function assertRequired(opts, required) { const missing = required.filter((k) => !opts[k]); if (missing.length) { throw new Error(`Missing required options: ${missing.map((m) => `--${m}`).join(', ')}`); } } function makeRawEmail({ from, to, subject, body, isHtml = false }) { const contentType = isHtml ? 'text/html; charset="UTF-8"' : 'text/plain; charset="UTF-8"'; const msg = [ `From: ${from}`, `To: ${to}`, `Subject: ${subject}`, 'MIME-Version: 1.0', `Content-Type: ${contentType}`, '', body, ].join('\r\n'); return Buffer.from(msg) .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/g, ''); } async function getClients() { const keyPath = resolveKeyPath(); if (!keyPath) { throw new Error('Service account key not found. Set GW_KEY_PATH or place the file in ~/.openclaw/workspace/.clawdbot/credentials/google-workspace/service-account.json'); } const auth = new google.auth.GoogleAuth({ keyFile: keyPath, scopes: SCOPES, clientOptions: { subject: DEFAULT_SUBJECT }, }); const authClient = await auth.getClient(); const gmail = google.gmail({ version: 'v1', auth: authClient }); const calendar = google.calendar({ version: 'v3', auth: authClient }); return { gmail, calendar, keyPath }; } async function cmdWhoami(clients) { const profile = await clients.gmail.users.getProfile({ userId: 'me' }); console.log(JSON.stringify({ impersonating: DEFAULT_SUBJECT, keyPath: clients.keyPath, profile: profile.data, }, null, 2)); } async function cmdSend(clients, opts) { assertRequired(opts, ['to', 'subject', 'body']); const raw = makeRawEmail({ from: DEFAULT_SUBJECT, to: opts.to, subject: opts.subject, body: opts.body, isHtml: !!opts.html, }); const res = await clients.gmail.users.messages.send({ userId: 'me', requestBody: { raw }, }); console.log(JSON.stringify({ ok: true, id: res.data.id, threadId: res.data.threadId }, null, 2)); } async function cmdSearchMail(clients, opts) { assertRequired(opts, ['query']); const maxResults = Math.max(1, Math.min(50, Number(opts.max || 10))); const list = await clients.gmail.users.messages.list({ userId: 'me', q: opts.query, maxResults, }); const ids = (list.data.messages || []).map((m) => m.id).filter(Boolean); const out = []; for (const id of ids) { const msg = await clients.gmail.users.messages.get({ userId: 'me', id, format: 'metadata', metadataHeaders: ['From', 'To', 'Subject', 'Date'], }); const headers = Object.fromEntries((msg.data.payload?.headers || []).map((h) => [h.name, h.value])); out.push({ id, threadId: msg.data.threadId, snippet: msg.data.snippet, from: headers.From, to: headers.To, subject: headers.Subject, date: headers.Date, }); } console.log(JSON.stringify({ count: out.length, messages: out }, null, 2)); } async function cmdSearchCalendar(clients, opts) { const maxResults = Math.max(1, Math.min(50, Number(opts.max || 10))); const res = await clients.calendar.events.list({ calendarId: opts.calendar || 'primary', q: opts.query || undefined, timeMin: opts.timeMin, timeMax: opts.timeMax, singleEvents: true, orderBy: 'startTime', maxResults, }); const events = (res.data.items || []).map((e) => ({ id: e.id, summary: e.summary, status: e.status, start: e.start, end: e.end, location: e.location, hangoutLink: e.hangoutLink, })); console.log(JSON.stringify({ count: events.length, events }, null, 2)); } async function cmdCreateEvent(clients, opts) { assertRequired(opts, ['summary', 'start', 'end']); const tz = opts.timeZone || 'America/Chicago'; const event = { summary: opts.summary, description: opts.description, location: opts.location, start: { dateTime: opts.start, timeZone: tz }, end: { dateTime: opts.end, timeZone: tz }, }; const res = await clients.calendar.events.insert({ calendarId: opts.calendar || 'primary', requestBody: event, }); console.log(JSON.stringify({ ok: true, id: res.data.id, htmlLink: res.data.htmlLink }, null, 2)); } (async function main() { try { const args = parseArgs(process.argv.slice(2)); const cmd = args._[0]; if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') { usage(); process.exit(0); } const clients = await getClients(); if (cmd === 'whoami') return await cmdWhoami(clients); if (cmd === 'send') return await cmdSend(clients, args); if (cmd === 'search-mail') return await cmdSearchMail(clients, args); if (cmd === 'search-calendar') return await cmdSearchCalendar(clients, args); if (cmd === 'create-event') return await cmdCreateEvent(clients, args); throw new Error(`Unknown command: ${cmd}`); } catch (err) { console.error(`ERROR: ${err.message}`); process.exit(1); } })();