Quick start ยท Features ยท Architecture ยท Configuration ยท Deliverability ยท Contributing ยท Discussions
RayMail is a complete, open-source, self-hosted email stack you run on your own VPS. It replaces the usual pile of Postfix + Dovecot + Roundcube + a tracking SaaS with one Docker Compose file:
- A real mail server โ RFC-compliant SMTP, IMAP and JMAP from Stalwart, with automatic DKIM key generation.
- A modern webmail client โ a fast, Gmail-style three-pane workspace built in Next.js 15 and TypeScript.
- Delivery telemetry that is actually yours โ open and click tracking, a full
queued โ sent โ delivered โ opened โ clickedpipeline, and a document vault, all stored locally in SQLite. No third-party tracking pixel, no data leaving your server. - An optional AI assistant โ draft replies, summarise long threads and brief your inbox, with aggressive token budgeting so it stays cheap.
Desktop and mobile clients (Outlook, Apple Mail, Thunderbird, K-9, Gmail app) connect over standard IMAP/SMTP. The webmail speaks JMAP over the internal Docker network, so mail credentials never reach the browser.
curl -fsSL https://raw.githubusercontent.com/DeveloperSarim/raymail/main/install.sh | bashThe installer audits your ports, warns you if your provider blocks outbound :25, generates secrets, fixes container volume ownership, starts the stack, completes the mail-server setup handshake and prints the exact DNS records you need.
Step by step (click to expand)
# 1. clone
git clone https://github.com/DeveloperSarim/raymail.git
cd raymail
# 2. configure
cp .env.example .env
$EDITOR .env # set MAIL_DOMAIN, MAIL_HOSTNAME, APP_URL
# 3. prepare volumes
# both containers run unprivileged - root-owned bind mounts are the
# single most common first-boot failure
mkdir -p stalwart/etc stalwart/data/logs data
sudo chown -R 2000:2000 stalwart/etc stalwart/data # stalwart runs as uid 2000
sudo chown -R 1001:1001 data # web runs as uid 1001
# 4. start
docker compose up -d
# 5. finish setup, then print your DNS records
./deploy/dns-records.py
# 6. TLS + reverse proxy
sudo ./deploy/setup-tls.sh
# 7. health check
./deploy/verify.sh| Minimum | |
|---|---|
| OS | Any Linux with Docker (Ubuntu 22.04+ tested), or macOS for local development |
| RAM | 1 GB (2 GB comfortable) |
| Disk | 5 GB + your mail |
| Ports | 25, 465, 587, 993 free, plus two loopback ports |
| DNS | A domain you control |
|
|
|
|
The built-in mail server console โ create mailboxes, rotate passwords, copy DNS records, inspect listeners and the outbound queue.
flowchart LR
MX[Sending MX]
C[Outlook / Apple Mail / Thunderbird]
B[Browser]
P[Reverse proxy 443]
W[RayMail web - Next.js]
SW[Stalwart - 25 465 587 993]
DB[(SQLite telemetry)]
R[Smarthost relay 587]
MX -->|inbound SMTP| SW
C -->|IMAP and SMTP| SW
B --> P
P --> W
W <-->|JMAP| SW
W --> DB
SW -->|outbound| R
R --> MX
Delivery pipeline โ a message only moves forward; bounced is terminal from anywhere.
flowchart LR
Q[queued] --> S[sent]
S --> D[delivered]
D --> O[opened]
O --> C[clicked]
Q --> X[bounced]
S --> X
D --> X
| Port | Bind | Purpose | Exposed |
|---|---|---|---|
25 |
0.0.0.0 |
Inbound MX | ๐ Public |
465 |
0.0.0.0 |
SMTP submission, implicit TLS | ๐ Public |
587 |
0.0.0.0 |
SMTP submission, STARTTLS | ๐ Public |
993 |
0.0.0.0 |
IMAP, implicit TLS | ๐ Public |
3880 |
127.0.0.1 |
Webmail + telemetry API | ๐ Proxy only |
3881 |
127.0.0.1 |
Mail server admin + JMAP | ๐ Loopback only |
RayMail never binds
:80or:443โ those stay with whatever web server you already run.
raymail/
โโโ docker-compose.yml # stalwart + web
โโโ install.sh # one-command installer
โโโ deploy/
โ โโโ setup-tls.sh # certbot + reverse-proxy vhost
โ โโโ configure-relay.py # smarthost for blocked :25
โ โโโ dns-records.py # prints records incl. live DKIM
โ โโโ verify.sh # read-only health check
โโโ web/src/
โโโ app/ # routes: pages at /, API under /api
โโโ components/ # presentational UI
โโโ hooks/ # TanStack Query bindings
โโโ lib/ # db, telemetry tokens, session crypto
โโโ services/ # JMAP + Stalwart admin + DeepSeek clients
โโโ types/ # domain models
Everything lives in .env.
| Variable | Required | Purpose |
|---|---|---|
MAIL_DOMAIN |
โ | Domain RayMail handles mail for |
MAIL_HOSTNAME |
โ | Public hostname, used in SMTP greetings |
APP_URL |
โ | Public URL, used for tracking links |
TELEMETRY_SECRET |
โ | Signs tracking tokens and encrypts sessions |
STALWART_ADMIN_USER / _PASSWORD |
โ | Administrator mailbox |
RELAY_HOST / _PORT / _USERNAME / _PASSWORD |
Smarthost โ required when :25 is blocked |
|
DEEPSEEK_API_KEY |
โ | Enables the AI assistant |
DEEPSEEK_MODEL |
โ | Defaults to deepseek-chat |
Desktop client settings
Incoming IMAP mail.example.com 993 SSL/TLS
Outgoing SMTP mail.example.com 587 STARTTLS
Username the full address, you@mail.example.com
Password your mailbox password
Auth normal password, required for outgoing
Works with Microsoft Outlook, Apple Mail, Thunderbird, K-9 Mail and the Gmail app.
AI token budgeting
The cost control is in what is not sent to the model:
| Lever | Effect |
|---|---|
| HTML stripped to text | Drops markup, styles and tracking pixels before the model sees anything |
| Quoted history removed | A reply chain repeats the thread on every message; it is paid for once |
| Character budget | Bodies capped at ~6k characters, biased to the head where the ask lives |
| Overview uses envelopes only | Sender + subject + preview โ hundreds of tokens instead of tens of thousands |
| Results cached on a content hash | Re-opening a message costs nothing; only changed mail is re-summarised |
max_tokens per task |
Every task has a natural length and is capped to it |
Actual spend is shown in the admin dashboard, split into tokens in, tokens out, and results served from cache.
Self-hosted mail lands in spam for a small number of fixable reasons. In order of impact:
- PTR mismatch โ forward and reverse DNS must agree. Set the reverse record for your IP to your mail hostname.
- Missing or misaligned DKIM/SPF/DMARC โ
./deploy/dns-records.pyprints the exact records, including your live DKIM public keys. - Blocked outbound
:25โ many providers block it. RayMail then relays through a smarthost on:587; inbound:25is unaffected. - DMARC alignment โ if you relay, your Return-Path is usually a subdomain. Use relaxed alignment (
adkim=r; aspf=r) or every relayed message fails. - A brand-new domain has no reputation. Start with
p=none, send slowly, and tighten top=quarantineonce reports come back clean.
Two questions come up more than any other, both answered in detail in Discussions: mail still goes to spam with DKIM/SPF/DMARC set ยท first boot fails with "Permission denied"
Troubleshooting matrix
| Symptom | Likely cause | Check |
|---|---|---|
| Outbound mail stuck in queue | :25 egress blocked, no relay set |
RELAY_HOST in .env |
| Container exits on first boot | Root-owned bind mounts | chown -R 2000:2000 stalwart/ |
certbot fails |
A record missing or not propagated |
dig +short A mail.example.com |
| Outlook rejects the password | Using the local part, not the full address | Log in as you@mail.example.com |
465/993 silent, no handshake |
No certificate installed yet | Run ./deploy/setup-tls.sh |
| TLS warning in a mail client | Server started before the cert existed | docker restart raymail-stalwart |
| Opens never register | Recipient blocks remote images | Expected โ clicks still track |
| Mail goes to spam | See the five points above | Gmail โ Show original |
| Reverse proxy won't reload | Vhost syntax | apache2ctl configtest |
cd web
npm install
npm run dev # http://localhost:3000
npm run typecheck # tsc --noEmit, strict mode
npm test # token forgery + open-redirect guardsThe test suite runs on Node's built-in runner with no framework. It covers the security boundary that matters most: tracking tokens are HMAC-signed, so opens and clicks cannot be forged and the click redirector cannot be repointed at another host.
Contributions are welcome โ issues, features and documentation alike.
- Fork the repository and create a branch:
git checkout -b feature/my-change - Keep TypeScript strict โ
npm run typecheckmust pass - Add a test when you touch security or money paths
- Commit with a clear message and open a pull request
Good first issues
- Server-side JMAP search (the list currently filters client-side)
- Bounce ingestion from the Stalwart queue into the telemetry pipeline
- Multi-account support in the webmail
- A nginx and a Caddy variant of
deploy/setup-tls.sh - Thread grouping in the message list
Released under the MIT License. Use it, fork it, ship it.
Stalwart Mail Server ยท Next.js ยท Tailwind CSS ยท TanStack Query ยท Zustand ยท Lucide
Built and maintained by DeveloperSarim
Keywords โ self-hosted email server ยท open source webmail ยท docker mail server ยท email tracking ยท open and click tracking ยท SMTP IMAP JMAP server ยท DKIM SPF DMARC ยท Stalwart mail ยท Next.js webmail client ยท privacy-first email ยท self-hosted Gmail alternative
