M4: Wire FilterEngine into live sync so matched rules actually fire #222

Merged
KofTwentyTwo merged 3 commits from story/m4-wire-filter-engine into dev 2026-07-30 00:11:37 +00:00
3 Commits
Author SHA1 Message Date
James Maes 782cc47921 merge dev: bring in real JMAP client, keep filter-engine wiring
Resolves the sync.rs conflict between M3 (real JmapEngine::with_credentials
wiring, wiremock-backed sync tests) and M4 (FilterEngine threaded through
sync, apply_filter_actions, is_new_message fire-once guard): keeps dev's
JMAP wiremock stub counts/assertions and this branch's FilterEngine
parameters and new filter E2E tests side by side.
2026-07-29 19:09:50 -05:00
James Maes 766ca42a13 fix(filters): fire filter actions exactly once per new message
Sync is not incremental yet, so every re-sync re-fetches and
re-persists messages the backend still reports. Applying filter
actions unconditionally on every persisted message meant a rule kept
re-firing on every sync cycle for a message that arrived once,
enqueueing a fresh outbox mutation and execution log entry each time.

Guard fetch_and_persist so it only routes a message through
apply_filter_actions when the message did not already exist in the
store before this sync pass (DatabaseError::is_not_found, checked
before save_email's INSERT OR REPLACE erases the distinction). Also
makes the immediate-action match in apply_filter_actions exhaustive
over the remote action variants instead of a wildcard, so a future
immediate-style action can't silently fall through to remote routing.
2026-07-29 19:01:42 -05:00
James Maes 2381614d00 feat(filters): wire FilterEngine into live inbound sync
Every synced message is now evaluated against the daemon's live
FilterEngine after it is persisted. MARK READ/UNREAD apply immediately
via set_message_read; MOVE/COPY/FLAG/UNFLAG/DELETE/FORWARD/CALL WEBHOOK
enqueue a real PendingRemoteMutation through the existing outbox for
the background worker to drain. Every match appends a
FilterExecutionLog entry, so a rule match now produces a genuine,
persisted, observable side effect instead of nothing.

The evaluate-and-route logic lives in a standalone apply_filter_actions
helper (not inlined into fetch_and_persist) so other sync paths can
reuse the exact same routing instead of re-deriving it.
2026-07-29 18:49:56 -05:00