ShippedDesktop App

Mailautumn

Complete frontend rewrite of Mailspring — threaded conversations, AI integration, and a built-in CRM

ElectronReact 18TypeScriptTailwind v4JotaiVitebetter-sqlite3

Listen

0:00 / --:--

The Problem

A standard email inbox treats every kind of email the same way. Conversations with colleagues sit next to newsletters, receipts, automated updates, and formal threads where every reply repeats the same signature block. It's a single list for fundamentally different things. Moving from Mac to Linux — and losing Spark in the process — was the moment I decided to build the client that fixed both: a solid Linux-native experience and a UI that understands what kind of email it's looking at. Conversations are formatted like Slack messages with signatures stripped. You see the actual new content, not the same block of text repeated six times. Newsletters get their own feed tab, laid out like an editorial feed rather than a list of unread items. Receipts surface the key detail at a glance without opening. Different types of communication, different interfaces.

A full rewrite of the Mailspring email client UI while keeping its battle-tested C++ sync engine. The goal was a modern, keyboard-first email experience with Slack-style threading, multi-provider AI assistance, and a lightweight CRM. Built as a daily driver, not a demo. 15,000+ lines of TypeScript across Electron main and React renderer.

What's interesting

Reuse what works, replace what doesn't

Mailspring's C++ mailsync binary handles IMAP/SMTP, threading, and full-text search. Years of battle-tested reliability. The app spawns it as a child process over stdio, reading from a shared SQLite database. This let the entire frontend be rebuilt in React without touching sync logic. Identifying the right abstraction boundary meant months of reliable email instead of reimplementing IMAP edge cases.

Content extraction as domain knowledge

Threaded conversations only work if each message shows only new content. The content extractor (457 LOC) uses domain-specific selectors to identify and strip Gmail, Outlook, Yahoo, and Apple Mail quote blocks, signature delimiters, and attribution patterns. Each client formats them differently so each gets its own handling. This is invisible to users but the difference between a conversation view and a noise wall.

Multi-provider AI with a single interface

A unified callAI() function routes to OpenAI, Anthropic, or Ollama, handling model name translation, endpoint path normalisation, and Anthropic's unique header format transparently. Temperature 0 for thread summaries and classification (reproducible), 0.7 for compose drafts (natural variation). Three different use cases, deliberately tuned differently.

Optimistic updates without flickering

After archive or trash, the UI marks threads as removed and suppresses delta-triggered reloads for 5 seconds, then clears after 30. Without this, the mailsync delta arrives before the database is consistent and the archived thread reappears momentarily. The guard is simple but the absence of it is the kind of thing that makes a daily driver feel broken.

Why this matters

Building a daily driver is a different constraint than building a demo. Every decision gets load-tested at real email volume, and the things that matter most are invisible — the message that doesn't reappear after archiving, the conversation that correctly strips the quoted block you'd already read six times. The right abstraction boundary was the key call: building on Mailspring's sync engine meant months of reliable email instead of reimplementing IMAP edge cases. That tradeoff shaped everything else.