Files
mailgoserver/README.md
T

11 KiB

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, 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).

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 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.

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 (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.