Email channel for Zeno (Resend inbound + outbound).
Project description
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.
Part of the Zeno framework.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zeno_channel_email-1.0.0rc1.tar.gz.
File metadata
- Download URL: zeno_channel_email-1.0.0rc1.tar.gz
- Upload date:
- Size: 27.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4397cc8b6817659a564196ff7df0806bc1f2cad49b70c7cd9db28c3c69d22218
|
|
| MD5 |
eb94d8435249b549bda637ac6f0c6f2a
|
|
| BLAKE2b-256 |
06bb184736e2c3c7d3613da5cdaf00efe8938da6c2f48930d2c14da6245eb086
|
Provenance
The following attestation bundles were made for zeno_channel_email-1.0.0rc1.tar.gz:
Publisher:
publish.yml on nkootstra/zeno
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zeno_channel_email-1.0.0rc1.tar.gz -
Subject digest:
4397cc8b6817659a564196ff7df0806bc1f2cad49b70c7cd9db28c3c69d22218 - Sigstore transparency entry: 1383549078
- Sigstore integration time:
-
Permalink:
nkootstra/zeno@7369fbec32dfe63880e968970c2e8070e6091b4f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/nkootstra
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7369fbec32dfe63880e968970c2e8070e6091b4f -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file zeno_channel_email-1.0.0rc1-py3-none-any.whl.
File metadata
- Download URL: zeno_channel_email-1.0.0rc1-py3-none-any.whl
- Upload date:
- Size: 19.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
541a9bf425a984c65798a6cb2e307442c32ba9862d5a3891551d4ce0dc6c27b7
|
|
| MD5 |
33c3f33fe811c8cfc42d47b44e24cc7a
|
|
| BLAKE2b-256 |
bab8c77cd8619bcffdb5cfff5311dd356f47d693798dc1e25cfa54a9ad25f816
|
Provenance
The following attestation bundles were made for zeno_channel_email-1.0.0rc1-py3-none-any.whl:
Publisher:
publish.yml on nkootstra/zeno
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
zeno_channel_email-1.0.0rc1-py3-none-any.whl -
Subject digest:
541a9bf425a984c65798a6cb2e307442c32ba9862d5a3891551d4ce0dc6c27b7 - Sigstore transparency entry: 1383549094
- Sigstore integration time:
-
Permalink:
nkootstra/zeno@7369fbec32dfe63880e968970c2e8070e6091b4f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/nkootstra
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7369fbec32dfe63880e968970c2e8070e6091b4f -
Trigger Event:
workflow_dispatch
-
Statement type: