---
title: "Building a Serverless Mail Service with Cloudflare Workers (Zero Dependencies!)"
date: "2026-06-22"
tags: ["cloudflare", "serverless", "email", "tcp", "sockets", "tutorial"]
description: "A step-by-step guide to writing a native, raw SMTP client in a Cloudflare Worker using cloudflare:sockets and a Google App Password."
author: "Varun"
thumbnail: "https://images.unsplash.com/photo-1555066931-4365d14bab8c?q=80&w=1200&auto=format&fit=crop"
---

# Building a Zero-Dependency Mail Service with Cloudflare Workers and Google App Passwords

Have you ever needed to send transactional emails, like contact form submissions, from a static website or a markdown blog? Usually, you'd reach for a third-party API like Resend, Sendgrid, or Mailgun, or rely on heavy Node.js packages like `nodemailer`. 

But what if you want to use your existing Google Workspace or Gmail account to send these emails *natively* from the edge? 

In this tutorial, we will build a **zero-dependency**, lightning-fast `mailservicing` API using **Cloudflare Workers** and the new `cloudflare:sockets` API. We will literally be speaking SMTP directly to Google's servers over raw TCP!

## Why Native Sockets?

Cloudflare Workers provide a fantastic edge-computing environment. Historically, sending emails via SMTP was tricky because Workers run in an Edge runtime, not a full Node.js environment, meaning packages like `nodemailer` often fail with connection timeouts.

Instead of fighting with polyfills or downloading hundreds of kilobytes of dependencies, we can write a tiny native SMTP client that uses Cloudflare's native `connect()` API!

## Prerequisites

Before we start, you'll need:
1. A [Cloudflare](https://dash.cloudflare.com/) account.
2. Node.js installed on your machine.
3. A Google/Gmail Account with 2-Step Verification enabled.

## Step 1: Generate a Google App Password

Since Google disables basic SMTP authentication using your main password for security reasons, you must generate an App Password:

1. Go to your Google Account management dashboard.
2. Navigate to **Security**.
3. Under "How you sign in to Google," make sure **2-Step Verification** is turned ON.
4. Click on **2-Step Verification**, scroll down to **App passwords**, and click it.
5. Create a new app password (name it something like `Cloudflare Worker`).
6. **Save this 16-character password.** You will not be able to see it again!

## Step 2: Initialize the Project

We'll start by creating a new directory and initializing our project. 

```bash
mkdir mailservicing
cd mailservicing
npm init -y
```

Now, let's install the necessary Typescript dependencies (notice we aren't installing any email packages!):

```bash
npm install -D typescript @cloudflare/workers-types wrangler
```

## Step 3: Configure Cloudflare Worker

Create a `wrangler.toml` file in the root of your project. 

```toml
name = "mailservicing"
main = "src/index.ts"
compatibility_date = "2025-08-20"

# [vars]
# GMAIL_USER = "your-email@gmail.com"
# GMAIL_APP_PASSWORD = "your-app-password"
```

## Step 4: Write the Custom SMTP Client

We will organize our code in a modular, production-grade structure inside the `src` folder.

### 1. The Types (`src/types.ts`)
Let's define our environment variables and request payload.

```typescript
export interface Env {
  GMAIL_USER: string;
  GMAIL_APP_PASSWORD: string;
}

export interface EmailRequest {
  to: string;
  subject: string;
  text?: string;
  html?: string;
}
```

### 2. The Raw Socket Client (`src/smtp-client.ts`)
This class handles reading from and writing to the raw TCP stream.

```typescript
export class SMTPClient {
  private buffer = '';
  private decoder = new TextDecoder();
  private reader: ReadableStreamDefaultReader;
  private writer: WritableStreamDefaultWriter;

  constructor(socket: any) {
    this.reader = socket.readable.getReader();
    this.writer = socket.writable.getWriter();
  }

  async readResponse(): Promise<string> {
    let response = '';
    while (true) {
      while (!this.buffer.includes('\\r\\n')) {
        const { value, done } = await this.reader.read();
        if (done && !value) {
          if (this.buffer.length > 0) break;
          throw new Error('Socket closed unexpectedly by server');
        }
        if (value) this.buffer += this.decoder.decode(value, { stream: true });
      }

      const newlineIndex = this.buffer.indexOf('\\r\\n');
      let line = '';
      if (newlineIndex !== -1) {
        line = this.buffer.substring(0, newlineIndex + 2);
        this.buffer = this.buffer.substring(newlineIndex + 2);
      } else {
        line = this.buffer;
        this.buffer = '';
      }
      
      response += line;

      // SMTP multi-line responses end with a space (e.g., "250 OK")
      if (/^\\d{3} /.test(line) || line === '') break;
    }
    return response;
  }

  async sendCommand(command: string, expectedCode: string): Promise<string> {
    const encoder = new TextEncoder();
    await this.writer.write(encoder.encode(command + '\\r\\n'));
    
    const response = await this.readResponse();
    if (!response.startsWith(expectedCode)) {
      throw new Error(`SMTP Error. Command: ${command.split(' ')[0]} | Expected ${expectedCode}, got: ${response}`);
    }
    return response;
  }

  upgrade(newSocket: any) {
    this.reader = newSocket.readable.getReader();
    this.writer = newSocket.writable.getWriter();
  }

  release() {
    this.writer.releaseLock();
    this.reader.releaseLock();
  }
}
```

### 3. The Mailer Service (`src/mailer.ts`)
Here is the magic. We use Cloudflare's `connect()` to establish a connection to `smtp.gmail.com` on port 587. We explicitly send the `STARTTLS` command and then instantly upgrade the socket using `socket.startTls()`!

```typescript
import { connect } from 'cloudflare:sockets';
import { SMTPClient } from './smtp-client';
import { Env, EmailRequest } from './types';

export async function sendEmail(env: Env, request: EmailRequest): Promise<void> {
  const { to, subject, text, html } = request;

  let socket = connect({ hostname: 'smtp.gmail.com', port: 587 }, { secureTransport: 'starttls', allowHalfOpen: false });
  const client = new SMTPClient(socket);

  try {
    await client.readResponse();

    await client.sendCommand('EHLO localhost', '250');
    await client.sendCommand('STARTTLS', '220');
    
    // CRITICAL: We must release the stream locks BEFORE calling startTls()
    client.release(); 
    socket = socket.startTls();
    client.upgrade(socket);

    await client.sendCommand('EHLO localhost', '250');

    const authStr = btoa(`\\x00${env.GMAIL_USER}\\x00${env.GMAIL_APP_PASSWORD}`);
    await client.sendCommand(`AUTH PLAIN ${authStr}`, '235');

    await client.sendCommand(`MAIL FROM:<${env.GMAIL_USER}>`, '250');
    await client.sendCommand(`RCPT TO:<${to}>`, '250');
    await client.sendCommand('DATA', '354');

    let message = `From: ${env.GMAIL_USER}\\r\\nTo: ${to}\\r\\nSubject: ${subject}\\r\\n`;
    if (html) {
      message += `Content-Type: text/html; charset="UTF-8"\\r\\n\\r\\n${html}\\r\\n.`;
    } else {
      message += `Content-Type: text/plain; charset="UTF-8"\\r\\n\\r\\n${text}\\r\\n.`;
    }
    await client.sendCommand(message, '250');
    await client.sendCommand('QUIT', '221');

  } finally {
    client.release();
    socket.close();
  }
}
```

### 4. The API Endpoint (`src/index.ts`)
Finally, our HTTP entry point.

```typescript
import { Env, EmailRequest } from './types';
import { sendEmail } from './mailer';

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const corsHeaders = {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    };

    if (request.method === 'OPTIONS') return new Response(null, { headers: corsHeaders });
    if (request.method !== 'POST') return new Response('Method Not Allowed', { status: 405, headers: corsHeaders });

    try {
      const body = await request.json() as EmailRequest;
      if (!body.to || !body.subject || (!body.text && !body.html)) {
        return new Response(JSON.stringify({ error: 'Missing required fields' }), { status: 400, headers: corsHeaders });
      }

      await sendEmail(env, body);

      return new Response(JSON.stringify({ success: true, message: 'Email sent successfully via custom SMTP client!' }), {
        status: 200,
        headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      });

    } catch (error: any) {
      console.error('SMTP Error:', error);
      return new Response(JSON.stringify({ error: 'Internal Server Error', details: error.message }), {
        status: 500,
        headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      });
    }
  },
};
```

## Step 5: Test Locally

Create an `.dev.vars` file in your project root to hold your local environment variables:
```env
GMAIL_USER="your-email@gmail.com"
GMAIL_APP_PASSWORD="your-app-password"
```

Start the local server:
```bash
npx wrangler dev
```

Test it from your terminal:
```bash
curl -X POST http://localhost:8787 \\
  -H "Content-Type: application/json" \\
  -d '{"to":"recipient@example.com", "subject":"Test from CF Worker", "text":"Hello from the edge!"}'
```

## Step 6: Deploy to Cloudflare!

First, securely add your secrets to your Cloudflare account:

```bash
npx wrangler secret put GMAIL_USER
npx wrangler secret put GMAIL_APP_PASSWORD
```

Finally, deploy the worker!

```bash
npx wrangler deploy
```

And that's it! You have built your very own native, zero-dependency SMTP client running on the edge!

## Source Code

You can find the complete source code and examples for this project on our GitHub repository:
[https://github.com/Arisecraft/worker-mailer](https://github.com/Arisecraft/worker-mailer)
