Skip to content

fix(chat): fetch inbound email body from the Received Emails API (#320)#325

Open
iPLAYCAFE-dev wants to merge 1 commit into
QuackbackIO:mainfrom
iPLAYCAFE-dev:fix/resend-inbound-body-fetch
Open

fix(chat): fetch inbound email body from the Received Emails API (#320)#325
iPLAYCAFE-dev wants to merge 1 commit into
QuackbackIO:mainfrom
iPLAYCAFE-dev:fix/resend-inbound-body-fetch

Conversation

@iPLAYCAFE-dev

Copy link
Copy Markdown
Contributor

Fixes #320

Problem

Resend's email.received webhook payload is metadata-only — it carries no text/html body. ingestInboundEmail read data.text inline, so every inbound reply that routed to a conversation arrived body-less, was stripped to '', and dropped as empty (200-acked, so Resend never retried). Net effect: email reply-threading silently never works on the Resend inbound channel.

Fix

When the parsed payload has no text but carries the provider email id, fetch the body via the Received Emails API (resend.emails.receiving.get — already available in the pinned resend SDK) before extractReplyText:

  • Fetch after routing + dedupe — unroutable/duplicate deliveries never cost an API call.
  • not_foundnull → the existing empty drop; any other API error throws → the route 500s → Resend redelivers (idempotent via the Message-ID dedupe + the partial unique index).
  • html-only bodies fall back through a small htmlToText (block tags → newlines, entities unescaped, per-line leading whitespace stripped so extractReplyText's ^-anchored quote separators still match).
  • parseInboundEmail additionally prefers message_id for dedupe and exposes email_id as emailId.

No config, schema, or API-surface changes; when the payload does carry inline text (e.g. other providers, tests), behavior is unchanged and no fetch happens.

Testing

  • 9 new unit tests: metadata-only fetch happy path, html→text fallback, fetch skipped when inline text present, not_foundempty, transient failure propagates for redelivery, emailId/message_id parsing precedence, htmlToText behavior (incl. feeding extractReplyText), and the no-API-key → null guard in @quackback/email.
  • Full email-path suite green locally (chat-email-inbound, chat-email-ingest, email-webhook-handler, chat-email-channel, packages/email): 70/70.
  • Verified end-to-end on a production install (v0.13.1 + this patch): a real external mail to a minted reply+<id>.<sig>@<inbound-domain> address traversed MX → Resend → webhook → Svix verify → ingest and threaded into its conversation with the fetched body as the message content. Without the patch the identical delivery ends status:"empty".

Notes

@CLAassistant

CLAassistant commented Jul 4, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

…ckbackIO#320)

Resend's `email.received` webhook payload is metadata-only — it carries
no text/html body. ingestInboundEmail read `data.text` inline, so every
inbound reply routed to a conversation was stored-body-less, stripped to
'', and dropped as `empty` (200-acked, never retried): email
reply-threading silently never worked on the Resend inbound channel.

Fix: when the parsed payload has no text but carries the provider email
id, fetch the body via the Received Emails API
(`resend.emails.receiving.get`, already in the pinned SDK) before
extractReplyText. Design points:

- Fetch AFTER routing + dedupe, so unroutable/duplicate deliveries never
  cost an API call.
- `not_found` -> null -> the existing `empty` drop; any other API error
  throws, the route 500s, and Resend redelivers (idempotent via the
  Message-ID dedupe).
- html-only bodies fall back through a small htmlToText (block tags ->
  newlines, entities unescaped, per-line leading whitespace stripped so
  extractReplyText's ^-anchored quote separators still match).
- parseInboundEmail also picks up `message_id` for dedupe precedence and
  exposes `email_id` as `emailId`.

Verified end-to-end on a production install: a real external mail to a
minted reply+<id>.<sig> address threaded into its conversation with the
fetched body as content; without the fix the same delivery ends
status:'empty'.

Fixes QuackbackIO#320
@iPLAYCAFE-dev iPLAYCAFE-dev force-pushed the fix/resend-inbound-body-fetch branch from d0df7e3 to 74606de Compare July 4, 2026 19:08
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.

Bug: Inbound replies dropped on Resend: code reads body from metadata-only webhook

2 participants