A domain user's log, the message page, a domain's statistics, the level-2
count and the notifier's refusal count picked send-log rows by domain name,
so a domain deleted and re-added under its old name showed its earlier mail
to the next assignee and went on counting it.
Migration 0008 (not run on the live database yet, so extended here):
send_log.domain_id and inbound_spam_log.inbound_domain_id, REFERENCES ...
ON DELETE SET NULL, filled once by name from the registry as it is at
migration time, each with an (id, created_at) index.
- Writes: InsertQueued / InsertRejected set domain_id to the domain of the
row's login (loginDomainID) - the journal milter and the sender check's
refusal rows both go through them; the inbound journal sets
inbound_domain_id from the inbound domain's name at insert.
- Reads: SendLogFilter.DomainIDs (was Domains, by name) and
InboundSpamFilter.DomainIDs are the scope; the global role still reads
everything. The message page checks row.DomainID against the user's
domains. DomainSendStats and CountMessages keep their name argument and
select domain_id = (SELECT id FROM domains WHERE name = ?). The notifier
joins send_log.domain_id to the domain and its application.
- DMARC reports stay by name on purpose (a report is about the DNS name).
Tests: store - scope by ids; a re-added domain's log, level-2 count and
statistics hold its own row only, the global log both; inbound scope by
ids. Handlers - the new assignee gets 404 for the old row's page and does
not see it in the log, the global role sees both. Tests that wrote rows for
a login that is no application now register it (a row is a domain's through
its application). Schema at 0008 with the two columns and indexes.
schema-migrations row 8, CHANGELOG [Unreleased], audit plan line ticked,
roadmap 19/62.
Evidence: go vet ./... clean; go test ./... ok locally but for the two
Windows-only tests; the server run follows before the push.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
M7: DeleteUser counted the global users and then deleted; the handler's
demotion and CreateGlobalUser checked and then wrote. MaxOpenConns(1)
serialises statements, not pairs: two globals deleting or demoting each
other at once could leave none. Now:
- DeleteUser: a transaction that deletes first (taking the write lock) and
then requires a global left (requireAGlobal) - ErrLastGlobal rolls back.
- SaveUser: likewise for a demotion, the count inside the transaction after
the write. The handler's early count stays only to answer before hashing a
password; SaveUser decides, and ErrLastGlobal maps to the same message.
- CreateGlobalUser: INSERT ... SELECT ... WHERE NOT EXISTS (SELECT 1 FROM
users), one statement.
M9: submitUserUpdate made five writes in a row (UpdateUser, sessions,
SetUserRole, ClearUserReach with its error only logged, SetUserReach).
store.SaveUser(UserChange) writes name, hash, e-mail, role and reach in one
transaction (a global user's reach cleared); sessions end after it commits.
The create path had the same flaw - user, reach and then e-mail in three
writes - and now goes through AddUser, one transaction; CreateUser stays as
a wrapper for its many test callers. SetUserRole is gone; writeUserReach is
the reach write inside a transaction.
Tests: store - two globals demoting, and deleting, each other from two
goroutines: exactly one refused, one global left; a refused SaveUser (taken
name, a reach to no domain, nothing assigned) changes nothing; promoted to
global loses the reach; AddUser with a bad reach leaves no user; four setups
at once create one user. Handlers (the users pages, direct): the whole
change saved, back to domain with a reach; the last global not demoted and
the typed rename not written; a domain user with nothing assigned refused,
nothing changed; create with e-mail and reach.
CHANGELOG [Unreleased], audit plan lines ticked (two), roadmap 17/62.
Evidence: go vet ./... clean; go test ./internal/store ok; the users tests of
./internal/web/handlers ok; the server run follows before the push.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>