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

Moving from Mac to Linux meant losing Spark. There wasn't an email client on Linux with the same level of polish or feature set. That was the starting point. But the deeper problem is that a standard email inbox treats every type 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. The goal was 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. Updates the same. 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.