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.0rc1.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.0rc1-py3-none-any.whl (19.1 kB view details)

Uploaded Python 3

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

Hashes for zeno_channel_email-1.0.0rc1.tar.gz
Algorithm Hash digest
SHA256 4397cc8b6817659a564196ff7df0806bc1f2cad49b70c7cd9db28c3c69d22218
MD5 eb94d8435249b549bda637ac6f0c6f2a
BLAKE2b-256 06bb184736e2c3c7d3613da5cdaf00efe8938da6c2f48930d2c14da6245eb086

See more details on using hashes here.

Provenance

The following attestation bundles were made for zeno_channel_email-1.0.0rc1.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.0rc1-py3-none-any.whl.

File metadata

File hashes

Hashes for zeno_channel_email-1.0.0rc1-py3-none-any.whl
Algorithm Hash digest
SHA256 541a9bf425a984c65798a6cb2e307442c32ba9862d5a3891551d4ce0dc6c27b7
MD5 33c3f33fe811c8cfc42d47b44e24cc7a
BLAKE2b-256 bab8c77cd8619bcffdb5cfff5311dd356f47d693798dc1e25cfa54a9ad25f816

See more details on using hashes here.

Provenance

The following attestation bundles were made for zeno_channel_email-1.0.0rc1-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