Skip to content

connve/cloudflare-email-forwarder

Repository files navigation

Cloudflare Email Forwarder

License: MPL 2.0 Test Suite Security Audit Release

Cloudflare Workers email forwarder that parses incoming emails and sends them to a webhook endpoint.

Repository Structure

├── 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

Features

  • 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

Configuration

Environment Variables

Required. Set via wrangler secret put or Cloudflare Dashboard:

  • HTTP_WEBHOOK_URL - Webhook endpoint for email forwarding
  • HTTP_WEBHOOK_API_TOKEN - Bearer token for webhook authentication

Retry Queue (Optional)

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 * * * *"]

Domain Filtering (Optional)

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"

Email Output Format

{
  "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 Address Format

  • Email addresses are parsed into structured objects with name and email fields
  • If no display name is present, the name field will be an empty string
  • to, cc, and bcc are arrays to support multiple recipients
  • from is a single object (only one sender allowed)

Special Handling

  • Auto-forwarded emails: Extracts original sender from Return-Path header
  • BCC scenarios: Separates original recipient from BCC recipient
  • Headers: Converts hyphenated headers to snake_case
  • Internal filtering: Drops internal-to-internal emails when configured

Retry Mechanism

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.

Monitoring

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

Development

npm install
npx wrangler dev
npm test

Local environment variables can be set via .env file.

Contributing

Enable pre-commit hooks:

git config core.hooksPath .githooks

Hooks run tests, TypeScript checks, lint, and security audits before each commit.

Deployment

For manual deployment, see Cloudflare Workers documentation.

GitHub Actions Deployment

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 permissions
  • HTTP_WEBHOOK_API_TOKEN - Bearer token for webhook authentication

GitHub Variables:

  • CLOUDFLARE_ACCOUNT_ID - Your Cloudflare account ID
  • HTTP_WEBHOOK_URL - Webhook endpoint URL
  • KV_DOMAIN_FILTER_ID - Domain filter KV namespace ID
  • KV_RETRY_QUEUE_ID - Retry queue KV namespace ID

License

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/.

About

Ready to use example of cloudflare email worker

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published