mixandClaude Opus 5.5 610023ef6a panel-redesign: Inbound domains, domain and delete on the kit; Spam filter box
Stage 2, step 5. Written by Sonnet, reviewed by Opus.

  - Three pages named after their mockups: in-domains, in-domain,
    in-domain-delete, with typed page data (view/inbound.go), fixtures
    and tests. The engine pages inbound, inbound_domain and
    inbound_delete are gone with their templates and are off
    legacy_pages.txt.
  - The Spam filter box (plan, Who sees what). The panel now reads
    INBOUND_ANTISPAM_MILTER and INBOUND_ANTISPAM_MILTER_ACTION, with the
    semantics of build/postfix-config.sh (on only with the inbound relay
    on and a milter set; accept | tempfail). It reports them, nothing
    more:
      on   Applies to / Filter (the address, global role only) / If the
           filter is down (accepted unfiltered | deferred)
      off  one sentence; the variable is named for the global role only
    The list shows "Spam filter on|off" in its head.
  - Handlers pass typed data; routes, POST targets, field names,
    validation and authorization (global: all inbound domains; domain
    role: the assigned ones, another tenant's id is a 404; add and
    delete are global-only) are unchanged. A test saves the upstream and
    recipients forms untouched and finds the domain unchanged.

Left out of the mockups' Spam filter box and menu - no source before
inbound-antispam-panel (2.1): the engine name, the quarantine row, the
"refused at the door, see the log" row, the lists row, "Log for this
domain". Also "Checked N min ago" on the MX box: the check carries no
time.

Not done: the filter's socket on Health and Overview. internal/health
checks a unix socket by stat; the documented form of the filter's
address is inet:host:port and needs a dial that does not exist yet.

Differences from the mockups, for the owner:
  - refusals and confirmations are the kit flash above the boxes;
  - the MX tag is coloured by the check in every state; an MX that
    points elsewhere shows "In DNS now";
  - the lead of the domain page follows what is saved ("Not accepting
    mail yet: no upstream is saved");
  - the delete page lists only what the domain has;
  - /help#inbound opens Help at the top until the Help step.

Evidence
  - docs/assets/panel-redesign/evidence/in-domains.png, in-domain.png,
    in-domain-delete.png: 1440 px wide, headless Edge, real templates
    served locally with their fixtures (filter on) - NOT from the
    container (no Docker on this machine).
  - panel/audit.js on the three pages at 1440 and 1280: 352 texts
    checked, problems: [].
  - That the panel process sees the two variables inside the container
    is read from supervisord.conf and entrypoint.sh, not run.
  - go test -count=1 ./internal/web/... ./cmd/panel
      ok  internal/web
      ok  internal/web/auth
      ok  internal/web/handlers
      ok  internal/web/validate
      ok  internal/web/view
      ok  cmd/panel

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:39:20 +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 drawer and /help page for Status and domain controls
  • Per-application client IP allow-list for SMTP AUTH (optional)
  • Web panel — domains, applications, deliveries, mail queue, system log, backup
  • 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:1.9.5

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%