mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 18:11:27 -07:00
update skills
This commit is contained in:
@@ -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)
|
||||
195
.agents/skills/cloudflare-deploy/references/email-routing/api.md
Normal file
195
.agents/skills/cloudflare-deploy/references/email-routing/api.md
Normal 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).
|
||||
@@ -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 }}
|
||||
```
|
||||
@@ -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
|
||||
@@ -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 |
|
||||
Reference in New Issue
Block a user