![]()
28 Sep 2026 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, in 2022, was a toolbox of Python scripts for email. 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 |
02 Nov 2026 5 weeks of daily use later, the app has changed more than the first version suggested:
- Triage before reading. The Inbox splits into People, Notifications, Newsletters and Receipts. My local model moves a company email out of People when the headers miss it, and it drafts a reply in my voice for each new People email, in the background.
Dreplies with that draft. - A real composer. It works like Apple Mail: address chips, hidden Cc and Bcc, suggestions with the arrow keys, the quote folded away. Esc keeps the email as a draft, and I can undo a send for 10 seconds. It checks before the send, for example a mentioned attachment that isn't there.
- Every function on 1 key.
⌘Kopens a Command Centre with every action of the app, searchable by name or key. Every button has a tooltip with its key. - Fewer emails I didn't ask for. 1 key unsubscribes, another blocks the sender or the whole domain on the server. An alias that leaks gets blocked so that its emails bounce, and senders drop it.
- Emails into my other tools. Save to sends attachments to Scanee, my document scanner, or to a folder. Copy as Markdown turns any email into clean text for Claude.
All features
Views and navigation
- Global inbox. All 7 mailboxes in 1 list, 1 row per conversation, with each mailbox's folders in the sidebar.
- Inbox sections. People, Notifications, Newsletters, Receipts and All, switched with
[and]. - Today view. Per mailbox: new today, unread people, drafts ready, follow-ups due, red, snoozes that end today.
- Fixed views. Inbox, Red, Waiting, Snoozed, Sent, Archive, Outbox and Drafts, on keys
1to8. - Mailbox filter.
⌥1to⌥9shows 1 mailbox in every view. Each mailbox has its colour and logo. - Group by sender.
Ggroups the list by sender, and the triage keys act on the whole group. - Sender's domain.
Ffilters every view to the sender's domain, across all folders. - Command Centre.
⌘Klists every function with its key. A command that can't run says why. - Search.
⌘⇧For/, full text over every mailbox, with filter menus and operators such asfrom:,in:,after:,has:attachment. The first results show in milliseconds. - Light and dark. Follow macOS, light or dark, with
⌘⇧L. - Zoom. 9 levels for the whole window,
⌘0returns to my reading size. - Square corners. 1 setting for the window and every highlight, shared with my other apps.
Reading
- Keyboard first.
j/kto move,earchive,#trash,!red,rreply,?shows every key. Keys never fire while I type. - Read state by hand. Selecting an email doesn't mark it read.
utoggles it,⌘Zundoes it. - Mailbox bar. A wide bar in the mailbox colour for unread rows, a thin one for read rows.
- Sender logos. From a logo service, never from the sender's own site, which would tell the sender I opened the email.
- Remote images blocked.
iloads them for the sender's whole domain and remembers it. - Attachment chips. File type tile, name, kind and size. Drag a chip out to Finder.
- Headers and contact.
hshows the headers,⇧Hcopies the full headers for a spam report,Cshows the contact card. - Copy as Markdown.
ycopies the email with its headers as clean Markdown. - Colour rules. A small TOML file tints rows by sender, subject or recipient domain.
- Red. Only the
!key sets red. It's the server flag, so my iPhone shows the same red.
Writing
- Composer. Address chips that show the address, suggestions, hidden Cc and Bcc, a folded quote, signature per mailbox.
- From and aliases. A reply goes out from the alias the sender wrote to, envelope included.
⌥⌘Ftypes a new alias at send time. - Only allowed senders. The From menu lists only the addresses each mail server accepts.
- Drafts. Esc keeps the email, autosave every 4 seconds, a Drafts view with a preview.
- Undo send. 10 seconds to take an email back with
⌘Z. - Checks before the send. No recipient, no subject, a reply with no text, a no-reply address, a mentioned attachment that's missing.
- Attachments. Attach from the bar or drop files anywhere on the composer.
- New email from anywhere.
⌘Nopens a composer from any state, from the mailbox on screen. - NicAI drafts on request.
d: I type 2 lines of facts, my local model writes 3 drafts in my voice, I pick 1. - NicAI drafts ahead of time. The model drafts a reply to each new People email in the background.
Dreplies with it,Ycopies it.
Triage and workflows
- Waiting on reply.
wmarks a thread. It clears when the other person replies. - Follow-ups. An overdue waiting thread comes back to the top of the Inbox. The composer sets one with
⌘⇧F. - Snooze.
s, then 1 key: in 3 hours, tonight, tomorrow morning, next Monday. - Unsubscribe.
U: 1-click unsubscribe, else an email from the alias, else the web page. Then the sender's Inbox emails are archived. - Block.
Bblocks 1 address or the whole domain on the server. I can also block a sender at 1 alias only. - Block an alias. Every email to that alias bounces, so mailing services drop the address.
- Save to.
⇧Ssends attachments to Scanee, Downloads, an expenses folder or any folder I pick. Scanee runs in the background and the email shows "In Scanee" at once.
Sync and agents
- Sync daemon. A Python daemon owns the network: IMAP IDLE per mailbox, reconnects after sleep, a status pill per mailbox.
- Mail.app import. History imported from Mail.app's files, with no download.
- Agent CLI.
emailee status,unread,search,get,sync,composeandshow, all with--json. Nothing in the CLI sends, deletes or marks mail read. - Compose from other tools.
emailee://composeopens a draft that NicAI CC wrote, ready for me to check and send.
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.
- 02 Nov 2026 Search froze the app. Each keystroke ran the full-text match again for each of about 10,000 conversations. 1 query to the index instead made search 280 times faster: 59 ms instead of 16.4 s for 1 letter.
- 02 Nov 2026 Scanee made me wait. Filing an attachment in Scanee takes 30-60 seconds. The daemon now stages the files and answers at once, and a background thread files them, 1 at a time.


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.
- Templates: my recurring replies, filled in with 1 key.
- Rules that act: move or snooze by rule, not only colour.
02 Nov 2026 Smarter triage, the 4th idea on the first list, is built: the Inbox sections and the local triage model sort newsletters, notifications and receipts out of the People view.
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.