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.

See also

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.2.tar.gz (27.7 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.2-py3-none-any.whl (19.2 kB view details)

Uploaded Python 3

File details

Details for the file zeno_channel_email-1.0.2.tar.gz.

File metadata

  • Download URL: zeno_channel_email-1.0.2.tar.gz
  • Upload date:
  • Size: 27.7 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.2.tar.gz
Algorithm Hash digest
SHA256 8b2718554ac52a6b9ece61bac4ee4a902df18ff7773f70973963337284ef5e19
MD5 d55d5640e3f87f254c8bab1560b6b07d
BLAKE2b-256 9200b0b734b3cbed8abe534c1fd273d2b8d0551e29e6b80c166eb1b23011f649

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zeno_channel_email-1.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 17e69ee72579ea2310e206f01f6f049db9f482ba3b5be1644e4c10b053296992
MD5 c01bbbb66866c57ac3966a5f35d7b177
BLAKE2b-256 0ef36098ff0bb7dc49a783990f12e58080bd18bed41734ba648ed072c6aad6e9

See more details on using hashes here.

Provenance

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