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 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. - Admin dashboard (
/pymta-manager) — manage domains, senders, mailboxes, DKIM 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. - 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)
cd docker-deploy
docker compose --profile standalone up -d --build
See docker-deploy/README.md for the rspamd-bundled variant
and full details (ports, volumes, first-boot config).
From source
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, passwordPassword123!. 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).
From there: log into /pymta-manager, add a domain (and complete its DNS ownership
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
/pymta-manager.
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
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.
Build
cd mailgoserver
go build -o mailgoserver .
Bind ports 25/143/465/993 (and 80, for Let's Encrypt) without root
The default SMTP/IMAP ports are the real standard ones now, so running the binary directly (not via Docker) needs one of:
sudo setcap 'cap_net_bind_service=+ep' ./mailgoserver
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
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
deployment — those containers run as root, so binding 25/143/465/993 directly just
works with no extra setup.
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:
[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.
Docker
See 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.
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 plain200 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'sshortlivedcertificate 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 to80([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.
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_origininsettings.inito 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 hostlocalhost, so passkeys need the nginx+TLS setup above (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.
License
See LICENSE.