Files

7.4 KiB
Raw Permalink Blame History

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.
  • Each worked in its own scratch copy and proved findings with tests: _proof_tests/.
  • 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

  1. 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.
  2. 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.
  3. 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.
  4. Override keys aren't lowercased — A (F7). Info@/o=5 would be ignored; none in today's data.
  5. Credentials in the notify URL are written to the log — B (F7); plain http:// with basic auth isn't warned about.
  6. Error responses expose internals — B (F10): internal Redis addresses and paths; admin_addr defaults to all interfaces without auth.
  7. Sentinel mode starts without a Redis password — B (F12): every write then fails; only /health and metrics show it.
  8. counterctl warns one day too early — B (F6): a date exactly 31 days back is reported as expired.
  9. A recovered panic isn't counted in requests_total — B (F8).
  10. 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.