Commit Graph
10 Commits
Author SHA1 Message Date
mixandClaude Opus 5 23d52bab7c fix: port-25 milter chain, DMARC report validation, auto rate-limit window
test / test (push) Has been cancelled
Review pass over the 1.4.0-1.9.1 work.

Port 25 no longer inherits main.cf's submission milter chain: smtpd_milters
is always overridden on smtp/inet, to the antispam milter or to nothing.
Without that, OpenDKIM (strict, tempfail) gated relayed mail during a signing
outage, and the journal-milter filed inbound mail into the outbound send log
under the sender's domain -- so Deliveries showed other people's mail and a
forged From: consumed that domain's level-2 budget. architecture.md already
described the intended behaviour; the code now matches.

DMARC reports are checked before they are stored: the policy domain must be a
configured sending domain, and must match the +tag of a per-domain hosted
address. The rua address form is predictable, so anyone able to reach it could
file reports under any domain and evict genuine ones through the retention cap.
Zip attachments are now decompressed as well as gzip.

The auto level-2 ceiling is scaled to the level-1 window before the multiplier
is applied; any window other than 3600s shifted the allowance by window/3600.

Migration 0010 indexes send_log(app_login, created_at) -- both the per-MAIL
FROM rate-limit count and the per-application statistics were scanning the
created_at range.

e2e now regenerates the Postfix config in-container for all four port-25 flag
combinations and requires postfix check to pass; the 1.9.1 crash-loop shipped
in 1.4.0 and survived three releases because only the flags-off path was
exercised.

Migration 0009 stays as shipped -- its conversion branch only fires for a
database carrying application-scope allowed_ips from before 1.9.0. The schema
reference notes that the old list was permissive and the new column is
restrictive; the guide documents the allow-list as a sending control on a
fail-open path, not an authentication boundary.

Also: gofmt across cmd/ and internal/, unused duplicate validators dropped from
internal/web/validate, export no longer swallows store errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 14:17:39 +03:00
mixandCursor b4a9b93cf2 release: 1.9.0
test / test (push) Has been cancelled
Application client IP allow-list restricts which addresses may submit as a SASL login; level-2 rate limits override the domain ceiling per application (higher or lower, capped at L1). Migration 0009, authips form, milter enforcement, export/import, and operator docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 23:51:41 +03:00
mixandCursor 27aeadc71d release: 1.7.0
test / test (push) Has been cancelled
Receive DMARC aggregate reports on port 25 and show parsed summaries in the panel. Close Unreleased; pin compose and docs to 1.7.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 22:53:36 +03:00
mixandCursor c1ec4fbd79 release: 1.6.0
test / test (push) Has been cancelled
Add 30-day send statistics and auto level-2 rate limits on the domain page. Close Unreleased; pin compose and docs to 1.6.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 22:26:51 +03:00
mixandCursor 0d98d92642 Add optional inbound relay (backup-MX) behind INBOUND_RELAY_ENABLE.
test / test (push) Has been cancelled
Port 25 accepts only configured domains and listed recipients, then forwards to an upstream; the outbound path is unchanged when the flag is off.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 23:17:30 +03:00
mixandCursor 15baa1e5d0 feat(panel): add domain-admin role with per-domain authorization
test / test (push) Has been cancelled
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 23:43:59 +03:00
mixandCursor efaf016c5f Fix DMARC guidance for send-only relays with optional rua= settings.
test / test (push) Has been cancelled
The panel now suggests policy-only DMARC by default, lets operators configure a default and per-domain report address, and DNS-checks hub _report._dmarc records. Future in-panel report ingestion is tracked as dmarc-reports in the roadmap.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 22:47:34 +03:00
mixandClaude Opus 5 a92d583053 feat: log-tailer offset persistence + in-flight L2 rate-limit accounting (code-review.md § Phase 3)
- logtail: persist the read position (offset + fingerprint of the log's
  first 512 bytes) in a new logtail_state table (migration 0003) and
  resume from it on start, so delivery lines written while the panel was
  down are parsed instead of skipped and their send-log rows no longer
  stay "queued" forever. Fingerprint mismatch (rotated/recreated while
  down) reads the file from the start — re-parsing is idempotent; a
  first-ever start with nothing stored still begins at end-of-file.
  Writes are throttled to one per 5s, forced on rotation and shutdown.

- milter: count messages that passed the level-2 check but have not
  reached the send log yet (internal/milter/inflight.go), so concurrent
  SMTP sessions cannot each spend the same last slot. A literal
  count+insert transaction, as the review suggested, is not possible:
  the count happens at MAIL FROM and the insert at end-of-message.
  Reservations are released after the insert, on ABORT, and after a
  10-minute TTL — a client that drops mid-transaction must not be able
  to hold a slot, since the limiter is fail-open by design.

Docs: architecture.md (log tailer, persistence, L2 counting),
security.md and roadmap.md (restart gap closed, container recreate
remains), CHANGELOG, progress.md, code-review.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 17:12:37 +03:00
mixandClaude Sonnet 5 538a4b6603 feat: implement B.1 — persist login sessions in SQLite with sliding idle timeout
Sessions move from an in-memory map (absolute 12h TTL) to a `sessions`
table (migration 0002), storing only the SHA-256 of the token. Expiry is
now a sliding idle window (PANEL_SESSION_IDLE_DAYS, default 7, no
absolute cap), extended at most once an hour and never by the
monitoring screens' background polling (GET + HX-Request), so a
forgotten open tab doesn't keep a session alive indefinitely. A login
now survives a container restart or redeploy.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 23:21:49 +03:00
mixandClaude Opus 4.8 d72a383904 Phase 2: SQLite persistence, admin setup-link, login/sessions
Implements the secure single-admin panel entry (spec 7.6).

- internal/store: modernc.org/sqlite (pure Go, static build), WAL +
  foreign keys, embedded PRAGMA user_version migrations; schema 0001
  covers admin/settings/domains/applications/send_log/rate_limits (spec 9).
- Setup secret-link (spec 7.6.1): 128-bit crypto/rand token, printed to
  log + /data/setup-token (0600), 10-min TTL with regeneration, per-IP
  rate limit, subtle.ConstantTimeCompare, failures don't invalidate,
  one-time admin form, permanent invalidation once admin exists (/setup 404).
- bcrypt admin password; server-side username/password validation.
- Login + in-memory sessions, crypto-random token, cookie
  HttpOnly/Secure/SameSite (Secure toggleable for dev HTTP), login
  rate limit, auth middleware.
- html/template base layout + setup/login/dashboard, vendored htmx 2.0.4.
- build/entrypoint.sh: fix bind-mounted /data ownership as root before
  supervisord drops to the unprivileged panel user (found via container test).

Verified on selfpost.example.com: go vet/build/test/gofmt clean; e2e curl
of setup+login flows; docker build + run with -v ./data:/data creates the
DB and 0600 token owned by panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:24:09 +03:00