You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(email): scope templates to the sending team and substitute variables literally - #478
Template lookup is scoped to the sending team (apps/web/src/server/service/email-service.ts)
sendEmail, the suppressed-recipient path in sendBulkEmails, and the main sendBulkEmails path now share a single loadTeamTemplate(templateId, teamId) helper that queries { id, teamId }. This matches the dashboard's templateProcedure.
Template rendering is moved into a shared renderTemplate helper, which replaces three copies of the same code.
sendBulkEmails loads every referenced template before it writes anything, so an unknown template can't leave a batch half-created. The batch template cache used to key on Number(templateId). Every cuid becomes NaN, so all templates in a batch shared one cache entry. It now keys on teamId:templateId.
Variable substitution is literal
replaceVariables uses string split/join instead of new RegExp(\{{${key}}}`). Keys are matched as plain text, so .*, [, name|codeand(a+)+no longer act as regex patterns. It runs in linear time and can't throw on any key. Values are inserted as-is, with no$& / `` $ `` / $' expansion. Normal `{{name}}` substitution works exactly as before.
replaceTemplateTokens (double opt-in) already escaped its keys. It now also passes the value through a function so replacement patterns are never expanded.
API behavior change
When POST /v1/emails or POST /v1/emails/batch gets a templateId that doesn't exist or belongs to another team, it now returns 404 NOT_FOUND "Template not found". Before, it ignored the template and sent using the request's subject/html. For batch, the whole request fails and nothing is created.
How tested
New email-service.unit.test.ts:
replaceVariables: normal substitution, unknown placeholders left alone, special-character keys treated literally, a pathological key on a 5k-char subject (<100ms), and $&/$`/$'/$$/$1 values inserted literally.
sendEmail: a same-team template renders, and another team's template is rejected with NOT_FOUND before any email is created or queued.
sendBulkEmails: two different templates in one batch each render correctly (covers the cache fix), and another team's template rejects the batch before any record is created, including suppressed records.
New send-email.api.test.ts: POST /v1/emails with another team's templateId returns 404 and the lookup includes teamId.
Template variables are now substituted literally, preventing special characters in variable names or values from changing the rendered message unexpectedly.
Email templates are checked against the requesting team. Missing templates, or templates belonging to another team, now return a not-found error instead of being silently skipped.
Bulk email requests with an unavailable template fail before email records are created or messages are queued.
Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 5 remain after this review.
Walkthrough
Template variable replacement now inserts values literally and uses an empty string for missing values. Single and bulk email sends load templates by both template ID and team ID. Bulk sends preload distinct team-and-template pairs before processing emails. Tests cover rendering, unavailable templates, and cases where no email is created or queued.
Suggested reviewers: vicke4
Priority: Normal
Change: Bug fix
Merge Risk: Minimal · up to 41ba4
The template changes are mergeable after normal checks; no actionable issue remains identified.
Pre-merge checks: 4 · 1
Failed checks: 1 warning
Check name
Status
Explanation
Resolution
Docstring Coverage
Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 4 files.
Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed checks: 4 passed
Check name
Status
Explanation
Description Check
Check skipped - CodeRabbit’s high-level summary is enabled.
Title check
The title clearly and concisely summarizes the two main changes: team-scoped template lookup and literal variable substitution.
Linked Issues check
Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check
Check skipped because no linked issues were found for this pull request.
Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Warning
Some tools did not complete. Review the errors below.
🔧 ESLint
If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Template lookup is scoped to the sending team (
apps/web/src/server/service/email-service.ts)sendEmail, the suppressed-recipient path insendBulkEmails, and the mainsendBulkEmailspath now share a singleloadTeamTemplate(templateId, teamId)helper that queries{ id, teamId }. This matches the dashboard'stemplateProcedure.renderTemplatehelper, which replaces three copies of the same code.sendBulkEmailsloads every referenced template before it writes anything, so an unknown template can't leave a batch half-created. The batch template cache used to key onNumber(templateId). Every cuid becomesNaN, so all templates in a batch shared one cache entry. It now keys onteamId:templateId.Variable substitution is literal
replaceVariablesuses stringsplit/joininstead ofnew RegExp(\{{${key}}}`). Keys are matched as plain text, so.*,[,name|codeand(a+)+no longer act as regex patterns. It runs in linear time and can't throw on any key. Values are inserted as-is, with no$&/ `` $`` /$'expansion. Normal `{{name}}` substitution works exactly as before.replaceTemplateTokens(double opt-in) already escaped its keys. It now also passes the value through a function so replacement patterns are never expanded.API behavior change
When
POST /v1/emailsorPOST /v1/emails/batchgets atemplateIdthat doesn't exist or belongs to another team, it now returns 404NOT_FOUND"Template not found". Before, it ignored the template and sent using the request'ssubject/html. For batch, the whole request fails and nothing is created.How tested
email-service.unit.test.ts:replaceVariables: normal substitution, unknown placeholders left alone, special-character keys treated literally, a pathological key on a 5k-char subject (<100ms), and$&/$`/$'/$$/$1values inserted literally.sendEmail: a same-team template renders, and another team's template is rejected with NOT_FOUND before any email is created or queued.sendBulkEmails: two different templates in one batch each render correctly (covers the cache fix), and another team's template rejects the batch before any record is created, including suppressed records.send-email.api.test.ts:POST /v1/emailswith another team'stemplateIdreturns 404 and the lookup includesteamId.pnpm --filter=web test:unit(147 passed),test:api(23 passed),typecheckclean.Summary by CodeRabbit