Skip to content

feat(messaging): point a sender refusal at the from a send names - #579

Closed
Silthus wants to merge 11 commits into
email-v2/06-template-editorfrom
email-v2/08-admission-checks-django
Closed

Silthus wants to merge 11 commits into
email-v2/06-template-editorfrom
email-v2/08-admission-checks-django

Conversation

@Silthus

@Silthus Silthus commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Part of the code-first rebuild of the email send stack; replaces parts of PostHog#121850 (refuse a send the send checks would skip).

Problem

  • A developer whose code send names a from that no verified sender covers gets 400 sender_not_verified with attr: null. The answer does not say which field to fix.
  • An SDK or agent that maps attr to the field in error has nothing to point at. The same code without a from means the project has no default sender, a different fix.

Impact

Pays into Grow Workflows through billed workflows_emails actions per paying org: a developer fixes the right field on the first refused send. In scope: Messaging, email API sends.

Merge Danger

Note

🟢 Merge risk: low · two-way door: a revert answers attr: null again

  • Only the attr of one refusal changes, on send/email/, behind workflows-email-from-code. The code, status and detail stay the same.
  • Template sends by id are unchanged: they cannot name a from.

Changes

Kind Files Lines
Production 3 +10 −4
Tests 1 +19 −2
  • A send that names a from no verified sender covers answers sender_not_verified with attr: "from". That holds for a code send and for a send by template.key.
  • A send without from on a project with no verified default sender keeps attr: null, since the caller sent no field to fix.
  • The refusal reuses master's TransactionalEmailRejected path and the {type, code, detail, attr} shape, with type: invalid_request and the status from master's code map.
  • TransactionalEmailRejected subclasses DRF's ValidationError, so exceptions_hog reads its field. A dict detail on a plain APIException answers 500 there.
  • The code-aware PIPELINE_REFUSALS decoder for cdp-api's answers stays as it is on master.

How did you test this code?

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

Test rationale:

  • The sender row of the send-options table now expects attr: "from" for a key send with an unverified from. It caught the null before the fix.
  • A new row: a code send without from on a project with no default sender answers attr: null. Without it, a class that always names from would pass.
  • Both sender rows assert type, code and attr: a refusal that turns into validation_error, or skips the status map, fails them.

Proofs on this head c8a4107 (saved with fleet proof):

  • Live run: Django at this head, cdp-api and both CDP workers at the node stack f9abed0, maildev as the sink. 21 requests through the public API. A code send from an unverified domain answers 400 with {"type": "invalid_request", "code": "sender_not_verified", "attr": "from"} and sends nothing.
  • Prod shape: a gap, accepted by the standing rule until read-only production access exists on 2026-11-02.
  • Lane gate on this head: ruff, the IDOR check, the migration checks, repo-wide mypy, and the messaging and workflows tests pass.
Earlier live run, 2026-10-31
  • Live run, 2026-10-31. Django at the top of the stack (3a30f51c529), cdp-api and both CDP workers at feat(cdp): send a template from the workflow version Django graded #584 (f9abed0841c), mail caught in maildev.

    Send Answer
    Code send from an unverified domain 400 sender_not_verified, attr: "from", nothing sent
    Send by template.key from an unverified domain 400 sender_not_verified, attr: "from", nothing sent
    MCP send tool from an unverified domain Tool error that names the from field
  • Prod shape: a gap until read-only production access exists on 2026-11-02.

CI: the fork has no Depot runners, so those jobs never start. Semgrep Checks Pass fails on every fork PR, because its path filter job runs only for the PostHog owner; semgrep with the repo's rules passes locally on the changed files. The lane check reports cross_lane=false.

Release status

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

Docs update

None.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

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

Question Answer Who Where
Does this PR keep its own refusal reader? No; it reuses master's refusal code and keeps the code-aware PIPELINE_REFUSALS decoder 👤 Human map #461, 2026-10-28
Does Django check that the sender can send before it forwards a code send? Yes, already: on each send Django resolves the sender from the project's email integrations and refuses one that is missing or unverified. No new check 🤖 Agent pending approval
Does a sender refusal get its own code, such as sender_not_allowed? No; attr: "from" versus attr: null tells the two cases apart, and no client reads a second code 🤖 Agent pending approval
May the code carry comments? No; names carry the reasons 👤 Human map #461, 2026-10-25

Involvement: one brief with Michael's shrink decision; one review round (GPT-6.1 Sol) answered; QA swarm on 2026-11-01 with two panel rounds (GPT-6.1 Sol review, Opus 5.5 fresh eyes) and two final reads, the last clean: 2 findings fixed, none rejected; live run captured.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

🤖 CI report

ℹ️ MCP UI apps size — 34 app(s), 27969.1 KB JS

Built size of each MCP UI app (main.js + styles.css).

App JS CSS
debug 844.3 KB 209.1 KB
action 767.7 KB 209.1 KB
action-list 835.3 KB 209.1 KB
cohort 766.7 KB 209.1 KB
cohort-list 834.3 KB 209.1 KB
email-template 766.6 KB 209.1 KB
error-details 771.6 KB 209.1 KB
error-issue 768.9 KB 209.1 KB
error-issue-list 836.7 KB 209.1 KB
experiment 832.4 KB 209.1 KB
experiment-list 836.1 KB 209.1 KB
experiment-results 834.9 KB 209.1 KB
feature-flag 834.7 KB 209.1 KB
feature-flag-list 838.4 KB 209.1 KB
feature-flag-testing 770.0 KB 209.1 KB
inline-scan 767.2 KB 209.1 KB
insight-actors 833.8 KB 209.1 KB
llm-costs 831.1 KB 209.1 KB
session-recording 768.9 KB 209.1 KB
survey 768.3 KB 209.1 KB
survey-global-stats 831.1 KB 209.1 KB
survey-list 836.0 KB 209.1 KB
survey-stats 831.1 KB 209.1 KB
trace-span 767.1 KB 209.1 KB
trace-span-list 835.2 KB 209.1 KB
vision-observation-list 834.4 KB 209.1 KB
workflow 767.0 KB 209.1 KB
workflow-list 834.7 KB 209.1 KB
workflow-setup-status 766.7 KB 209.1 KB
query-results 1086.5 KB 209.1 KB
render-ui 1162.4 KB 209.1 KB
visual-review-snapshots 771.6 KB 209.1 KB
workflow-data-suggestions 769.4 KB 209.1 KB
workflow-sending-rules 768.0 KB 209.1 KB
ℹ️ MCP agent API — agent-facing tool changes

What this PR changes for agents, from the tool schema snapshots and tool definitions.

Tools changed (1):

Tool Params Scopes Annotations Schema chars
workflows-send-email description changed

@Silthus
Silthus force-pushed the email-v2/06-template-editor branch from bb9ed7b to 4117342 Compare October 25, 2026 14:59
@Silthus
Silthus force-pushed the email-v2/08-admission-checks-django branch from 49cab17 to 24881fa Compare October 25, 2026 15:00
@Silthus
Silthus force-pushed the email-v2/06-template-editor branch from 4117342 to dc7683f Compare October 25, 2026 17:38
@Silthus
Silthus force-pushed the email-v2/08-admission-checks-django branch from 24881fa to 801bdfc Compare October 25, 2026 17:38
@Silthus
Silthus force-pushed the email-v2/06-template-editor branch from dc7683f to d321dd8 Compare October 25, 2026 22:27
@Silthus
Silthus force-pushed the email-v2/08-admission-checks-django branch from a5d494f to 1d7fe4c Compare October 25, 2026 22:28
@Silthus
Silthus force-pushed the email-v2/06-template-editor branch from d321dd8 to 6730e40 Compare October 26, 2026 01:11
@Silthus
Silthus force-pushed the email-v2/08-admission-checks-django branch from 1d7fe4c to 0eeb308 Compare October 26, 2026 01:11
@Silthus
Silthus force-pushed the email-v2/06-template-editor branch from 6730e40 to d3da69f Compare October 26, 2026 02:34
@Silthus
Silthus force-pushed the email-v2/08-admission-checks-django branch 2 times, most recently from 25f064a to 0f434a3 Compare October 26, 2026 05:29
@Silthus
Silthus force-pushed the email-v2/06-template-editor branch from 2332fd3 to 72e6cf9 Compare October 31, 2026 12:51
@Silthus
Silthus force-pushed the email-v2/08-admission-checks-django branch from 0f434a3 to f3b92ce Compare October 31, 2026 13:47
@Silthus Silthus changed the title feat(messaging): answer a send the checks refuse with why and fix feat(messaging): point a sender refusal at the from a send names Oct 31, 2026
A code send, or a send by `template.key`, that names a `from` on no
verified domain now answers `sender_not_verified` with `attr: "from"`, so
an SDK can show which field to fix. A send without `from` whose project
has no verified default sender still names no field.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@Silthus
Silthus force-pushed the email-v2/06-template-editor branch from 7b2198c to 1390333 Compare October 31, 2026 16:05
@Silthus
Silthus force-pushed the email-v2/08-admission-checks-django branch from f3b92ce to 1c8455d Compare October 31, 2026 16:09
@Silthus

Silthus commented Nov 2, 2026

Copy link
Copy Markdown
Owner Author

Moved to PostHog#127181.

@Silthus Silthus closed this Nov 2, 2026
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