Emailee is my own mail app for macOS. It shows my 7 personal mailboxes in 1 global inbox, and I drive it from the keyboard. It replaces Mail.app.
The goal isn't to finish it. The goal is a mail app that I customise over time, until it fits exactly how I work. Every mail app I have used makes me adapt to it. With Claude Code, a feature I want is 1 session away, so the app can adapt to me instead.
The name reuses an old one: my first Emailee was a toolbox of email scripts (!projects/emailee). The -ee suffix marks my other tools too.
Why build a mail app
Mail.app does the basics well. It fails on the things I care about:
- Keyboard. I want 1 key per action: next, archive, flag, snooze, reply. Mail.app needs the mouse or 2-key shortcuts for most of them.
- Red means red. In Mail.app, colours and flags mix with unread states and rules. I want red to mean 1 thing: I flagged it, by hand, because it needs me.
- Colours I choose. I want to see at a glance which mailbox an email came from, and to colour some emails with my own rules. For example, beta-tester feedback for a side project shows in purple.
- My workflows. Follow-ups, snooze, and replies drafted by my own local model aren't built into any mail app I know.
What v1 does
| Feature | How it works |
|---|---|
| Global inbox | All 7 mailboxes in 1 list, 1 row per conversation. Each mailbox also has its own folders in the sidebar |
| Keyboard | j/k to move, e archive, # trash, ! red, u read, r reply, / search, ? shows every key |
| Mailbox colours | Each mailbox has a colour: a bar on the left edge and a light tint on every row |
| Colour rules | A small TOML file. The first matching rule tints the row, for example by sender and subject |
| Red | Only the ! key sets red. It is the server flag, so my iPhone shows the same red |
| Waiting on reply | w marks a thread. It clears by itself when the other person replies. Auto-replies don't count |
| Snooze | s, then 1 key: in 3 hours, tonight, tomorrow morning, next Monday |
| NicAI drafts | d: I type 2 lines of facts, my local model writes 3 drafts in my voice, I pick 1 |
| Search | Full text over every mailbox, with from:, to:, is:red, has:attachment |
| Zoom | ⌘= and ⌘-, ⌘0 to reset. Fonts, rows and the message scale together |
How I built it
I built it in 2 days, in Claude Code sessions. The first session was only analysis: can I sync each mailbox, what does each option cost, what can go wrong. Then a written spec, then a review of that spec by a second agent. The review found 34 defects before a line of code existed, 6 of them blockers. That step saved me more time than any other.
Swift or a web view?
The one real decision was the app itself. I asked Claude for pros and cons before I chose:
| Option | For | Against |
|---|---|---|
| Swift (SwiftUI) | Truly native: real lists, menus, dark mode, speed | I can't maintain Swift myself |
| Python + web view | 1 language I know, fast to change | Looks "almost native", breaks on Python upgrades |
I chose Swift for the window and kept Python for everything else. A mail app is open all day, and "almost native" shows in every scroll. The hard part, sync, stays in Python, where I can read it.
For business people
Email is the tool I use most, and I have never been able to change it. Now I can. The first version took 2 days. Each new feature is a short session: describe it, test it, keep it.
The costs:
- Maintenance is mine. If a mail server changes something, I fix it. Claude does the work, but I have to notice the problem.
- Security is mine too. Passwords sit in the macOS Keychain, remote images are blocked by default, and the app never runs scripts from an email.
- It isn't a product. It fits 1 person. That is the point.
For technical people
Architecture
IMAP / SMTP servers (7 mailboxes)
|
emailee_sync.py Python daemon under launchd, standard library only
| 1 thread per mailbox (IMAP IDLE), 1 fast and 1 slow worker
v
emailee.db SQLite in WAL mode + 1 .eml file per message
^
Emailee.app SwiftUI + AppKit, no third-party packages
reads SQLite, writes only its own tables and a queue
- The daemon owns the network. The app never talks to a mail server. It writes an action (archive, flag, send) to a queue table, and the daemon runs it within 1 to 2 seconds.
- 1 owner per table. The app writes its own state (snooze, waiting, local read), the daemon writes everything else. Both sides upsert only their own columns.
- Change detection: the app checks SQLite's
PRAGMA data_versionevery second and reloads when the daemon has committed. - The message list is an AppKit
NSTableView. It scrolls tens of thousands of conversations smoothly, which a pure SwiftUI list doesn't guarantee. - Message view:
WKWebViewwith JavaScript off and a content rule list that blocks every remote load. That stops tracking pixels without writing an HTML sanitiser.
Details that mattered
- No download for history. Mail.app keeps every message on disk as an
.emlxfile: a byte count, the message, then a plist. Emailee imported 8,500 messages from those files in 41 seconds. The first IMAP sync then only matches server IDs by Message-ID. - Stable message keys. The key is a hash of account, Message-ID and folder type. A move keeps the key, so red, snooze and waiting state survive when I archive on my phone.
- Keys vs typing. Single-letter shortcuts must never fire while I type. Every key goes through 1 event monitor that steps aside when a text field has focus.
- A crash worth knowing. WebKit cancels image requests when you switch messages fast. Answering a cancelled request crashes the app. The fix: track open requests and drop the late answers.


What comes next
The list grows as I use it. Ideas so far:
- Sender card: what I know about the person, from my own local data.
- Smarter triage: my local model sorts newsletters and receipts out of the inbox.
- Templates: my recurring replies, filled in with 1 key.
- Rules that act: move or snooze by rule, not only colour.
Each one follows the same loop: I notice a friction in my day, I describe it, and Emailee changes.
Further Reading
- What is NicAI for the rest of my NicAI stack.
- Portee: one page for all my local apps for the dashboard that runs my other local apps.