Conversation
This was referenced Sep 21, 2026
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.
Mechanism
Closes #3534. Direct
datamachine/send-emailcalls with no acting principal currently fail (email_auth_ref_required, oremail_mailbox_forbiddenwith an explicit default ref), which has been breaking registration/welcome emails and anonymous contact-form mail since the #3218 gate landed. The queued path already works for the same callers because it stampsissuer_type: systemandEmailAuth::can_use_default()honors theprincipal_less_system+_trusted_executioncontext.This PR exposes that existing path to direct sends — no new policy branch, no new mailbox model:
send-emailinput schema gainssystem(boolean, defaultfalse), documented as server-side only.execute(), whensystem: trueand there is no acting user and no acting agent, the sender is resolved viaEmailAuth::resolve_mailbox_for_principal( $account, 'send', array( 'principal_less_system' => true ) )— the samecan_use_default()branch the queued worker's system path reaches. The resolved mailbox identity becomesfrom_email/reply_to/from_name(instead of the admin_email fallback).auth_refstill takes precedence when both are supplied (pre-existing branch untouched).REST / MCP / agent guard
The flag is stripped from input before normalization (it never reaches the payload, logs, or grant material) unless every eligibility check in
SendEmailAbility::canSendAsSystem()holds:PermissionHelper::acting_user_id() <= 0— a logged-in non-manager passingsystem: truegets the flag stripped and is denied by the existingemail_auth_ref_requiredgate; managers keep their pre-existing legacy-sender behavior (the flag never applies to them — their sender stays the admin_email fallback).null === PermissionHelper::get_acting_agent_id()— MCP / agent bearer-token callers are always stripped.REST_REQUEST) are stripped unless executing inside the trustedPermissionHelper::run_as_authenticated()seam.Point 3 deserves explanation: the primary reported consumer (registration/welcome email in extrachill-users) runs
ec_send_email()insiderun_as_authenticated(0)during REST-handled registration POSTs. A blanketREST_REQUESTstrip would keep the reported bug unfixed. External REST/MCP tool callers can never reach the honored branch anyway:checkPermission()denies any caller without a principal, and any caller with a principal is covered by checks 1–2. This mirrors how_mailbox_grantis protected — not by refusing REST requests, but by making the capability producible only from trusted server-side code paths.System mailbox option
New
datamachine_email_system_mailboxsite option (string account name, defaultdefault), consulted by that path. Naming a dedicated transactional account later requires no code change. Note that resolution goes through the existingcan_access()policy untouched: a named site-scope account is currently denied under a principal-less context (email_mailbox_forbidden) — exactly as the queued system path behaves today — so onlydefaultis usable as the system mailbox until a policy change deliberately extends principal-less access to site-scope named accounts (deliberately out of scope here, and away from #3510's active rebase). The option is forward-plumbed so that change lands without touching this path.CLI:
wp datamachine email mailboxesRedacted operator listing (account, scope, owner id, IMAP host, IMAP user, plus a
systemmarker column). Backed by a newEmailAuth::get_mailbox_index()— the storage enumeration lives in the provider; the CLI never parsesdatamachine_auth_databy hand. Passwords/tokens are never included (asserted by test).Tests
New
tests/send-email-system-flag-smoke.php(pure-PHP runtime smoke, realEmailAuth+ realSendEmailAbility, 35 assertions), covering:system: truewith no principal authorizes the default mailbox and dispatches with the mailbox identity.system: truefrom a logged-in non-manager (and from an acting agent) is denied with the existingemail_auth_ref_requiredcode; manager path unchanged.systemis stripped for REST origin outside the trusted seam, and honored inside it.auth_ref_unresolved; site-scoped named account → existingemail_mailbox_forbiddenpolicy).get_mailbox_index()/ CLI redaction contract (no credential fields, no secrets in output).All pre-existing email-area smoke tests still pass (
send-email-template-smoke121 assertions,named-mailbox-delegation-smoke,named-mailbox-security-contract-smoke,abilities-send-email-load-order-smoke,cli-global-options-smoke,email-reply-sent-copy-smoke,legacy-email-upgrade-auth-smoke,auth-ref-handler-config-smoke).php -lclean on all changed files;homeboy review audit --changed-since origin/main --profile pr→ pass.Lint note:
homeboy review lint --changed-onlyreports 6 findings — all on lines byte-identical to origin/main (verified by linting main's own file versions, which produce the same findings at main's line numbers). No new findings are introduced by this diff; fixing the pre-existing patterns is out of scope here to keep the diff tight for #3510's parallel rebase.Scope vs #3510 / extrachill-network#235
ec_send_email()passessystem => truewhen there is no acting user).EmailAuthchange is an added read-onlyget_mailbox_index()method;can_use_default(),can_access(), andresolve_mailbox()are untouched.systemkey forwarded through a queued payload is inert — non-manager issuers can't queue without anauth_ref(queue gate denies first), and with anauth_refthe pre-existingauth_refbranch takes precedence inexecute().Closes #3534