Self-hosted email + calendar — entirely on Cloudflare. No Google. No iCloud. No third-party mail service.
| 📥 Inbound email | Email Routing → Worker email() handler → parsed & stored in D1 |
| 📤 Outbound email | Send real mail as you@yourdomain.com via the send_email binding |
| 📅 Calendar invites | Parses text/calendar, RSVP Yes / Maybe / No sends a real iCal METHOD:REPLY — organizers see you@yourdomain.com: Accepted |
| 🗓️ Independent calendar | Create/edit/delete events in your own calendar, send REQUEST/CANCEL invites, import .ics anywhere |
| ⏰ Reminders | Cron trigger emails you before events |
| 🔐 Auth | Password login, HMAC-signed session cookie |
| 🖥️ UI | Minimal dark interface — Inbox, Sent, Compose, Month-view Calendar |
flowchart LR
A[📧 Someone emails<br/>you@yourdomain.com] -->|Email Routing| W[☁️ Worker]
W -->|parse + store| DB[(D1)]
UI[🖥️ Built-in UI] -->|REST API| W
W -->|send_email| OUT[📤 Outbound]
CRON[⏰ Cron */10m] --> W
GCAL[📅 Google invite] -->|text/calendar| W
W -->|METHOD:REPLY| GCAL2[Organizer sees<br/>✅ Accepted]
git clone https://github.com/DevArtsLab/devarts-mail.git
cd devarts-mail
./install.shThat's it. The installer:
npm install- generates
.dev.varswith strong random secrets (and prints your UI password) - runs
wrangler loginif needed (browser OAuth — that's the only interaction) - provisions D1, runs migrations, uploads secrets, deploys, and creates the Email Routing rule
you@domain → worker
Everything lives in wrangler.jsonc — worker, assets, D1, send_email, cron, routes, and all app variables:
| Var | Default | Meaning |
|---|---|---|
MAIL_DOMAIN |
devartslab.com |
Your domain |
PRIMARY_ADDRESS |
amir@devartslab.com |
Send/receive identity |
DISPLAY_NAME |
Amir | DevArts Lab |
From-name on outgoing mail |
AUTO_ACCEPT_INVITES |
"false" |
Auto-accept incoming invites |
INVITE_REMINDER_MINUTES |
"30" |
Reminder email before events (0 = off) |
SESSION_TTL_HOURS |
"168" |
UI session length |
Secrets live in .dev.vars locally / wrangler secret in prod — never committed.
├── wrangler.jsonc # ← THE unified config
├── install.sh # one-command setup
├── migrations/ # D1 schema
├── scripts/setup.mjs # provisioning engine (idempotent)
├── src/
│ ├── index.ts # entry: fetch + email + scheduled
│ ├── inbound.ts # inbound parse → store → RSVP side-effects
│ ├── mail.ts # postal-mime in / mimetext + send_email out
│ ├── ical.ts # RFC 5545 iCal parser + generator
│ ├── api.ts # REST API for the UI
│ ├── auth.ts # cookie session auth
│ └── db.ts # D1 access layer
├── public/ # the UI (served via Workers Assets)
└── test/ # vitest unit tests
npm run dev # local dev server
npm test # unit tests
npm run typecheck # TypeScript check
npm run deploy # deploy
npm run setup # full re-provisioning (idempotent)
npm run migrate:remote # apply D1 migrationsPush to main auto-deploys via GitHub Actions (set repo secrets
CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID).
- Clone →
./install.shafter editingwrangler.jsoncvars(MAIL_DOMAIN,PRIMARY_ADDRESS,DISPLAY_NAME,D1_DATABASE_NAME) - Done — your mail + calendar runs on Cloudflare, data stays in your D1.
- Replies to calendar invites are real RFC 5546
METHOD:REPLYemails withIn-Reply-Tothreading — Gmail/Google Calendar records your status. - Email Sending requires the
cf-bounceDNS records on your domain — don't delete them. - Full calendar export:
GET /api/calendar.ics.
Built on Cloudflare Workers · D1 · Email Routing · Email Sending
