Skip to content

Email Routing with Workers -- Custom Processing

DodaTech 7 min read

In this tutorial, you'll learn about Email Routing with Workers. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Cloudflare Email Routing with Workers enables Serverless email processing -- you can receive emails at any custom domain, Process them with a Worker, and forward, store, or reject messages based on custom logic without running an email server.

Why Email Routing with Workers Matters

Managing email infrastructure is notoriously complex. You need MX records, SMTP servers, spam filtering, virus scanning, and storage -- all while handling variable volumes of incoming mail. Cloudflare Email Routing replaces this with a simple model: emails sent to your domain are caught by Cloudflare's email infrastructure and delivered to a Worker function you write. The Worker receives the email as a structured object with headers, sender, recipients, and body. You can inspect the content, run spam checks, extract attachments, store messages in R2 or D1, forward to external addresses, or trigger other business logic. Unlike traditional email processing which requires always-on servers, Workers run only when an email arrives. Combined with Cloudflare's security products, email processing happens entirely within Cloudflare's edge network. This feature is distinct from REST APIs -- it handles the SMTP-to-HTTP translation so your Worker never deals with raw SMTP protocols.

Real-world use: A support team receives customer emails at support@example.com. Each email is processed by a Worker that scans for keywords, routes urgent messages to a Slack Webhook, archives the original to R2, and stores metadata in D1 for reporting -- all without any email server software.

Email Routing Architecture

flowchart TD
    S[Sender] --> MTA[MTA over SMTP]
    MTA --> CF[Cloudflare Email Routing]
    CF --> W[Email Worker]
    W --> R2[Archive to R2]
    W --> D1[Metadata to D1]
    W --> FWD[Forward to External]
    W --> SL[Slack/Webhook]
    W --> AUTO[Auto-Reply]

    subgraph Worker_Processing
        H[Headers] --> P[Parse]
        P --> FIL[Filter Rules]
        FIL --> ACT[Action]
    end

    style CF fill:#f90,color:#fff
    style W fill:#3498db,color:#fff
    style R2 fill:#2ecc71,color:#fff
    style D1 fill:#2ecc71,color:#fff

Cloudflare intercepts inbound emails at the MX level, converts them to HTTP requests, and delivers them to your Worker. The Worker receives an EmailMessage object and calls methods to forward, reject, or Process the message.

Setting Up Email Routing

# Step 1: Configure MX records in Cloudflare DNS
# Priority 10: mx.cloudflare.com

# Step 2: Enable Email Routing in the Cloudflare Dashboard
# Email -> Email Routing -> Get Started

# Step 3: Create a catch-all or route-specific email address
# Route: support@example.com -> Worker: email-processor

# Step 4: Write the worker and deploy
npx wrangler deploy src/email-worker.js
// wrangler.toml
name = "email-processor"
main = "src/email-worker.js"

[env.production]
# No special bindings required for basic Email Routing
# Environment variables for forwarding rules:
vars = { FORWARD_TO = "team@company.com", ARCHIVE_BUCKET = "email-archive" }

Email Routing requires MX record changes which can take 30-60 minutes to propagate. Once configured, you define routes in the Cloudflare Dashboard or API that map email addresses to Workers.

Basic Email Forwarding with Filtering

export default {
  async email(message, env, ctx) {
    const from = message.from;
    const to = message.to;
    const subject = message.headers.get('subject') || 'No Subject';

    // Filter spam senders
    const blockedDomains = ['spam.com', 'mailer.example.com'];
    const senderDomain = from.split('@')[1];
    if (blockedDomains.includes(senderDomain)) {
      console.log(`Blocked email from ${from}`);
      // Reject the email -- it will not be delivered
      return;
    }

    // Auto-reply to specific senders
    if (subject.includes('Unsubscribe')) {
      await message.reply('You have been unsubscribed successfully.');
      console.log(`Sent unsubscribe confirmation to ${from}`);
      return;
    }

    // Forward to the team inbox
    await message.forward(env.FORWARD_TO);
    console.log(`Forwarded email from ${from} about "${subject}" to ${env.FORWARD_TO}`);
  }
};

// Expected behavior:
// Email from spam.com -> Blocked, rejected, not forwarded
// Email with "Unsubscribe" subject -> Auto-reply sent, not forwarded
// Normal email from legitimate sender -> Forwarded to team@company.com

The email handler is the entry point for Email Routing Workers. It receives an EmailMessage object with from, to, headers, and raw properties. The message.forward() method sends the email to another address. The message.reply() method sends an automatic reply.

Processing Attachments and Archiving

export default {
  async email(message, env, ctx) {
    const emailId = crypto.randomUUID();
    const timestamp = new Date().toISOString();

    // Store the raw email in R2 for archival
    const rawEmail = await new Response(message.raw).arrayBuffer();
    await env.ARCHIVE.put(
      `emails/${timestamp}-${emailId}.eml`,
      rawEmail,
      { httpMetadata: { contentType: 'message/rfc822' } }
    );

    // Store metadata in D1 for searchability
    await env.DB.prepare(`
      INSERT INTO email_log (id, from_addr, to_addr, subject, received_at, has_attachments)
      VALUES (?, ?, ?, ?, ?, ?)
    `).bind(
      emailId,
      message.from,
      message.to,
      message.headers.get('subject') || '',
      timestamp,
      message.attachments.length > 0 ? 1 : 0
    ).run();

    // Forward the original email
    await message.forward(env.FORWARD_TO);

    console.log(`Archived email ${emailId} from ${message.from}`);
  }
};

// Expected behavior:
// 1. Email arrives for support@example.com
// 2. Raw .eml file saved to R2 at emails/2026-06-23T10:30:00Z-uuid.eml
// 3. Metadata row inserted into D1 email_log table
// 4. Original email forwarded to the team inbox
// 5. All processing completes within a few hundred milliseconds

This pattern archives every email for Compliance and enables full-text search of email metadata via D1. The raw .eml file in R2 can be served to authorized users for review. Attachments are accessible through the message.attachments array.

Multi-Route Email Processing

const routes = [
  {
    pattern: /^support@/,
    handler: async (message, env) => {
      await message.forward('support-team"@company".com');
      await env.DB.prepare('INSERT INTO ticket_queue (from_addr, subject) VALUES (?, ?)')
        .bind(message.from, message.headers.get('subject'))
        .run();
    }
  },
  {
    pattern: /^billing@/,
    handler: async (message, env) => {
      const body = await new Response(message.raw).text();
      if (body.includes('REFUND')) {
        await message.forward('billing-manager"@company".com');
      } else {
        await message.forward('billing-team"@company".com');
      }
    }
  },
  {
    pattern: /^noreply@/,
    handler: async (message) => {
      // Silently discard
      console.log(`Discarded noreply email from ${message.from}`);
    }
  }
];

export default {
  async email(message, env, ctx) {
    for (const route of routes) {
      if (route.pattern.test(message.to)) {
        await route.handler(message, env);
        return;
      }
    }
    // Default: forward to general inbox
    await message.forward('general@company.com');
  }
};

// Expected behavior:
// support@example.com -> Forwarded to support-team, logged to D1
// billing@example.com with REFUND -> Forwarded to billing-manager
// billing@example.com without REFUND -> Forwarded to billing-team
// noreply@example.com -> Silently discarded
// unknown@example.com -> Forwarded to general@company.com

By routing emails based on the recipient address, a single Worker can handle multiple email roles. Each route pattern matches against the message.to value and applies custom logic.

Common Errors

Error Cause Fix
Email Routing not enabled for domain MX records not configured or routing not active Verify MX records point to mx.<a href="/web-servers-hosting/cloudflare/">Cloudflare</a>.com and Email Routing is toggled on in the dashboard
Worker handler not found Worker does not export an email function Export a named email function from your Worker module (not fetch)
Message too large Email exceeds 10MB size limit Cloudflare Email Routing supports up to 10MB messages; reject oversized messages at the MX level
Failed to forward message Destination mail server rejects the email Verify the forwarding address exists and is not blocking Cloudflare's sending IPs
Attachment processing failed Attachment exceeds per-file limits Check size limits; Process large attachments asynchronously and store in R2

Practice Questions

  1. What function must a Worker export to handle incoming emails?
  2. How do you archive a raw email to R2 for Compliance purposes?
  3. How can you route emails differently based on the recipient address?

FAQ

Can I send emails from Workers using Email Routing?

Email Routing handles inbound email only. For outbound email from Workers, use a transactional email service like SendGrid, Mailgun, or AWS SES, and call their APIs from your Worker via fetch(). Cloudflare does not provide SMTP relay for outgoing mail.

Does Email Routing preserve the original sender when forwarding?

When you forward an email with message.forward(), Cloudflare preserves the original sender in the headers but the envelope sender may change. Use the dkim-signature to verify authenticity. For true sender preservation, consider embedding the original sender in the forwarded message body.

What happens if my Worker throws an error during email processing?

If the Worker throws an error before calling forward() or reply(), the email is not delivered. Cloudflare logs the error and you can retrieve it via wrangler tail. Implement try-catch blocks and dead-letter logic to handle failures gracefully.

Summary

Cloudflare Email Routing with Workers lets you receive, process, and forward emails using Serverless functions. The Worker receives an EmailMessage with headers, body, and attachments. Use message.forward() to relay emails, message.reply() to auto-respond, or Process and archive to R2 and D1. Multi-route pattern matching enables sophisticated email workflows from a single Worker. Email Routing replaces traditional mail server infrastructure with edge functions. DodaTech uses Email Routing Workers to Process customer support tickets and archive all correspondence for Compliance.

This guide is brought to you by the developers of Cloudflare, Docker containers, and Durga Antivirus Pro at DodaTech.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro