feat(mailer): gate outbound email on AppConfig access-control ban list#5018
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview Signup email validation is simplified: the custom Reviewed by Cursor Bugbot for commit c9a340c. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f331a0b. Configure here.
|
@greptile review |
Greptile SummaryThis PR gates all outbound email through the AppConfig access-control ban list by introducing an
Confidence Score: 5/5Safe to merge. The ban-list gate is correctly threaded through both the single-send and batch paths, AppConfig failures truly fail open, and the new tests cover the key scenarios. The core change is straightforward: a synchronous filter on a cached config is inserted before the unsubscribe check and send. The AppConfig layer already handles errors by returning the last-known value or falling back to env vars, so the fails-open guarantee holds. The two noted issues are both message-accuracy gaps that do not affect delivery correctness or security. No files require special attention beyond the two minor observability gaps in mailer.ts. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["sendEmail(options)"] --> B["applyBanList(options)"]
B --> C["getAccessControlConfig()"]
C --> D{"AppConfig enabled?"}
D -- No --> E["fromEnv() fallback"]
D -- Yes --> F["fetchAppConfigProfile (cached ~30s TTL)"]
F --> G{"Fetch succeeded?"}
G -- No --> H["return last known value ?? fromEnv()"]
G -- Yes --> I["parseConfig(json)"]
E --> J["AccessControlConfig"]
H --> J
I --> J
J --> K["filter via isEmailBlockedByAccessControl"]
K --> L{"all banned?"}
L -- Yes --> M["return null -> SKIPPED_BANNED_RESULT"]
L -- "partial (no audit log)" --> N["return filtered options"]
L -- None banned --> O["return original options"]
N --> P["shouldSkipForUnsubscribe"]
O --> P
P -- unsubscribed --> Q["SKIPPED_UNSUBSCRIBED_RESULT"]
P -- allowed --> R["processEmailData -> dispatchWithFallback"]
Reviews (2): Last reviewed commit: "ci(migrations): restore dev schema-push ..." | Re-trigger Greptile |
| }), | ||
| ] | ||
| : []), | ||
| ...(isSignupEmailValidationEnabled ? [emailHarmony()] : []), |
There was a problem hiding this comment.
Silent removal of
disposable-email-domains signup check
emailHarmony() with no validator option uses only better-auth-harmony's built-in Mailchecker list. The previous custom validator explicitly called validateEmailWithMailchecker(email) && !(await isDisposableEmailDomain(email)), layering the disposable-email-domains dataset (~120K exact domains) on top of Mailchecker. Dropping the custom validator quietly narrows signup validation to Mailchecker alone — any disposable provider covered by disposable-email-domains but not by Mailchecker can now register. This change isn't mentioned in the PR description; if it's intentional (e.g., the AppConfig ban list now handles this), it should be documented.
|
@greptile review |

Summary
sendEmail/sendBatchEmailsso block-lifecycle emails (payment-failure, dispute, credit-purchase, usage-threshold) — and all other outbound mail — skip recipients on the AppConfig access-control ban listapplyBanListhelper filters recipients viaisEmailBlockedByAccessControl(same predicate used at signup/login/execution); drops only banned addresses from multi-recipient sends, skips entirely when all are bannedType of Change
Testing
Tested manually.
bun run lintclean,bun run check:api-validation:strictpassed. Added mailer tests (single banned recipient, partial-array filtering, batch skip) — 18/18 pass; billing webhook tests 3/3 pass.Checklist