feat(engagement): templates — the email block family, renderer and seeded set (engagement Phase 5a)
Every subject and body moves out of `mailer.js` into `engagement_templates` rows an operator can edit. A relocation, not a regression: nothing that sends mail today starts depending on an operator authoring something first. - `email.*` block family in its own registry, sharing the page family's envelope walk and validate-then-sanitize order by binding rather than by copy. - A server-side renderer producing both parts of a multipart message; the text part is byte-identical to the literals this commit deletes. - Nine seeded templates, six of them wired now; the seeder's `customized = 0` guard lives in the UPDATE's own WHERE. - `renderByKey` falls back to the shipped seed when a row is missing or unusable, so no failure of the table can stop a password reset. Also fixes `check:hosts` reading the template key `auth.email-verify` as the hostname `auth.email`. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,18 @@ test('an ordinary sentence with a full stop is not a hostname', () => {
|
||||
assert.deepEqual(hostsIn(`const msg = 'Send failed. Check the host and port.'`), [])
|
||||
})
|
||||
|
||||
test('a dotted identifier is not a hostname just because a real TLD is a label', () => {
|
||||
// `.email`, `.mail` and `.app` are real TLDs, so an engagement template key or a
|
||||
// trigger id can look like a host to a regex. A real hostname's TLD is its LAST
|
||||
// label; these carry on into another word.
|
||||
assert.deepEqual(hostsIn(`const key = 'auth.email-verify'`), [])
|
||||
assert.deepEqual(hostsIn(`await body('auth.email-verify', { verifyUrl })`), [])
|
||||
assert.deepEqual(hostsIn(`const t = 'core.mail_bounced'`), [])
|
||||
// …and the real thing still trips it, so the loosening did not blunt the check.
|
||||
assert.deepEqual(hostsIn(`const h = 'smtp.somewhere.email'`), ['smtp.somewhere.email'])
|
||||
assert.deepEqual(hostsIn(`const h = 'relay.somewhere.email:587'`), ['relay.somewhere.email'])
|
||||
})
|
||||
|
||||
// ── the pieces, directly ────────────────────────────────────────────────────
|
||||
|
||||
test('maskComments blanks comments but keeps string bodies and line count', () => {
|
||||
|
||||
Reference in New Issue
Block a user