↓ Skip to main content
  1. Posts/

Self-Host a Free Email Service on Cloudflare: cloud-mail Deployment Guide

·516 words·3 mins· loading · loading · ·
HJP
Author
HJP
Always learning, always growing.
Table of Contents

This is an English summary of the Chinese article. All keys, domains, and IDs below are placeholders <...> — replace them with your own.

What problem this solves
#

Own a domain but no email? Third-party mail hosts cost money or cap accounts. This guide shows how to run a full-featured email service on Cloudflare’s free tier using the open-source maillab/cloud-mail project — no VPS required. Result: receive mail at any xxx@your-domain, send from a web client, store attachments in R2, and share via self-service registration.

Architecture
#

Email Routing (*@your-domain → Worker)
        ↓
cloud-mail Worker (Hono)
  ├─ D1        → accounts / mails / settings
  ├─ KV        → sessions / cache
  ├─ R2        → attachments (custom-domain direct link)
  ├─ send_email→ CF Email Sending (sending channel 1)
  ├─ AI        → CAPTCHA recognition (optional)
  └─ Vue3 frontend → web mail client (built & uploaded with Worker)

Cloudflare resources needed
#

ResourceRole
WorkerRuns cloud-mail (handlers: fetch / email / scheduled)
D1Accounts, mails, system settings
KVSessions, cache
R2Attachments (r2.<your-domain> custom domain)
Email RoutingReceiving: *@<your-domain> → Worker
send_emailSending via CF Email Service (Beta)

Steps
#

  1. Clone & configure — git clone cloud-mail; edit mail-worker/wrangler.toml (D1/KV/R2/send_email bindings + [vars]); npx wrangler deploy --config wrangler.toml. The Vue3 frontend in mail-vue/ builds automatically on deploy.
  2. Init database & admin — visit https://mail.<your-domain>/api/init/<jwt_secret>, log in as admin@<your-domain>, confirm receive/send/register toggles.
  3. Receiving (Email Routing) — CF’s catch-all (type=all) does not support worker actions, so create a literal rule *@<your-domain> → worker. MX/SPF/DKIM are auto-created. Verify by emailing any xxx@<your-domain> and viewing it in the web client.
  4. Sending (dual channel) — CF Email Sending via [[send_email]] binding, with Resend fallback: if CF rejects (e.g. unverified recipient E_RECIPIENT_NOT_ALLOWED), auto-fallback to Resend using a per-domain token. Free quota: 3000 emails/mo, 100/day. Check the resendEmailId field to see which channel delivered (CF: <xxx@domain>; Resend: UUID).
  5. Attachments in R2 — set storageType=R2 and bind r2.<your-domain> custom domain. ⚠️ Upstream bug: the /attachments/ route always reads KV; patch it to dispatch by storageType (R2 reads from env.r2, KV keeps the old path) — see PR #325.
  6. Registration — open registration (register=0), disable CAPTCHA/code for closed groups; enable them if exposed publicly.

Known pitfalls
#

  1. orm_log must not be the string "true"/"false" — drizzle crashes with this.logger.logQuery is not a function. Pass empty string or bool-check.
  2. Frontend sends JWT without a Bearer prefix when calling APIs.
  3. R2 requires manual one-click activation in the CF dashboard on first use.
  4. Email Routing wildcard: catch-all can’t target a worker — use a literal *@domain rule; the worker action’s value is the script name.
  5. Mail-list APIs crash on missing accountId (Cannot read properties of undefined) — guard with allReceive=0 default, ?? 0 on empty results, and Number conversion in latest.

Summary
#

Cloudflare’s free tier fully covers a personal/small-group email service: Email Routing for receiving, CF Email Sending + Resend fallback for sending, D1/KV/R2 for data, and a Vue3 web client. No VPS, no server upkeep. Before public deployment, sanitize everything — admin passwords, JWT secrets, API tokens, account IDs belong only in server-side environment variables.

Full Chinese version: /posts/cloud-mail-email-service/

Related