메일 서비스(사이트 대표·독립)·메일함·사용량·보안·전환·보존과 AriMail 도입 수정(KST 월 경계, 발신 키 오류, 도메인별 보장량, 100% 제한, 발송 큐·속도, 운영 상태, SSO) (§15~§18·§0.4)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
a014e3cd05
commit
a1cde81332
@@ -134,8 +134,22 @@ export function normalizeEntry(body, current = null, { keepAuth = false } = {})
|
||||
}
|
||||
const note = String(body.note === undefined ? (current?.note || '') : body.note || '').trim().slice(0, 200);
|
||||
const enabled = body.enabled === undefined ? (current ? current.enabled !== false : true) : !(body.enabled === false || body.enabled === 'false' || body.enabled === '0' || body.enabled === 0);
|
||||
// [Ariweb Suite 변경] Ariweb Suite 연동 정보(§0.4): 사이트 대표 메일(site_bound) / 독립 메일 서비스(standalone) 구분과 포털 식별자. 운영 상태(operating)는 이전 중 두 서버가 함께 처리하지 않게 합니다.
|
||||
const ariweb = normalizeAriweb(body.ariweb === undefined ? current?.ariweb : body.ariweb);
|
||||
const operating = body.operating === undefined ? (current?.operating || 'active') : String(body.operating || 'active');
|
||||
if (!OPERATING_STATES.includes(operating)) throw invalid('operating');
|
||||
const stamp = new Date().toISOString();
|
||||
return { domain, enabled, hosts, mailbox: `${mailboxLocal}@${domain}`, auth: { mode, wwwDbFile: mode === 'www' ? wwwDbFile : '', ownerLogin: mode === 'www' ? ownerLogin : '', webmakerDir: mode === 'webmaker' ? webmakerDir : '', webmakerUser: mode === 'webmaker' ? webmakerUser : '', siteId: mode === 'webmaker' ? siteId : '' }, note, createdAt: current?.createdAt || stamp, updatedAt: stamp };
|
||||
return { domain, enabled, hosts, mailbox: `${mailboxLocal}@${domain}`, auth: { mode, wwwDbFile: mode === 'www' ? wwwDbFile : '', ownerLogin: mode === 'www' ? ownerLogin : '', webmakerDir: mode === 'webmaker' ? webmakerDir : '', webmakerUser: mode === 'webmaker' ? webmakerUser : '', siteId: mode === 'webmaker' ? siteId : '' }, note, ariweb, operating, createdAt: current?.createdAt || stamp, updatedAt: stamp };
|
||||
}
|
||||
/** [Ariweb Suite 변경] 도메인 운영 상태. active 가 아니면 수신 임시 거부·발신 차단(limits.operatingState). */
|
||||
export const OPERATING_STATES = ['active', 'migrating_out', 'migrating_in', 'frozen'];
|
||||
export const ARIWEB_KINDS = ['site_bound', 'standalone'];
|
||||
/** Ariweb 연동 정보 정규화: 없으면 null. kind 는 site_bound|standalone, 나머지는 포털 식별자 문자열(64자). */
|
||||
export function normalizeAriweb(v) {
|
||||
if (!v || typeof v !== 'object') return null;
|
||||
const kind = String(v.kind || ''); if (!ARIWEB_KINDS.includes(kind)) throw invalid('ariweb.kind');
|
||||
const id = (x) => { const s = String(x ?? '').trim().slice(0, 64); if (s && !/^[A-Za-z0-9_.:-]+$/.test(s)) throw invalid('ariweb'); return s; };
|
||||
return { kind, siteId: id(v.siteId), groupId: id(v.groupId), mailServiceId: id(v.mailServiceId) };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,7 +175,8 @@ function primaryEntry(baseEnv, stored = null) {
|
||||
// 로그인 방식은 환경 파일이 정합니다: LOGIN_MODE=webmaker(+WEBMAKER_DIR) → webmaker, WWW_DB_FILE(시험에서는 메모리 WWW_DB) → www, 그 밖에는 local.
|
||||
const mode = String(baseEnv.LOGIN_MODE || '') === 'webmaker' && baseEnv.WEBMAKER_DIR ? 'webmaker' : baseEnv.WWW_DB_FILE || baseEnv.WWW_DB ? 'www' : 'local';
|
||||
const auth = { mode, wwwDbFile: mode === 'www' ? String(baseEnv.WWW_DB_FILE || '') : '', ownerLogin: mode === 'www' ? String(baseEnv.MAILBOX_OWNER_LOGIN || '') : '', webmakerDir: mode === 'webmaker' ? String(baseEnv.WEBMAKER_DIR) : '', webmakerUser: mode === 'webmaker' ? String(baseEnv.WEBMAKER_USER || '') : '', siteId: mode === 'webmaker' ? String(baseEnv.WEBMAKER_SITE || '') : '' };
|
||||
return { domain, enabled: true, primary: true, hosts, mailbox, auth, note: stored?.note || '', createdAt: stored?.createdAt || new Date().toISOString(), updatedAt: stored?.updatedAt || new Date().toISOString() };
|
||||
// [Ariweb Suite 변경] 기본 도메인도 ariweb·operating 은 등록부 값을 그대로 둡니다(환경 파일에는 없는 정보).
|
||||
return { domain, enabled: true, primary: true, hosts, mailbox, auth, note: stored?.note || '', ariweb: stored?.ariweb || null, operating: OPERATING_STATES.includes(stored?.operating) ? stored.operating : 'active', createdAt: stored?.createdAt || new Date().toISOString(), updatedAt: stored?.updatedAt || new Date().toISOString() };
|
||||
}
|
||||
|
||||
/** 등록부를 읽습니다. 없으면 기본 도메인만 담아 만듭니다(1.0.7 로 올린 서버의 첫 시작). 파일을 쓸 수 없으면 메모리에서만 동작합니다. */
|
||||
@@ -270,7 +285,7 @@ export function openTenants(baseEnv, { migrationsDir, onCreate = null, log = con
|
||||
// external(1.0.8): webmaker 연동 도메인의 웹메이커 계정 수(읽지 못하면 오류 문구). 화면이 "계정 N명(+웹메이커 M명)" 으로 보여 줍니다.
|
||||
let external = null;
|
||||
if (e.auth.mode === 'webmaker') { try { const r = await externalUsers(env); external = r.ok ? { count: r.accounts.length, site: r.site } : { count: 0, error: r.error }; } catch (err) { external = { count: 0, error: err.message }; } }
|
||||
out.push({ domain: e.domain, primary: e.domain === registry.primary, enabled: e.enabled !== false, hosts: e.hosts, mailbox: e.mailbox, auth: publicAuth(e.auth), note: e.note || '', createdAt: e.createdAt, updatedAt: e.updatedAt, inbound: env.MAIL_INBOUND || 'resend', outbound: env.MAIL_OUTBOUND || 'resend', setupDone: env.CONFIG?.SETUP_DONE === true, accounts, messages, external, dir: e.domain === registry.primary ? configDir(env) : dirOf(e.domain), initialPassword });
|
||||
out.push({ domain: e.domain, primary: e.domain === registry.primary, enabled: e.enabled !== false, hosts: e.hosts, mailbox: e.mailbox, auth: publicAuth(e.auth), note: e.note || '', ariweb: e.ariweb || null, operating: e.operating || 'active', createdAt: e.createdAt, updatedAt: e.updatedAt, inbound: env.MAIL_INBOUND || 'resend', outbound: env.MAIL_OUTBOUND || 'resend', setupDone: env.CONFIG?.SETUP_DONE === true, accounts, messages, external, dir: e.domain === registry.primary ? configDir(env) : dirOf(e.domain), initialPassword });
|
||||
}
|
||||
return out;
|
||||
},
|
||||
@@ -305,7 +320,8 @@ export function openTenants(baseEnv, { migrationsDir, onCreate = null, log = con
|
||||
update(domain, body) {
|
||||
const t = this.byDomain(domain); if (!t) throw new Error('not_found');
|
||||
const isPrimary = t.entry.domain === registry.primary;
|
||||
const next = isPrimary ? { ...t.entry, ...normalizeEntry({ hosts: body.hosts, note: body.note }, t.entry, { keepAuth: true }), enabled: true, primary: true, mailbox: t.entry.mailbox, auth: t.entry.auth } : normalizeEntry({ ...body, domain: t.entry.domain, mailbox: undefined }, t.entry);
|
||||
// [Ariweb Suite 변경] 기본 도메인도 ariweb(연동 정보)·operating(운영 상태)은 바꿀 수 있습니다.
|
||||
const next = isPrimary ? { ...t.entry, ...normalizeEntry({ hosts: body.hosts, note: body.note, ariweb: body.ariweb, operating: body.operating }, t.entry, { keepAuth: true }), enabled: true, primary: true, mailbox: t.entry.mailbox, auth: t.entry.auth } : normalizeEntry({ ...body, domain: t.entry.domain, mailbox: undefined }, t.entry);
|
||||
// 기본 도메인의 첫 호스트는 환경 파일의 PUBLIC_HOST(+PUBLIC_PATH)로 고정합니다(목록에서 빠지거나 뒤로 가면 앞에 되돌림).
|
||||
if (isPrimary) { const ph = normalizeHost(baseEnv.PUBLIC_HOST || ''), first = ph + (normalizePrefix(baseEnv.PUBLIC_PATH) || ''); next.hosts = [first, ...next.hosts.filter((h) => hostNameOf(h) !== ph)]; }
|
||||
for (const other of tenants.values()) if (other !== t) for (const h of next.hosts) if (other.entry.hosts.some((x) => hostNameOf(x) === hostNameOf(h))) { const e = new Error('host_taken'); e.field = 'hosts'; e.detail = h; throw e; }
|
||||
|
||||
Reference in New Issue
Block a user