mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-08 21:07:45 +00:00
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:
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"]
|
||||
}]
|
||||
}
|
||||
```
|
||||
Reference in new issue
Block a user