Courrier is a desktop mail client for Microsoft Outlook and Gmail accounts. It is a pnpm/Turborepo monorepo with an Electron desktop app and a Fastify relay for provider update notifications.
The app signs in with Microsoft or Google OAuth, reads folders and messages through provider APIs, and provides a focused three-pane mail interface with separate compose windows for browsing, searching, and triaging messages.
- Microsoft sign-in with MSAL and Google sign-in with installed-app OAuth
- Multi-account session handling for Outlook and Gmail accounts
- Outlook folder navigation and Gmail label navigation, including nested folders
- Message list pagination and provider search
- HTML and plain-text message rendering with sanitized HTML content
- Read/unread, move, drag-to-folder, compose, reply, and move-to-trash actions
- Rich-text compose with recipient autocomplete and local file attachments
- Attachment download/open support for provider-hosted attachments
- Optional live mailbox invalidation through Microsoft Graph webhooks and Gmail Pub/Sub push notifications
- Hardened Electron bridge with context isolation and trusted IPC checks
- Responsive layout for narrower desktop windows
- Desktop shell: Electron Forge
- Build tooling: Vite and TypeScript
- UI: React, Tailwind CSS, shadcn/Base UI primitives, lucide-react icons
- Editor: Tiptap
- Data: TanStack Query and TanStack Router
- Auth and mail: MSAL Node, MSAL Node Extensions, Microsoft Graph, Gmail API
- Relay: Fastify, WebSocket, and Zod
- Testing: Vitest and Testing Library
- Node.js and pnpm 11
- A Microsoft account with Outlook mail access or a Google account with Gmail
- A Microsoft Entra app registration or Google OAuth desktop client
Install dependencies:
pnpm installCreate a local environment file:
Copy-Item apps/desktop/.env.example apps/desktop/.envEdit apps/desktop/.env and set at least one provider client ID:
MICROSOFT_CLIENT_ID=<Application client ID>
GOOGLE_CLIENT_ID=<Google OAuth desktop client ID>
GOOGLE_CLIENT_SECRET=<optional Google OAuth desktop client secret>
GOOGLE_PUBSUB_TOPIC=projects/<project>/topics/<topic>The relay variables are optional for basic local desktop use. Add them only when you are running live update relay integration.
Start the app in development mode:
pnpm startElectron Forge starts the Electron main process, preload script, and Vite renderer. In development, Chromium DevTools open automatically.
Courrier is a public desktop client, so it uses Microsoft OAuth without a client secret. Create a Microsoft Entra app registration with a native redirect URI of:
http://localhost
The app uses delegated Microsoft Graph access for the signed-in user. Mail actions such as marking messages read, moving messages, and moving messages to trash require mailbox write permission. Sending new messages and replies requires mail send permission.
For the full Microsoft setup flow, see docs/oauth.md. For the Google setup flow, see docs/google-oauth.md.
For Gmail, create a Google OAuth desktop client and enable the Gmail API and
People API. Gmail live updates use Gmail watch with a Cloud Pub/Sub topic.
Configure the Pub/Sub push subscription to deliver to:
https://your-relay.example.com/google/pubsub
If you set GOOGLE_PUBSUB_VERIFICATION_TOKEN on the relay, include it as a
token query parameter in the push endpoint URL.
| Command | Description |
|---|---|
pnpm start |
Run Courrier desktop in development mode. |
pnpm dev |
Run workspace development tasks. |
pnpm build |
Build workspace packages and apps. |
pnpm package |
Package the Electron app locally. |
pnpm make |
Create distributable installers/packages. |
pnpm test |
Run workspace Vitest suites. |
pnpm test:coverage |
Run the desktop Vitest suite with coverage. |
pnpm typecheck |
Run workspace TypeScript checks. |
pnpm lint |
Run workspace ESLint checks. |
apps/
desktop/ Electron desktop app
relay/ Fastify provider webhook and WebSocket relay
packages/
mail-contracts/ Zod schemas and shared relay/desktop event types
tsconfig/ Shared TypeScript base config
docs/
oauth.md Microsoft OAuth registration and troubleshooting guide
google-oauth.md Google OAuth, Gmail API, and Pub/Sub setup guide
Microsoft Graph change notifications and Gmail Pub/Sub push notifications require
a public HTTPS webhook endpoint. The desktop app keeps provider tokens locally
and creates provider subscriptions, while apps/relay receives webhook POSTs and
pushes compact invalidation events to the desktop app over WebSocket.
The relay exposes:
GET /healthfor health checksPOST /graph/notificationsfor Microsoft Graph subscription validation and notificationsPOST /google/pubsubfor Gmail Pub/Sub push notifications- a WebSocket endpoint used by the desktop app for registration and event delivery
The current relay is intended for a self-hosted, single-user deployment. It uses
an in-memory store with bounded event retention, so registrations and pending
events are lost on process restart and are not shared across multiple relay
instances. Add a durable RelayStore before running it as a production
multi-instance service.
Relay environment variables:
RELAY_PUBLIC_URL=https://your-relay.example.com
RELAY_ADMIN_TOKEN=<shared relay admin token, at least 24 chars>
GOOGLE_PUBSUB_VERIFICATION_TOKEN=<optional shared push endpoint token>
PORT=3001
HOST=0.0.0.0apps/relay reads these values from the host process environment.
apps/relay/.env.example is a template unless your deployment runner loads it.
Desktop relay environment variables:
RELAY_PUBLIC_URL=https://your-relay.example.com
RELAY_ADMIN_TOKEN=<same shared relay admin token>Because Courrier is a public desktop client, do not use the shared relay admin token for a public multi-user relay. Treat it as a self-hosted deployment secret.
Courrier keeps Electron renderer privileges narrow:
contextIsolationis enabled andnodeIntegrationis disabled.- The preload script exposes only the typed
window.courrierAPI. - Main-process IPC handlers reject messages from untrusted pages.
- Renderer windows trust only the packaged app file or the configured Vite development origin, not arbitrary localhost pages.
- External navigation opens in the system browser instead of inside the app.
- Remote resources in HTML mail are stripped by default before iframe rendering.
- Microsoft tokens are cached with MSAL Node Extensions where platform support is available.
- Plaintext token cache fallback is disabled by default. Set
COURRIER_ALLOW_PLAINTEXT_TOKEN_CACHE=trueonly if you accept storing tokens without OS encryption on the current machine.