Comments only, no behaviour changed. go-milter v0.4.1 (session.go: one
session per connection, replaced after a non-continue reply; macros per
stage), Postfix 3.7.11 (milter8.c, smtpd_check.c
reject_auth_sender_login_mismatch(), mail_addr_find.c, showq_compat.c,
mail_conf_time.c, mail_params.h), Cyrus SASL 2.1.28 (saslpasswd.c,
plugins/sasldb.c: deleting a missing login is not an error), OpenDKIM
2.11.0~beta2 (opendkim-db.c, opendkim-genkey), Berkeley DB 5.3 db_dump,
rspamd 3.11.0. Comments that claimed what the source denies are corrected
(sasldb keeps the password as given, not hashed; an embedded newline is
kept, not truncated; rspamd sorts symbols by absolute weight); fixtures say
seen or composed. The sender check's From: parsing gets no citations: it is
replaced by the port of OpenDKIM's dkim_mail_parse next.
Contradictions left for their own commits: B-G5 cleanAddress, B-PS1 first
key wins, B-PQ1 the forced-expire flag, B-PQ10/PQ11 Postfix's time units.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Postfix's reject_sender_login_mismatch compares the SASL login with the
sender map without regard to case - seen on the e2e stand this time (a
one-off probe, not committed: map rewritten to "APP1", client authenticated
as "app1", RCPT accepted). The panel stored logins as typed under a BINARY
UNIQUE, so "Ops" and "ops" were two applications to the panel and one sender
to Postfix: a tenant could send with another tenant's envelope sender.
- app.foldLogin: Service.Create lower-cases the login (owner: fold, do not
refuse); CreateWithSettings rolls back the stored (folded) login.
- ImportApplication keeps the login as written: folding it would rename the
SASL account and break the imported application's working credentials;
the index below refuses a case clash there too.
- Migration 0008 (DDL): CREATE UNIQUE INDEX idx_applications_login_nocase ON
applications (login COLLATE NOCASE). The column's own BINARY UNIQUE stays
(SQLite cannot change a collation without a table rebuild, which a
migration transaction cannot do with foreign keys on). Checked on the live
installation first: 4 logins, no case clash; "Quieter3C" keeps its case.
0008 is not run anywhere yet; send_log.domain_id joins it later in stage 2.
Tests: "Ops" created as ops, SASL account and map binding in lower case,
OPS in another domain and an imported oPs refused; the store refuses ops
beside Ops; schema at user_version 8 with the index.
CHANGELOG [Unreleased], audit plan line ticked, roadmap 15/61.
Evidence on the test server for this tree (with the probe): go vet clean,
go test ./... 23 packages ok, make e2e exit 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage 3, step 2. Reviewed by Fable (read-only), fixed by Sonnet with a
failing test before each fix, checked by Opus. No Critical or High
finding: route authorization, tenant isolation by id, the /server/
subtree guard, output encoding and the origin check were found sound.
Fixed
- M1: a session was keyed by user name and had no link to the user. A
deleted or renamed user's cookie stayed alive (renewed on every hit,
before the user was looked up) and became a session of whoever was
later created under that name, with that account's role. The
baseline now has sessions.user_id REFERENCES users(id) ON DELETE
CASCADE; the principal is resolved by id and a session is renewed
only after its user was found. RenameSession is gone.
- M2: any user's password change deleted every other user's sessions.
It ends that user's other sessions only.
- L1: a password set on the user form ends that user's sessions (an
administrator editing their own account keeps the current one).
- L2: store, saslpasswd2 and Postfix reload errors were shown verbatim
on the application form, the inbound upstream / recipients forms and
the limit recalculation - pages a domain administrator reaches.
Validation errors (app.ValidationError, inbound.ValidationError,
store.ErrNotAutoRateLimit) keep their text; anything else is logged
and shown as "Could not save ... Check the logs and try again."
- L3: store.ListDMARCReports treated an empty domain list as "all". It
takes a scope now (AllDomains or a list; an empty list is no rows),
and the DMARC hub is open to the domain role as the plan says: the
reports of the assigned domains, without the instance-wide ingest
box. The menu entry was already shown to them and led to a 404.
- I1: /static/ answered with a listing of the embedded assets. A path
that is not a known asset is a 404.
Consequences
- The global DMARC hub lists reports of domains that were deleted
since, too.
- dev/migrate1x (not in the tree) no longer carries sessions over:
everyone signs in again after the transfer.
- docs/schema-migrations.md: the sessions row.
Recorded, not changed - for the owner:
- L4: the report-address select names the followed user and their
address to every administrator of the domain, by default the global
administrator's login name. The plan asks for it.
- L5: a delegated inbound upstream may be any host and port, loopback
and private ranges included - within the trusted-operator model; to
be listed among the accepted risks in security.md.
- L6: Account's profile is saved without the password; becomes a
requirement once password-reset mails to users.email.
- Still echoing an error's text: backup and domain import (global
only), the domain form's validation messages.
Not checked by the review: anything needing the container (e2e, real
Postfix / DNS), CSP behaviour in a live browser.
go test -count=1 ./internal/web/... ./internal/store/... ./internal/app/...
./internal/inbound/... ./internal/dmarc/... ./cmd/panel
ok (all eleven packages; dev/migrate1x also ok)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bounce could be recorded as a successful delivery. The log-tailer's
Fixed:
- A bounce could be recorded as a successful delivery. The log-tailer's
- Log-tailer resumes where it stopped instead of jumping to end-of-file on
- Level-2 rate limit no longer overshoots under concurrency: messages that
Changed:
- The project has a single public home: `github.com/mixeme/selfpost`. Codeberg
- Code comments no longer cite the archived specification. References like
- `docs/code-review.md` is gone. Its plan is finished — phases 0 (bar the
- `docs/architecture.md` gained a *Code layers* section: a diagram of
- Phase 1 doc/code hygiene (`docs/code-review.md`): removed ~30 stale
… (1 more in CHANGELOG)
Security:
- Pre-release security review (plan § D, model Fable, 2026-08-06): full pass
- `saslpasswd2` argv: the application login is now passed after a `--`
Added:
- Optional password encryption for the two secret-bearing downloads (plan
- Domain import accepts an encrypted export: the envelope is detected by its
- `selfpost-backup` writes encrypted archives and reads them back:
- `TestParseDelivery` covers the exotic mail.log shapes the review asked for
- docs: README *Encrypting a backup or export*; `docs/security.md` §
… (14 more in CHANGELOG)
Removed:
- docs: `security.md` — accepted risk «restore old backup revives session rows»
Changed:
- docs: `implementation-plan.md` trimmed to the sole open v1.x gate — pre-release
- docs: `architecture.md` — sessions (SQLite, idle renew, password change),
- docs: `development.md` — expanded e2e stack and `release.yml` matrix workflow.
- docs: `security.md` — accepted risk for send-log rows stuck at `queued` after
- docs: `roadmap.md` — optional send-log / `mail.log` follow-ups under v1.x tail.
… (10 more in CHANGELOG)
Full detail: CHANGELOG.md (0.5.0).
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).