zeno-channel-email
Email channel for Zeno backed by Resend. Hosts a Starlette + uvicorn server for Resend's inbound webhook and posts outbound replies (with correct RFC 5322 threading headers) to Resend's REST API.
Verifies inbound webhooks with Svix-style HMAC-SHA256 signatures and
enforces a sender allowlist. Replies to an inbound email land in the
same thread in Gmail, Outlook, and Apple Mail because the channel
owns the Message-ID / In-Reply-To / References header math.
Install
uv add 'zeno-framework[email]'
# or, without the AI package:
uv add zeno-channel-email
Minimal usage
from zeno.channels.email import EmailChannel
channel = EmailChannel(
api_key="re_...", # Resend API key
signing_secret="whsec_...", # Resend webhook secret
from_address="zeno@mail.example.com",
allowed_senders=("you@example.com",),
host="127.0.0.1",
port=8080,
webhook_path="/webhook",
)
# Then pass it to ZenoApp(channels=[channel]) exactly like any Channel.
The signing secret rotation flow uses Svix's multi-secret window: add the new secret in the Resend dashboard first, restart the process with the new env var, then remove the old secret. No downtime.
See apps/zeno-example-chat
for a runnable reference wiring that turns the email channel on only
when the relevant env vars are set.
Behavior
- Signature verification: Resend signs inbound webhooks with
Svix. Bad or missing signature →
401. Every other drop (stale timestamp, unknown event type, unallowlisted sender, malformed JSON,fetch_receivedfailure, queue full) returns200so Resend doesn't retry and so the 401 audit signal stays meaningful. - Two-phase inbound: Resend's
email.receivedwebhook carries metadata only. The channel GETs/emails/received/{email_id}for the body and headers before enqueuing anIncomingMessage. - Inbound →
IncomingMessage:user_id= normalized lower-caseFrom:address (parsed withemail.utils.parseaddrto defeat display-name injection),text= preferredtext/plainbody with HTML fallback,thread_key= same asuser_idso replies route back. RFC-5322 headers are cached per-thread in-process so outbound replies can build correctIn-Reply-To/References. Attachments on inbound are ignored in 0.7.0. - Outbound: text + optional attachments via
POST /emails.multipart/alternativeis synthesized from the agent's plaintext reply (no user-authored HTML templates). Message-ID is generated per reply. References chain is capped at 50 entries to defend against crafted-header DoS. - Capabilities:
supports_threading=True,supports_images=False(attachments flow outbound only in 0.7.0).
Security notes
signing_secretandapi_keyare markedrepr=Falseon the channel dataclass so they don't leak in traceback / debug output.- The 300 s Svix timestamp window is mandatory (replay defense).
allowed_sendersis normalized to lower-case at construction; inboundFrom:is also normalized so the allowlist check is case-insensitive.- Resend's
POST /emailscaps a single email at 40 MB (attachments counted after base64 encoding).
Testing
uv run pytest packages/zeno-channel-email
Tests use httpx.MockTransport for Resend's REST surface and the
Starlette TestClient for the router. No Resend account required
in CI.
See also
zeno-channel-cli,zeno-channel-sendblue— other first-party channels.docs/adapters.md— writing your ownChannel.
Part of the Zeno framework.
Metadata
Release files for zeno-channel-email 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| zeno_channel_email-1.1.0.tar.gz | 27.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| zeno_channel_email-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 47.0 kB
Release files / zeno_channel_email-1.1.0.tar.gz
| Download URL | zeno_channel_email-1.1.0.tar.gz |
|---|---|
| Size | 27.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2a43c1a7fc7d70556ef07244601b98798086f87d27c2776eb190c7a3b55069e5
|
|
BLAKE2b-256 checksum How to use checksums |
2f9b4aafdce476cca41823d36d93766b535b61e2927857fa73bf6faa23c22e37
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 1, 2026.
Transparency logRelease files / zeno_channel_email-1.1.0-py3-none-any.whl
| Download URL | zeno_channel_email-1.1.0-py3-none-any.whl |
|---|---|
| Size | 19.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d432a1c0696c2f8672f4ca334e65b0624e193a9b2682287dcc9cd99487b649b8
|
|
BLAKE2b-256 checksum How to use checksums |
41ca8de92e5311c17ac0a4bc7bd84691614690cac1b7cdc3b26113ea7f404138
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 1, 2026.
Transparency log