fix(sales): diagnose inbound-only mail path behind blocked sender trust - #102
Conversation
send:setup now resolves MX records and flags Cloudflare Email Routing as inbound-only, so the DKIM warning points at the real first step (connect an outbound provider) instead of implying a selector exists to find. The sender-setup guide's fix order leads with the provider step. Physical postal address and dkimSelector stay empty: no real address is published for this business and no DKIM records exist in DNS, so neither value may be invented.
The active-operator surface gate regenerates the tracked sender/market docs with a fixed clock and byte-compares them. Regenerate through that exact path so the 11/10 proof-run brief, parity readiness, benchmark, matrix, and guide carry the new inbound-only mail path evidence with the canonical generated date.
There was a problem hiding this comment.
nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Warning Review limit reached
Next review available in: 36 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (9)
Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 36ee83aa5a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const inboundOnly = mx.every((host) => /^route\d+\.mx\.cloudflare\.net\.?$/.test(host)); | ||
| checks.push({ name: "Outbound mail path", status: inboundOnly ? "missing" : "found", domain: mx.join(", ") }); |
There was a problem hiding this comment.
Base outbound readiness on sender configuration, not MX
MX records describe where inbound mail is received, not how the domain sends mail. A valid setup using Cloudflare Email Routing for inbound mail and Resend/Postmark/SendGrid for outbound will continue to show this check as missing, while a domain with non-Cloudflare MX records but no outbound provider is reported as found. This makes the new diagnosis unreliable in both directions; derive outbound readiness from an explicitly configured sending provider and its authentication instead.
Useful? React with 👍 / 👎.
| } catch { | ||
| return []; |
There was a problem hiding this comment.
Distinguish failed MX lookups from absent records
When DNS is temporarily unavailable or returns SERVFAIL/ETIMEOUT, this catch converts the resolver failure into an empty result, and the caller consequently states that no MX records exist and replies will bounce. Running send:setup or regenerating tracked operator artifacts during a transient resolver failure therefore produces a false configuration diagnosis and potentially instructs the operator to change valid DNS; preserve the error so lookup failure can be reported separately or retried.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f03b9a46db
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| checks.push({ name: "Outbound mail path", status: "missing", domain: `no MX records for ${domain}` }); | ||
| warn("no MX records for sender domain", "No MX records exist, so replies to the sender address would bounce. Add MX records or use a sender domain that can receive replies before cold email."); |
There was a problem hiding this comment.
Feed the no-MX failure into the email send gate
When the configured sender has a postal address and DKIM selector but genuinely has no MX records, this branch makes send:setup warn that replies will bounce, but sendChannelGuidance() still computes emailReady solely from those local config values. Consequently, checked consumers such as update-prospect-pipeline.mjs, complete-prospect-batch-send.mjs, and the prospect outbox permit and recommend email despite this new failure; propagate the setup failure into the shared email-readiness gate so operators cannot proceed with a reply address known to reject mail.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4894f85fbd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| 1. Connect an outbound sending provider for \`${setup.senderDomain || "the sender domain"}\` (Cloudflare Email Routing forwards inbound mail only and cannot send). | ||
| 2. Add a real sender postal address to \`senderPhysicalAddress\` in \`growth-brain/ops/agency-config.json\` (business address, PO box, or private mailbox). | ||
| 3. In the outbound provider, enable DKIM and copy the selector. | ||
| 4. Add the provider's DKIM TXT record in Cloudflare DNS at \`${dkimHost}\`. |
There was a problem hiding this comment.
Preserve the provider-supplied DKIM record type
When the operator chooses Outlook/Microsoft 365—one of the providers recommended by this change—the provider supplies selector1 and selector2 CNAME records rather than a DKIM TXT record. This unconditional instruction can therefore make the operator create the wrong DNS record type, preventing provider verification and leaving DKIM blocked; instruct them to publish the exact record type and values supplied by the provider, as the previous conditional wording did.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a33702845b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| } else { | ||
| checks.push({ name: "Outbound mail path", status: "missing", domain: `no MX records for ${domain}` }); | ||
| warn("no MX records for sender domain", "No MX records exist, so replies to the sender address would bounce. Add MX records or use a sender domain that can receive replies before cold email."); |
There was a problem hiding this comment.
Honor implicit MX fallback before declaring replies broken
When a sender domain has no MX record but its A/AAAA address accepts SMTP, standard mail routing treats that address as an implicit MX target. This branch instead states that replies will bounce and marks setup as failed, so a valid receiving configuration is misdiagnosed; verify the implicit host before emitting the no-reply-path warning.
Useful? React with 👍 / 👎.
| const inboundOnly = mx.every((host) => /^route\d+\.mx\.cloudflare\.net\.?$/.test(host)); | ||
| checks.push({ name: "Outbound mail path", status: inboundOnly ? "missing" : "found", domain: mx.join(", ") }); |
There was a problem hiding this comment.
Reject null MX records as a reply path
When a domain publishes a null MX record (0 .) to declare that it accepts no mail, mx.length is nonzero and the root exchange does not match the Cloudflare pattern, so this records the path as found. Such a sender cannot receive replies and may let send:setup pass once the other checks are satisfied; detect the null-MX sentinel and report it as unavailable.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 69385bde80
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| 4. If the mail provider gives a DKIM TXT record, add it in Cloudflare DNS at \`${dkimHost}\`. | ||
| 5. Run \`npm run send:setup\`. | ||
| 6. If it is clean, email can join contact forms and DMs as an outbound route. | ||
| 1. Connect an outbound sending provider for \`${setup.senderDomain || "the sender domain"}\` (Cloudflare Email Routing forwards inbound mail only and cannot send). |
There was a problem hiding this comment.
Only prescribe Cloudflare remediation when detected
When send:configure -- --sender-email=... switches to a domain that uses another mail host—or when an outbound provider is already configured—send:guide still claims Cloudflare Email Routing is in use and instructs the operator to connect a provider, even if the setup checks pass. Derive this fix-order step from the actual inbound-only warning/check instead of emitting it unconditionally; the HTML instructions have the same issue.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
The outbound mail path still routes through Cloudflare Email Routing, which forwards inbound mail only and blocks cold email until sender trust is clean. PR #102 already diagnosed the inbound-only mail path and explained that the physical address and DKIM selector cannot be invented when no business address is published and no DKIM records exist in DNS. DNS now exposes a real DKIM record at resend._domainkey.tinystudio.io (`p=` RSA key published by Resend), so the sender trust check can be populated from the live selector instead of guessing. Save dkimSelector as `resend` in agency-config.json and widen the discovery list to cover the modern outbound providers a founder-led studio is most likely to use (Resend, Postmark, Mailgun, Amazon SES, Mailjet, Brevo, SparkPost, Klaviyo, HubSpot, Mailchimp, Elastic Email, MailerSend, Fastmail, Tutanota). The selector list still does not invent values; it only lets the check discover a real selector the provider has already published. After this change: - DKIM: ready (resend._domainkey.tinystudio.io) - Sender trust warnings drop from three rules to one: the postal address. - export-sender-setup-guide now reports the actual configured selector (or a discovered candidate) in the dry-run command and adapts the fix-order steps to the current state, so the guide stops telling the operator to enable DKIM that is already enabled. - The four tracked operator artifacts that read the sender setup state (11-10-proof-run, market-parity-readiness, competitive-proof-matrix, market-parity-benchmark-2026) and the sender-setup-guide markdown and HTML are regenerated through the same fixed-clock path the active operator surface gate uses, so the byte-identical check stays green and the operator sees the actual sender trust state. senderPhysicalAddress stays empty: no physical address is published on tinystudio.in or any privacy/terms/contact surface, so the value cannot be invented. The remaining sender warning points at that single blocker. Co-authored-by: Nish <nish3451@users.noreply.github.com>
What
npm run send:setupstayed blocked on two empty config values —senderPhysicalAddressanddkimSelectoringrowth-brain/ops/agency-config.json—with no diagnosis of why they are empty. This PR makes the block precise and
actionable; it does not invent either value.
Root cause (verified, not guessed)
site explicitly publishes "No base city or office address is stated", whois is
privacy-redacted, and no address exists in the repo. CAN-SPAM requires a real
address in commercial mail; fabricating one would put a false compliance footer
(
appendEmailComplianceFooter) into real emails.common selectors). MX is
route*.mx.cloudflare.net— Cloudflare Email Routing,which is inbound-only and cannot send outbound mail. No outbound provider
is wired into this repo, so no selector exists to discover.
Both values are real-world facts only the operator can supply.
What changed
scripts/check-outbound-sender-setup.mjsnow resolves MX records and emits anOutbound mail path check. When MX is Cloudflare Email Routing (and no DKIM
selector is configured), it warns
outbound mail path is inbound-onlywith theexact unblock sequence: connect a sending provider (Google Workspace, Zoho
Mail, Outlook, Resend, Postmark, SendGrid), enable DKIM there, save the exact
selector. No-MX domains get a bounce warning.
scripts/export-sender-setup-guide.mjsFix Order now leads with the providerstep instead of implying a selector exists to find.
surface-gate path (byte-identical to
test-active-operator-surfaces.mjs),so
send:setup, the guide, the 11/10 proof-run brief, market parityreadiness, competitive matrix, and benchmark all carry the new evidence.
Verification
npm run send:setupnow shows SPF found, DMARC found, outbound mail pathmissing (inbound-only), DKIM discovery missing — with precise guidance.
npm run cipasses end-to-end (exit 0), including the active-operatorsurfaces byte-gate.
Operator next step (Nish)
private mailbox) →
send:configure -- --physical-address="..." --dry-run.record at
<selector>._domainkey.tinystudio.ioin Cloudflare DNS →send:configure -- --dkim-selector=... --dry-run, then apply.Until then the block is intentional and correct: contact forms/DMs remain the
recommended channel.