@

a flightlesstux project × claude code

eeMail

Self-hosted. Gmail-inspired. Built entirely with Claude.

⚡ IMAP / SMTP 🔒 GPG Encrypted 🐳 Docker Compose ⚠ WIP — coming soon
Work in progress · release coming soon

the problem

Email clients are
a privacy nightmare.

  • Gmail scans your inbox to serve ads
  • Outlook uploads contacts without asking
  • Cloud clients breach confidentiality
  • Fat Electron apps slow your machine

100%

data stays on your machine

The solution: run your own email client. Access any IMAP/SMTP account. Beautiful UI. Everything local.

vision

Gmail experience.
Your server. Your rules.

📬
Multi-account
Gmail, Fastmail, your own SMTP — unified inbox, one UI.
🔒
GPG encrypted
Credentials never stored in plaintext. Auto-generated GPG keypair on first run.
Real-time push
IMAP IDLE → Valkey pub/sub → SSE → browser. Zero polling.
🔍
Local FTS5 search
SQLite FTS5 indexes subject, from, to, body. Instant local search.
🧵
Thread view
Collapse / expand conversation threads, just like Gmail.
🎹
Keyboard shortcuts
Full Gmail-compatible keyboard navigation. Power users welcome.

stack

Everything chosen
for a reason.

Next.js 14 Frontend
Node.js / Express Backend
SQLite + FTS5 Database
Valkey (Redis fork) Cache
imapflow IMAP client
nodemailer SMTP
OpenPGP.js Crypto
Tiptap editor Compose

one command to run it all → docker compose up -d

enter claude

The conversation
that built eeMail.

E
I want to build a self-hosted Gmail clone. IMAP/SMTP, GPG encryption, local SQLite, Docker. Real-time push via SSE. Let's go.
C
Love it. Let's start with the schema — accounts, emails, folders, labels, FTS5 virtual table for instant search…
E
Add multi-layer auth: TOTP, YubiKey FIDO2, and Duo MFA. All stackable.
C
Done. otplib + @simplewebauthn/server + Duo Web SDK v4. Each layer optional, all stackable, session cookies HTTPOnly + Strict.
E
Think what we can add more and how we can improve?
C
Virtual scrolling, thread view, attachment parsing, OAuth2 sign-in, on-demand body fetch — all in one PR. Shall I?
E
do it all

what we built

5 features. One PR.

📜
Virtual scrolling
@tanstack/react-virtual — renders only visible rows. Thousands of emails, zero jank.
🧵
Thread view
Expand / collapse conversation threads. Route ordering bug caught & fixed by Claude.
📎
Attachment parsing
simpleParser on body fetch → chips in reader. Stored in SQLite, served on demand.
Real-time SSE push
IMAP IDLE → Valkey pub/sub → SSE → React hook. New mail without any polling.
🔑
OAuth2 sign-in
PKCE flow for Gmail/Outlook. Token refresh handled silently. No password needed.
git log --oneline
feat: virtual scroll + thread view + attachments
feat: SSE real-time push via Valkey pub/sub
feat: OAuth2 PKCE for Gmail & Outlook

architecture decision

Emails live on
the server. Always.

▸ Sync Strategy

  • Fetch only envelope + headers + bodyStructure on sync
  • Full body fetched on-demand when user opens email
  • Cached in Valkey 5 min (emailbody:{id})
  • IMAP IDLE — no polling, zero wasted connections
  • UIDNEXT + HIGHESTMODSEQ tracked per folder
IMAP connection pool capped at 2 per account — 1 for IDLE, 1 for commands. Protects against rate-bans from Gmail/Outlook.
sync flow
# IMAP IDLE (always-on)
IMAP → EXISTS response
sync → fetch UID envelope
SQLite → insert email row
Valkey → PUBLISH mail:new
SSE → push to browser
# User opens email
GET /api/v1/emails/:id
cache? HIT → return immediately
cache? MISS → IMAP BODY fetch
Valkey → SET emailbody:{id} 300s

debugging story

The three-bug chain
Claude unravelled.

BUG #1 — swatch killing theme

Dark swatches (Slate, Midnight, Forest) added html.dark themselves — then theme toggle tried to undo it but the swatch handler was running after.

BUG #2 — blocking script race

The inline <script> in layout.tsx added .dark for dark swatches on page load — overriding the theme preference already set.

BUG #3 — useEffect ordering

applyTheme() ran before applyThemeSwatch() — swatch always won.

FIX — separation of concerns

  • applyThemeSwatch → only sets --swatch-bg
  • applyTheme → sole authority on html.dark
  • useEffect order fixed: swatch first, theme last
  • Blocking script: no more .dark injection
" Claude traced the bug backwards through three layers of initialization — something that would have taken me a day took 10 minutes. "

— flightlesstux

CI / security

Three gates.
No exceptions.

7 PARALLEL SECURITY JOBS

npm audit
gitleaks — full git history
trivy — CVEs in deps
CodeQL — SAST
Semgrep — OWASP Top 10
njsscan — Node.js patterns
license-check — copyleft

CI GATES (all 3 required)

✓ CI unit + integration + Docker build
✓ API Quality smoke + contract + negative tests
✓ Security 7 parallel jobs above
Coverage thresholds enforced: 80% lines · 75% branches · 80% functions.
OWASP ZAP DAST runs weekly. k6 load tests run weekly.

by the numbers

Claude + human.
What we shipped.

25+
files changed
7
security jobs
5
test types
4
auth layers

COVERAGE TARGETS

Lines≥ 80%
Branches≥ 75%
Functions≥ 80%

AUTH LAYERS

1 Master passphrase (bcrypt + GPG unlock)
2 TOTP — otplib RFC 6238 + 10 backup codes
3 YubiKey / FIDO2 — @simplewebauthn
4 Duo MFA — Universal Prompt SDK v4

what's next

WIP Roadmap

actively building

COMING SOON

01 Mobile-responsive layout
02 Drag-and-drop label assignment
03 S/MIME + PGP email signing
04 Contacts & people graph
05 Import/Export & backup

WHY BUILD WITH CLAUDE?

  • Proposes features I hadn't thought of
  • Catches bugs across three files at once
  • Writes tests while writing code
  • Remembers architecture decisions
  • Never gets tired of refactoring
eeMail is a real proof that a single person + Claude can produce production-grade, security-hardened software — faster than a team could plan a sprint.
@

eeMail

Self-hosted. Encrypted. Built with Claude.

quick start
$ git clone github.com/flightlesstux/eemail
$ docker compose up -d
open http://localhost:3000
github.com/flightlesstux/eemail built with claude code ⚠ wip — coming soon

made by flightlesstux × claude sonnet