config(git,claude,ssh): templated gitconfig; claude settings/commands/hooks/skills; ssh config

gitconfig: name/email from chezmoi data, gh credential helper by PATH
lookup (portable to darwin). claude: no credentials, history, projects,
sessions, or plugin caches. ssh config as private_ (0600).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Prad NukalaandClaude Fable 5 committed 2026-08-09 14:53:46 -04:00
1 parent 3c691e8dc5
commit 20891f3910
476 files changed
+59086 -7

No files matched your search

@@ -0,0 +1,103 @@
---
name: cloudflare-email-service
description: Send and receive transactional emails with Cloudflare Email Service (Email Sending + Email Routing). Use when building email sending (Workers binding or REST API), email routing, Agents SDK email handling, or integrating email into any app — Workers, Node.js, Python, Go, etc. Also use for email deliverability, SPF/DKIM/DMARC, wrangler email setup, MCP email tools, or when a coding agent needs to send emails. Even for simple requests like "add email to my Worker" — this skill has critical config details.
---
# Cloudflare Email Service
Your knowledge of the Cloudflare Email Service, Email Routing or Email Sending may be outdated. **Prefer retrieval over pre-training** for any Cloudflare Email Service task.
Cloudflare Email Service lets you send transactional emails and route incoming emails, all within the Cloudflare platform. Your knowledge of this product may be outdated — it launched in 2025 and is evolving rapidly. **Prefer retrieval over pre-training** for any Email Service task.
**If there is any discrepancy between this skill and the sources below, always trust the original source.** The Cloudflare docs, REST API spec, `@cloudflare/workers-types`, and Agents SDK repo are the source of truth. This skill is a convenience guide — it may lag behind the latest changes. When in doubt, retrieve from the sources below and use what they say.
## Retrieval Sources
| Source | How to retrieve | Use for |
|--------|----------------|---------|
| Cloudflare docs | `cloudflare-docs` search tool or URL `https://developers.cloudflare.com/email-service/` | API reference, limits, pricing, latest features |
| REST API spec | `https://developers.cloudflare.com/api/resources/email_sending` | OpenAPI spec for the Email Sending REST API |
| Workers types | `https://www.npmjs.com/package/@cloudflare/workers-types` | Type signatures, binding shapes |
| Agents SDK docs | Fetch `docs/email.md` from `https://github.com/cloudflare/agents/tree/main/docs` | Email handling in Agents SDK |
## FIRST: Check Prerequisites
Before writing any email code, verify the basics are in place:
1. **Domain onboarded?** Run `npx wrangler email sending list` to see which domains have email sending enabled. If the domain isn't listed, run `npx wrangler email sending enable userdomain.com` or see [cli-and-mcp.md](references/cli-and-mcp.md) for full setup instructions.
2. **Binding configured?** Look for `send_email` in `wrangler.jsonc` (for Workers)
3. **postal-mime installed?** Run `npm ls postal-mime` (only needed for receiving/parsing emails)
## What Do You Need?
Start here. Find your situation, then follow the link for full details.
| I want to... | Path | Reference |
|--------------|------|-----------|
| **Send emails from a Cloudflare Worker** | Workers binding (no API keys needed) | [sending.md](references/sending.md) |
| **Send emails from an AI agent built with [Cloudflare Agents SDK](https://developers.cloudflare.com/agents/)** | `onEmail()` + `replyToEmail()` in Agent class | [sending.md](references/sending.md) |
| **Send emails from an external app or agent** (Node.js, Go, Python, etc.) | REST API with Bearer token | [rest-api.md](references/rest-api.md) |
| **Send emails from a coding agent** (Claude Code, Cursor, Copilot, etc.) | MCP tools, wrangler CLI, or REST API | [cli-and-mcp.md](references/cli-and-mcp.md) |
| **Receive and process incoming emails** (Email Routing) | Workers `email()` handler | [routing.md](references/routing.md) |
| **Set up Email Sending or Email Routing** | `wrangler email sending enable` / `wrangler email routing enable`, or Dashboard | [cli-and-mcp.md](references/cli-and-mcp.md) |
| **Improve deliverability, avoid spam folders** | Authentication, content, compliance | [deliverability.md](references/deliverability.md) |
## Quick Start — Workers Binding
Add the binding to `wrangler.jsonc`, then call `env.EMAIL.send()`. The `from` domain must be onboarded via `npx wrangler email sending enable yourdomain.com`.
```jsonc
// wrangler.jsonc
{ "send_email": [{ "name": "EMAIL" }] }
```
```typescript
const response = await env.EMAIL.send({
to: "user@example.com",
from: { email: "welcome@yourdomain.com", name: "My App" },
subject: "Welcome!",
html: "<h1>Welcome!</h1>",
text: "Welcome!",
});
```
The binding is recommended for Workers — no API keys needed. If a user specifically requests the REST API from within a Worker (e.g., they already have an API token workflow), that works too — see [rest-api.md](references/rest-api.md).
See [sending.md](references/sending.md) for the full API, batch sends, attachments, custom headers, restricted bindings, and Agents SDK integration.
## Quick Start — REST API
For apps outside Workers, or within Workers if the user explicitly requests it. Key differences from the Workers binding:
- Endpoint: `POST https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send`
- `from` object uses `address` (not `email`): `{ "address": "...", "name": "..." }`
- `replyTo` is `reply_to` (snake_case)
- Response returns `{ delivered: [], permanent_bounces: [], queued: [] }` (not `messageId`)
See [rest-api.md](references/rest-api.md) for curl examples, response format, and error handling.
## Common Mistakes
| Mistake | Why It Happens | Fix |
|---------|---------------|-----|
| Forgetting `send_email` binding in wrangler config | Email Service uses a binding, not an API key | Add `"send_email": [{ "name": "EMAIL" }]` to wrangler.jsonc |
| Sending from an unverified domain | Domain must be onboarded onto Email Sending before first send | Run `wrangler email sending enable yourdomain.com` or onboard in Dashboard |
| Reading `message.raw` twice in email handler | The raw stream is single-use — second read returns empty | Buffer first: `const raw = await new Response(message.raw).arrayBuffer()` |
| Missing `text` field (HTML only) | Some email clients only show plain text; also helps spam scores | Always include both `html` and `text` versions |
| Using email for marketing/bulk sends | Email Service is for transactional email only | Use a dedicated marketing email platform for newsletters and campaigns |
| Forwarding to unverified destinations | `message.forward()` only works with verified addresses | Run `wrangler email routing addresses create user@gmail.com` or add in Dashboard |
| Testing with fake addresses | Bounces from non-existent addresses hurt sender reputation | Use real addresses you control during development |
| Hardcoding API tokens in source code | Tokens in code get committed and leaked | Use environment variables or Cloudflare secrets |
| Ignoring the `from` domain requirement | The `from` address must use a domain onboarded to Email Service | Verify the domain first, then send from `anything@that-domain.com` |
| Using `email` key in REST API `from` object | REST API uses `address` not `email` for `from` object | Use `{ "address": "...", "name": "..." }` for REST, `{ "email": "...", "name": "..." }` for Workers |
| Using `replyTo` in REST API | REST API uses snake_case field names | Use `reply_to` for REST API, `replyTo` for Workers binding |
## References
Read the reference that matches your situation. You don't need all of them.
- **[references/sending.md](references/sending.md)** — Workers binding API, attachments, Agents SDK email. For Workers or Agents SDK.
- **[references/rest-api.md](references/rest-api.md)** — REST endpoint, curl examples, error handling. For apps NOT on Workers.
- **[references/routing.md](references/routing.md)** — Inbound `email()` handler, forwarding, replying, parsing. For receiving emails.
- **[references/cli-and-mcp.md](references/cli-and-mcp.md)** — Domain setup, wrangler commands, MCP tools. For first-time setup.
- **[references/deliverability.md](references/deliverability.md)** — SPF/DKIM/DMARC, bounces, suppressions, best practices.
@@ -0,0 +1,125 @@
# CLI, MCP, and Project Setup
Manage Cloudflare Email Service from the command line and coding agents.
For full CLI reference, run `npx wrangler email --help`. For Dashboard setup, see the [getting started docs](https://developers.cloudflare.com/email-service/get-started/).
## Wrangler Email Commands
```
wrangler email routing
├── enable/disable <domain> # Toggle email routing
├── dns get <domain> # Show required DNS records
├── rules list/create/update/delete # Manage routing rules
└── addresses list/create/delete # Destination addresses (account-scoped)
wrangler email sending
├── enable/disable <domain> # Toggle email sending
├── dns get <domain> # Show sending DNS records (SPF, DKIM)
├── send --from --to ... # Send an email (builder flags)
└── send-raw --from --to ... # Send a raw MIME email
```
## Domain Setup
### Via Dashboard
1. Navigate to **Compute & AI** > **Email Service** > **Email Sending** (or **Email Routing**)
2. Select **Onboard Domain** > choose domain > **Add records and onboard**
This auto-adds SPF (TXT) and DKIM (CNAME/TXT) records. DNS usually propagates within 5-15 minutes.
### Via CLI
```bash
npx wrangler email sending enable yourdomain.com
npx wrangler email sending dns get yourdomain.com # Verify records
```
## Local Development
Add `"remote": true` to send real emails during `wrangler dev`:
```jsonc
{ "send_email": [{ "name": "EMAIL", "remote": true }] }
```
```bash
npx wrangler dev
```
Emails are actually sent — use test addresses you control. Remove `"remote": true` before deploying.
## Cloudflare MCP Server
If you have the [Cloudflare MCP server](https://github.com/cloudflare/mcp) (`https://mcp.cloudflare.com/mcp`) configured, you can manage Email Service through its `search` and `execute` tools.
Use `search` to find email sending endpoints:
```javascript
// search tool — find all email sending API endpoints
async () => {
const results = [];
for (const [path, methods] of Object.entries(spec.paths)) {
if (path.includes('email/sending')) {
for (const [method, op] of Object.entries(methods)) {
results.push({ method: method.toUpperCase(), path, summary: op.summary });
}
}
}
return results;
}
```
Then use `execute` to call them — for example, checking sending limits or sending an email:
```javascript
// execute tool — check sending quota
async () => {
return cloudflare.request({
method: "GET",
path: `/accounts/${accountId}/email/sending/limits`
});
}
// execute tool — send an email
async () => {
return cloudflare.request({
method: "POST",
path: `/accounts/${accountId}/email/sending/send`,
body: {
to: "user@example.com",
from: { address: "notifications@yourdomain.com", name: "My App" },
subject: "Deployment Complete",
html: "<h1>Deployed!</h1>",
text: "Deployed!"
}
});
}
```
GraphQL analytics queries also work through `execute` — see [deliverability.md](deliverability.md#graphql-analytics-api) for query examples. Note that email analytics are **zone-level** datasets (`emailSendingAdaptiveGroups`, `emailSendingAdaptive`) queried under `viewer > zones`, and require the **Analytics Read** token permission.
## Sending from CLI / Agents
```bash
npx wrangler email sending send \
--from "agent@yourdomain.com" \
--to "developer@company.com" \
--subject "Deployment Complete" \
--text "Your Worker was deployed successfully."
```
Or via REST API:
```bash
curl "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/email/sending/send" \
--header "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
--header "Content-Type: application/json" \
--data '{
"to": "developer@company.com",
"from": {"address": "agent@yourdomain.com", "name": "Build Agent"},
"subject": "Deployment Complete",
"text": "Your Worker was deployed successfully."
}'
```
@@ -0,0 +1,285 @@
# Email Deliverability & Best Practices
For full details, see the [deliverability docs](https://developers.cloudflare.com/email-service/concepts/deliverability/) and [email authentication docs](https://developers.cloudflare.com/email-service/concepts/email-authentication/). All the monitoring endpoints below can be called via the [REST API](rest-api.md), [Wrangler CLI, or the Cloudflare MCP server](cli-and-mcp.md).
## What Cloudflare Handles
When you onboard a domain, Cloudflare auto-configures:
- **SPF** — TXT records authorizing Cloudflare's sending infrastructure
- **DKIM** — Records for cryptographic signing of outbound emails
- **IP reputation** — Managed sending infrastructure optimized for deliverability
- **Soft bounce retries** — Automatic exponential backoff for temporary failures
- **Suppression lists** — Hard-bounced addresses automatically blocked
- **Feedback loops** — ISP complaint signals processed and acted on
Consider adding a **DMARC** record if you don't have one: `v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@yourdomain.com`
## Bounce Handling
**Hard bounces** — permanent failures (address doesn't exist, domain doesn't exist). Never retried. Address auto-added to suppression list. Sending to suppressed address returns `E_RECIPIENT_SUPPRESSED`.
**Soft bounces** — temporary failures (mailbox full, server down, greylisting). Cloudflare auto-retries with exponential backoff.
## Suppression Lists
**Account list** (your account) — spam complaints from recipients. Cloudflare integrates with Postmasters to auto-suppress. You can manually add/remove addresses in the Dashboard.
See the [suppressions docs](https://developers.cloudflare.com/email-service/concepts/suppressions/) for details.
## Your Responsibilities
### Content
- Include both HTML and plain text versions
- Use a recognizable sender name: `{ email: "noreply@app.com", name: "My App" }`
- Write honest subject lines — avoid ALL CAPS, excessive punctuation
- Include `List-Unsubscribe` headers for recurring emails
- Use full URLs from your domain — avoid URL shorteners
### List Quality
- Validate email addresses before sending
- Implement double opt-in for subscriptions
- Honor unsubscribe requests promptly
### Transactional Only
Email Service is for **transactional email** (triggered by user actions: signups, password resets, order confirmations). Marketing/bulk campaigns are not permitted — use a dedicated marketing platform.
## Monitoring Deliverability
### Dashboard
Per-domain and account-wide analytics are available in the Cloudflare dashboard:
1. Log in to the [Cloudflare dashboard](https://dash.cloudflare.com) and select your account.
2. Go to **Compute & AI** > **Email Service**.
3. Select a domain or view account-wide metrics.
4. Select the **Analytics** tab.
### Send Response
Every send (REST API or Workers binding) returns immediate delivery feedback. Check the response to track per-send outcomes:
```json
{
"result": {
"delivered": ["user@example.com"],
"permanent_bounces": ["bad@nonexistent.com"],
"queued": ["slow@recipient.com"]
}
}
```
Log these to build your own delivery rate metrics.
### Sending Limits
Check your account's daily sending quota:
```bash
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/limits" \
--header "Authorization: Bearer <API_TOKEN>"
```
Returns:
```json
{
"result": {
"quota": { "value": 5000, "unit": "day" }
}
}
```
### Suppression List
Addresses that hard-bounced or received spam complaints are auto-suppressed. You can query and manage suppressions via the API.
**List suppressions (account-wide):**
```bash
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression?page=1&per_page=100&order=created_at&direction=desc" \
--header "Authorization: Bearer <API_TOKEN>"
```
Returns:
```json
{
"page": 1,
"per_page": 100,
"total": 2,
"result": [
{
"id": "396a5436-d4b0-42a6-b3fc-48e8fa522321",
"email": "bounced@example.com",
"reason": "hard_bounce",
"created_at": "2026-03-15T10:00:00Z",
"expires_at": null
}
]
}
```
Query params: `page`, `per_page` (max 1000), `order` (`email` | `expires_at` | `created_at`), `direction` (`asc` | `desc`).
**Manually suppress an address:**
```bash
curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression" \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{ "email": "user@example.com", "expires_at": "2026-06-01T00:00:00Z" }'
```
`expires_at` is optional — omit for permanent suppression.
**Remove a suppression:**
```bash
curl -X DELETE "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression/{suppression_id}" \
--header "Authorization: Bearer <API_TOKEN>"
```
Zone-level suppressions are also available at `/zones/{zone_id}/email/sending/suppression` with the same interface.
### GraphQL Analytics API
Email Service exposes two zone-level datasets via the [GraphQL Analytics API](https://developers.cloudflare.com/analytics/graphql-api/). You can explore the schema interactively at [graphql.cloudflare.com/explorer](https://graphql.cloudflare.com/explorer). Metrics are retained for 31 days.
| Dataset | Description |
|---------|-------------|
| `emailSendingAdaptiveGroups` | Aggregated counts grouped by dimensions (status, date, domain, auth results, etc.) |
| `emailSendingAdaptive` | Individual email events with full detail (from, to, subject, messageId, errors, etc.) |
These are **zone-level** datasets — query under `viewer > zones`, not `accounts`.
**Aggregated dimensions** (`emailSendingAdaptiveGroups`):
| Dimension | Type | Description |
|-----------|------|-------------|
| `date` | Date | Day-level grouping |
| `datetime` | Time | Exact timestamp (also: `datetimeMinute`, `datetimeFiveMinutes`, `datetimeFifteenMinutes`, `datetimeHour`) |
| `status` | string | Delivery status |
| `eventType` | string | Event type |
| `sendingDomain` | string | The sending domain |
| `envelopeTo` | string | Recipient address |
| `errorCause` | string | Error cause for failed sends |
| `arc`, `dkim`, `dmarc`, `spf` | string | Email authentication results |
| `isSpam`, `isNDR`, `isLastEvent` | uint8 | Boolean flags |
| `spamScore`, `spamThreshold` | uint32 | Spam scoring |
**Individual event fields** (`emailSendingAdaptive`) additionally include: `from`, `to`, `subject`, `messageId`, `sessionId`, `errorDetail`.
**Email counts by status and date:**
```graphql
query EmailSendingByStatus(
$zoneTag: string!
$start: Date!
$end: Date!
) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
emailSendingAdaptiveGroups(
filter: { date_geq: $start, date_leq: $end }
limit: 10000
orderBy: [date_DESC]
) {
count
dimensions {
date
status
}
}
}
}
}
```
**Filter by status (e.g. only failures):**
```graphql
query EmailFailures(
$zoneTag: string!
$start: Date!
$end: Date!
) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
emailSendingAdaptiveGroups(
filter: { date_geq: $start, date_leq: $end, status: "deliveryFailed" }
limit: 10000
orderBy: [date_DESC]
) {
count
dimensions {
date
errorCause
sendingDomain
}
}
}
}
}
```
**Individual email events (troubleshooting):**
```graphql
query RecentEmailEvents(
$zoneTag: string!
$start: Time!
$end: Time!
) {
viewer {
zones(filter: { zoneTag: $zoneTag }) {
emailSendingAdaptive(
filter: { datetime_geq: $start, datetime_leq: $end }
limit: 50
orderBy: [datetime_DESC]
) {
datetime
from
to
subject
status
eventType
sendingDomain
messageId
errorCause
errorDetail
dkim
dmarc
spf
isSpam
}
}
}
}
```
Note: `emailSendingAdaptive` filters use `datetime_geq`/`datetime_leq` (Time type, e.g. `"2026-04-01T00:00:00Z"`), while `emailSendingAdaptiveGroups` uses `date_geq`/`date_leq` (Date type, e.g. `"2026-04-01"`).
**curl example:**
```bash
curl "https://api.cloudflare.com/client/v4/graphql" \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{
"query": "query($zoneTag:string!,$start:Date!,$end:Date!){viewer{zones(filter:{zoneTag:$zoneTag}){emailSendingAdaptiveGroups(filter:{date_geq:$start,date_leq:$end},limit:10000,orderBy:[date_DESC]){count,dimensions{date,status}}}}}",
"variables": {
"zoneTag": "<ZONE_ID>",
"start": "2026-03-15",
"end": "2026-04-15"
}
}'
```
## Metrics to Watch
| Metric | Target | If Out of Range |
|--------|--------|-----------------|
| Delivery rate | > 95% | Check for invalid addresses; verify DNS records |
| Hard bounce rate | < 2% | Clean your email list |
| Complaint rate | < 0.1% | Make unsubscribe easier; stop unwanted emails |
@@ -0,0 +1,184 @@
# Sending Emails — REST API
Send emails via HTTP requests from any application. If your app runs on Cloudflare Workers, use the [Workers binding](sending.md) instead — simpler and more performant.
For the full OpenAPI specification, refer to the [Email Sending API reference](https://developers.cloudflare.com/api/resources/email_sending/methods/send).
## Endpoint
```
POST https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send
```
## Authentication
Authenticate with a [Cloudflare API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) that has email sending permission:
```
Authorization: Bearer <API_TOKEN>
```
## Request Fields
**IMPORTANT:** The REST API uses different field names than the Workers binding in some cases.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `to` | string or string[] | Yes | Recipient(s), max 50 combined with cc/bcc |
| `from` | string or `{address, name}` | Yes | Sender — **uses `address` not `email`** for object form |
| `subject` | string | Yes | Email subject line |
| `html` | string | No* | HTML body |
| `text` | string | No* | Plain text body |
| `cc` | string or string[] | No | CC recipients |
| `bcc` | string or string[] | No | BCC recipients |
| `reply_to` | string or `{address, name}` | No | **Snake_case**, not camelCase |
| `attachments` | array | No | File attachments and inline images |
| `headers` | object | No | Custom email headers |
*At least one of `html` or `text` required. Include both for best deliverability.
**Key differences from Workers binding:** `from` uses `address` (REST) vs `email` (Workers), `reply_to` (REST) vs `replyTo` (Workers), `content_id` (REST) vs `contentId` (Workers) for inline attachments, errors use numeric codes (REST) vs string `E_*` codes (Workers), and responses return delivery status (`delivered`/`permanent_bounces`/`queued`) instead of just `messageId`.
## Examples
Simple send:
```bash
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send" \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{
"to": "user@example.com",
"from": "welcome@yourdomain.com",
"subject": "Welcome!",
"html": "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
"text": "Welcome! Thanks for signing up."
}'
```
Multiple recipients with CC/BCC:
```bash
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send" \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{
"to": ["user1@example.com", "user2@example.com"],
"cc": ["manager@company.com"],
"bcc": ["archive@company.com"],
"from": { "address": "orders@yourdomain.com", "name": "Orders" },
"reply_to": "support@yourdomain.com",
"subject": "Order Confirmation",
"html": "<h1>Your order is confirmed</h1>",
"text": "Your order is confirmed"
}'
```
With attachments:
```bash
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send" \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{
"to": "customer@example.com",
"from": "invoices@yourdomain.com",
"subject": "Your Invoice",
"html": "<h1>Invoice attached</h1>",
"attachments": [
{
"content": "JVBERi0xLjQKJeLjz9MK...",
"filename": "invoice.pdf",
"type": "application/pdf",
"disposition": "attachment"
}
]
}'
```
With inline images (embedded via `cid:` URI):
```bash
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send" \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{
"to": "user@example.com",
"from": "newsletter@yourdomain.com",
"subject": "Check this out",
"html": "<h1>Hello!</h1><img src=\"cid:logo\">",
"attachments": [
{
"content": "iVBORw0KGgoAAAANSUhEUg...",
"filename": "logo.png",
"type": "image/png",
"disposition": "inline",
"content_id": "logo"
}
]
}'
```
Attachment fields differ by type:
- **Regular attachment**: `content` (base64), `filename`, `type` (MIME), `disposition: "attachment"`
- **Inline image**: `content` (base64), `filename`, `type` (MIME), `disposition: "inline"`, `content_id` (referenced in HTML as `cid:<content_id>`)
With custom headers:
```bash
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/send" \
--header "Authorization: Bearer <API_TOKEN>" \
--header "Content-Type: application/json" \
--data '{
"to": "user@example.com",
"from": "notifications@yourdomain.com",
"subject": "Your weekly digest",
"html": "<h1>Weekly Digest</h1>",
"headers": {
"List-Unsubscribe": "<https://yourdomain.com/unsubscribe?id=abc123>",
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
"X-Campaign-ID": "weekly-digest-2026-03"
}
}'
```
See the [email headers reference](https://developers.cloudflare.com/email-service/reference/headers/) for the full list of allowed headers.
## Response
Success:
```json
{
"success": true,
"errors": [],
"messages": [],
"result": {
"delivered": ["recipient@example.com"],
"permanent_bounces": [],
"queued": []
}
}
```
Error (numeric codes, not `E_*` string codes like Workers):
```json
{
"success": false,
"errors": [{ "code": 1000, "message": "Sender domain not verified" }],
"result": null
}
```
## Error Handling
| Status | Meaning | Retry? |
|--------|---------|--------|
| 200 | Success | N/A |
| 400 | Validation error | No — fix the request |
| 401 | Invalid API token | No — check your token |
| 429 | Rate limited | Yes — exponential backoff |
| 500 | Server error | Yes — exponential backoff |
Only retry on 429 and 500. Validation errors (400) won't succeed on retry.
@@ -0,0 +1,199 @@
# Receiving & Routing Inbound Email
Handle incoming emails sent to your domain via a Worker's `email()` handler. Forward, reply, reject, or parse emails programmatically.
For full API details, see the [Email Routing docs](https://developers.cloudflare.com/email-service/api/route-emails/email-handler/).
## Email Handler
Export an `email()` function from your Worker. No special wrangler binding needed — a routing rule connects incoming addresses to your Worker.
```typescript
export default {
async email(message, env, ctx): Promise<void> {
console.log(`Email from ${message.from} to ${message.to}`);
await message.forward("team@company.com");
},
} satisfies ExportedHandler<Env>;
```
Set up routing rules in **Dashboard** > **Compute & AI** > **Email Service** > **Email Routing** > **Routing Rules**, or via `wrangler email routing rules create`.
## ForwardableEmailMessage
The `message` parameter is a `ForwardableEmailMessage`. Run `npx wrangler types` to get the full type definition. Key properties and methods:
- `message.from` / `message.to` — envelope addresses (SMTP MAIL FROM / RCPT TO). `message.from` is trustworthy; header addresses can be spoofed.
- `message.headers` — `Headers` object (use `.get("subject")`, `.get("message-id")`, etc.)
- `message.raw` — `ReadableStream<Uint8Array>` of raw MIME content. **Single use** — buffer before accessing.
- `message.rawSize` — size in bytes
- `message.setReject(reason)` — reject with a permanent SMTP error
- `message.forward(rcptTo, headers?)` — forward to a verified destination
- `message.reply(emailMessage)` — reply with an `EmailMessage` object
## Core Operations
### Forward
```typescript
await message.forward("team@company.com");
// With custom headers
await message.forward("team@company.com", new Headers({
"X-Original-Recipient": message.to,
}));
```
Destination must be verified first (Dashboard or `wrangler email routing addresses create`).
### Reject
```typescript
message.setReject("Your message was blocked");
```
### Reply
Using `env.EMAIL.send()` (recommended — no extra dependencies):
```typescript
async email(message, env, ctx) {
const subject = message.headers.get("subject") || "";
await env.EMAIL.send({
to: message.from,
from: message.to,
subject: `Re: ${subject}`,
html: "<p>Thanks! We'll respond shortly.</p>",
text: "Thanks! We'll respond shortly.",
});
await message.forward("team@company.com");
}
```
Using `message.reply()` with MIME (more control, requires `mimetext` + `nodejs_compat`):
```typescript
import { EmailMessage } from "cloudflare:email";
import { createMimeMessage } from "mimetext";
async email(message, env, ctx) {
const msg = createMimeMessage();
const messageId = message.headers.get("Message-ID");
if (messageId) msg.setHeader("In-Reply-To", messageId);
msg.setSender({ name: "Support", addr: "support@yourdomain.com" });
msg.setRecipient(message.from);
msg.setSubject("Re: " + (message.headers.get("subject") || ""));
msg.addMessage({ contentType: "text/plain", data: "Thanks for reaching out!" });
await message.reply(new EmailMessage("support@yourdomain.com", message.from, msg.asRaw()));
}
```
## Parsing Emails
Use [postal-mime](https://www.npmjs.com/package/postal-mime) to parse raw MIME content:
```typescript
import PostalMime from "postal-mime";
async email(message, env, ctx) {
const rawBuffer = await new Response(message.raw).arrayBuffer();
const parsed = await PostalMime.parse(rawBuffer);
console.log("Subject:", parsed.subject);
console.log("Text:", parsed.text);
console.log("Attachments:", parsed.attachments.length);
}
```
## Store and Reply Later (Human-in-the-Loop)
A common pattern is to store incoming emails in a Durable Object (SQLite) so a human or AI agent can review and reply later — rather than replying immediately in the `email()` handler. This enables support inboxes, approval workflows, and AI-drafted replies.
### Architecture
```
Email Routing → email() handler → parse + store in DO → user reviews later → reply via send_email binding
```
The `email()` handler stores the email and returns immediately. Replies happen later via a separate HTTP request or agent action.
### Receive and Store
```typescript
import PostalMime from "postal-mime";
export class MailboxDO extends DurableObject {
async storeEmail(from: string, to: string, subject: string, body: string,
messageId: string, inReplyTo: string | null) {
this.ctx.storage.sql.exec(
`INSERT INTO emails (sender, recipient, subject, body, message_id, in_reply_to, date, read)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), 0)`,
from, to, subject, body, messageId, inReplyTo
);
}
}
export default {
async email(message, env, ctx) {
const raw = await new Response(message.raw).arrayBuffer();
const parsed = await PostalMime.parse(raw);
const id = env.MAILBOX.idFromName(message.to);
const stub = env.MAILBOX.get(id);
await stub.storeEmail(
message.from,
message.to,
parsed.subject || "(no subject)",
parsed.text || parsed.html || "",
message.headers.get("message-id") || "",
message.headers.get("in-reply-to") || null,
);
// Optionally trigger an AI agent to draft a reply (non-blocking)
// ctx.waitUntil(notifyAgent(env, message.to, emailId));
},
} satisfies ExportedHandler<Env>;
```
### Reply Later
When a user (or agent) decides to reply, build proper threading headers and send via the `send_email` binding:
```typescript
// In an HTTP handler or agent tool — not in the email() handler
async function replyToStoredEmail(env: Env, original: StoredEmail, replyBody: string) {
// Build threading headers (In-Reply-To + References per RFC 2822)
const headers: Record<string, string> = {};
if (original.messageId) {
headers["In-Reply-To"] = original.messageId;
headers["References"] = original.messageId;
}
await env.EMAIL.send({
to: original.sender,
from: original.recipient,
subject: `Re: ${original.subject}`,
text: replyBody,
html: `<p>${replyBody}</p>`,
headers,
});
}
```
### Key Design Points
- **Buffer `message.raw` once**, parse with `postal-mime`, then store structured fields. Don't store the raw stream.
- **Extract `Message-ID`, `In-Reply-To`, and `References`** headers during ingest for threading. Fall back to subject-based matching for emails without threading headers.
- **Use Durable Object SQLite** for per-mailbox storage — each mailbox gets its own DO instance keyed by email address, providing natural isolation.
- **Store attachments separately** in R2 (binary blobs), with metadata in SQLite.
- **Defer heavy work** (AI drafting, notifications) via `ctx.waitUntil()` so the `email()` handler returns quickly.
- **Never auto-send from the `email()` handler** in a human-in-the-loop flow. Store a draft, let the user review, then send via a separate action.
## Gotchas
- **`message.raw` is single-use.** Buffer first: `const raw = await new Response(message.raw).arrayBuffer()`
- **Destinations must be verified.** Forwarding to unverified addresses fails silently.
- **Handler must act.** If your handler returns without consuming raw, forwarding, or rejecting, the email is dropped.
- **DMARC/SPF for replies.** If sending replies, ensure your domain has proper SPF/DKIM records (auto-configured on domain onboarding).
@@ -0,0 +1,252 @@
# Sending Emails — Workers Binding & Agents SDK
Send emails from Cloudflare Workers using the native binding, or from AI agents using the Agents SDK. If your app is NOT on Workers, use the [REST API](rest-api.md) instead.
## Workers Binding
### Configuration
```jsonc
// wrangler.jsonc
{
"send_email": [
{ "name": "EMAIL" }
]
}
```
For local development, add `"remote": true` so email sends are proxied to the real service:
```jsonc
{ "send_email": [{ "name": "EMAIL", "remote": true }] }
```
Run `npx wrangler types` to auto-generate the `Env` interface with your `EMAIL` binding. This produces a `worker-configuration.d.ts` with the real `SendEmail`, `EmailAttachment`, `EmailAddress`, and related types from the workerd runtime. Always use these generated types — do not define them manually.
**Note:** Workers binding uses `email` in the from object (`EmailAddress` type). REST API uses `address`. See [rest-api.md](rest-api.md).
## send()
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const response = await env.EMAIL.send({
to: "user@example.com",
from: { email: "welcome@yourdomain.com", name: "My App" },
subject: "Welcome!",
html: "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
text: "Welcome! Thanks for signing up.",
});
return new Response(`Sent: ${response.messageId}`);
},
} satisfies ExportedHandler<Env>;
```
Multiple recipients (max 50 combined to + cc + bcc):
```typescript
const response = await env.EMAIL.send({
to: ["user1@example.com", "user2@example.com"],
cc: ["manager@company.com"],
bcc: ["archive@company.com"],
from: { email: "orders@yourdomain.com", name: "Orders" },
replyTo: "support@yourdomain.com",
subject: "Order Confirmation #12345",
html: "<h1>Your order is confirmed</h1>",
text: "Your order is confirmed",
});
```
## Attachments
**Important:** The Workers binding and REST API handle attachment content differently:
- **Workers binding:** `content` accepts `string | ArrayBuffer | ArrayBufferView`. Strings are treated as **raw content** (not base64). For text files, pass the raw string. For binary files (images, PDFs), pass an `ArrayBuffer`. Note: `ArrayBuffer` does not work with `"remote": true` in local dev — deploy to test binary attachments.
- **REST API:** `content` is always a **base64-encoded string**. See [rest-api.md](rest-api.md).
```typescript
// Text file attachment — pass raw string content
const response = await env.EMAIL.send({
to: "customer@example.com",
from: "invoices@yourdomain.com",
subject: "Your Report",
html: "<h1>Report attached</h1>",
text: "Report attached.",
attachments: [{
content: "Name,Amount\nWidget A,100\nWidget B,250", // Raw text, NOT base64
filename: "report.csv",
type: "text/csv",
disposition: "attachment",
}],
});
// Binary file attachment — use ArrayBuffer
const pdfBytes = await fetchPdfFromSomewhere(); // Returns ArrayBuffer
const response = await env.EMAIL.send({
to: "customer@example.com",
from: "invoices@yourdomain.com",
subject: "Your Invoice",
html: "<h1>Invoice attached</h1>",
text: "Invoice attached.",
attachments: [{
content: pdfBytes,
filename: "invoice-12345.pdf",
type: "application/pdf",
disposition: "attachment",
}],
});
// Inline image — reference in HTML with cid:<contentId>
const imageBytes = await fetchImageFromSomewhere(); // Returns ArrayBuffer
const response = await env.EMAIL.send({
to: "user@example.com",
from: "marketing@yourdomain.com",
subject: "New Product",
html: '<img src="cid:product-hero" alt="Product" />',
attachments: [{
content: imageBytes,
filename: "product.png",
type: "image/png",
disposition: "inline",
contentId: "product-hero",
}],
});
```
Total email size (body + attachments) cannot exceed 25 MiB.
## Custom Headers
Only whitelisted headers allowed. See the [headers reference](https://developers.cloudflare.com/email-service/reference/headers/).
```typescript
const response = await env.EMAIL.send({
to: "user@example.com",
from: "notifications@yourdomain.com",
subject: "Your weekly digest",
html: "<h1>Weekly Digest</h1>",
headers: {
"In-Reply-To": "<original-message-id@yourdomain.com>",
"List-Unsubscribe": "<https://yourdomain.com/unsubscribe?id=abc123>",
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
"X-Campaign-ID": "weekly-digest-2026-03",
},
});
```
## Legacy EmailMessage API
The `EmailMessage` API remains supported. Uses raw MIME via `mimetext`:
```typescript
import { EmailMessage } from "cloudflare:email";
import { createMimeMessage } from "mimetext";
const msg = createMimeMessage();
msg.setSender({ name: "Sender", addr: "sender@yourdomain.com" });
msg.setRecipient("recipient@example.com");
msg.setSubject("Hello");
msg.addMessage({ contentType: "text/html", data: "<h1>Hello</h1>" });
await env.EMAIL.send(new EmailMessage("sender@yourdomain.com", "recipient@example.com", msg.asRaw()));
```
Requires `npm install mimetext` and `"nodejs_compat"` in compatibility flags.
## Agents SDK Email
Agents can receive and reply to emails natively via the Agents SDK.
```jsonc
// wrangler.jsonc
{
"durable_objects": {
"bindings": [{ "name": "EmailAgent", "class_name": "EmailAgent" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["EmailAgent"] }],
"send_email": [{ "name": "EMAIL", "destination_address": "reply@yourdomain.com" }]
}
```
```typescript
import { Agent } from "agents";
import { type AgentEmail } from "agents/email";
import PostalMime from "postal-mime";
export class EmailAgent extends Agent<Env, State> {
async onEmail(email: AgentEmail) {
const parsed = await PostalMime.parse(await email.getRaw());
await this.replyToEmail(email, {
fromName: "My Agent",
subject: `Re: ${parsed.subject}`,
body: "Thanks for your email! I'll look into this.",
});
}
}
```
Route emails to agents with resolvers:
```typescript
import { routeAgentEmail } from "agents";
import { createAddressBasedEmailResolver } from "agents/email";
export default {
async email(message, env) {
await routeAgentEmail(message, env, {
resolver: createAddressBasedEmailResolver("EmailAgent"),
});
},
};
```
Resolver types: `createAddressBasedEmailResolver` (recipient → instance name), `createSecureReplyEmailResolver(secret)` (HMAC-signed replies), `createCatchAllEmailResolver("Agent", "default")` (single inbox). Use `isAutoReplyEmail(email.headers)` to skip vacation responders.
## Error Handling
```typescript
try {
const response = await env.EMAIL.send({ /* ... */ });
} catch (error) {
// error.code is one of the E_* error codes
console.error(`Failed: ${error.code} - ${error.message}`);
}
```
These error codes are for the **Workers binding** (thrown as Error objects with `.code` and `.message`). The **REST API** returns standard Cloudflare API numeric error codes instead — see [rest-api.md](rest-api.md).
| Error Code | What It Means | What to Do |
|------------|---------------|------------|
| `E_VALIDATION_ERROR` | Invalid payload | Check email format, required fields |
| `E_FIELD_MISSING` | Required field missing | Add `to`, `from`, or `subject` |
| `E_TOO_MANY_RECIPIENTS` | Combined to/cc/bcc exceeds 50 | Split into multiple sends |
| `E_SENDER_NOT_VERIFIED` | Domain not onboarded | Run `wrangler email sending enable yourdomain.com` or onboard in Dashboard |
| `E_RECIPIENT_NOT_ALLOWED` | Recipient not in allowed list | Add to `allowed_destination_addresses` |
| `E_RECIPIENT_SUPPRESSED` | Address bounced or reported spam | Remove from your list; check suppression list in Dashboard |
| `E_SENDER_DOMAIN_NOT_AVAILABLE` | Domain not available for sending | Complete domain onboarding |
| `E_CONTENT_TOO_LARGE` | Content exceeds 25 MiB | Reduce attachments or body |
| `E_RATE_LIMIT_EXCEEDED` | Rate limit hit | Retry with exponential backoff |
| `E_DAILY_LIMIT_EXCEEDED` | Daily quota reached | Wait or request limit increase |
| `E_DELIVERY_FAILED` | SMTP delivery failure | Check recipient address, retry if transient |
| `E_INTERNAL_SERVER_ERROR` | Service temporarily unavailable | Retry with exponential backoff |
| `E_HEADER_NOT_ALLOWED` | Header not on whitelist | Use an allowed header; see [headers reference](https://developers.cloudflare.com/email-service/reference/headers/) |
| `E_HEADER_USE_API_FIELD` | Must use API field instead | Set `From`, `To`, etc. via the dedicated API fields, not `headers` |
| `E_HEADER_VALUE_INVALID` | Header value is malformed or empty | Fix the value format (e.g., List-Unsubscribe needs angle-bracket URIs) |
| `E_HEADER_VALUE_TOO_LONG` | Header value exceeds 2,048 bytes | Shorten the header value |
| `E_HEADER_NAME_INVALID` | Invalid header name | Fix characters or keep under 100 bytes |
| `E_HEADERS_TOO_LARGE` | Total headers exceed 16 KB | Reduce number or size of custom headers |
| `E_HEADERS_TOO_MANY` | More than 20 non-X headers | Reduce to 20 or fewer whitelisted headers |
For `E_RATE_LIMIT_EXCEEDED` and `E_DELIVERY_FAILED`, retry with exponential backoff. For validation errors (`E_VALIDATION_ERROR`, `E_FIELD_MISSING`, `E_SENDER_NOT_VERIFIED`), fix the request — retrying won't help.
## Restricted Bindings
Restrict which `from` addresses a binding can use:
```jsonc
{
"send_email": [{
"name": "RESTRICTED_EMAIL",
"allowed_sender_addresses": ["noreply@yourdomain.com", "support@yourdomain.com"]
}]
}
```