Files
selfpost/cmd/panel/httpserver.go
T
mix 6ebb6f56d6 Phase 6: journal-milter + send-log status tailer + retention
Implement the structured send log (spec 7.3), the project's highest-risk
component since a milter bug can break the relay itself.

- internal/milter: go-milter v0.4.1 journal-milter. Per-connection session
  collects SASL login, From, recipients and Subject across callbacks and
  writes one send_log "queued" row per (queue-id, recipient) at EOM
  (spec 7.3.3). Monitoring only: callbacks return Continue/Accept, recorder
  errors are logged never propagated, so it can never block mail.
- internal/logtail: polling mail.log tailer with rotation handling (inode
  change / truncation), parses sent/deferred/bounced/expired by queue-id +
  recipient and advances rows; background retention sweep prunes rows past
  SEND_LOG_RETENTION_DAYS (default 90) at startup and every 6h.
- internal/store/sendlog.go: InsertQueued, UpdateStatus (case-insensitive
  recipient match), DeleteSendLogBefore + status constants.
- cmd/panel: open the store once and share it across http/milter/tailer;
  replace the journal/logtail stubs with the real roles.
- build/postfix-config.sh: bounded milter timeouts (15/15/30s) so a hung
  milter also fails open in seconds, not the 300s default.

Fix found in-container: SASL login (app_login) was empty because go-milter
keys macros exactly as Postfix sends them, and multi-character macro names
arrive brace-wrapped ({auth_authen}); the SASL-less Phase 0 spike could not
observe this. Added a brace-tolerant macro lookup.

Verified on selfpost.example.com: gofmt/vet/unit tests green; container e2e
records rows with correct fields and advances status via the tailer; fail-open
confirmed for both an unreachable and a hung milter; retention prunes at start.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 22:58:34 +03:00

58 lines
1.7 KiB
Go

package main
import (
"context"
"errors"
"log"
"net/http"
"time"
"codeberg.org/mix/selfpost/internal/app"
"codeberg.org/mix/selfpost/internal/domain"
"codeberg.org/mix/selfpost/internal/postfix"
"codeberg.org/mix/selfpost/internal/store"
"codeberg.org/mix/selfpost/internal/web"
)
// serveHTTP runs the control-panel HTTP server until ctx is cancelled, using
// the database handle shared by all roles. From Phase 2 this serves the real
// setup, login and authenticated panel surface (spec 7.6).
func serveHTTP(ctx context.Context, cfg config, st *store.Store) error {
// Applications own the SASL accounts and the Postfix sender map; the domain
// service delegates to them when a domain (and its applications) is deleted.
pf := postfix.New(cfg.postfixDir)
apps := app.NewService(st, app.NewSASLDB(cfg.saslDBPath, cfg.saslRealm), pf)
domains := domain.NewService(st, domain.NewOpenDKIM(cfg.opendkimDir), apps, cfg.dkimSelectorDef)
srvApp, err := web.New(st, domains, apps, web.Config{
Hostname: cfg.hostname,
CookieSecure: cfg.cookieSecure,
}, cfg.setupTokenPath)
if err != nil {
return err
}
if err := srvApp.Start(); err != nil {
return err
}
srv := &http.Server{
Addr: cfg.httpAddr,
Handler: srvApp.Handler(),
ReadHeaderTimeout: 10 * time.Second,
}
// Shut the server down cleanly when the process is asked to stop.
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
}()
log.Printf("http panel listening on %s", cfg.httpAddr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}