A personal email triage agent designed for an ADHD brain. Reads 3 inboxes via IMAP, classifies every thread with a cross-channel sanity check (Jira status + Sent-folder staleness), and surfaces only what actually needs action — in a single native macOS surface. Default class is NOISE; the classifier must justify promotion.
At a glance
- 568 daily messages → 7 P0/P1 rows on a typical day
- Triage time: ~90 min → ~8 min after 4 weeks of iteration
- False-positive P0: 7–9/day → 2–3/day after cross-channel verification landed
- Single surface — a native SwiftUI macOS app (no Electron, no Slack digest, no mobile push)
- 3-section collapsible UI — P0 / P1 / P2 with Done + Archive shortcuts (no Snooze)
- Backend — FastAPI + 3 systemd timers (poll-imap, classify-batch, sync-actions) on a small commodity VM
- Database — Postgres 16 with three tables:
threads,classifications,actions - Classifier — Claude Haiku 4.5 with thread-latest-only input, share-notification hard-rules, future-tense gate
- Cost — ~$1.20/month in classifier tokens after subset-test discipline
- Reachable — private HTTPS endpoint via a Cloudflare named tunnel; bearer auth + optional CF Access on the UI
Stack
Python 3.11 · FastAPI · Postgres 16 · systemd timers · Claude Haiku 4.5 · IMAP (imaplib) · Gmail API (for label/archive sync) · Jira REST · Swift + SwiftUI (native macOS, NSPanel + Liquid Glass aesthetic) · Cloudflare Tunnel
Documentation
| Doc | Read this for |
|---|---|
| PRD | What & why — problem framing, JTBD, goals, scope, milestones |
| Architecture | System diagrams, data flow (pull → classify → surface → action), failure modes |
| Implementation | Tech stack, schema, classifier prompt design, perf numbers, security model |
| Notes | Chronological decision log + the reclassify cost trap |
Quickstart for the operator
# 1. Provision a small commodity VM (1 vCPU / 1 GB is enough)
# 2. Postgres 16 + 3 systemd timers (poll-imap, classify-batch, sync-actions)
# 3. Drop IMAP creds + Gmail OAuth token + Jira PAT into /etc/mail-assistant/
# 4. cloudflared named tunnel → https://<your-host>
# 5. Build the Mac app:
xcodebuild -project MailAssistant.xcodeproj -scheme MailAssistant -configuration Release
# 6. Open Mail-Assistant.app → enter bearer token → triage
Project status
| Day | Milestone |
|---|---|
| 1 | IMAP poller + Postgres schema + naive 4-class classifier (P0/P1/P2/Archive) |
| 1 | Default flipped to NOISE; classifier must justify promotion |
| 1-2 | Cross-channel checks added (Jira status + Sent-folder staleness) |
| 2 | Thread-latest-only classification (drop history) |
| 2 | Share-notification hard-rule (Sheets/Notion/Dropbox/Figma) |
| 2 | SwiftUI app — 3 collapsible sections, Done/Archive shortcuts |
| 2 | Action sync — Done/Archive propagates to Gmail (archive INBOX + label) |
| 2 | Deployment: cron-host VM + Cloudflare named tunnel |
Total build time: 2 days concentrated work.
Why this exists
Inbox triage was costing 90 minutes per morning of high-fatigue context-switching before any real work happened. Off-the-shelf options (Gmail Priority Inbox, SaneBox, Superhuman) optimize for click-through, not “needs reply” — and none of them know that the Jira ticket the email refers to is already Done, or that I already replied 6 minutes ago from my phone. Mail-Assistant is built around that missing context layer.