SkimMail
English · Tiếng Việt · 中文
Skim all your inboxes. A lightweight, self-hosted, read-first multi-account email reader — IMAP core + Gmail/Outlook OAuth — packaged as a single Go binary that embeds a React SPA. No external services required.
License: Business Source License 1.1 (proprietary; converts to AGPL in 2030) · Security: SECURITY.md · Docs: Wiki
Everything below is shipped. Multi-account mail, per-account egress, rules and alerts, notifications, backup & restore, plugins, remote access and multi-user roles are all in the current release — AI Triage is the only headline feature still on the roadmap. Release-by-release detail:
CHANGELOG.md.
Features
Read
- Multi-account IMAP — add accounts with host/port + app-password. OAuth for Gmail & Outlook (XOAUTH2 over IMAP) is optional and uses your own Google/Azure app credentials.
- Unified inbox across all accounts, lightweight threading, 3-pane UI with a sandboxed reading pane. Remote images stay blocked until you ask for them — and then SkimMail fetches them, not your browser, through that account's own egress, so the sender's server never sees you.
- Account groups — organize your accounts into named, colour-coded groups and read each group's mail together.
- Real-time new-mail push via IMAP IDLE → WebSocket, with a poll-fallback so mail still arrives if IDLE drops — the backend syncs even when no client is open.
- Search, 2-tier — fast local full-text (SQLite FTS5) + optional server-side fallback to reach mail outside the cached window.
- Read-fast power tools — snooze, pin, Starred across every account, split-inbox bundles, multi-select + bulk actions, move to any folder with undo, VIP / mute senders listed and undoable in Settings, one-click unsubscribe (RFC 8058), and an unsubscribe shortlist that ranks the bulk senders you mostly leave unread — selected on the sender's own List-Unsubscribe header, so account-security mail is never offered.
- Keyboard-first (Superhuman-style) + Focus/triage mode + command palette (⌘K) + PWA (installable, offline shell) + i18n (English / Tiếng Việt / 中文, dark & light themes).
- Server keywords — the flags your other mail clients already set are shown as read-only chips. Off by default: most mailboxes have none, and an empty row of chips is worse than no row.
- Screenshot-safe mode — every displayed address, name, subject, preview line and attachment name is replaced with a realistic stand-in and message bodies are not rendered at all, so you can file a bug or write a post with a real inbox on screen. Off by default, per device.
Give every account its own way out
- Per-account egress. Route each account's mail through its own SOCKS5
proxy, HTTP proxy, embedded WireGuard tunnel, or a serverless
relay you deploy yourself — paste one file into Cloudflare Workers, or run
skimmail relayin a container. Work and personal never have to share an exit IP. Mail clients normally leave this to external plumbing (wireproxy, a per-app VPN, a second container); here it is a field on the account. - Proof, not just configuration. Every connection reports the public IP it actually leaves from — proxy, tunnel, relay and Direct alike — and SkimMail warns you when two accounts meant to be separate arrive from the same address. Test opens a real connection through that egress and reports latency.
- Fail-closed. Switching a connection off never drops its accounts back to a plain one: they go degraded and stop syncing until you pick Direct on purpose — so a disabled egress cannot quietly leak your home IP.
Automate and operate
- Rules & Signals — match incoming mail on sender, subject (text or regex), account or group, or "looks like a one-time code", then notify a channel, toast in-app, send a push, or apply a tag you can click to filter by. One-time codes get an OTP chip that copies the code; the code itself is never stored.
- Notifications, six channels — a plain webhook, Telegram, Slack, Discord, Microsoft Teams or Google Chat, as many as you like, each with its own Send test — plus Web Push to browser and phone (standard VAPID keys SkimMail generates itself; no Firebase account), with quiet hours, content privacy and a per-account toggle.
- Sync health — an account that keeps failing stops on its own instead of hammering your provider; a board shows every account as Stopped / Degraded / Paused with one-click Resume, Retry and Stop, and alerts fire when a sync stops, a proxy or tunnel goes down, or an OAuth sign-in is about to expire.
- Backup & restore — an encrypted snapshot of the whole instance (accounts, mail and settings) that restores onto any SkimMail server, even one on a different database — plus scheduled backups to a local folder or any S3-compatible storage (MinIO, Cloudflare R2, Backblaze B2, Wasabi, AWS S3).
- Remote access without port-forwarding — publish SkimMail through a Cloudflare Tunnel or Tailscale, with a QR code to open it on your phone. Those helpers and the WireGuard engine are plugins downloaded on demand against a signed, SHA256-pinned index; install, update and remove them from Settings ▸ Plugins.
- How far back to sync, and what to keep offline — a sync depth per account (30 days by default, or full history), set instance-wide in Settings ▸ Sync and overridable per mailbox. Message bodies you open are cached locally so re-reading costs nothing, with background prefetch you can switch off if you would rather it never read ahead.
- Where your mail is kept, and for how long — cached message bodies live in a blob store you choose: the local filesystem, or any S3-compatible bucket, switchable from Settings ▸ Storage with a real connection test and a migration that resumes if it is interrupted. The retention side sits in Settings ▸ Security: current usage, a size ceiling, how many days to keep a body, and a manual purge. SQLite is the default database, and PostgreSQL and MySQL are compiled into every released build — no rebuild needed.
- Configured in the browser, not a text editor — public URL, OAuth apps and sign-in mode are settings inside the app, and anything set in the environment still wins and shows as locked, naming the variable to remove. In-app self-update installs a new release in one click on binary/apt installs, and Settings ▸ Logs gives a live log view, runtime log level and rotation.
Security & multi-user
- Multi-user with real separation —
AUTH_MODE=usersgives each person their own accounts, mail and settings, invisible to everyone else, as owner, operator or viewer; create them withskimmail user add. A single-user instance stays exactly as simple as it was. - Security layer — a one-time claim code gates the very first sign-in; credentials, OAuth tokens and cached message bodies are encrypted at rest (AES-256-GCM); plus login rate-limit + lockout, 2FA, a server-enforced idle lock that draws over every open tab rather than only the one you touched, CSRF/origin checks, an SSRF guard, signed releases and a signed plugin index, and an append-only audit log.
- Yours, and it stays that way — one Go binary with the React SPA inside, SQLite built in (PostgreSQL and MySQL optional; MySQL is experimental), always-on background sync, and no phone-home. Community Edition is free; Sponsor and Pro keys are verified entirely offline, and a key expiring never takes away anything you already added.
Requirements
Just Docker — SQLite is built in, no external services needed.
(Building from source or contributing? See README.dev.md.)
Quick start
No source needed — grab the published compose file (it pulls the prebuilt image):
curl -fsSLO https://lyquyduong.github.io/skimmail/docker-compose.yml
docker compose up -d # → http://localhost:8080 (SQLite + filesystem)
Or run the published image directly (distroless + non-root; data in the /data volume):
docker run -d --name skimmail -p 8080:8080 -v skimmail-data:/data \
-e AUTH_MODE=passphrase ghcr.io/lyquyduong/skimmail:latest
One image, one package. Embedded WireGuard egress, PostgreSQL, MySQL and S3
support are all in the default build — the WireGuard engine arrives as a
SHA256-pinned plugin SkimMail downloads the first time you add a tunnel, so there
is nothing extra to install and no second image to choose. The separate
skimmail-wg edition was retired in 1.7.0; if you still have that apt package,
apt remove skimmail-wg is safe.
Install on Ubuntu / Debian (apt)
curl -fsSL https://lyquyduong.github.io/skimmail/apt/skimmail.gpg \
| sudo tee /usr/share/keyrings/skimmail.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/skimmail.gpg] https://lyquyduong.github.io/skimmail/apt stable main" \
| sudo tee /etc/apt/sources.list.d/skimmail.list
sudo apt update && sudo apt install skimmail # runs as a systemd service → http://localhost:8080
Config lives in /etc/default/skimmail; data in /var/lib/skimmail; logs go to
the systemd journal (sudo journalctl -u skimmail -f) and a rotating file at
/var/log/skimmail/skimmail.log — level, rotation, truncate, and a realtime
viewer live in Settings ▸ Logs. Upgrades come via apt upgrade. All
release channels (image, .deb, apt repo) ship from GitHub — see
DISTRIBUTION.md.
First login
Open http://localhost:8080. Before it lets you create anything, SkimMail asks for a one-time claim code it printed at first boot — it is what stops whoever reaches the port first from claiming your instance:
sudo journalctl -u skimmail | grep 'claim code' # apt (sudo: the journal is root-only)
docker compose logs skimmail | grep 'claim code' # docker compose (service name; the container is skimmail-skimmail-1)
docker logs skimmail 2>&1 | grep 'claim code' # docker run (only if you passed --name skimmail, as above)
Lost it? Mint a replacement — the old one stops working:
sudo -u skimmail skimmail --data-dir /var/lib/skimmail claim-code --rotate
On a closed network you can skip the gate entirely with SKIMMAIL_SKIP_CLAIM=1.
Then the default auth mode is passphrase: the first password you enter becomes
your master passphrase — remember it (it's used to log in again). Leave the
username blank.
Add a mail account
Add an IMAP account: email · host (e.g. imap.gmail.com) · port 993 ·
security TLS · app password.
Gmail takes an app-specific password (not your normal password) — two clicks, no Cloud project. Outlook.com and Hotmail cannot: Microsoft issues no app passwords for personal accounts and has switched basic authentication off, so those mailboxes need OAuth.
To enable the "Add with Google/Microsoft" buttons, set the OAuth credentials below. A public
BASE_URLis not required — an instance without one finishes the sign-in by pasting the address the browser lands on.
Press ↻ Sync (or wait for the background poll). New mail then pushes in real-time over WebSocket.
Usage
- Search — type in the search box (Enter). With a single account selected, the "Search server ↗" button also queries the mail server and caches the hits.
- Groups — create account groups in Settings → Groups; pick a group to read just those accounts' mail.
- Bundles — filter the list by Primary / Newsletter / Transactional / Notifications. 💤 Snoozed shows snoozed mail.
- Multi-select — tick rows for bulk read / archive / delete.
- Reading pane — 🗄 Archive · 🗑 Delete (with Undo) · 💤 Snooze · 📌 Pin · ✉️ Unsubscribe · ★ VIP · 🔇 Mute.
- Keyboard —
?shows all shortcuts:j/kmove ·Enter/oopen ·earchive ·#delete ·ssnooze ·xselect ·Spacequick-peek ·/search ·fFocus mode ("Get to Zero") ·Escclose. - Tags — tag mail yourself from the message list or the reading pane, or let
a rule do it; click a tag chip to filter, or type
tag:namein the search box. Tags come off again as easily as they go on, and a new rule can be applied to mail you already have after showing you how many messages it would touch. - Images — a blocked-image banner offers Show images, plus Always or Never from this sender (both reversible; the instance-wide switch is in Settings ▸ Security).
- PWA — install from the browser address bar; the app shell works offline.
- Language / theme — switch EN / VI / 中 and dark/light in the sidebar.
Configuration
Config is via environment variables (12-factor) — see .env.example.
Since 1.9.0 most settings are also editable in the app (Settings), and a value
set in the environment always wins and shows there as locked. The full reference —
every variable, the config planes, and what overrides what — is in the
wiki ▸ Configuration.
| Variable | Default | Purpose |
|---|---|---|
AUTH_MODE |
passphrase |
passphrase (one shared passphrase) or users (named accounts with roles). Since 1.10.0 users is really multi-user: each account owns its own mailboxes and cannot see anyone else's, and each user is owner, operator or viewer. Create them with skimmail user add. A third value, none, turns authentication off entirely; it additionally requires SKIMMAIL_ALLOW_AUTH_NONE=1 in the process environment, because auth.mode is writable from the UI and the CLI and a stolen session must not be able to downgrade the instance on its own. |
DATA_DIR |
./data |
SQLite DB, master key, blobs |
LISTEN_ADDR |
:8080 |
listen address |
BASE_URL |
– | public URL — needed for the OAuth redirect flow, WebSocket and PWA. OAuth still works without it via the paste flow. |
TRUST_PROXY |
false |
trust X-Forwarded-* behind a reverse proxy |
TRUSTED_PROXIES |
– | comma-separated CIDRs trusted as proxy hops when TRUST_PROXY=true (default: loopback/private only) |
TRUSTED_ORIGINS |
– | extra CORS/CSRF-allowed origins (comma-separated) |
BACKGROUND_POLL_INTERVAL |
5m |
poll-fallback period for every account |
MAX_CONCURRENT_SYNCS |
4 |
max mailboxes syncing at once |
SYNC_RATE_PER_MIN |
10 |
max sync starts per minute (stagger) |
SYNC_DEPTH_DAYS |
30 |
header sync window (0 = full history) |
GOOGLE_CLIENT_ID / _SECRET |
– | Gmail OAuth app (optional) |
MICROSOFT_CLIENT_ID / _SECRET |
– | Outlook OAuth app (optional) |
Security
Credentials, OAuth tokens and cached message bodies are encrypted at rest
(AES-256-GCM). The first sign-in is gated by a one-time claim code. The server
applies login rate-limiting + lockout, CSRF/origin checks, an SSRF guard on
outbound fetches, strict security headers + a sandboxed email iframe, a
server-enforced idle lock, and an append-only audit log (GET /api/audit). See
SECURITY.md for the full posture and how to report a
vulnerability.
Uninstall (complete removal)
Uninstalling never deletes your data by itself — accounts, the mail cache, and the
encryption master key live in the Docker volume or /var/lib/skimmail. The
steps marked ⚠ below delete them permanently (there is no way back without a
backup).
Docker Compose
docker compose down -v # ⚠ stops + removes containers AND the data volumes
docker rmi ghcr.io/lyquyduong/skimmail:latest 2>/dev/null
rm docker-compose.yml # the file you downloaded
Docker (docker run)
docker rm -f skimmail
docker volume rm skimmail-data # ⚠ all data
docker rmi ghcr.io/lyquyduong/skimmail:latest
APT (Ubuntu / Debian)
sudo apt purge skimmail # stops the service, removes the binary, the
# systemd unit and /etc/default/skimmail
sudo rm -rf /var/lib/skimmail # ⚠ data — apt keeps it on purpose
sudo rm -rf /var/log/skimmail # log file + rotated backups
sudo userdel skimmail # the system user created at install
# remove the APT repo + signing key you added:
sudo rm -f /etc/apt/sources.list.d/skimmail.list /usr/share/keyrings/skimmail.gpg
sudo apt update
Manual binary (tarball) — stop the process (or your own systemd unit), then
delete the binary and its data dir (DATA_DIR, default ./data next to the binary).
Developers
Building from source, dev mode (hot reload), VS Code setup, build tags, tests, and
project layout are in README.dev.md.
License & Tiers
SkimMail is proprietary software. It is licensed under the Business Source License 1.1 (Licensor: Solutions101 (S101)). The source code is private; only compiled binaries are distributed to users. SPDX: BUSL-1.1.
Three Tiers
- Community Edition — Free, 10 accounts / 1 sign-in / 3 proxies / 2 groups / 5 GB storage (no license key needed)
- Sponsor Edition — Thank-you for GitHub Sponsors / backers (25 accounts / 3 sign-ins / 5 proxies / 5 groups / 15 GB)
- Pro Edition — Paid license key; unlimited accounts/proxies/groups/storage + future AI Triage service
See COMMERCIAL-TERMS.md for details, or the full explainer (EN + VI) in docs/LICENSING.md. Questions about licensing: see SECURITY.md.
License Conversion (2030)
After July 23, 2030, SkimMail will automatically convert to AGPL-3.0-or-later. Until then, source code remains proprietary.
Other Notices
- Trademark: The "SkimMail" name is not covered by the source license (see
TRADEMARK.md) - Third-party dependencies: Licenses listed in
NOTICE - Privacy: No phone-home; optional Pro/Sponsor data collection (see
PRIVACY.md) - Contributing: Requires CLA
- License key operations: See
LICENSE-SIGNING.md(maintainers only)