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,151 @@
# Cloudflare Email Workers
Process incoming emails programmatically using Cloudflare Workers runtime.
## Overview
Email Workers enable custom email processing logic at the edge. Build spam filters, auto-responders, ticket systems, notification handlers, and more using the same Workers runtime you use for HTTP requests.
**Key capabilities**:
- Process inbound emails with full message access
- Forward to verified destinations
- Send replies with proper threading
- Parse MIME content and attachments
- Integrate with KV, R2, D1, and external APIs
## Quick Start
### Minimal ES Modules Handler
```typescript
export default {
async email(message, env, ctx) {
// Reject spam
if (message.from.includes('spam.com')) {
message.setReject('Blocked');
return;
}
// Forward to inbox
await message.forward('inbox@example.com');
}
};
```
### Core Operations
| Operation | Method | Use Case |
|-----------|--------|----------|
| Forward | `message.forward(to, headers?)` | Route to verified destination |
| Reject | `message.setReject(reason)` | Block with SMTP error |
| Reply | `message.reply(emailMessage)` | Auto-respond with threading |
| Parse | postal-mime library | Extract subject, body, attachments |
## Reading Order
For comprehensive understanding, read files in this order:
1. **README.md** (this file) - Overview and quick start
2. **configuration.md** - Setup, deployment, bindings
3. **api.md** - Complete API reference
4. **patterns.md** - Real-world implementation examples
5. **gotchas.md** - Critical pitfalls and debugging
## In This Reference
| File | Description | Key Topics |
|------|-------------|------------|
| [api.md](./api.md) | Complete API reference | ForwardableEmailMessage, SendEmail bindings, reply() method, postal-mime/mimetext APIs |
| [configuration.md](./configuration.md) | Setup and configuration | wrangler.jsonc, bindings, deployment, dependencies |
| [patterns.md](./patterns.md) | Real-world examples | Allowlists from KV, auto-reply with threading, attachment extraction, webhook notifications |
| [gotchas.md](./gotchas.md) | Pitfalls and debugging | Stream consumption, ctx.waitUntil errors, security, limits |
## Architecture
```
Incoming Email → Email Routing → Email Worker
Process + Decide
┌───────────────┼───────────────┐
↓ ↓ ↓
Forward Reply Reject
```
**Event flow**:
1. Email arrives at your domain
2. Email Routing matches route (e.g., `support@example.com`)
3. Bound Email Worker receives `ForwardableEmailMessage`
4. Worker processes and takes action (forward/reply/reject)
5. Email delivered or rejected based on worker logic
## Key Concepts
### Envelope vs Headers
- **Envelope addresses** (`message.from`, `message.to`): SMTP transport addresses (trusted)
- **Header addresses** (parsed from body): Display addresses (can be spoofed)
Use envelope addresses for security decisions.
### Single-Use Streams
`message.raw` is a ReadableStream that can only be read once. Buffer to ArrayBuffer for multiple uses.
```typescript
// Buffer first
const buffer = await new Response(message.raw).arrayBuffer();
const email = await PostalMime.parse(buffer);
```
See [gotchas.md](./gotchas.md#readablestream-can-only-be-consumed-once) for details.
### Verified Destinations
`forward()` only works with addresses verified in the Cloudflare Email Routing dashboard. Add destinations before deployment.
## Use Cases
- **Spam filtering**: Block based on sender, content, or reputation
- **Auto-responders**: Send acknowledgment replies with threading
- **Ticket creation**: Parse emails and create support tickets
- **Email archival**: Store in KV, R2, or D1
- **Notification routing**: Forward to Slack, Discord, or webhooks
- **Attachment processing**: Extract files to R2 storage
- **Multi-tenant routing**: Route based on recipient subdomain
- **Size filtering**: Reject oversized attachments
## Limits
| Limit | Value |
|-------|-------|
| Max message size | 25 MiB |
| Max routing rules | 200 |
| Max destinations | 200 |
| CPU time (free tier) | 10ms |
| CPU time (paid tier) | 50ms |
See [gotchas.md](./gotchas.md#limits-reference) for complete limits table.
## Prerequisites
Before deploying Email Workers:
1. **Enable Email Routing** in Cloudflare dashboard for your domain
2. **Verify destination addresses** for forwarding
3. **Configure DMARC/SPF** for sending domains (required for replies)
4. **Set up wrangler.jsonc** with SendEmail binding
See [configuration.md](./configuration.md) for detailed setup.
## Service Worker Syntax (Deprecated)
Modern projects should use ES modules format shown above. Service Worker syntax (`addEventListener('email', ...)`) is deprecated but still supported.
## See Also
- [Email Routing Documentation](https://developers.cloudflare.com/email-routing/)
- [Workers Platform](https://developers.cloudflare.com/workers/)
- [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/)
- [postal-mime on npm](https://www.npmjs.com/package/postal-mime)
- [mimetext on npm](https://www.npmjs.com/package/mimetext)

View File

@@ -0,0 +1,237 @@
# Email Workers API Reference
Complete API reference for Cloudflare Email Workers runtime.
## ForwardableEmailMessage Interface
The main interface passed to email handlers.
```typescript
interface ForwardableEmailMessage {
readonly from: string; // Envelope MAIL FROM (SMTP sender)
readonly to: string; // Envelope RCPT TO (SMTP recipient)
readonly headers: Headers; // Web-standard Headers object
readonly raw: ReadableStream; // Raw MIME message (single-use stream)
readonly rawSize: number; // Total message size in bytes
setReject(reason: string): void;
forward(rcptTo: string, headers?: Headers): Promise<void>;
reply(message: EmailMessage): Promise<void>;
}
```
### Properties
| Property | Type | Description |
|----------|------|-------------|
| `from` | string | Envelope sender (SMTP MAIL FROM) - use for security |
| `to` | string | Envelope recipient (SMTP RCPT TO) |
| `headers` | Headers | Message headers (Subject, Message-ID, etc.) |
| `raw` | ReadableStream | Raw MIME message (**single-use**, buffer first) |
| `rawSize` | number | Message size in bytes |
### Methods
#### setReject(reason: string): void
Reject with permanent SMTP 5xx error. Email not delivered, sender may receive bounce.
```typescript
if (blockList.includes(message.from)) {
message.setReject('Sender blocked');
}
```
#### forward(rcptTo: string, headers?: Headers): Promise<void>
Forward to verified destination. Only `X-*` custom headers allowed.
```typescript
await message.forward('inbox@example.com');
// With custom headers
const h = new Headers();
h.set('X-Processed-By', 'worker');
await message.forward('inbox@example.com', h);
```
#### reply(message: EmailMessage): Promise<void>
Send a reply to the original sender (March 2025 feature).
```typescript
import { EmailMessage } from 'cloudflare:email';
import { createMimeMessage } from 'mimetext';
const msg = createMimeMessage();
msg.setSender({ name: 'Support', addr: 'support@example.com' });
msg.setRecipient(message.from);
msg.setSubject(`Re: ${message.headers.get('Subject')}`);
msg.setHeader('In-Reply-To', message.headers.get('Message-ID'));
msg.setHeader('References', message.headers.get('References') || '');
msg.addMessage({
contentType: 'text/plain',
data: 'Thank you for your message.'
});
await message.reply(new EmailMessage(
'support@example.com',
message.from,
msg.asRaw()
));
```
**Requirements**:
- Incoming email needs valid DMARC
- Reply once per event, recipient = `message.from`
- Sender domain = receiving domain, with DMARC/SPF/DKIM
- Max 100 `References` entries
- Threading: `In-Reply-To` (original Message-ID), `References`, new `Message-ID`
## EmailMessage Constructor
```typescript
import { EmailMessage } from 'cloudflare:email';
new EmailMessage(from: string, to: string, raw: ReadableStream | string)
```
Used for sending emails (replies or via SendEmail binding). Domain must be verified.
## SendEmail Interface
```typescript
interface SendEmail {
send(message: EmailMessage): Promise<void>;
}
// Usage
await env.EMAIL.send(new EmailMessage(from, to, mimeContent));
```
## SendEmail Binding Types
```jsonc
{
"send_email": [
{ "name": "EMAIL" }, // Type 1: Any verified address
{ "name": "LOGS", "destination_address": "logs@example.com" }, // Type 2: Single dest
{ "name": "TEAM", "allowed_destination_addresses": ["a@ex.com", "b@ex.com"] }, // Type 3: Dest allowlist
{ "name": "NOREPLY", "allowed_sender_addresses": ["noreply@ex.com"] } // Type 4: Sender allowlist
]
}
```
## postal-mime Parsed Output
postal-mime v2.7.3 parses incoming emails into structured data.
```typescript
interface ParsedEmail {
headers: Array<{ key: string; value: string }>;
from: { name: string; address: string } | null;
to: Array<{ name: string; address: string }> | { name: string; address: string } | null;
cc: Array<{ name: string; address: string }> | null;
bcc: Array<{ name: string; address: string }> | null;
subject: string;
messageId: string | null;
inReplyTo: string | null;
references: string | null;
date: string | null;
html: string | null;
text: string | null;
attachments: Array<{
filename: string;
mimeType: string;
disposition: string | null;
related: boolean;
contentId: string | null;
content: Uint8Array;
}>;
}
```
### Usage
```typescript
import PostalMime from 'postal-mime';
const buffer = await new Response(message.raw).arrayBuffer();
const email = await PostalMime.parse(buffer);
console.log(email.subject);
console.log(email.from?.address);
console.log(email.text);
console.log(email.attachments.length);
```
## mimetext API Quick Reference
mimetext v3.0.27 composes outgoing emails.
```typescript
import { createMimeMessage } from 'mimetext';
const msg = createMimeMessage();
// Sender
msg.setSender({ name: 'John Doe', addr: 'john@example.com' });
// Recipients
msg.setRecipient('alice@example.com');
msg.setRecipients(['bob@example.com', 'carol@example.com']);
msg.setCc('manager@example.com');
msg.setBcc(['audit@example.com']);
// Headers
msg.setSubject('Meeting Notes');
msg.setHeader('In-Reply-To', '<previous-message-id>');
msg.setHeader('References', '<msg1> <msg2>');
msg.setHeader('Message-ID', `<${crypto.randomUUID()}@example.com>`);
// Content
msg.addMessage({
contentType: 'text/plain',
data: 'Plain text content'
});
msg.addMessage({
contentType: 'text/html',
data: '<p>HTML content</p>'
});
// Attachments
msg.addAttachment({
filename: 'report.pdf',
contentType: 'application/pdf',
data: pdfBuffer // Uint8Array or base64 string
});
// Generate raw MIME
const raw = msg.asRaw(); // Returns string
```
## TypeScript Types
```typescript
import {
ForwardableEmailMessage,
EmailMessage
} from 'cloudflare:email';
interface Env {
EMAIL: SendEmail;
EMAIL_ARCHIVE: KVNamespace;
ALLOWED_SENDERS: KVNamespace;
}
export default {
async email(
message: ForwardableEmailMessage,
env: Env,
ctx: ExecutionContext
): Promise<void> {
// Fully typed
}
};
```

View File

@@ -0,0 +1,112 @@
# Email Workers Configuration
## wrangler.jsonc
```jsonc
{
"name": "email-worker",
"main": "src/index.ts",
"compatibility_date": "2025-01-27",
"send_email": [
{ "name": "EMAIL" }, // Unrestricted
{ "name": "EMAIL_LOGS", "destination_address": "logs@example.com" }, // Single dest
{ "name": "EMAIL_TEAM", "allowed_destination_addresses": ["a@ex.com", "b@ex.com"] },
{ "name": "EMAIL_NOREPLY", "allowed_sender_addresses": ["noreply@ex.com"] }
],
"kv_namespaces": [{ "binding": "ARCHIVE", "id": "xxx" }],
"r2_buckets": [{ "binding": "ATTACHMENTS", "bucket_name": "email-attachments" }],
"vars": { "WEBHOOK_URL": "https://hooks.example.com" }
}
```
## TypeScript Types
```typescript
interface Env {
EMAIL: SendEmail;
ARCHIVE: KVNamespace;
ATTACHMENTS: R2Bucket;
WEBHOOK_URL: string;
}
export default {
async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) {}
};
```
## Dependencies
```bash
npm install postal-mime mimetext
npm install -D @cloudflare/workers-types wrangler typescript
```
Use postal-mime v2.x, mimetext v3.x.
## tsconfig.json
```json
{
"compilerOptions": {
"target": "ES2022", "module": "ES2022", "lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"moduleResolution": "bundler", "strict": true
}
}
```
## Local Development
```bash
npx wrangler dev
# Test receiving
curl --request POST 'http://localhost:8787/cdn-cgi/handler/email' \
--url-query 'from=sender@example.com' --url-query 'to=recipient@example.com' \
--header 'Content-Type: text/plain' --data-raw 'Subject: Test\n\nHello'
```
Sent emails write to local `.eml` files.
## Deployment Checklist
- [ ] Enable Email Routing in dashboard
- [ ] Verify destination addresses
- [ ] Configure DMARC/SPF/DKIM for sending
- [ ] Create KV/R2 resources if needed
- [ ] Update wrangler.jsonc with production IDs
```bash
npx wrangler deploy
npx wrangler deployments list
```
## Dashboard Setup
1. **Email Routing:** Domain → Email → Enable Email Routing
2. **Verify addresses:** Email → Destination addresses → Add & verify
3. **Bind Worker:** Email → Email Workers → Create route → Select pattern & Worker
4. **DMARC:** Add TXT `_dmarc.domain.com`: `v=DMARC1; p=quarantine;`
## Secrets
```bash
npx wrangler secret put API_KEY
# Access: env.API_KEY
```
## Monitoring
```bash
npx wrangler tail
npx wrangler tail --status error
npx wrangler tail --format json
```
## Troubleshooting
| Error | Fix |
|-------|-----|
| "Binding not found" | Check `send_email` name matches code |
| "Invalid destination" | Verify in Email Routing dashboard |
| Type errors | Install `@cloudflare/workers-types` |

View File

@@ -0,0 +1,125 @@
# Email Workers Gotchas
## Critical Issues
### ReadableStream Single-Use
```typescript
// ❌ WRONG: Stream consumed twice
const email = await PostalMime.parse(await new Response(message.raw).arrayBuffer());
const rawText = await new Response(message.raw).text(); // EMPTY!
// ✅ CORRECT: Buffer first
const buffer = await new Response(message.raw).arrayBuffer();
const email = await PostalMime.parse(buffer);
const rawText = new TextDecoder().decode(buffer);
```
### ctx.waitUntil() Errors Silent
```typescript
// ❌ Errors dropped silently
ctx.waitUntil(fetch(webhookUrl, { method: 'POST', body: data }));
// ✅ Catch and log
ctx.waitUntil(
fetch(webhookUrl, { method: 'POST', body: data })
.catch(err => env.ERROR_LOG.put(`error:${Date.now()}`, err.message))
);
```
## Security
### Envelope vs Header From (Spoofing)
```typescript
const envelopeFrom = message.from; // SMTP MAIL FROM (trusted)
const headerFrom = (await PostalMime.parse(buffer)).from?.address; // (untrusted)
// Use envelope for security decisions
```
### Input Validation
```typescript
if (message.rawSize > 5_000_000) { message.setReject('Too large'); return; }
if ((message.headers.get('Subject') || '').length > 1000) {
message.setReject('Invalid subject'); return;
}
```
### DMARC for Replies
Replies fail silently without DMARC. Verify: `dig TXT _dmarc.example.com`
## Parsing
### Address Parsing
```typescript
const email = await PostalMime.parse(buffer);
const fromAddress = email.from?.address || 'unknown';
const toAddresses = Array.isArray(email.to) ? email.to.map(t => t.address) : [email.to?.address];
```
### Character Encoding
Let postal-mime handle decoding - `email.subject`, `email.text`, `email.html` are UTF-8.
## API Behavior
### setReject() vs throw
```typescript
// setReject() for SMTP rejection
if (blockList.includes(message.from)) { message.setReject('Blocked'); return; }
// throw for worker errors
if (!env.KV) throw new Error('KV not configured');
```
### forward() Only X-* Headers
```typescript
headers.set('X-Processed-By', 'worker'); // ✅ Works
headers.set('Subject', 'Modified'); // ❌ Dropped
```
### Reply Requires Verified Domain
```typescript
// Use same domain as receiving address
const receivingDomain = message.to.split('@')[1];
await message.reply(new EmailMessage(`noreply@${receivingDomain}`, message.from, rawMime));
```
## Performance
### CPU Limit
```typescript
// Skip parsing large emails
if (message.rawSize > 5_000_000) {
await message.forward('inbox@example.com');
return;
}
```
Monitor: `npx wrangler tail`
## Limits
| Limit | Value |
|-------|-------|
| Max message size | 25 MiB |
| Max rules/zone | 200 |
| CPU time (free/paid) | 10ms / 50ms |
| Reply References | 100 |
## Common Errors
| Error | Fix |
|-------|-----|
| "Address not verified" | Add in Email Routing dashboard |
| "Exceeded CPU time" | Use `ctx.waitUntil()` or upgrade |
| "Stream is locked" | Buffer `message.raw` first |
| Silent reply failure | Check DMARC records |

View File

@@ -0,0 +1,102 @@
# Email Workers Patterns
## Parse Email
```typescript
import PostalMime from 'postal-mime';
export default {
async email(message, env, ctx) {
const buffer = await new Response(message.raw).arrayBuffer();
const email = await PostalMime.parse(buffer);
console.log(email.from, email.subject, email.text, email.attachments.length);
await message.forward('inbox@example.com');
}
};
```
## Filtering
```typescript
// Allowlist from KV
const allowList = await env.ALLOWED_SENDERS.get('list', 'json') || [];
if (!allowList.includes(message.from)) {
message.setReject('Not allowed');
return;
}
// Size check (avoid parsing large emails)
if (message.rawSize > 5_000_000) {
await message.forward('inbox@example.com'); // Forward without parsing
return;
}
```
## Auto-Reply with Threading
```typescript
import { EmailMessage } from 'cloudflare:email';
import { createMimeMessage } from 'mimetext';
const msg = createMimeMessage();
msg.setSender({ addr: 'support@example.com' });
msg.setRecipient(message.from);
msg.setSubject(`Re: ${message.headers.get('Subject')}`);
msg.setHeader('In-Reply-To', message.headers.get('Message-ID') || '');
msg.addMessage({ contentType: 'text/plain', data: 'Thank you. We will respond.' });
await message.reply(new EmailMessage('support@example.com', message.from, msg.asRaw()));
```
## Rate-Limited Auto-Reply
```typescript
const rateKey = `rate:${message.from}`;
if (!await env.RATE_LIMIT.get(rateKey)) {
// Send reply...
ctx.waitUntil(env.RATE_LIMIT.put(rateKey, '1', { expirationTtl: 3600 }));
}
```
## Subject-Based Routing
```typescript
const subject = (message.headers.get('Subject') || '').toLowerCase();
if (subject.includes('billing')) await message.forward('billing@example.com');
else if (subject.includes('support')) await message.forward('support@example.com');
else await message.forward('general@example.com');
```
## Multi-Tenant Routing
```typescript
// support+tenant123@example.com → tenant123
const tenantId = message.to.split('@')[0].match(/\+(.+)$/)?.[1] || 'default';
const config = await env.TENANT_CONFIG.get(tenantId, 'json');
config?.forwardTo ? await message.forward(config.forwardTo) : message.setReject('Unknown');
```
## Archive & Extract Attachments
```typescript
// Archive to KV
ctx.waitUntil(env.ARCHIVE.put(`email:${Date.now()}`, JSON.stringify({
from: message.from, subject: email.subject
})));
// Attachments to R2
for (const att of email.attachments) {
ctx.waitUntil(env.R2.put(`${Date.now()}-${att.filename}`, att.content));
}
```
## Webhook Integration
```typescript
ctx.waitUntil(
fetch(env.WEBHOOK_URL, {
method: 'POST',
body: JSON.stringify({ from: message.from, subject: message.headers.get('Subject') })
}).catch(err => console.error(err))
);
```