베타 DB · email-reply 매핑 분석

왜 김기한 대표(돛단배수산) 회신이 기존 스레드에 안 묶였나

2026-05-26 10:35 / 10:38 UTC · 회신 2건 · workspace 린다세일즈 (b3e2c3bf…) · 분석 시각 2026-05-27

TL;DR

증거 — DB 스냅샷 (베타)

발송 측 (outbound 3건)

thread_id (공통) <010c019dd6a80c91-75183480-b48b-4bed-a241-a1968bfcf2e4-000000@ap-northeast-2.amazonses.com>
2026-04-29 00:33“(광고) 돛단배수산 해외 영업 관련하여 제안드립니다”
2026-04-30 16:17“(광고) 돛단배수산 해외 영업 관련하여 한 번 더 연락드립니다”
2026-05-12 03:42“(광고) 돛단배수산 대표님, K-Food 해외 수요 관련하여 공유드립니다”
lead_id / sequence_id65b2b7e0… / a48f4fce…
workspaceb3e2c3bf… (린다세일즈)
from_emailrinda@send.grinda.ai

회신 측 (inbound 2건, 김기한 → rinda)

2026-05-26 10:35subject = (빈 값) · Message-ID = <f46a427a…@cmweb03.nm>
2026-05-26 10:38subject = “제목 : 해외세일즈 자동화건” · Message-ID = <3fa93de7…@cmweb01.nm>
In-Reply-To없음 raw 헤더에 부재. DKIM h= 서명 목록에도 없음
References없음
저장된 thread_id각 메일의 자기 Message-ID (= 새 스레드)
lead_id 매칭성공 65b2b7e0… (from_email로 찾음)
sequence_id 매칭실패 — NULL
email_replies 레코드0건 (= 회신함 UI에 안 보임)

원인 진단 — 4가지 후보 비교

(A) 캠페인 발송 문제

아님

outbound 3건 모두 동일 thread_id, 동일 lead_id, 동일 sequence_id로 정상 묶임. SES 발송도 성공. 발송 파이프라인 결함이 아님.

(B) 고객 측 클라이언트 동작 — 네이버 메일 앱

직접 원인

회신 raw 헤더에 In-Reply-To·References전무. 게다가 첫 번째 회신은 제목이 비어있고, 두 번째 회신은 “제목 : 해외세일즈 자동화건”처럼 새로 작성한 제목 포맷이다 (Re: 접두사 없음). 네이버 모바일 앱에서 "회신" 버튼 대신 "새 메일 쓰기"로 작성했거나, 네이버 앱 자체가 cmweb*.nm 게이트웨이를 지나며 회신 헤더를 누락시켰을 가능성.

(C) 시스템 매칭 로직 한계

보조 원인

webhook.service.ts 가 회신 매칭에 In-Reply-To를 1차 키로 쓰고, References 헤더는 thread 매칭에 사용하지 않는다 (파싱만 함). 헤더가 없을 때 fallback이 있지만 leadId && sequenceId 둘 다 매칭돼야 발동.

(D) 계정/도메인 불일치

fallback을 막은 결정타

enrollment 의 user_email_account_idrinda@mail.rinda.ai(SES 발송 계정)인데, 김기한이 답장한 수신 inbox는 rinda@send.grinda.ai(SendGrid inbound). webhook에서 account.id가 송신 계정과 달라서 enrollment 검색이 0건 → sequenceId NULL → fallback 진입 실패. 같은 워크스페이스인데도 끊김.

판정
고객 클라이언트 과실이 1차, 우리 쪽 fallback이 도메인 분리 때문에 못 잡은 게 2차. “캠페인 발송”이나 “일반적 시스템 버그”는 아님.

흐름 요약

관련 코드

elysia-server/src/services/webhook.service.ts

// L263: in_reply_to 없으면 자기 message_id 가 그대로 thread_id
let threadId = headers.messageId

// L265: 회신 매칭은 In-Reply-To 1개 키에만 의존
if (headers.inReplyTo) { /* 원본 outbound 찾아서 thread/lead/sequence 상속 */ }

// L1154: fallback — leadId+sequenceId 둘 다 있어야 발동
if (!headers.inReplyTo && leadId && sequenceId) { /* ... */ }

// L361: enrollment 매칭 시 userEmailAccountId 필터
//      → 수신 inbox 계정과 발송 계정 도메인이 다르면 0건
eq(sequenceEnrollments.userEmailAccountId, account.id)

권장 해결책

  1. fallback 매칭 조건을 완화

    leadId && sequenceIdleadId 만 있어도 같은 workspace의 최근 30일 outbound를 찾도록. (도메인 mismatch 케이스를 잡음, 이번 김기한 케이스가 바로 이걸로 풀린다.)

  2. enrollment 검색에서 userEmailAccountId 필터 제거 또는 workspace 스코프로 완화

    같은 워크스페이스 안에서 발송/수신 계정이 다른 multi-account 셋업이 일반적이므로 workspace + leadId 가 더 안전.

  3. References 헤더도 thread 매칭에 사용

    현재 파싱만 하고 사용하지 않음. 일부 클라이언트는 In-Reply-To 없이 References만 보내기도 함.

  4. subject 기반 secondary 매칭 (안전망)

    Re:/RE:/회신: 접두사를 정규화한 뒤 동일 workspace + 동일 from↔to 쌍의 outbound subject와 30일 이내 매칭. confidence가 낮으면 “자동 묶음 후보” 로 별도 표시.

  5. 운영 관점: 회신함에 “고아 inbound” 섹션 추가

    매칭 실패한 inbound도 lead가 매칭됐다면 회신함에 노출 (현재는 사실상 묻힘). 이번처럼 헤더가 깨져 들어와도 사람이 수동 연결 가능.