release: 0.1.0: Phases 0-11: panel, Postfix/DKIM, milter, backup, Docker image

Initial feature-complete implementation of the v1.0 specification (phases 0-11 of `docs/implementation-plan.md`).
Added:
  - Panel (Go, single static binary) with SQLite persistence, one-time
  - Domain management with per-domain DKIM (RSA-2048, generated in pure Go) and
  - Application (sender identity) management: SASL credentials via `sasldb2`,
  - Full Postfix relay config generated from env at container start: SMTPS 465,
  - Journal milter (pure Go, `go-milter`) recording every send to `send_log`;
  … (6 more in CHANGELOG)

Full detail: CHANGELOG.md (0.1.0).
This commit is contained in:
mix
2026-09-22 15:24:51 +03:00
commit 8ce050ef7f
157 changed files with 17631 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
package e2e
import (
"fmt"
"strings"
"time"
)
// wantRunning are the supervisord programs that must be RUNNING once the
// container is up (build/supervisord.conf). postfix-reload is deliberately
// excluded: autostart=false, its healthy resting state is STOPPED.
var wantRunning = []string{"opendkim", "panel", "postfix", "cert-reload", "logrotate"}
// checkSupervisorProcesses shells into the container directly (not through the
// panel's /status page) so it works before an administrator account even
// exists — this is the first thing the harness checks after `docker compose
// up` (plan C.4). Programs take a moment to leave STARTING right after the
// container starts, so this polls rather than checking once.
func checkSupervisorProcesses(s *stack) error {
var lastErr error
err := waitFor("all supervised programs to reach their steady state", 20*time.Second, 500*time.Millisecond, func() (bool, error) {
out, execErr := s.execIn("selfpost", "supervisorctl", "-c", "/etc/supervisor/supervisord.conf", "status")
// supervisorctl exits non-zero when any program is not RUNNING, which
// is expected for postfix-reload — parse the output regardless of exit
// status.
states := parseSupervisorStatus(out)
if len(states) == 0 {
lastErr = fmt.Errorf("supervisorctl status produced nothing to parse: %v\n%s", execErr, out)
return false, lastErr
}
for _, name := range wantRunning {
state, ok := states[name]
if !ok {
lastErr = fmt.Errorf("program %q not reported by supervisorctl:\n%s", name, out)
return false, lastErr
}
if state != "RUNNING" {
lastErr = fmt.Errorf("program %q is %s, want RUNNING:\n%s", name, state, out)
return false, lastErr
}
}
if state, ok := states["postfix-reload"]; ok && state != "STOPPED" {
lastErr = fmt.Errorf("program postfix-reload is %s, want STOPPED (autostart=false):\n%s", state, out)
return false, lastErr
}
return true, nil
})
return err
}
func parseSupervisorStatus(out string) map[string]string {
states := make(map[string]string)
for _, line := range strings.Split(out, "\n") {
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
states[fields[0]] = fields[1]
}
return states
}