--- 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": "", "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 ` — on every `/api/*` request. - `x-custom-auth: ` — 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": "noreply@foo.com", "to": "abc@yourdomain.com", "created_at": "2026-04-21 10:00:00", "sender": "Foo ", "subject": "Your code is 123456", "text": "Your code is 123456\n", "html": "

Your code is 123456

", "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": "abc123@example.com", "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/" -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": "recipient@example.com", "to_name": "Recipient", "subject": "Hello", "content": "

Hi

", "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":"someone@example.com","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.