Repository navigation
feat: count mail per domain (domain pages, phases 1 and 2) - #70
Merged
Merged
Conversation
Otitodev
force-pushed
the
feat/domain-workspace-data
branch
from
November 6, 2026 07:12
16e9caf to
cc8cb9d
Compare
A domain's page has to list and count what was sent from it, and the domain was only ever inside `from_address`, a string that may carry a display name. Finding one domain's mail meant parsing every row. `emails.from_domain` stores it, with an index on project, domain and id so the newest-first listing a page needs is one index scan. It is derived and never set by hand: assigning `from_address` fills it in, via a validator, so the constructor, a test that builds an `Email` directly and a later code path all agree without each remembering to. The reduction is `domain_of`, which takes the mailbox with `parseaddr` first so a display name containing an `@` is not taken for it, and uses the domain after the last `@` because a quoted local part may hold one. An address with no `@` has no domain, and the first draft of this returned the whole string for it; the test for that case is what caught it. `domain_of` is in a module of its own, `seskit_core.addressing`, that imports nothing from SESKit. It first sat beside `bare_address` in `email.message`, and the model importing that closed a cycle (message, security, models, message) that raised an ImportError when `seskit_core.services` was imported first. No test noticed, because every test happens to import the models first, so a fresh-interpreter test now guards it. Text rather than a reference to the identity, for the reason `InboundEmail` does the same: removing a domain stops future mail, and must not erase or orphan what was already sent. The column is nullable for an address with nothing to store. The migration fills existing rows in batches by primary key, with the reduction written out in the migration rather than imported, so it keeps meaning what it meant on the day it ran. `test_migrations.py` builds the schema one revision back, writes mail the way the old code did, runs the migration and checks the result, since a column that is NULL for every old row would leave a domain's page empty until it was sent from again. This is phase 1 of plans/domain-workspace.md. Nothing reads the column yet. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Otitodev
force-pushed
the
feat/domain-workspace-data
branch
from
November 6, 2026 07:17
cc8cb9d to
b48b93d
Compare
A domain's page shows that domain's counts, rates and chart. The Overview numbers were computed for the whole project, so a domain that sends cleanly would have shown the bounce rate of a noisy neighbour. `compute_metrics` and `activity_series` take an optional `DomainScope`. With one, every count and every rate is that domain's own: the rates divide by its own sends, not the project's. Without one they behave exactly as before. Which mail belongs to a domain is one rule, in `services/domain_scope.py`. A verified domain covers its subdomains, as SES does, unless a more specific identity of the same project exists: mail from `a@mail.example.com` belongs to `mail.example.com` if that was added and to `example.com` otherwise, and nothing is counted under two domains of one project. An address identity covers that address and nothing else. Another project's domains are not looked at. The rule is written twice, in Python for a test to read and in SQL for a page to afford, and a test runs the same senders through both and requires the answers to match. The senders include lookalikes (`notexample.com`, `example.com.evil.test`) and a LIKE wildcard (`ex_mple.com` must not match `exAmple.com`), which are the ways a suffix match goes wrong. This is phase 2 of plans/domain-workspace.md. The email list and the status counts live in the route and move with the pages in phase 3. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Phases 1 and 2 of
plans/domain-workspace.md: a page per domain, so someone managing several domains can open one and see only its mail.Phase 1.
emails.from_domain, derived from the address whenever it is set, indexed by project, domain and id, and filled in for existing rows by the migration.domain_ofis in its own module because the model needs it and putting it besidebare_addressclosed an import cycle that only a fresh interpreter sees; a test now guards that.Phase 2.
compute_metricsandactivity_seriestake an optionalDomainScope, so a domain's counts, rates and chart are its own, divided by its own sends. A verified domain covers its subdomains unless a more specific identity of the same project exists; an address identity covers only itself. The rule is written in Python and in SQL, and a test requires the two to agree on lookalike domains and a LIKE wildcard.Nothing in the dashboard uses this yet. The pages are phase 3.
Checks
ruff,ruff format --check,mypy .and the tests that need no database pass locally. The SQL half of the scope rule and the scoped metrics are database tests and have not run locally either.🤖 Generated with Claude Code