feat(mailer): add AWS SES and SMTP providers with auto-detect fallback#4710
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Adds new SES ( Extends env schema with the new variables (plus an Reviewed by Cursor Bugbot for commit 7705d31. Configure here. |
Greptile SummaryThis PR refactors the email subsystem into a provider-abstraction layer, adding AWS SES (via nodemailer +
Confidence Score: 5/5Safe to merge — the new SES and SMTP providers are well-guarded at startup, the provider fallback chain works correctly, and the existing test suite passes without modification. The refactor cleanly separates concerns with no breaking changes. The one finding is a timing-race edge case in the per-message batch path where data.count can over-report by the number of users who unsubscribe in the very small window between batch preparation and dispatch — it requires both a race condition and a non-batch provider to trigger, so it has negligible real-world impact. apps/sim/lib/messaging/email/mailer.ts — specifically the per-message fallback in sendBatchEmails. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[sendEmail / sendBatchEmails] --> B{shouldSkipForUnsubscribe?}
B -- yes --> C[Return SKIPPED_UNSUBSCRIBED_RESULT]
B -- no --> D[processEmailData]
D --> E{activeProviders.length === 0?}
E -- yes --> F[Return MOCK_EMAIL_RESULT - console log only]
E -- no --> G[dispatchWithFallback]
G --> H{Try Resend}
H -- success --> I[Return result]
H -- fail --> J{Try SES}
J -- success --> I
J -- fail --> K{Try SMTP}
K -- success --> I
K -- fail --> L{Try Azure}
L -- success --> I
L -- fail --> M[Return all-providers-failed error]
subgraph sendBatchEmails
N[prepareBatch - check unsubscribe per email] --> O{sendable.length === 0?}
O -- yes --> P[Return skipped/failed result]
O -- no --> Q{Provider with sendBatch?}
Q -- yes Resend --> R[batchProvider.sendBatch]
R -- success --> S[mergeBatchResults]
R -- fail --> T[Per-message fallback via sendEmail]
Q -- no --> T
T --> S
end
Reviews (4): Last reviewed commit: "fix(mailer): batch degrades isUnsubscrib..." | Re-trigger Greptile |
- Force a single @aws-sdk/client-sesv2 install via root package.json overrides; @types/nodemailer pulled in a nested copy whose nominal class brand made the two SDK type identities incompatible, breaking the CI build. With one install the cast disappears. - Batch result message now reports successCount instead of sendable.length when entries are skipped, so "5 emails sent" no longer overstates delivery on partial failures. - SMTP provider now warns when SMTP_HOST is set without SMTP_PORT, and when only one of SMTP_USER/SMTP_PASS is set — both previously silent misconfigurations. - SMTP_SECURE schema is z.boolean() to match every other boolean in env.ts; runtime parsing is still handled by envBoolean. - Strip the verbose TSDoc comments I had added.
|
@greptile |
|
@cursor review |
- mergeBatchResults: data.count and the message now report only emails that were actually delivered, not skipped-unsubscribed ones (they returned success: true and inflated the count). Empty-sendable branch distinguishes "all unsubscribed" from "mixed skip/failure" so the message stops lying when some entries fail validation. - ses.ts: revert the package.json override approach (bun honors it locally but CI still installs a nested @types/nodemailer copy). Reinstate the `as unknown as` cast with a single-line WHY comment.
|
@greptile |
|
@cursor review |
A transient DB error in isUnsubscribed used to abort the whole batch because the call sat outside the per-email try/catch in prepareBatch. Wrap the unsubscribe check inside the same catch so a rejection becomes a per-recipient failure, matching sendEmail's behavior. Lock it in with a regression test.
|
@greptile |
|
@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 7705d31. Configure here.
Summary
MailProviderinterface, one file per provider, factory-based auto-detection)@aws-sdk/client-sesv2transport; credentials resolved through the standard AWS provider chainenvBooleanhelper next toenvNumberto fix thez.coerce.boolean("false") === truefootgun underskipValidation: true.env.example, helm values + schema, and self-hosting docsType of Change
Testing
tsc --noEmit,biome check,bun run check:api-validation,helm lintall cleanhelm templaterenders the 6 new env vars (AWS_SES_REGION,SMTP_HOST/PORT/USER/PASS/SECURE) into the app SecretenvBooleanverified against"false","FALSE","0","no","",undefinedChecklist