# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Structure
- **Backend**: `worker/` — Cloudflare Workers API using Hono framework. Entry: `worker/src/worker.ts`, APIs under `worker/src/*_api/`.
- **Frontend**: `frontend/` — Vue 3 + Naive UI app deployed to Cloudflare Pages. Routes in `frontend/src/router/`.
- **Pages middleware**: `pages/functions/_middleware.js` — Routes API calls to Worker backend.
- **Mail parser**: `mail-parser-wasm/` — Rust WASM email parser.
- **SMTP/IMAP proxy**: `smtp_proxy_server/` — Python proxy server.
- **DB schema/migrations**: `db/` — SQLite via Cloudflare D1, dated migration patches.
- **Docs**: `vitepress-docs/` — VitePress documentation site (zh + en).
- **E2E tests**: `e2e/` — Playwright tests in Docker Compose (API, browser, SMTP proxy).
- **Changelogs**: `CHANGELOG.md` (中文) + `CHANGELOG_EN.md` (English).
## Build & Dev Commands
Run inside each subfolder with `pnpm`:
| Folder | Dev | Build | Lint | Deploy |
|--------|-----|-------|------|--------|
| `worker/` | `pnpm dev` | `pnpm build` | `pnpm lint` | `pnpm deploy` |
| `frontend/` | `pnpm dev` | `pnpm build` | — | `pnpm deploy` |
| `vitepress-docs/` | `pnpm dev` | `pnpm build` | — | — |
| `mail-parser-wasm/` | — | `wasm-pack build --release` | — | — |
SMTP proxy: `pip install -r smtp_proxy_server/requirements.txt` then `python smtp_proxy_server/main.py`.
## E2E Tests
Tests run in Docker Compose with Playwright. From `e2e/`:
```bash
npm test # Build, run all tests, exit
npm run test:down # Clean up containers
```
Test categories: `tests/api/` (API tests), `tests/browser/` (UI tests with Chromium), `tests/smtp-proxy/` (SMTP/IMAP proxy tests).
The Docker frontend serves over **HTTPS** (self-signed cert) with Vite proxy to worker — required for WebAuthn (`navigator.credentials`) and `crypto.subtle` which need a secure context. Browser tests use `ignoreHTTPSErrors: true`.
Key patterns for browser tests:
- Frontend hashes passwords with SHA-256 (`crypto.subtle`) before sending — API test registration must use pre-hashed passwords if UI login is needed.
- VueUse `useStorage('key', '')` with string default uses **raw string** serialization — set localStorage with raw value, not `JSON.stringify()`.
- WebAuthn browser tests use CDP virtual authenticator (`WebAuthn.enable` + `WebAuthn.addVirtualAuthenticator`).
## Architecture
### Worker Auth Flow (`worker/src/worker.ts`)
Three auth layers applied via Hono middleware, each using different headers:
| Path prefix | Header | Purpose |
|-------------|--------|---------|
| `/api/*` | `Authorization: Bearer <jwt>` | Address (mailbox) credential |
| `/user_api/*` | `x-user-token` | User account JWT |
| `/admin/*` | `x-admin-auth` | Admin password |
| (any) | `x-user-access-token` | User role-based access token |
| (any) | `x-custom-auth` | Optional global access password |
| (any) | `x-lang` | Language preference (`en`/`zh`) |
Public endpoints (no auth): `/open_api/*`, `/user_api/login`, `/user_api/register`, `/user_api/passkey/authenticate_*`, `/user_api/oauth2/*`.
### Worker Email Flow (`worker/src/email/`)
Cloudflare Email Worker entry: `email()` in `worker/src/email/index.ts`. Processing pipeline:
1. Parse raw email → check junk → check address exists
2. Auto-reply if configured → forward if configured → webhook if enabled
3. Store in D1 database
### Frontend State (`frontend/src/store/index.js`, `frontend/src/api/index.js`)
Global state via VueUse `useStorage` for persistence. The `api` module wraps axios with auto-attached auth headers and fingerprinting. API base URL comes from `VITE_API_BASE` env var (empty = same origin).
## Coding Style
- `worker/` uses TypeScript + ESLint; `frontend/` uses Vue SFCs.
- Keep existing naming patterns: `*_api/` folders, `utils/`, `models/`.
- ESM imports only (`type: module`).
## Commits & PRs
- Use Conventional Commits: `feat:`, `fix:`, `docs:`, `style:`, `refactor:`, `perf:`.
- PRs should explain scope; add screenshots for UI changes.
- Use squash merge for PRs.
## Post-Task Checklist (IMPORTANT)
After completing any feature, bug fix, or improvement, **always check**:
1. **CHANGELOG.md** (中文) and **CHANGELOG_EN.md** (English) — both must be updated under the current `(main)` version section with the change entry. Follow the existing format: `- feat/fix/docs: |模块| 描述`.
2. **Documentation** — if the change involves new environment variables, new API endpoints, or configuration changes, update the corresponding docs in `vitepress-docs/docs/zh/` and `vitepress-docs/docs/en/`. Key files:
- `guide/worker-vars.md` — Worker environment variables
- `guide/ui/` — Frontend deployment docs
- `guide/feature/` — Feature-specific docs
- `api/` — API reference docs
3. **Both languages** — docs and changelogs exist in Chinese and English; always update both.
## Config
- Worker settings in `worker/wrangler.toml` (see `wrangler.toml.template` for bindings).
- Frontend uses `VITE_*` env vars. Don't commit secrets.
---
name: cf-temp-mail-version-upgrade
description: Upgrade the project version number. Use when the user asks to bump the version, upgrade the version, or prepare a new release version. Supports major, minor, and patch upgrades.
---
# Version Upgrade
Upgrade the version number of the cloudflare_temp_email project.
## Files to modify
1. `frontend/package.json` — `version` field
2. `worker/package.json` — `version` field
3. `worker/src/constants.ts` — `VERSION` constant (format: `VERSION: 'v' + '1.4.0'`)
4. `pages/package.json` — `version` field
5. `vitepress-docs/package.json` — `version` field
6. `CHANGELOG.md` — add new version placeholder
7. `CHANGELOG_EN.md` — add new version placeholder (English)
## Upgrade workflow
1. Read `frontend/package.json` to get the current version.
2. Compute the new version based on the upgrade type:
- major: 1.3.0 → 2.0.0
- minor: 1.3.0 → 1.4.0
- patch: 1.3.0 → 1.3.1
3. Update the `version` field in every `package.json` listed above.
4. Update the `VERSION` constant in `worker/src/constants.ts`.
5. Insert a new version placeholder at the top of `CHANGELOG.md`.
6. Insert a new version placeholder at the top of `CHANGELOG_EN.md`.
7. Rename the previous version heading in both changelogs from `## v{OLD_VERSION}(main)` to `## v{OLD_VERSION}` so only the new active development version keeps `(main)`.
## CHANGELOG format
In `CHANGELOG.md`, insert before the existing `## v{OLD_VERSION}(main)` line (i.e. right after the closing `</p>` of the language-switch link):
```markdown
## v{VERSION}(main)
### Features
### Bug Fixes
### Improvements
```
After inserting the new placeholder, update the old heading:
```diff
-## v{OLD_VERSION}(main)
+## v{OLD_VERSION}
```
`CHANGELOG_EN.md` uses the same format and must receive the same old-heading update.
## Commit message format
```
feat: upgrade version to v{VERSION}
- Update version number to {VERSION} in all package.json files
- Add v{VERSION} placeholder in CHANGELOG.md
- Move (main) marker from v{OLD_VERSION} to v{VERSION}
```
---
name: cf-temp-mail-agent-mail
description: Read and send mails from a cloudflare_temp_email mailbox using a user-supplied Address JWT and API base URL. Use when the user (or an agent such as OpenClaw / Codex / Cursor) needs to list the inbox, fetch a specific message, or send an email via the server-parsed /api/parsed_mails, /api/parsed_mail/:id, and /api/send_mail endpoints. Falls back to local parsing of /api/mail/:id raw source with mail-parser-wasm + postal-mime if the parsed endpoints are unavailable. Does NOT handle mailbox creation — the user provides the JWT themselves.
---
# Temp-Mail Agent Usage
## Prerequisites
The user must first **open the frontend** (e.g. `https://mail.example.com`) in a browser and create or log into a mailbox address. This step may require passing a Turnstile CAPTCHA that agents cannot complete. After that, the **Address JWT** is displayed in the frontend UI and can be copied directly.
## Inputs the user must provide
- `BASE` — API base URL, e.g. `https://mail.example.com`.
- `JWT` — Address JWT, visible and copyable from the frontend UI after creating or logging into a mailbox.
- *(optional)* `SITE_PASSWORD` — only if the deployment enabled `x-custom-auth`.
If anything is missing, ask the user before making requests.
## Credential persistence
To avoid asking every time, save credentials to `~/.cf-temp-mail/credentials.json`:
```json
{
"base": "https://mail.example.com",
"jwt": "<ADDRESS_JWT>",
"site_password": ""
}
```
On first use, if the file exists, read and use it. If not, ask the user and save for next time. Before each request, validate the JWT via `GET /api/settings` — if it returns `401`, inform the user the JWT is expired and ask for a fresh one, then update the file.
## Required headers
- `Authorization: Bearer <JWT>` — on every `/api/*` request.
- `x-custom-auth: <SITE_PASSWORD>` — only when the site requires it.
- `x-lang: en` or `zh` — optional, error-message language.
Do not send the Address JWT as `x-user-token` — that is a different JWT type and will yield `401 InvalidAddressCredentialMsg`.
## Primary path: parsed endpoints
| Task | Method | Path | Returns |
| ------------------- | ------ | ---------------------------------- | ----------------------------------------- |
| Address info | GET | `/api/settings` | `{ address, send_balance }` |
| List parsed mails | GET | `/api/parsed_mails?limit=&offset=` | `{ results: [parsedMail], count }` |
| Get one parsed mail | GET | `/api/parsed_mail/:id` | `parsedMail` |
`limit` 1–100, `offset` 0-based. On `429`, back off.
`parsedMail` shape:
```json
{
"id": 42,
"message_id": "<...>",
"source": "[email protected]",
"to": "[email protected]",
"created_at": "2026-04-21 10:00:00",
"sender": "Foo <[email protected]>",
"subject": "Your code is 123456",
"text": "Your code is 123456\n",
"html": "<p>Your code is <b>123456</b></p>",
"attachments": [
{ "filename": "a.pdf", "mimeType": "application/pdf", "disposition": "attachment", "size": 12345 }
]
}
```
Attachments carry metadata only; no binary content.
### 1. Smoke-test the JWT
```bash
curl -s "$BASE/api/settings" -H "Authorization: Bearer $JWT"
# → { "address": "[email protected]", "send_balance": 0 }
```
If this returns `401`, JWT is wrong / expired / mismatched with `BASE` — ask the user for a fresh one.
### 2. List the inbox
```bash
curl -s "$BASE/api/parsed_mails?limit=20&offset=0" \
-H "Authorization: Bearer $JWT"
```
### 3. Get one mail
```bash
curl -s "$BASE/api/parsed_mail/<id>" -H "Authorization: Bearer $JWT"
```
## Send mail
Requires `send_balance > 0` (check via `/api/settings`). The deployment must have a send method configured (Resend / SMTP / Cloudflare Email Routing binding).
| Task | Method | Path | Body / Returns |
| ----------------------- | ------ | ------------------------------- | ------------------------------------------------- |
| Request send access | POST | `/api/request_send_mail_access` | `{}` → `{ status: "ok" }` |
| Send mail | POST | `/api/send_mail` | `sendMailBody` → `{ status: "ok" }` |
| List sent (sendbox) | GET | `/api/sendbox?limit=&offset=` | `{ results: [...], count }` |
| Delete sent item | DELETE | `/api/sendbox/:id` | `{ success: true }` |
`sendMailBody`:
```json
{
"from_name": "My Name",
"to_mail": "[email protected]",
"to_name": "Recipient",
"subject": "Hello",
"content": "<p>Hi</p>",
"is_html": true
}
```
`from_name` and `to_name` are optional (can be empty string). `is_html: false` sends plain text.
### Send example
```bash
curl -s -X POST "$BASE/api/send_mail" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{"from_name":"","to_mail":"[email protected]","to_name":"","subject":"Test","content":"Hello","is_html":false}'
```
## Fallback: local parse of raw source
If `/api/parsed_mails` / `/api/parsed_mail/:id` returns `404` (older deployment) or a parse error, fall back to `/api/mails` / `/api/mail/:id` (RFC822 `raw`) and parse locally. Mirror the frontend strategy in `frontend/src/utils/email-parser.js`: try **`mail-parser-wasm`** first, fall back to **`postal-mime`**.
```bash
npm i mail-parser-wasm postal-mime
```
```js
// parseRaw.mjs — drop-in parser matching frontend behavior
async function parseRaw(raw) {
try {
const { parse_message } = await import('mail-parser-wasm');
const m = parse_message(raw);
if (m?.subject && (m?.body_html || m?.text)) {
return {
sender: m.sender || '',
subject: m.subject || '',
text: m.text || '',
html: m.body_html || '',
attachments: (m.attachments || []).map(a => ({
filename: a.filename || a.content_id || '',
mimeType: a.content_type || '',
size: a.content?.length ?? 0,
})),
};
}
} catch { /* fall through */ }
const PostalMime = (await import('postal-mime')).default;
const p = await PostalMime.parse(raw);
const sender = p.from?.name && p.from?.address
? `${p.from.name} <${p.from.address}>`
: (p.from?.address || '');
return {
sender,
subject: p.subject || '',
text: p.text || '',
html: p.html || '',
attachments: (p.attachments || []).map(a => ({
filename: a.filename || a.contentId || '',
mimeType: a.mimeType || '',
size: a.content?.length ?? 0,
})),
};
}
// usage
const row = await (await fetch(`${BASE}/api/mail/${id}`, {
headers: { Authorization: `Bearer ${JWT}` },
})).json();
const parsed = await parseRaw(row.raw);
```
For attachment bytes, use `postal-mime` directly — `parsed.attachments[i].content` is a `Uint8Array`.
## Polling discipline
- Start at `poll=3s`, exponential backoff capped at 10s.
- Dedupe by mail `id`.
- Never poll faster than once per second.
- Respect `429` — sleep and retry.
## Common errors
- `401 InvalidAddressCredentialMsg` — JWT wrong/expired/sent via wrong header. Ask the user for a fresh JWT.
- `401 CustomAuthPasswordMsg` — site requires `x-custom-auth`; attach `SITE_PASSWORD`.
- `400 InvalidLimitMsg` / `InvalidOffsetMsg` — `limit` must be 1..100, `offset ≥ 0`.
- `404` on `/api/parsed_mail*` — deployment predates the parsed endpoints; use the fallback.
- `429` — rate limited; back off.