OpenMail
A self-hosted, open-source mail server suite in a single Node.js process — full-stack email with SMTP / IMAP / POP3, a modern three-pane webmail, an admin dashboard, multi-tenancy, DKIM, anti-spam and 2FA. Zero native dependencies, one SQLite file, up and running in minutes.
Why OpenMail
Most self-hosted mail stacks glue together a dozen daemons (Postfix, Dovecot, Rspamd, Redis, MySQL…). OpenMail is a deliberately simple, single-binary-style mail platform written entirely in JavaScript:
- One process runs SMTP (MX + Submission + SMTPS), IMAP, IMAPS, POP3, POP3S, the HTTP API and the webmail UI.
- Zero native modules —
node:sqlite(built into Node.js 22+), pure-JS crypto. Installs in seconds on Windows, Linux, macOS, ARM64. - Real protocols — Thunderbird / Apple Mail / Outlook can connect via IMAP/SMTP out of the box.
- Sane defaults — DKIM keys auto-generated per domain, DNS records rendered in the admin UI, spam scoring built in.
Feature matrix
References / In-Reply-To / normalized subject |
| Hygiene | One-click archive of read inbox mail, empty trash / junk |
| AI assistant | Bring your own OpenAI-compatible API (Base URL + model + key, AES-encrypted server-side): AI draft writing with tone control, translate emails to 12 languages, analyze mail (summary / key points / action items / reply hint) |
| Email templates | Save recurring mail as templates, import HTML templates (.html file or paste) or generate them with AI; one-click insert in the composer |
Contacts
Add / edit / delete, groups, search, vCard & CSV import/export, recipient autocomplete from contacts + directory, compose from contact.Accounts & security
| Feature | Details | | --- | --- | | Auth | Email + password (bcrypt), optional open registration with admin code | | 2FA | TOTP (RFC 6238) with QR code enrollment — Google Authenticator compatible | | Sessions | Multi-device sessions list, per-session and bulk revocation, configurable TTL | | RBAC |admin / user / temp roles enforced server-side on every route |
| Audit log | Logins, failed logins, sends, admin actions, deliveries — all recorded with IP |
Domains & multi-tenancy
Multiple domains per instance · per-mailbox storage quotas · aliases with forwarding · Catch-all per domain · automatic 2048-bit DKIM key generation with copy-paste DNS records.Admin dashboard
Live stats & 14-day send/receive chart · user CRUD / ban / quota / password reset · domain & DKIM management · alias manager · outbound queue inspector · global blacklist · audit log browser · SMTP test · one-click SQLite backup.Anti-spam & authentication
- Inbound authentication via SPF, DKIM and DMARC verification (
mailauth) - Rule-based spam scoring (auth failures, keywords, HELO checks…) → auto junk folder, optional SMTP-level rejection
- Outbound mail is DKIM-signed automatically using each domain's key
- Per-user and global sender blacklists
- Rate limiting per IP on all SMTP ports
Protocols
| Protocol | Dev port | Production port | Purpose | | --- | --- | --- | --- | | SMTP (MX) | 2525 | 25 | Server-to-server delivery | | Submission | 2587 | 587 | User sending with SASL auth (STARTTLS) | | SMTPS | 2546 | 465 | Implicit TLS sending | | IMAP | 1143 | 143 | Mail pull | | IMAPS | 1993 | 993 | Implicit TLS mail pull | | POP3 | 1110 | 110 | Mail download | | POP3S | 1995 | 995 | Implicit TLS download | | HTTP | 3000 | reverse-proxy 443 | Webmail / admin / REST API |The IMAP/POP3 servers are intentionally minimal (LOGIN, LIST, SELECT, FETCH, STORE, SEARCH, APPEND, IDLE, MOVE…) — enough for everyday client use, not a full RFC 3501 implementation.
Installation
OpenMail ships four install paths — pick one:
| Method | Command / Entry | Best for |
| --- | --- | --- |
| One-click script | curl -fsSL https://raw.githubusercontent.com/chenyuwebwawa/OpenMail/main/install.sh \| bash | Fresh Linux VPS (installs Node 22, source, .env, systemd, language packs) |
| Docker | docker compose up -d --build (source) · ghcr.io/chenyuwebwawa/openmail:latest / chenyutech/openmail:latest (prebuilt) | Any server with Docker / 1Panel |
| Manual server | git clone && npm install && npm start | Development, Windows (see start-windows.bat), custom setups |
| Hosting panels | BT Panel (宝塔) guide · 1Panel guide | Existing panel-managed servers |
1. One-click (Linux server)
# Interactive: prompts for domain, generates .env, registers a systemd service,
installs all 10 UI language packs and prints the admin password.
curl -fsSL https://raw.githubusercontent.com/chenyuwebwawa/OpenMail/main/install.sh | bash
Behind a network that blocks raw.githubusercontent (e.g. mainland China)? Any of:
curl -fsSL https://cdn.jsdelivr.net/gh/chenyuwebwawa/OpenMail@main/install.sh | bash
git clone https://github.com/chenyuwebwawa/OpenMail.git /opt/openmail && cd /opt/openmail && bash install.sh
Non-interactive with options:
bash install.sh --domain example.com --standard-ports --dir /opt/openmail
Updating later — one command on the server (backs up data, snapshots runtime files, pulls the latest code, installs deps, restarts):
cd /opt/openmail && bash scripts/update.sh # add --check to preview new commits
Flags: --domain (mail domain), --standard-ports (use 25/587/… instead of dev ports), --port (HTTP), --dir (install dir). The server runs as a non-root user via systemd (systemctl status openmail).
2. Docker
OpenMail runs as a single container — SQLite and attachments live in bind mounts, no sidecars. Two ways to get the image:
Option A — build from source (recommended, always works):
git clone https://github.com/chenyuwebwawa/OpenMail.git && cd OpenMail
cp .env.example .env # set domain / base URL / relay / TLS
docker compose up -d --build
docker-compose.yml reads every OM_* value from your .env (via ${VAR:-default}) instead of hardcoding it; the database, the auto-generated secret.key and admin-credentials.txt persist in ./data, attachments in ./files. The admin password is printed on first launch (also written to data/admin-credentials.txt).
Option B — prebuilt image (published automatically to GHCR and Docker Hub):
The Docker Image workflow builds multi-arch images (linux/amd64, linux/arm64) on every push to main, on v* tags and on manual dispatch, and publishes the same tags to both registries:
- GHCR —
ghcr.io/chenyuwebwawa/openmail:latest(uses the built-inGITHUB_TOKEN, no secrets required) - Docker Hub —
chenyutech/openmail:latest(public, anonymous pull works; pushed via the maintainer'sDOCKERHUB_USERNAME/DOCKERHUB_TOKENrepo secrets)
# if the package is private, log in first with a PAT that has read:packages:
docker login ghcr.io -u <github-user>
docker pull ghcr.io/chenyuwebwawa/openmail:latest
or from Docker Hub (public, anonymous pull works):
docker pull chenyutech/openmail:latest
docker run -d --name openmail --restart unless-stopped \
-p 3000:3000 -p 25:25 -p 587:587 -p 465:465 \
-p 143:143 -p 993:993 -p 110:110 -p 995:995 \
-v openmail-data:/app/data -v openmail-files:/app/files \
-e OM_PRIMARY_DOMAIN=example.com -e OM_BASE_URL=https://mail.example.com \
ghcr.io/chenyuwebwawa/openmail:latest # or: chenyutech/openmail:latest
OM_SECRETis optional: leave it unset and the first boot generates a key, persisted to/app/data/secret.keyinside the data volume (so recreating the container keeps saved AI keys working). If you set it explicitly, keep it fixed — changing it invalidates every saved AI API key.
Or use the prebuilt image through Compose (no local build):
# use either registry — both expose the same tags
echo "OPENMAIL_IMAGE=ghcr.io/chenyuwebwawa/openmail:latest" >> .env
echo "OPENMAIL_IMAGE=chenyutech/openmail:latest" >> .env
docker compose up -d
Image not available? Images are built by GitHub Actions and published to GHCR, and — when the repo secretsDOCKERHUB_USERNAME+DOCKERHUB_TOKENare configured (as they are for this repository) — to Docker Hub asdocker.io/chenyutech/openmail(public, multi-arch, anonymous pull). On a brand-new fork the packages may not exist yet: use Option A, or run Actions → Docker Image → Run workflow once; for anonymous GHCR pulls, set the package visibility to Public under repo → Packages → Package settings.
> Healthcheck: the image probes the public routeGET /(HTTP 200) every 30 s, sodocker compose psshowshealthyonce the app is up. Upgrades:docker compose pull && docker compose up -dfor the prebuilt image, orgit pull && docker compose up -d --buildfor source builds (source installs usescripts/update.shinstead).
> Full Docker guide — GHCR/Docker Hub publishing, ports & firewall, reverse-proxy TLS, backups, multi-arch and troubleshooting: docs/DOCKER.md.
3. Manual
Requirements: Node.js ≥ 22.5 (no native build tools needed).
git clone https://github.com/chenyuwebwawa/OpenMail.git
cd OpenMail
npm install
npm start
First launch prints the auto-created admin credentials and writes them to data/admin-credentials.txt. On Windows, run start-windows.bat for a double-click launcher.
4. Hosting panels
- BT Panel (宝塔面板) — docs/panels/BAOTA.md: full walkthrough including the 宝塔邮局 (BT Mail plugin) adaptation — auto-detects port conflicts with Postfix/Dovecot, can take over mail services or co-exist on dev ports, reverse-proxy site setup, SSL reuse, and migration steps from BT Mail to OpenMail. One-shot adapter script:
scripts/baota-adapt.sh. - 1Panel — docs/panels/1PANEL.md: Docker-Compose orchestration, OpenResty reverse-proxy site, ACME certificates and firewall rules.
Production checklist
- DNS — add A, MX, SPF, DKIM, DMARC records (the admin UI renders them for you; details in docs/DNS-Guide.md).
- TLS — point
OM_TLS_CERT/OM_TLS_KEYat a Let's Encrypt certificate (or terminate TLS at Nginx/Caddy). Self-signed certs are generated automatically for development. - Outbound relay — most clouds block outbound port 25. Configure
OM_RELAY_*to route external mail through an upstream smarthost, or use a provider with port 25 open. - rDNS (PTR) — set a reverse DNS record on your server IP pointing to your mail hostname.
- Backups — download SQLite snapshots from the admin UI and back up
files/; see docs/DEPLOYMENT.md.
Interface languages (10)
The webmail and admin console ship with English and 简体中文 built in, plus 8 installable language packs (provided as separate content in langpacks/):
🇬🇧 English · 🇨🇳 简体中文 · 🇫🇷 Français · 🇪🇸 Español · 🇵🇹 Português · 🇷🇺 Русский · 🇸🇦 العربية · 🇮🇳 हिन्दी · 🇧🇩 বাংলা · 🇵🇰 اردو
Arabic and Urdu render the whole UI right-to-left automatically. Switch anytime in Settings → Language.
Install packs (any one of):
node scripts/install-langpacks.mjs --all # CLI: install every pack
node scripts/install-langpacks.mjs fr ar ur # CLI: specific packs
npm run langpacks # show status
or in the admin web UI (Settings → Language → Install), or by copying langpacks/.json → public/locales/. Missing keys fall back to English; see langpacks/README.md to write your own pack.
AI assistant (OpenAI-compatible)
The AI features (write email, translate, analyze, generate HTML templates) run server-side and call any OpenAI-compatible POST {base}/chat/completions endpoint. Configure them per user in Settings → AI assistant: choose a provider preset (OpenAI / DeepSeek / Moonshot / Zhipu GLM / Ollama / custom), fill in Base URL, model and API key, then press Test connection. The key is encrypted with AES-256-GCM in the SQLite database and is never returned in clear text.
- Base URL formats — either the service root /
…/v1(https://api.deepseek.com,https://api.openai.com/v1) or the full endpoint copied from the provider docs (https://api.deepseek.com/chat/completions). It is normalized on save, and the server calls{base}/chat/completionswithAuthorization: Bearer— the exact OpenAI chat-completions request shape (stream: false). - DeepSeek example — select the
DeepSeekpreset (Base URLhttps://api.deepseek.com, modeldeepseek-chat, ordeepseek-reasonerfor thinking), paste the API key and save. No manual URL editing is needed. - Model list — Fetch models queries the upstream
GET {base}/modelsand lets you pick from a dropdown; manual input still works. - Enable / disable — the switch defaults to on for the first setup. While disabled, generation endpoints return
403 AI_DISABLEDwith a hint, but Test connection still works for diagnostics. - Provider differences — reasoning/thinking models are supported: if the provider rejects
max_tokens, it is retried asmax_completion_tokens; unsupportedtemperature/response_formatare dropped and retried; SSE streams, content-part arrays andreasoning_contentresponses are handled. Errors include the resolved endpoint, HTTP status and the upstream message. - Secrets — saved API keys are encrypted with
OM_SECRET; when unset, a key is generated once and persisted todata/secret.key(back it up withdata/, and never change it or saved keys stop decrypting). Keys stored by older builds that used a per-boot random secret can no longer be decrypted — re-enter the API key once after upgrading.
Configuration
Everything is configured via environment variables (or a .env file) — see .env.example. Highlights:
| Variable | Default | Description |
| --- | --- | --- |
| OM_PRIMARY_DOMAIN | localhost | Domain created on first boot (with DKIM keys) |
| OM_BASE_URL | http://localhost:3000 | Public URL, used in reset links |
| OM_SECRET | auto-generated | Server-side key used to encrypt saved AI API keys. When unset, a random value is generated and persisted to data/secret.key; changing it invalidates stored AI keys (they must be re-entered) — set a fixed long random value in production |
| OM_REGISTRATION | true | Allow self-service registration |
| OM_RELAY_HOST/PORT/USER/PASS | – | Upstream SMTP relay for outbound mail |
| OM_TLS_CERT / OM_TLS_KEY | – | PEM certificate for STARTTLS / implicit TLS |
| OM_ANTISPAM, OM_SPAM_SCORE | true, 5 | Junk threshold |
CLI maintenance tool:
node server/setup.js # show help
node server/setup.js reset-admin [email protected] NewPass123
node server/setup.js add-domain example.org
node server/setup.js list-users
Architecture
┌────────────────────────── one Node.js process ──────────────────────────┐
│ HTTP :3000 SMTP :25 Submission :587/465 IMAP :143/993│
│ ┌───────────┐ ┌─────────┐ ┌────────────┐ ┌──────────┐ │
│ │ Webmail │ │ MX pipe │ │ SASL auth │ │ Minimal │ │
│ │ Admin UI │ │ ▼ │ │ ▼ │ │ IMAP/POP3│ │
│ │ REST API │ │ mailauth(SPF/DKIM/DMARC) + spam score │ │
│ └─────┬─────┘ └────┬────┘ └─────┬──────┘ └────┬─────┘ │
│ │ └───────┬────────┘ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ mailstore (folders, threads, quota, filters) │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ node:sqlite (WAL) files/ attachments (per-user) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ scheduler: scheduled send · outbound queue retry · bounce notices │
└─────────────────────────────────────────────────────────────────────────┘
openmail/
├── .github/workflows/ # CI: build & publish multi-arch images to GHCR + Docker Hub
├── install.sh # one-click installer (Node 22 + systemd + langpacks)
├── start-windows.bat # Windows double-click launcher
├── scripts/ # update.sh (one-click updater) · doctor.sh (self-diagnosis) · install-langpacks.mjs · baota-adapt.sh (BT panel)
├── server/
│ ├── index.js # entry: HTTP + all services + first-run init
│ ├── smtp.js # MX / Submission / SMTPS servers
│ ├── imap.js # minimal IMAP4rev1 server
│ ├── pop3.js # POP3/POP3S server
│ ├── db.js # node:sqlite schema + helpers
│ ├── config.js # env config
│ ├── setup.js # CLI maintenance tool
│ ├── util/ # TOTP, DKIM keys, TLS, sessions, RBAC
│ ├── mail/ # mailstore, delivery pipeline, outbound, scheduler
│ └── routes/ # auth / mail / contacts / settings / admin / langs APIs
├── public/ # webmail + admin SPA (vanilla ES modules, no build)
│ ├── js/i18n.js # i18n runtime (10 languages, RTL aware)
│ └── locales/ # installed language files (en + zh built in)
├── langpacks/ # extra language packs + installer manifest (8 languages)
├── scripts/
│ ├── install-langpacks.mjs
│ ├── update.sh — 一键更新
│ └── baota-adapt.sh # BT Panel (宝塔) adapter incl. BT Mail plugin handling
├── tests/e2e.mjs # 119-check offline e2e suite (API, SMTP, IMAP, POP3, i18n, mock-AI)
├── docs/ # DNS guide, deployment guide, Docker guide (docs/DOCKER.md), panel guides, screenshots
├── Dockerfile · docker-compose.yml · .env.example
Open http://localhost:3000/for the interactive product site; the webmail lives at/app/.
Stack: Express 5 · nodemailer (MIME/DKIM) · smtp-server · mailparser · mailauth (SPF/DKIM/DMARC) · bcryptjs · node:sqlite. Frontend is a dependency-free vanilla-JS SPA with light/dark themes.
Comparison with other self-hosted solutions
| Solution | Style | Best for | Resources | | --- | --- | --- | --- | | OpenMail | Single Node.js process, batteries included | Personal / small teams, learning, fast deploys | 512 MB RAM | | mailcow | Docker full stack, most complete | SMEs wanting everything | 6 GB RAM + 20 GB disk | | Mailu | Lightweight Docker | Resource-constrained setups | 1–3 GB RAM | | iRedMail | Traditional installer | Bare-metal preference | 2 GB+ RAM | | Modoboa | Panel-centric, modular | Mail hosting providers | 2 GB+ RAM | | Stalwart | Rust, all-in-one, JMAP | Performance-focused | low (512 MB) | | Mox / Maddy | Go, minimal ops | Personal, enthusiasts | 512 MB– | | Postfix+Dovecot+Roundcube | Classic combo | Deep customization | à la carte |
Hardware & network requirements
| Tier | CPU | RAM | Disk | | --- | --- | --- | --- | | Personal (< 10 users) | 1 GHz dual-core | 512 MB – 1 GB | 20 GB SSD | | Team (10–50 users) | 2–4 cores | 2 GB | 100 GB+ SSD |
Network: a static public IPv4, outbound port 25 (or a relay), reverse DNS (PTR) on your IP, firewall limited to the mail ports. Mainland-China cloud providers usually block outbound 25 — use OM_RELAY_* or an overseas VPS.
Development & testing
npm test # 119-check offline e2e suite against a running server (mock AI upstream included)
npm run test:secret # verify the auto-generated OM_SECRET survives restarts
npm run dev # node --watch auto-restart
The suite covers registration/login, internal & scheduled sending, attachments, external delivery over raw SMTP, SASL submission, IMAP (fetch/store/search/append/list), POP3, folders, filters, blacklists, contacts import/export, 2FA, and every admin API including DKIM/DNS records.
Security model & known limitations
- Passwords bcrypt-hashed; sessions are random 256-bit tokens stored hashed; cookies are HttpOnly/SameSite.
- Mail HTML is sanitized (script/style/iframes/event handlers stripped) and links get rel=noopener`.
- SMTP open-relay protection: port 25 only accepts recipients on hosted domains; Submission requires auth.
- Known limitations (roadmap): IMAP is minimal (no CONDSTORE/QRESYNC), no JMAP yet, anti-spam is rule-based rather than Bayesian, no built-in CalDAV/CardDAV, per-message quota accounting is approximate on shared attachments.
- This project ships real mail-server functionality — run it on a properly configured host with TLS, and keep backups.
License
MIT — contributions welcome!