2026-08-15 06:22:02 +01:00
|
|
|
# mailgoserver
|
|
|
|
|
|
|
|
|
|
A self-hosted email server in one static Go binary: SMTP (send + receive, direct-to-MX
|
|
|
|
|
delivery, DKIM signing), IMAP mailbox storage, an admin web dashboard, and a self-service
|
|
|
|
|
webmail portal. It's a Go port of PyMTA-server — same feature set, one process instead of
|
|
|
|
|
a Python venv + separate services.
|
|
|
|
|
|
|
|
|
|
## What it does
|
|
|
|
|
|
|
|
|
|
- **SMTP MTA** — accepts mail for domains you configure, relays outbound mail directly
|
|
|
|
|
to the recipient's MX (no smarthost needed), signs outgoing mail with DKIM, and
|
|
|
|
|
enforces per-domain sender authentication or IP whitelisting so it can't be used as an
|
|
|
|
|
open relay.
|
|
|
|
|
- **IMAP mailbox storage** — real, retrievable mailboxes (Thunderbird, Outlook, any IMAP
|
|
|
|
|
client) with AES-encrypted-at-rest message storage, per-mailbox quotas, send-as
|
|
|
|
|
aliases, and filter rules (move to folder, forward, discard, based on from/subject/
|
|
|
|
|
body conditions with AND/OR logic).
|
|
|
|
|
- **Spam filtering** — a built-in heuristic score always runs; optionally point it at an
|
|
|
|
|
[rspamd](https://rspamd.com) instance for a lot more signal (see `docker-deploy/` for
|
|
|
|
|
a container that bundles rspamd for you).
|
|
|
|
|
- **Webmail portal** (`/webmail`) — inbox/folders, HTML compose with attachments,
|
|
|
|
|
drafts, search, keyboard shortcuts, conversation grouping, recipient autocomplete,
|
|
|
|
|
filter-rule management, and PGP (OpenPGP encrypt/decrypt/sign/verify) and S/MIME
|
|
|
|
|
(sign/verify) support per mailbox.
|
2026-08-15 21:51:43 +01:00
|
|
|
- **Admin dashboard** (`/smtp-server`) — manage domains, senders, mailboxes, DKIM
|
2026-08-15 12:35:44 +01:00
|
|
|
keys, IP whitelisting, TLS (self-signed/custom, or up to two simultaneous Let's
|
|
|
|
|
Encrypt certificates — DNS-01 via Cloudflare/Route53/DigitalOcean/Google Cloud DNS, and
|
|
|
|
|
HTTP-01 for domains you don't manage DNS for, optionally covering the server's own IP
|
|
|
|
|
too — independently assignable per listener, e.g. HTTP-01 for mail and DNS-01 for the
|
|
|
|
|
dashboard), and review email + auth logs.
|
2026-08-15 06:22:02 +01:00
|
|
|
- **Security hardening built in** — CSRF protection, security headers (CSP, X-Frame-
|
|
|
|
|
Options, etc.), Cloudflare-aware trusted-proxy IP resolution, login rate limiting and
|
|
|
|
|
account lockout, TOTP + WebAuthn/passkey MFA (admin and mailbox owners), and automatic
|
|
|
|
|
temporary IP blacklisting for SMTP/IMAP brute-force or relay-abuse attempts with an
|
|
|
|
|
admin-visible Blacklist page and dashboard attack-count tiles.
|
|
|
|
|
|
|
|
|
|
## Quick start
|
|
|
|
|
|
|
|
|
|
### Docker (fastest)
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cd docker-deploy
|
|
|
|
|
docker compose --profile standalone up -d --build
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
See [`docker-deploy/README.md`](docker-deploy/README.md) for the rspamd-bundled variant
|
|
|
|
|
and full details (ports, volumes, first-boot config).
|
|
|
|
|
|
|
|
|
|
### From source
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
go build -o mailgoserver .
|
|
|
|
|
./mailgoserver
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
One static binary — no venv, no `pip install`, no gunicorn. Requires Go 1.26+ to build;
|
|
|
|
|
the binary itself has no runtime dependencies (pure-Go SQLite driver, no cgo).
|
|
|
|
|
|
|
|
|
|
## What to expect on first run
|
|
|
|
|
|
|
|
|
|
There's no config to write up front. The first time it starts in a given working
|
|
|
|
|
directory, it generates:
|
|
|
|
|
|
|
|
|
|
- `settings.ini` — every setting with an inline comment explaining it (SMTP/IMAP ports,
|
|
|
|
|
hostname, TLS, DKIM key size, mailbox quotas, MFA enforcement, rate limits, and so
|
|
|
|
|
on). Regenerated only if missing — it's never overwritten or merged into on later
|
|
|
|
|
runs, so edits stick.
|
|
|
|
|
- A self-signed TLS certificate (used until you either supply your own or enable Let's
|
|
|
|
|
Encrypt from the admin dashboard).
|
|
|
|
|
- An empty SQLite database, with one seeded admin account: username `admin`, password
|
|
|
|
|
`Password123!`. Logging in with it **immediately forces** a username + password
|
|
|
|
|
change before anything else in the dashboard is reachable — the default credentials
|
|
|
|
|
can never be left in place.
|
|
|
|
|
- A mailstore master key and a CSRF-signing app secret, both generated once and reused
|
|
|
|
|
on every subsequent start — back these up like any other secret (losing the mailstore
|
|
|
|
|
master key makes all stored mail unrecoverable, even for admins).
|
|
|
|
|
|
2026-08-15 21:51:43 +01:00
|
|
|
From there: log into `/smtp-server`, add a domain (and complete its DNS ownership
|
2026-08-15 06:22:02 +01:00
|
|
|
verification), add a mailbox or sender, and you're sending/receiving. The bare `/` root
|
|
|
|
|
redirects to the webmail login (`/webmail/login`) by default, since most visitors are
|
|
|
|
|
mailbox owners, not admins — there's a "Login as Admin" link from there to
|
2026-08-15 21:51:43 +01:00
|
|
|
`/smtp-server`.
|
2026-08-15 06:22:02 +01:00
|
|
|
|
|
|
|
|
Default ports (all configurable in `settings.ini`): SMTP `25`, direct-TLS SMTP `465`,
|
|
|
|
|
IMAP `143`, direct-TLS IMAP `993` — the real standard mail ports, so binding them
|
|
|
|
|
directly needs root or `setcap` (see below), which the Docker deployment already
|
|
|
|
|
handles for you. Admin/webmail HTTP `5000` / HTTPS `5001` stay deliberately
|
|
|
|
|
non-privileged; put a reverse proxy or your own `80`/`443` mapping in front of those if
|
2026-08-15 12:35:44 +01:00
|
|
|
you want the dashboard on standard web ports too. Port `80` is also used, but only
|
|
|
|
|
transiently, if you enable Let's Encrypt's HTTP-01 challenge (see below) — the same
|
|
|
|
|
setcap/root/Docker rule applies to it as to the mail ports.
|
2026-08-12 12:56:22 +01:00
|
|
|
|
|
|
|
|
## Build
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cd mailgoserver
|
|
|
|
|
go build -o mailgoserver .
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-15 12:35:44 +01:00
|
|
|
## Bind ports 25/143/465/993 (and 80, for Let's Encrypt) without root
|
2026-08-12 12:56:22 +01:00
|
|
|
|
2026-08-15 06:22:02 +01:00
|
|
|
The default SMTP/IMAP ports are the real standard ones now, so running the binary
|
|
|
|
|
directly (not via Docker) needs one of:
|
2026-08-12 12:56:22 +01:00
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
sudo setcap 'cap_net_bind_service=+ep' ./mailgoserver
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-15 06:22:02 +01:00
|
|
|
or run it as root, or via the systemd unit below (which grants the capability instead
|
|
|
|
|
of running as root). Same purpose as `script_setup_py_environment.sh`'s `setcap` step
|
2026-08-15 12:35:44 +01:00
|
|
|
on the Python venv, applied to the compiled binary instead. `cap_net_bind_service`
|
|
|
|
|
covers every port under 1024, so this one grant is also what lets Let's Encrypt's
|
|
|
|
|
HTTP-01 challenge bind :80 (only while an obtain/renew is actually running, see the
|
|
|
|
|
Let's Encrypt page below). **Not needed for the Docker
|
2026-08-15 06:22:02 +01:00
|
|
|
deployment** — those containers run as root, so binding 25/143/465/993 directly just
|
|
|
|
|
works with no extra setup.
|
2026-08-12 12:56:22 +01:00
|
|
|
|
|
|
|
|
## systemd (unified process)
|
|
|
|
|
|
|
|
|
|
The Go binary runs the SMTP listeners and the web UI in one process (no GIL, so no
|
|
|
|
|
need to split them into separate services the way `script_install_service.sh` split
|
|
|
|
|
`pymta-smtp.service` / `pymta-web.service` for the Python version). One unit is enough:
|
|
|
|
|
|
|
|
|
|
```ini
|
|
|
|
|
[Unit]
|
|
|
|
|
Description=mailgoserver (SMTP + web admin)
|
|
|
|
|
After=network.target
|
|
|
|
|
|
|
|
|
|
[Service]
|
|
|
|
|
Type=simple
|
|
|
|
|
WorkingDirectory=/opt/mailgoserver
|
|
|
|
|
ExecStart=/opt/mailgoserver/mailgoserver --host 127.0.0.1 --port 5000
|
|
|
|
|
Restart=always
|
|
|
|
|
RestartSec=5
|
|
|
|
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
|
|
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
|
|
|
|
PrivateTmp=true
|
|
|
|
|
ProtectSystem=strict
|
|
|
|
|
ReadWritePaths=/opt/mailgoserver
|
|
|
|
|
ProtectHome=true
|
|
|
|
|
|
|
|
|
|
[Install]
|
|
|
|
|
WantedBy=multi-user.target
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
If you do want the SMTP and web parts as separate services (matching the Python
|
|
|
|
|
split exactly), run two units with `--smtp-only` and `--web-only` respectively —
|
|
|
|
|
both flags exist for this.
|
|
|
|
|
|
|
|
|
|
## nginx
|
|
|
|
|
|
|
|
|
|
No changes needed. `script_nginx_setup.sh` reverse-proxies to `http://127.0.0.1:5000`
|
|
|
|
|
and terminates its own TLS for the web UI — mailgoserver listens on the same host:port
|
|
|
|
|
by default, so the existing nginx config works unmodified. The SMTP TLS listener still
|
|
|
|
|
consumes `ssl_certs/server.crt`/`server.key`, same as before.
|
|
|
|
|
|
2026-08-15 06:22:02 +01:00
|
|
|
## Docker
|
|
|
|
|
|
|
|
|
|
See [`docker-deploy/`](docker-deploy/) — a standalone image and one that bundles the
|
|
|
|
|
latest rspamd in the same container, both via a single `docker-compose.yml` using
|
|
|
|
|
Compose profiles.
|
|
|
|
|
|
2026-08-15 12:35:44 +01:00
|
|
|
## Certificates
|
|
|
|
|
|
|
|
|
|
Three independent listeners need a TLS certificate: SMTP direct-TLS (465), IMAP
|
|
|
|
|
direct-TLS (993), and the admin/webmail HTTPS UI (5001). Each can be assigned a
|
|
|
|
|
different one, from the admin dashboard's **Settings** page (TLS/SSL Configuration
|
|
|
|
|
card):
|
|
|
|
|
|
|
|
|
|
- **Custom** — self-signed by default (generated on first run), or your own uploaded
|
|
|
|
|
cert/key.
|
|
|
|
|
- **Let's Encrypt (DNS-01)** — automatic, needs a supported DNS provider (Cloudflare,
|
|
|
|
|
Route53, DigitalOcean, Google Cloud DNS). Configure on the dashboard's **Let's
|
|
|
|
|
Encrypt** page.
|
|
|
|
|
- **Let's Encrypt (HTTP-01)** — automatic, needs no DNS provider at all, only port 80
|
|
|
|
|
reachable from the internet — the right choice for a domain whose DNS isn't hosted
|
|
|
|
|
anywhere this server can automate. Once enabled (needs a restart to take effect), this
|
|
|
|
|
binds a small HTTP server that stays up for the life of the process — hit it directly
|
|
|
|
|
(`curl http://your-host/`) and you should get a plain `200 ok`, which is the easiest
|
|
|
|
|
way to confirm your router/reverse-proxy port-forwarding actually reaches this host,
|
|
|
|
|
independent of running a real obtain. Optionally also covers the server's own public IP
|
|
|
|
|
address on the same certificate (autodetected, or a manual override) — note this forces
|
|
|
|
|
Let's Encrypt's `shortlived` certificate profile (the only one that currently allows IP
|
|
|
|
|
identifiers), so those certificates are valid for only ~6 days and renew far more often,
|
|
|
|
|
handled automatically. Configure on the **Let's Encrypt** page. The local bind port
|
|
|
|
|
defaults to `80` (`[Server] HTTP_LETSENCRYPT_PORT`) — change this only if something
|
|
|
|
|
ahead of this host (a router or reverse proxy) forwards the internet-facing port 80 to
|
|
|
|
|
a different local port; Let's Encrypt itself always connects to port 80, there's no way
|
|
|
|
|
to make it use a different port on the CA side.
|
|
|
|
|
|
|
|
|
|
A common setup: HTTP-01 for the mail listeners (SMTP-TLS/IMAP-TLS) since mail clients
|
|
|
|
|
rarely validate hostnames strictly, paired with DNS-01 (or a real custom cert) for the
|
|
|
|
|
web UI where browsers do. Both Let's Encrypt certificates can be enabled at once — they're
|
|
|
|
|
obtained and renewed independently — and switching which listener uses which needs a
|
|
|
|
|
restart to take effect. All of this is also settable directly in `settings.ini`: see the
|
|
|
|
|
`[TLS]` (`smtp_tls_cert`/`imap_tls_cert`/`web_https_cert`), `[LetsEncrypt]` (DNS-01), and
|
|
|
|
|
`[LetsEncryptHTTP]` (HTTP-01) sections.
|
|
|
|
|
|
2026-08-12 12:56:22 +01:00
|
|
|
## Admin dashboard login
|
|
|
|
|
|
|
|
|
|
First run seeds one account: username `admin`, password `Password123!`. Logging in
|
|
|
|
|
with it immediately forces a username + password change before anything else in the
|
|
|
|
|
dashboard is reachable — the default credentials can never be left in place.
|
|
|
|
|
|
|
|
|
|
Optional second factors, enabled per-account from **Account** in the sidebar:
|
|
|
|
|
- **Authenticator app (TOTP)** — works anywhere, no extra config.
|
|
|
|
|
- **Passkeys / security keys (WebAuthn)** — bound to the exact origin the dashboard
|
|
|
|
|
is served at. Set `[Auth] rp_id` / `rp_origin` in `settings.ini` to your real public
|
|
|
|
|
domain before registering passkeys in production (e.g. `rp_id = mail.example.com`,
|
|
|
|
|
`rp_origin = https://mail.example.com`). The defaults (`localhost` /
|
|
|
|
|
`http://localhost:5000`) only work for local testing — WebAuthn requires either
|
|
|
|
|
HTTPS or the literal host `localhost`, so passkeys need the nginx+TLS setup above
|
2026-08-15 06:22:02 +01:00
|
|
|
(or the Docker deployment) to work behind a real domain.
|
|
|
|
|
|
|
|
|
|
## Abuse protection
|
|
|
|
|
|
|
|
|
|
Failed SMTP/IMAP auth attempts (bad passwords, denied relay attempts) are counted per
|
|
|
|
|
source IP; once a configurable threshold is hit within a window, that IP is temporarily
|
|
|
|
|
blocked at the listener level — before the SMTP/IMAP banner is even sent — with the
|
|
|
|
|
block duration doubling on repeat offenses up to a cap. Manage active blocks and exempt
|
|
|
|
|
trusted IPs from this check entirely from **Blacklist** in the admin sidebar (a global-
|
|
|
|
|
admin-only page) — this is separate from the IP whitelist used to authorize
|
|
|
|
|
unauthenticated relay for a domain. Tunable in `settings.ini`'s `[Security]` section.
|
2026-08-12 12:59:59 +01:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## License
|
|
|
|
|
|
2026-08-15 06:22:02 +01:00
|
|
|
See [LICENSE](LICENSE).
|