Skip to content

feat(email): secure AI-agent inbound email webhook (inert until configured)#214

Merged
arigatoexpress merged 1 commit into
mainfrom
feat/agent-email-inbox
Jun 20, 2026
Merged

feat(email): secure AI-agent inbound email webhook (inert until configured)#214
arigatoexpress merged 1 commit into
mainfrom
feat/agent-email-inbox

Conversation

@arigatoexpress

Copy link
Copy Markdown
Owner

Foundation for the agent email inbox. POST /api/email/inbound: svix-verified, strict-allowlisted, triages allowlisted mail to a CRM lead + staff notify. Inert in prod until RESEND_WEBHOOK_SECRET is set (returns 'disabled'), so this deploy is a no-op until you activate. NO auto-reply (gated). Uses a *.resend.app receiving address — no DNS change (THO MX is Turbify/Yahoo, must not be touched). 6 tests, ruff clean. Activation runbook in the commit body.

…gured)

Adds POST /api/email/inbound for Resend inbound (email.received) webhooks, so an
allowlisted customer email is triaged into a CRM lead + a staff notification.

Security (inbound email is untrusted):
- Feature-flagged: returns 200 'disabled' unless RESEND_WEBHOOK_SECRET is set, so
  deploying this is a no-op until you configure it.
- svix HMAC-SHA256 signature verification with a 5-min replay window (implemented
  directly — the app's resend SDK 2.5.1 predates webhooks.verify; no risky bump).
- Strict sender allowlist (INBOUND_EMAIL_ALLOWLIST, exact or @Domain), fail-closed.
- Triage -> Lead(source=email) + notify_new_lead. NO auto-reply (outward sends are
  gated, CRITICAL rule 1) — a reply must be drafted + human-approved.

Activation (no DNS — texashomeoutlet.com MX is Turbify/Yahoo, must not change):
  1. Resend: create a *.resend.app receiving address.
  2. Register an email.received webhook -> https://www.texashomeoutlet.com/api/email/inbound.
  3. Set prod env: RESEND_WEBHOOK_SECRET=whsec_..., INBOUND_EMAIL_ALLOWLIST=a@b.com,@trusted.com

6 tests (svix verify, allowlist fail-closed, disabled/bad-sig/dropped/processed). ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 19748228d1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread main.py
if not _INBOUND_ALLOWLIST:
return False # no allowlist configured -> process nothing (fail closed)
bare = _inbound_bare_email(sender)
if bare in _INBOUND_ALLOWLIST:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not treat the From header as authentication

When INBOUND_EMAIL_ALLOWLIST contains a trusted address or domain, any unauthenticated sender can forge the email's RFC5322 From value to match it; the Svix signature only proves Resend delivered the webhook, not that the SMTP sender is authenticated. In that scenario this allowlist is bypassed and arbitrary inbound mail can create CRM leads and staff notifications, so the gate needs a non-spoofable signal (for example verified auth results or a shared inbound alias/token) before returning true.

Useful? React with 👍 / 👎.

Comment thread main.py
name = (sender.split("<")[0].strip() or bare.split("@")[0]) if sender else bare
try:
lead = Lead(
lead_id=f"email_{uuid.uuid4().hex[:12]}",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make inbound webhook handling idempotent

This random lead_id means the same email.received webhook always creates a fresh CRM lead and sends another staff notification if Resend retries after a lost acknowledgement or an operator replays a succeeded event; Resend documents webhooks as at-least-once and replayable. Use data.email_id, message_id, or svix-id as an idempotency key before persisting/notifying so duplicate deliveries do not duplicate leads.

Useful? React with 👍 / 👎.

@arigatoexpress arigatoexpress merged commit 3ad21fb into main Jun 20, 2026
2 checks passed
@arigatoexpress arigatoexpress deleted the feat/agent-email-inbox branch June 20, 2026 02:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant