A cross-platform (macOS + Windows) desktop email client with an Apple Mail style UI, an Outlook style calendar tab, and a Google Tasks style tasks tab. Themed to match the OS, designed for ADHD-friendly use, with pluggable mail/calendar/task providers and optional cloud or local AI.
- Electron + Vite + React + TypeScript (pnpm workspace)
better-sqlite3for local message/event/task cacheelectron-builderfor packaging (DMG on macOS, NSIS on Windows)- Providers: Gmail (googleapis), Microsoft (Graph + MSAL), Apple (CalDAV + IMAP), generic IMAP/SMTP (ImapFlow + nodemailer), POP3 (node-poplib)
- AI: OpenAI/Anthropic-compatible cloud OR local Ollama (settings-driven)
apps/
main/ Electron main process (window, IPC, scheduler, secure storage, notifications)
renderer/ React UI: Mail / Calendar / Tasks / Settings tabs
packages/
core/ Domain models (Account, Folder, Message, Event, Task), focus + snooze logic
providers/ Adapters: gmail, microsoft, apple-caldav, imap-smtp, pop3
ai/ LLM client abstraction (cloud or local Ollama)
ui-kit/ Themed React primitives, ADHD defaults
storage/ SQLite + electron-store + safeStorage wrappers
New to the codebase or just reviewing it? This section takes you from a fresh clone to a running app with no prior knowledge assumed.
| Tool | Version | Notes |
|---|---|---|
| Node.js | 22 or 24 |
engines requires >=20, but use an even LTS (22 or 24) - see note below. |
| pnpm | 9 |
This repo is a pnpm workspace. Install via Corepack (below) - don't use npm/yarn. |
| Git | any recent | To clone the repo. |
| Platform build tools | only if compiling | Native modules (better-sqlite3) - needed only when no prebuilt binary matches your Node. |
Use Node 22 or 24.
better-sqlite3(andbetter-sqlite3-multiple-ciphers) only ship prebuilt binaries for specific Node ABIs - currently Node 22 (ABI 127) and 24 (ABI 137), not Node 18 or 20. On Node 22/24pnpm installjust downloads the prebuilt binary, so you need no compiler and no Python. On Node 18/20 there is no prebuilt binary, so the install falls back to compiling from source (node-gyp), which is the usual cause of theNo prebuilt binaries found/Could not find any Pythoninstall failure.
Platform build tools are needed only if you must compile from source (i.e. you're on a Node version without a prebuilt binary, or an unusual platform):
- macOS: Xcode Command Line Tools (
xcode-select --install). - Windows: "Desktop development with C++" workload (Visual Studio Build Tools).
- All: a working
node-gyptoolchain. Python 3.11 is the safest choice - Python 3.12+ droppeddistutils, which the native rebuild chain still expects.
git clone <repo-url> && cd GingerMail2
corepack enable # makes the pinned pnpm@9 available
pnpm install
pnpm rebuild-native # REQUIRED: rebuild better-sqlite3 against Electron's Node ABI
pnpm dev # launches the desktop window with hot reloadWhy
pnpm rebuild-native?better-sqlite3ships prebuilt for plain Node, but Electron bundles its own Node ABI. Skipping this step is the #1 first-run failure - the app crashes on launch with a native-module version mismatch. If you ever hit that error, just rerun it.
You do not need any OAuth/provider credentials to boot the app and review the
UI - pnpm dev runs fine without them. Credentials are only needed to actually
connect a live mail/calendar/task account (see docs/PACKAGING.md).
These run without any credentials, so they're the quickest way to confirm a healthy setup:
pnpm typecheck # TypeScript project-reference build, no emit
pnpm lint # ESLint (zero warnings allowed)
pnpm test # unit + component tests across all packagesSee the Layout tree above for the full map. The key entry points:
apps/main/entry.cjs+apps/main/src/- Electron main process (windows, IPC, scheduler, secure storage).apps/renderer/src/- the React UI (Mail / Calendar / Tasks / Settings tabs).packages/core/- domain models (Account, Folder, Message, Event, Task) and focus/snooze logic.packages/providers/- the Gmail / Microsoft / Apple / IMAP / POP3 adapters.
Helpful docs: docs/ROADMAP.md (what's shipped vs. planned),
docs/QA.md (testing + UI/UX notes), docs/PACKAGING.md
(builds, signing, OAuth setup), and docs/compliance/ (security program).
| Symptom | Fix |
|---|---|
pnpm install fails at better-sqlite3 with No prebuilt binaries found (target=18.x …) then gyp ERR! find Python / Could not find any Python |
You're on Node 18/20, which has no prebuilt binary, so it tries (and fails) to compile. Switch to Node 22 or 24 (nvm install 22 && nvm use 22, or corepack-aware setup) and rerun pnpm install - it then downloads the prebuilt binary, no Python needed. (Alternatively, stay on your Node version and install Python 3 + Xcode CLT so it can compile from source.) |
Unsupported engine: wanted {"node":">=20"} |
Your Node is too old; install Node 22 or 24 (see above). |
better-sqlite3 / "NODE_MODULE_VERSION" mismatch |
Rerun pnpm rebuild-native. |
pnpm not found or wrong version |
corepack enable && corepack prepare pnpm@9.0.0 --activate. |
node-gyp / ModuleNotFoundError: distutils (macOS) |
Use Python 3.11 (brew install python@3.11). |
pnpm install
pnpm rebuild-native # native module rebuild against Electron's Node ABIAfter pnpm install (and pnpm rebuild-native), launch GingerMail one of two ways:
Development (hot reload): starts the Vite renderer and the Electron main process together and opens the desktop window with live reload on both.
pnpm devPackaged build: build a native installer for your current OS, then launch
the installed app. Output lands in release/<version>/. Local builds are
unsigned by default (they launch with a Gatekeeper/SmartScreen warning); the
release pipeline signs and notarizes automatically when the signing secrets are
present (see Releasing).
pnpm dist:mac:no-ollama # DMG (macOS host) -> open the .dmg, drag to /Applications, launch
pnpm dist:win:no-ollama # NSIS (Windows host) -> run the .exe installer, launch from Start menu
pnpm dist:linux:no-ollama # AppImage (Linux host) -> chmod +x the .AppImage, then run itThe
:no-ollamavariants skip the bundled-Ollama fetch and fall back to a user-installed Ollama. For a guaranteed-unsigned local build (never hunts for a keychain identity), use the:devvariants (pnpm dist:mac:dev). Drop the suffix (e.g.pnpm dist:mac) once certs are in place for a signed, Ollama-bundled build. Seedocs/PACKAGING.mdfor signing, cross-platform release builds, and the full matrix.
The native installers (.dmg / .exe / .AppImage) are the primary way to run
GingerMail. As a convenience, the project also ships a Docker image that runs the
Linux build on a minimal KasmVNC
desktop, so you can reach the full app from a browser without installing anything
natively.
Option A - download from a GitHub Release (no build toolchain needed):
# Download gingermail-<version>-docker.tar.gz from the Release assets, then:
docker load < gingermail-<version>-docker.tar.gzOption B - build it yourself from the repo root:
docker build -t gingermail:dev .The build is multi-stage: it compiles the renderer + main + packages, runs
electron-builder --linux dir, and copies the unpacked app onto the KasmVNC
base image. See Dockerfile.
docker run -d --name gingermail \
-p 3001:3001 \
--shm-size=1g \
-v gingermail-data:/config \
gingermail:<version> # or gingermail:dev if you built locallyThen open https://localhost:3001 (self-signed cert, so accept the browser warning on first load).
| Flag | Why |
|---|---|
-p 3001:3001 |
KasmVNC web UI (HTTPS). Map to another host port if 3001 is taken. |
--shm-size=1g |
Gives Chromium enough shared memory; the app may crash without it. |
-v gingermail-data:/config |
Persists accounts, cache, and settings across restarts. |
docker logs -f gingermail # follow startup / app logs
docker stop gingermail # stop
docker start gingermail # start again (data persists in the volume)
docker rm -f gingermail # remove the container (volume is kept)To upgrade, docker rm -f gingermail, load/build the new image, and docker run
again with the same -v gingermail-data:/config volume.
The container runs Electron with
--no-sandboxbecause containers lack the user-namespace sandbox, so treat Docker as a convenience channel rather than the hardened native build. Full build details are indocs/PACKAGING.md.
Releases are produced by the Release GitHub Actions workflow
(.github/workflows/release.yml), not by hand.
Building all three platforms reliably from one machine isn't possible (Windows
NSIS needs Wine, Linux AppImage needs a Linux toolchain), so CI builds each OS on
its own native runner.
flowchart LR
tag["Push tag vX.Y.Z"] --> mode["mode job: channel=prod, signed, publish"]
mode --> mac["macOS runner: DMG (arm64)"]
mode --> win["Windows runner: NSIS (x64+arm64)"]
mode --> linux["Linux runner: AppImage"]
mode --> docker["Docker image: ghcr.io + tarball"]
mac --> draft["Draft GitHub Release (signed assets)"]
win --> draft
linux --> draft
docker --> draft
draft --> review["Review + verify signatures"]
review --> publish["Publish release"]
# 1. Bump "version" in package.json, commit it.
# 2. Tag and push:
git tag v1.0.5
git push origin v1.0.5A tag push runs the prod channel: it builds the macOS DMG (arm64), Windows
NSIS (x64 + arm64), Linux AppImage, and the Docker image; signs/notarizes each
when the corresponding secrets exist (graceful unsigned fallback otherwise); and
creates a draft GitHub Release with all assets attached
(.dmg / .exe / .AppImage, *.sig, SHA256SUMS, latest*.yml,
.blockmap, and the Docker tarball). You then verify the assets and publish the
draft. The build matrix is fail-fast: false, so one OS leg failing doesn't
block the others.
From the Actions tab, run the workflow manually (workflow_dispatch) with
channel = dev to get unsigned installers for all three OSes - macOS
notarization is forced off and keychain discovery disabled, so no leg can fail
for missing credentials. Tick publish_release to also cut a draft pre-release
named … (dev / unsigned).
See docs/PACKAGING.md for the required signing secrets,
how to provision Apple/Windows/GPG credentials, signature-verification commands,
and the bundled-Ollama toggle.
Every pull request and push to main runs the CI workflow
(.github/workflows/ci.yml):
pnpm lint- ESLint (zero warnings allowed)pnpm typecheck-tsc -bacross the workspacepnpm test- unit + component testspnpm audit:prod- fails on high/critical vulnerabilities in production deps- A CycloneDX SBOM (
pnpm sbom) is generated and uploaded as an artifact onmain.
Installed apps check for updates on launch via electron-updater
(apps/main/src/autoUpdater.ts). The feed is the
generic provider configured in
electron-builder.yml
(https://updates.gingermail.app/${channel}/${os}); the latest*.yml manifests
are SHA512-signed and verified before any bytes hit disk, so a hostile mirror
can't substitute or downgrade an installer. The runtime auto-updater ships behind
a safety toggle (updates.optIn) that is off by default and is a no-op until
the publish endpoint exists.
- Low-stimulation palette and generous spacing by default
- Universal snooze on every email / event / task
- Focus Mode (Cmd/Ctrl+Shift+F) - dims everything but the active item, suppresses notifications, optional Pomodoro break nudge
- Optional dyslexic-friendly font, adjustable density and font size
- Notifications batched into a single digest by default (not per email)
Phased build - see docs/ROADMAP.md for what's shipped and what's next.
- Security policy:
SECURITY.md; hardening summary:docs/security-hardening.md. - NIST 800-53 / 800-171 / CSF 2.0 / FedRAMP-readiness program (scope, control
crosswalk, threat model, SSP, POA&M):
docs/compliance/.
GingerMail is source-available, licensed under the PolyForm Noncommercial License 1.0.0.
- Noncommercial use is free - personal, hobby, research, educational, and use by nonprofit/government organizations is permitted at no cost.
- Commercial use requires a separate license. If you want to use GingerMail for a commercial purpose, contact the author to arrange a commercial license.
Copyright (c) 2026 William Skidmore. All rights not expressly granted by the PolyForm Noncommercial License are reserved.