docs: 10단계(파일럿 공존) — 시범 동안의 받는 사람별 결과 · 사내 목록

시범 동안 새 서버에 없는 사내 주소는 엔진이 mailcow 로 중계하므로 웹메일이 곧바로 not-found 로 알리지 못하고
나중의 반송 메일로 알게 된다. 사내 목록도 새 서버의 계정(시범 사용자)뿐이다.
클라이언트 · 서버 코드는 바꾸지 않았다 — 계약과 받는 사람별 결과 주석에 그 차이를 적었다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-30 09:37:24 +09:00
parent 30b3d28996
commit 4c1737f222
2 changed files with 10 additions and 1 deletions
@@ -15,6 +15,8 @@ import type { JmapDeliveryStatus, JmapEmailSubmission } from './jmap.types';
* 남긴다(키는 받는 사람 주소). 거절된 사람은 `delivered: "no"` 와 SMTP 응답으로만 남고 엔진이 **다시 보내지도 반송하지도 않는다**
* (Stalwart 0.16.27 — 2026-10-28 확인):
* - 거절: `550 5.1.2 Mailbox does not exist.` (없는 사내 주소) · `452 4.4.5 Rate limit exceeded, try again later.` (엔진 한도)
* 파일럿 공존(10단계) 동안은 새 서버에 없는 사내 주소를 엔진이 옛 서버로 중계하므로 여기서 거절되지 않는다 — 그쪽의 거절은 나중에 반송 메일로 온다
* (계약 2절 "받는 사람별 결과", mail/README.md "파일럿 공존")
* - 받음: `{ delivered: "unknown", smtpReply: "250 2.1.5 Queued" }` — 사내 · 바깥 모두 대기열에 들어가면 이 모양이다.
* 바깥에서 나중에 실패하면 지금처럼 반송 메일이 온다
* 판단은 deliveryStatus 로 한다 — undoStatus 는 모두 거절이면 final(붙잡아도 — 2026-10-29 확인), 하나라도 대기열에 들어가면 pending 일 뿐이다.
+8 -1
View File
@@ -361,6 +361,10 @@ interface RejectedRecipient {
엔진의 거절 글은 로그에 남기지 않는다(뺀 주소만 가려서).
- **숨은참조**: 엔진은 내보내는 사본(사내 배달 · 바깥 SMTP 모두)에서 `Bcc` 머리글을 뗀다 — 보낸 사본에만 남는다(envelope 을 주든 말든 같다).
개발의 Mailpit 은 받은 envelope 가운데 머리글에 없는 주소를 자기 `Bcc:` 줄로 **맨 앞에 붙여** 보인다 — 엔진이 보낸 것이 아니다(머리글 없는 메일을 envelope 으로만 보내도 그렇다).
- **파일럿 공존 동안**(10단계 — mail/README.md "파일럿 공존"): 새 서버에 없는 사내 주소는 거절되지 않는다 — 엔진이 옛 서버(mailcow)로 중계하고,
그쪽이 받지 않으면 몇 초 뒤 반송 메일(`MAILER-DAEMON@<도메인>`)이 받은편지함에 온다(바깥 주소와 같은 모양). 그래서 그런 주소는 `rejectedRecipients` · `MAIL_010` · `MAIL_011` 에
나오지 않고, 제출 기록도 대기열에서 빠지면 `250 2.1.5 Queued` 로 돌아가 나중의 실패를 남기지 않는다. 시스템 계정(관리자 · 게이트웨이 · 사내 목록)은 그대로 `not-found`.
setup 이 받는 쪽 가드를 확인하지 못하면 중계를 켜지 않거나 다시 닫는다(종료 코드 1 — 열린 중계를 막으려고). 그동안은 평소처럼 곧바로 `not-found` 다.
- **모두 거절**(받은 사람이 아무도 없음) — 보내지 않은 것으로 다룬다. 이 요청이 만든 보낸 사본은 지우고(아무에게도 가지 않았다 — 그 요청이 방금 만든 메일이라 조건 없이 지운다),
이어 쓴 저장본(`draftId`)은 지우지 않는다. 거절 가운데 5xx 가 하나라도 있으면 400 `MAIL_010`, 모두 4xx 면 503 `MAIL_011`.
같은 키로 다시 보내면 새로 보낸다(지운 사본은 중복 방지에 걸리지 않는다).
@@ -1027,7 +1031,8 @@ interface PendingSend {
엔진이 그 시각을 거절하면(`forbiddenMailFrom` — 엔진 한도가 바뀐 경우 등) 400 `VAL_001` 이고 만든 보낸 사본은 지운다(구현하며 정함).
7일은 엔진 한도다(`x:MtaExtensions` 의 `futureRelease` — 로그인 세션 `7d`. JMAP 능력의 `maxDelayedSend` 는 30일로 보이지만 엔진이 7일 넘게는 거절한다 — 2026-10-29 확인).
붙잡은 메일의 대기열 만료는 풀리는 시각 + 3일이라 7일 안은 안전하다(같은 날 확인). 예약에는 보내기 취소 시간을 더하지 않는다. 응답 `pendingSend: { kind: 'scheduled', sendAt }`.
- **받는 사람 확인은 제출 순간에 한다** — 붙잡아도 엔진이 사내 받는 사람을 그 자리에서 묻는다(없는 사내 주소는 곧바로 `delivered: no` · `550 5.1.2`, 반송 없음 — 2026-10-29 확인).
- **받는 사람 확인은 제출 순간에 한다** — 붙잡아도 엔진이 사내 받는 사람을 그 자리에서 묻는다(없는 사내 주소는 곧바로 `delivered: no` · `550 5.1.2`, 반송 없음 — 2026-10-29 확인.
파일럿 공존 동안은 새 서버에 없는 사내 주소가 옛 서버로 중계된다 — 위 "받는 사람별 결과").
그래서 위 "받는 사람별 결과"(일부 거절 `rejectedRecipients` · 모두 거절 `MAIL_010` · `MAIL_011`)는 그대로다. 바깥 주소는 풀린 뒤 배달되고, 실패하면 지금처럼 반송 메일이 온다.
붙잡은 제출의 결과는 사내 · 바깥 모두 곧바로 `250 2.1.5 Queued` 다(거절된 사람만 `delivered: no`). 모두 거절인데 제출이 아직 기다리고 있으면(모두 4xx 등)
같은 요청에서 먼저 취소하고 사본을 지운다 — 풀리는 시각에 다시 보내지 않게(취소를 엔진이 받지 않아도 사본은 지우고 warn). 모두 5xx 면 엔진이 붙잡아도 곧바로 `final` 이라 지우기만 한다(같은 날 확인).
@@ -1346,6 +1351,8 @@ interface DirectoryEntry {
(비밀번호 같은 칸은 없다 — 2026-10-29 확인). 이 권한은 엔진 관리자가 내주지도 빼지도 못해 setup 이 **복구 관리자로 실행될 때** 만든다(개발은 늘, 운영은 첫 배포의 `--recovery` — mail/README.md "사내 목록 계정").
이 계정은 메일을 받지 않는다 — 그 주소(+태그 꼴 포함)로 오는 메일은 받는 쪽 RCPT 단계에서 없는 주소처럼 550 이다(setup 이 관리자 · 게이트웨이와 함께 맞춘다 —
받아 놓고 보낸 사람에게 반송하지 않게, mail/README.md "보안"). 그 뒤의 막으로 역할에 메일 받기 권한이 없고 저장 한도도 1MiB 다.
- **파일럿 공존 동안**(10단계)에는 새 서버의 계정 — 파일럿 사용자 — 만 나온다. 목록을 읽을 수는 있으므로 "로그인한 직원 기록" 대신 쓰기는 켜지지 않는다 —
동료는 내 주소록 · 최근 주고받은 주소(옛 메일을 옮기면 채워진다)로만 추천에 나온다. 파일럿 동안 새 서버에 동료 계정을 만들지 않는다(만들면 그 사람에게 가는 메일이 새 서버에 갇힌다).
- 서버는 목록을 10분 기억한다(모든 직원이 함께 — 서버 메모리). 사내 목록 계정이 없거나 읽지 못하면 503 `MAIL_001` 이고, 받는 사람 추천의 "사내"는 전처럼 로그인한 직원 기록으로 대신한다.
- 한 항목(`GET /api/directory-entries/{id}`)은 그 기억한 목록에서 찾는다 — 화면이 주소로 연 사내 항목. `excludeGroupId` 는 그 직원 주소록의 그 그룹 구성원이 가진 주소(대소문자 무시)를 뺀다.