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.

The Emailee app icon: a white letter with the hexagon edelweiss logo in a red envelope, on a light grey square, with the name Emailee under it

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. D replies 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. ⌘K opens 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 1 to 8.
  • Mailbox filter. ⌥1 to ⌥9 shows 1 mailbox in every view. Each mailbox has its colour and logo.
  • Group by sender. G groups the list by sender, and the triage keys act on the whole group.
  • Sender's domain. F filters every view to the sender's domain, across all folders.
  • Command Centre. ⌘K lists every function with its key. A command that can't run says why.
  • Search. ⌘⇧F or /, full text over every mailbox, with filter menus and operators such as from:, 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, ⌘0 returns to my reading size.
  • Square corners. 1 setting for the window and every highlight, shared with my other apps.

Reading

  • Keyboard first. j/k to move, e archive, # trash, ! red, r reply, ? shows every key. Keys never fire while I type.
  • Read state by hand. Selecting an email doesn't mark it read. u toggles it, ⌘Z undoes 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. i loads 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. h shows the headers, ⇧H copies the full headers for a spam report, C shows the contact card.
  • Copy as Markdown. y copies 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. ⌥⌘F types 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. ⌘N opens 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. D replies with it, Y copies it.

Triage and workflows

  • Waiting on reply. w marks 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. B blocks 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. ⇧S sends 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, compose and show, all with --json. Nothing in the CLI sends, deletes or marks mail read.
  • Compose from other tools. emailee://compose opens 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_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.
  • 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

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.