Humans shouldn't code - they make mistakes
A cross-platform email client — iOS, Android, and web all from one codebase — built as a Capacitor shell around a React + Vite + TypeScript UI.
Mail platforms are pluggable: the UI talks only to a MailProvider interface, and each
platform is a proxy registered behind it. Gmail is the first. Organization is
tag-based — messages carry any number of tags; there are no folders.
AI is built into how the client runs — and it's entirely self-hosted, behind a
MailIntelligence interface of its own. Point it at any OpenAI-compatible inference
server you run yourself — Ollama, vLLM,
or LM Studio — and arriving mail is auto-tagged into your real
tags, long threads open with a summary and action items, replies start from an AI
draft you edit, and the search box takes plain language ("invoices from ACME last
month"). Your mail content never leaves your machines, nothing is ever sent without
your explicit action, and if the AI server is down the client still reads, tags, and
sends mail normally.
Storage is local-first: synced mail lives in an on-device SQLite database, so previously synced threads stay readable offline, and text search runs on your device — each message carries a Bloom filter of its content words (stop words excluded) that makes local search instant without ever being able to miss a real match.
- Project status
- Running the client
- Running tests
- Adding a mail platform
- Extending with plug-ins
- Safety
Spec stage. This repo currently contains the specifications the code is generated from, not the code itself:
| File | Role |
|---|---|
SKILL.md |
The overall process and architecture: components, TDD loop, order of operations |
user-stories/typescript_mail_provider.md |
Spec: MailProvider interface, shared model, ProviderRegistry |
user-stories/python_gmail_bridge.md |
Spec: the local Gmail bridge service |
user-stories/typescript_gmail_proxy.md |
Spec: GmailProvider, the first concrete proxy |
user-stories/typescript_mail_intelligence.md |
Spec: MailIntelligence + LocalIntelligence, the self-hosted AI core |
user-stories/typescript_mail_store.md |
Spec: MailStore + SqliteMailStore, offline storage and Bloom-filter search |
user-stories/typescript_plugin_system.md |
Spec: MailPlugin + PluginHost, typed crash-isolated extension points |
user-stories/typescript_email_ui.md |
Spec: the AI-driven screens and Capacitor shell |
TODO.md |
Long-term feature backlog (derived from FairEmail) |
CLAUDE.md |
Working rules for code generation (strict TDD) |
User stories in the user-stories/*.md files are the source of truth: every story becomes a
test before any implementation is written (red → green → refactor → commit).
Startup checklist — verify each step before moving to the next:
Startup:
- [ ] 1. Bridge started and /health returns ok
- [ ] 2. AI server running and model loaded (optional but recommended)
- [ ] 3. App started
.venv/bin/python bridge/app.py
The bridge talks to Gmail itself; the app talks only to the bridge (localhost, port
8765 by default — see --port, --token, --client-secret, --verbose).
Verify before continuing: curl http://127.0.0.1:8765/health → {"status": "ok"}.
If it fails, fix the bridge first — nothing downstream works without it.
The first run needs a browser: it opens a Google sign-in page to authorize the account and saves a reusable token file. It cannot authenticate headless on a cold start; every run after that reuses the token silently.
Start Ollama, vLLM, or LM Studio with your chosen model loaded.
Verify: curl $VITE_AI_BASE_URL/models (e.g. http://127.0.0.1:11434/v1/models)
lists the model you set in VITE_AI_MODEL. If the server is down or the model is
missing, the app still runs — AI affordances show an actionable error instead.
In a browser during development:
npm run dev
On a device or simulator:
npm run build
npx cap sync
npx cap run ios # or: npx cap run android
If the device can't reach the host's localhost (e.g. the Android emulator), point the
app at the bridge with VITE_BRIDGE_URL (the Android emulator reaches the host at
http://10.0.2.2:8765).
AI features need a self-hosted inference server — Ollama, vLLM, or LM Studio, all of
which serve an OpenAI-compatible /v1 endpoint. Configure it with VITE_AI_BASE_URL
(default http://127.0.0.1:11434/v1, Ollama's default; LM Studio uses :1234/v1, vLLM
:8000/v1) and VITE_AI_MODEL (the model you've pulled/loaded, e.g. llama3.1). Mail
content goes only to that server — never to a cloud AI service. Without a running server,
the AI affordances are disabled and everything else works.
.venv/bin/python -m pytest bridge/tests/ -q # Python bridge
npx vitest run # all TypeScript layers (providers, intelligence, store, plugins, UI)
Tests are fully mocked — nothing touches a real account or the network.
Implement the MailProvider interface and register the provider at the composition
root. That's the whole job — no UI changes, ever. The UI renders whatever the
interface returns and hides anything the provider's capabilities() doesn't support.
Implement the MailPlugin interface and register it at the composition root. Plug-ins
contribute through typed extension points — message-view panels, compose transforms,
thread actions, settings panels — and are crash-isolated: a plug-in that throws is
auto-disabled with an error shown in the plug-in settings screen, and core mail flows
keep working. Enable, disable, and configure plug-ins from settings.
- The bridge binds to
127.0.0.1only; the mailbox is never reachable off-device. - Trash only moves mail to Gmail's Trash — nothing is ever deleted permanently.
- Mail data and credentials stay on your device; there is no third-party server.