This standalone SMTP server is implemented entirely with the Go standard library. It provides a minimal feature set suitable for local testing or development environments where an external mail transfer agent is not available.
Current release: v0.4.0
- Implements core SMTP verbs (HELO/EHLO, MAIL FROM, RCPT TO, DATA, RSET, NOOP, QUIT) with multi-recipient support, dot-stuffing handling, a 10 MiB message-size limit, and per-command deadlines to keep sessions responsive.
- Enforces allow-listed access by host/IP before any banner is sent, adding a coarse ingress control layer for the unauthenticated listener.
- Persists accepted messages to disk with per-recipient hashing so stored artefacts are private yet available for later inspection or reprocessing.
- Performs outbound delivery via MX resolution, randomised equal-priority retries, opportunistic STARTTLS, and jittered exponential backoff managed by the in-memory queue.
- Scales queue throughput with a configurable pool of concurrent delivery workers so busy deployments keep pace with inbound traffic.
- Optionally signs outbound mail with DKIM when selector, key, and domain values are supplied through environment variables.
- Provides built-in observability: a health server exposes readiness,
/metricsinstrumentation, and an optional live audit log stream; audit logging can be toggled at runtime and fanned out to subscribers. - Loads configuration entirely from environment variables (with
.envsupport) covering ports, banner text, TLS/DKIM assets, and queue storage paths, simplifying containerised or systemd deployments.
- Live overview: https://gopherpost.io
- Fallback GitHub Pages URL: https://its-donkey.github.io/smtpserver/
- Source for the static site lives in
docs/(GitHub Pages ready). - GitHub Pages configuration: Settings → Pages → “Deploy from a branch” → select
mainand/docs; set custom domain togopherpost.io. - Contact:
gday@gopherpost.io
cp .env.example .env
# Set SMTP_ALLOW_NETWORKS or SMTP_ALLOW_HOSTS before starting
go run ./...
# or build a local binary
go build -o gopherpost ./...
./gopherpostThis server is configured entirely through environment variables (automatically loaded from a local .env file when present). Boolean values must be specified as true or false.
SMTP_PORT # TCP port to bind for the SMTP listener (default 2525).
SMTP_HOSTNAME # Hostname advertised in SMTP banners and HELO/EHLO (default system hostname).
SMTP_BANNER # Custom greeting appended to the initial 220 response (default GopherPost ready).
SMTP_DEBUG # Enable verbose audit logging when `true` (default `false`).
SMTP_HEALTH_ADDR # Listen address for the health server (default :8080).
SMTP_HEALTH_PORT # Override only the port component of the health address (e.g. 9090).
SMTP_HEALTH_DISABLE # Disable the health endpoint when `true` (default `false`).
SMTP_QUEUE_PATH # Directory used to persist inbound messages (default ./data/spool).
SMTP_QUEUE_WORKERS # Number of concurrent delivery workers processing the outbound queue (default logical CPU count).
SMTP_RETENTION_DAYS # Number of days to retain stored messages before automatic cleanup (default 7).SMTP_AUTH_USERS # Comma-separated list of user:password pairs for SMTP AUTH (e.g. alice:secret,bob:pass123).
SMTP_AUTH_INSECURE # Allow AUTH without TLS when `true` (default `false`, strongly discouraged).When SMTP_AUTH_USERS is configured, the server advertises AUTH PLAIN and AUTH LOGIN in EHLO responses (only over TLS unless SMTP_AUTH_INSECURE is set). Authenticated users bypass the SMTP_REQUIRE_LOCAL_DOMAIN restriction, allowing them to send from any address.
SMTP_ALLOW_NETWORKS # Comma-separated CIDR blocks/IPs allowed to connect (e.g. 192.0.2.0/24,203.0.113.5). When unset, all connections are rejected.
SMTP_ALLOW_HOSTS # Comma-separated hostnames allowed to connect (e.g. mail.example.com). When unset alongside networks, all connections are rejected.
SMTP_REQUIRE_LOCAL_DOMAIN # Require `MAIL FROM` senders to match `SMTP_HOSTNAME` when `true` (default `true`). SMTP_TLS_DISABLE # Skip loading TLS certificates when `true` (default `false`).
SMTP_TLS_CERT # Path to the PEM certificate served for STARTTLS (e.g. /etc/ssl/certs/smtp.crt).
SMTP_TLS_KEY # Path to the PEM private key matching the TLS cert (e.g. /etc/ssl/private/smtp.key). SMTP_DKIM_SELECTOR # DKIM selector published in DNS (e.g. mail1).
SMTP_DKIM_KEY_PATH # Filesystem path to the DKIM private key (e.g. /etc/dkim/mail1.key).
SMTP_DKIM_PRIVATE_KEY # Inline PEM-formatted DKIM private key (e.g. -----BEGIN RSA PRIVATE KEY-----).
SMTP_DKIM_DOMAIN # Domain to sign messages as when overriding the sender domain (e.g. example.com). Security note: configure SMTP_ALLOW_NETWORKS, SMTP_ALLOW_HOSTS, and SMTP_REQUIRE_LOCAL_DOMAIN to enforce ingress and sender restrictions. For authenticated access, configure SMTP_AUTH_USERS and ensure TLS is enabled. Deploy behind firewalls or proxies and run as a non-root service account.
Use an absolute path for SMTP_QUEUE_PATH when running the daemon under systemd so that the service ReadWritePaths setting can be aligned.
Prerequisites: Linux host with systemd, Go 1.21 or newer, and root access.
# 1. Build the binary
go build -o gopherpost ./...
# 2. Create a dedicated user and runtime directories
sudo useradd --system --home /opt/gopherpost --shell /usr/sbin/nologin gopherpost
sudo install -d -o gopherpost -g gopherpost -m 0755 /opt/gopherpost /var/lib/gopherpost /var/spool/gopherpost
sudo install -d -m 0755 /etc/gopherpost
# 3. Install the binary and systemd unit
sudo install -o root -g root -m 0755 gopherpost /usr/local/bin/gopherpost
sudo install -m 0644 packaging/systemd/gopherpost.service /etc/systemd/system/gopherpost.service
# 4. Configure environment
sudo cp .env.example /etc/gopherpost/gopherpost.env
sudo ${EDITOR:-nano} /etc/gopherpost/gopherpost.env
# - Set SMTP_ALLOW_NETWORKS or SMTP_ALLOW_HOSTS to the networks/hosts you trust
# - Ensure SMTP_QUEUE_PATH=/var/spool/gopherpost (or adjust the unit's ReadWritePaths accordingly)
# 5. Activate the service
sudo systemctl daemon-reload
sudo systemctl enable --now gopherpostThe install.sh helper automates the same steps, generates a tailored unit file, and performs an optional health check. Run it as root:
sudo ./install.shYou will be prompted for the environment values (defaults are shown inline). The script will build the binary, create required directories, install the service to /etc/systemd/system/gopherpost.service, reload systemd, and start the daemon. Provide an absolute SMTP_QUEUE_PATH so the generated ReadWritePaths remain valid.
Messages are persisted automatically. On-disk filenames include a message identifier and a hash of the recipient address so personally identifiable information is not exposed through filenames.
$ telnet localhost 2525
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 localhost Simple Go SMTP Server
HELO example.com
250 localhost greets example.com
MAIL FROM:<alice@example.com>
250 Sender <alice@example.com> OK
RCPT TO:<bob@example.net>
250 Recipient <bob@example.net> OK
DATA
354 End data with <CR><LF>.<CR><LF>
Subject: Hello
This is a test email.
.
250 Message accepted for delivery
QUIT
221 localhost signing off
Connection closed by foreign host.This server now supports direct delivery to recipient domains via MX record resolution. It performs MX preference sorting, randomises equal-priority records, and upgrades to STARTTLS only when advertised by the remote host.
Messages that fail to deliver are automatically retried with capped exponential backoff and jitter to avoid thundering herd effects.
Incoming messages are saved to disk under ./data/spool/YYYY-MM-DD/ by default. Override the directory by setting SMTP_QUEUE_PATH or by calling storage.SetBaseDir before accepting traffic (useful for tests or containerised deployments).
Set SMTP_DEBUG=true to enable verbose delivery logs.
Set SMTP_TLS_CERT and SMTP_TLS_KEY to enable STARTTLS. Certificates are served with a minimum TLS version of 1.2.
The outbound client upgrades to TLS when the remote server advertises the capability, but it never accepts invalid certificates.
An HTTP endpoint is available at :8080/healthz (override via SMTP_HEALTH_ADDR) for readiness/liveness probes. If the configured port cannot be bound the SMTP server continues without the health listener.
Structured metrics are exported at /metrics in expvar JSON format whenever the health server is running.
When SMTP_DEBUG=true, visiting the /healthz endpoint renders OK followed by a live stream of audit log entries so you can tail activity from a browser or curl.
- Enable local Git hooks for commit and branch checks:
git config core.hooksPath .githooks
- Branch naming:
type/section/kebab-feature(e.g.,feature/queue/retry-jitter). - Commit style:
<type-short> (section): <message>- type-short: feat, fix, hotfix, design, refactor, test, doc
- PR template fields: include
Type:andSection:at the top of the description. - Update
CHANGELOG.mdfor every PR unless you apply theno-changeloglabel.
Example commit subject
refactor (module): rename module and imports to gopherpost
Example PR body
Type: refactor
Section: module
Summary:
Rename module path and all imports to `gopherpost`.
Changes
- Update `go.mod` module path
- Update internal imports
- Adjust tests
Checklist
- [x] CHANGELOG.md updated
- [x] Branch name follows convention
- [x] Commits follow convention