Skip to content

[PHP-WASM] Expose sendmail stdin as a live event stream - #3996

Merged
adamziel merged 1 commit into
trunkfrom
codex/sendmail-mail-capture
Jul 15, 2026
Merged

[PHP-WASM] Expose sendmail stdin as a live event stream#3996
adamziel merged 1 commit into
trunkfrom
codex/sendmail-mail-capture

Conversation

@bgrgicak

@bgrgicak bgrgicak commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

PHP can write a message to sendmail through mail(), popen(), or proc_open(), but PHP-WASM has no reusable transport for observing those bytes. This adds a sendmail-compatible null transport. It never delivers mail. It emits sendmail.spawned with raw stdin as a live ReadableStream<Uint8Array>.

Registration is explicit:

import { sendmailSpawnHandler } from '@php-wasm/util';

php.setCommandSpawnHandler('sendmail', sendmailSpawnHandler(php));
php.addEventListener('sendmail.spawned', async (event) => {
	if (event.type !== 'sendmail.spawned') {
		return;
	}
	const rawMessage = await new Response(event.stdin).arrayBuffer();
	// Parse or relay rawMessage.
});

The event fires with the first stdin chunk, before PHP finishes writing. The transport stays alive until stdin closes. Empty input exits with EX_TEMPFAIL and emits no event. Input above 20 MB errors the stream and exits with status 1. Cancelling the stream stops delivery to that reader but keeps draining stdin, so PHP can finish writing and receive the transport's exit status. There is no backpressure to PHP; a slow listener may buffer the full message.

Node tests cover mail(), proc_open(), delayed input, empty and oversized input, and runtime rotation. Browser tests cover live delivery to multiple listeners and cancellation while PHP continues writing.

This PR does not install the transport by default. #4063 installs it in remote Playground instances.

#4064 provides the marked stdin event transport and per-listener streams used here.

Comment thread packages/php-wasm/util/src/lib/concat-bytes.ts Outdated
@bgrgicak bgrgicak self-assigned this Jul 10, 2026
@bgrgicak
bgrgicak marked this pull request as ready for review July 10, 2026 11:29
@bgrgicak
bgrgicak requested review from a team and brandonpayton and removed request for a team July 10, 2026 11:29
@akirk

akirk commented Jul 13, 2026

Copy link
Copy Markdown
Member

I think it'd be easier to find the code that mimicks sendmail if we put it into a spawn-handlers/sendmail.ts with the filename reflecting the command it pretends to be.

Comment thread packages/playground/wordpress/src/boot.ts Outdated
Comment thread packages/php-wasm/util/src/lib/set-sendmail-spawn-handler.ts Outdated
@bgrgicak
bgrgicak requested a review from adamziel July 13, 2026 09:03
@bgrgicak bgrgicak changed the title [PHP-WASM] Capture outbound mail via a sendmail spawn handler Jul 13, 2026
Comment thread packages/php-wasm/util/src/lib/spawn-handlers/sendmail.ts Outdated
Comment thread packages/php-wasm/util/src/lib/spawn-handlers/sendmail.ts Outdated
Comment thread packages/php-wasm/util/src/lib/spawn-handlers/sendmail.ts Outdated
Base automatically changed from codex/php-command-spawn-handlers to trunk July 14, 2026 15:55
@adamziel
adamziel requested review from a team, JanJakes and Copilot July 14, 2026 15:55
@adamziel
adamziel force-pushed the codex/sendmail-mail-capture branch from 8f0db93 to 9dd1d4d Compare July 14, 2026 16:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds deterministic outbound email capture for PHP-WASM by intercepting sendmail spawns and emitting email.received events, with WordPress boot registering the handler by default.

Changes:

  • Introduces a sendmail command-specific spawn handler that buffers stdin and dispatches an email.received event.
  • Adds per-command spawn handler routing in PHP (generic vs command-specific precedence; survives runtime rotation).
  • Adds Node + WordPress integration tests covering capture behavior, error paths, and runtime rotation.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/playground/wordpress/src/test/email-capture.spec.ts Adds a WordPress-level test asserting wp_mail() triggers email.received.
packages/playground/wordpress/src/boot.ts Registers the sendmail command-specific spawn handler during WordPress boot.
packages/php-wasm/util/src/lib/spawn-handlers/sendmail.ts Implements the sendmail spawn handler and defines PHPEmailReceivedEvent.
packages/php-wasm/util/src/lib/index.ts Exports sendmail handler APIs and moves byte-concat exports to a dedicated module.
packages/php-wasm/util/src/lib/concat-bytes.ts Adds reusable byte/ArrayBuffer concatenation helpers.
packages/php-wasm/universal/src/lib/universal-php.ts Extends the PHPEvent union to include PHPEmailReceivedEvent.
packages/php-wasm/universal/src/lib/php.ts Adds dispatcher-based spawn routing with command-specific overrides.
packages/php-wasm/universal/src/lib/php.spec.ts Adds unit test coverage for the new spawn routing behavior.
packages/php-wasm/universal/src/lib/index.ts Re-exports PHPEmailReceivedEvent type for consumers.
packages/php-wasm/node/src/test/php-email-capture.spec.ts Adds Node runtime tests for sendmail capture, size limits, and runtime rotation.
packages/php-wasm/node/project.json Registers the new Node test file in project test targets.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/php-wasm/util/src/lib/spawn-handlers/sendmail.ts Outdated
Comment thread packages/php-wasm/util/src/lib/spawn-handlers/sendmail.ts Outdated
Comment thread packages/php-wasm/util/src/lib/spawn-handlers/sendmail.ts Outdated
Comment thread packages/php-wasm/universal/src/lib/php.ts
Comment thread packages/php-wasm/util/src/lib/spawn-handlers/sendmail.ts Outdated
@adamziel adamziel changed the title [PHP-WASM] Capture outbound email via a sendmail spawn handler Jul 14, 2026
@adamziel adamziel changed the title [PHP-WASM] Capture outbound sendmail stdin as a stream Jul 14, 2026
@adamziel
adamziel force-pushed the codex/sendmail-mail-capture branch from 857e84e to 5bdfb92 Compare July 14, 2026 23:59
adamziel added a commit that referenced this pull request Jul 15, 2026
`PHPWorker` can now deliver an event whose `stdin` remains a live
`ReadableStream` across the browser worker boundary. Comlink applies
transfer handlers only to the top-level value, so a stream stored in an
event's `stdin` property otherwise reaches `postMessage()` without a
transfer list and structured cloning fails.

The event handler serializes `stdin` through a dedicated stream handler.
It transfers the stream directly when the runtime supports that
operation and uses the existing `MessagePort` bridge otherwise. The
bridge transfers owned chunk copies because `ReadableStream.tee()`
branches share chunk objects and detaching one branch's buffer must not
invalidate the others. Consumer cancellation travels back over the port
and cancels the source reader.

A stream can only be transferred once, so `PHPWorker` gives each
listener its own tee branch. Slow listeners may queue unread chunks
because this transport does not coordinate backpressure between
branches.

Browser-worker tests cover native stream transfer and the forced
`MessagePort` fallback, including incremental delivery to two listeners
and source cancellation after both tee branches cancel.

This is the worker transport required by #3996. It adds no sendmail
behavior.
@adamziel
adamziel force-pushed the codex/sendmail-mail-capture branch from ec7e0de to a49cd15 Compare July 15, 2026 11:49
@adamziel adamziel changed the title [PHP-WASM] Emit sendmail.spawned with a stdin stream Jul 15, 2026
@adamziel
adamziel force-pushed the codex/sendmail-mail-capture branch from 1023fef to 49267b4 Compare July 15, 2026 17:21
@adamziel
adamziel changed the base branch from trunk to codex/transfer-event-stdin-streams July 15, 2026 17:21
@adamziel
adamziel force-pushed the codex/transfer-event-stdin-streams branch from 20f5efe to beb8a52 Compare July 15, 2026 17:49
@adamziel
adamziel force-pushed the codex/sendmail-mail-capture branch from 49267b4 to 24034e3 Compare July 15, 2026 17:49
Base automatically changed from codex/transfer-event-stdin-streams to trunk July 15, 2026 18:21
@adamziel
adamziel force-pushed the codex/sendmail-mail-capture branch 2 times, most recently from 7a67a7a to 8ef5dcb Compare July 15, 2026 19:37
@adamziel
adamziel force-pushed the codex/sendmail-mail-capture branch from 8ef5dcb to f5a59cd Compare July 15, 2026 19:41
@adamziel
adamziel merged commit a3c54bd into trunk Jul 15, 2026
53 checks passed
@adamziel
adamziel deleted the codex/sendmail-mail-capture branch July 15, 2026 20:08
adamziel added a commit that referenced this pull request Jul 15, 2026
Adds `sendmailSpawnHandler()` (introduced in #3996) to PHP.wasm objects
instantiated in the `remote.html` web worker. The handler catches any
emails sent by WordPress and re-exposes it through an event listener as
follows:

```ts
php.setCommandSpawnHandler('sendmail', sendmailSpawnHandler(php));

// In the website package:
php.addEventListener('sendmail.spawned', async (event) => {
	if (event.type !== 'sendmail.spawned') {
		return;
	}
	const rawMessage = await new Response(event.stdin).arrayBuffer();
	// Parse or relay rawMessage.
});
```

The goal is to expose these emails in the UI in a follow-up PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment