(A) 캠페인 발송 문제
아님outbound 3건 모두 동일 thread_id, 동일 lead_id, 동일 sequence_id로 정상 묶임. SES 발송도 성공. 발송 파이프라인 결함이 아님.
2026-05-26 10:35 / 10:38 UTC · 회신 2건 · workspace 린다세일즈 (b3e2c3bf…) · 분석 시각 2026-05-27
cmweb01.nm·cmweb03.nm)이 회신 시 In-Reply-To와 References 헤더를 아예 보내지 않음. DKIM의 h= 서명 목록에서도 빠짐.thread_id로 정상 묶임. 발송 측 결함 아님.rinda@send.grinda.ai)과 enrollment의 발송 계정(rinda@mail.rinda.ai)이 다른 도메인이라서 enrollment 매칭이 빠짐 → sequenceId NULL → fallback 미발동.email_replies 테이블에 0건, UI 회신함 미연동.| 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_id | 65b2b7e0… / a48f4fce… |
| workspace | b3e2c3bf… (린다세일즈) |
| from_email | rinda@send.grinda.ai |
| 2026-05-26 10:35 | subject = (빈 값) · Message-ID = <f46a427a…@cmweb03.nm> |
|---|---|
| 2026-05-26 10:38 | subject = “제목 : 해외세일즈 자동화건” · 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에 안 보임) |
outbound 3건 모두 동일 thread_id, 동일 lead_id, 동일 sequence_id로 정상 묶임. SES 발송도 성공. 발송 파이프라인 결함이 아님.
회신 raw 헤더에 In-Reply-To·References가 전무. 게다가 첫 번째 회신은 제목이 비어있고, 두 번째 회신은 “제목 : 해외세일즈 자동화건”처럼 새로 작성한 제목 포맷이다 (Re: 접두사 없음). 네이버 모바일 앱에서 "회신" 버튼 대신 "새 메일 쓰기"로 작성했거나, 네이버 앱 자체가 cmweb*.nm 게이트웨이를 지나며 회신 헤더를 누락시켰을 가능성.
webhook.service.ts 가 회신 매칭에 In-Reply-To를 1차 키로 쓰고, References 헤더는 thread 매칭에 사용하지 않는다 (파싱만 함). 헤더가 없을 때 fallback이 있지만 leadId && sequenceId 둘 다 매칭돼야 발동.
enrollment 의 user_email_account_id는 rinda@mail.rinda.ai(SES 발송 계정)인데, 김기한이 답장한 수신 inbox는 rinda@send.grinda.ai(SendGrid inbound). webhook에서 account.id가 송신 계정과 달라서 enrollment 검색이 0건 → sequenceId NULL → fallback 진입 실패. 같은 워크스페이스인데도 끊김.
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)
leadId && sequenceId → leadId 만 있어도 같은 workspace의 최근 30일 outbound를 찾도록. (도메인 mismatch 케이스를 잡음, 이번 김기한 케이스가 바로 이걸로 풀린다.)
userEmailAccountId 필터 제거 또는 workspace 스코프로 완화
같은 워크스페이스 안에서 발송/수신 계정이 다른 multi-account 셋업이 일반적이므로 workspace + leadId 가 더 안전.
References 헤더도 thread 매칭에 사용
현재 파싱만 하고 사용하지 않음. 일부 클라이언트는 In-Reply-To 없이 References만 보내기도 함.
Re:/RE:/회신: 접두사를 정규화한 뒤 동일 workspace + 동일 from↔to 쌍의 outbound subject와 30일 이내 매칭. confidence가 낮으면 “자동 묶음 후보” 로 별도 표시.
매칭 실패한 inbound도 lead가 매칭됐다면 회신함에 노출 (현재는 사실상 묻힘). 이번처럼 헤더가 깨져 들어와도 사람이 수동 연결 가능.