Skip to content

fix(email): scope templates to the sending team and substitute variables literally - #478

Merged
KMKoushik merged 1 commit into
mainfrom
hq/template-scope
Oct 31, 2026
Merged

KMKoushik merged 1 commit into
mainfrom
hq/template-scope

Conversation

@KMKoushik

@KMKoushik KMKoushik commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

What changed

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.
  • pnpm --filter=web test:unit (147 passed), test:api (23 passed), typecheck clean.

Summary by CodeRabbit

  • Bug Fixes
    • 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.

@vercel

vercel Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
unsend-marketing Ready Ready Preview Oct 25, 2026 10:35pm UTC

@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: bbcace8a-b0b5-4f5e-9464-31276f04f21c
Commits

Reviewing files that changed from the base of the PR and between 7634811 and 41ba431.

Files selected for processing: 4
  • apps/web/src/server/public-api/api/emails/send-email.api.test.ts
  • apps/web/src/server/service/double-opt-in-service.ts
  • apps/web/src/server/service/email-service.ts
  • apps/web/src/server/service/email-service.unit.test.ts

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: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning 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 Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely summarizes the two main changes: team-scoped template lookup and literal variable substitution.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed 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.

apps/web/src/server/public-api/api/emails/send-email.api.test.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

apps/web/src/server/service/double-opt-in-service.ts

ESLint skipped: the matched ESLint configuration already failed (missing-dependency).

apps/web/src/server/service/email-service.ts

ESLint skipped: the matched ESLint configuration already failed (missing-dependency).

  • 1 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying usesend with  Cloudflare Pages  Cloudflare Pages

Latest commit: 41ba431
Status: ✅  Deploy successful!
Preview URL: https://4c64c4cd.usesend.pages.dev
Branch Preview URL: https://hq-template-scope.usesend.pages.dev

View logs

@KMKoushik
KMKoushik merged commit 749cb2e into main Oct 31, 2026
6 checks passed
@KMKoushik
KMKoushik deleted the hq/template-scope branch October 31, 2026 05:20

This branch was successfully deployed

1 active deployment
Preview — 41ba4310 Deployed Oct 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant