Commit Graph

2 Commits

Author SHA1 Message Date
mix 56a4fa892d Phase 8: level-2 differentiated rate limits (spec 7.4)
The journal-milter, until now a pure monitor, now refuses a message with a
4xx tempfail (RespTempFail/451) at MAIL FROM when a per-domain or per-
application limit is exceeded. Key is the client IP; the count is
COUNT(DISTINCT queue_id) over a sliding window reusing the send log; the
limit applies only when a non-empty IP binding matches the client (empty
binding => level-1 only, per spec 7.4). Enforcement is fail-open on the
milter's own errors — a limiter malfunction never blocks mail, and Postfix's
level-1 anvil limit stays the independent backstop. Refused messages are
recorded in send_log with status "rejected" for UI visibility.

- store/ratelimits.go: RateLimit type (+Active/AllowsIP), id-keyed get/set/
  delete for the panel, name/login-keyed lookup + windowed distinct-message
  count for the milter, DeleteRateLimitsForDomain. No migration — the
  rate_limits table has existed since Phase 2.
- milter: enforce at MailFrom, fail-open helper overLimit, InsertRejected.
- web: server-side validated IP/ceiling/window forms on the domain page and
  per application; routes POST /domains/{id}/ratelimit and
  /applications/{aid}/ratelimit. Milter reads rows live, so no reload.
- domain/app services clear limits on deletion (rate_limits has no FK cascade).

Unit tests + container e2e (p8) green: refusal on both scopes, unregistered
IP ignored, fail-open with the panel stopped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 21:57:38 +03:00
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