fix(email): unify provider failure classification with canonical bounce categories - #38
Merged
Merged
Conversation
…ce categories Delegate classifyEmailFailure() in EmailService to canonical classifyBounce() in @leadforge/schema. Correctly distinguish provider/network errors from permanent policy/spam rejection, avoiding treating reputation blocks as transient network failures. Update mailbox health repository to track POLICY failures, and ensure valid contacts are not falsely suppressed on policy rejections. Closes #32.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
kjxcodez
added a commit
that referenced
this pull request
Sep 11, 2026
…suppression fix(outreach): enforce company dnc and domain suppression cascade (#38)
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.
Problem
The forensic audit (Issue #24, PR #31) identified that
classifyEmailFailure()inapps/api/src/services/email/email.service.tsfailed to correctly classify critical provider rejection conditions:PROVIDERcategory and were recorded against mailbox health as transientNETWORKfailures.550(such as550 5.7.26) were misclassified asINVALID_RECIPIENT, triggering false-positive recipient suppression of valid contacts.@leadforge/schema.Root Cause
classifyEmailFailure()relied on ad-hoc regex and string inspection that evaluated generic550before checking for policy or authentication rejection markers, and did not delegate to@leadforge/schema'sclassifyBounce().@leadforge/schema,classifyBounce()evaluatedstatusCode === 550underMAILBOX_UNAVAILABLEbefore checking for5.7.1/5.7.26/ SPF / DMARC underPOLICY_REJECTION.'POLICY'failure categories, defaulting to'NETWORK'on unclassified provider rejections.EmailService.send()evaluated contact suppression broadly onfailure.category === INVALID_RECIPIENTrather than gating strictly on verified hard bounces (isHardBounce: true).Changes
packages/schema/src/utils/bounce-classifier.ts:SPAM_REJECTIONandPOLICY_REJECTION(including5.7.1,5.7.26, SPF/DKIM/DMARC, Spamhaus/blocklists) are evaluated before generic 550MAILBOX_UNAVAILABLE.packages/schema/src/utils/bounce-classifier.test.ts:550 5.7.26(DMARC/SPF) and554 5.7.1(relay denied) to ensure they categorize asPOLICY_REJECTION.apps/api/src/db/models/email-account.model.ts&apps/api/src/repositories/email-account/email-account.repository.ts:'POLICY'tolastFailureCategoryinEmailAccountHealth.recordSendFailure()to handle'POLICY'failures: places mailbox in a 10-minute cooldown or sets status toBLOCKEDwithoperatorActionRequired: trueafter 3 consecutive policy rejections.apps/api/src/services/email/email.service.ts:classifyEmailFailure()to delegate to canonicalclassifyBounce().SPAM_REJECTION,POLICY_REJECTION, andAUTHENTICATION_REJECTIONtoEmailFailureCategory.POLICY(retryable: false,isHardBounce: false).MAILBOX_UNAVAILABLE,DOMAIN_UNAVAILABLE, andHARD_BOUNCEtoEmailFailureCategory.INVALID_RECIPIENT(isHardBounce: true).send(), records'POLICY'failure against mailbox health.send(), gates automatic contact suppression onfailure.isHardBounce === true, preventing false-positive suppression on policy blocks.apps/api/src/services/email/unified-failure-classification.test.ts(12 tests).apps/api/src/services/email/outbound-provider-rejection-audit.test.tsFinding 1 tests to verify remediation.Safety Impact
Tests
pnpm --filter @leadforge/schema test: 125 passed (125)pnpm --filter @leadforge/schema check-types: Clean (0 errors)pnpm --filter api check-types: Clean (0 errors)pnpm --filter api test src/services/email/: 18 test files passed (119 passed)Verification
554 5.7.1 Spam detectedis classified asPOLICY(non-retryable,isHardBounce: false).550 5.7.26 DMARC/SPFis classified asPOLICY(non-retryable,isHardBounce: false).550 5.1.1 User unknownis classified asINVALID_RECIPIENT(isHardBounce: true).452 Mailbox fullis classified asPROVIDER(retryable: true,isHardBounce: false).DEGRADEDon 1st policy failure andBLOCKEDon 3rd consecutive policy failure.Scope
Related Issue