Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Cloudflare platform skill covering Workers, D1, R2, KV, AI, Durable Objects, and security.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/email-workers/gotchas.md
1# Email Workers Gotchas23## Critical Issues45### ReadableStream Single-Use67```typescript8// ❌ WRONG: Stream consumed twice9const email = await PostalMime.parse(await new Response(message.raw).arrayBuffer());10const rawText = await new Response(message.raw).text(); // EMPTY!1112// ✅ CORRECT: Buffer first13const buffer = await new Response(message.raw).arrayBuffer();14const email = await PostalMime.parse(buffer);15const rawText = new TextDecoder().decode(buffer);16```1718### ctx.waitUntil() Errors Silent1920```typescript21// ❌ Errors dropped silently22ctx.waitUntil(fetch(webhookUrl, { method: 'POST', body: data }));2324// ✅ Catch and log25ctx.waitUntil(26fetch(webhookUrl, { method: 'POST', body: data })27.catch(err => env.ERROR_LOG.put(`error:${Date.now()}`, err.message))28);29```3031## Security3233### Envelope vs Header From (Spoofing)3435```typescript36const envelopeFrom = message.from; // SMTP MAIL FROM (trusted)37const headerFrom = (await PostalMime.parse(buffer)).from?.address; // (untrusted)38// Use envelope for security decisions39```4041### Input Validation4243```typescript44if (message.rawSize > 5_000_000) { message.setReject('Too large'); return; }45if ((message.headers.get('Subject') || '').length > 1000) {46message.setReject('Invalid subject'); return;47}48```4950### DMARC for Replies5152Replies fail silently without DMARC. Verify: `dig TXT _dmarc.example.com`5354## Parsing5556### Address Parsing5758```typescript59const email = await PostalMime.parse(buffer);60const fromAddress = email.from?.address || 'unknown';61const toAddresses = Array.isArray(email.to) ? email.to.map(t => t.address) : [email.to?.address];62```6364### Character Encoding6566Let postal-mime handle decoding - `email.subject`, `email.text`, `email.html` are UTF-8.6768## API Behavior6970### setReject() vs throw7172```typescript73// setReject() for SMTP rejection74if (blockList.includes(message.from)) { message.setReject('Blocked'); return; }7576// throw for worker errors77if (!env.KV) throw new Error('KV not configured');78```7980### forward() Only X-* Headers8182```typescript83headers.set('X-Processed-By', 'worker'); // ✅ Works84headers.set('Subject', 'Modified'); // ❌ Dropped85```8687### Reply Requires Verified Domain8889```typescript90// Use same domain as receiving address91const receivingDomain = message.to.split('@')[1];92await message.reply(new EmailMessage(`noreply@${receivingDomain}`, message.from, rawMime));93```9495## Performance9697### CPU Limit9899```typescript100// Skip parsing large emails101if (message.rawSize > 5_000_000) {102await message.forward('[email protected]');103return;104}105```106107Monitor: `npx wrangler tail`108109## Limits110111| Limit | Value |112|-------|-------|113| Max message size | 25 MiB |114| Max rules/zone | 200 |115| CPU time (free/paid) | 10ms / 30s default, 5min max |116| Reply References | 100 |117118## Common Errors119120| Error | Fix |121|-------|-----|122| "Address not verified" | Add in Email Routing dashboard |123| "Exceeded CPU time" | Use `ctx.waitUntil()` or upgrade |124| "Stream is locked" | Buffer `message.raw` first |125| Silent reply failure | Check DMARC records |126