update skills

This commit is contained in:
2026-03-17 16:53:22 -07:00
parent 0b0783ef8e
commit f9a530667e
389 changed files with 54512 additions and 1 deletions

View File

@@ -0,0 +1,89 @@
# Cloudflare Email Routing Skill Reference
## Overview
Cloudflare Email Routing enables custom email addresses for your domain that route to verified destination addresses. It's free, privacy-focused (no storage/access), and includes Email Workers for programmatic email processing.
**Available to all Cloudflare customers using Cloudflare as authoritative nameserver.**
## Quick Start
```typescript
// Basic email handler
export default {
async email(message, env, ctx) {
// CRITICAL: Must consume stream before response
const parser = new PostalMime.default();
const email = await parser.parse(await message.raw.arrayBuffer());
// Process email
console.log(`From: ${message.from}, Subject: ${email.subject}`);
// Forward or reject
await message.forward("verified@destination.com");
}
} satisfies ExportedHandler<Env>;
```
## Reading Order
**Start here based on your goal:**
1. **New to Email Routing?** → [configuration.md](configuration.md) → [patterns.md](patterns.md)
2. **Adding Workers?** → [api.md](api.md) § Worker Runtime API → [patterns.md](patterns.md)
3. **Sending emails?** → [api.md](api.md) § SendEmail Binding
4. **Managing via API?** → [api.md](api.md) § REST API Operations
5. **Debugging issues?** → [gotchas.md](gotchas.md)
## Decision Tree
```
Need to receive emails?
├─ Simple forwarding only? → Dashboard rules (configuration.md)
├─ Complex logic/filtering? → Email Workers (api.md + patterns.md)
└─ Parse attachments/body? → postal-mime library (patterns.md § Parse Email)
Need to send emails?
├─ From Worker? → SendEmail binding (api.md § SendEmail)
└─ From external app? → Use external SMTP/API service
Having issues?
├─ Email not arriving? → gotchas.md § Mail Authentication
├─ Worker crashing? → gotchas.md § Stream Consumption
└─ Forward failing? → gotchas.md § Destination Verification
```
## Key Concepts
**Routing Rules**: Pattern-based forwarding configured via Dashboard/API. Simple but limited.
**Email Workers**: Custom TypeScript handlers with full email access. Handles complex logic, parsing, storage, rejection.
**SendEmail Binding**: Outbound email API for Workers. Transactional email only (no marketing/bulk).
**ForwardableEmailMessage**: Runtime interface for incoming emails. Provides headers, raw stream, forward/reject methods.
## In This Reference
- **[configuration.md](configuration.md)** - Setup, deployment, wrangler config
- **[api.md](api.md)** - REST API + Worker runtime API + types
- **[patterns.md](patterns.md)** - Common patterns with working examples
- **[gotchas.md](gotchas.md)** - Critical pitfalls, troubleshooting, limits
## Architecture
```
Internet → MX Records → Cloudflare Email Routing
├─ Routing Rules (dashboard)
└─ Email Worker (your code)
├─ Forward to destination
├─ Reject with reason
├─ Store in R2/KV/D1
└─ Send outbound (SendEmail)
```
## See Also
- [Cloudflare Docs: Email Routing](https://developers.cloudflare.com/email-routing/)
- [Cloudflare Docs: Email Workers](https://developers.cloudflare.com/email-routing/email-workers/)
- [postal-mime npm package](https://www.npmjs.com/package/postal-mime)

View File

@@ -0,0 +1,195 @@
# Email Routing API Reference
## Worker Runtime API
### Email Handler Interface
```typescript
interface ExportedHandler<Env = unknown> {
email?(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): void | Promise<void>;
}
```
### ForwardableEmailMessage
Main interface for incoming emails:
```typescript
interface ForwardableEmailMessage {
readonly from: string; // Envelope sender (e.g., "sender@example.com")
readonly to: string; // Envelope recipient (e.g., "you@yourdomain.com")
readonly headers: Headers; // Web API Headers object
readonly raw: ReadableStream; // Raw MIME message stream
setReject(reason: string): void;
forward(rcptTo: string, headers?: Headers): Promise<void>;
}
```
**Key Properties:**
| Property | Type | Description |
|----------|------|-------------|
| `from` | `string` | Envelope sender (MAIL FROM), not header From |
| `to` | `string` | Envelope recipient (RCPT TO), not header To |
| `headers` | `Headers` | Email headers (Subject, From, To, etc.) |
| `raw` | `ReadableStream` | Raw MIME message (consume once only) |
**Methods:**
- `setReject(reason)`: Reject email with bounce message
- `forward(rcptTo, headers?)`: Forward to verified destination, optionally add headers
### Headers Object
Standard Web API Headers interface:
```typescript
// Access headers
const subject = message.headers.get("subject");
const from = message.headers.get("from");
const messageId = message.headers.get("message-id");
// Check spam score
const spamScore = parseFloat(message.headers.get("x-cf-spamh-score") || "0");
if (spamScore > 5) {
message.setReject("Spam detected");
}
```
### Common Headers
`subject`, `from`, `to`, `x-cf-spamh-score` (spam score), `message-id` (deduplication), `dkim-signature` (auth)
### Envelope vs Header Addresses
**Critical distinction:**
```typescript
// Envelope addresses (routing, auth checks)
message.from // "bounce@sender.com" (actual sender)
message.to // "you@yourdomain.com" (your address)
// Header addresses (display, user-facing)
message.headers.get("from") // "Alice <alice@sender.com>"
message.headers.get("to") // "Bob <you@yourdomain.com>"
```
**Use envelope addresses for:**
- Authentication/SPF checks
- Routing decisions
- Bounce handling
**Use header addresses for:**
- Display to users
- Reply-To logic
- User-facing filtering
## SendEmail Binding
Outbound email API for transactional messages.
### Configuration
```jsonc
// wrangler.jsonc
{
"send_email": [
{ "name": "EMAIL" }
]
}
```
### TypeScript Types
```typescript
interface Env {
EMAIL: SendEmail;
}
interface SendEmail {
send(message: EmailMessage): Promise<void>;
}
interface EmailMessage {
from: string | { name?: string; email: string };
to: string | { name?: string; email: string } | Array<string | { name?: string; email: string }>;
subject: string;
text?: string;
html?: string;
headers?: Headers;
reply_to?: string | { name?: string; email: string };
}
```
### Send Email Example
```typescript
interface Env {
EMAIL: SendEmail;
}
export default {
async fetch(request, env, ctx): Promise<Response> {
await env.EMAIL.send({
from: { name: "Acme Corp", email: "noreply@yourdomain.com" },
to: [
{ name: "Alice", email: "alice@example.com" },
"bob@example.com"
],
subject: "Your order #12345 has shipped",
text: "Track your package at: https://track.example.com/12345",
html: "<p>Track your package at: <a href='https://track.example.com/12345'>View tracking</a></p>",
reply_to: { name: "Support", email: "support@yourdomain.com" }
});
return new Response("Email sent");
}
} satisfies ExportedHandler<Env>;
```
### SendEmail Constraints
- **From address**: Must be on verified domain (your domain with Email Routing enabled)
- **Volume limits**: Transactional only, no bulk/marketing email
- **Rate limits**: 100 emails/minute on Free plan, higher on Paid
- **No attachments**: Use links to hosted files instead
- **No DKIM control**: Cloudflare signs automatically
## REST API Operations
Base URL: `https://api.cloudflare.com/client/v4`
### Authentication
```bash
curl -H "Authorization: Bearer $API_TOKEN" https://api.cloudflare.com/client/v4/...
```
### Key Endpoints
| Operation | Method | Endpoint |
|-----------|--------|----------|
| Enable routing | POST | `/zones/{zone_id}/email/routing/enable` |
| Disable routing | POST | `/zones/{zone_id}/email/routing/disable` |
| List rules | GET | `/zones/{zone_id}/email/routing/rules` |
| Create rule | POST | `/zones/{zone_id}/email/routing/rules` |
| Verify destination | POST | `/zones/{zone_id}/email/routing/addresses` |
| List destinations | GET | `/zones/{zone_id}/email/routing/addresses` |
### Create Routing Rule Example
```bash
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/email/routing/rules" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"name": "Forward sales",
"matchers": [{"type": "literal", "field": "to", "value": "sales@yourdomain.com"}],
"actions": [{"type": "forward", "value": ["alice@company.com"]}],
"priority": 0
}'
```
Matcher types: `literal` (exact match), `all` (catch-all).

View File

@@ -0,0 +1,186 @@
# Email Routing Configuration
## Wrangler Configuration
### Basic Email Worker
```jsonc
// wrangler.jsonc
{
"name": "email-worker",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"send_email": [{ "name": "EMAIL" }]
}
```
```typescript
// src/index.ts
export default {
async email(message, env, ctx) {
await message.forward("destination@example.com");
}
} satisfies ExportedHandler;
```
### With Storage Bindings
```jsonc
{
"name": "email-processor",
"send_email": [{ "name": "EMAIL" }],
"kv_namespaces": [{ "binding": "KV", "id": "abc123" }],
"r2_buckets": [{ "binding": "R2", "bucket_name": "emails" }],
"d1_databases": [{ "binding": "DB", "database_id": "def456" }]
}
```
```typescript
interface Env {
EMAIL: SendEmail;
KV: KVNamespace;
R2: R2Bucket;
DB: D1Database;
}
```
## Local Development
```bash
npx wrangler dev
# Test with curl
curl -X POST 'http://localhost:8787/__email' \
--header 'content-type: message/rfc822' \
--data 'From: test@example.com
To: you@yourdomain.com
Subject: Test
Body'
```
## Deployment
```bash
npx wrangler deploy
```
**Connect to Email Routing:**
Dashboard: Email > Email Routing > [domain] > Settings > Email Workers > Select worker
API:
```bash
curl -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/email/routing/settings" \
-H "Authorization: Bearer $API_TOKEN" \
-d '{"enabled": true, "worker": "email-worker"}'
```
## DNS (Auto-Created)
```dns
yourdomain.com. IN MX 1 isaac.mx.cloudflare.net.
yourdomain.com. IN MX 2 linda.mx.cloudflare.net.
yourdomain.com. IN MX 3 amir.mx.cloudflare.net.
yourdomain.com. IN TXT "v=spf1 include:_spf.mx.cloudflare.net ~all"
```
## Secrets & Variables
```bash
# Secrets (encrypted)
npx wrangler secret put API_KEY
# Variables (plain)
# wrangler.jsonc
{ "vars": { "THRESHOLD": "5.0" } }
```
```typescript
interface Env {
API_KEY: string;
THRESHOLD: string;
}
```
## TypeScript Setup
```bash
npm install --save-dev @cloudflare/workers-types
```
```json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"moduleResolution": "bundler",
"strict": true
}
}
```
```typescript
import type { ForwardableEmailMessage } from "@cloudflare/workers-types";
export default {
async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): Promise<void> {
await message.forward("dest@example.com");
}
} satisfies ExportedHandler<Env>;
```
## Dependencies
```bash
npm install postal-mime
```
```typescript
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
const parser = new PostalMime();
const email = await parser.parse(await message.raw.arrayBuffer());
console.log(email.subject);
await message.forward("inbox@corp.com");
}
} satisfies ExportedHandler;
```
## Multi-Environment
```bash
# wrangler.dev.jsonc
{ "name": "worker-dev", "vars": { "ENV": "dev" } }
# wrangler.prod.jsonc
{ "name": "worker-prod", "vars": { "ENV": "prod" } }
npx wrangler deploy --config wrangler.dev.jsonc
npx wrangler deploy --config wrangler.prod.jsonc
```
## CI/CD (GitHub Actions)
```yaml
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
```

View File

@@ -0,0 +1,196 @@
# Gotchas & Troubleshooting
## Critical Pitfalls
### Stream Consumption (MOST COMMON)
**Problem:** "stream already consumed" or worker hangs
**Cause:** `message.raw` is `ReadableStream` - consume once only
**Solution:**
```typescript
// ❌ WRONG
const email1 = await parser.parse(await message.raw.arrayBuffer());
const email2 = await parser.parse(await message.raw.arrayBuffer()); // FAILS
// ✅ CORRECT
const raw = await message.raw.arrayBuffer();
const email = await parser.parse(raw);
```
Consume `message.raw` immediately before any async operations.
### Destination Verification
**Problem:** Emails not forwarding
**Cause:** Destination unverified
**Solution:** Add destination, check inbox for verification email, click link. Verify status: `GET /zones/{id}/email/routing/addresses`
### Mail Authentication
**Problem:** Legitimate emails rejected
**Cause:** Missing SPF/DKIM/DMARC on sender domain
**Solution:** Configure sender DNS:
```dns
example.com. IN TXT "v=spf1 include:_spf.example.com ~all"
selector._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=..."
_dmarc.example.com. IN TXT "v=DMARC1; p=quarantine"
```
### Envelope vs Header
**Problem:** Filtering on wrong address
**Solution:**
```typescript
// Routing/auth: envelope
if (message.from === "trusted@example.com") { }
// Display: headers
const display = message.headers.get("from");
```
### SendEmail Limits
| Issue | Limit | Solution |
|-------|-------|----------|
| From domain | Must own | Use Email Routing domain |
| Volume | ~100/min Free | Upgrade or throttle |
| Attachments | Not supported | Link to R2 |
| Type | Transactional | No bulk |
## Common Errors
### CPU Time Exceeded
**Cause:** Heavy parsing, large emails
**Solution:**
```typescript
const size = parseInt(message.headers.get("content-length") || "0") / 1024 / 1024;
if (size > 20) {
message.setReject("Too large");
return;
}
ctx.waitUntil(expensiveWork());
await message.forward("dest@example.com");
```
### Rule Not Triggering
**Causes:** Priority conflict, matcher error, catch-all override
**Solution:** Check priority (lower=first), verify exact match, confirm destination verified
### Undefined Property
**Cause:** Missing header
**Solution:**
```typescript
// ❌ WRONG
const subj = message.headers.get("subject").toLowerCase();
// ✅ CORRECT
const subj = message.headers.get("subject")?.toLowerCase() || "";
```
## Limits
| Resource | Free | Paid |
|----------|------|------|
| Email size | 25 MB | 25 MB |
| Rules | 200 | 200 |
| Destinations | 200 | 200 |
| CPU time | 10ms | 50ms |
| SendEmail | ~100/min | Higher |
## Debugging
### Local
```bash
npx wrangler dev
curl -X POST 'http://localhost:8787/__email' \
--header 'content-type: message/rfc822' \
--data 'From: test@example.com
To: you@yourdomain.com
Subject: Test
Body'
```
### Production
```bash
npx wrangler tail
```
### Pattern
```typescript
export default {
async email(message, env, ctx) {
try {
console.log("From:", message.from);
await process(message, env);
} catch (err) {
console.error(err);
message.setReject(err.message);
}
}
} satisfies ExportedHandler;
```
## Auth Troubleshooting
### Check Status
```typescript
const auth = message.headers.get("authentication-results") || "";
console.log({
spf: auth.includes("spf=pass"),
dkim: auth.includes("dkim=pass"),
dmarc: auth.includes("dmarc=pass")
});
if (!auth.includes("pass")) {
message.setReject("Failed auth");
return;
}
```
### SPF Issues
**Causes:** Forwarding breaks SPF, too many lookups (>10), missing includes
**Solution:**
```dns
; ✅ Good
example.com. IN TXT "v=spf1 include:_spf.google.com ~all"
; ❌ Bad - too many
example.com. IN TXT "v=spf1 include:a.com include:b.com ... ~all"
```
### DMARC Alignment
**Cause:** From domain must match SPF/DKIM domain
## Best Practices
1. Consume `message.raw` immediately
2. Verify destinations
3. Handle missing headers (`?.`)
4. Use envelope for routing
5. Check spam scores
6. Test locally first
7. Use `ctx.waitUntil` for background work
8. Size-check early

View File

@@ -0,0 +1,229 @@
# Common Patterns
## 1. Allowlist/Blocklist
```typescript
// Allowlist
const allowed = ["user@example.com", "trusted@corp.com"];
if (!allowed.includes(message.from)) {
message.setReject("Not allowed");
return;
}
await message.forward("inbox@corp.com");
```
## 2. Parse Email Body
```typescript
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
// CRITICAL: Consume stream immediately
const raw = await message.raw.arrayBuffer();
const parser = new PostalMime();
const email = await parser.parse(raw);
console.log({
subject: email.subject,
text: email.text,
html: email.html,
from: email.from.address,
attachments: email.attachments.length
});
await message.forward("inbox@corp.com");
}
} satisfies ExportedHandler;
```
## 3. Spam Filter
```typescript
const score = parseFloat(message.headers.get("x-cf-spamh-score") || "0");
if (score > 5) {
message.setReject("Spam detected");
return;
}
await message.forward("inbox@corp.com");
```
## 4. Archive to R2
```typescript
interface Env { R2: R2Bucket; }
export default {
async email(message, env, ctx) {
const raw = await message.raw.arrayBuffer();
const key = `${new Date().toISOString()}-${message.from}.eml`;
await env.R2.put(key, raw, {
httpMetadata: { contentType: "message/rfc822" }
});
await message.forward("inbox@corp.com");
}
} satisfies ExportedHandler<Env>;
```
## 5. Store Metadata in KV
```typescript
import PostalMime from 'postal-mime';
interface Env { KV: KVNamespace; }
export default {
async email(message, env, ctx) {
const raw = await message.raw.arrayBuffer();
const parser = new PostalMime();
const email = await parser.parse(raw);
const metadata = {
from: email.from.address,
subject: email.subject,
timestamp: new Date().toISOString(),
size: raw.byteLength
};
await env.KV.put(`email:${Date.now()}`, JSON.stringify(metadata));
await message.forward("inbox@corp.com");
}
} satisfies ExportedHandler<Env>;
```
## 6. Subject-Based Routing
```typescript
export default {
async email(message, env, ctx) {
const subject = message.headers.get("subject")?.toLowerCase() || "";
if (subject.includes("[urgent]")) {
await message.forward("oncall@corp.com");
} else if (subject.includes("[billing]")) {
await message.forward("billing@corp.com");
} else if (subject.includes("[support]")) {
await message.forward("support@corp.com");
} else {
await message.forward("general@corp.com");
}
}
} satisfies ExportedHandler;
```
## 7. Auto-Reply
```typescript
interface Env {
EMAIL: SendEmail;
REPLIED: KVNamespace;
}
export default {
async email(message, env, ctx) {
const msgId = message.headers.get("message-id");
if (msgId && await env.REPLIED.get(msgId)) {
await message.forward("archive@corp.com");
return;
}
ctx.waitUntil((async () => {
await env.EMAIL.send({
from: "noreply@yourdomain.com",
to: message.from,
subject: "Re: " + (message.headers.get("subject") || ""),
text: "Thank you. We'll respond within 24h."
});
if (msgId) await env.REPLIED.put(msgId, "1", { expirationTtl: 604800 });
})());
await message.forward("support@corp.com");
}
} satisfies ExportedHandler<Env>;
```
## 8. Extract Attachments
```typescript
import PostalMime from 'postal-mime';
interface Env { ATTACHMENTS: R2Bucket; }
export default {
async email(message, env, ctx) {
const parser = new PostalMime();
const email = await parser.parse(await message.raw.arrayBuffer());
for (const att of email.attachments) {
const key = `${Date.now()}-${att.filename}`;
await env.ATTACHMENTS.put(key, att.content, {
httpMetadata: { contentType: att.mimeType }
});
}
await message.forward("inbox@corp.com");
}
} satisfies ExportedHandler<Env>;
```
## 9. Log to D1
```typescript
import PostalMime from 'postal-mime';
interface Env { DB: D1Database; }
export default {
async email(message, env, ctx) {
const parser = new PostalMime();
const email = await parser.parse(await message.raw.arrayBuffer());
ctx.waitUntil(
env.DB.prepare("INSERT INTO log (ts, from_addr, subj) VALUES (?, ?, ?)")
.bind(new Date().toISOString(), email.from.address, email.subject || "")
.run()
);
await message.forward("inbox@corp.com");
}
} satisfies ExportedHandler<Env>;
```
## 10. Multi-Tenant
```typescript
interface Env { TENANTS: KVNamespace; }
export default {
async email(message, env, ctx) {
const subdomain = message.to.split("@")[1].split(".")[0];
const config = await env.TENANTS.get(subdomain, "json") as { forward: string } | null;
if (!config) {
message.setReject("Unknown tenant");
return;
}
await message.forward(config.forward);
}
} satisfies ExportedHandler<Env>;
```
## Summary
| Pattern | Use Case | Storage |
|---------|----------|---------|
| Allowlist | Security | None |
| Parse | Body/attachments | None |
| Spam Filter | Reduce spam | None |
| R2 Archive | Email storage | R2 |
| KV Meta | Analytics | KV |
| Subject Route | Dept routing | None |
| Auto-Reply | Support | KV |
| Attachments | Doc mgmt | R2 |
| D1 Log | Audit trail | D1 |
| Multi-Tenant | SaaS | KV |