From 2c7cac0777f0d90a5ed3e045e363c14f01c422b5 Mon Sep 17 00:00:00 2001 From: chitcommit <208086304+chitcommit@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:45:47 +0000 Subject: [PATCH] feat: inbound email handler for finance@chitty.cc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route emails to finance@chitty.cc through Cloudflare Email Routing to the chittyfinance Worker. Stores raw .eml in R2 at inbound-email/_.eml with metadata (from, to, subject, size). Indexes in KV for 90-day lookup. Dashboard action needed: create Email Routing rule for finance@chitty.cc → chittyfinance Worker under chitty.cc zone > Email > Email Routing. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/worker.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/server/worker.ts b/server/worker.ts index b63481a..e454cee 100755 --- a/server/worker.ts +++ b/server/worker.ts @@ -21,6 +21,47 @@ export default { if (!ok) console.warn('[cron:discovery] heartbeat failed'); })); }, + + async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) { + const from = message.from; + const to = message.to; + const subject = message.headers.get('subject') || '(no subject)'; + const messageId = message.headers.get('message-id') || `${Date.now()}`; + const size = message.rawSize; + + console.log(`[email:inbound] from=${from} to=${to} subject="${subject}" size=${size}`); + + // Store raw email in R2 for document ingestion pipeline + const ts = new Date().toISOString().replace(/[:.]/g, '-'); + const sanitizedId = messageId.replace(/[<>]/g, '').replace(/[^a-zA-Z0-9@._-]/g, '_'); + const key = `inbound-email/${ts}_${sanitizedId}.eml`; + + const rawBytes = await new Response(message.raw).arrayBuffer(); + await env.FINANCE_R2.put(key, rawBytes, { + customMetadata: { + from, + to, + subject, + messageId, + receivedAt: new Date().toISOString(), + sizeBytes: String(size), + }, + }); + + console.log(`[email:inbound] stored in R2: ${key} (${rawBytes.byteLength} bytes)`); + + // Index in KV for quick lookup + const kv = env.FINANCE_KV; + const indexEntry = JSON.stringify({ + key, + from, + to, + subject, + receivedAt: new Date().toISOString(), + sizeBytes: rawBytes.byteLength, + }); + await kv.put(`email:inbound:${ts}`, indexEntry, { expirationTtl: 86400 * 90 }); // 90 days + }, } satisfies ExportedHandler; // Re-export the Agent DO class so Wrangler can bind it