Skip to content

feat(messaging): send code emails through the sandbox sender without a domain - #633

Draft
Silthus wants to merge 6 commits into
sbx-api/integration-djangofrom
sbx-api/sa2-fallback
Draft

Silthus wants to merge 6 commits into
sbx-api/integration-djangofrom
sbx-api/sa2-fallback

Conversation

@Silthus

@Silthus Silthus commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Part of map #461 (sandbox sender for API sends, slice SA2). Stacked on SA1 (#636, #637, #638, split from #632) through sbx-api/integration-django, a merge of #585, #586 and #638.

Problem

  • A project without a verified domain can't send an email from code: every code send without a from answers 400 sender_not_verified.
  • PostHog's sandbox sender already sends workflow and test emails to organization members, but the send API never picks it.

Merge Danger

Warning

🔴 Merge risk: high · two-way door: a revert brings back sender_not_verified for these sends

Changes

A code send with no from gains a fallback to the sandbox sender:

flowchart LR
  A[code send without from]:::y --> B{default sender verified?}
  B -- yes --> C[own sender]:::b
  B -- no --> D[400 sender_not_verified]:::r
  classDef b fill:#1d4aff,stroke:#1d4aff,color:#fff;
  classDef r fill:#f54e00,stroke:#f54e00,color:#fff;
  classDef y fill:#f9bd2b,stroke:#f9bd2b,color:#000;
Loading
flowchart LR
  A[code send without from]:::y --> B{default sender verified?}
  B -- yes --> C[own sender]:::b
  B -- no --> E{any own verified sender?}
  E -- yes --> D[400 sender_not_verified]:::r
  E -- no --> F[sandbox sender]:::b
  classDef b fill:#1d4aff,stroke:#1d4aff,color:#fff;
  classDef r fill:#f54e00,stroke:#f54e00,color:#fff;
  classDef y fill:#f9bd2b,stroke:#f9bd2b,color:#000;
Loading
  • A project with no verified sender of its own sends through the sandbox sender. Django provisions it on the first send, with the provisioning endpoint's checks. The CDP API finds the new integration even before the read replica has it (feat(cdp): refuse a sandbox code send before it is queued #637).
  • A from on the sandbox address or its domain answers 400 sender_not_verified on from. A reply_to answers 400 invalid_request on reply_to.
  • cdp-api's sandbox refusals reach the caller as is. Its 503 sandbox_check_failed becomes 503 delivery_unavailable, saying nothing was sent.
  • The usage event and the first-send event carry delivery_mode. The response doesn't.
  • A send by template id keeps its stored sender.

How did you test this code?

Test seams: the send/email/ route through the DRF client with cdp-api mocked (TestTransactionalEmailThroughTheSandboxSender).

Test rationale: the fallback, the verified sender without a default, the flag off, each refused from and reply_to, each forwarded refusal, and the mode on a first send and a replay.

Proofs (saved with fleet proof):

  • Live run through posthog-node a57fb728bbf: a member gets the email, a non-member and a fourth send past a cap of 3 get 400, from and reply_to get 400, and a replay claims nothing.
  • Cross-version: with cdp-api before SA1, a non-member send answers 202 and the worker skips it. The old Django ignores the new delivery_mode answer.
  • Shadow diff against the stack without SA1 and feat(messaging): send code emails through the sandbox sender without a domain #633: own-sender sends are unchanged (answers, deliveries, From, Reply-To). Every no-sender difference is the sandbox fallback or one of its refusals. A keyed record written by either cdp-api version replays in the other.
  • Prod shape: a gap until read-only production access exists on 2026-11-02.

CI: this PR sits on an integration base until the sandbox stack (through #587) and the send stacks (through #584 and #585) reach master.

Release status

  • This change is behind a feature flag and is not available to users

Docs update

  • docs/internal/workflows-email-sending-tiers.md gains "Sandbox sends from code".

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5)

Skills: /tdd, /writing-tests, /improving-drf-endpoints, /writing-user-facing-copy, /qa-swarm, /pr-description, /writing-pr-descriptions

Question Answer Who Where
When does a code send use the sandbox? Only without from, and only when the project has no verified sender of its own 👍 Accepted default map #461, 2026-10-26
What answers a named sandbox from? sender_not_verified on from, replacing the planned sender_not_allowed, which this base doesn't have 🤖 Agent this PR
What answers a check cdp-api can't run? 503 delivery_unavailable, saying nothing was sent 🤖 Agent this PR
How is the old-cdp-api window closed? Deploy order, because cdp-api ignores fields it doesn't know 🤖 Agent this PR
What does a blocked organization's code send answer? sender_not_verified, as before the sandbox existed, so the answer doesn't reveal the block 🤖 Agent this PR

Involvement: one design pass, a 7-lens security swarm, a 5-lens fix round and one clean final read (GPT-6.1 Sol and Claude Opus 5.5), and a clean GPT-6.1 Sol read of each later base merge; live run, two cross-version runs and a shadow diff captured.

More context for AI agents
  • Session: lane sbx-api of map Map: send code-managed email templates with one PostHog call #461, Claude Code.
  • Decisions: the default-sender resolver still excludes the sandbox; the omitted-from path is the only way in.
  • Follow-ups: a template send by id whose stored sender is the sandbox ignores a caller's reply_to instead of refusing it; a sandbox suspension's sending_suspended text points at the Reputation tab; the worker's captureBlocked calls, from the sandbox stack, still wait with no deadline.
  • Read closely: code_send_sender in transactional_email_workflows.py, and _accepted_send in transactional_email.py.

🤖 Generated with Claude Code

Silthus and others added 3 commits November 1, 2026 14:00
…sender

A code send that names no from, in a project with no verified sender of
its own, now goes out through the sandbox sender. A named sandbox from
and a reply_to are refused. The pipeline's sandbox refusals reach the
caller, and the usage and first-send events carry the delivery mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A code send provisions the sandbox integration with the endpoint's checks,
not its event, and a blocked organization gets sender_not_verified before
the CDP API runs. The CDP API and the worker find a new integration the
replica lacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed sender

A sender with no address is the default one, so the caller's address no
longer needs to travel beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus and others added 3 commits November 1, 2026 23:45
#585 reports a project's first code send through one operation; the merge keeps that operation and passes it this part's delivery_mode property.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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