generated from ttipo/project-base
fix(docker): mail setup — SMTP 받는 메일 크기 한도 50MiB · 리로드 뒤 EHLO 로 확인 · IMAP/메일 크기 한도는 [주의] 만
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Failing after 8m39s
CI / Frontend (lint · type-check · test · build) (pull_request) Failing after 8m5s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 1m56s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m32s
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Failing after 8m39s
CI / Frontend (lint · type-check · test · build) (pull_request) Failing after 8m5s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 1m56s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m32s
개발 엔진 실측(2026-11-03, 0.16.27): 큰 메일 한 통을 받으면 엔진 메모리가 그 크기의 5.5배쯤 는다(IMAP APPEND · SMTP 받기 같다). SMTP 받는 한도(x:MtaStageData 의 maxMessageSize, 기본 100MiB)는 x:Email 한도와 따로라, 기본값 · 1g 에서 바깥 104MB 한 통이 984/1024MiB 였다(운영은 106.4MB IMAP APPEND 로 OOM). 사용자 결정 — 메일 컨테이너 메모리는 1g 그대로 두고, setup 이 받는 한도를 50MiB 로 맞춘 뒤 리로드 뒤 EHLO 의 SIZE 로 실제로 걸렸는지 본다(저장 때 줄만으로는 리로드 실패를 못 거른다 — 리뷰). IMAP · 메일 크기 한도는 imapsync 이전 동안 운영자가 잠시 올리므로 setup 이 바꾸지 않고 기본보다 크면 [주의] 만 낸다. README 에 잰 값 · 정책 · 이전 뒤 되돌리는 차례(세 값 → setup [확인] → 1g)와 문제 해결 줄, docker 스킬에 한 항목. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+51
-4
@@ -138,6 +138,9 @@ node --env-file=.env.development mail/scripts/setup.mjs
|
||||
로그인한 직원의 발송(웹메일)에는 걸지 않고 바깥에서 들어오는 메일에는 그대로. 설명·key 로 찾고, 없으면 만들지 않고 `[주의]` ([엔진 한도](#엔진-한도)).
|
||||
계정마다 임시 업로드 크기 합계(`x:Jmap` 의 `uploadQuota` — 1시간 동안 올린 양, 기본 50,000,000바이트)를 첨부 합계 한도의 20배(기본 524,288,000바이트 = 25MB × 20) 이상으로 올린다 —
|
||||
더 크게 둔 값은 내리지 않고, 개수 · 기간 · 한 번 크기는 그대로 ([엔진 한도](#엔진-한도) "임시 업로드 한도").
|
||||
SMTP 로 받는 메일 한 통의 크기(`x:MtaStageData` 의 `maxMessageSize`, 기본 100MiB)를 50MiB(52,428,800바이트)로 낮춘다 — 큰 메일 한 통에 엔진 메모리가 그 크기의 5.5배쯤 들어 1g 로는 100MB 한 통이 한도에 닿는다.
|
||||
더 작게 둔 값은 올리지 않고, 갈래가 있거나 else 가 숫자 하나가 아닌 식 · 0 이면 바꾸지 않고 `[주의]`. 리로드 뒤 메일 컨테이너 안에서 EHLO 의 `SIZE` 로 걸렸는지 본다(`[확인] 받는 메일 크기 한도 — EHLO SIZE …`).
|
||||
IMAP 요청 · 메일 크기 한도(`x:Imap` · `x:Email`)는 이전 때 잠시 올릴 수 있어 바꾸지 않고, 엔진 기본보다 크면 `[주의]` 만 낸다 ([엔진 한도](#엔진-한도) "받는 메일 크기").
|
||||
**엔진 DNS · Spamhaus** — 엔진의 DNS(`x:DnsResolver`)를 메일 전용 리졸버로(운영은 리졸버에 바로 `Custom`, 개발은 도커 DNS 를 거쳐 `System` — Mailpit 이름 때문),
|
||||
스팸 필터의 Spamhaus 두 항목은 키(`MAIL_SPAMHAUS_DQS_KEY`)가 있고 시험을 통과하면 DQS 로 켜고 아니면 끈다(공개 미러는 쓰지 않는다 — [DNS 리졸버와 스팸 차단 목록](#dns-리졸버와-스팸-차단-목록-dnsbl)).
|
||||
**엔진 웹훅**(12단계) — 인증 이벤트 셋을 백엔드의 받는 곳(`http://backend:<BACKEND_PORT>/api/internal/mail-engine-events`)으로 `MAIL_WEBHOOK_SECRET` 로 서명해 보내는
|
||||
@@ -192,7 +195,7 @@ node --env-file=.env.development mail/scripts/setup.mjs
|
||||
| `MAIL_DOMAIN` | `arasystem.test` / 운영 `arasystem.it.kr` |
|
||||
| `MAIL_GATEWAY_USER` · `MAIL_GATEWAY_PASSWORD` | 게이트웨이 주소 · 비밀번호 |
|
||||
| `MAIL_ATTACHMENT_MAX_SIZE_MB` | 첨부 1개 최대 크기 (기본 20). 프론트 nginx 본문 한도는 이 값 + 1MB 로 자동, 외부 프록시는 직접 — [docker 스킬](../.claude/skills/docker/SKILL.md) "운영 환경" |
|
||||
| `MAIL_ATTACHMENT_TOTAL_MAX_SIZE_MB` | 메일 한 통의 첨부 합계 (기본 25, 파일당 한도 이상). setup 이 엔진의 임시 업로드 크기 합계를 이 값의 20배 이상으로 맞추므로 첨부 한도를 바꾸면 setup 도 다시 실행한다 ([엔진 한도](#엔진-한도)) |
|
||||
| `MAIL_ATTACHMENT_TOTAL_MAX_SIZE_MB` | 메일 한 통의 첨부 합계 (기본 25, 파일당 한도 이상). setup 이 엔진의 임시 업로드 크기 합계를 이 값의 20배 이상으로 맞추므로 첨부 한도를 바꾸면 setup 도 다시 실행한다 ([엔진 한도](#엔진-한도)). 30 넘게 올리려면 받는 메일 크기 한도(50MiB — 같은 절 "받는 메일 크기")도 본다 |
|
||||
| `MAIL_DIRECTORY_LOCAL_PART` | 사내 목록 계정 아이디 (기본 `mail-directory`) — setup 과 같은 값을 읽는다 ([사내 목록 계정](#사내-목록-계정)) |
|
||||
| `MAIL_WEBHOOK_SECRET` | 엔진 웹훅의 서명 키(12단계 — 영문 · 숫자 32자 이상, 운영 필수) — setup 이 같은 값으로 엔진 웹훅을 만든다. 바꾸면 setup 과 backend 를 함께 ([엔진 웹훅](#엔진-웹훅--메일-프로그램-로그인-기록)) |
|
||||
| `APP_NETWORK_SUBNET` | 앱 네트워크 대역 (기본 `172.16.71.0/24`) — 웹훅 받는 쪽이 접속 주소(소켓)가 이 대역인지 본다 |
|
||||
@@ -629,6 +632,40 @@ sudo docker logs "${PROJECT_NAME}-mail" 2>&1 | grep 'cannot validate DNSSEC'
|
||||
- 확인 (2026-10-29, 0.16.27): setup 을 이어서 두 번 돌리면 첫 번째는 `[변경] … 524,288,000바이트 … 전 50,000,000`, 두 번째는 `[유지]`.
|
||||
setup 뒤에는 이미 60MiB 를 올린 계정이 20MiB 를 더 올릴 수 있었다(바꾸기 전에는 429). `maxUploadCount` · `uploadTtl` · `maxUploadSize` 는 그대로였다.
|
||||
|
||||
**받는 메일 크기** (`x:MtaStageData` · `x:Imap` · `x:Email` 싱글톤) — 큰 메일 한 통을 받으면 엔진 메모리가 그 크기의 5.5배쯤 는다.
|
||||
그래서 바깥에서 받는 한 통의 크기를 메일 컨테이너의 메모리 한도(1g — [메모리 예산](#메모리-예산))에 맞춰 둔다.
|
||||
|
||||
| 설정 | 걸리는 곳 | 기본값 | setup |
|
||||
| --- | --- | --- | --- |
|
||||
| `x:MtaStageData` 의 `maxMessageSize` | SMTP 로 받는 메일 한 통(25 · 465 — EHLO 의 `SIZE`. 465 는 시험하지 않았다) | `{"match":{},"else":"104857600"}` (100MiB) | 50MiB(52,428,800)보다 크면 50MiB 로 낮춘다. 더 작게 둔 값은 올리지 않고, 갈래가 있거나 else 가 숫자 하나가 아닌 식 · 0 이면 바꾸지 않고 `[주의]`. 리로드 뒤 EHLO 의 `SIZE` 로 확인한다(`[확인]`) |
|
||||
| `x:Imap` 의 `maxRequestSize` | IMAP 요청 하나(APPEND 의 리터럴) | 52,428,800 (50MiB) | 바꾸지 않는다 — 기본보다 크면 `[주의]` |
|
||||
| `x:Email` 의 `maxMessageSize` · `maxAttachmentSize` | JMAP · IMAP 으로 넣는 메일 · 첨부(엔진 설명 기준 — 따로 시험하지 않았다). SMTP 배달에는 걸리지 않는다 | 75,000,000 · 50,000,000 | 바꾸지 않는다 — 기본보다 크면 `[주의]` |
|
||||
|
||||
- **넘으면** — SMTP 는 `552 5.3.4 Message too big for system.` 이다(보낸 쪽이 `SIZE` 를 알리면 MAIL FROM 에서, 알리지 않아도 넘는 순간에 — 받아 두지 않는다). 바깥 보낸 사람은 반송을 받는다.
|
||||
IMAP 은 리터럴을 받기 전에 `BAD [PARSE] Literal exceeds the maximum request size of 52428800 bytes.` 다. 웹메일은 큰 파일을 [대용량 첨부](#대용량-첨부-11단계--인프라)(링크)로 보내므로 걸리지 않는다.
|
||||
- **IMAP · 메일 크기 한도를 바꾸지 않는 까닭** — imapsync 로 옮기는 동안 큰 메일 때문에 잠시 올린다([큰 메일](#큰-메일-운영-2026-11-03)). 그 도중에 setup 을 돌려도 이전이 깨지지 않게 알리기만 한다 — 이전을 마치면 관리 화면에서 기본으로 되돌린다.
|
||||
- 잰 값 (2026-11-03 개발 엔진 0.16.27 — 메일 컨테이너 cgroup 의 anon 을 0.1초마다, 무작위 첨부 하나인 메일, 스왑 없음):
|
||||
|
||||
| 넣은 길 | 25MB | 50MB | 75MB | 100MB 남짓 | 115MB |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| IMAP APPEND | +115MiB | +256MiB | +396MiB | +540MiB (100MB) | +614MiB |
|
||||
| SMTP 받기 (인증 없음 · 스팸 필터) | +100MiB | +224~270MiB | +409MiB | +557MiB (104MB) | — |
|
||||
|
||||
- 큰 메일을 넣은 뒤 엔진의 평소 사용량(기준선)이 127MiB 에서 408MiB 까지 쌓였고 몇 분 안에는 내려오지 않았다 — 가장 클 때 = 그때의 기준선 + 위 값.
|
||||
- 기본값 그대로 · 1g 에서 바깥 104MB 한 통이 **984 / 1024MiB** 였다(겨우 버팀). 운영은 1g 에서 106.4MB IMAP APPEND 로 OOM 이 났다([큰 메일](#큰-메일-운영-2026-11-03)).
|
||||
- 받는 한도 50MiB 에서는 50MB 한 통이 가장 클 때 581MiB, 60MB 는 거절되고 메모리가 그대로였다(`SIZE` 없이 보내도 +31MiB).
|
||||
- `x:Email` 의 `maxMessageSize`(75MB)는 SMTP 배달에 걸리지 않았다 — 104MB 가 그대로 배달됐다.
|
||||
- 확인 (2026-11-03, 0.16.27 개발): setup 을 이어서 두 번 돌리면 첫 번째는 `[변경] 받는 메일 크기 한도(SMTP — x:MtaStageData 의 maxMessageSize) 52,428,800바이트 (50MiB) · 전 104,857,600`,
|
||||
두 번째는 `[유지]` 이고, 둘 다 `[확인] IMAP 요청 · 메일 크기 한도 — 엔진 기본 이하 (…)` 가 이어진다.
|
||||
리로드 뒤의 확인은 두 번 다 `[확인] 받는 메일 크기 한도 — EHLO SIZE 52,428,800 (메일 컨테이너 <앱 네트워크 주소>:25 · 리로드 뒤 엔진이 알리는 값)` 이다.
|
||||
setup 이 맞췄는데 엔진이 104,857,600 을 알리면 다섯 번 다시 본 뒤 종료 1, SIZE 줄이 없으면 `[주의]` 다(가짜 클라이언트로 본 갈래 — 실제 엔진에서 리로드 실패를 만들지는 않았다).
|
||||
`x:Imap` 의 `maxRequestSize` 를 115,343,360 으로 올려 두고 돌리면 `[주의] IMAP 요청 · 메일 크기 한도가 엔진 기본보다 큽니다 — …` 이고 값은 그대로 남았다.
|
||||
- **확인하지 않은 것**
|
||||
- 메일 프로그램이 465 로 보내는 메일도 같은 식이라 50MiB 를 넘으면 552 일 것이다.
|
||||
- 웹메일 발송(JMAP 제출)은 엔진 안의 SMTP 세션이라 같은 한도에 걸릴 수 있다. 메일 한 통 ≈ 첨부 합계 × 1.37(base64 · 줄바꿈) + 본문 4MB(인코딩까지 5.5MiB 남짓) + 서명 그림이라
|
||||
첨부 합계 기본 25MB 면 41MiB 남짓으로 아래지만 32MB 남짓부터 넘는다 — `MAIL_ATTACHMENT_TOTAL_MAX_SIZE_MB` 를 30 넘게 올리려면 이 한도와 [메모리 예산](#메모리-예산)을 함께 본다.
|
||||
- 큰 메일 두 통 이상을 함께 받는 경우는 재지 않았다 — 쌓인 기준선 408MiB 에 50MB 두 통(270MiB 씩)이면 950MiB 남짓이다. 메일 프로그램은 465 제출과 보낸편지함 APPEND 를 거의 함께 한다.
|
||||
|
||||
운영 전에 정할 것:
|
||||
|
||||
- **메일을 많이 받는 주소** — 같은 바깥 도메인에서 한 사람이 시간당 25통 넘게 받으면 그 뒤는 바깥 서버가 미뤘다가 다시 보낸다(늦게 받는다).
|
||||
@@ -787,7 +824,7 @@ sudo docker exec "${PROJECT_NAME}-backend" node -e "const s=require('net').conne
|
||||
| --- | --- | --- | --- |
|
||||
| clamav (clamd) | 2g | 0.95GB — 서명 363만 개를 읽은 뒤 | 서명을 다시 읽을 때 1.0GB(두 벌을 들지 않는다), 2GB 검사 중 1.0~1.2GB + 받은 파일의 페이지 캐시(한도 안에서 줄여 쓴다) |
|
||||
| clamav-updater (freshclam) | 1g | 수십 MB | 처음 기동(서명 전체를 받아 읽어 봄) 0.7GB, 평소 갱신은 daily 만 |
|
||||
| mail | 1g | 0.1~0.3GB | 100MB 남짓 메일 한 통을 넣을 때 1g 를 넘었다(2026-11-03 — 서버 B 는 이전하는 동안 2g, [큰 메일](#큰-메일-운영-2026-11-03)) |
|
||||
| mail | 1g | 0.1~0.3GB (큰 메일 뒤 0.4GB 남짓까지 쌓인다 — 2026-11-03) | 메일 한 통을 받을 때 그 크기 × 5.5 쯤 더 — 받는 한도 50MiB(setup)면 +0.3GB 남짓. 운영에서 106.4MB 한 통이 1g 를 넘었다(2026-11-03 — 개발 104MB 는 984/1024MiB · 서버 B 는 이전하는 동안만 2g, [큰 메일](#큰-메일-운영-2026-11-03) · [받는 메일 크기](#엔진-한도)) |
|
||||
| backend | 1g | 0.3~0.5GB | 조각 · 받기는 스트림이라 파일 크기와 상관없다 |
|
||||
| db · frontend · mail-resolver | 512m · 512m · 128m | 작다 | |
|
||||
| **합** | **6.15GB** | 2.5~3GB | 나머지 1.8GB 남짓을 커널 · dockerd · 페이지 캐시가 쓴다 |
|
||||
@@ -1674,12 +1711,18 @@ sudo rm -f /root/imapsync/mailcow-master.pass /root/imapsync/migration.pass
|
||||
|
||||
| imapsync 출력 (종료 코드) | 까닭 | 한 일 |
|
||||
| --- | --- | --- |
|
||||
| 그 메일에서 `literal exceeds …`, 끝에 `Detected N errors` (114 `EXIT_ERR_APPEND`) | 엔진 한도(IMAP 요청 · 메일 · 첨부 크기)보다 크다 — 그 메일만 빠지고 나머지는 옮긴다 | 관리 화면에서 세 값을 110 MiB(`115343360`)로 — Email 의 E-mail Size · Attachment Size, IMAP 의 `maxRequestSize`. setup 은 이 값을 맞추지 않는다(올린 값이 그대로 남는다) |
|
||||
| 그 메일에서 `literal exceeds …`, 끝에 `Detected N errors` (114 `EXIT_ERR_APPEND`) | 엔진 한도(IMAP 요청 · 메일 · 첨부 크기)보다 크다 — 그 메일만 빠지고 나머지는 옮긴다 | 관리 화면에서 세 값을 110 MiB(`115343360`)로 — Email 의 E-mail Size · Attachment Size, IMAP 의 `maxRequestSize`. setup 은 이 셋을 바꾸지 않고 기본보다 크면 `[주의]` 만 낸다 — 이전을 마치면 되돌린다(아래) |
|
||||
| 100MB 남짓 한 통에서 `socket closed`, 그 뒤 몇 사람이 `Connection refused` (`EXIT_CONNECTION_FAILURE_HOST2`) | 엔진이 1g 를 넘어 커널에 죽고(OOM — `dmesg` 에 stalwart) 다시 뜨는 동안이다 | `sudo docker update --memory 2g --memory-swap 2g <PROJECT_NAME>-mail` 뒤 그 사람들만 다시 — 모두 통과 |
|
||||
|
||||
- `docker update` 는 재시작 · 멈춤에도 남지만 **컨테이너를 다시 만들면 compose 의 1g 로 돌아간다** — 다시 만들었으면 다시 건다. 확인 `sudo docker inspect -f '{{.HostConfig.Memory}} {{.RestartCount}}' <PROJECT_NAME>-mail`(2147483648 이면 2g).
|
||||
- 110 MiB 보다 큰 메일은 옮기지 못한다 — 운영에서는 보내지 못한 초안 두 통(244 · 135 MiB)이었고, 사용자가 따로 보관하기로 했다(그 사람만 끝까지 `Detected 2 errors` · 114).
|
||||
- 이전이 끝난 뒤 한도 · 메모리를 어떻게 둘지는 아직 정하지 않았다 — 한도를 올린 채 두면 엔진에 2g 가 들고, [메모리 예산](#메모리-예산)의 합이 7.15GB(백업이 도는 동안 8.15GB)가 된다.
|
||||
- **이전을 마치면 되돌린다**(2026-11-03 사용자 결정) — 「이전 마치기」 뒤 차례대로:
|
||||
1. 관리 화면에서 세 값을 기본으로 — E-mail Size `75000000` · Attachment Size `50000000` · IMAP `maxRequestSize` `52428800`.
|
||||
2. setup 을 돌려 `[완료]`(종료 0)로 끝나고 `[확인] 받는 메일 크기 한도 — EHLO SIZE 52,428,800` 줄이 있는지, 크기 한도 `[주의]` 가 없는지 본다 —
|
||||
`[변경]` · `[유지]` 줄은 저장한 값이고, 엔진에 걸린 값은 리로드 뒤의 이 `[확인]` 줄이다(리로드가 실패하면 엔진은 옛 한도로 받는다).
|
||||
3. 메모리를 compose 의 값으로 — `sudo docker update --memory 1g --memory-swap 2g <PROJECT_NAME>-mail`.
|
||||
|
||||
받는 한도를 낮추기 전에 1g 로 돌리면 바깥 큰 메일 한 통에 엔진이 죽을 수 있다([엔진 한도](#엔진-한도) "받는 메일 크기"). 올린 채 두면 엔진에 2g 가 들고, [메모리 예산](#메모리-예산)의 합이 7.15GB, 백업이 도는 동안은 8.15GB 로 8GB 를 넘는다.
|
||||
|
||||
#### 개발에서 확인한 것 (imapsync — 2026-11-02)
|
||||
|
||||
@@ -2250,6 +2293,10 @@ sudo docker logs --tail 3 "${PROJECT_NAME}-frontend"
|
||||
| 웹메일 발송의 받는 사람이 `452 4.4.5 Rate limit exceeded` 로 거절된다 | setup 출력에 `받는 쪽 한도 … 인증 없는 세션에만` 이 있는지 본다. 받는 쪽 한도를 새로 만들었다면 그 `match` 도 — 엔진 로그 `smtp.rate-limit-exceeded` 의 `id` 가 어느 한도인지 알려 준다 ([엔진 한도](#엔진-한도)) |
|
||||
| 한 직원의 첨부 올리기 · 필터 · 부재중 저장이 429 `REQ_001` 로 막힌다 (백엔드 로그 `"reason":"mail-engine-rate-limit","call":"upload"`) | 엔진의 임시 업로드 한도(계정마다 1시간 — 크기 합계 · 개수)를 넘었다. 올린 것이 만료되면(최대 1시간) 풀린다. setup 출력에 `임시 업로드 한도 — 계정마다 크기 합계 …` 가 있는지 보고, 개수를 넘는 직원이 잦으면 `maxUploadCount` 를 올린다 ([엔진 한도](#엔진-한도) "임시 업로드 한도") |
|
||||
| `임시 업로드 한도(x:Jmap 의 uploadQuota = …)를 읽지 못해 바꾸지 않았습니다` · `[주의] 임시 업로드 한도(x:Jmap 의 uploadQuota)가 0 입니다` | 엔진이 숫자가 아닌 값(판을 올려 속성이 바뀌었는지 본다)이나 0 을 돌려줬다 — 관리 API `x:Jmap/get` 으로 `uploadQuota` 를 보고 첨부 합계 한도 × 20 이상으로 둔 뒤 `ReloadSettings` (0 이 제한 없음인지는 확인하지 않았다) |
|
||||
| `[주의] IMAP 요청 · 메일 크기 한도가 엔진 기본보다 큽니다 — …` | imapsync 이전 때 올린 값이 남았다 — 이전을 마쳤으면 관리 화면에서 기본(`maxRequestSize` 52,428,800 · E-mail Size 75,000,000 · Attachment Size 50,000,000)으로 되돌린다([큰 메일](#큰-메일-운영-2026-11-03)). 이전 중이면 그대로 둔다 — setup 은 바꾸지 않는다 |
|
||||
| `받는 메일 크기 한도(SMTP — x:MtaStageData 의 maxMessageSize)를 읽지 못해 맞추지 않았습니다` · `[주의] 받는 메일 크기 한도(…)가 운영자의 식이라 …` · `…가 0 입니다` | 엔진이 식이 아닌 꼴(판을 올려 속성이 바뀌었는지 본다) · 갈래가 있는 식 · 0 을 돌려줬다 — 관리 API `x:MtaStageData/get` 으로 `maxMessageSize` 를 보고 갈래와 else 를 모두 1 이상 52,428,800 이하로 둔 뒤 `ReloadSettings`(EHLO 의 `SIZE` 로 확인). setup 은 갈래가 없는 숫자 식만 고친다 |
|
||||
| `받는 메일 크기 한도가 엔진에 걸리지 않았습니다 — 리로드 뒤 EHLO SIZE … 가 52,428,800바이트 (50MiB) 보다 크다` (종료 1) | 저장은 했는데 리로드가 걸리지 않았다 — 같은 실행의 리로드 `[실패]`(바깥 DNS 가 안 되면 엔진이 리로드를 통째로 거절한다 — [리졸버 장애 때](#리졸버-장애-때)) · `[주의]` 를 먼저 고치고 setup 을 다시 돌린다. 걸리기 전에는 엔진이 옛 한도로 받으니 메일 메모리를 1g 로 내리지 않는다([큰 메일](#큰-메일-운영-2026-11-03)) |
|
||||
| `[주의] 받는 메일 크기 한도가 엔진에 걸렸는지 확인하지 못했습니다 — …` | 메일 컨테이너 안의 curl 로 25 에 EHLO 를 보내지 못했거나, 응답에 SIZE 가 없거나 숫자가 아니다 — 관리 API `x:MtaStageData/get` 으로 값을 보고 리로드한 뒤 다시 실행한다. `[확인]` 줄을 보기 전에는 메모리를 1g 로 내리지 않는다 |
|
||||
| `[주의] 사내 목록 계정이 없습니다 (…)` | 복구 관리자 없이 돌렸다 — 실패가 아니다. 운영: "운영 배포" 1)·`up -d mail` → 3) `--recovery` → 4)·5) 를 한 번. 개발: `MAIL_RECOVERY_ADMIN` 을 채우고 `up -d mail` 후 setup. 그동안 백엔드는 로그인한 직원 기록으로 사내 목록을 대신한다 ([사내 목록 계정](#사내-목록-계정)) |
|
||||
| `[주의] 사내 목록 계정이 setup 기준과 다릅니다 (역할의 권한이 다름 … · 계정의 역할이 다름)` | 누가 역할·계정을 고쳤다. 관리자로는 되돌릴 수 없다(엔진이 거절) — 위와 같이 복구 관리자로 setup. 이어서 나오는 `[확인]`·`[주의]` 줄이 지금 백엔드가 읽을 수 있는지 알려 준다 |
|
||||
| `…(MAIL_DIRECTORY_LOCAL_PART)는 setup 이 만들지 않은 계정의 주소라 …` | 그 주소를 실제 계정이 쓴다(그 계정은 건드리지 않았다). `MAIL_DIRECTORY_LOCAL_PART` 를 아무도 쓰지 않는 아이디로 바꾸고 `up -d backend` → setup 재실행 |
|
||||
|
||||
+201
-2
@@ -51,6 +51,9 @@
|
||||
* — 로그인한 직원의 발송(웹메일)에는 걸지 않고, 바깥에서 들어오는 메일에는 그대로 (mail/README.md "엔진 한도")
|
||||
* 계정마다 임시 업로드 크기 합계(1시간 동안 올린 양)를 첨부 합계 한도의 20배 이상으로 올린다 (설정 리로드 뒤 반영)
|
||||
* — 더 크게 둔 값은 내리지 않고, 개수 · 기간 · 한 번 크기는 그대로
|
||||
* SMTP 로 받는 메일 한 통의 크기(x:MtaStageData 의 maxMessageSize)를 50MiB 이하로 낮춘다 (설정 리로드 뒤 반영 · SMTP_MESSAGE_SIZE_LIMIT 머리말)
|
||||
* — 더 작게 둔 값은 올리지 않고, 갈래가 있거나 else 가 숫자 하나가 아닌 식 · 0 이면 [주의]. 리로드 뒤 메일 컨테이너 안에서 EHLO 의 SIZE 로
|
||||
* 걸렸는지 본다. IMAP 요청 · 메일 크기 한도(x:Imap · x:Email)는 바꾸지 않고 엔진 기본보다 크면 [주의] 만
|
||||
* 휴지통 · 스팸메일함 자동 비우기 30일(들어온 때부터)과 엔진 정리 시각 — 계정 정리 KST 03:00 · 데이터 04:00 · 원문 04:30
|
||||
* (13단계 — 백업 03:30 의 앞뒤, 설정 리로드 뒤 반영 · TRASH_RETENTION_MS 머리말)
|
||||
* 엔진 DNS(x:DnsResolver)를 메일 전용 리졸버로 — 운영은 리졸버에 바로(Custom), 개발은 도커 DNS 를 거쳐(System — Mailpit 이름 때문).
|
||||
@@ -206,6 +209,30 @@ const DEFAULT_ATTACHMENT_MAX_SIZE_MB = 20;
|
||||
const DEFAULT_ATTACHMENT_TOTAL_MAX_SIZE_MB = 25;
|
||||
const MEGABYTE = 1024 * 1024;
|
||||
const POSITIVE_INT_PATTERN = /^[1-9]\d*$/;
|
||||
/**
|
||||
* SMTP 로 받는 메일 한 통의 상한 — x:MtaStageData 싱글톤의 maxMessageSize(25 · 465 — 465 는 시험하지 않았다 · EHLO 의 SIZE 로 알린다 ·
|
||||
* 엔진 기본 {"match":{},"else":"104857600"} = 100MiB)를 이 값 이하로 둔다 (mail/README.md "엔진 한도" 「받는 메일 크기」 · ensureMessageSizeLimits ·
|
||||
* 리로드 뒤 확인 checkMessageSizeLimit). 2026-11-03 개발 엔진(0.16.27)에서 잰 것:
|
||||
* - 큰 메일 한 통을 받으면 엔진 메모리(cgroup anon)가 그 크기의 5.5배쯤 는다 — IMAP APPEND 와 SMTP 받기가 같다(100MB IMAP +540MiB · 104MB SMTP +557MiB · 50MB SMTP +224~270MiB).
|
||||
* 받은 뒤 기준선이 127MiB 에서 408MiB 까지 쌓이고 몇 분 안에는 내려오지 않았다. 기본값 · 메모리 1g(스왑 없음)에서 바깥 104MB 한 통이 984/1024MiB 였고,
|
||||
* 운영은 1g 에서 106.4MB IMAP APPEND 로 OOM 이 났다 — 메일 컨테이너 메모리 1g 는 그대로 두고 받는 한 통을 줄인다(2026-11-03 사용자 결정).
|
||||
* - 넘는 메일은 552 5.3.4 "Message too big for system." — 보낸 쪽이 SIZE 를 알리면 MAIL FROM 에서, 알리지 않으면 넘는 순간에(받아 두지 않고) 거절한다.
|
||||
* 값은 저장한 뒤 ReloadSettings 로 걸린다(EHLO 의 SIZE 가 52428800 으로 바뀐다).
|
||||
* - x:Email 의 maxMessageSize · maxAttachmentSize 와 x:Imap 의 maxRequestSize 는 SMTP 배달에 걸리지 않는다(Email.maxMessageSize 75MB 에서 104MB SMTP 메일이 배달됐다) —
|
||||
* SMTP 로 받는 크기는 이 값 하나로만 막는다
|
||||
*/
|
||||
const SMTP_MESSAGE_SIZE_LIMIT = 50 * MEGABYTE;
|
||||
/**
|
||||
* IMAP 요청 · 메일 · 첨부 크기의 엔진 기본값 (0.16.27 — 2026-11-03 개발 엔진에서 관리 API 로 읽은 값). setup 은 이 셋을 바꾸지 않는다 —
|
||||
* imapsync 이전 동안 운영자가 큰 메일 때문에 잠시 올리므로(mail/README.md "큰 메일") 이전 도중에 setup 을 돌려도 이전이 깨지지 않게.
|
||||
* 기본보다 크면 [주의] 만 낸다 — 큰 메일 한 통에 엔진 메모리가 그 크기의 5.5배쯤 든다(SMTP_MESSAGE_SIZE_LIMIT 머리말).
|
||||
* IMAP 은 maxRequestSize 를 넘는 APPEND 를 리터럴을 받기 전에 BAD [PARSE] "Literal exceeds the maximum request size of …" 로 거절한다.
|
||||
* 0.16.27 의 값이다 — 엔진 판을 올리면 다시 읽어 맞춘다
|
||||
*/
|
||||
const ENGINE_DEFAULT_SIZE_LIMITS = {
|
||||
Imap: { maxRequestSize: 52_428_800 },
|
||||
Email: { maxMessageSize: 75_000_000, maxAttachmentSize: 50_000_000 },
|
||||
};
|
||||
/**
|
||||
* 계정 아이디(주소의 @ 앞) 형식 — 영문·숫자·. _ -, 앞뒤는 영문·숫자, 64자 이하, 대소문자 무시 (소문자로 맞춰 검사).
|
||||
* ⚠️ 백엔드 backend/src/modules/auth/employee-address.ts 의 LOCAL_PART_PATTERN(isLocalPart)과 같은 규칙이다 —
|
||||
@@ -3143,6 +3170,95 @@ async function ensureUploadQuota(client, cfg) {
|
||||
log(`[변경] ${label} ${formatCount(cfg.uploadQuotaFloor)}바이트 (${floor}) · 전 ${formatCount(quota)} · ${untouched}`);
|
||||
}
|
||||
|
||||
/** x:<Type> 싱글톤의 칸들 — 읽지 못하면(메서드 오류 · 요청 실패 · 싱글톤 없음) 멈추지 않고 { error: 까닭 } */
|
||||
async function readSingleton(client, type, properties) {
|
||||
try {
|
||||
const item = (await call(client, `x:${type}/get`, { ids: ['singleton'], properties })).list?.[0];
|
||||
return item ? { value: item } : { error: `x:${type}/get 에 singleton 이 없다` };
|
||||
} catch (error) {
|
||||
if (!(error instanceof SetupError)) throw error;
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 받는 메일 크기 한도 (SMTP_MESSAGE_SIZE_LIMIT · ENGINE_DEFAULT_SIZE_LIMITS 머리말 — mail/README.md "엔진 한도" 「받는 메일 크기」).
|
||||
* 1. SMTP 로 받는 한 통(x:MtaStageData 의 maxMessageSize)을 SMTP_MESSAGE_SIZE_LIMIT 이하로 — 저장만 한다(main 의 리로드로 반영).
|
||||
* 더 작게 둔 값은 올리지 않는다. 0 은 뜻(제한 없음인지)을 확인하지 않았고, 갈래가 있거나 else 가 숫자 하나가 아닌 식은 운영자의 것이라 바꾸지 않고 [주의].
|
||||
* 2. IMAP 요청 · 메일 · 첨부 크기(x:Imap · x:Email)는 읽기만 한다 — 엔진 기본보다 크면 [주의], 읽지 못해도 멈추지 않는다.
|
||||
* 돌려주는 값: 1 의 상태 — 리로드 뒤 확인(checkMessageSizeLimit)에 넘긴다. 'saved' 이번에 낮춰 저장 · 'kept' 단순한 꼴(갈래 없음 · 숫자 else)로 이미 한도 이하 ·
|
||||
* 'operator' 운영자의 식 · 'zero' 0 · 'unread' 읽지 못함(problems)
|
||||
* ⚠️ 출력의 "받는 메일 크기 한도" 는 운영 차례 문서(mail/README.md "큰 메일")가 이 글자로 확인한다 — 바꾸지 않는다
|
||||
*/
|
||||
async function ensureMessageSizeLimits(client) {
|
||||
const label = '받는 메일 크기 한도(SMTP — x:MtaStageData 의 maxMessageSize)';
|
||||
const mib = `${SMTP_MESSAGE_SIZE_LIMIT / MEGABYTE}MiB`;
|
||||
const limit = `${formatCount(SMTP_MESSAGE_SIZE_LIMIT)}바이트 (${mib})`;
|
||||
const memory = '큰 메일 한 통에 엔진 메모리가 그 크기의 5.5배쯤 든다';
|
||||
const smtp = await readSingleton(client, 'MtaStageData', ['maxMessageSize']);
|
||||
const expression = smtp.value?.maxMessageSize;
|
||||
let state;
|
||||
if (smtp.error || !isExpression(expression)) {
|
||||
state = 'unread';
|
||||
problems.push(
|
||||
`${label}를 읽지 못해 맞추지 않았습니다 (${smtp.error ?? JSON.stringify(expression) ?? '칸 없음'}) — ${limit} 이하여야 합니다 (mail/README.md "엔진 한도").`,
|
||||
);
|
||||
} else if (branchesOf(expression).length > 0 || !/^\d+$/.test(String(expression.else))) {
|
||||
state = 'operator';
|
||||
warn(
|
||||
`${label}가 운영자의 식이라(갈래가 있거나 else 가 숫자 하나가 아니다) 바꾸지 않았습니다 (${JSON.stringify(expression)}) — ` +
|
||||
`모든 갈래와 else 가 ${limit} 이하인지 관리 화면에서 확인하세요. ${memory} (mail/README.md "엔진 한도").`,
|
||||
);
|
||||
} else {
|
||||
const size = Number(expression.else);
|
||||
if (size === 0) {
|
||||
state = 'zero';
|
||||
warn(`${label}가 0 입니다 — 0 의 뜻(제한 없음인지)을 확인하지 않아 바꾸지 않았습니다. 관리 화면에서 1 이상 ${limit} 이하로 두세요 — ${memory} (mail/README.md "엔진 한도").`);
|
||||
} else if (size > SMTP_MESSAGE_SIZE_LIMIT) {
|
||||
state = 'saved';
|
||||
await setObjects(client, 'MtaStageData', { update: { singleton: { maxMessageSize: { match: {}, else: String(SMTP_MESSAGE_SIZE_LIMIT) } } } });
|
||||
log(`[변경] ${label} ${limit} · 전 ${formatCount(size)}`);
|
||||
} else {
|
||||
state = 'kept';
|
||||
log(`[유지] ${label} ${formatCount(size)}바이트 (${mib} 이하)`);
|
||||
}
|
||||
}
|
||||
|
||||
// IMAP 요청 · 메일 · 첨부 크기 — 이전(imapsync) 동안 운영자가 올리므로 바꾸지 않는다. 0 · null 은 뜻을 확인하지 않은 값이라 "확인하지 못함"
|
||||
const scope = 'IMAP 요청 · 메일 크기 한도';
|
||||
const large = [];
|
||||
const unread = [];
|
||||
const within = [];
|
||||
for (const [type, defaults] of Object.entries(ENGINE_DEFAULT_SIZE_LIMITS)) {
|
||||
const read = await readSingleton(client, type, Object.keys(defaults));
|
||||
if (read.error) {
|
||||
unread.push(`x:${type} — ${read.error}`);
|
||||
continue;
|
||||
}
|
||||
for (const [field, engineDefault] of Object.entries(defaults)) {
|
||||
const value = read.value[field];
|
||||
const name = `x:${type} 의 ${field}`;
|
||||
if (!Number.isSafeInteger(value) || value <= 0) unread.push(`${name} = ${JSON.stringify(value) ?? '칸 없음'}`);
|
||||
else if (value > engineDefault) large.push(`${name} ${formatCount(value)}(기본 ${formatCount(engineDefault)})`);
|
||||
else within.push(`${name} ${formatCount(value)}`);
|
||||
}
|
||||
}
|
||||
if (large.length > 0) {
|
||||
warn(
|
||||
`${scope}가 엔진 기본보다 큽니다 — ${large.join(' · ')}. ${memory} — 메모리 1g 로는 100MB 남짓(운영 106.4MB) 한 통에서 죽을 수 있다. ` +
|
||||
'이전(imapsync)하는 동안 올렸다면 마친 뒤 관리 화면에서 기본으로 되돌린다 — setup 은 바꾸지 않는다 (mail/README.md "큰 메일").',
|
||||
);
|
||||
}
|
||||
if (unread.length > 0) {
|
||||
warn(
|
||||
`${scope}를 읽지 못했거나 뜻을 확인하지 않은 값(0 · null 등)이라 엔진 기본보다 큰지 확인하지 못했습니다 (${unread.join(' · ')}) — ` +
|
||||
'setup 은 바꾸지 않는다, 관리 화면에서 본다 (mail/README.md "엔진 한도").',
|
||||
);
|
||||
}
|
||||
if (large.length === 0 && unread.length === 0) log(`[확인] ${scope} — 엔진 기본 이하 (${within.join(' · ')})`);
|
||||
return state;
|
||||
}
|
||||
|
||||
/** 시 · 분 → "HH:MM" */
|
||||
function formatClock(hour, minute) {
|
||||
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
||||
@@ -3426,8 +3542,10 @@ async function checkHttpEndpointPolicy(cfg) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 컨테이너 안의 curl 로 RCPT 까지만 보낸다 — { mailFrom: MAIL FROM 응답 줄, rcpt: 받는 주소(소문자)마다 응답 줄 }. curl 을 부르지 못하면 null.
|
||||
* 메일 컨테이너 안의 curl 로 RCPT 까지만 보낸다 — { mailFrom: MAIL FROM 응답 줄, rcpt: 받는 주소(소문자)마다 응답 줄,
|
||||
* ehlo: { sent: EHLO 를 보냈는가, size: EHLO 응답의 SIZE 값 — 숫자 글 · 숫자가 없으면 '' · SIZE 줄이 없으면 null }, status: curl 종료 코드 }. curl 을 부르지 못하면 null.
|
||||
* MAIL FROM 이 거절되거나 받는 주소가 모두 거절되면 curl 은 DATA 전에 멈춘다(종료 코드 55) — 그래서 거절될 주소만 넣는다.
|
||||
* 받는 주소를 하나도 주지 않으면 curl 은 EHLO 뒤 HELP 만 하고 끝낸다(MAIL FROM · RCPT · DATA 없음 — curl 8.14.1, 2026-11-03 개발 엔진) — EHLO 만 볼 때 쓴다.
|
||||
* sender 가 '<>' 면 빈 봉투(MAIL FROM:<>)다 (RCPT_CHECK_SENDER).
|
||||
*/
|
||||
function smtpRepliesInsideMail(cfg, mailIp, sender, recipients) {
|
||||
@@ -3454,9 +3572,26 @@ function smtpRepliesInsideMail(cfg, mailIp, sender, recipients) {
|
||||
{ input: '', encoding: 'utf8' },
|
||||
);
|
||||
if (result.error) return null;
|
||||
const replies = { mailFrom: null, rcpt: new Map() };
|
||||
const replies = { mailFrom: null, rcpt: new Map(), ehlo: { sent: false, size: null }, status: result.status };
|
||||
let asked = null;
|
||||
let inEhlo = false;
|
||||
for (const line of (result.stderr ?? '').split(/\r?\n/)) {
|
||||
if (/^> EHLO /.test(line)) {
|
||||
replies.ehlo.sent = true;
|
||||
inEhlo = true;
|
||||
continue;
|
||||
}
|
||||
if (inEhlo) {
|
||||
// EHLO 응답은 여러 줄이다 — "250-…" 이 이어지고 "250 …" 이 끝이다 (SIZE 는 RFC 1870 — 숫자가 없을 수 있다)
|
||||
const extension = /^< 250([ -])(.*)$/.exec(line);
|
||||
if (extension) {
|
||||
const size = /^SIZE(?:\s+(\d+))?\s*$/i.exec(extension[2].trim());
|
||||
if (size) replies.ehlo.size = size[1] ?? '';
|
||||
if (extension[1] === ' ') inEhlo = false;
|
||||
continue;
|
||||
}
|
||||
if (/^[<>] /.test(line)) inEhlo = false;
|
||||
}
|
||||
if (/^> MAIL FROM:/.test(line)) {
|
||||
asked = { mailFrom: true };
|
||||
continue;
|
||||
@@ -3516,6 +3651,66 @@ async function checkSystemRecipientsRejected(cfg, addresses) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 받는 메일 크기 한도가 리로드 뒤 엔진에 실제로 걸렸는지 — 메일 컨테이너 안에서 자기 네트워크 주소의 25(인증 없는 세션 — 바깥 메일이 들어오는 길)로
|
||||
* EHLO 를 보내 SIZE 를 읽는다(smtpRepliesInsideMail 에 받는 주소 없이 — EHLO · HELP 뿐, 메일은 보내지 않는다). 루프백이 아니라 네트워크 주소인 까닭은
|
||||
* 시스템 계정 RCPT 확인과 같다. ensureMessageSizeLimits 의 [변경] · [유지] 는 저장한 값이라, 리로드가 걸리지 않았으면 엔진은 옛 한도(기본 100MiB)로 받는다 —
|
||||
* 운영 차례(mail/README.md "큰 메일")는 이 [확인] 줄을 보고 메일 컨테이너 메모리를 1g 로 되돌린다.
|
||||
* - SIZE 가 1 이상 SMTP_MESSAGE_SIZE_LIMIT 이하 → [확인]
|
||||
* - 더 큰데 setup 이 단순한 꼴로 저장 · 유지했다(state 'saved' · 'kept') → problems(종료 1). 운영자의 식 · 0 · 읽지 못함이면 [주의]
|
||||
* - SIZE 줄이 없거나 숫자가 아니거나 0, 확인 자체를 못 함 → [주의]
|
||||
* ⚠️ "[확인] 받는 메일 크기 한도 — EHLO SIZE" 는 운영 차례 문서가 이 글자로 확인한다 — 바꾸지 않는다
|
||||
*/
|
||||
async function checkMessageSizeLimit(cfg, state) {
|
||||
const label = '받는 메일 크기 한도';
|
||||
const limit = `${formatCount(SMTP_MESSAGE_SIZE_LIMIT)}바이트 (${SMTP_MESSAGE_SIZE_LIMIT / MEGABYTE}MiB)`;
|
||||
const ours = state === 'saved' || state === 'kept';
|
||||
const mailIp = mailContainerIp(cfg);
|
||||
if (!mailIp) {
|
||||
warn(`docker 를 쓸 수 없거나 mail 컨테이너 주소를 몰라 ${label}가 엔진에 걸렸는지(EHLO 의 SIZE) 확인하지 못했습니다.`);
|
||||
return;
|
||||
}
|
||||
const where = `메일 컨테이너 ${mailIp}:25`;
|
||||
const sizeOf = (replies) => (/^\d+$/.test(replies?.ehlo.size ?? '') ? Number(replies.ehlo.size) : null);
|
||||
const within = (size) => size !== null && size >= 1 && size <= SMTP_MESSAGE_SIZE_LIMIT;
|
||||
let replies = null;
|
||||
// setup 이 맞춘 값이면 리로드가 퍼질 때까지 몇 번 더 본다 — 운영자의 식 · 0 은 기다려도 바뀌지 않는다
|
||||
for (let attempt = 0; attempt < (ours ? SETTLE_ATTEMPTS : 1); attempt += 1) {
|
||||
if (attempt > 0) await sleep(POLL_INTERVAL_MS / 2);
|
||||
replies = smtpRepliesInsideMail(cfg, mailIp, RCPT_CHECK_SENDER, []);
|
||||
if (within(sizeOf(replies))) break;
|
||||
}
|
||||
const size = sizeOf(replies);
|
||||
if (within(size)) {
|
||||
const note = state === 'operator' ? ' · 운영자의 식이라 다른 세션(465 · 인증한 세션 등)은 다를 수 있다 — 위 [주의]' : '';
|
||||
log(`[확인] ${label} — EHLO SIZE ${formatCount(size)} (${where} · 리로드 뒤 엔진이 알리는 값${note})`);
|
||||
return;
|
||||
}
|
||||
if (size !== null && size > SMTP_MESSAGE_SIZE_LIMIT) {
|
||||
const head = `${label}가 엔진에 걸리지 않았습니다 — 리로드 뒤 EHLO SIZE ${formatCount(size)} (${where}) 가 ${limit} 보다 크다`;
|
||||
if (ours) {
|
||||
problems.push(
|
||||
`${head} — 엔진에 저장된 값은 ${limit} 이하다(setup 이 이번에 저장했거나 그대로 두었다). 리로드가 실패했거나 반영되지 않았으니 위 [실패] · [주의] 를 보고 ` +
|
||||
'setup 을 다시 돌린다 — 걸리기 전에는 엔진이 옛 한도로 받아 큰 메일 한 통에 메모리 1g 를 넘을 수 있다 (mail/README.md "큰 메일").',
|
||||
);
|
||||
} else {
|
||||
const untouched = {
|
||||
operator: '운영자의 식이라 setup 이 바꾸지 않았다 — 위 [주의]',
|
||||
zero: '0 이라 setup 이 바꾸지 않았다 — 위 [주의]',
|
||||
unread: 'setup 이 값을 읽지 못해 맞추지 않았다 — 끝의 [실패] 항목',
|
||||
};
|
||||
warn(`${head} — ${untouched[state] ?? 'setup 이 맞추지 않은 값이다'}. 관리 화면에서 ${limit} 이하로 두고 다시 실행하세요 (mail/README.md "엔진 한도").`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let why;
|
||||
if (!replies) why = 'docker exec 로 curl 을 부르지 못했다';
|
||||
else if (!replies.ehlo.sent) why = `EHLO 를 보내지 못했다 (curl 종료 코드 ${replies.status})`;
|
||||
else if (replies.ehlo.size === null) why = 'EHLO 응답에 SIZE 줄이 없다';
|
||||
else why = `EHLO 의 SIZE 가 한도를 알리지 않는다 (SIZE ${replies.ehlo.size || '숫자 없음'})`;
|
||||
warn(`${label}가 엔진에 걸렸는지 확인하지 못했습니다 — ${why} (${where}). 관리 API 로 x:MtaStageData 의 maxMessageSize 를 보고, 리로드한 뒤 다시 실행하세요 (mail/README.md "엔진 한도").`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 운영 — 바깥에서 쓰는 주소(MAIL_PUBLIC_URL) 그대로 /jmap/session 이 403 인지 (이 스크립트를 돌리는 호스트에서).
|
||||
* 컨테이너 안 확인과 달리 실제 공개 경로를 본다 — 앞단 프록시가 TLS 를 끝내고 8080(http 리스너)으로 넘기면
|
||||
@@ -5270,10 +5465,12 @@ async function main() {
|
||||
}
|
||||
// 엔진 한도 — 받는 사람 단위 한도는 바깥에서 들어오는 메일에만 (로그인한 직원의 발송에는 걸지 않는다 · 시범 공존이면 기존 서버 IP 도 뺀다),
|
||||
// 계정마다 임시 업로드 크기 합계는 첨부 합계 한도의 20배 이상으로 (둘 다 아래 리로드로 반영)
|
||||
// SMTP 로 받는 메일 한 통은 50MiB 이하로 (아래 리로드로 반영) — IMAP 요청 · 메일 크기 한도는 이전 동안 운영자가 올리므로 바꾸지 않고, 엔진 기본보다 크면 [주의] 만
|
||||
await ensureRecipientThrottle(admin, pilot);
|
||||
// 시범 공존 A 의 나머지 — 기존 서버 IP 의 MAIL FROM · 검사 예외 · 연결 한도 예외 · 자동 차단 예외 (꺼져 있으면 setup 이 넣은 것만 뺀다)
|
||||
await ensurePilotInboundTrust(admin, pilot);
|
||||
await ensureUploadQuota(admin, cfg);
|
||||
const messageSize = await ensureMessageSizeLimits(admin);
|
||||
// 휴지통 · 스팸메일함 30일 자동 비우기와 엔진 정리 시각(새벽 KST — 13단계, 아래 리로드로 반영)
|
||||
const retention = await ensureDataRetention(admin, cfg);
|
||||
// 엔진 DNS — 메일 전용 리졸버(운영은 바로 · 개발은 도커 DNS 를 거쳐), Spamhaus 두 항목은 키가 있으면 DQS 로 · 없으면 끈다 (아래 리로드로 반영)
|
||||
@@ -5297,6 +5494,8 @@ async function main() {
|
||||
}
|
||||
await checkHttpEndpointPolicy(cfg);
|
||||
if (systemRecipientsRejected) await checkSystemRecipientsRejected(cfg, systemRecipients);
|
||||
// 받는 메일 크기 한도가 위 리로드로 실제로 걸렸는지 — 메일 컨테이너 안에서 EHLO 의 SIZE (저장 때의 [변경] · [유지] 만으로는 모른다)
|
||||
await checkMessageSizeLimit(cfg, messageSize);
|
||||
if (pilot) await checkPilotSenderGuard(cfg);
|
||||
checkEngineDnssec(cfg, engineDnsChanged);
|
||||
await checkEngineWebhook(admin, cfg, webhook);
|
||||
|
||||
Reference in New Issue
Block a user