feat: add Gmail Pub/Sub watcher for inbound email automation

New ChannelAdapter that monitors Gmail via Google Cloud Pub/Sub push
notifications with polling fallback. Supports OAuth2 auth, configurable
watch labels, template rendering with email metadata placeholders
(from, to, subject, snippet, date, id, labels).

Wired into daemon lifecycle and gateway (POST /gmail/push endpoint).
Includes 16 tests covering auth, templates, push notifications, and
channel routing.
This commit is contained in:
William Valentin
2026-02-07 15:39:24 -08:00
parent 131d23989c
commit 06438bb44f
8 changed files with 1008 additions and 1 deletions
+37
View File
@@ -25,6 +25,7 @@ import type { Config } from '../config/index.js';
import type { ToolRegistry } from '../tools/registry.js';
import type { ToolExecutor } from '../tools/executor.js';
import type { WebhookHandler } from '../automation/webhooks.js';
import type { GmailWatcher } from '../automation/gmail.js';
export interface GatewayServerConfig {
port: number;
@@ -45,6 +46,8 @@ export interface GatewayServerConfig {
channelRegistry?: { list(): Array<{ readonly name: string; readonly status: string }> };
/** Optional webhook handler for inbound webhook HTTP routes. */
webhookHandler?: WebhookHandler;
/** Optional Gmail handler for Pub/Sub push notifications. */
gmailHandler?: GmailWatcher;
}
export class GatewayServer {
@@ -223,6 +226,25 @@ export class GatewayServer {
}
}
// Gmail Pub/Sub push route — bypass gateway auth (Google sends push notifications directly)
if (this.config.gmailHandler && req.method === 'POST' && req.url?.startsWith('/gmail/push')) {
try {
const body = await this.readRequestBody(req);
const parsed = JSON.parse(body) as { message?: { data?: string } };
const data = parsed?.message?.data;
if (data) {
await this.config.gmailHandler.handlePushNotification(data);
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
} catch (err) {
console.error('Gmail push handler error:', err instanceof Error ? err.message : err);
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid request' }));
}
return;
}
// Apply auth to HTTP requests when configured
const authConfig = this.config.auth ?? {};
if (this.config.authHttp !== false && authConfig.token) {
@@ -299,4 +321,19 @@ export class GatewayServer {
setWebhookHandler(handler: WebhookHandler): void {
this.config.webhookHandler = handler;
}
/** Set the Gmail handler for Pub/Sub push notifications (late binding). */
setGmailHandler(handler: GmailWatcher): void {
this.config.gmailHandler = handler;
}
/** Read the full request body as a string. */
private readRequestBody(req: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
req.on('data', (chunk: Buffer) => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
req.on('error', reject);
});
}
}