fix(email): support multi-recipient inbound delivery and eliminate ghost threads - #220
Conversation
|
Reviewed — this is a good fix and I want it. The diagnosis is the part I'd highlight: It pairs well with #216, which I merged today — that one stops the worker turning upstream 5xx into a hard bounce. Together they close both halves of #198/#200's "valid mail permanently bounced" story. I can't merge it right now: the branch is Two things to check while rebasing:
One heads-up on timing, not on this PR: production is currently blocked on migration Rebase and I'll take it. |
…ost threads - Add migration 0006 to replace global unique index on smtp_message_id with per-company index (company_id, LOWER(smtp_message_id)). - Build the new company index and drop the legacy index CONCURRENTLY outside a transaction block to avoid blocking live production inbound writes. - Add uniq_email_messages_company_smtp_id to REQUIRED_SCHEMA_INDEXES. - Deduplicate inbound recipient addresses from To and Cc. - Group resolved recipients by company and fan out per tenant rather than per recipient. - Clean up empty conversations on message persistence failure to prevent ghost threads. - Fix false race_dedup logs and false 550 bounces. - Add comprehensive integration tests for multi-recipient and cross-tenant email delivery. Fixes yetone#198
9836066 to
c1f2e24
Compare
|
Rebased onto latest
All 7 CI checks are green and the branch is clean and |
|
Re-reviewed after the rebase — both points are addressed properly, and I verified the parts that carry risk rather than taking the description on trust:
Merging. One rollout note, repeating what I said above so it isn't a surprise: this bumps both schema bounds to 6, so it won't reach production until migration 0002's precheck is cleared — nothing has deployed since 2026-09-03. It'll go out in the same catch-up run. Thanks for the clean rebase. |
Version bump to 0.16.1, rolling up #220. Claude-Session: https://claude.ai/code/session_0126NkM9crkemuV6Ho4LWv59
Summary
Fixes #198.
Inbound email delivery suffered from several interlocking issues when delivering to multiple recipients or across tenants:
uniq_email_messages_smtp_idwas unique globally onLOWER(smtp_message_id). When an email was addressed to recipients across multiple companies, the second company's insert was rejected with duplicate key violation.recipientsdid not deduplicate addresses ([...to, ...cc]), and the fanout looped per recipient instead of per company. When multiple recipients belonged to the same company, iteration 1 inserted the message and iteration 2 attempted to insert the samesmtp_message_idinto the same company, causing a duplicate key error falsely logged asrace_dedup.findOrCreateEmailConversationcommitted a conversation in its own transaction beforepersistEmailMessage. When persistence failed, an empty conversation with 0 messages was left behind indefinitely.email-gateto issue a permanent 550 bounce ("No such recipient") for valid mailboxes.Key Changes
0005_email_messages_company_smtp_id):uniq_email_messages_smtp_id.uniq_email_messages_company_smtp_idon(company_id, LOWER(smtp_message_id)) WHERE smtp_message_id IS NOT NULL.server/src/api/inbound-email.ts):ToandCc.companyId.memberIds.conv.created === trueand message persistence fails, eliminating ghost threads.server/src/__integration__/inbound.test.ts):Message-ID, To/Cc deduplication, and ghost conversation rollback.Verification
npm run lint: 0 warnings, 0 errors.npm run typecheck&npm run server:typecheck: passed.npm run guard:big-brain,guard:llm-tracked,guard:engine-registry: passed.node --import tsx --test server/src/__integration__/inbound.test.ts: all 16 tests passed.node --import tsx --test server/src/__integration__/schema-migrations.test.ts: all 4 tests passed.npm run test: all 1181 unit tests passed.npm run test:integration: all 323 integration tests passed.