Skip to content

feat(mailer): gate outbound email on AppConfig access-control ban list#5018

Merged
TheodoreSpeaks merged 3 commits into
stagingfrom
feat/block-lifecycle-email
Jun 13, 2026
Merged

feat(mailer): gate outbound email on AppConfig access-control ban list#5018
TheodoreSpeaks merged 3 commits into
stagingfrom
feat/block-lifecycle-email

Conversation

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator

Summary

  • Add a ban-list gate inside sendEmail/sendBatchEmails so block-lifecycle emails (payment-failure, dispute, credit-purchase, usage-threshold) — and all other outbound mail — skip recipients on the AppConfig access-control ban list
  • New applyBanList helper filters recipients via isEmailBlockedByAccessControl (same predicate used at signup/login/execution); drops only banned addresses from multi-recipient sends, skips entirely when all are banned
  • Fails open: config is cached (~30s TTL) with env fallback, so an unreachable AppConfig never blocks all mail

Type of Change

  • Bug fix

Testing

Tested manually. bun run lint clean, bun run check:api-validation:strict passed. Added mailer tests (single banned recipient, partial-array filtering, batch skip) — 18/18 pass; billing webhook tests 3/3 pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jun 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Jun 13, 2026 1:34am

Request Review

@cursor

cursor Bot commented Jun 13, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Outbound email to banned users now depends on AppConfig access-control (fails open on config errors); signup loses the extra disposable-domain list beyond better-auth-harmony defaults.

Overview
Outbound mail now respects the same AppConfig access-control ban list used at signup/login: sendEmail and sendBatchEmails run recipients through applyBanList / isEmailBlockedByAccessControl before unsubscribe checks and provider dispatch. Banned addresses are skipped with a success-style skipped-banned result; multi-recipient sends keep only allowed addresses; batch prep mirrors the same behavior. Config is cached with env fallback so unreachable AppConfig fails open (mail is not blocked globally). Mailer tests cover full skip, partial filtering, and batch skips.

Signup email validation is simplified: the custom emailHarmony validator that combined Mailchecker with the disposable-email-domains dataset is removed in favor of default emailHarmony(). The disposable-email-domains dependency, server helper, ambient types, and tests are deleted.

Reviewed by Cursor Bugbot for commit c9a340c. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread apps/sim/lib/auth/auth.ts
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@greptile-apps

greptile-apps Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR gates all outbound email through the AppConfig access-control ban list by introducing an applyBanList helper in mailer.ts that calls getAccessControlConfig + isEmailBlockedByAccessControl before any unsubscribe or send step. It also removes the disposable-email-domains package and its custom emailHarmony validator, replacing the layered signup check with the built-in Mailchecker list alone.

  • Ban-list gate (mailer.ts): applyBanList short-circuits sendEmail and prepareBatch for banned addresses; the AppConfig fetch is cached (~30s TTL) with an env-var fallback, so a cold/unreachable AppConfig fails open rather than blocking mail.
  • Disposable-domain check removed (auth.ts): The custom emailHarmony validator is dropped; signup validation now uses only the built-in Mailchecker list.
  • Tests (mailer.test.ts): Three new cases cover the single-banned, partial-array, and batch-ban paths.

Confidence Score: 5/5

Safe 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

Filename Overview
apps/sim/lib/messaging/email/mailer.ts Core change: adds applyBanList helper integrated into both sendEmail and prepareBatch. Logic is sound and AppConfig correctly deduplicates in-flight cold-path fetches. Minor observability gap when a partial-recipient email silently drops banned addresses.
apps/sim/lib/messaging/email/mailer.test.ts Adds three new test cases covering single-banned, partial-array, and batch-ban scenarios; mocks are correctly reset per-test.
apps/sim/lib/auth/auth.ts Removes the custom emailHarmony validator that layered disposable-email-domains on top of Mailchecker, reverting to emailHarmony() with only its built-in list. Discussed in previous review thread.
apps/sim/lib/messaging/email/disposable-domains.server.ts Deleted — the isDisposableEmailDomain module and its tests are removed along with the disposable-email-domains dependency.
apps/sim/package.json Removes the disposable-email-domains dependency, consistent with the deleted module.

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"]
Loading

Reviews (2): Last reviewed commit: "ci(migrations): restore dev schema-push ..." | Re-trigger Greptile

Comment thread apps/sim/lib/auth/auth.ts
}),
]
: []),
...(isSignupEmailValidationEnabled ? [emailHarmony()] : []),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment thread .github/workflows/migrations.yml Outdated
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks TheodoreSpeaks merged commit fb9e481 into staging Jun 13, 2026
15 checks passed
@TheodoreSpeaks TheodoreSpeaks deleted the feat/block-lifecycle-email branch June 13, 2026 01:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant