Antispam: online checks no longer switch off Bayes; replies teach ham

- The "online reputation checks" rspamd profile used groups_enabled,
  which means ONLY those groups: with the switch on, a message got the
  RBL lookups alone (1 rule instead of 19, score 0.00) and Bayes never
  ran, so every spam lesson was ignored. It now disables nothing.
- A reply or reply-all teaches the answered message as ham (not
  forwards or new mail, not one's own Sent/Drafts, never over an
  explicit spam mark; once per message). message.sent now carries the
  draft's kind and reply_to_message_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 10:14:05 +02:00
co-authored by Claude Opus 5.5
parent 1aa0673720
commit b122276958
5 changed files with 97 additions and 2 deletions
+6
View File
@@ -338,6 +338,12 @@ async def send_job(ctx: dict[str, Any], outbox_id: str) -> None:
# need to read the outbox row. payload to/cc are "Name <email>" strings.
"to": _addr_dicts(draft.payload.get("to")),
"cc": _addr_dicts(draft.payload.get("cc")),
# What it answers: "reply" / "reply_all" / "forward" / "new", and the
# message (a reply says the user wanted that mail — antispam learns it).
"kind": draft.kind,
"reply_to_message_id": (
str(draft.reply_to_message_id) if draft.reply_to_message_id else None
),
},
)
await bus.publish(
@@ -541,3 +541,50 @@ async def test_done_accounts_are_skipped_unless_forced(monkeypatch: pytest.Monke
await jobs.bootstrap_account({}, user_id=str(uuid.uuid4()), account_id=account, force=True)
assert ran == [True] # the button re-runs it
assert kv.written == [] # already listed: not appended twice
def _sent(kind: str = "reply", replied: str | None = "m") -> dict[str, Any]:
return {
"user_id": str(uuid.uuid4()),
"kind": kind,
"reply_to_message_id": str(uuid.uuid4()) if replied else None,
}
async def test_a_reply_teaches_ham(wired) -> None: # type: ignore[no-untyped-def]
fake = wired()
await hook.on_message_sent(_sent("reply_all"))
assert fake.taught == [False]
assert fake.recorded == [{"verdict": "ham", "action": "none"}]
assert fake.published[0][1]["verdict"] == "ham"
@pytest.mark.parametrize("kind", ["forward", "new"])
async def test_forwards_and_new_mail_teach_nothing(wired, kind) -> None: # type: ignore[no-untyped-def]
fake = wired()
await hook.on_message_sent(_sent(kind))
assert fake.taught == []
@pytest.mark.parametrize("learned", ["ham", "spam"])
async def test_a_reply_does_not_overrule_or_repeat_a_lesson(wired, learned) -> None: # type: ignore[no-untyped-def]
fake = wired(learned=learned)
await hook.on_message_sent(_sent())
assert fake.taught == []
async def test_a_reply_to_ones_own_message_teaches_nothing(wired) -> None: # type: ignore[no-untyped-def]
fake = wired()
async def _own(*_a: object, **_k: object) -> dict[str, Any]:
return {"account_id": "acc-1", "mailbox_special_use": "sent"}
fake.messages.envelope = _own
await hook.on_message_sent(_sent())
assert fake.taught == []
async def test_a_reply_while_the_engine_is_down_records_nothing(wired) -> None: # type: ignore[no-untyped-def]
fake = wired(engine_up=False)
await hook.on_message_sent(_sent())
assert fake.taught == [False] and fake.recorded == []