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,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)
|
||||
237
.agents/skills/cloudflare-deploy/references/email-workers/api.md
Normal file
237
.agents/skills/cloudflare-deploy/references/email-workers/api.md
Normal 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
|
||||
}
|
||||
};
|
||||
```
|
||||
@@ -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` |
|
||||
@@ -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 |
|
||||
@@ -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))
|
||||
);
|
||||
```
|
||||
Reference in New Issue
Block a user