Mailautumn is a Slack-style AI email hub built on the Mailspring sync engine. Electron desktop app wrapping the mailsync C++ binary.
- Display name: Mailautumn
- Internal name: mailspring-next (DO NOT change — safeStorage keyring key depends on it)
- Working directory:
mailspring-next/— all code edits happen here, NOT inMailspring/
- Shell: Electron (latest)
- Bundler: Vite (via electron-vite)
- Frontend: React 18 + TypeScript + Tailwind CSS v4
- State: Jotai (atomic model — NOT Zustand, NOT Redux)
- DB reads: better-sqlite3 (preload/utility process)
- DB writes: mailsync (child process, JSON stdio)
- Credentials: Electron safeStorage
- Rich text: TipTap v2 (compose/reply)
- Linting: ESLint flat config + Husky + lint-staged
npm run dev # Start dev mode (Electron + Vite HMR)
npm run build # Build main + preload + renderer to out/
npm run typecheck # TypeScript strict check (both main + renderer)
npm run lint # ESLint across src/
npm run dist:linux # Package for Linux (AppImage + deb)DANGER: npm start, npm test, npm run test-window in the parent Mailspring/ directory launch full old Electron. Never run from CLI.
IMPORTANT: Changes to src/main/ or src/preload/ require npm run build (or npx electron-vite build) — HMR only covers the renderer.
- Main process (
src/main/): Electron main, IPC handlers, mailsync bridge, AI pipeline, OAuth, identity server - Preload (
src/preload/): contextBridge API surface — every renderer→main call goes through here - Renderer (
src/renderer/): React app, Jotai atoms, components, theme system, plugin system - mailsync: C++ binary at
resources/mailsync/mailsync.bin, communicates via JSON over stdio
- Small, feature-scoped commits. One feature/fix per commit, not bundles.
- Conventional commit format:
feat:,fix:,refactor:,chore:,docs: - NO Co-Authored-By signature. User preference.
- Run
npm run typecheckbefore pushing. The pre-commit hook runs lint-staged (ESLint on staged.ts/.tsxfiles). Typecheck is manual but required before push. - Review diffs before committing. No accidental debug code, console.log, or credential leaks.
Never use bracket notation for sizes, spacing, or colors:
- Bad:
text-[11px],w-[327px],min-w-[280px],gap-[6px] - Good:
text-xs,w-72,min-w-64,gap-1.5
Use standard Tailwind utilities or extend the theme in index.css via @theme if a value is genuinely needed. Color hex in brackets (e.g., text-[#4285f4]) is acceptable only for one-off brand colors that don't warrant a theme variable.
- No empty
.catch(() => {})— Always log or handle:.catch(err => console.error('[context]', err)) - Minimize
any— Use proper types.anyis a warning in ESLint; reduce count over time. - No
as anycasts without a comment explaining why. - Typecheck must pass (
npm run typecheck) before merging. Zero tolerance for type errors.
For any user-triggered action that calls the backend:
- Guard — check preconditions (account exists, thread selected, etc.)
- Loading state — set
sending/loadingto true - API call —
await window.api.someMethod(...) - Check result —
if (result && !result.sent)orif (result.error)— show error to user - Update UI — only on success
- Clear loading — in
finallyblock
Never console.error without also showing UI feedback. API failure ≠ empty state — show an error message.
When doing optimistic updates (star, archive, mark read, etc.):
- Call
markOptimistic()before updating atoms - Update the Jotai atom immediately (optimistic)
- Fire the IPC task (fire-and-forget with
.catch(err => console.error(...))) - The
isOptimisticWindow()guard suppresses delta-triggered reloads for 2s to avoid flickering
- Every renderer→main call goes through
src/preload/index.ts - Every preload method maps to an
ipcMain.handle()insrc/main/ipc-handlers.ts - Keep these two files in sync. If you add a preload method, add the handler. If you add a handler, expose it in preload.
- Main process uses
electron-logfor logging (notconsole.log). Renderer uses the logger module atsrc/renderer/lib/logger.tsfor structured logging.
All database writes go through mailsync via queueTask(). The serializeTask() function in mailsync.ts maps renderer task objects to the format the C++ binary expects.
- Every task type (
ChangeStarredTask,SendDraftTask, etc.) must have acasein theswitchblock - If you add a new task type, add the case AND rebuild (
npx electron-vite build) — stale builds silently drop tasks - Test sends after any change to task serialization
Before committing, verify:
- Scope — Is this commit focused on one thing?
- Typecheck —
npm run typecheckpasses - Lint —
npm run linthas zero errors (warnings are tracked, not blocking) - No console.log — Use
electron-log(main) or the logger module (renderer) - No hardcoded secrets — API keys, tokens, passwords never in source
- No empty
.catch()— Always log errors - No arbitrary Tailwind values — Use standard utilities
- Build works —
npm run buildsucceeds
Plugins use the slot-based registry at src/renderer/plugins/registry.ts:
- Register components for named slots (e.g.,
message-sidebar) - Plugin code lives in
src/renderer/plugins/<name>/ - Backend for plugins uses separate SQLite databases (not
edgehill.db) - Plugins are opt-in via settings toggles
- Follow the same TypeScript, lint, and testing standards as core code
DECISIONS.md— Confirmed architecture and design decisionsPROJECT_PLAN.md— Original phase breakdownFEATURE_INVENTORY.md— 16-client, 84-feature cross-reference