Forwarded mails (e.g. Yandex webmail) arrive as a single text/html part
with no <html>/<body> wrapper and no text/plain alternative. Two bugs made
their codes unextractable:
- stripHtmlTags dropped href attributes, so button-only mails (Netflix
'Get Code') reached the LLM with no URL at all → codeExist=0.
Anchors now render as 'label [url]' with & decoded.
- extractMailBodies only recognised single-part HTML wrapped in
<html>/<body>, so htmlBody was null and the Rendered preview was empty.
A bare fragment with Content-Type text/html now counts as the html body.
Verified against the real forwarded Netflix mail from production raw_mails:
content now carries the travel/verify link and classifies 5/5 as
login_code.
- From line now renders the org name followed by <from_addr> when both exist.
- On mobile (<lg) the inbox is a master-detail flow: tapping a row opens
the detail full-screen (scrolled to top) with a back button, instead of
stacking both cards and forcing a scroll past 20 rows to reach the code.
Desktop two-column layout is unchanged.
Gmail/Yahoo bulk-sender rules (2024) force large senders (Netflix via SES,
etc.) to add List-Unsubscribe to verification emails too, so it can no
longer be an unconditional bulk signal — real access-code mail was being
filtered before the LLM and never reached code_mails.
List-ID/List-Post, Precedence bulk/list, and ESP campaign X-headers remain
unconditional. List-Unsubscribe alone is now exempted when the decoded
subject looks transactional (code/verify/OTP/sign-in/reset/验证码/…);
genuine ads that slip through still get codeExist=0 from the classifier.
- Wrap the Worker with @cloudflare/workers-oauth-provider: /oauth/token,
/oauth/register (dynamic client registration), and /.well-known/* are
implemented by the library; tokens/grants/clients live in OAUTH_KV.
- New consent page at /oauth/authorize reuses the session cookie;
unauthenticated visitors bounce through the SPA login via returnTo.
- Grant props store only userId; oauthPropsAuth re-fetches the user from
D1 on every MCP request, so deleting a user revokes access instantly.
Permissions still flow exclusively through visibleMails().
- OAuth is optional: without the OAUTH_KV binding the provider is skipped
entirely and /mcp keeps working with aik_ API keys only. With it, both
credentials coexist (Bearer aik_ requests pre-route around the provider).
- Docs and wrangler examples updated; KV block commented out by default.
- src/mcp.ts: unused v1 JSON-RPC MCP implementation (claimed Basic Auth,
old tool names); the live server is src/mcp/server.ts
- test/mcp.test.ts: only tested the dead file
- db/schema.sql: superseded by migrations/
- Replace server-rendered HTML with React 18 + Vite + shadcn/ui dashboard
(mail list, detail panel, Extracted/Raw/Rendered tabs, sonner toasts)
- Add REST API endpoints: GET /api/mails, GET /api/mails/:id
- Fix MIME base64 body extraction (rewrite extractMailBodies with proper
multipart boundary parsing and atob decoding)
- Add isPromotionalEmail() pre-LLM filter (List-Unsubscribe, List-ID,
Precedence: bulk, ESP X-headers) to avoid wasting AI tokens on ads
- Fix codeAndLink() to use regex URL extraction instead of comma-split,
preventing layout break on URLs containing commas
- Add ASSETS binding to wrangler.toml.example for static frontend serving
- Migrate from npm to pnpm; add pnpm-workspace.yaml
- Add CLAUDE.md and update AGENTS.md with current architecture