Capture the email an app sends
Capture application mail locally, without sending it to an external recipient, and show it to the participant in a private inbox.
This page describes local capture. For fresh hosted inboxes, see real email receiving; that route has a separate parsed renderer, strict remote-asset blocking and private lease lifecycle.
The catch captures application mail without sending it to an external recipient.
humanish gives each participant an address and a matching
/inbox/for/<address-digest> URL. The same address scope applies to list, message,
plain view, latest-message and JSON routes. Going back from a missing message
stays in that scope. An unknown scope is empty; it never falls back to all mail.
A participant without an assigned address receives no inbox instruction.
This works with separate participant worlds, shared worlds, and an external catch. Sharing an application world does not require sharing an inbox. If a study deliberately assigns the same address to two participants, both see that address's mail. Generated addresses that collide after normalization are made distinct. A message addressed to several participants appears in each recipient's inbox and retains its actual To field.
/inbox remains available and is labeled Shared operator inbox. Address digests
are routing identifiers, not authentication credentials. The catch is study
infrastructure, not a mailbox service with hostile-tenant isolation. Keep its network
exposure appropriate for test mail. The read-only listener cannot expose the private
/deliveries drain, even when supplied a valid drain token. The capture listener
retains its existing optional bearer-token protection for that endpoint.
A standalone catch tracks its generated files. Reusing its directory with a changed
--recipient roster or an emptied delivery log removes obsolete message, latest and
recipient routes; unrelated files are untouched.
Local browser studies
Run a catch on the same host as the local study:
humanish comms catch --port 8025 --dir .humanish/mail-catchConfigure your app's email transport to send to http://127.0.0.1:8025
using its supported HTTP send configuration, or add --smtp-port 1025 and
point its SMTP transport at 127.0.0.1:1025. humanish does not change an
already-running app's environment. No real mailbox-provider credentials are
needed. This captures mail sent by that app; it does not receive arbitrary
internet email.
Add this to an existing local app-url study:
comms:
email:
external:
catchBaseUrl: http://127.0.0.1:8025Then run humanish doctor --study <study> and humanish run <study>. Omit
recipients to assign an address per participant automatically, or declare explicit
{ lane, address } entries for an existing roster. The participant receives its
address and inbox URL in its mission. Verification links keep their original
app origin; ensure they target the loopback app port selected by the study.
Each local desktop forwards a separate host-owned read-only proxy for its assigned recipient. Other recipient routes, the operator inbox, mail sends and the private delivery drain are unavailable through that proxy. The study closes these proxies with its desktops and leaves your catch running. Host-side collection uses the existing digest-only comms evidence. Supported captured images and explicit unavailable-image placeholders work as described below.
The local path supports captured email only. Provider-backed real receiving retains its separate hosted-route and management-credential requirements.
Existing external catches
Upgrade the humanish installation that runs humanish comms catch and restart that
catch process, as well as upgrading the installation that starts the study. Updating
only the study client leaves an older catch without participant routes.
Before allocating participants, the client checks /health on both catchBaseUrl
and a distinct inboxBaseUrl. Each must return the normal service marker and
capabilities: ["recipient-inbox-v1", ...]. Missing, malformed or older responses
fail admission with an upgrade/restart instruction. Existing email capture endpoints
and unscoped operator URLs remain available; their payload contracts are unchanged.
Captured and remote images
The SMTP catch preserves MIME parts with a Content-ID when they contain PNG, JPEG,
GIF or WebP bytes. The generic /emails payload can also carry runtime-only
inlineImages entries with contentId, contentType and base64. Provider-specific
attachment fields are not converted; unsupported or missing CID references show an
explicit unavailable-image placeholder instead of pretending capture succeeded.
Captured images are limited to 12 entries, 1 MiB each and 2 MiB total. Before creating a data URL, the renderer checks that the base64 re-encodes to the same text and that the bytes carry the declared raster format's signature. This is not a full image decoder; malformed raster content can still fail in the browser. Inline data URLs receive the same per-image checks. SVG, arbitrary attachment URLs and local paths are not fetched or converted.
HTTP(S) images remain browser loads with no-referrer. humanish does not fetch or
proxy them server-side. Relative image URLs use the existing declared origin-rewrite
map when available; otherwise they receive an explicit placeholder. Remote URLs that
return errors retain the browser's alt-text fallback. The renderer does not claim to
repair a dead URL. The inbox retains script-src 'none' and the existing HTML
neutralization and origin-rewrite protections.
Raw mail and image bytes remain runtime catch data. Persisted comms evidence remains digest-only; adding image capture does not add raw attachments to the run artifact.
Reproduce the contract
pnpm exec vitest run tests/comms/ tests/substrates/local/captured-inbox.test.ts
pnpm exec tsx scripts/comms-inbox-proof.tsThe browser proof starts owned loopback HTTP, read-only inbox and SMTP listeners,
sends fictional multipart mail through the real MIME parser, and renders two recipient
inboxes in Chromium at desktop and phone sizes. It checks decoded CID/data/remote
images, unavailable sources, a genuine remote 404, recipient isolation across HTML
and JSON, navigation, script blocking, referrer suppression, directory reuse and drain
access. Screenshots and a machine-readable receipt are saved under the ignored
.humanish/comms-inbox-proof/ directory. It uses no external email delivery, paid model
calls or hosted desktop, and makes no claim about those providers.