docs: 메일 이전 — 계약 6절 · mail/README imapsync 차례 · 전환 절차

imapsync 절을 새 차례(계정 → 1차 → MX → 마지막 imapsync → 5분 뒤 이전 마치기 → 임시 비밀번호 나눠 주기)로 다시 썼다.
mailcow 고정 마스터 계정(사무실 PC 의 mailcow.conf), 서버 B 의 명령(고정 이미지 · 인증서 확인 · 비밀번호 파일),
개발에서 확인한 것(폴더 맞춤 · 수 · 날짜 · 표시 · 한글 폴더 · 차이분 · 만료 뒤 실패)을 적었다.
엔진 사실: 만료가 미래면 그때까지 IMAP 정상, 기본 만료의 찌꺼기는 끈 뒤에도 남는다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 22:51:16 +09:00
parent c8287c4819
commit 24179edaaa
2 changed files with 220 additions and 29 deletions
+36 -4
View File
@@ -2405,6 +2405,11 @@ interface FileUploadCreate {
그 밖의 메일 API 가 만료를 만나면(운영 도구가 재설정한 직후 등) 401 `AUTH_001` 이다 — 화면이 바로 갱신해 그 단계로(세션이 끊겼으면 로그인 화면으로) 간다.
- 만료된 동안은 대리 접근으로 메일을 넣거나 앱 비밀번호를 지우는 일도 막힌다(개발 엔진 확인 — 세션에 메일 계정이 없어 `Mailbox/get` · `Email/import` · `SieveScript` 를 부를 수 없고 `x:AppPassword` 는 `forbidden`,
되는 것은 blob 올리기 · `x:AccountPassword/get·set` · `/api/account`) — 그래서 운영 도구는 그 일을 만료를 걸기 전에 한다(아래 "운영 도구").
- 만료된 비밀번호로는 IMAP 로그인이 응답 없이 끊기고 SMTP 인증은 550 이다 — 메일 옮기기(imapsync)가 새 엔진에 들어가지 못한다.
- 만료 시각을 **미래**로 두면 그때까지 IMAP · SMTP · JMAP 이 모두 정상이고, 그 시각이 지나면 엔진이 스스로 만료로 다룬다. `null` 은 만료 없음이다(2026-11-02 개발 엔진 확인 — 20초 뒤로 걸고 지나기 전 · 뒤를 보았다).
운영 도구의 "이전 중"은 이것과 실행 기록으로 가른다(아래 "메일 이전").
- 엔진의 비밀번호 기본 만료(`x:Authentication` 의 `passwordDefaultExpiry`)가 켜져 있으면 직원이 비밀번호를 바꿀 때 엔진이 "지금 + 기본 만료"를 **저장**하고, 끈 뒤에도 그 만료가 지날 때까지 남긴다(0.16.27 소스 `registry/mapping/account.rs`).
그래서 미래 만료만으로는 이전 중을 가를 수 없다. 기본 만료는 켜지 않는다(켜면 직원 비밀번호가 주기적으로 만료된다).
- 엔진에서 비밀번호를 바꾸거나 막아도 웹메일 세션은 끊기지 않는다. 갱신이 게이트웨이로 계정이 있는지만 보고, 엔진에는 바뀐 것을 알리는 칸이 없다.
- 그래서 운영 도구가 세션을 직접 끊는다.
- 엔진에는 2단계 인증을 강제하는 설정이 없다 — 서버 정책으로 둔다.
@@ -2507,16 +2512,17 @@ interface FileUploadCreate {
| 명령 | 하는 일 |
| --- | --- |
| 입사 | 주소 · 이름 · 메일함 용량 → 엔진 계정(직원 역할) + 임시 비밀번호(무작위 — 한 번만 보여 줌) + 만료(첫 로그인 때 바꾸기) |
| 비밀번호 재설정 | 임시 비밀번호 + 만료, 웹메일 세션 모두 끊기, 앱 비밀번호 지우기, 받은편지함 알림. 2단계는 그대로다(따로 해제) |
| 입사 | 주소 · 이름 · 메일함 용량 → 엔진 계정(직원 역할) + 임시 비밀번호(무작위 — 한 번만 보여 줌) + 만료(첫 로그인 때 바꾸기). "mailcow 에서 옮겨 올 메일이 있습니까?"에 y 면 임시 비밀번호 대신 **이전용 비밀번호**(만료 30일 뒤 — 아래 "메일 이전") |
| 비밀번호 재설정 | 임시 비밀번호 + 만료, 웹메일 세션 모두 끊기, 앱 비밀번호 지우기, 받은편지함 알림. 2단계는 그대로다(따로 해제). 이전 중인 계정은 하지 않고 「메일 이전」으로 안내한다 |
| 2단계 해제 | 휴대폰 분실 — 엔진 OTP 지우기 + `enabledAt` 정리 + 받은편지함 알림 |
| 잠금 | 퇴사 · 털림 — 엔진 `authenticate` 빼기, 웹메일 세션 모두 끊기. 묻고: 그 직원의 공개 링크 끊기, 받는 메일 넘기기(Sieve `redirect :copy`), 자동 응답. 메일은 계속 받는다 |
| 잠금 풀기 | `authenticate` 되돌리기. 비밀번호 재설정을 함께 권한다 |
| 잠금 풀기 | `authenticate` 되돌리기. 비밀번호 재설정을 함께 권한다(이전 중인 계정이면 권하지 않는다) |
| 퇴사 마무리 | 잠긴 계정만. 엔진 계정 지우기(메일 영구 삭제 — 주소를 다시 쳐서 확인) + 우리 DB 의 그 주소 행 정리 — 같은 주소를 다시 쓸 때 섞이지 않게 |
| 링크 끊기 | 신고된 공개 링크 주소로 → `revoked_by = operator` |
| 공용 주소 | MailingList 만들기 · 구성원 넣기 / 빼기 · 목록(구성원 받은편지함으로 복사) |
| 현황 | 직원마다 잠금 · 2단계 · 앱 비밀번호 수 · 마지막 웹메일 로그인 · 메일함 사용량 |
| 현황 | 직원마다 잠금 · 2단계 · 앱 비밀번호 수 · 마지막 웹메일 로그인 · 메일함 사용량 · 이전 중(끝나는 날) |
| 정책 · 설정 | 2단계 필수(켜기 · 시작일 · 예외), 문의 연락처 |
| 메일 이전 (2026-11-02 더함) | 이전 현황(이전 중인 계정 · 끝나는 때 · 메일함 사용량) · 이전용 비밀번호 다시 만들기(한 사람 또는 이전 중인 계정 모두 — 잃었거나 30일을 늘릴 때) · 이전 마치기(한 사람 또는 이전 중인 계정 모두 → 직원용 임시 비밀번호) |
- 차례가 있는 명령
- 비밀번호 재설정: 앱 비밀번호 지우기(대리 접근) → 웹메일 세션 끊기(DB) → 알림 메일 넣기(대리 접근) → 엔진에 임시 비밀번호 + 만료 → 세션 한 번 더 끊기.
@@ -2534,6 +2540,32 @@ interface FileUploadCreate {
- 잠금 풀기는 `authenticate` 빼기만 되돌리고 남은 것이 없으면 상속(Inherit)으로 돌린다 — 직접 정한 권한(Replace)인 계정은 바꾸지 않는다.
- 엔진의 답을 받지 못해 결과를 모르는 실패(시간 초과 · 네트워크 · 5xx)는 "결과를 모릅니다 — 같은 명령을 다시 하세요"로 알린다 — 재설정은 넣은 알림을 지우지 않고, 퇴사 마무리는 DB 를 건드리지 않는다(다시 하면 남은 것만).
퇴사 마무리에서 그 직원이 공용 주소의 마지막 구성원이면 뺄지 먼저 묻는다(빼면 받는 사람이 없다). 2단계 필수 예외 주소 넣기 · 빼기는 y/N 뒤 퇴사 마무리와 같은 행 잠금 안에서 읽고 쓴다.
- **메일 이전**(2026-11-02 더함 — 전 직원 전환의 imapsync, 차례 · 명령은 mail/README "imapsync"):
- 왜: imapsync 는 새 엔진에 IMAP 으로 들어가야 하는데 임시 비밀번호는 만료라 끊긴다(위 "엔진 사실"). 직원이 첫 로그인에서 비밀번호를 바꾸면 우리는 모르고 앱 비밀번호도 지워져 마지막 차이분을 옮길 길이 없다.
그래서 비밀번호를 나눠 주기 전에 옮기기를 모두 끝낸다 — 계정 만들기(이전용) → imapsync 1차 → MX 바꾸기 → 마지막 imapsync → 이전 마치기 → 임시 비밀번호 나눠 주기.
- **이전 중** = 엔진 비밀번호 만료가 미래(지금 + 31일 안)이고, 실행 기록(`ops_audit_log`)에서 그 주소의 판정 기록 가운데 마지막이 이전용(`hire-migration` · `migration-password`)인 계정이다.
판정 기록 = 비밀번호를 거는 명령(`hire` · `hire-migration` · `password-reset` · `migration-password` · `migration-finish`)의 success · partial, 그리고 **이전용 명령의 failure** —
결과를 모르는 실패는 엔진에 걸렸을 수 있어서다(걸렸으면 「다시 만들기」(그 주소)로 바로 되살린다. 실제로 거절됐으면 계정이 없거나 옛 만료 그대로라 만료 조건이 가른다). 이전용이 아닌 명령의 failure 는 건너뛴다.
판정 함수는 하나다(`modules/ops/migration-state.ts`) — 현황 · 재설정 가드 · 잠금 풀기 · 「메일 이전」이 모두 쓴다.
새 표는 두지 않는다 — 엔진의 만료만 보면 기본 만료의 찌꺼기(위 "엔진 사실")를 이전 중으로 읽어 직원의 비밀번호 · 앱 비밀번호를 지울 수 있다(2026-11-02 재리뷰).
실행 기록을 읽지 못하면 「메일 이전」은 멈추고, 재설정 · 잠금 풀기는 엔진 만료만 보고 보수적으로 막는다(잘못 덮지 않게).
이전용 비밀번호는 만든 때부터 30일이고, 잊어도 그때 엔진이 막는다(그 뒤는 첫 로그인 때 바꾸기와 같은 상태).
- 이전용 비밀번호는 imapsync 에만 쓰고 직원에게 주지 않는다. 한 번만 보이고 실행 기록 · 로그에 없다(임시 비밀번호와 같다).
**운영 도구 한 번 실행 안에서는 같은 값**이다 — 처음 만들 때 한 번 보이고, 그 실행의 다음 입사 · 다시 만들기는 그 값을 건다(다시 보이지 않는다). imapsync 의 대상 비밀번호 파일 하나로 모든 계정을 옮기게 하려는 것이다.
값은 그 프로세스 메모리에만 있다(관리자 비밀번호처럼). 만료는 바뀔 것을 보일 때 정한 시각부터 30일이다(모두면 모두 같은 시각).
같은 실행 안에서 값을 잃었으면 도구를 끝내고 다시 시작한 뒤 「다시 만들기」(빈 값 = 모두)를 한다 — 같은 실행의 다시 만들기는 그 실행의 값을 다시 걸 뿐 새 값을 보이지 않는다.
- 도구는 관리자로 들어온 직후 엔진의 비밀번호 기본 만료를 한 번 읽어, 켜져 있으면 첫 화면에 알리기만 한다(이전 중 판정은 실행 기록으로 가르므로 막지 않는다). 읽지 못하면 경고 로그만 남긴다.
- 다시 만들기: 한 사람, 또는 주소를 비우면 이전 중인 계정 모두(잃어버렸거나 30일이 다가올 때 — 그 실행의 값으로 맞춘다).
비밀번호를 바꾸기 전에 그 주소의 웹메일 세션을 끊는다 — 임시 비밀번호(만료)로 들어와 새 비밀번호 단계에 묶인 세션이, 만료가 미래로 바뀌면 다음 갱신에서 제한이 풀리기 때문이다(끊지 못하면 바꾸지 않는다).
한 사람일 때는 이전 중이거나 비밀번호가 만료된 계정(30일이 지난 이전 계정 · 아직 바꾸지 않은 임시 비밀번호 — 엔진에서는 가를 수 없다 — 바뀔 것에 "지금 비밀번호는 더 쓰지 못합니다")만 받는다. 만료된 계정은 "모두"에 넣지 않는다.
직원이 이미 비밀번호를 정한 계정(만료 없음)은 막는다 — 걸면 직원이 로그인하지 못한다. 잠긴 계정 · 2단계 인증을 켠 계정(비밀번호로는 IMAP 에 들어가지 못한다)도 막는다.
- 이전 마치기의 차례(계정마다): 앱 비밀번호가 있으면 지우기 → 웹메일 세션 끊기 → **이전 중 로그인 기록 지우기**(우리 DB `login_events` — imapsync 의 IMAP 로그인이 엔진 웹훅으로 쌓인다. 직원이 첫날 모르는 IMAP 로그인 수십 줄을 보지 않게)
→ 엔진에 직원용 임시 비밀번호 + 만료 → 세션 한 번 더 끊기. **받은편지함 알림 메일은 넣지 않는다**(재설정 알림은 "관리자가 비밀번호를 바꿈"이라 새 직원이 놀란다).
한 계정이 실패해도 나머지를 이어 하고, 끝에 주소 · 임시 비밀번호 표를 한 번 보인다(앞에서 만든 것은 실패가 나도 반드시 보인다). 로그인 기록을 지우지 못한 계정은 비밀번호를 바꾸지 않고 멈춘다(넘긴 뒤에는 지울 명령이 없다).
잠긴 계정은 마치지 않는다. y/N 뒤 계정마다 다시 읽어 그사이 비밀번호 처지가 바뀌었으면(다른 운영자가 먼저 마침 등) 하지 않는다. 세션 끊는 사유는 운영 도구의 `ended-by-operator` 그대로다.
실행 기록은 계정마다 — command `migration-finish`(이전 마치기) · `migration-password`(다시 만들기), 이전용 입사는 `hire-migration`(임시 입사는 `hire` 그대로), detail 에 `이전용(이번 실행 공통 · ~날짜)`.
마지막 imapsync 가 끝나고 5분쯤 뒤에 마친다 — 엔진 웹훅이 실패한 묶음을 5분까지 다시 보내므로, 바로 마치면 늦게 온 IMAP 로그인 몇 줄이 직원의 로그인 기록에 남을 수 있다.
끝의 표를 잃었으면(터미널이 끊김 등) 실행 기록에서 `migration-finish` 가 success 인 주소를 찾아 「비밀번호 재설정」으로 새 임시 비밀번호를 만든다(이미 마친 계정은 이전 중이 아니라 재설정이 받는다).
- 관리자 계정에서 대리 접근 권한(`impersonate`)을 빼므로(인프라 — mail/README) 도구가 직원의 메일함 · Sieve · 앱 비밀번호를 다룰 때는 웹메일과 같은 게이트웨이 대리 접근을 쓴다. 관리자 Basic 은 계정 관리 API(`x:Account` · `x:MailingList`)에만 쓴다.
- **도구로 끊은 세션도 15초 안에 막힌다.** 세션 끊기는 DB 로 하고, 서버는 끊은 세션 기억을 15초마다 DB 에서 채운다(5절의 기억에 더한다).
- 잠금은 도구로 한다. 엔진 관리 화면에서 `authenticate` 만 빼면 웹메일 세션이 남는다 — 갱신이 503 `MAIL_001` 을 내며 세션을 그대로 둔다(엔진의 대리 접근 403 을 설정 문제로 본다).