NCP 버킷 생성 화면의 칸마다 무엇을 고르는지 적는다. 공개는 목록을 누구에게나 열고,
잠금(보존 기간)은 restic 의 prune 을 막고, KMS 암호화는 되돌릴 수 없고 restic 이 이미 암호화한다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
설치 2)는 서버에서 openssl 로 만들고 "비밀번호 관리자에도 둔다"고만 적어, 서버 밖에 두는
길이 빠져 있었다. 생성기로 먼저 만들어 저장한 뒤 read -rsp 로 붙여 넣게 바꿨다 — 서버 밖
사본이 백업보다 먼저 생긴다. 서버에서 만드는 길은 sudo cat 으로 한 번 옮기는 줄과 함께 남겼다.
회사가 꺼낼 수 있는 곳에 두라는 것(운영자 한 사람만이 아니라)과 재설정이 없다는 것을 더했다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 서버 B(11단계)를 main 으로 올리며 확인한 것을 차례에 남긴다.
- DB 가 이미 고정할 digest 로 돌고 있으면 새 이미지가 없어 날짜를 기다리지 않아도 된다 — 0) 확인 명령
- 12단계 전 서버는 웹훅 키를 먼저 넣는다(키 없이 띄우면 백엔드가 env 검사에서 멈춘다)
- --recovery 가 Spamhaus ZEN 시간 초과로 종료 1 이어도 관리자 허용 IP 는 걸렸으니 보통 setup 으로 간다(ZEN 을 다시 켠다)
- 운영은 마이그레이션 로그가 없어 migrations 표로 본다
- 손 백업에 DB 덤프를 더하고, 떠 있는 alpine 대신 지금 도는 DB 이미지 · 읽기 권한만 · 0600 으로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
운영 mailcow 에서 확인(2026-11-03): 마스터 두 값은 빈 값(아직 아무도 고정하지 않음).
최근 7일 Dovecot 로그인 줄과 지금 연결이 모두 앞단 터널 주소 10.9.0.1(사설) — 공인 주소 0.
mailcow 의 Fail2ban 은 사설 주소를 막지 않으므로 서버 B 를 예외에 넣지 않는다.
남는 것은 Dovecot 의 늦추기(같은 주소의 바깥 직원 모두)라 실패를 되풀이하지 않는다.
이 mailcow 의 docker 로그(json-file · 회전 없음 · 428MB)는 --since 만 주면 0줄이고
--tail 은 된다 — 처음부터 읽다 중간의 읽지 못하는 줄에서 말없이 멈추는 것으로 보인다.
로그 명령을 --tail 로 바꾸고, 로그 없이 지금 연결을 보는 doveadm who 를 더했다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
운영 mailcow.conf 에 DOVECOT_MASTER_USER · _PASS 가 주석 없이 있었는데, mailcow 의
generate_config.sh 가 두 줄을 빈 값으로 넣어 두므로 "주석이 아니면 정해 둔 것"으로 읽으면 틀린다.
값 대신 비었는지만 보는 확인으로 바꾸고, 끝낼 때는 줄을 지우지 않고 값을 비우게 했다.
mailcow 의 Fail2ban(netfilter)은 사설 · 루프백 주소를 막지 않는다(원본 main.py) — 예외가
필요한지는 mailcow 가 Contabo 앞단 · WSL 을 지난 서버 B 를 어떤 주소로 보느냐에 달렸다.
준비에 계정 없이 원본 로그인만 보는 --justlogin(두 쪽 모두 mailcow)과 mailcow 로그에서
그 주소 · 지난 사흘의 접속 주소를 보는 명령을 넣었다(개발 Dovecot 2.3.21.1 · mawk 1.3.4 로 시험).
운영 사실: 설치 폴더 /opt/mailcow-dockerized, 판 2026-05-12(32156a33).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mailcow.conf 와 mailcow 저장소가 root 소유라 sudo 없이는 grep 이 거절되고 git 이 dubious ownership 으로 멈췄다(2026-11-02 사용자 실행).
이전하는 동안 그 PC 가 절전 · 재부팅되면 imapsync 가 끊긴다는 줄도 더했다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
imapsync 절을 새 차례(계정 → 1차 → MX → 마지막 imapsync → 5분 뒤 이전 마치기 → 임시 비밀번호 나눠 주기)로 다시 썼다.
mailcow 고정 마스터 계정(사무실 PC 의 mailcow.conf), 서버 B 의 명령(고정 이미지 · 인증서 확인 · 비밀번호 파일),
개발에서 확인한 것(폴더 맞춤 · 수 · 날짜 · 표시 · 한글 폴더 · 차이분 · 만료 뒤 실패)을 적었다.
엔진 사실: 만료가 미래면 그때까지 IMAP 정상, 기본 만료의 찌꺼기는 끈 뒤에도 남는다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
전 직원 전환의 imapsync 는 새 엔진에 IMAP 으로 들어가야 하는데, 입사 · 재설정의 임시 비밀번호는
만료라 IMAP 이 끊긴다. 직원이 비밀번호를 바꾸면 마지막 차이분을 옮길 길도 없었다.
- 「입사」의 옮겨 올 메일 질문 → 이전용 비밀번호(엔진 만료 30일 뒤 · 실행 한 번 안에서 같은 값 · 메모리에만)
- 메뉴 「11. 메일 이전」 — 이전 현황 · 이전용 비밀번호 다시 만들기(세션을 먼저 끊는다) · 이전 마치기
(앱 비밀번호 → 세션 → imapsync 로그인 기록 지우기 → 임시 비밀번호 + 만료 → 세션, 알림 메일 없음, 끝에 표 한 번)
- 이전 중 = 엔진 만료가 미래(31일 안) + 실행 기록의 마지막 판정 기록이 이전용(hire-migration · migration-password)
— 엔진 기본 만료의 찌꺼기를 이전 중으로 읽지 않게
- 재설정 · 잠금 풀기 가드, 현황의 이전 중 표시, 엔진 기본 만료는 첫 화면 알림만
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
화면은 시각을 브라우저 시간대로 보이는데, 두 테스트의 고정 시각이 UTC 문자열이라
"오늘 오전 9:12" 같은 기대 글이 KST(개발 PC)에서만 맞았다 — CI(UTC)에서는 "오늘 오전 12:12" 로 세 건 실패.
고정 시각을 지역 시각(new Date(2026, …))으로 만들고, 같은 값을 다시 적던 기대값은 고정값을 가리키게 했다.
UTC · KST · America/Los_Angeles · Pacific/Kiritimati 에서 34건 통과, 전체 2,354건 UTC 통과.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 스킬 표: nestjs(제한된 세션 · 2단계 필수 정책 · 받은편지함 알림 · 운영 도구 · 15초 동기화 · 만료 계정의 엔진 사실), vue3(제한된 세션 화면 · 로그인 틀 단계 · 유예 줄 · 메일 프로그램 연결 · 30일 줄),
docker(이미지 표 — PostgreSQL · restic · shellcheck, 자동 설정 server, 호스트 systemd 백업 · 감시 절, setup 의 관리자 단단히 · 휴지통 30일, 로그 앵커 규칙, CI infra 잡).
- 규칙: security(제한된 세션 · 운영 도구 비밀 · 관리자 계정 · 알림 메일 링크 없음 · 퇴사 마무리 · 백업 비밀 · restic 의 root 예외), logging(컨테이너 로그 보존 · 실행 기록 · 자동 설정 쿼리),
env-structure(자동 설정 env · DB_IMAGE · ops.env 예외 · OPS_ALLOW_NO_TTY), error-codes(AUTH_006 ~ AUTH_010), checklist(13단계 운영 준비), release-ops 담당 범위(mail/ · ops/).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 계약 6절: 엔진 실측(만료 계정은 대리 접근 메서드도 막힘 · 2단계를 먼저 봄 · 잠긴 계정에도 402), 제한을 푸는 규칙(그 세션만 · 다음 제한으로 · 2단계를 켜면 다른 제한 세션은 끊음),
운영 도구의 차례(재설정 · 잠금 · 퇴사 마무리)와 결과를 모르는 실패, TTY, 만료 계정의 메일 API 401, 2단계를 켠 잠긴 계정의 로그인 AUTH_004.
- 디자인 README 13단계: 빈 칸 · 넘기지 못함 · 모르는 제한 상태.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 제한된 세션: restriction 이 있으면 보호 화면 대신 로그인 틀의 그 단계(새 비밀번호 → 2단계 켜기)로, AUTH_010 은 알림 없이 me 를 다시 받아 옮긴다.
제한된 동안은 나가기 확인을 묻지 않고(확인에서 이동을 취소하면 메일 화면에 갇혔다 — 리뷰) 실시간 알림을 열지 않는다.
- 로그인 때 넣은 비밀번호는 반응형이 아닌 클로저에 10분 · 세션이 끝나면 지운다. 2단계를 켠 직원의 새 비밀번호 단계는 다음 코드를 받는다.
- 유예 노란 줄(시작일 전날까지), 설정 › 2단계 인증의 필수 안내, 설정 › 메일 프로그램 연결(펼침 줄 · 새 Outlook 은 권하지 않음), 휴지통 · 스팸메일함 30일 줄, 로그인 · 코드 단계의 문의 줄.
검증: lint · type-check · 단위 2,354 · 실사용 시험 89/89.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
전 직원 전환 때 관리자가 계정을 다루고(입사 · 비밀번호 재설정 · 2단계 해제 · 잠금 · 퇴사 마무리 · 공용 주소) 회사가 2단계 인증을 필수로 할 수 있게 했다.
- 제한된 세션(AUTH_010): 임시 비밀번호(엔진 만료) · 2단계 필수인 직원은 로그인은 되지만 새 비밀번호 · 2단계 켜기만 지난다. 푸는 요청은 그 세션만 다음 제한으로 바꾸고,
2단계를 켜면 2단계를 거친 적 없는 다른 세션은 끊는다. 만료는 게이트웨이 대리 접근 세션의 권한(다섯)으로 로그인 · 갱신 때 알아챈다.
- 운영 도구 CLI(dist/cli/ops.js): 바뀔 것 → y/N → 실행 → ops_audit_log. 관리자 비밀번호는 TTY 에서만 · 입력 기록 0, 재설정은 앱 비밀번호 → 세션 → 알림 → 만료 차례(만료 뒤 대리 접근이 막힌다).
- 받은편지함 알림 메일(Email/import — 링크 없음): 재설정 · 2단계 해제 · 위험 파일 판정. 끊은 세션 기억은 15초마다 DB 에서 채운다.
- 2단계를 켠 잠긴 계정도 로그인은 AUTH_004 — 엔진이 잠긴 계정에도 2단계를 먼저 봐, 게이트웨이 자신의 권한으로 잠김과 설정 문제를 가른다(실사용 시험에서 찾음).
검증: lint 0 · 단위 3,138 · e2e 266 · 리뷰 두 번 · 실사용 시험(브라우저 + 운영 도구) 89/89.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
전 직원 전환 전에 서버가 스스로 지키고 되살릴 수 있게 했다.
- 관리자 단단히(setup): 관리자 역할에서 impersonate 를 빼고 관리자 허용 IP 를 앱 네트워크로 — 관리자 비밀번호로 바깥 993 · JMAP 에서 직원 메일함에 대리 로그인되던 길을 막았다.
운영에서 처음 걸 때는 되돌릴 복구 관리자가 있을 때만 건다(관리자 잠김 방지).
- 휴지통 · 스팸 30일 정리를 KST 03:00 으로 명시. imapsync 로 옮긴 메일도 옮긴 때부터 센다(임시 엔진 실측).
- restic 백업(호스트 systemd 03:30 — 메일 볼륨 warm → 엔진 멈춤 9초 안팎 → final, pg_dump, 업로드, env) · 복원(새 서버 차례를 빈 클론에서 실측) · 15분 점검 · 부팅 되살리기, Healthchecks 하트비트.
- 메일 프로그램 자동 설정: frontend nginx 의 autoconfig · autodiscover 고정 응답(엔진 것은 POP3 를 알리고 입력을 되돌려 쓴다).
- 모든 서비스 docker 로그 보존, DB 이미지 판 · digest 고정.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
관리자가 준 임시 비밀번호 바꾸기 · 2단계 인증 필수 · 메일 프로그램 연결 안내 · 받은편지함 알림 메일 · 휴지통 30일 안내와 서버 안 운영 도구를 정했다.
엔진 실험(Stalwart 0.16.27): 계정 끄기 칸이 없어 로그인 권한을 빼서 잠그고, 비밀번호가 만료되면 웹메일(대리 접근)도 메일 메서드가 막혀 서버가 새 비밀번호 단계로 보낸다.
사용자 결정(2026-10-31): 새 비밀번호 단계는 다음 코드를 기다려 넣고, 필수 직원의 휴대폰 바꾸기는 관리자 해제, 알림 메일에는 링크를 넣지 않고, 새 Outlook 은 권하지 않는다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
security: 2단계 인증 · 앱 비밀번호 · 끊은 세션 바로 막기 · 엔진이 부르는 길(세 겹) · 요청 대상의 날 \ · #.
nestjs · vue3: 골격 표에 계정 보안 행, 확장 때 옮길 메모리 상태, 엔진 Basic 402 는 장애가 아니다.
docker · env-structure · checklist: MAIL_WEBHOOK_SECRET · /api/internal 닫기 · 닫는 경로 시험 · setup 의 웹훅.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
계약 5절에 구현과 리뷰에서 정한 것을 반영했다 — 같은 코드가 확인 중이어도 AUTH_006, 402 는 정식 주소 기준,
웹훅은 내용 지문 · DB 에 넣은 뒤 200 · 1,000행씩, 요청 대상의 날 \ · # 는 400, 끊은 세션의 실시간 알림 닫기 등.
디자인 README 12단계 절에 QR 160 · 결과 창 [복사] 44 · 10분 · 코드 단계 오류 자리 · 코드 2분 다시 받지 않음을 더했다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
메일 프로그램(IMAP · SMTP) 로그인 기록을 엔진 웹훅으로 받는다.
- 루트 env MAIL_WEBHOOK_SECRET(setup · backend 공용, 운영 필수)과 backend 의 APP_NETWORK_SUBNET
- setup 이 엔진 웹훅을 맞추고(표시 머리글 — 소금 + 키 지문) 서명한 빈 묶음으로 받는 곳을 확인한다
키를 바꾼 실행 · 게이트웨이 OTP · 테스트 직원의 402 를 따로 다룬다
- nginx 두 server 가 /api/internal/ 을 닫고, 요청 대상의 날 \(경로) · # · 0x80 이상은 location 전에 400
Express 가 이런 대상을 다시 읽어 닫은 경로(/api/internal · /api/health)가 백엔드에서 열렸다(2026-10-31 리뷰)
- CI "닫는 경로 시험"(두 server × 세 경로의 변형) · 지시어만 세는 정적 검사
- mail/README: 웹훅 · 12단계 전부터 운영 중인 서버를 올리는 차례 · 휴대폰을 잃은 직원(관리 API 명령)
엔진 DNS 가 Custom(운영)이어도 웹훅은 backend 를 푼다 — 메일 컨테이너가 도커 DNS 를 쓴다(개발에서 확인).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
로그인은 비밀번호가 맞고 2단계를 켠 직원만 코드 단계로 간다. 설정 › 계정에 2단계 인증 · 앱 비밀번호 · 로그인 기록을 더하고,
비밀번호 화면에 2단계를 켠 직원의 코드 칸을 둔다.
비밀(코드 단계의 비밀번호 · 2단계 키 · 앱 비밀번호 값)은 화면 메모리에만 두고, 세션이 바뀌면 그 창을 모두 닫는다 —
12단계부터 다른 기기의 로그아웃이 이 세션을 바로 끊는다. 비밀이 드는 요청은 Pinia 액션 밖에서 보낸다(devtools 기록).
QR 은 uqr 0.1.3(MIT · 의존성 없음 — 2단계 인증 화면 청크에만)으로 SVG 를 그린다(v-html 없음).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
엔진(Stalwart 0.16.27)의 TOTP 와 앱 비밀번호를 쓰되, 엔진이 하지 않는 것을 서버가 맡는다.
- 등록 때 첫 코드 확인, 성공에 쓴 코드 2분 막기(확인 중인 같은 코드도), 코드 실패 한도(15분 5 · 24시간 20)
- 웹메일 로그인 · 비밀번호 바꾸기에서 앱 비밀번호 모양 거절, 앱 비밀번호는 메일 프로그램 권한만
- 엔진 Basic 의 402 를 장애(MAIL_001)로 다루던 것을 고쳐 코드 단계로 — 402 면 정식 주소로 한도 · 기록
- 비밀번호를 바꾸면 앱 비밀번호도 지우고, 끊은 세션은 액세스 토큰의 sid 로 바로 막는다
- 메일 프로그램 로그인은 엔진 웹훅(소켓 주소 · HMAC · 내용 지문 · 1,000행씩)으로 받아 90일 남긴다
- 요청 대상에 날 \ · # 가 있으면 라우팅 전에 400 — Express 가 이를 다시 읽어 앞단이 닫은 경로가 열렸다
에러 코드 AUTH_006 ~ AUTH_009 와 웹 · 앱 사전, 마이그레이션 AddAccountSecurity 를 함께 넣었다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2단계 인증(인증 앱 TOTP) · 앱 비밀번호 · 로그인 기록을 엔진(Stalwart 0.16.27) 실험 결과에 맞춰 정했다.
엔진이 코드 확인 · 메일 프로그램 막기 · 앱 비밀번호를 맡고, 서버는 첫 코드 확인 · 다시 쓰기 막기 · 실패 한도 · 기록을 맡는다.
사용자 결정(2026-10-31): 비밀번호를 바꾸면 앱 비밀번호도 지우고, 끊은 세션은 바로 막는다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 운영 서버 첫 배포와 파일럿 공존을 켜며 실제로 막힌 것을 mail/README 에 적었다.
- 도메인 자동 인증서의 Additional Hostnames 를 비우면 기본 이름 묶음을 한 주문으로 요청해, 옛 서버를 가리키는
autoconfig 등 때문에 mx1 까지 못 받고 자체 서명 인증서를 냈다 — mx1 만 넣고 AcmeRenewal 한 번.
- mailcow 는 받는 IP 와 나가는 IP 가 달랐다 — Transport Map Test 가 550 5.7.1 이면 새 서버 로그의 remoteIp 로 찾아 목록에 더한다.
- 가비아는 TXT 조각마다 250자 미만이라 RSA DKIM 을 다시 나눠 넣는다(명령 포함).
- 첫 전달의 mailcow 451 지연, Gmail 의 ed25519 neutral, Spamhaus 시험 일시 SERVFAIL, 새 셸의 MAIL_ADMIN_URL 은 문제 해결 줄로.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- security: 대용량 첨부는 확장자 허용 목록만 보는 예외와 그 세 겹, 공개 링크(토큰 해시 · 공개 호스트 분리 · 조건부 UPDATE ·
경로마다 전체 상한 · 앞단과 백엔드가 경로를 같게 읽기 — 대소문자 두 겹).
- nestjs: 파일 저장 · 조각 올리기 / 공개 링크 · 내려받기 행, 라우팅은 대소문자를 가린다(createHttpAdapter), 경로 숫자 함정,
확장 때 옮길 것(ChunkGate · 검사 차례 · cron · FILE_BLOB_STORE).
- vue3: 대용량 첨부 행. docker: 두 server · 스니펫 · 받기 location · limit_conn · 머리글 버퍼 · ClamAV 절 · 이미지 표 · 볼륨 · CI ·
Git Bash 경로 변환 함정. env-structure · checklist: SHARE_PUBLIC_URL(포털과 다른 호스트 — CORS_ORIGIN 에 넣지 않음) · CLAMAV_HOST.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 쓰기: 20MB 넘는 파일 · 합계 25MB 를 넘기는 파일 · 메일에 못 담는 형식은 저절로 링크로(묻는 창 없음). 고르자마자 서버에 만들고
16MiB 조각(Content-Digest)을 파일 3개까지 함께, 끊기면 2 · 5 · 15 · 30초로 다시 · 1분 넘으면 멈춤 + [다시 시도],
새로고침 뒤 같은 파일을 다시 고르면 잇는다. 다 올릴 때까지 [보내기]는 aria-disabled + 까닭, 위험 판정은 빨간 줄 + 실시간 알림.
대용량 파일이 있으면 첫 원문 인용 앞을 data-ara-quote-start · bodyQuoteStart 로 알린다(서버가 그 자리에 덩이를 넣는다).
- 읽기: 첨부 칸의 대용량 항목(서버의 largeFiles), 본문 덩이 접기, 보낸 사람의 받은 수 · 링크 끊기(끊은 날 — revokedAt).
- 설정 › 메일 › 대용량 첨부: 보관 사용량 · 보내지 않은 파일 · 보낸 링크(끊기).
- 공개 화면 /s/:token: 세션 복원 없이(meta.public) · credentials omit · no-referrer, 라우트는 대소문자를 가린다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- file-storage: /api/file-uploads(만들기 · 16MiB 조각 PUT — Content-Digest · datasync · 같은 조각은 하나만 · 끝내기 · 상태 · 지우기),
/api/file-quota. 보관량(직원 · 서버 · 디스크 여유)은 pg_advisory_xact_lock 안에서 센다. 저장 포트 FILE_BLOB_STORE(지금은 로컬 볼륨).
ClamAV INSTREAM 을 직접 구현했다 — 판정 글은 한 곳(암호 · 한도 → 검사하지 못함, 스트림 크기 한도 → 설정 오류로 검사 중 그대로 + error),
다 보낸 뒤 판정 대기 30분 + 1MB 마다 1초 · TCP keepalive. 검사 차례는 DB pending(동시 2), 정리 cron.
- file-sharing · mail: 메일 한 통에 링크 하나(토큰 256비트 — DB 에는 SHA-256 만). 보낼 때 서버가 본문에 덩이를 넣고(표시는 서버만 —
두 정화기가 사용자 HTML 의 data-ara-* · 주석을 지운다), 링크는 확실한 실패(SendNotStartedException)에서만 거둔다.
공개 /api/public-links/{token} · 파일 받기(Range · 조건부 UPDATE 로 받은 수), 링크 끊기 · 목록 · 읽기 화면의 largeFiles.
- 공개 호스트 미들웨어는 원문 · 라우터 경로 · WHATWG URL · 퍼센트를 푼 원문을 모두 보고(리뷰 — 절대 주소 꼴 · \ 요청이 막기를 지났다),
라우팅은 대소문자를 가린다(createHttpAdapter — /FILES/ 같은 변형이 앞단 상한을 비켜 갔다).
- env: SHARE_PUBLIC_URL(nginx 와 같은 모양 검사 · 운영에서 포털과 같은 호스트면 기동 중단) · MAIL_LARGE_FILE_* · FILE_STORAGE_* · CLAMAV_*.
- 에러 코드 FILE_003 ~ FILE_008 · MAIL_016 — 웹 · 앱 사전 다시 생성. 마이그레이션 둘.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ClamAV 1.4.6 LTS 를 둘로 나눴다: clamd 는 백엔드와만 닿는 internal 네트워크(바깥 길 없음 · 서명 볼륨 읽기 전용),
freshclam 은 자기 바깥 길에만. 비-root · cap_drop ALL. 운영은 늘 뜨고 개발은 profile clamav.
clamd.conf 는 한 파일 한도 2047M 까지 끝까지 검사하고 암호 · 한도 초과를 판정으로 알린다(실측표는 mail/README).
검사 중에 죽으면 남던 임시 파일을 띄울 때 지운다.
- nginx 를 포털 · 공개 내려받기(files.) 두 server 로 가른다(SHARE_PUBLIC_URL 과 CORS_ORIGIN 의 호스트 비교 — 같으면 포털이 함께 연다).
조각 PUT 17m, 받기는 버퍼링 · 압축 끔 + X-Accel-Buffering 넘김 + 머리글 버퍼 16k,
limit_conn 은 X-Forwarded-For 마지막 칸으로 IP 마다 공개 10 · 앱 안 30, 전체 공개 400 · 앱 안 200.
받기 location 은 /api/ 블록 안의 대소문자를 가리지 않는 정규식이다(리뷰 — /FILES/ 같은 변형이 상한을 비켜 갔다).
접근 로그는 경로 모양과 43자 토큰 모양으로 토큰을 가린다.
- 파일은 named volume <PROJECT_NAME>_files(개발도 — builder 단계가 /app/files 를 node 소유로 만든다), 백업하지 않는다.
- CI infra 잡: nginx 두 꼴 · 받기 경로 변형 시험(가짜 백엔드) · clamconf(-n 없이 기본값까지) · 틀린 SHARE_PUBLIC_URL 거부.
- mail/README: 악성 검사 · 공개 호스트 · NPM 칸(Advanced 다섯 줄) · 메모리 예산 · 문제 해결.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
일반 첨부 한도를 넘는 파일을 로그인 없이 받는 링크로 보내는 기능의 계약과 화면 원본이다.
계약 4절은 2026-10-30 확정한 한도(파일 2GB · 메일 10개 · 직원 10GB · 30일)와 조각 올리기 ·
악성 검사(ClamAV 실측 판정) · 공개 호스트 분리 · 받기 동시 연결 상한 · 운영 확인을 담는다.
리뷰에서 바꾼 것(경로 대소문자 · 공개 호스트 경로 해석 · 판정 대기 30분 + 1MB 마다 1초 ·
전체 상한을 공개 400 · 앱 안 200 으로 나눔)과 알려진 한계도 함께 적었다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docker 스킬에 리졸버 네트워크 두 개 · 이미지 고정(digest) · 비-root · CI 설정 검사 ·
장애와 재부팅 때 할 일을, env 규칙에 MAIL_RESOLVER_SUBNET/IP 와 setup 전용 비밀
MAIL_SPAMHAUS_DQS_KEY(운영은 파일에만)를 더했다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
실제 시범은 직원 메일함이 아니라 관리용 새 주소로 한다. mailcow 에 같은 주소의 메일함을
받기 전용으로 먼저 만들어야 Transport Map 과 reject_unlisted_sender 가 맞는다.
운영 mailcow 는 SSH 없이 관리 화면만 쓸 수 있어, 명령 절차마다 화면에서 하는 길을 표로 적었다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
엔진의 DNS 조회(DNSBL · SPF · DKIM · DMARC · MX · PTR · TLSA)를 공용 · ISP DNS 가 아니라
이 서버의 리졸버로 푼다. Spamhaus 는 공용 DNS 를 거친 조회를 거부하고, DNSSEC 을 검증해야
엔진의 DANE 이 켜지기 때문이다.
- mail-resolver: unbound 1.26.2(digest 고정 · 비-root · cap_drop ALL). mail 과만 닿는
internal 네트워크 mail-dns 와 바깥 길 mail-dns-egress. 사설 주소의 답 · 질의를 막는다.
- setup: 바꾸기 전에 리졸버 네트워크 · 주소 · DNSSEC 을 확인한다(UDP 는 루트 SOA, TCP 는
루트 DNSKEY — UDP DNSKEY 는 루트 키 교체 기간에 잘려 오탐이 난다). 엔진 DNS 는 운영 Custom,
개발 System. Spamhaus 키가 있으면 시험 항목을 확인한 뒤 DQS 로 켜고 없으면 두 항목을 끈다.
키는 모든 출력에서 가린다.
- mail 은 리졸버를 기다리되(required: false) 리졸버 오류 하나로 메일 전체가 서지 않게 한다.
- README: 리졸버와 스팸 차단 목록, 운영 배포 차례, 리졸버 장애 때(리로드 거절 · restart 비상 길),
DNSSEC 이 깨진 도메인의 예외.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
운영 frontend 는 호스트 포트를 열지 않는다는 규칙에 예외(remote-proxy — 사설 IP 에만)를 적고, compose 가 --env-file 의
COMPOSE_FILE 을 읽는다는 것(빈 값은 실패), 프록시가 다른 서버면 TRUST_PROXY 를 주소 목록으로 둔다는 것을 env · 체크리스트에 맞췄다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
운영 메일 서버를 NCP 에 따로 두고, 포털의 TLS 는 기존 서버의 Nginx Proxy Manager(다른 사이트들과 80 · 443 을 함께 쓴다)가
같은 VPC 사설망으로 넘기기로 했다. 메일 엔진이 그 서버의 443 을 직접 써야 해서(ACME · MTA-STS) 프록시를 같은 호스트에 둘 수 없다.
- docker-compose.remote-proxy.yml: frontend 를 proxy 네트워크에서 빼고 8080 을 FRONTEND_PRIVATE_BIND(사설 IPv4:포트)에만 게시한다.
compose 의 ${…:?} 는 빈 값만 막아 포트뿐인 값이면 모든 주소에 열리므로, 한 번짜리 frontend-bind-check 가 값을 먼저 보고
아니면 frontend 를 띄우지 않는다(네트워크 없음 · 비-root · DB 이미지의 셸).
- .env.production 의 COMPOSE_FILE 로 켠다 — 운영 명령(--env-file .env.production)을 그대로 쓰고 -f 를 빠뜨려 기본 구성으로 도는 일이 없게.
- TRUST_PROXY 는 <EDGE_NETWORK_SUBNET>,<프록시 서버 사설 IP> 주소 목록, NPM 은 v2.16.0 소스로 기본값을 확인해 고급 칸 세 줄
(읽기 150s · 접근 로그 끔 · 사설 대역의 X-Real-IP 믿지 않기)과 Block Common Exploits 끄기(메일 검색이 403)를 적었다.
- NCP 는 공인 IP 를 사설 IP 로 NAT 하므로 게시 주소로는 바깥을 막지 못한다 — 8080 은 ACG 가 프록시 서버 사설 IP 에서만 받는다.
- CI infra 잡이 이 조합의 config 와 frontend-bind-check 의 받을 값 · 거절할 값을 본다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
파일럿 키 둘은 setup 만 읽는다(compose · backend 에 넘기지 않는다). 셸 값과 env 파일이 다르면 멈추는 규칙의 예외를 바로 적었다 —
복구 관리자(원래 셸 일회성)와 파일럿 키: 개발은 한 번 시험하려고 셸로 줄 수 있고, 운영은 파일에 없는 시범 값이 셸에 있으면 멈춘다
(엔진에 남는 설정이라 파일만 보고도 알 수 있게).
docker 스킬에는 켜고 끄는 차례 · 가드 없는 relay 를 만들지 않는 규칙 · "끄지 못했습니다"일 때 할 일 · 운영 확인 항목을 더했다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
전면 전환 전에 한 사람만 같은 @arasystem.it.kr 주소로 새 서버를 쓰고 나머지 직원은 mailcow 에 둔다.
MX 는 mailcow 그대로이고 mailcow 의 Transport Map 이 시범 주소 하나만 새 서버로 넘긴다 —
새 서버는 자기에게 없는 사내 주소를 mailcow 로 중계해야 해서 루트 env 두 개(MAIL_PILOT_LEGACY_HOST ·
MAIL_PILOT_LEGACY_IPS)로 setup 이 맞추게 했다.
- 켜는 차례는 막는 것부터: 시범 표시(기존 서버 IP 의 자동 차단 예외) → RCPT 가드(인증 없는 세션의 없는 사내 주소 550) ·
위조 막기(사내 도메인 MAIL FROM 은 mailcow IP 에서만) · 그 IP 의 SPF · 역방향 DNS · DMARC 검사와 받는 쪽 한도 예외 →
리로드 → 켜기 전 가드 확인 → legacy 경로 · 발송 전략(인증한 제출 · 엔진이 만든 메일만 legacy) → 리로드 →
allowRelaying → 켠 뒤 확인. allowRelaying 은 저장 즉시 먹어 가드보다 먼저 켜면 열린 중계가 된다.
- 가드 없는 relay 를 setup 이 만들지 않는다: 가드는 "2초 안에 550"으로 가린다(가드 거절은 곧바로 · 엔진 자체 거절은
waitOnFail 5초 뒤). 아니면 켜지 않거나 바로 다시 끄고, 끄지 못하면 가드만이라도 다시 건 뒤 사실대로 알리고 멈춘다.
끌 때는 allowRelaying 을 먼저 끄고 엔진에서 다시 읽어 꺼진 것을 본 뒤에만 나머지를 뺀다 — 주인을 모르는 relay 는
끄지도 가드를 빼지도 않는다.
- 설명 칸이 없는 식의 주인은 시범 표시의 주소로 가린다 — 같은 꼴의 운영자 설정은 덮어쓰지 않고, 헷갈리면 멈춘다.
- 운영은 env 파일의 값으로만 켜고(포트 25 만 · 문서 대역 · 사설로 풀리는 이름 · 자기 자신 거부), 값이 빠졌다고
저절로 끄지 않는다(끄기는 --end-pilot). 처음 켤 때 DKIM TXT 가 없거나 엔진의 키와 다르면 켜지 않는다.
개발은 셸 값으로 한 번 시험한다.
리뷰 세 번(H1 · M1 · M2 · L1~L9 → N1~N8 → R1~R4)을 반영했다. 개발 엔진(0.16.27, 기존 서버 역 Mailpit)에서
켬 · 같은 값으로 다시([유지]) · 끔을 돌려 끈 뒤 엔진 설정이 처음과 같았다.
mailcow 쪽 절차와 운영 흐름은 아직 해 보지 않았다 — mail/README.md "파일럿 공존".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
시범 동안 새 서버에 없는 사내 주소는 엔진이 mailcow 로 중계하므로 웹메일이 곧바로 not-found 로 알리지 못하고
나중의 반송 메일로 알게 된다. 사내 목록도 새 서버의 계정(시범 사용자)뿐이다.
클라이언트 · 서버 코드는 바꾸지 않았다 — 계약과 받는 사람별 결과 주석에 그 차이를 적었다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nestjs: 보내기 취소 · 예약 발송 · 외부 이미지 중계 · 차단과 설정 행, 체크리스트에 "붙잡힌 메일을 옮기거나 지우는 새 경로는 readHeldEmails 로 먼저 막는다"와
"발송 제출은 envelope 을 직접 준다"(주지 않으면 엔진이 받지 않는 주소를 말없이 뺀다), 인스턴스를 늘릴 때 옮길 것에 이미지 표의 암호 키 · 이미지 동시 수 · 설정 60초 기억.
vue3: 9단계 행 셋(보내기 취소 · 예약, 외부 이미지 · 인쇄, 차단 · 설정)과 밀도 예외의 쓰임(새 토큰 없음).
docker: 외부 이미지 중계 때문에 백엔드가 바깥 80 · 443 으로 나가야 하는 것, 도커 망 대역은 사설로, 같은 외부 프록시 뒤에 "내부 주소면 허용" 사이트를 두지 않는 것,
그 경로만의 요청 수 한도(IP 마다 1분 600회 — 뉴스레터 한 통에 이미지가 수십 개).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
쓰기 화면의 [보내기 ▾](예약 발송 창), 보내기 취소 알림(남은 시간 막대), 메일함 패널 「예약됨」 · 예약 목록, 읽기 화면의 안내 줄([이미지 보기] · [고치기] · [예약 취소] · [차단 풀기]),
인쇄 화면(새 탭 — A4 폭으로 다시 재서 빈 쪽 없이), 보낸 사람 차단 창, 설정 › 보내기 · 읽기 · 차단한 주소. 디자인 README "9단계", 계약 2절.
리뷰로 고친 것:
- 붙잡힌 메일을 여러 통 처리로 지워도 예약이 그대로 나갔다 — 고르지 못하게 했다(고르기 칸 aria-disabled + 까닭, 하나 · 범위 · 모두 고르기가 건너뛴다).
읽기 화면 [삭제]는 확인(처음 초점 「취소」) 뒤 발송을 취소하고 휴지통으로 — 서버도 MAIL_015 로 막는다.
- 엔진이 발송을 풀어도 상태가 바뀌지 않아 "보내는 중" · 「예약됨」이 남았다 — 보낼 시각 + 2초에 목록 · 열린 메일 · 예약 수를 다시 맞춘다(MAIL_014 때도).
- 설정을 연달아 고르면 늦게 온 앞 응답이 화면과 서버를 갈랐다 — 칸마다 저장은 한 번에 하나, 끝나면 마지막으로 고른 값으로 맞춘다.
- 응답을 기다리는 사이 다른 목록으로 간 사용자를 옛 목록의 다음 메일로 끌고 갔다 — 같은 목록 · 같은 메일을 보고 있을 때만 옮긴다(3 · 4단계 단추도).
- 이미지를 보일 때 본문 틀의 CSP 를 우리 출처의 중계 경로로만 연다 — 'self' 면 정화를 빠져나간 CSS url() 이 다른 API 를 부를 수 있었다.
- 알림 단추는 한 번만 돈다, 보내기 취소 알림의 마감은 설정한 초로 묶는다, [보내기 ▾] 메뉴는 화면 안에서 연다(쓰기 화면이 움직이지 않게),
차단 목록 · 설정 저장의 상태가 멈추던 것, 초점을 잃던 것과 알리지 않던 라이브 영역.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
계약 2절 9단계. 보내기 취소 · 예약은 엔진 제출의 HOLDFOR · HOLDUNTIL 로 붙잡고(7일 안), 취소는 undoStatus: canceled 뒤 그 메일을 임시보관함의 초안으로 되돌린다.
외부 이미지는 서버가 대신 받아 온다 — 모든 주소가 공인일 때만 푼 주소로 고정 연결 · 80/443 · 다시 가기 3번 · 10초 · 5MB · 앞 바이트로 그림 형식, 중계 주소의 표는 AES-GCM 으로 암호화.
차단은 필터 · 부재중과 같은 Sieve 스크립트의 맨 앞, 설정은 DB 표 mail_settings(마이그레이션 AddMailSettings).
리뷰로 고친 것:
- 붙잡힌 메일을 옮기거나 지워도 엔진은 시간이 되면 보낸다 — 한 통 · 여러 통 PATCH · DELETE · 영구 삭제 · 메일함 지우기 · 비우기 · 필터 적용이 엔진에 먼저 묻고 막는다(MAIL_015 · failedIds · skippedIds).
읽지 못하거나 다 받았는지 응답의 total 로 확인하지 못하면 바꾸지 않는다(MAIL_001), 보낼 시각을 읽지 못한 기다리는 제출도 붙잡힌 것으로 본다.
- 발송은 늘 envelope 을 직접 준다 — 엔진이 오타 도메인 같은 주소를 말없이 빼고 보냈다. 거절한 주소를 not-found 로 빼고 다시 제출한다
(한 차례에 하나 — 거절 글의 따옴표 토큰과 정확히 같은 주소부터, 20차례까지), 같은 키 재시도는 제출의 envelope 으로 같은 답을 다시 만든다.
- 이미지 중계: 서버 16 · 직원 6 동시 수(받는 수가 적은 직원부터), 자리는 응답이 끝날 때까지, 버퍼는 한 번에 — 메모리가 동시 수 × 5MB 를 넘지 않게.
- 다시 취소해도 200(취소 표시가 있으면 옮기지 않는다), 엔진이 확인한 취소만 취소로 본다, 모두 거절된 붙잡은 발송은 취소한 뒤 사본을 지운다.
- 차단 주소는 dot-atom 만, 이미 있는 주소를 POST 하면 꺼진 규칙을 켠다, 로그의 중계 주소 가리기는 대소문자 무시.
- 연결 · 이름 풀기를 실제 소켓으로 시험한다(테스트 전용 포트 · DNS 서버 — 앱은 80/443 고정).
에러 코드 MAIL_014(이미 보냄) · MAIL_015(붙잡힌 메일)를 더하고 MAIL_013 문구에 차단 목록을 넣었다(뜻은 그대로). 클라이언트 사전을 생성했다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
보내기 취소 · 예약 발송(엔진이 제출을 붙잡는다 — FUTURERELEASE), 외부 이미지 중계, 인쇄, 보낸 사람 차단, 보내기 · 읽기 설정의 계약(2절)을 정하고,
확정한 캔버스 보드 넷(MailReadTools · MailPrint · MailSendLater · SettingsSendRead)을 둔다. 디자인 README 에 구현하며 정한 보완을 적었다.
리뷰(화면 한 번 · 서버 두 번)로 정한 것도 함께 적었다 — 붙잡힌 메일은 취소로만 보낸편지함을 떠난다(MAIL_015 — 모든 옮기기 · 지우기 길, 확인하지 못하면 닫힌 쪽),
발송은 늘 envelope 을 직접 준다(엔진이 받지 않는 주소를 not-found 로 빼고 다시 제출 — 전에는 말없이 빠졌다), 다시 취소해도 200,
이미지 중계의 동시 수(서버 16 · 직원 6 — 받는 수가 적은 직원부터), 차단 주소는 dot-atom 만 · 다시 POST 하면 꺼진 규칙을 켠다.
엔진 사실(2026-10-29): envelope 의 거절은 제출 전체 · 첫 주소 하나, 내보내는 사본에서 Bcc 를 뗀다(개발의 Mailpit 이 보이는 Bcc 줄은 Mailpit 이 붙인다), 제출 조회의 정렬 이름은 sentAt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nestjs: 주소록 행(캐시 · 크기 한도 · PATCH 병합 · 되돌리기 표), 받는 사람 q 맞추기가 frontend 와 짝이라는 것,
DTO 검사 차례(그냥 쌓으면 아래 것부터 돈다 — applyDecorators 로 형식 → 길이 → 모양), 엔진 사실(같은 id 를 한 요청의 destroy 에 두 번 넣지 않는다),
인스턴스를 늘릴 때 옮길 메모리 목록에 되돌리기 표.
vue3: 주소록 · 메일과 함께 행, 받는 사람 자동 완성이 초성도 서버에 묻는 것, ModalDialog medium, 8단계의 밀도 예외(새 토큰 없이 다시 씀).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
레일의 주소록 모듈 — 내 연락처 · 그룹 · 사내 목록(직원 · 공용 주소), 초성 찾기, 연락처 추가 · 고치기 창, 그룹 · 구성원 넣기 —
과 메일 연동(받는 사람 추천의 주소록 · 그룹, 연락처 · 그룹 · 사내의 [메일 쓰기], 읽기 화면 ⋯ 「보낸 사람을 주소록에 추가」)을 더한다.
디자인 README "8단계", 계약 3절.
리뷰로 고친 것:
- 쓰기 화면을 채우는 라우터 상태에 연 직원을 싣는다 — 같은 탭에서 다른 직원이 로그인한 뒤 뒤로 가면 앞 직원의 받는 사람이 채워졌다.
- [고치기]는 창을 열 때 새로 받고 바꾼 칸만 보낸다 — 받아 둔 값으로 다른 탭 · 휴대폰의 변경을 덮었다. 검사도 바꾼 칸만.
- [되돌리기]는 서버의 되돌리기 표로 — 다시 만들기로는 다른 프로그램의 칸 · uid 를 잃었다.
- 읽기 화면에서 연 연락처 추가 창을 메일 레이아웃으로 옮겼다 — 메일을 바꾸면 쓴 것이 확인 없이 사라졌다.
- 그룹을 이어 고르면 앞 그룹이 빠지던 것, 저장 중 창을 닫으면 거절이 묻히던 것, 구성원 넣기의 첫 쪽이 모두 구성원이면 막히던 것(excludeGroupId).
- 받는 사람 추천은 초성 · 조합 중 글자도 서버에 묻는다(서버가 같은 맞추기 — 같은 표본 표를 두 쪽 테스트에 둔다).
- 바꿀 때마다 불러온 쪽을 모두 다시 받지 않고 응답을 서버 차례로 그 자리에 넣는다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
계약 3절. 내 주소록은 엔진의 JMAP Contacts(직원마다 기본 주소록)에, 사내 목록은 엔진 계정 목록을 사내 목록 계정으로만 읽는다.
엔진 검색은 낱말 단위이고 이름 정렬을 못 해서, 직원마다 주소록을 엔진 state 로 확인하는 캐시에 두고 서버가 찾고(초성 · 전화 숫자) 정렬한다.
쓰기는 직원마다 차례로 · ifInState 로 하고, 어긋나거나 엔진이 잠깐 거절하면 다시 읽어 세 번까지.
리뷰로 고친 것:
- 전화번호 정규식이 입력 길이의 제곱이었다(1MB 본문 하나로 약 24분 CPU) — 정규식을 나누고, DTO 검사를 applyDecorators 로 묶어 형식 → 길이 → 모양 차례로 돌린다.
- 고치기는 PATCH(보낸 칸만 검사) — 칸 안의 남의 속성(label · contexts · fax · pref · role · 둘째 조직)은 지금 카드와 합쳐 지킨다(CardDAV 와 함께 쓰려고).
- [되돌리기]는 지우기 직전에 읽은 카드 전체를 5분 기억했다가 같은 uid 로 다시 만든다(되돌리기 표 — 우리 규칙 밖의 값 · 다른 프로그램의 칸도 그대로).
- 큰 주소록이 캐시에 들지 못해 요청마다 엔진을 통째로 다시 읽었다 — 크기 한도 800만을 쓰기에 걸고 캐시의 한 사람 상한과 맞췄다.
- 받는 사람 추천의 q 를 화면과 같은 맞추기(초성 · 조합 중 글자)로 — 주소록이 크면 미리 받은 100줄이 주소록으로 차서 사내 · 최근 주소의 초성 찾기가 사라졌다.
- excludeGroupId · 사내 항목 하나 · q 254자, 판은 읽기 전에 물은 것으로, 한글 이름 조각은 붙여 잇는다.
에러 코드 CONTACT_001(같은 이름 그룹) · CONTACT_002(주소록 한도)를 더하고 클라이언트 사전을 생성했다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
사내 목록은 엔진의 계정 목록(이름 · 주소)을 읽어야 하는데, 직원 역할은 그 권한을 일부러 막아 두었고 엔진 관리자는 그 권한을 내주지 못한다.
그래서 setup 이 복구 관리자로 돌 때 비밀번호 없는 읽기 전용 계정(MAIL_DIRECTORY_LOCAL_PART, 기본 mail-directory)을 만든다 —
백엔드는 게이트웨이 대리 접근으로만 쓴다. 메일을 받지 않게 역할에서 받기 권한을 빼고 저장 한도도 작게 둔다.
관리자 · 게이트웨이 · 사내 목록 계정은 메일을 받지 않는데, 엔진은 RCPT 를 받아 놓고 배달할 때 거절해 보낸 사람에게 반송했다(backscatter —
위조된 발신자에게 반송이 나가 서버 평판이 떨어진다). RCPT 단계 스크립트로 이 주소(+태그 꼴 포함)를 없는 주소처럼 550 으로 거절하고,
setup 이 끝날 때 RCPT 만 보내 스스로 확인한다. 다른 설정이 그 자리를 쓰고 있으면 건드리지 않고 알린다.
일반 관리자는 공용 주소(Group 계정)를 만들지 못한다는 것도 운영 배포 절차에 적었다 — 복구 관리자로 만든다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
주소록(내 연락처 · 그룹 · 사내 목록)과 메일 연동의 계약(3절)을 정하고, 확정한 캔버스 보드 셋(Contacts · ContactsEdit · ContactsMail)을 둔다.
리뷰(서버 · 화면)로 정한 것도 함께 적었다 — 되돌리기는 서버가 원본을 5분 기억했다가 그대로 다시 만들고,
고치기는 PATCH(바꾼 칸만 · 칸 안의 남의 속성을 지킨다), 받는 사람 추천의 q 는 쓰기 화면과 같은 맞추기,
주소록 크기 한도 800만(캐시가 통째로 기억할 수 있게), excludeGroupId · 사내 항목 하나 · q 254자, 쓰기 화면 채우기는 연 직원에게 묶는다.
엔진이 한 요청의 destroy 에 같은 id 가 두 번 있으면 통째로 serverUnavailable 로 거절하는 것도 적었다(2026-10-29 확인).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- vue3 스킬: 이미 받은 메일에 필터 적용 행, 진행 알림 결과의 단추, ModalDialog 의 reshow(),
모달 창의 나가기 확인(useDialogLeaveConfirm), 부재중 isActive 줄
- nestjs 스킬: mail-filter-runs 의 부품(판정 · 견줄 글 · HTML 글자 · cursor 2판 · 본문 상한)과
엔진 Sieve 와 짝인 파일을 함께 고치고 실배달로 다시 맞추는 규칙, 인스턴스를 늘리면 옮길 잠금에 filterRuns
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 고치기 창의 「받은편지함에 이미 있는 메일에도 적용」(창을 열 때마다 꺼짐): 저장한 뒤 cursor 가 끝날 때까지
보내며 진행 알림을 고치고, 끝나면 그 자리에서 결과(적용한 수 · [{메일함} 보기], 멈추면 [다시 시도]로 이어서)
- 이미 받은 메일에 할 일이 없는 필터(지워진 메일함 · 받은편지함으로만 · 전달만)와 이 탭에서 적용 중에는 칸을 끈다.
실패 알림은 응답이 온 때 세션이 그대로일 때만, 나아가지 못한 응답이 세 번이면 멈춘다
- 고치기 창의 나가기 확인([취소] · Esc · 다른 화면 · 탭 닫기 — 6단계의 한계를 고친다). 브라우저가 스스로 닫은 창은
ModalDialog 의 reshow() 로 다시 띄우고 초점을 닫히기 전 자리로 돌려준다
- 부재중 화면: 켜 둔 자동 응답이 엔진에서 꺼져 있으면(isActive: false) 머리 아래 노란 줄
- 진행 알림의 결과에도 단추를 붙인다(useToast)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
본문의 규칙 하나로 받은편지함을 최근 메일부터 판정하고 표시 · 옮기기만 한다(전달 · 지우기 없음).
한 요청은 25초 안에서 하고 cursor 로 이어 하며, 같은 직원은 한 번에 하나다(진행 중이면 429 REQ_001).
- 판정(mail-filter-match.ts)은 엔진 Sieve(sieve-script.ts 의 FIELD_TESTS)를 그대로 옮겼다. 견줄 글은
원래 머리글(mime-text.ts 의 decodeHeaderText)과 텍스트 · HTML · 텍스트 첨부(html/html-text.ts)에서 뽑는다.
실엔진 배달 94통(HTML 75 · 머리글 19)을 표 테스트로 고정했다 — HTML5 에만 있는 이름 참조는 알려진 한계
- 이어 할 자리(cursor 2판)는 위 경계 · 남긴 마지막 메일과 그 받은 시각이다. 시작한 뒤 온 메일은 보지 않고,
그 메일이 빠지면 그 초부터 다시 읽는다 — 리뷰가 가짜 엔진으로 재현한 건너뜀을 고쳤다
- 옮기기는 읽은 상태 그대로일 때만(ifInState), 본문 값은 부분마다 256KiB · 한 통 1MiB 까지
- 오래 걸리는 처리의 시간 규칙(BulkRun)을 jmap/bulk-run.ts 로 옮겨 메일함 처리와 함께 쓴다
- 부재중 GET · PUT 응답에 isActive — 엔진에서 우리 Sieve 스크립트가 꺼져 있으면 false(본문으로는 받지 않는다)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
엔진(Stalwart 0.16.27)은 계정마다 1시간 동안 올린 양을 50MB 까지만 받는다(x:Jmap 의 uploadQuota).
20MB 첨부 셋이면 그 직원의 첨부 올리기와 필터 · 부재중 저장(Sieve 스크립트도 업로드로 올린다)이
한 시간까지 429 REQ_001 로 막혔다(2026-10-29 개발 엔진에서 확인).
- setup 이 uploadQuota 를 첨부 합계 한도(MAIL_ATTACHMENT_TOTAL_MAX_SIZE_MB — 백엔드와 같은 계산)의
20배 이상으로 올린다. 더 크게 둔 값은 내리지 않고, 개수 · 기간 · 한 번 크기는 그대로다
- 첨부 한도 env 를 바꾸면 setup 도 다시 돌린다(env 예시 · mail/README.md · docker 스킬 · env 규칙)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>