First commit Go counter service
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
# Code review — 2026-10-14
|
||||
|
||||
Review of the first implementation of migadu_counters (see `../plans/2026-10-13-migadu-counters.md`).
|
||||
Nothing in the code was changed by the review.
|
||||
|
||||
## How it was done
|
||||
|
||||
- Two independent reviewers (agents) each read the whole repository. Neither saw the other's results.
|
||||
- **A** — correctness and runtime behaviour: [`reviewer-a-correctness.md`](reviewer-a-correctness.md)
|
||||
- **B** — security, contracts and operability: [`reviewer-b-security-contracts.md`](reviewer-b-security-contracts.md)
|
||||
- Each worked in its own scratch copy and proved findings with tests: [`_proof_tests/`](_proof_tests/README.md).
|
||||
- The coordinating agent then verified every finding (see "Verification" below), merged duplicates and ranked them.
|
||||
18 distinct issues remain; 4 of them were found by both reviewers.
|
||||
|
||||
## Findings
|
||||
|
||||
Who found it: A / B / both, with the reviewer's own IDs for traceability.
|
||||
|
||||
### High
|
||||
|
||||
**1. If mailqueuer disconnects or times out before the reply, the count is lost** — A (F1)
|
||||
- `internal/server/server.go:156` runs the Redis write with the request context. When the client hangs up, Go cancels
|
||||
the context and go-redis aborts before sending MULTI.
|
||||
- Re-run during verification: 20 of 20 delayed requests and 300 of 300 immediate ones were not counted. The most
|
||||
dangerous finding, because fire-and-forget clients often close early.
|
||||
- Fix: run the counting under `context.WithoutCancel(r.Context())` with its own time budget.
|
||||
|
||||
**2. A bad value in a field that isn't counted rejects the whole request** — both (B F1, A F3)
|
||||
- `internal/api/validate.go` strictly validates `mail_from` although it only feeds the exempt check; a 400 there means
|
||||
the recipient is never counted.
|
||||
- Senders trigger it easily: mizu's SMTP parser turns `"john smith"@x.com` into `john smith@x.com`, mizu runs with
|
||||
SMTPUTF8 (`info@bücher.de`), address literals and trailing dots pass, and mailqueuer forwards `X-Mail-From` unchecked.
|
||||
- On `/out`, an unusual external recipient (`x@[192.0.2.1]`, `x@host_name.example.com`) gives 400 and the sender's
|
||||
outgoing count is lost too.
|
||||
- Fix: never reject because of `mail_from`; validate sender and recipient sides separately and count whichever is
|
||||
valid; convert UTF-8 domains to punycode.
|
||||
|
||||
### Medium
|
||||
|
||||
**3. Support/token logins aren't counted** — A (F2)
|
||||
- mizu puts the raw SASL login into `X-Auth-User` (`pkg/smtp/auth_session.go:160` → `poster.go:186`), so master logins
|
||||
arrive as `user@dom@SUFFIX` and fail validation with 400.
|
||||
- Fix: strip the second `@SUFFIX` and count the base address.
|
||||
|
||||
**4. Redis failures with an unclear outcome lose the limit notification** — both (B F3, A F4)
|
||||
- If Redis applies MULTI/EXEC but the reply times out, or one command inside EXEC fails (a key holding a non-number)
|
||||
while the others succeed, the service answers 503 and logs "nothing counted" — but the counters moved.
|
||||
- If that increment reached the limit, no later increment can reach it again: no notification that day.
|
||||
- Fix: use each command's own result; after a timeout re-read the keys from the master; log "outcome unknown".
|
||||
|
||||
**5. Huge CDB records are re-parsed on every request** — A (F5)
|
||||
- `boxmailhere.com` (193 KB, 9,798 overrides): about 0.9 ms and 1.4 MB per lookup, 2.2 ms and 1.6 MB per request
|
||||
(re-measured).
|
||||
- Fix: cache parsed records per loaded CDB.
|
||||
|
||||
**6. An empty `auth_token` leaves the API open with only a warning** — B (F4)
|
||||
- A typo such as `auth-token =` starts the daemon unauthenticated; anyone who can reach it can inflate any customer's
|
||||
counters, which triggers the web app's limit actions.
|
||||
- Fix: make an empty token fatal unless explicitly opted out; make unknown config keys errors.
|
||||
|
||||
**7. Plus-addressed recipients get their own counters** — B (F2); A notes the Lua plugin did the same
|
||||
- Mail to `b+1@…` creates `cnt/A/b+1@…`; the mailbox's own counter (the one the web app charts) never moves. If such
|
||||
a counter reaches its limit, the web app answers "mailbox not found".
|
||||
- rcptd strips the `+tag`, so stripping it here too seems right. **Needs a product decision.**
|
||||
|
||||
**8. One malformed CDB row breaks counting for anything touching that domain** — both (B F5, A F6)
|
||||
- A non-numeric org id gives 503 and drops the other side's valid count; the plan says such rows are skipped.
|
||||
- Fix: treat the row as not hosted and log a warning.
|
||||
|
||||
### Low
|
||||
|
||||
9. **Notification retries can send customers duplicate emails** — B (F11), A (F9). Resolved disagreement: the
|
||||
organisation handler has a once-per-day guard (`migadu_web core/accounts.ex:1928-1956`), the mailbox handler
|
||||
(`core/email.ex:3572-3584`) emails on every call, so a retry after a slow-but-successful call means a second
|
||||
email. Also: shutdown cuts off pending retries, retries keep their concurrency slot while waiting, and the
|
||||
"after 5 attempts" log line can be wrong.
|
||||
10. **A SIGHUP during startup kills the daemon** — A (F8). The signal handler is installed only after the Redis
|
||||
ping, which can take up to 2 s.
|
||||
11. **Rejected requests leave no trace** — both (B F9, A F2 note). Nothing in the counting log, no trace_id in the app
|
||||
log; together with #2 the lost counts are invisible.
|
||||
12. **Override keys aren't lowercased** — A (F7). `Info@/o=5` would be ignored; none in today's data.
|
||||
13. **Credentials in the notify URL are written to the log** — B (F7); plain `http://` with basic auth isn't warned about.
|
||||
14. **Error responses expose internals** — B (F10): internal Redis addresses and paths; `admin_addr` defaults to all
|
||||
interfaces without auth.
|
||||
15. **Sentinel mode starts without a Redis password** — B (F12): every write then fails; only `/health` and metrics show it.
|
||||
16. **counterctl warns one day too early** — B (F6): a date exactly 31 days back is reported as expired.
|
||||
17. **A recovered panic isn't counted in `requests_total`** — B (F8).
|
||||
18. **Work keeps running after a handler timeout** — A (F10, not reproduced): not tracked by shutdown, a late panic is
|
||||
silently lost. Largely covered by the fix for #1.
|
||||
|
||||
**Test gaps** — B (F13): the suite misses most of the above; `TestValidation_JSONAndFields` asserts the 400 behind #2
|
||||
as correct; watcher tests can occasionally pass on a stale signal.
|
||||
|
||||
## Verification
|
||||
|
||||
Done by the coordinating agent after both reviews:
|
||||
|
||||
- Both scratch copies were checked to have source files identical to the repo, so the proof tests ran against the
|
||||
reviewed code. B's tests fail as described; A's demonstration tests reproduce their evidence (#1 was worse on
|
||||
re-run: 0 counted instead of 21 of 300).
|
||||
- External facts checked in the sibling repos:
|
||||
- mailqueuer reads `X-Mail-From` without validating it (`internal/handler/ingest.go`).
|
||||
- mizu's go-smtp fork unquotes quoted local parts and copies the domain verbatim (`parse.go` `parseMailbox`,
|
||||
`parseLocalPart`); mizu sets `EnableSMTPUTF8 = true` (`cmd/mizu-server/main.go:1363`).
|
||||
- mizu stores the raw SASL username as the authenticated user (`pkg/smtp/auth_session.go:160`) and sends it as
|
||||
`X-Auth-User`; `user@domain@SUFFIX` logins are documented in mizu's `CLAUDE.md`.
|
||||
- Web app: org-level limit handlers are guarded once per day, the mailbox-level ones are not (see #9).
|
||||
- Reviewer disagreements: #7 (A: Lua parity, B: defect) is left as a product decision; #9 settled as above.
|
||||
|
||||
## Suggested order
|
||||
|
||||
Fix #1–4, #6, #8 and #10 first (small, they protect the counts), then #5 and #7 (#7 after the decision), then the
|
||||
low items. Turn each proof test into a regression test as its finding is fixed.
|
||||
Reference in New Issue
Block a user