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:
+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