Cloudflare Workers email forwarder that parses incoming emails and sends them to a webhook endpoint.
├── src/
│ ├── index.ts # Main email worker + scheduled retry processor
│ ├── email-message.ts # Email parsing, types & utilities
│ ├── email-message.test.ts # Email parsing tests
│ ├── retry.ts # Retry logic with exponential backoff
│ └── retry.test.ts # Retry mechanism tests
├── package.json
├── package-lock.json
├── tsconfig.json # TypeScript configuration
├── vitest.config.ts # Test configuration
├── worker-configuration.d.ts # Worker types
├── wrangler.toml .example # Wrangler configuration example
├── .gitignore
└── README.md
- Parses multipart email content (text, HTML, headers)
- Forwards to webhook with Bearer token authentication
- Optional automatic retry with exponential backoff for failed requests
- Optional domain filtering (block spam domains, prevent internal email loops)
- Auto-forwarded email detection with original sender extraction
- Handles BCC, CC, and complex email routing
Required. Set via wrangler secret put or Cloudflare Dashboard:
HTTP_WEBHOOK_URL- Webhook endpoint for email forwardingHTTP_WEBHOOK_API_TOKEN- Bearer token for webhook authentication
Without retry queue, failed webhook requests are logged but not retried. Recommended for production.
To enable automatic retry with exponential backoff:
wrangler kv:namespace create "RETRY_QUEUE"Add to wrangler.toml:
[[kv_namespaces]]
binding = "RETRY_QUEUE"
id = "your-namespace-id"
[triggers]
# Configure based on your KV read limits:
# Every minute (60 KV reads/hour): crons = ["* * * * *"]
# Every 10 minutes (6 KV reads/hour): crons = ["*/10 * * * *"]
# Every hour (1 KV read/hour): crons = ["0 * * * *"]
crons = ["*/10 * * * *"]Block spam domains or prevent internal email loops.
wrangler kv:namespace create "DOMAIN_FILTER"Add to wrangler.toml:
[[kv_namespaces]]
binding = "DOMAIN_FILTER"
id = "your-namespace-id"Configure rules:
# Block spam domains
wrangler kv:key put --binding=DOMAIN_FILTER "blocked:spam.com" "true"
# Internal domains (both from/to = dropped to prevent loops)
wrangler kv:key put --binding=DOMAIN_FILTER "internal:yourcompany.com" "true"{
"subject": "Email Subject",
"from": {
"name": "Sender Name",
"email": "sender@example.com"
},
"to": [
{
"name": "Recipient Name",
"email": "recipient@example.com"
}
],
"cc": [
{
"name": "CC Name",
"email": "cc@example.com"
}
],
"bcc": [
{
"name": "BCC Name",
"email": "bcc@example.com"
}
],
"date": "Mon, 1 Jan 2024 12:00:00 +0000",
"message_id": "<message-id>",
"headers": {
"content_type": "multipart/alternative",
"x_custom_header": "value"
},
"body": {
"text": "Plain text version",
"html": "<html>HTML version</html>"
},
"raw_content": "Complete raw email content..."
}- Email addresses are parsed into structured objects with
nameandemailfields - If no display name is present, the
namefield will be an empty string to,cc, andbccare arrays to support multiple recipientsfromis a single object (only one sender allowed)
- Auto-forwarded emails: Extracts original sender from
Return-Pathheader - BCC scenarios: Separates original recipient from BCC recipient
- Headers: Converts hyphenated headers to snake_case
- Internal filtering: Drops internal-to-internal emails when configured
When webhook delivery fails, the email is saved to RETRY_QUEUE and retried with exponential backoff:
Backoff delays: 1m → 2m → 4m → 8m → 16m → 32m → 1h → 2h → 4h → 8h
These are minimum delays - actual retry happens at the next cron run. Configure cron interval based on your KV limits.
After 10 attempts (~15 hours), requests move to dead letter queue (failed:* prefix).
Retries use current environment variables, so credential rotation applies automatically.
# List pending retries
wrangler kv:key list --binding RETRY_QUEUE --prefix "retry:"
# List failed requests (dead letter queue)
wrangler kv:key list --binding RETRY_QUEUE --prefix "failed:"
# View specific retry
wrangler kv:key get --binding RETRY_QUEUE "retry:{timestamp}:{id}"
# Watch logs
wrangler tailnpm install
npx wrangler dev
npm testLocal environment variables can be set via .env file.
Enable pre-commit hooks:
git config core.hooksPath .githooksHooks run tests, TypeScript checks, lint, and security audits before each commit.
For manual deployment, see Cloudflare Workers documentation.
Example workflow for automated deployment:
name: Deploy Email Forwarder
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: |
cd email-forwarder
npm ci
- name: Create wrangler.toml
run: |
cat > email-forwarder/wrangler.toml <<EOF
name = "email-forwarder"
main = "src/index.ts"
compatibility_date = "2024-09-18"
workers_dev = false
[[kv_namespaces]]
binding = "DOMAIN_FILTER"
id = "${{ vars.KV_DOMAIN_FILTER_ID }}"
[[kv_namespaces]]
binding = "RETRY_QUEUE"
id = "${{ vars.KV_RETRY_QUEUE_ID }}"
[triggers]
crons = ["* * * * *"]
[observability]
enabled = true
head_sampling_rate = 1
EOF
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: email-forwarder
secrets: |
HTTP_WEBHOOK_API_TOKEN
vars: |
HTTP_WEBHOOK_URL
env:
HTTP_WEBHOOK_API_TOKEN: ${{ secrets.HTTP_WEBHOOK_API_TOKEN }}
HTTP_WEBHOOK_URL: ${{ vars.HTTP_WEBHOOK_URL }}GitHub Secrets:
CLOUDFLARE_API_TOKEN- Cloudflare API token with Workers permissionsHTTP_WEBHOOK_API_TOKEN- Bearer token for webhook authentication
GitHub Variables:
CLOUDFLARE_ACCOUNT_ID- Your Cloudflare account IDHTTP_WEBHOOK_URL- Webhook endpoint URLKV_DOMAIN_FILTER_ID- Domain filter KV namespace IDKV_RETRY_QUEUE_ID- Retry queue KV namespace ID
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.