src/index.ts— Cloudflare Worker entrypoint: HTTP auth gate, REST API (/api/mails,/api/mails/:id), email handler, AI extraction, D1 writes, promotional filter.src/rpcEmail.ts— RPC-compatibleForwardableEmailMessagewrapper.src/index.html— Legacy fallback UI (server-rendered table). Kept as fallback whenASSETSbinding is absent. Do not delete.web/— React 18 + Vite + Tailwind + shadcn/ui frontend. Built toweb/dist, served via CloudflareASSETSbinding.db/schema.sql— D1 schema:raw_mails(every incoming email) +code_mails(AI-extracted codes only).
# Install
pnpm install # root (Worker) deps
# Dev
pnpm run dev # wrangler dev — local Worker on :8787
pnpm run dev:remote # wrangler dev --remote — use live D1
pnpm -C web run dev # Vite dev server on :5173 (proxies /api → :8787)
# Build & deploy
pnpm run build:web # vite build → web/dist
pnpm run deploy # build:web + wrangler deploy
# Misc
pnpm run test # vitest with @cloudflare/vitest-pool-workers
pnpm run cf-typegen # regenerate Worker type bindingsLocal setup:
cp wrangler.toml.example wrangler.toml # fill in database_id + secrets
pnpm wrangler d1 execute inbox-d1 --local --file=db/schema.sql
pnpm run build:web
pnpm run dev- Promotional filter runs before LLM.
isPromotionalEmail()checksList-Unsubscribe,List-ID,Precedence: bulk/list, and known ESP X-headers. If matched, raw email is still saved toraw_mailsbut LLM is skipped. - Two DB tables, strict separation. Every email →
raw_mails. Only emails with extracted codes/links →code_mails. Never skipraw_mailsinsert. src/index.htmlis a fallback, not dead code. The Worker servesASSETSfirst; if noASSETSbinding, falls back to the old HTML template renderer.web/distis gitignored. Built at deploy time viapnpm run deploy. No need to commit build artifacts.ASSETSbinding name must beASSETS(matched insrc/index.tsasenv.ASSETS).- Base64 MIME parts are decoded with
atob()inextractMailBodies(). Cloudflare Workers runtime supportsatob/btoa.
The AI layer uses a single unified function callProvider(config: ProviderConfig, prompt) in src/index.ts. All provider-specific logic is contained there — do not add new per-provider methods.
Env vars (set in wrangler.toml [vars] or as Cloudflare Secrets):
| Variable | Required | Description |
|---|---|---|
AI_BASE_URL |
✅ | Provider base URL, no trailing slash |
AI_API_KEY |
✅ | API key (use Secret in production) |
AI_API_FORMAT |
✅ | openai | responses | anthropic |
AI_MODEL |
✅ | Model ID |
AI_FALLBACK_BASE_URL |
optional | Fallback provider base URL |
AI_FALLBACK_API_KEY |
optional | Fallback API key |
AI_FALLBACK_API_FORMAT |
optional | Fallback format |
AI_FALLBACK_MODEL |
optional | Fallback model ID |
Fallback is only active when all four AI_FALLBACK_* vars are set. Primary retries 3× before fallback is attempted (1×).
Format → endpoint mapping:
openai→POST /v1/chat/completions(OpenAI, Gemini OpenAI-compat, DeepSeek, Groq, …)responses→POST /v1/responses(OpenAI Responses API)anthropic→POST /v1/messages(Anthropic Claude direct; sendsanthropic-version: 2023-06-01,max_tokens: 1024)
- Tabs, LF, UTF-8, no trailing whitespace (see
.editorconfig). - Prettier: single quotes, semicolons, print width 140.
PascalCaseclasses/types,camelCasefunctions/variables,UPPER_SNAKE_CASEconstants.
- Never commit
wrangler.tomlwith real secrets. web/src/App.tsxuses DOMPurify + sandboxed iframe for email HTML preview. Do not relax sandbox.- Basic Auth gate is enforced at the top of
WorkerEntrypoint.fetch()before any API route.
Conventional Commits: feat:, fix:, docs:, chore:. Keep commits atomic.