6ebb6f56d6
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>
58 lines
1.7 KiB
Go
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
|
|
}
|