-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Filter out invalid recipients from batch email sends #3113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis PR removes email address filters from payout and partner notification flows while adding compensating client-side filtering in Changes
Sequence DiagramsequenceDiagram
participant Caller
participant sendBatchEmail
participant sendViaResend as send-via-resend.ts
participant Resend
rect rgb(200, 220, 255)
Note over Caller,Resend: Old flow: conditional sending
Caller->>sendBatchEmail: emails (pre-filtered for valid to addresses)
sendBatchEmail->>sendViaResend: emails array
alt emails.length > 0
sendViaResend->>Resend: batch.send(payload)
Resend-->>sendViaResend: result
else
sendViaResend-->>Caller: null
end
end
rect rgb(255, 240, 200)
Note over Caller,Resend: New flow: always attempt send, filter server-side
Caller->>sendBatchEmail: emails (may include invalid to addresses)
sendBatchEmail->>sendViaResend: emails array
sendViaResend->>sendViaResend: filter entries: where entry.to exists
alt filteredBatch.length > 0
sendViaResend->>Resend: batch.send(filteredBatch)
Resend-->>sendViaResend: result
else
sendViaResend-->>Caller: { data: null, error: null }
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20–25 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
packages/email/src/index.ts (1)
44-54: SMTP fallback lacks null-recipient filtering.The Resend path now filters out emails without a
toaddress (seesend-via-resend.tslines 79-99), but the SMTP fallback path attempts to send all emails, including those with potentially nulltoaddresses. This inconsistency could cause SMTP sends to fail at runtime.Apply this diff to add filtering:
if (smtpConfigured) { + const validEmails = emails.filter((email) => email?.to); + + if (validEmails.length === 0) { + return { + data: null, + error: null, + }; + } + await Promise.all( - emails.map((p) => + validEmails.map((p) => sendViaNodeMailer({ to: p.to, subject: p.subject, text: p.text, react: p.react, }), ), ); return { data: null, error: null, }; }apps/web/lib/actions/partners/bulk-ban-partners.ts (1)
29-55: Query should filter for non-null partner emails.The query selects
partner.emailbut doesn't filter foremail: { not: null }in the WHERE clause. This meansprogramEnrollmentscould include partners with null email addresses, making the non-null assertions on lines 196 and 203 unsafe.Apply this diff to add email filtering to the query:
const programEnrollments = await prisma.programEnrollment.findMany({ where: { partnerId: { in: partnerIds, }, programId, status: { not: "banned", }, + partner: { + email: { + not: null, + }, + }, }, select: {apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts (1)
9-37: Query should filter for non-null partner.email.The query filters for
paypalEmail: { not: null }but doesn't filter foremail: { not: null }. Sincepartner.emailis used with non-null assertions on lines 67 and 70, the query should ensure email is not null.Apply this diff to add email filtering:
partner: { payoutsEnabledAt: { not: null, }, + email: { + not: null, + }, paypalEmail: { not: null, }, },
🧹 Nitpick comments (2)
apps/web/lib/api/domains/claim-dot-link-domain.ts (1)
177-188: Consider filtering for non-null user emails.The code always calls
sendBatchEmail(even with an empty array) and usesuser.email!non-null assertions. While user emails are typically required for authentication, for consistency with the broader PR pattern of defensive email handling, consider adding an email filter to the query.Apply this diff:
users: { where: { role: "owner", + user: { + email: { + not: null, + }, + }, }, select: { user: true, }, },packages/email/src/send-via-resend.ts (1)
57-99: Good defensive filtering, but consider logging filtered emails.The filtering logic correctly handles null/undefined emails and missing
toaddresses. However, when emails are filtered out, there's no indication in the logs or return value. This could make it difficult to diagnose why expected emails weren't sent.Consider adding a log statement when emails are filtered:
const filteredBatch = emails.reduce( (acc, email) => { if (!email?.to) { return acc; } acc.push(resendEmailForOptions(email)); return acc; }, [] as ReturnType<typeof resendEmailForOptions>[], ); + if (filteredBatch.length < emails.length) { + console.info( + `Filtered out ${emails.length - filteredBatch.length} email(s) without valid 'to' address`, + ); + } if (filteredBatch.length === 0) { return { data: null, error: null, }; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts(1 hunks)apps/web/lib/actions/partners/bulk-ban-partners.ts(1 hunks)apps/web/lib/api/domains/claim-dot-link-domain.ts(2 hunks)packages/email/src/index.ts(2 hunks)packages/email/src/send-via-resend.ts(2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-12T22:23:10.390Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 3098
File: apps/web/lib/actions/partners/message-program.ts:49-58
Timestamp: 2025-11-12T22:23:10.390Z
Learning: In apps/web/lib/actions/partners/message-program.ts, when checking if a partner can continue messaging after messaging is disabled, the code intentionally requires `senderPartnerId: null` (program-initiated messages) to prevent partners from continuing to send junk messages. Only conversations started by the program should continue after messaging is disabled, as a spam prevention mechanism.
Applied to files:
apps/web/lib/actions/partners/bulk-ban-partners.ts
📚 Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Applied to files:
apps/web/lib/actions/partners/bulk-ban-partners.ts
🧬 Code graph analysis (5)
apps/web/lib/actions/partners/bulk-ban-partners.ts (2)
packages/email/src/templates/partner-banned.tsx (1)
PartnerBanned(17-84)apps/web/lib/zod/schemas/partners.ts (1)
BAN_PARTNER_REASONS(67-74)
apps/web/lib/api/domains/claim-dot-link-domain.ts (1)
packages/email/src/index.ts (1)
sendBatchEmail(31-70)
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts (1)
packages/email/src/index.ts (1)
sendBatchEmail(31-70)
packages/email/src/index.ts (3)
packages/email/src/resend/types.ts (1)
ResendBulkEmailOptions(10-10)packages/email/src/resend/client.ts (1)
resend(3-5)packages/email/src/send-via-resend.ts (1)
sendBatchEmailViaResend(57-107)
packages/email/src/send-via-resend.ts (2)
packages/email/src/resend/types.ts (1)
ResendBulkEmailOptions(10-10)packages/email/src/resend/client.ts (1)
resend(3-5)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (2)
packages/email/src/index.ts (1)
31-37: LGTM: Parameter renamed for clarity.The rename from
payloadtoemailsimproves readability and aligns with the filtering changes in the Resend implementation.apps/web/lib/actions/partners/bulk-ban-partners.ts (1)
195-211: Unsafe non-null assertions on partner.email.The code uses
partner.email!non-null assertions on lines 196 and 203, butpartner.emailcould be null since the query doesn't filter for non-null emails. While the downstream filtering insend-via-resend.tswill silently drop these entries, it's better to either:
- Filter at the database level (see previous comment), or
- Use optional chaining and handle null explicitly for visibility
The current approach could lead to silent failures where partners don't receive ban notifications without any error indication.
⛔ Skipped due to learnings
Learnt from: TWilson023 Repo: dubinc/dub PR: 3098 File: apps/web/lib/actions/partners/message-program.ts:49-58 Timestamp: 2025-11-12T22:23:10.390Z Learning: In apps/web/lib/actions/partners/message-program.ts, when checking if a partner can continue messaging after messaging is disabled, the code intentionally requires `senderPartnerId: null` (program-initiated messages) to prevent partners from continuing to send junk messages. Only conversations started by the program should continue after messaging is disabled, as a spam prevention mechanism.Learnt from: devkiran Repo: dubinc/dub PR: 2735 File: apps/web/lib/actions/partners/update-discount.ts:60-66 Timestamp: 2025-08-14T05:57:35.546Z Learning: In the partner groups system, discounts should always belong to a group. The partnerGroup relation should never be null when updating discounts, so optional chaining on partnerGroup?.id may be unnecessary defensive programming.Learnt from: devkiran Repo: dubinc/dub PR: 2754 File: apps/web/lib/partnerstack/schemas.ts:47-52 Timestamp: 2025-08-16T11:14:00.667Z Learning: The PartnerStack API always includes the `group` field in partner responses, so the schema should use `.nullable()` rather than `.nullish()` since the field is never omitted/undefined.
Summary by CodeRabbit