- Shipped
- September 12, 2026 at 6:08 AM UTC
- Author
- Kamo
- Commit
- 5d4aa03
The do-not-email list could be written to but not read, changed or explained. This adds the one place it is read and written from, for every service: email_optout_topics an address that stopped one kind of mail, or paused all of it email_optout_events insert-only history: who (recipient, member, system), from where (client IP, the whole X-Forwarded-For chain, user agent, language, host, referer, session hash), why (reason, source, note, feedback), through what (campaign, lead), and the state before and after email_suppressions keeps its meaning, so every sender that already honours it still does. EmailOptOutLedger answers "may this be sent" from all three and writes one history row per part of the state a change actually moved. A member allowing mail again must say why; that note is the record of consent. Until the new tables exist the ledger falls back to the list alone and refuses changes it cannot record, rather than failing sends. Plain class, not a @Service: every service scans com.kamo, and one without the email repositories would fail to start constructing it. Each service declares it with @Bean. Also MANAGE_EMAIL_OPT_OUTS (320), the org right for Settings -> Email -> Opt Out, a child of MANAGE_EMAIL_SETTINGS.