Skip to content

audit(outreach): forensic audit of blocked and provider-rejected outbound emails - #31

Merged
kjxcodez merged 1 commit into
mainfrom
audit/outbound-provider-rejections
Sep 9, 2026
Merged

kjxcodez merged 1 commit into
mainfrom
audit/outbound-provider-rejections

Conversation

@kjxcodez

@kjxcodez kjxcodez commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Problem

Outbound cold outreach emails sent via connected mailboxes are being blocked or rejected by receiving mail servers, corporate email security gateways (e.g. Proofpoint, Mimecast, Microsoft Defender), and Google Workspace / Gmail API infrastructure. Currently, these rejections are improperly classified within the delivery ledger and do not trigger campaign safety halts, allowing campaigns to continue blasting messages and destroying sender domain reputation.

Investigation

Traced the complete outbound delivery path end-to-end:

  1. Campaign -> automation:workflow (apps/desktop/src/main/workers/plugins/automation.ts) & bulk outreach (outreach.ts).
  2. sdk.outreach.sendEmail -> API router -> EmailService.send() (apps/api/src/services/email/email.service.ts).
  3. Outbound provider abstraction in apps/api/src/services/email/email-account.service.ts (enforcing Zero Outbound SMTP / direct Nodemailer elimination).
  4. Direct MIME generation and Google REST API transmission via GmailProvider.sendMessage() (apps/api/src/services/google/gmail.provider.ts).
  5. Asynchronous DSN bounce ingestion and classification via ReconciliationService.pollInbound() (apps/api/src/services/email/reconciliation.service.ts) and parseDsnReport() (packages/schema/src/utils/bounce-classifier.ts).
  6. State machine transitions in apps/api/src/repositories/email-delivery/email-delivery.repository.ts.

Findings

  1. Misclassification of Permanent Spam/Policy Blocks as Transient Network Errors:
    SMTP 554 5.7.1 spam and gateway rejections fall through classifyEmailFailure() to generic PROVIDER, which EmailService.send maps to NETWORK. This treats fatal reputation blocks as transient network hiccups, allowing repeated toxic sends.
  2. False-Positive Hard Bounce Suppression on HTTP 400:
    In gmail.provider.ts, HTTP 400 (malformed MIME, header length, bad characters) is hardcoded as INVALID_RECIPIENT. EmailService.send then permanently suppresses the contact address as HARD_BOUNCE and corrupts the contact status to BOUNCED / INVALID.
  3. Misdiagnosis of Google 403 as Token Revocation:
    In gmail.provider.ts, all HTTP 403 responses (daily sending quota exceeded, bulk sender abuse blocks) are hardcoded to MAILBOX_REAUTH_REQUIRED, falsely claiming OAuth credentials expired.
  4. Rate Limit Cooldown Failure on 429:
    GmailProvider throws SENDER_RATE_LIMITED (rate_limit), but EmailService.send checks PROVIDER_RATE_LIMITED (provider_rate_limited), skipping setProviderCooldown().
  5. Reconciliation DSN Overwrite:
    ReconciliationService.pollInbound() line 707 hardcodes failureCategory: EmailFailureCategory.INVALID_RECIPIENT, discarding the parsed DSN classification (e.g. SPAM_REJECTION, POLICY_REJECTION).
  6. Zero Campaign-Level Circuit Breakers:
    Neither automation.ts nor outreach.ts pauses the campaign when provider or recipient rejections occur.
  7. Zero Domain-Level Throttling / Company Cardinality:
    Burst sending to dozens of contacts at the same company (e.g., 64 contacts at @targetcompany.com) occurs without pacing, triggering gateway spam burst heuristics.
  8. Violation of Ambiguous Send Invariant:
    VALID_DELIVERY_TRANSITIONS permits AMBIGUOUS -> SENDING and FAILED -> SENDING, allowing blind duplicate sends without verifying provider-side acceptance.

Evidence

  • Comprehensive forensic report: docs/forensics/phase-5-item-a-outbound-provider-rejection-audit.md
  • Dedicated reproduction & qualification test suite: apps/api/src/services/email/outbound-provider-rejection-audit.test.ts (8/8 passing).

Root Cause

  • classifyEmailFailure() in email.service.ts lacks branches for SMTP 554, spam filters, and policy rejections, and does not integrate canonical classifyBounce().
  • GmailProvider.sendMessage() uses coarse HTTP status code branching that conflates quota/abuse blocks with auth revocation (403) and malformed requests with invalid recipient addresses (400).
  • Divergent error token naming prevents automated cooldowns on HTTP 429.
  • Lack of campaign-level circuit breakers and domain-level pacing in desktop workers.

Severity

CRITICAL (P0): Severe sender domain reputation destruction, false-positive lead suppression, and broken operator governance.

Recommended Remediation Boundary (Phase 6)

  1. Refactor classifyEmailFailure() to delegate to classifyBounce() in @leadforge/schema.
  2. Differentiate HTTP 403 (quota vs auth) and HTTP 400 (malformed payload vs invalid recipient) in GmailProvider.
  3. Align rate limit error tokens (PROVIDER_RATE_LIMITED).
  4. Preserve parsed DSN categories in ReconciliationService.
  5. Implement campaign-level pause circuit breaker on consecutive provider rejections.
  6. Enforce domain-level pacing to prevent spam burst triggers.
  7. Enforce ambiguous delivery invariant (disallow blind AMBIGUOUS -> SENDING re-dispatch).

Tests / Verification

pnpm --filter api exec vitest run src/services/email/outbound-provider-rejection-audit.test.ts
pnpm --filter api check-types

Result: 8 tests passed, 0 type errors.

Scope

Audit Only. No remediation code or provider modifications were implemented in this PR.

Related Issue

Fixes #24 (Audit Phase 5 Item A)

@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
leadforge-os-api Ready Ready Preview Sep 9, 2026 1:11pm UTC
leadforge-os-marketing Ready Ready Preview Sep 9, 2026 1:11pm UTC

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.

audit(outreach): forensic audit of blocked and provider-rejected outbound emails

1 participant