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:
+15
-8
@@ -46,10 +46,11 @@ func StatsWindow(retentionDays int, entityCreated time.Time) (since time.Time, h
|
||||
}
|
||||
|
||||
// DomainSendStats returns 30-day (or shorter when retention is lower) send
|
||||
// statistics for a domain name.
|
||||
// statistics for a domain, named by its name: the rows that belong to the
|
||||
// domain registered under it now (audit 2026-10-12, L12).
|
||||
func (s *Store) DomainSendStats(name string, retentionDays int, createdAt time.Time) (SendStats, error) {
|
||||
since, hours, windowDays := StatsWindow(retentionDays, createdAt)
|
||||
stats, err := s.sendStats("domain", name, since, hours)
|
||||
stats, err := s.sendStats(domainByName, name, since, hours)
|
||||
if err != nil {
|
||||
return SendStats{}, err
|
||||
}
|
||||
@@ -61,7 +62,7 @@ func (s *Store) DomainSendStats(name string, retentionDays int, createdAt time.T
|
||||
// AppSendStats returns send statistics for an application login.
|
||||
func (s *Store) AppSendStats(login string, retentionDays int, createdAt time.Time) (SendStats, error) {
|
||||
since, hours, windowDays := StatsWindow(retentionDays, createdAt)
|
||||
stats, err := s.sendStats("app_login", login, since, hours)
|
||||
stats, err := s.sendStats("app_login = ?", login, since, hours)
|
||||
if err != nil {
|
||||
return SendStats{}, err
|
||||
}
|
||||
@@ -70,30 +71,36 @@ func (s *Store) AppSendStats(login string, retentionDays int, createdAt time.Tim
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (s *Store) sendStats(column, ref string, since time.Time, hours float64) (SendStats, error) {
|
||||
// domainByName selects a domain's send-log rows by the id of the domain
|
||||
// registered under the name it is given.
|
||||
const domainByName = "domain_id = (SELECT d.id FROM domains d WHERE d.name = ?)"
|
||||
|
||||
// sendStats counts the rows that match, a condition with one placeholder
|
||||
// taking ref.
|
||||
func (s *Store) sendStats(match, ref string, since time.Time, hours float64) (SendStats, error) {
|
||||
sinceStr := since.UTC().Format(time.RFC3339)
|
||||
|
||||
var total int64
|
||||
err := s.db.QueryRow(
|
||||
`SELECT COUNT(DISTINCT queue_id) FROM send_log
|
||||
WHERE `+column+` = ? AND status != ? AND created_at >= ?`,
|
||||
WHERE `+match+` AND status != ? AND created_at >= ?`,
|
||||
ref, StatusRejected, sinceStr,
|
||||
).Scan(&total)
|
||||
if err != nil {
|
||||
return SendStats{}, fmt.Errorf("send stats total %s %q: %w", column, ref, err)
|
||||
return SendStats{}, fmt.Errorf("send stats total %q: %w", ref, err)
|
||||
}
|
||||
|
||||
var peak int64
|
||||
err = s.db.QueryRow(
|
||||
`SELECT COALESCE(MAX(bucket_count), 0) FROM (
|
||||
SELECT COUNT(DISTINCT queue_id) AS bucket_count FROM send_log
|
||||
WHERE `+column+` = ? AND status != ? AND created_at >= ?
|
||||
WHERE `+match+` AND status != ? AND created_at >= ?
|
||||
GROUP BY substr(created_at, 1, 13)
|
||||
)`,
|
||||
ref, StatusRejected, sinceStr,
|
||||
).Scan(&peak)
|
||||
if err != nil {
|
||||
return SendStats{}, fmt.Errorf("send stats peak %s %q: %w", column, ref, err)
|
||||
return SendStats{}, fmt.Errorf("send stats peak %q: %w", ref, err)
|
||||
}
|
||||
|
||||
avg := float64(total) / hours
|
||||
|
||||
Reference in New Issue
Block a user