Add opt-in tenant-to-client bindings for invitation mail
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 35s
Account journey acceptance / journeys (push) Failing after 7s

User-engine side of EMAIL-WP-0008: USER_ENGINE_MAIL_CLIENT_BINDINGS maps
tenant globs to a single client_id carried on invitation mail envelopes
only. Unmatched or conflicting tenants fail as mail_binding_denied before
either lane is contacted, and the outbox now records that reason instead
of collapsing it to delivery_unavailable. Unset keeps legacy envelopes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 445231@bnt-lap001
Assistant-Session: 024a807e-0cc5-4f2c-bc8a-b83322d1395d
This commit is contained in:
tegwick 2026-10-09 11:21:17 +02:00
parent a9360842f8
commit adf6e992a6
9 changed files with 199 additions and 2 deletions

View file

@ -19,6 +19,7 @@ class NoRedirect(HTTPRedirectHandler):
from user_engine.domain import OutboxEvent
from user_engine.mail_bindings import load_mail_client_bindings, mail_client_for
_MAIL_EVENTS = {"family_member.invited", "family_invitation.resent"}
@ -34,12 +35,14 @@ class HTTPOutboxDeliveryAdapter:
mail_url: str | None = None,
mail_bearer_token: str | None = None,
timeout_seconds: float = 5.0,
mail_client_bindings: str | None = None,
) -> None:
self.event_url = event_url
self.mail_url = mail_url
self.event_bearer_token = event_bearer_token
self.mail_bearer_token = mail_bearer_token
self.timeout_seconds = timeout_seconds
self.mail_client_bindings = load_mail_client_bindings(mail_client_bindings)
def __call__(self, event: OutboxEvent) -> None:
envelope = {
@ -57,7 +60,15 @@ class HTTPOutboxDeliveryAdapter:
if self.mail_url and event.event_type in _MAIL_EVENTS:
if not self.mail_bearer_token:
raise RuntimeError("mail delivery token is required")
self._post(self.mail_url, envelope, self.mail_bearer_token)
mail_envelope = dict(envelope)
if self.mail_client_bindings is not None:
try:
mail_envelope["client_id"] = mail_client_for(
event.tenant, self.mail_client_bindings
)
except ValueError:
raise DeliveryError("mail_binding_denied") from None
self._post(self.mail_url, mail_envelope, self.mail_bearer_token)
event_envelope = dict(envelope)
event_data = dict(envelope["data"])
if "primary_email" in event_data: