Clear the reading panel when its last message leaves
"Mark as Spam" moved the message but left it on screen. Two causes, both fixed generally rather than for this one action. `move_job` published message.changed without a thread_id, so the WS handler invalidated the message list but never the thread the user was reading — the panel had no reason to refetch. It now names the thread. And a thread whose last visible message has gone should not linger. MailLayout closes the panel when the open thread comes back empty, whoever emptied it — core action or plugin. `move_to_special` now also publishes the move immediately rather than waiting for the IMAP job, since a plugin acting on the user's behalf has no client-side handle to update optimistically the way the core endpoint's caller does. That exposed an ordering bug: the optimistic event reaches the passive-learning hook, which would re-teach the lesson /learn had just taught. /learn now records `learned` before moving, so the hook's dedup stands down — and records it with an upsert, because provider-filed spam has no verdict row of ours to update. One real mistake on the way: the event first reported the destination folder as the source, which would have made every move look like a no-op to anything reading the pair. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SqJYt7jD7oUEkoY68sBZo
This commit is contained in:
@@ -17,6 +17,7 @@ from sqlalchemy import text
|
||||
|
||||
from app.plugins.sdk import CurrentUserId, require_csrf, sdk
|
||||
from webmail_plugin_antispam import policy, rspamd
|
||||
from webmail_plugin_antispam.hook import _remember_learned
|
||||
from webmail_plugin_antispam.schemas import (
|
||||
LearnIn,
|
||||
MessageContextOut,
|
||||
@@ -225,24 +226,21 @@ async def learn(body: LearnIn, user_id: CurrentUserId) -> dict[str, object]:
|
||||
if raw is not None:
|
||||
taught = await rspamd.learn(raw, user_key=str(user_id), spam=spam)
|
||||
|
||||
# Record BEFORE moving. The move publishes message.changed, which the passive
|
||||
# learning hook reads as "the user filed this as spam" — and it would teach
|
||||
# the very lesson we just taught. Writing `learned` first makes that hook's
|
||||
# dedup see it and stand down. An upsert, because a message the provider
|
||||
# filed as spam has no verdict row of ours to update.
|
||||
await _remember_learned(
|
||||
user_id,
|
||||
body.message_id,
|
||||
verdict=body.verdict,
|
||||
action="moved" if spam else "none",
|
||||
)
|
||||
|
||||
moved = await _SDK.actions.move_to_special(
|
||||
user_id, body.message_id, "junk" if spam else "inbox"
|
||||
)
|
||||
|
||||
async with _SDK.session(user_id) as s:
|
||||
await s.execute(
|
||||
text(
|
||||
"UPDATE plg_antispam_verdicts SET learned = :l, action = :a "
|
||||
"WHERE message_id = :m AND user_id = :u"
|
||||
),
|
||||
{
|
||||
"l": body.verdict,
|
||||
"a": "moved" if spam else "none",
|
||||
"m": str(body.message_id),
|
||||
"u": str(user_id),
|
||||
},
|
||||
)
|
||||
await s.commit()
|
||||
return {"status": "ok", "taught": taught, "moved": moved}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user