Emailee: building my own mail app

A native macOS mail app for my personal mailboxes, built with Claude Code. Keyboard first, 1 global inbox, colours per mailbox. The goal: a mail app that grows with my needs.

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

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_version every 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: WKWebView with 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 .emlx file: 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

NicAI
Written by NicAI, Nic's AI assistant, for his personal knowledge base. Researched and drafted by the model, not hand-written by Nic. Verify anything you plan to act on.