Antispam: never classify during backfill

message.new fires for imported history as well as new arrivals, and EventBus
awaits hooks inline. As written, adding an account would have scored every
message in its backfill — filing years-old inbox mail into Junk on the server,
and adding ~70 ms of serialized HTTP per message to the backfill loop (twelve
minutes on a 10k-message account). It is also when the filter is least able to
judge, because Bayes has learned nothing yet.

The envelope now carries account_state, and the hook returns unless it is
"live". The backfill window belongs to the C2 learning bootstrap (§C.5), which
reads that history as training data instead of acting on it.

Tests pin the gating: backfilling accounts are not even read from the blob
store, non-inbox folders are ignored, a live inbox arrival still classifies, and
an unreachable engine still delivers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SqJYt7jD7oUEkoY68sBZo
This commit is contained in:
2026-09-17 07:59:15 +02:00
co-authored by Claude Opus 5
parent c3ae613bf4
commit dee1983b84
3 changed files with 156 additions and 3 deletions
@@ -72,6 +72,16 @@ async def on_message_new(payload: dict[str, Any]) -> None:
if env.get("mailbox_special_use") != "inbox":
return
# Never during backfill. message.new fires for every imported message too, and
# classifying history is wrong twice over: it would file years-old inbox mail
# into Junk on the server, and it would do so inline in the backfill loop —
# ~70 ms of HTTP per message, serialized, on an account with thousands of
# them. It is also when accuracy is lowest, because Bayes has learned nothing
# yet. The backfill window belongs to the C2 learning bootstrap (§C.5), not
# to classification.
if env.get("account_state") != "live":
return
settings = await policy.load_settings(user_id)
account_id = str(env.get("account_id") or "")
if not settings.covers(account_id):