improvement(emails): funnel every sender through the shared render and subject layer - #6482
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Billing and inbox bypasses removed: Shared tokens and guards: Email base styles add User-visible tweaks: Payment-failed subject is brand-aware; agent reply links shift from blue to neutral dashed underline (platform link style). Reviewed by Cursor Bugbot for commit 42d1ba7. Configure here. |
Greptile SummaryThe PR centralizes remaining email rendering and subject generation behind the shared email layer and adds a build-time boundary guard.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/components/emails/boundary.test.ts | Adds sender discovery and guards against direct renderer access, template-component usage, and inline product subjects; the previously reported dynamic-import hole is addressed. |
| apps/sim/components/emails/render.ts | Adds shared wrappers for inbox response/error rendering and sanitizes unsafe URL schemes. |
| apps/sim/components/emails/subjects.ts | Centralizes additional brand-aware payment, plan, request-confirmation, and OTP subjects. |
| apps/sim/components/emails/agent/inbox-response-email.tsx | Moves inbox reply and error markup into the shared email component layer. |
| apps/sim/lib/mothership/inbox/response.ts | Replaces the local email implementation with shared rendering and subject helpers. |
| apps/sim/lib/billing/webhooks/invoices.ts | Routes payment-failure emails through the shared renderer and brand-aware subject helper. |
| apps/sim/lib/billing/core/subscription.ts | Routes subscription welcome subjects through the shared subject layer and avoids redundant plan-name resolution. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Sender[Email sender] --> Subject[Shared subject helpers]
Sender --> Render[Shared render wrappers]
Subject --> Mailer[sendEmail]
Render --> Template[Shared email templates]
Template --> Mailer
Guard[Boundary tests] -. validates .-> Sender
Guard -. validates .-> Render
Reviews (3): Last reviewed commit: "fix(emails): mock the module the limit-n..." | Re-trigger Greptile
… new subject helper
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
1 issue from previous review remains unresolved.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 5d9388e. Configure here.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 42d1ba7. Configure here.

Follow-up to #6479. That PR made the shared layer correct; this one makes every sender actually use it.
What was bypassing it
Auditing all 19
sendEmailcall sites turned up three real bypasses:invoices.tsimported thePaymentFailedEmailcomponent and calledrender()itself instead of usingrenderPaymentFailedEmail, and hardcoded'Payment Failed - Action Required'— the only subject in the system that wasn't brand-aware, because there was nopayment-failedcase inEmailSubjectType.subscription.tsbuilt its subject inline with${(await import('@/ee/whitelabeling')).getBrandConfig().name}embedded in the template literal, and calledgetDisplayPlanNametwice. It also importedgetEmailSubjectand never used it.lib/mothership/inbox/response.tscarried an entire second email system — its ownInboxResponseEmailcomponent and ~150 lines of style literals (#ededed,#2563eb,#f5f5f5), none of it on the shared tokens. Moved tocomponents/emails/agent/inbox-response-email.tsxbehindrenderInboxResponseEmail/renderInboxErrorEmail; the file drops from 317 to 80 lines.Subjects now live in
subjects.ts— added apayment-failedcase plusgetPlanWelcomeSubject,getRequestConfirmationSubject, andgetOtpSubjectalongside the existinggetLimitEmailSubject. Removed threeEmailSubjectTypecases that nothing called.Two user-facing copy fixes
getBrandConfig().name.Payment failed on Sim — action required(was brand-free). Its preview text had also drifted to different casing; both now read from one source.Guard
components/emails/boundary.test.tsfails the build if a sender imports@react-email/render, imports a template component instead of its wrapper, or builds its own subject. It scans only files that actually callsendEmail, and exempts bracket-tagged internal team-inbox subjects. Verified it fails on each violation.Type of Change
Testing
850 tests pass across
components/emails,lib/billing,lib/mothership,lib/messaging,lib/invitations,lib/workflows/schedules. Type-check and lint clean.Rendered all 23 templates and scanned for
#dedede,#ededed,#707070,#f8f8f8,#2563eb,border-radius:6px,font-weight:700|bold,text-transform:uppercase, and the literalMothership— 0 hits.Two review agents audited this for regressions and over-engineering. Fixes applied from that pass: restored
Roboto/Helvetica Neueto the system font stack (tokenizing had silently dropped them, affecting Android/Linux), removed a deadsentDateprop, replaced brittle regex parsing inbase.tokens.test.tswith a direct import of the Tailwind config (mdwas matchingborderRadius.md), narrowed the boundary scan to real senders, and deleted a test that only asserted its own fixture data.One deliberate visual change to call out: agent-reply links were
#2563ebblue and are now neutral with a dashed underline, matchingbaseStyles.link. That is the platform treatment, but it is a visible change to a live email.Checklist