refactor: remove unused Customer.io transactional email codepath#1498
Open
evanjacobson wants to merge 6 commits intomainfrom
Open
refactor: remove unused Customer.io transactional email codepath#1498evanjacobson wants to merge 6 commits intomainfrom
evanjacobson wants to merge 6 commits intomainfrom
Conversation
Production has been on Mailgun. Removes email-customerio.ts, customerio-node dependency, EMAIL_PROVIDER config, and CIO provider option from admin email testing page. CIO user deletion in external-services.ts is preserved (different API keys, used for marketing audience cleanup).
14 tasks
Contributor
Code Review SummaryStatus: 1 Issue Found | Recommendation: Address before merge Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)N/A Other Observations (not in diff)Issues found in changed files outside the diff and therefore not eligible for inline comments:
Files Reviewed (80 files)
Reviewed by gpt-5.4-20260305 · 2,171,694 tokens |
evanjacobson
commented
Mar 24, 2026
Contributor
Author
There was a problem hiding this comment.
most of these will be filtered out after #1356 merges
17 tasks
The jest.mock for @/lib/email only provided sendOrganizationInviteEmail, but the email-testing-router (loaded transitively via the router tree) now imports subjects from @/lib/email at module level. Add the missing exports to the mock.
…-Org/cloud into improvement/remove-customerio-email
jeanduplessis
approved these changes
Mar 25, 2026
| // Mock the email service to prevent actual API calls during tests | ||
| jest.mock('@/lib/email', () => ({ | ||
| sendOrganizationInviteEmail: jest.fn().mockResolvedValue(undefined), | ||
| subjects: { orgInvitation: 'Kilo: Teams Invitation' }, |
Contributor
There was a problem hiding this comment.
Why do we have these additions in this PR?
evanjacobson
added a commit
that referenced
this pull request
Mar 25, 2026
) ## Summary Adds NeverBounce email verification before sending transactional emails to reduce our ~3% bounce rate. Blocks `invalid` and `disposable` emails; allows `valid`, `catchall`, `unknown` through. Fails open if NeverBounce is unavailable. **How it works:** - New `src/lib/email-neverbounce.ts` calls the NeverBounce single-check API with a 5s timeout - `send()` returns `SendResult` (`{ sent: true } | { sent: false, reason }`) so callers can react - Blocked sends and API failures reported to Sentry **Caller handling:** - Magic link route returns 400 with "Unable to deliver email to this address" - Org invite route expires the invitation row and throws `BAD_REQUEST` - Billing lifecycle cron removes idempotency row on rejection, allowing retry next run - Admin email testing routes through `send()` so NeverBounce is exercised ## Verification ### Automated tests (50 passing) - [x] `npx tsc --noEmit` — no type errors - [x] `pnpm lint` — clean - [x] `email-neverbounce.test.ts` — 11 tests (all result types, fail-open, Sentry reporting, timeout, params) - [x] `magic-link/route.test.ts` — 10 tests - [x] `autoTopUp.test.ts` — 16 tests - [x] `email.test.ts` — 13 tests - [x] Verified via automated agent that this branch preserves all CIO code intact (10/10 checks pass) ### Manual tests (all passing on localhost) - [x] **Invalid email blocked**: `fakeperson@xyznotarealdomainever.comtld` → HTTP 400 - [x] **Disposable email blocked**: `test@mailinator.com` → HTTP 400 - [x] **Valid email allowed**: `support@neverbounce.com` → HTTP 200 - [x] **Real email delivered**: `evan@kilocode.ai` → email received in inbox - [x] **Magic link with invalid email**: returns "Unable to deliver" error - [x] **Org invite with invalid email**: error returned, invitation expired - [x] **NeverBounce not configured (fail-open)**: emails send normally without API key ## Visual Changes N/A — no UI changes in this PR (admin page provider selector is unchanged). ## Reviewer Notes - `NEVERBOUNCE_API_KEY` has been added to Vercel. - This PR is independent of the CIO removal PR (#1498) — they can merge in either order. When combined, they produce the same result as the original PR #1493. - NeverBounce API only supports auth via query parameter — no header auth option available.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Removes the unused Customer.io transactional email codepath. Production has been on Mailgun.
email-customerio.tsand removedcustomerio-nodedependencyEMAIL_PROVIDERconfig and routing logic —send()always uses Mailguntemplatesobject (CIO template ID mapping), madesubjectsthe single source of truthcreditsVars(removed CIO Liquid-only variables)Note:
external-services.tsCIO user deletion is preserved — it uses different API keys (CUSTOMERIO_SITE_ID/CUSTOMERIO_API_KEY) for marketing audience cleanup.Verification
npx tsc --noEmit— no type errorspnpm lint— cleanVisual Changes
Reviewer Notes
CUSTOMERIO_EMAIL_API_KEYandEMAIL_PROVIDERfrom Vercel. KeepCUSTOMERIO_SITE_IDandCUSTOMERIO_API_KEY(used byexternal-services.tsfor marketing user deletion).