Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

5 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

AvaMail

Self-hosted email on your own domain, running entirely on Cloudflare's edge.

Receive mail through Cloudflare Email Routing, store it in a D1 database (attachments optionally in R2), send through Brevo, and manage everything from a Vue 3 web app served by the same Worker. One deploy, no servers, free-tier friendly.

πŸ€– Installing with an AI agent (Claude Code, etc.)? Point it at agent-install.md β€” a step-by-step install guide written for agents.

Features

Mail

  • Catch-all receiving on your domain β€” every address at your domain lands in one inbox
  • Sending via Brevo's transactional API, with delivery tracking (delivered / opened / bounced) reported back by webhook
  • Conversation threading via Message-ID / In-Reply-To headers
  • Drafts, reply-with-quote, forwarding (original attachments included), and scheduled send (cron-delivered)
  • Archive, labels, starring, and full-text search (SQLite FTS5 over subjects, bodies, and addresses)
  • Attachments up to 5MB/file in D1, or 25MB/file with an optional R2 bucket
  • Contacts with autocomplete in Compose β€” people you write to are captured automatically
  • Per-message .eml download and whole-mailbox mbox export
  • Optional forwarding of received mail to a personal inbox as backup

App

  • Vue 3 SPA with full light / dark / system theming
  • Dashboard: daily send/receive activity chart, recent messages, system status, storage usage
  • Installable PWA with payload-free push notifications on new mail
  • Remote-image blocking (tracking-pixel protection) with per-email reveal

Security

  • Passkey sign-in (FIDO2/WebAuthn) β€” Touch ID, Windows Hello, security keys β€” plus a password fallback
  • PBKDF2 password hashing (100k iterations), session tokens stored as SHA-256 hashes
  • Received HTML sanitized with DOMPurify before rendering
  • Strict Content-Security-Policy and security headers on every response
  • Shared rate limit for password and passkey login attempts
  • Server-side attachment validation, safe download headers, sanitized filenames

Architecture

                        Cloudflare Edge
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                                                                β”‚
β”‚  Email Routing ──▢ Email Worker ──┬─▢ D1 (mail, contacts,      β”‚
β”‚   (catch-all)                     β”‚      settings, sessions,   β”‚
β”‚                                   β”‚      FTS index)            β”‚
β”‚  Cron (*/5) ──▢ scheduled send ────                            β”‚
β”‚                                   β”œβ”€β–Ά R2 (attachments,         β”‚
β”‚  HTTPS ──▢ Worker (Hono API) β”€β”€β”€β”€β”€β”˜      optional)             β”‚
β”‚              β”‚                                                 β”‚
β”‚              └─▢ Vue 3 SPA (static assets)                     β”‚
β”‚                                                                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚ send                        β”‚ delivery webhooks
               β–Ό                             β”‚
             Brevo β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  Browser ◀── Web Push (VAPID, no payload) ── Email Worker

Quick start

Prerequisites: a Cloudflare account with a domain, a Brevo account (free tier is fine), Node.js 18+, and pnpm.

# 1. Clone and install
git clone https://github.com/MrOplus/AvaMail.git
cd AvaMail
pnpm install

# 2. Authenticate wrangler
npx wrangler login

# 3. Create the database
cd packages/worker
npx wrangler d1 create avamail-db          # note the database_id in the output

# 4. Configure
cp wrangler.toml.example wrangler.toml     # then paste your database_id into it

# 5. Initialize the schema (fresh installs only β€” see Upgrading otherwise)
npx wrangler d1 execute avamail-db --remote --file=./src/db/schema.sql

# 6. Build and deploy
cd ../..
pnpm -r build
cd packages/worker
npx wrangler deploy

Your app is now live at https://avamail.<your-subdomain>.workers.dev.

First-run configuration (in the app)

  1. Open the URL and create your admin password. Add a passkey under Settings β†’ Security for passwordless sign-in.

  2. Settings β†’ Cloudflare β€” create an API token (dash.cloudflare.com β†’ API Tokens β†’ Create Custom Token) with these permissions, then enter it with your Account ID and domain:

    Scope Permission Access
    Account Email Routing Addresses Edit
    Zone Zone Read
    Zone DNS Edit
    Zone Email Routing Rules Edit
    Zone Zone Settings Edit

    Saving enables Email Routing on your zone and points the catch-all rule at the worker. If inbound mail ever stops, the Fix Email Routing button re-applies the rule.

  3. Settings β†’ Brevo β€” paste your Brevo API key (app.brevo.com β†’ SMTP & API). Saving also auto-registers the delivery-status webhook.

  4. Verify your sending domain in Brevo (Senders & Domains) β€” add the SPF / DKIM / DMARC records it gives you, or outbound mail will be rejected or spam-foldered.

  5. Settings β†’ Addresses β€” add at least one From address.

  6. Click Complete Setup, then test both directions: mail an external message to anything@yourdomain.com and send one out via Compose.

Configuration reference

wrangler.toml (gitignored; copy from wrangler.toml.example):

name = "avamail"                 # worker name β€” also the catch-all rule target
main = "src/index.ts"
compatibility_date = "2024-01-01"

[[d1_databases]]
binding = "DB"
database_name = "avamail-db"
database_id = "YOUR_DATABASE_ID_HERE"

[assets]
directory = "../../frontend/dist"
binding = "ASSETS"               # SPA deep links (/inbox, /login, …)
run_worker_first = true          # security headers on every response

# Optional β€” see "R2 attachments" below
# [[r2_buckets]]
# binding = "ATTACHMENTS"
# bucket_name = "avamail-attachments"

[triggers]
crons = ["*/5 * * * *"]          # scheduled send + session cleanup

[vars]
APP_NAME = "AvaMail"

Secrets (Cloudflare token, Brevo key) are never placed in config files β€” they're entered through the app UI and stored in D1.

R2 attachments (optional)

Without R2, attachments live base64-inline in D1 (5MB per file). To raise the cap to 25MB and keep the database lean:

  1. Enable R2 once in the Cloudflare dashboard (the API errors with code 10042 until you do).
  2. npx wrangler r2 bucket create avamail-attachments
  3. Uncomment the [[r2_buckets]] block in wrangler.toml and redeploy.

Existing inline attachments keep working β€” reads fall back to D1 automatically.

Upgrading

Fresh installs use schema.sql only. Existing databases apply what they're missing, each file exactly once:

cd packages/worker
# pre-passkey installs first bring the base schema up to date:
npx wrangler d1 execute avamail-db --remote --file=./src/db/schema.sql
# then the structural migration (adds threading/drafts/FTS/etc. β€” run ONCE):
npx wrangler d1 execute avamail-db --remote --file=./src/db/migrations/002-features.sql

Unsure which generation you're on? agent-install.md has a decision table keyed off which tables exist. After migrating: pnpm -r build and npx wrangler deploy.

Upgrading from a pre-passkey version signs everyone out once (session tokens are now stored hashed) β€” just log in again.

Development

# Terminal 1 β€” worker + local D1 on :8787
cd packages/worker
npx wrangler d1 execute avamail-db --local --file=./src/db/schema.sql
npx wrangler dev

# Terminal 2 β€” Vite dev server with HMR on :5173 (proxies /api to :8787)
cd frontend
pnpm run dev

Test the cron locally: start with npx wrangler dev --test-scheduled, then curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*".

Project structure

AvaMail/
β”œβ”€β”€ frontend/                  # Vue 3 SPA (Vite, Tailwind, Pinia)
β”‚   β”œβ”€β”€ public/                # PWA manifest, service worker, theme bootstrap
β”‚   └── src/
β”‚       β”œβ”€β”€ api/               # typed API client
β”‚       β”œβ”€β”€ components/        # EmailList, ActivityChart
β”‚       β”œβ”€β”€ stores/            # auth, emails, theme
β”‚       β”œβ”€β”€ utils/             # avatar helpers
β”‚       └── views/             # Dashboard, Inbox, EmailView, Compose,
β”‚                              # Drafts, Archive, Contacts, Settings, …
β”œβ”€β”€ packages/
β”‚   β”œβ”€β”€ worker/                # Cloudflare Worker (Hono)
β”‚   β”‚   └── src/
β”‚   β”‚       β”œβ”€β”€ api/           # routes: auth, webauthn, emails, contacts,
β”‚   β”‚       β”‚                  # push, webhooks, settings, setup
β”‚   β”‚       β”œβ”€β”€ db/            # schema.sql, migrations/, queries.ts
β”‚   β”‚       β”œβ”€β”€ lib/           # mailer, attachments (R2/D1), push (VAPID),
β”‚   β”‚       β”‚                  # eml builder, rate limiting, validation
β”‚   β”‚       β”œβ”€β”€ email-handler.ts   # inbound mail: parse, thread, store, notify
β”‚   β”‚       └── index.ts           # fetch + email + scheduled entrypoints
β”‚   β”œβ”€β”€ brevo-api/             # Brevo REST wrapper (send, senders, webhooks)
β”‚   └── cloudflare-email-api/  # Cloudflare REST wrapper (zones, routing, DNS)
└── agent-install.md           # agent-oriented install guide

API reference

All endpoints are under /api. Everything except login/setup ceremonies and the Brevo webhook requires Authorization: Bearer <token>.

Auth

Endpoint Method Description
/auth/status GET Auth state, needsSetup, hasPasskeys
/auth/setup POST Create the admin password (first run)
/auth/login POST Password login (rate limited: 5 / 15 min)
/auth/logout POST Invalidate the session
/auth/change-password POST Change password
/auth/webauthn/register/options Β· /verify POST Passkey registration ceremony
/auth/webauthn/login/options Β· /verify POST Passkey login ceremony (shares the login rate limit)
/auth/webauthn/credentials GET List passkeys
/auth/webauthn/credentials/:id DELETE Remove a passkey

Mail

Endpoint Method Description
/emails GET List (type=received|sent|draft|scheduled, archived, label, unread, starred, limit, offset)
/emails/search GET Full-text search (?q=)
/emails/labels GET All labels in use
/emails/stats GET Counts + storage usage
/emails/activity GET Daily send/receive counts (?days=14)
/emails/export/mbox GET Whole-mailbox mbox download
/emails/drafts POST Create or update a draft
/emails/send POST Send β€” supports draftId, scheduleAt, replyToEmailId, forwardOfEmailId, attachments
/emails/:id GET Single email with attachments + conversation thread
/emails/:id/eml GET Download as .eml
/emails/:id/read Β· /unread Β· /star POST Flags
/emails/:id/archive Β· /unarchive POST Archive state
/emails/:id/labels PUT Replace labels
/emails/:id DELETE Delete email + attachments (D1 and R2)
/emails/:id/attachments/:attachmentId GET Download an attachment

Contacts, push, setup

Endpoint Method Description
/contacts GET / POST List / create (upserts by email)
/contacts/:id PUT / DELETE Update / delete
/push/vapid-key GET VAPID public key
/push/subscribe Β· /unsubscribe POST Manage push subscriptions
/webhooks/brevo/:secret POST Brevo delivery events (auto-registered; secret-guarded)
/setup/status GET Configuration state
/setup/cloudflare POST Connect Cloudflare + enable routing
/setup/cloudflare/worker-routing POST Re-point the catch-all at the worker
/setup/brevo POST Connect Brevo + register webhook
/setup/addresses GET / POST / DELETE Manage From addresses
/setup/complete POST Finish onboarding

Troubleshooting

Symptom Fix
"Frontend assets not found" Build before deploying: pnpm -r build, then redeploy
Inbound mail not arriving Settings β†’ Cloudflare β†’ Fix Email Routing; verify MX records and that the zone is active; npx wrangler tail to watch the worker
Outbound rejected / in spam Verify the sending domain (SPF/DKIM/DMARC) in Brevo
No delivery badges on sent mail Re-save the Brevo API key in Settings to (re)register the webhook
429 on login Rate limit β€” wait 15 minutes (setup: 3/hour)
Signed out after upgrading Expected once (hashed session tokens) β€” sign in again
Passkey missing on a new domain Passkeys bind to the hostname; add one on the new host
Scheduled email shows "failed" Fix the Brevo config; the message is preserved under Drafts β€” edit and resend

License

MIT β€” see LICENSE.

Support

Issues and feature requests: GitHub Issues.

About

A self-hosted email management system built on Cloudflare Workers. Receive emails on your custom domain, store them in a database, and send emails through Brevo - all running on Cloudflare's edge network.

Topics

Resources

Stars

22 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages