mixandClaude Opus 5.5 86665f08e6
test / test (push) Canceled after 0s
audit: decisions of 2026-10-13/14 on what 2.4.0 left undone
Agreed with the owner, one question at a time; documents only, no code.

From from-header-binding's Not done list, each a step of the audit plan:
the Sender check column says "Connections turned away"; a stand test of
which key OpenDKIM signs with; Health connects to a milter socket instead
of stat-ing it; the cards layout orders a phone by the box's number;
a refusal the panel did not write is recovered from mail.log; a log
rotated while the panel was down is read to its end; the deferred counter
keeps a place in the log; downtime is counted when supervisord is silent.
"Today, UTC" became the roadmap item local-time (agreed): one instance
zone, no labels beside a time, days counted in that zone, with the
conditions for per-user zones and offsets, and the trap of mail.log's own
stamps written down.

The six steps on the log tailer are split off stage 1 into stage 2b, a
cut of its own (2.5.1); stages 3 and 4 move to 2.5.2 and 2.5.3.

Also: negative e2e cases pass only on their own refusal (stage 0);
notifications are owed per recipient (stage 4); a DNS outage is unknown,
not wrong (M14, M15 had no step); DMARC reports tested where they come in.
review-2026-08-followups is closed, its rows placed in stages or dropped
with a reason. Certificate warnings by lifetime: left as is, with the
condition for return.

The audit's checklist is 61 lines (was 48).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:29:13 +03:00

SelfPost

SelfPost

Self-hosted SMTP relay with a web panel — one Docker image, no provider.

Postfix, OpenDKIM, and a small Go panel run together under supervisord; you configure domains, DKIM keys, and SASL applications once, then point your apps at the SMTP endpoint.

SelfPost sends mail straight to the internet from your own IP, with per-domain DKIM signing. No mailboxes or webmail. An optional inbound relay (backup-MX / forwarder on port 25) forwards to an upstream; it does not store mail.

For: operators who run their own VPS or home server and want a simple relay they control, without a third-party SMTP provider.

Key properties: one container, multi-domain DKIM, SASL per application, send log and DNS checks in the panel, encrypted backups.

Features

  • Outbound SMTP (465/smtps; optional 587 submission) with per-domain DKIM signing
  • Optional inbound relay on port 25 (backup-MX / forwarder; off by default)
  • Optional DMARC aggregate report ingest on port 25 (off by default)
  • In-panel Help: a page of topics, opened from the ? in the head of a box
  • Per-application client IP allow-list for SMTP AUTH (optional)
  • Web panel grouped by mail direction — Overview, Outbound (domains, log, queue, DMARC reports), Inbound, Server (health, system log, backup, users, settings)
  • Multi-domain relay — each SASL application is bound to one sending domain
  • DNS status checks (PTR, SPF, DKIM, DMARC) with in-panel re-check
  • Two-level rate limiting — IP backstop (Postfix), per-domain ceilings, and trusted-IP app overrides
  • Full-server backup and single-domain export/import (optional password encryption)
  • Single Docker image; production data in a ./data bind mount (the quick start below uses a named Docker volume instead)

Documentation

Document Contents
Operator guide Reverse proxy, environment variables, DNS, IP warmup, panel operations, rate limiting, backup/restore, ports, image tag
Product boundaries Purpose, deployment assumptions, out-of-scope items, multi-domain model
Architecture As-built technical design
Security design Mandatory requirements, accepted risks, the CSRF ADR
Development Building, testing, docs rules, model routing, commits
Roadmap Open work (1.x+) — direction, not commitments
CHANGELOG Release history

Found a vulnerability? Do not open an issue — SECURITY.md has the private reporting channel and the scope.

Repository: https://github.com/mixeme/selfpost — source, issues, releases, and the ghcr.io/mixeme/selfpost image.

Requirements

Providing these is the operator's job — SelfPost cannot fix a blocked port or a missing PTR record for you. Details: Operator guide.

Platform

  • Docker + Compose v2 on the host
  • A reverse proxy in front of the panel (SelfPost never terminates HTTPS itself)
  • Rough sizing: 1 vCPU, 512 MB–1 GB RAM, 8–10 GB disk (send log and rotated mail.log are the main growth drivers; both sit in the ./data volume, and both are capped — 90 days and 14 files by default)

Network and IP

  • Static IP address
  • Outbound TCP port 25 unblocked (many consumer/cloud hosts block it by default)
  • PTR/rDNS for that IP pointing at your mail hostname (SELFPOST_HOSTNAME)
  • Reasonable starting IP reputation — a fresh IP still needs warmup

Per sending domain

For every domain you add in the panel:

  • SPF TXT record authorizing this server
  • DKIM TXT record (value shown on the domain page)
  • DMARC _dmarc TXT record

See Domain-level DNS in the operator guide.

Per inbound domain (optional)

Only if you turn on inbound relay (INBOUND_RELAY_ENABLE=true):

  • MX record pointing at SELFPOST_HOSTNAME (keep any existing primary MX if this host is backup-MX)

See Inbound relay in the operator guide.

Quick start

First boot — create the admin account. On a fresh container SelfPost prints a one-time setup URL (valid ten minutes). Open it in a browser to choose the administrator username and password. Until you do, the panel has no login. Production deploy: Full deployment in the operator guide.

One container, panel at http://127.0.0.1:8080 — no reverse proxy, no TLS files, no compose files. Good for clicking through the UI on your machine; outbound mail will not reach the real internet without DNS, PTR, and port 25.

docker run --rm -d --name selfpost-try \
  -p 127.0.0.1:8080:8080 \
  -e SELFPOST_HOSTNAME=mail.local.test \
  -e PANEL_COOKIE_SECURE=false \
  -v selfpost-try-data:/data \
  ghcr.io/mixeme/selfpost:2.4.1

Get the setup URL (pick one):

docker logs selfpost-try 2>&1 | grep -m1 'http'
docker exec selfpost-try cat /data/setup-token

The printed URL is https://mail.local.test/setup/<token>. For this local trial rewrite it to http://127.0.0.1:8080/setup/<token> (same path token; PANEL_COOKIE_SECURE=false so the cookie works over plain HTTP). Open it before it expires.

When finished:

docker rm -f selfpost-try && docker volume rm selfpost-try-data

More detail (limitations, optional throwaway TLS for local SMTP): Local trial in the operator guide.

Reference deploy

Production layout: one docker-compose.yml, a .env, persistent ./data, and TLS PEM files at ./certs (read by Postfix on 465/587). The panel is reached only through a reverse proxy on 443 — port 8080 is bound to localhost in the default compose file.

Full walkthrough — fetching the base files, setting up a reverse proxy and TLS (Apache/nginx/Caddy/Traefik), starting the container, and wiring up DNS — lives in the operator guide's Full deployment section, with proxy-specific commands under Reverse proxy.

The compose file always publishes 465, 587, and 25; Postfix listens on 587 only when SUBMISSION_ENABLE=true, and on 25 when INBOUND_RELAY_ENABLE=true and/or DMARC_REPORTS_ENABLE=true (see Ports). Bump the pinned image tag deliberately when upgrading, never :latest (why). Optional variables (TRUSTED_PROXY_CIDR, rate limits, retention): see Environment variables.

License

Copyright © 2026 Mikhail Yenuchenko.

AGPL-3.0. The AGPL closes the "SaaS loophole": if you run a modified version as a network-accessible service, you must make the modified source available to its users — not only when you distribute copies of the code. Third-party notices: NOTICE.

S
Description
No description provided
Readme AGPL-3.0
12 MiB
Languages
Go 91.4%
HTML 3.5%
Shell 2.9%
CSS 1%
JavaScript 0.6%
Other 0.6%