feat(engagement): the email channel on the engine, and the Teams migration (engagement Phase 6)
Email becomes a DeliveryChannel driven by rules, and the Team pipeline stops being
its own thing. `teamNotify.forumPost` now emits an event; a rule decides who is
mailed, through which template, and how often at most. One walk goes forum write
-> events.emit -> rule -> outbox -> worker -> email channel -> template -> SMTP.
Seven decisions settled by the org lead before any code:
- email only moves; the push tickle and the Discord bridge stay direct calls
- the EVENT carries its access-checked audience, and `members` resolves to it
- the four Team rules are seeded DISABLED, with an admin banner and a note
- team_notification_prefs stays, read by the engine as a scoped preference
- the payload wins and a structural projection fills the gaps
- the digest keeps computing at send time; only its state generalizes
- an unsubscribe token turns off the channel it names, and nothing else
Three defects found while building it:
- `email.button` never absolutized its href, while image and itemList both
did. Every rule-driven CTA would have been a dead relative link, because a
trigger's url variables are validated site-relative by construction.
- Phase 4a enqueued digest-mode recipients for a drain that Phase 6 decided
not to build. An outbox row snapshots the payload and so has none of the
three properties the digest design exists for, including the security one.
- the digest's send-log row carried no address_hash while the instant row
beside it did, which would have made half the mail uncorrelatable in Phase 9.
Also: engagement_digest_state + a replay-safe backfill, engagement_outbox.scope_key,
a v2 unsubscribe token that still verifies v1 forever, and the canonical
/public/engagement/unsubscribe pair with the old /public/teams path kept
permanently — mail is not editable once sent.
Verified with 1464 server tests, 324 client tests, and a live rig (MariaDB +
Mailpit + a real Team) covering the instant mail, the digest, the generic
template, a pre-migration unsubscribe link and the backfill's replay-safety.
Docs: RunicGateway/docs#TBD
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -52,7 +52,15 @@ registerEmailBlock({
|
||||
// still renders, inert, because dropping it silently would hide from the
|
||||
// reader that the mail was meant to offer them something.
|
||||
if (ctx.t(props.url).trim() === '') return ''
|
||||
const href = ctx.safeHref(props.url)
|
||||
// ABSOLUTIZED, like `email.image` and `email.itemList` already do, and this
|
||||
// was a real defect until Phase 6 put a rule-driven variable in here. A
|
||||
// trigger's `url` variables are validated site-RELATIVE by construction
|
||||
// (`engagementEmit.RELATIVE_URL`), so `{{actionUrl}}` interpolates to
|
||||
// `/guilds/the-silver-anvil` and a mail client has no origin to resolve that
|
||||
// against: the button rendered a dead link. `absolute()` returns null for a
|
||||
// relative path when no base is configured, which falls into the inert-label
|
||||
// branch below rather than shipping the broken href.
|
||||
const href = ctx.absolute(ctx.safeHref(props.url))
|
||||
const label = ctx.h(props.label)
|
||||
if (!href) {
|
||||
return (
|
||||
@@ -78,8 +86,12 @@ registerEmailBlock({
|
||||
)
|
||||
},
|
||||
toText(props, ctx) {
|
||||
const url = ctx.t(props.url).trim()
|
||||
if (url === '') return '' // see toHtml: no url, no block, in either part
|
||||
const raw = ctx.t(props.url).trim()
|
||||
if (raw === '') return '' // see toHtml: no url, no block, in either part
|
||||
// The text part shows the same absolute URL the button links to. Falls back
|
||||
// to the raw value rather than dropping the block: a reader who can see a
|
||||
// relative path can still find the site, and `itemList` makes the same trade.
|
||||
const url = ctx.absolute(raw) || raw
|
||||
const lead = props.textLead ? ctx.t(props.textLead).trim() : ''
|
||||
return lead ? `${lead}\n${url}` : url
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user