Logo

dev-resources.site

for different kinds of informations.

How to Process Incoming Emails and Trigger Webhooks, In-App Actions, and More Using Cloudflare Email Workers and D1 Database

Published at
1/4/2025
Categories
laravel
cloudflare
workers
automation
Author
elvisans
Author
8 person written this
elvisans
open
How to Process Incoming Emails and Trigger Webhooks, In-App Actions, and More Using Cloudflare Email Workers and D1 Database

It's common to have shared mailboxes across teams in companies, eg. contact emails or support email.
Managing them solely from emails clients can be cumbersome. In this article, we’ll demonstrate how to turn received emails into in-app notifications or tickets using Cloudflare Email Workers and D1 database. We will then process the emails message into a Laravel Application.

Overview

Workflow

  1. Receive Emails: Emails sent to [email protected] are processed using Cloudflare Email Workers.
  2. Store Emails: The emails are stored in Cloudflare D1 to make sure we wont loose the email even if the later processes fail.
  3. Webhook Call to Laravel: Cloudflare sends the email data to Laravel through a webhook.

Why Use This Approach?

  • Centralized Management: Manage tickets and notifications within your app.
  • Cloudflare’s Power: Leverage Cloudflare’s global edge network and D1 database.
  • Scalability: Handle a high volume of emails efficiently.

Step 1: Set Up Cloudflare Email Worker

Cloudflare’s Email Workers allow you to intercept emails and process them programmatically. Once your domain is paired with your Cloudflare, and email routing successfully configured, you can decide what happen when an email is received on a given mail address

Example Worker Code that save a mail content into D1

export default {
  async email(message, env, ctx) {
    try {
      // Parse email details
      const subject = message.headers.get('subject') || 'No Subject';
      const from = message.from || 'Unknown Sender';
      const rawEmail = await new Response(message.raw).text();

      // Prepare SQL query for storing email
      const query = `
        INSERT INTO emails (sender, subject, body, receivedAt) 
        VALUES (?, ?, ?, ?);
      `;
      const values = [from, subject, rawEmail, new Date().toISOString()];

      // Execute the SQL query
      await env.D1_DATABASE.prepare(query).bind(...values).run();

      // Return success response
      return new Response('Email stored successfully!', { status: 200 });
    } catch (error) {
      // Notify webhook about the error
      const webhookURL = 'https://webhook.site/d1ff620a-xxxxxxxxxxxxxxxxxx';
      const errorDetails = {
        message: 'Error occurred while storing email',
        error: error.toString(),
        stack: error.stack || null,
        timestamp: new Date().toISOString(),
      };

      // Post the error details to the webhook
      await fetch(webhookURL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(errorDetails),
      });

      // Log the error for debugging
      console.error('Error storing email:', error);

      // Return error response
      return new Response('Failed to store email', { status: 500 });
    }
  },
};

Enter fullscreen mode Exit fullscreen mode

The above code is using email worker syntax and assume you have a D1 Database binding available under the D1_DATABASE env variable

Configure Email Routing

  • Set up an email address like [email protected] in Cloudflare’s Email Routing.
  • Route emails to your worker for processing. For more information, please refer to the documentation

Step 2: Extend the Worker

Add REST API endpoints to fetch emails and mark them as sent.

Updated Worker Code

  async fetch(request, env) {
    const url = new URL(request.url);

    // Fetch emails that are not marked as read
    if (url.pathname === '/emails' && request.method === 'GET') {
      try {
        const query = `SELECT * FROM emails WHERE readAt IS NULL ORDER BY id DESC;`;
        const emails = await env.D1_DATABASE.prepare(query).all();

        return new Response(
          JSON.stringify({
            status: 'success',
            data: emails.results || [],
          }),
          { headers: { 'Content-Type': 'application/json' }, status: 200 }
        );
      } catch (err) {
        console.error('Error retrieving emails:', err);
        return new Response(
          JSON.stringify({
            status: 'error',
            message: 'Error retrieving emails',
            error: err.message,
          }),
          { headers: { 'Content-Type': 'application/json' }, status: 500 }
        );
      }
    }

    // Mark email as read
    if (url.pathname.startsWith('/emails/') && request.method === 'PATCH') {
      const id = url.pathname.split('/').pop();

      if (!id || isNaN(id)) {
        return new Response(
          JSON.stringify({
            status: 'error',
            message: 'Invalid email ID provided',
          }),
          { headers: { 'Content-Type': 'application/json' }, status: 400 }
        );
      }

      try {
        const query = `UPDATE emails SET readAt = ? WHERE id = ?;`;
        const result = await env.D1_DATABASE.prepare(query).bind(new Date().toISOString(), id).run();

        if (result.changes === 0) {
          return new Response(
            JSON.stringify({
              status: 'error',
              message: `Email with ID ${id} not found or already marked as read.`,
            }),
            { headers: { 'Content-Type': 'application/json' }, status: 404 }
          );
        }

        return new Response(
          JSON.stringify({
            status: 'success',
            message: `Email with ID ${id} marked as read.`,
          }),
          { headers: { 'Content-Type': 'application/json' }, status: 200 }
        );
      } catch (err) {
        console.error('Error updating email status:', err);
        return new Response(
          JSON.stringify({
            status: 'error',
            message: 'Error updating email status',
            error: err.message,
          }),
          { headers: { 'Content-Type': 'application/json' }, status: 500 }
        );
      }
    }

    // Default 404 response
    return new Response(
      JSON.stringify({
        status: 'error',
        message: 'Endpoint not found',
      }),
      { headers: { 'Content-Type': 'application/json' }, status: 404 }
    );
  }
Enter fullscreen mode Exit fullscreen mode

We have added the fetch handler that help us create HTTP endpoints from where we can fetch emails. you can learn more about the fetch handler here


Step 3: Integrate into a Laravel Project

I Assume you have a working Laravel application, and if not or you are new to laravel please see bootcamp.laravel.com.

We will need a model to store the processed emails and a webhook endpoint to receive the payload from the Cloudflare worker anytime a new mail is received.

In your project, create a model, the migration file and the controller by running the following command php artisan make:model SupportEmailNotification -mc, then :

  • In the migration file :
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('support_email_notifications', function (Blueprint $table) {
            $table->id();
            $table->string('sender');
            $table->string('subject');
            $table->longText('html_body');
            $table->longText('text_body');
            $table->boolean('read')->default(false);
            $table->timestamps();
            $table->string('cloudflare_id')->unique();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('support_email_notifications');
    }
};
Enter fullscreen mode Exit fullscreen mode
  • In the controller file :
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\SupportEmailNotification;
use Exception;
use PhpMimeMailParser\Parser;
use Illuminate\Support\Facades\Http;


class SupportEmailNotificationController extends Controller
{
    public function store(Request $request)
    {
        $validated = $request->validate(
            [
            'cloudflare_id' => 'required|string|unique:support_email_notifications,cloudflare_id',
            'sender' => 'required|string|max:255',
            'subject' => 'required|string|max:255',
            'raw_body' => 'required|string',
            ]
        );

        try{

            $parser = new Parser();
            $parser->setText($validated['raw_body']);

            // Extract email details
            $htmlBody = $parser->getMessageBody('html') ?? '';
            $textBody = $parser->getMessageBody('text') ?? '';

            // Save the email details to the database
            $emailNotification = new SupportEmailNotification();
            $emailNotification->sender = $validated['sender'];
            $emailNotification->subject = $validated['subject'];
            $emailNotification->html_body = $htmlBody;
            $emailNotification->text_body = $textBody;
            $emailNotification->read = false;
            $emailNotification->cloudflare_id = $validated['cloudflare_id'];
            $emailNotification->save();

            // Call Cloudflare read route to confirm reception
            $cloudflareId = $request->input('cloudflare_id');
            if ($cloudflareId) {
                $this->markAsReadInCloudflare($cloudflareId);
            }

            return response()->json(
                [
                'status' => 'success',
                ], 201
            );
        } catch (\Exception $e) {
            // Handle errors and return a 500 response
            return response()->json(['error' => $e->getMessage()], 500);
        }
    }

    private function markAsReadInCloudflare($cloudflareId)
    {
        //$url = "https://your-cloudflare-worker-domain/emails/{$cloudflareId}";
        $response = Http::patch($url);
        if (!$response->successful()) {
            throw new \Exception("Failed to mark email as read in Cloudflare. ID: $cloudflareId");
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

You must install the PHP-Mime-Mail-Parser that help us process the raw email content

  • In your model file, add the $fillable property
    protected $fillable = [
        'sender',
        'subject',
        'html_body',
        'text_body',
        'read',
        'cloudflare_id',
        'read_at',
    ];
Enter fullscreen mode Exit fullscreen mode
  • In your routes/api.php add the webhook route
//existing code
Route::post('/webhook/notifications', [App\Http\Controllers\SupportEmailNotificationController::class, 'store']);
Enter fullscreen mode Exit fullscreen mode

Finally run the migration : php artisan migrate.
The very last step is to update our cloudflare worker so that it will send a webhook call to our application whenever a new email is received.

export default {
  async email(message, env, ctx) {
    const webhookURL = 'https://domain.com/api/webhook/notifications';
    const errorWebHookURL = 'https://webhook.site/xxxxx-ab25-4f8b-8957-8d937d0d49d6';
    try {
      // Parse email details
      const subject = message.headers.get('subject') || 'No Subject';
      const from = message.from || 'Unknown Sender';
      const rawEmail = await new Response(message.raw).text();

      // Prepare SQL query for storing email
      const query = `
        INSERT INTO emails (sender, subject, body, receivedAt) 
        VALUES (?, ?, ?, ?);
      `;
      const values = [from, subject, rawEmail, new Date().toISOString()];

      // Execute the SQL query
      const result = await env.D1_DATABASE.prepare(query).bind(...values).run();

      // Notify webhook of the successfully stored email
      const webhookPayload = {
        cloudflare_id: result.meta.last_row_id.toString(),
        sender: from,
        subject: subject,
        raw_body: rawEmail,
        received_at: new Date().toISOString(),
      };

      await fetch(webhookURL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(webhookPayload),
      });

      // Return success response
      return new Response(
        JSON.stringify({ status: 'success', message: 'Email stored successfully!' }),
        { headers: { 'Content-Type': 'application/json' }, status: 200 }
      );
    } catch (error) {
      // Notify webhook about the error
      const errorDetails = {
        message: 'Error occurred while storing email',
        error: error.toString(),
        stack: error.stack || null,
        timestamp: new Date().toISOString(),
      };

      await fetch(errorWebHookURL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(errorDetails),
      });

      // Log the error for debugging
      console.error('Error storing email:', error);

      // Return error response
      return new Response(
        JSON.stringify({ status: 'error', message: 'Failed to store email', error: error.message }),
        { headers: { 'Content-Type': 'application/json' }, status: 500 }
      );
    }
  },

  async fetch(request, env) {
    const url = new URL(request.url);

    // Fetch emails that are not marked as read
    if (url.pathname === '/emails' && request.method === 'GET') {
      try {
        const query = `SELECT * FROM emails WHERE readAt IS NULL ORDER BY id DESC;`;
        const emails = await env.D1_DATABASE.prepare(query).all();

        return new Response(
          JSON.stringify({ status: 'success', data: emails.results || [] }),
          { headers: { 'Content-Type': 'application/json' }, status: 200 }
        );
      } catch (err) {
        console.error('Error retrieving emails:', err);
        return new Response(
          JSON.stringify({ status: 'error', message: 'Error retrieving emails', error: err.message }),
          { headers: { 'Content-Type': 'application/json' }, status: 500 }
        );
      }
    }

    // Mark email as read
    if (url.pathname.startsWith('/emails/') && request.method === 'PATCH') {
      const id = url.pathname.split('/').pop();

      if (!id || isNaN(id)) {
        return new Response(
          JSON.stringify({ status: 'error', message: 'Invalid email ID provided' }),
          { headers: { 'Content-Type': 'application/json' }, status: 400 }
        );
      }

      try {
        const query = `UPDATE emails SET readAt = ? WHERE id = ?;`;
        const result = await env.D1_DATABASE.prepare(query).bind(new Date().toISOString(), id).run();

        if (result.changes === 0) {
          return new Response(
            JSON.stringify({ status: 'error', message: `Email with ID ${id} not found or already marked as read.` }),
            { headers: { 'Content-Type': 'application/json' }, status: 404 }
          );
        }

        return new Response(
          JSON.stringify({ status: 'success', message: `Email with ID ${id} marked as read.` }),
          { headers: { 'Content-Type': 'application/json' }, status: 200 }
        );
      } catch (err) {
        console.error('Error updating email status:', err);
        return new Response(
          JSON.stringify({ status: 'error', message: 'Error updating email status', error: err.message }),
          { headers: { 'Content-Type': 'application/json' }, status: 500 }
        );
      }
    }

    // Default 404 response
    return new Response(
      JSON.stringify({ status: 'error', message: 'Endpoint not found' }),
      { headers: { 'Content-Type': 'application/json' }, status: 404 }
    );
  },
};

Enter fullscreen mode Exit fullscreen mode

You may have noticed the error webhook is from https://webhook.site which is a pretty cool free tool that helps you test webhook quickly. To call your local laravel instance from the cloudflare worker, you may need to use tunneling software like Cloudflare Tunnel


With Cloudflare email workers we can create a powerful, flexible system to manage emails in various ways.

Some practical examples include:

  • Turn Emails into Support Tickets: Automatically convert incoming customer emails into tickets, parsed and stored in the D1 database, ready for team follow-up.
  • Automate Reminders: Trigger automated task or deadline reminders based on email content.
  • In-App Notifications: Use email content to send real-time notifications to users within your app (e.g., account updates, special offers).

Going further we may study how to implement machine learning algorithms to categorize emails more accurately or trigger custom actions based on the email content and predefined actions.

cloudflare Article's
30 articles in total
Favicon
Building a JAMStack App with Eleventy.js, CloudFlare Workers and AthenaHealth APIs - Part 2
Favicon
Building a JAMStack App with Eleventy.js, CloudFlare Workers and AthenaHealth APIs - Part 1
Favicon
How I Set Up My Custom Domain and Email for Substack
Favicon
Using Cloudflare SSL with Elastic Beanstalk instances
Favicon
Traefik using owned SSL certificate
Favicon
Traefik Cloudflare DNS Challenge
Favicon
Use Cloudflare Snippets to set up a Docker Registry Mirror
Favicon
Cloudflare PyPI Mirror
Favicon
Secure Self-Hosting with Cloudflare Tunnels and Docker: Zero Trust Security
Favicon
How to Process Incoming Emails and Trigger Webhooks, In-App Actions, and More Using Cloudflare Email Workers and D1 Database
Favicon
12 things I learned about hosting serverless sites on Cloudflare
Favicon
Dynamic DNS sync with Cloudflare
Favicon
Solusi Comment Reply WordPress Error Karena Rocket Loader Cloudflare
Favicon
Implementing Gmail Sending with Cloudflare Workers - Setup Guide
Favicon
Next.js Optimization for Dynamic Apps: Vercel Edge vs. Traditional SSR
Favicon
Building Vhisper: Voice Notes App with AI Transcription and Post-Processing
Favicon
Host Responded to 4 TCP SYN Probes on Port 24567 from Source Port 53(PCI DSS Cloudflare Resolved)
Favicon
Fighting with Redirects: A Journey of Astro Site Migration
Favicon
Using PostHog in Remix Loaders and Actions on Cloudflare Pages
Favicon
Implementing Gmail API with Cloudflare Workers - Part 3: Implementation
Favicon
Cloudflare Zaraz VS WP Complianz
Favicon
Implementing Gmail Sending with Cloudflare Workers - Development Guide
Favicon
Implementing Cloudflare Workflows
Favicon
Building Honcanator: The AI Goose Generator
Favicon
Retrieval Augmented Geese - Semantic Search with the HONC Stack
Favicon
Simplify serverless scaling and data management with Fauna and Cloudflare Workers
Favicon
[Cloudflare] Redirect To Another Domain
Favicon
Step-by-Step Guide to Hosting Your Website on a VPS Using Caddy Server and Cloudflare
Favicon
Building an AI Cron Builder with Cloudflare Pages and Next.js
Favicon
Cloudflare + Remix + PostgreSQL with Prisma Accelerate's Self Hosting

Featured ones: