Skip to main content

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_received failure, queue full) returns 200 so Resend doesn't retry and so the 401 audit signal stays meaningful.
  • Two-phase inbound: Resend's email.received webhook carries metadata only. The channel GETs /emails/received/{email_id} for the body and headers before enqueuing an IncomingMessage.
  • Inbound → IncomingMessage: user_id = normalized lower-case From: address (parsed with email.utils.parseaddr to defeat display-name injection), text = preferred text/plain body with HTML fallback, thread_key = same as user_id so replies route back. RFC-5322 headers are cached per-thread in-process so outbound replies can build correct In-Reply-To / References. Attachments on inbound are ignored in 0.7.0.
  • Outbound: text + optional attachments via POST /emails. multipart/alternative is 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_secret and api_key are marked repr=False on the channel dataclass so they don't leak in traceback / debug output.
  • The 300 s Svix timestamp window is mandatory (replay defense).
  • allowed_senders is normalized to lower-case at construction; inbound From: is also normalized so the allowlist check is case-insensitive.
  • Resend's POST /emails caps 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zeno_channel_email-1.0.0rc2.tar.gz (27.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

zeno_channel_email-1.0.0rc2-py3-none-any.whl (19.1 kB view details)

Uploaded Python 3

File details

Details for the file zeno_channel_email-1.0.0rc2.tar.gz.

File metadata

  • Download URL: zeno_channel_email-1.0.0rc2.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

Hashes for zeno_channel_email-1.0.0rc2.tar.gz
Algorithm Hash digest
SHA256 1ba30700e388e20468c72c227d825c7cdc6bf09077b4117117dcdb08e85f2369
MD5 a21c256e161bc7bc825726b9d17b6c9d
BLAKE2b-256 f779b23d93641e78523b3906e2f582d4e991b1331ce4c701fe53c016b3eac4d5

See more details on using hashes here.

Provenance

The following attestation bundles were made for zeno_channel_email-1.0.0rc2.tar.gz:

Publisher: publish.yml on nkootstra/zeno

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file zeno_channel_email-1.0.0rc2-py3-none-any.whl.

File metadata

File hashes

Hashes for zeno_channel_email-1.0.0rc2-py3-none-any.whl
Algorithm Hash digest
SHA256 13b6e28f090132dd1c24022261276910a26e2c5795ea81144dba6dcaeb0e9b57
MD5 dfb890703b66e5193a3cae426d3a61c0
BLAKE2b-256 792cac551b9af4f8768e3c487e5043775b60d1205720870ecd054bb4f1b9ce78

See more details on using hashes here.

Provenance

The following attestation bundles were made for zeno_channel_email-1.0.0rc2-py3-none-any.whl:

Publisher: publish.yml on nkootstra/zeno

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page