발신 도메인 18-way 분산 교체 — 타당성 & 위험 분석

평판 나쁜 3개(chat·mail.rinda / send.grinda) → 18개 워밍업 도메인 분산 · 발송·답장·웹훅 코드 + 실데이터 검증
📊 beta production 🗓 2026-06-15 ⚙ 발송·답장·웹훅 코드 60+파일 🔍 18도메인 수신 실증

판정: 계획은 타당하다 (조건부)

발송 분산
코드 지원 ✓
답장 스레드 매칭
도메인 무관 ✓
18도메인 수신
실증됨 ✓
진행중 캠페인
절차 필요 ⚠
  1. 가장 큰 위험이 실증으로 해소 — 18개 워밍업 도메인은 이미 각 102~176건 답장을 수신 중(6/5~6/15). MX/SES Receipt Rule이 작동한다는 직접 증거 = "수신 인프라 누락으로 답장 유실" 위험 없음.
  2. "랜덤"이 아니라 더 안전한 sticky 해시 — 코드는 SHA256(리드 이메일) % 풀크기로 분배. 같은 바이어는 항상 같은 도메인, 1·2·3차 후속도 같은 주소. 한 바이어가 18개 도메인에서 받는 혼란은 구조적으로 발생 안 함.
  3. 스레드/시퀀스 매칭은 발신 도메인과 완전 무관 — SES 발신의 message_id@amazonses.com 고정이라 도메인을 바꿔도 In-Reply-To 매칭 키 불변.
  4. 진행중 enrollment는 자동으로 안 바뀜 — 발신계정이 sticky 고정. reallocate-sender-pool.ts --commit 실행이 세트로 필요(미발송분만 재분산, 발송완료분은 보존).
  5. 옛 3개 도메인 즉시 폐기 금지 — in-flight 시퀀스 답장이 그리로 옴. is_active=false로 발송만 끊고 수신은 grace 유지.

핵심 증거: 18개 도메인 수신 실증

코드상 가장 치명적 위험은 "발송은 되는데 그 도메인 MX/Receipt Rule이 없어 답장이 시스템에 진입조차 못 하고 로그도 없이 완전 유실"이다(코드에 수신 도메인 검증·계측 부재). 그러나 실데이터가 18개 전부 수신 작동을 증명한다.

도메인최근 답장 수신건수(누적)수신기간
ask·intro·note·knock·letter
142~1766/5~6/15
ping·meet·sync·hey·share·memo·post·voice
128~1386/5~6/15
say·talk·reach·hi·brief
102~1166/5~6/15
결론: 18개 도메인 모두 MX/Receipt Rule이 살아있고 답장이 정상 적재되고 있다. 계정도 각 2개씩 전부 verified + active. 수신 인프라 측면에서 추가 셋업 없이 즉시 분산 가능.

1발송 코드 — from 결정 & 분산

항목동작근거
from 주소계정 email_address고정(도메인=계정 1:1). 발송 시점 동적선택 없음send-email.ts:766
리드 분배SHA256(리드 이메일) % 풀크기 sticky 해시. 랜덤·라운드로빈 아님mailbox-rotation.service.ts:18
스텝 일관성1·2·3차 후속 전부 같은 발신주소(enrollment sender 상속)sequence-email-loader.worker.ts:165
신규 분산18계정을 sequence_email_accounts에 넣으면 자동 18-waygetMailboxPoolForSequence
진행중 enrollment자동 안 바뀜user_email_account_id 고정. 재할당 필요sender-pool-reallocate.service.ts:123
레이트리밋SES/SendGrid는 daily cap 면제 → 워밍업 보호 위해 daily_limit 명시 설정 필요send-email.ts:469
재할당 메커니즘: reallocate-sender-pool.ts는 ① 발송완료 enrollment 보존(스레드 일관성) ② 미발송분만 새 18개 풀로 재해시 ③ terminal(stopped/bounced 등) skip. 기본 DRY-RUN, --commit 필요. paused→resume 시 자동 호출.

2답장·웹훅 수신 코드

수신 진입점 3개

경로수신 대상비고
SES Inbound ConsumerReceipt Rule → S3 replies/ raw MIME단일 버킷/prefix polling. 도메인 분기 없음 — 18개 다 같은 prefix로 떨어지면 자동 처리
SendGrid Inbound ParseParse MX 걸린 도메인shared-secret 가드
AgentMail Webhookinbox_id 기준공용도메인과 무관(별 provider)

매칭 & 귀속

단계도메인 분산 영향
스레드/시퀀스 상속In-Reply-To/References → message_id (@amazonses.com 고정)없음 ✓
ws/계정 귀속to_email == user_email_accounts.email_address18계정 존재 필요
매칭 실패 시isReply:false early returnemails 미적재 = 유실
reply_classificationsubject/body 텍스트만도메인 무관

3수신 누락 시나리오 전수

시나리오결과이번 계획 해당
(a) MX/Receipt Rule 없는 도메인 발송 → 답장완전 유실 로그·DB 없음✓ 18개 수신 실증 → 해당 없음
(b) user_email_accounts에 없는 주소로 답장유실 early return일반 ws용 계정 생성 시 보장
(c) 바이어가 옛 주소(chat/mail/send.grinda)로 답장옛 MX + 옛 계정 row 둘 다 살아야 수신옛 도메인 grace 유지 필수
(d) 헤더 없는 답장(직접 새 메일)addr/subject fallback 시도계정 귀속 먼저 통과 필요
최우선 가드 = (c): 옛 3개 도메인을 즉시 끄면 진행중 시퀀스의 답장이 유실된다. rinda_managed_sending_domains.is_active=false발송만 차단하고 MX/계정row는 수 주간 유지(설계상 "기존 계정 유지" 지원).

4위험 매트릭스

위험심각도발생 가능성완화책
18도메인 수신 인프라 누락 → 답장 유실치명낮음(실증 해소)이미 작동 중 — 무조치
옛 도메인 즉시 폐기 → in-flight 답장 유실치명is_active=false + 수신 grace 유지
진행중 enrollment 도메인 안 바뀜높음reallocate-sender-pool --commit
일반 ws용 18계정 미생성 → 귀속 실패계정 발급 후 풀 등록
워밍업 도메인 과부하(SES cap 면제)계정별 daily_limit 명시·점진 ramp
유실 사후 감지 불가(계측 부재)상시도메인별 답장 회수율 모니터 추가
스레드 깨짐낮음없음message_id 도메인 무관

5실행 체크리스트 (순서 중요)

  1. 일반 ws용 18개 도메인 계정 발급 — 현재 18개 도메인은 2개 슈퍼 ws(50a26184·b3e2c3bf) 전용. 분산 대상 ws마다 prefix@각도메인 계정을 user_email_accounts에 생성(귀속 키 확보).
  2. 평판 나쁜 3계정을 sequence_email_accounts에서 제거 + 18계정 추가 — 신규 enrollment가 18-way로 자동 분산 시작.
  3. 진행중 캠페인 재할당reallocate-sender-pool.ts --commit(또는 pause→resume). 미발송분만 18개로 재분산, 발송완료분 보존.
  4. 각 계정 daily_limit 설정 — SES cap 면제이므로 워밍업 단계에 맞춰 보수적으로. 점진 ramp.
  5. 옛 3개 도메인 발송 차단rinda_managed_sending_domains.is_active=false(chat은 이미 infinite 전용). MX·계정 row는 유지. ⚠️ user_email_accounts row를 절대 DELETE하지 말 것 — status='inactive'로만(옛 주소 수신 영구 보존, §6 참조).
  6. 계측 추가 — 도메인별 발송 대비 답장 회수율 모니터링(현재 유실 시 로그조차 없음). 누락 조기 감지.

옛 주소로 오는 메일 — 무엇이 유실을 부르나

질문: "평판 나쁜 도메인을 전부 교체하면, 바이어가 옛 주소로 직접 보내는 메일을 웹훅이 못 잡지 않나?" — 코드 직답

결론: 웹훅 계정 귀속 쿼리는 emailAddress = to_email 단 하나만 본다. status·deleted_at·is_verified 필터가 전혀 없다(deleted_at 컬럼은 스키마에 존재조차 안 함). → 옛 계정 row를 DELETE하는 것만이 유실을 일으킨다. 비활성화·풀 제거·발송차단은 수신에 무영향.

귀속 쿼리 원문 (webhook.service.ts:269-290)

조건존재?의미
emailAddress = toEmail유일 조건주소 1:1 매칭(글로벌 unique index)
status = 'active'없음inactive여도 매칭됨
deleted_at IS NULL없음(컬럼 자체 부재)soft-delete 미구현
매칭 0건 시return {isReply:false}emails 적재도 없이 완전 유실

옛 계정 정리 방식별 영향

정리 방식수신 귀속결과
(a) sequence_email_accounts에서만 제거정상웹훅은 이 테이블 안 봄
(b) status='inactive'정상status 필터 없음
(c) 발송 도메인 is_active=false정상발송만 차단, 수신 독립
(d) user_email_accounts row DELETE깨짐옛 주소 메일 통째 유실

왜 중요한가 — 옛 주소는 아직 활발히 수신 중

옛 도메인inbound 7일inbound 30일in-flight active enrollment
mail.rinda.ai2171,337246,687
chat.rinda.ai1542,69913,898
send.grinda.ai272452,209

in-flight enrollment(특히 mail.rinda 24.7만)이 앞으로도 계속 발송→답장 유입. 옛 주소 수신 보존은 선택이 아니라 필수.

헤더 있는 답장 vs 직접 발신 — 둘 다 같은 게이트

스레드 매칭(message_id)이 성공해도 계정 귀속(to_email)이 단일 상위 게이트다. 귀속 0건이면 fallback에 도달조차 못 하고 return. 헤더 없는 직접발신도 to_email 귀속을 먼저 거친 뒤에야 addr/subject fallback을 탄다. 결국 옛 주소 row만 살아있으면 답장·직접발신 모두 잡힌다.

안전한 정리 공식: ① MX/Receipt Rule 유지 ② user_email_accounts row는 DELETE 금지, status='inactive'로만 ③ 발송 도메인 is_active=false + 풀 제거는 자유. row만 안 지우면 옛 주소 수신은 영구히 안전. (단 글로벌 unique index상 그 주소를 새 계정으로 재등록은 불가.)

!코드의 구조적 빈틈 (별도 개선 권고)