# 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 |