send log: rows belong to a domain by id (audit L12, 0008)

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>
This commit is contained in:
mix
2026-10-14 15:37:26 +03:00
co-authored by Claude Opus 5.5
parent e469034477
commit a618ffb0e4
19 changed files with 264 additions and 74 deletions
+8 -4
View File
@@ -94,7 +94,7 @@ func (h *Handlers) HandleDelivery(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
if !p.IsGlobal() && !domainNameSet(allowed)[row.Domain] {
if !p.IsGlobal() && !domainIDSet(allowed)[row.DomainID] {
http.NotFound(w, r)
return
}
@@ -113,7 +113,7 @@ func (h *Handlers) HandleDelivery(w http.ResponseWriter, r *http.Request) {
// The names link to their pages when there is a page to go to: a domain
// that was deleted since, or an application of another domain, is text.
for _, d := range allowed {
if d.Name != row.Domain {
if d.ID != row.DomainID {
continue
}
in.DomainHref = view.DomainHref(d.ID)
@@ -317,8 +317,10 @@ func (h *Handlers) outLogPage(r *http.Request) (*view.OutLog, error) {
allowedNames := domainNameSet(assigned)
domainNames := make([]string, 0, len(assigned))
domainIDs := make([]int64, 0, len(assigned))
for _, d := range assigned {
domainNames = append(domainNames, d.Name)
domainIDs = append(domainIDs, d.ID)
}
loginSet := make(map[string]bool)
@@ -341,8 +343,10 @@ func (h *Handlers) outLogPage(r *http.Request) (*view.OutLog, error) {
Domain: q.Get("domain"),
AppLogin: q.Get("app"),
// A global administrator reads the whole journal, including rows left
// behind by a domain that has since been deleted.
Domains: domainNames,
// behind by a domain that has since been deleted; anyone else the rows
// of the domains assigned to them, by id - not those an earlier domain
// of the same name left (audit 2026-10-12, L12).
DomainIDs: domainIDs,
AllDomains: p.IsGlobal(),
}
if !p.IsGlobal() {