fix(email-ingress): enqueue message.posted for LC-75 subscribers (LC-205) #268

Merged
longjacksonle merged 1 commit from fix/lc-205-email-ingress-message-posted into main 2026-05-29 20:38:23 +02:00

Summary

finalize_email_inbox_message_send (LC-77 per-room email ingress) never enqueued the message.posted outgoing-webhook event. So every LC-75 outgoing-webhook subscriber — including the bridge daemons LC-78 builds toward — silently missed every email-ingress message: no error, no log, no signal. The outgoing_actor email_inbox arm was already wired but unreachable because the enqueue call was never added at this path.

~11-line fix: add the enqueue, mirroring the webhook path exactly.

Verification (done before the fix)

Read all four finalize paths first. The three working ones agree on all axes, so the email path is an unambiguous missing sibling:

Path enqueue site actor arm ordering conditional?
finalize_message_send (web) after broadcast user after broadcast_room_message no
finalize_webhook_message_send (LC-74) after broadcast webhook after broadcast no
finalize_bridge_message_send (LC-78) after broadcast bridge after broadcast no
finalize_email_inbox_message_send MISSING (email arm exists, unreachable)

No inconsistency between the working paths — nothing to file separately. The email path is the closest sibling to webhook (empty user_id, synthetic actor, non-DM only, identical mention-reconcile structure), so the fix mirrors webhook verbatim, differing only in the outgoing_actor arm.

Cross-checked outgoing_actor's signature before commit: (user_id, webhook_id, email_inbox_id, bridge_id, bridge_foreign_name)email_inbox_id is the 3rd slot, so outgoing_actor("", None, Some(email_inbox_id), None, None) hits the email_inbox arm.

The fix

In finalize_email_inbox_message_send, immediately after broadcast_room_message (exactly where webhook's enqueue sits relative to its broadcast):

crate::outgoing::enqueue(
    &state.chat,
    "message.posted",
    room.id,
    serde_json::json!({
        "message_id": new_id,
        "email_inbox_id": email_inbox_id,
        "author": inbox_name,
        "body": body,
        "actor": super::outgoing_actor("", None, Some(email_inbox_id), None, None),
    }),
)
.await;

Tests (outgoing_webhook_email_ingress.rs, mirrors LC-78 outgoing_webhook_actor_payload)

Two independent tests (split positive/negative for sharp diagnosis):

  1. email_ingress_post_fires_message_posted_with_email_inbox_actor — drives the real post path (email_ingress::actor::post_email_message), reads the enqueued delivery row, asserts: event == "message.posted", actor.kind == "email_inbox" AND actor.email_inbox_id == inbox_id (specific variant + id, not is_some()), body matches, room_id matches.
  2. non_email_post_in_same_room_uses_user_actor_not_email_inbox — anti-over-fire guard: a normal user post in the same room yields actor.kind == "user", proving the email-inbox actor is selected by the path, not blanket-stamped on every message.posted.

Docs

docs/protocol-bridges.md: removed the "Email-ingress does not fire LC-75 outgoing webhooks" Known-gaps bullet (closed) and dropped the conditional caveat on the email-ingress actor-shape line.

Out of scope (confirmed)

No other finalize path is missing the enqueue (only email) → no grep-ban enforcement (N=1 isn't a pattern; revisit only if a third offender appears). No outgoing_actor refactor. No broader email-ingress flow expansion (covered by LC-77 tests).

Test plan

  • cargo check --tests clean, standalone + saas.
  • New tests 2/2 pass; adjacent paths (outgoing_webhook_actor_payload, outgoing_webhooks, email_ingress_process) all green.
  • Full just test (--no-fail-fast): 125 binaries, only the pre-existing lc77_webhook_render_fixture failures (4 tests, confirmed failing on clean main — drifted golden fixtures, unrelated to this change, filed separately).

Pre-existing finding (unchanged by this PR, still open)

lc77_webhook_render_fixture's 4 golden-fixture tests fail on clean main — rendered HTML diverged from committed fixtures. Needs FIXTURE_WRITE=1 regeneration under its own ticket. Not touched here.

## Summary `finalize_email_inbox_message_send` (LC-77 per-room email ingress) never enqueued the `message.posted` outgoing-webhook event. So every LC-75 outgoing-webhook subscriber — including the bridge daemons LC-78 builds toward — silently missed every email-ingress message: no error, no log, no signal. The `outgoing_actor` `email_inbox` arm was already wired but unreachable because the enqueue call was never added at this path. ~11-line fix: add the enqueue, mirroring the webhook path exactly. ## Verification (done before the fix) Read all four finalize paths first. The three working ones agree on all axes, so the email path is an unambiguous missing sibling: | Path | enqueue site | actor arm | ordering | conditional? | |---|---|---|---|---| | `finalize_message_send` (web) | after broadcast | `user` | after `broadcast_room_message` | no | | `finalize_webhook_message_send` (LC-74) | after broadcast | `webhook` | after broadcast | no | | `finalize_bridge_message_send` (LC-78) | after broadcast | `bridge` | after broadcast | no | | `finalize_email_inbox_message_send` | **MISSING** | (email arm exists, unreachable) | — | — | **No inconsistency between the working paths** — nothing to file separately. The email path is the closest sibling to webhook (empty `user_id`, synthetic actor, non-DM only, identical mention-reconcile structure), so the fix mirrors webhook verbatim, differing only in the `outgoing_actor` arm. Cross-checked `outgoing_actor`'s signature before commit: `(user_id, webhook_id, email_inbox_id, bridge_id, bridge_foreign_name)` — `email_inbox_id` is the 3rd slot, so `outgoing_actor("", None, Some(email_inbox_id), None, None)` hits the `email_inbox` arm. ## The fix In `finalize_email_inbox_message_send`, immediately after `broadcast_room_message` (exactly where webhook's enqueue sits relative to its broadcast): ```rust crate::outgoing::enqueue( &state.chat, "message.posted", room.id, serde_json::json!({ "message_id": new_id, "email_inbox_id": email_inbox_id, "author": inbox_name, "body": body, "actor": super::outgoing_actor("", None, Some(email_inbox_id), None, None), }), ) .await; ``` ## Tests (`outgoing_webhook_email_ingress.rs`, mirrors LC-78 `outgoing_webhook_actor_payload`) Two independent tests (split positive/negative for sharp diagnosis): 1. **`email_ingress_post_fires_message_posted_with_email_inbox_actor`** — drives the real post path (`email_ingress::actor::post_email_message`), reads the enqueued delivery row, asserts: `event == "message.posted"`, `actor.kind == "email_inbox"` AND `actor.email_inbox_id == inbox_id` (specific variant + id, not `is_some()`), `body` matches, `room_id` matches. 2. **`non_email_post_in_same_room_uses_user_actor_not_email_inbox`** — anti-over-fire guard: a normal user post in the same room yields `actor.kind == "user"`, proving the email-inbox actor is selected by the *path*, not blanket-stamped on every `message.posted`. ## Docs `docs/protocol-bridges.md`: removed the "Email-ingress does not fire LC-75 outgoing webhooks" Known-gaps bullet (closed) and dropped the conditional caveat on the email-ingress actor-shape line. ## Out of scope (confirmed) No other finalize path is missing the enqueue (only email) → no grep-ban enforcement (N=1 isn't a pattern; revisit only if a third offender appears). No `outgoing_actor` refactor. No broader email-ingress flow expansion (covered by LC-77 tests). ## Test plan - [x] `cargo check --tests` clean, standalone + saas. - [x] New tests 2/2 pass; adjacent paths (`outgoing_webhook_actor_payload`, `outgoing_webhooks`, `email_ingress_process`) all green. - [x] Full `just test` (`--no-fail-fast`): 125 binaries, only the pre-existing `lc77_webhook_render_fixture` failures (4 tests, confirmed failing on clean `main` — drifted golden fixtures, unrelated to this change, filed separately). ## Pre-existing finding (unchanged by this PR, still open) `lc77_webhook_render_fixture`'s 4 golden-fixture tests fail on clean `main` — rendered HTML diverged from committed fixtures. Needs `FIXTURE_WRITE=1` regeneration under its own ticket. Not touched here.
fix(email-ingress): enqueue message.posted so LC-75 subscribers see it (LC-205)
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 4m55s
check-secrets / TruffleHog (push) Successful in 4s
check-secrets / Nosey parker (push) Successful in 5s
check-secrets / Kingfisher (push) Successful in 6s
Create release / Create release from merged PR (pull_request) Has been skipped
check-secrets / Nosey parker (pull_request) Successful in 7s
check-secrets / TruffleHog (pull_request) Successful in 8s
check-secrets / Kingfisher (pull_request) Successful in 9s
04f034f4ce
finalize_email_inbox_message_send broadcast to WS and fanned mentions but never enqueued the message.posted outgoing-webhook event, so every LC-75 subscriber (bridge daemons, etc.) silently missed every email-ingress message - no error, no log. The outgoing_actor email_inbox arm was already wired but unreachable because the enqueue call was never added at this path.

Mirrors finalize_webhook_message_send exactly (the closest sibling: empty user_id, synthetic actor, non-DM, identical mention-reconcile structure): enqueue fires after broadcast_room_message, routes through outgoing_actor's email_inbox arm. The three working finalize paths (web/webhook/bridge) agree on ordering (after broadcast), payload shape, and unconditionality; no inconsistency surfaced, so this is the minimal sibling addition, not a finalize-path audit.

Test (outgoing_webhook_email_ingress.rs, mirrors the LC-78 outgoing_webhook_actor_payload shape): a positive test drives the real post path (email_ingress::actor::post_email_message) and asserts message.posted fires with actor.kind=email_inbox + matching email_inbox_id, body, and room_id; a separate negative test posts a normal user message in the same room and asserts actor.kind=user, proving the email-inbox actor is path-selected, not blanket-stamped.

Closes the "email-ingress does not fire LC-75 webhooks" known gap in docs/protocol-bridges.md.
longjacksonle deleted branch fix/lc-205-email-ingress-message-posted 2026-05-29 20:38:23 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!268
No description provided.