This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Install dependencies
npm install
# Run the app in development mode (uses --dev flag, data stored in Mailspring-dev folder)
npm start
# Run with specific language locale
npm start -- --lang=de
# Run linting (prettier + eslint)
npm run lint
# Run all tests
npm test
# Run window-specific tests
npm test-window
# TypeScript type checking in watch mode
npm run tsc-watch
# Build for production
npm run buildMailspring is an Electron-based email client written in TypeScript with React. It uses a plugin architecture where features are implemented as internal packages.
-
app/src/- Core application source codebrowser/- Main process code (application lifecycle, window management, auto-updates)flux/- Flux-based state management (actions, stores, models, tasks)components/- Reusable React UI componentsservices/- Application services (search, sanitization, etc.)registries/- Extension registries (components, extensions, database objects)global/- Global exports (mailspring-exports,mailspring-component-kit)
-
app/internal_packages/- Built-in plugins implementing features (composer, message-list, thread-list, preferences, themes, etc.)
IMPORTANT: Application source code lives in both
app/src/andapp/internal_packages/. When searching for usages of a module, symbol, or pattern, always search both directories. Searching onlyapp/src/will miss a large portion of the codebase and lead to incomplete changes.
app/spec/- Jasmine test specs
Global exports for plugins:
mailspring-exports- Core APIs: Actions, Stores, Models, Tasks, Utils, database accessmailspring-component-kit- Reusable UI components
Flux Architecture:
- Models (
flux/models/) - Data models: Message, Thread, Contact, Account, Folder, Label, etc. - Stores (
flux/stores/) - Application state: DatabaseStore, DraftStore, AccountStore, etc. - Tasks (
flux/tasks/) - Async operations: SendDraftTask, ChangeFolderTask, etc. - Actions (
flux/actions.ts) - Application-wide action dispatcher
Each plugin in internal_packages/ has:
package.json- Metadata withwindowTypesspecifying where plugin loadslib/main.ts- Entry point withactivate()anddeactivate()lifecycle hookslib/- Plugin source codestyles/- LESS stylesheetskeymaps/- Keyboard shortcut definitions
Important: The UI is read-only with respect to the database. All database modifications happen in the C++ sync engine (Mailspring-Sync). The Electron app requests changes via Tasks, and the sync engine streams entity changes back to create a real-time UI.
The sync engine is a separate C++ process spawned per account:
- Electron → Sync Engine: JSON messages sent via stdin (task requests, commands)
- Sync Engine → Electron: Newline-delimited JSON streamed via stdout (database change deltas)
┌─────────────────┐ stdin (JSON) ┌──────────────────┐
│ Electron UI │ ──────────────────────────────▶│ Mailspring-Sync │
│ (TypeScript) │ │ (C++) │
│ │ ◀────────────────────────────── │ │
└─────────────────┘ stdout (JSON deltas) └──────────────────┘
The MailsyncBridge (in main window only) manages sync process lifecycle, listens to Actions.queueTask, and forwards tasks to the appropriate account's sync process.
Tasks represent operations the user wants to perform (send email, star thread, move to folder). They are persisted models stored in the database.
Task Lifecycle:
- UI calls
Actions.queueTask(new SomeTask({...})) MailsyncBridge._onQueueTask()validates and sends to sync engine via stdin- Sync engine executes the task (local changes + remote API calls)
- Sync engine persists task status updates and emits deltas
- Task completion triggers
onSuccess()oronError()callbacks
Task States (flux/tasks/task.ts):
local- Not yet executedremote- Local phase complete, waiting for remotecomplete- Finished successfullycancelled- Cancelled before completion
Key Task Classes:
SendDraftTask,DestroyDraftTask- Email compositionChangeLabelsTask,ChangeFolderTask- OrganizationChangeStarredTask,ChangeUnreadTask- Status flagsSyncbackMetadataTask- Plugin metadata syncSyncbackEventTask- Calendar event sync
Undoable Tasks:
Tasks can support undo/redo by implementing canBeUndone and createUndoTask(). The UndoRedoStore automatically registers tasks with canBeUndone = true for undo.
Two patterns exist:
- Toggle pattern (
ChangeStarredTask): Undo simply flips a boolean flag - Snapshot pattern (
SyncbackMetadataTask,SyncbackEventTask): Store original state inundoData, swap on undo
// Snapshot pattern example
const undoData = { ics: event.ics, recurrenceStart: event.recurrenceStart };
event.ics = newIcs; // Modify after capturing
Actions.queueTask(SyncbackEventTask.forUpdating({ event, undoData, description: 'Edit event' }));See docs/undo-redo-task-pattern.md for detailed implementation guide.
The TaskQueue store observes Task model changes from the database and provides:
queue()- Active taskscompleted()- Finished taskswaitForPerformLocal(task)- Promise that resolves when task runs locallywaitForPerformRemote(task)- Promise that resolves when task fully completes
Database is read-only in Electron (flux/stores/database-store.ts):
DatabaseStore.inTransaction()throws - writes are not allowed- Uses SQLite in WAL mode via better-sqlite3 for concurrent reads
- The sync engine exclusively handles writes
Change Records (flux/stores/database-change-record.ts):
When the sync engine modifies data, it emits JSON deltas that become DatabaseChangeRecord objects:
{
type: 'persist' | 'unpersist',
objectClass: 'Thread' | 'Message' | ...,
objects: Model[],
objectsRawJSON: object[]
}Reactive Queries (flux/models/query-subscription.ts):
QuerySubscription provides live-updating query results:
// Subscribe to all unread threads
const subscription = new QuerySubscription(
DatabaseStore.findAll(Thread).where({ unread: true })
);
subscription.addCallback((threads) => this.setState({ threads }));
// Subscription automatically updates when DatabaseStore triggersObservable Integration (Rx.Observable.fromQuery):
Wrap queries as RxJS observables for reactive UI updates:
Rx.Observable.fromQuery(DatabaseStore.findAll(Thread))
.subscribe(threads => this.updateUI(threads));ObservableListDataSource (flux/stores/observable-list-data-source.ts):
Adapts QuerySubscription for virtualized list components (MultiselectList), supporting:
- Windowed/paginated data loading
- Selection state management
- Automatic updates from database changes
User Action → Actions.queueTask() → MailsyncBridge → stdin → Sync Engine
│
▼
UI Updates ← QuerySubscription ← DatabaseStore.trigger() ← stdout deltas
- Hot reload is available via
CTRL+R(Windows/Linux) orCMD+R(macOS) - Dev tools accessible via Menu > Developer > Toggle Developer Tools
- In dev tools console,
$mprovides access tomailspring-exportsfor debugging - Dev mode data is stored separately (e.g.,
~/.config/Mailspring-dev/on Linux)
Run linting after modifying TypeScript or JavaScript files.
{
"hooks": {
"after_edit": [
{
"command": "npm run lint",
"file_paths": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]
}
]
}
}