- Shipped
- September 12, 2026 at 6:53 PM UTC
- Author
- Kamo
- Commit
- 76ec6fa
Before any email touch the outbound gate now asks the organization's email opt-out ledger about the contact's address. The address is the one the contact key points at: the lead's email for lead:<uid>, the member's (or its user's) for member:<id>, and only when that lead or member belongs to the same organization. An address on the do-not-email list, or paused, is refused as SUPPRESSED_OPT_OUT with a reason that starts EMAIL_OPTED_OUT_ORG_LIST. It is checked straight after the contact's own opt-out and ahead of everything incidental, and it applies even to a contact the agent has never touched. If the list cannot be read the email is refused too (EMAIL_OPT_OUT_CHECK_FAILED): agent outreach is unsolicited, so an unanswered check must never become a send. Other channels are unchanged. An opt-out recorded through recordOptOut for email, or for every channel, now also puts the address on the organization's do-not-email list (reason UNSUBSCRIBE, source ai-agent-opt-out, context AI_CONTACT with the contact key), so every sender in the organization and the lead page see it. Ledger reads and writes run in a transaction of their own and never throw. The ledger is transactional itself, and an exception inside the caller's transaction would roll back the refusal's audit row or the contact's own opt-out. A failed write is logged and the contact's own opt-out still stands. The ledger is declared as a bean over its three repositories. Its probe for the new opt-out tables is built from the primary DataSource, the only one this service has. com.kamo.z.shared.email joins the repository scan; the bean and the scan must change together.