Files
AriWeb-Suite/modules/arimail/server/util.mjs
T

77 lines
4.9 KiB
JavaScript

export const json = (obj, status = 200, headers = {}) => new Response(JSON.stringify(obj), {
status, headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store', ...headers },
});
export const bad = (error, status = 400, extra = {}) => json({ ok: false, error, ...extra }, status);
export const now = () => new Date().toISOString();
export const plus = (ms) => new Date(Date.now() + ms).toISOString();
export const uid = () => crypto.randomUUID();
export const clean = (v, max = 500) => String(v ?? '').trim().slice(0, max);
export const isEmail = (v) => /^[^@\s]+@[^@\s.]+\.[^@\s]+$/.test(v);
export const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
export function address(value) { const s = String(value || '').trim().toLowerCase(); return (s.match(/<([^<>]+)>$/)?.[1] || s).trim(); }
/**
* 주소 목록 문자열("A <a@x>, b@y; c@z")을 소문자 주소 배열로 바꿉니다(중복 제거).
* 쉼표·세미콜론·줄바꿈으로 나누되, 따옴표 안("Kim, Minsu" <k@x>)과 꺾쇠 안의 구분자는 무시합니다.
* 예전에는 따옴표 안의 쉼표에서도 잘라 '"Kim' 과 'Minsu" <k@x>' 두 개로 나뉘어 저장·발송이 실패했습니다.
*/
export function addresses(value) {
if (Array.isArray(value)) return [...new Set(value.map(address).filter(Boolean))];
const list = []; let current = '', quoted = false, angle = false;
for (const ch of String(value || '')) {
if (ch === '"') quoted = !quoted; else if (ch === '<' && !quoted) angle = true; else if (ch === '>' && !quoted) angle = false;
if (!quoted && !angle && /[,;\n]/.test(ch)) { list.push(current); current = ''; } else current += ch;
}
list.push(current);
return [...new Set(list.map(address).filter(Boolean))];
}
export function secret(bytes = 32) {
return Buffer.from(crypto.getRandomValues(new Uint8Array(bytes))).toString('base64url');
}
export async function sha256(text) {
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
return Buffer.from(buf).toString('hex');
}
export function safeEqual(a, b) {
if (typeof a !== 'string' || typeof b !== 'string' || a.length !== b.length) return false;
let diff = 0; for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i); return diff === 0;
}
/**
* 프로세스 메모리 요청 제한. 단일 프로세스 전제입니다.
* - hit(): 시도를 1회 세고, 한도 안이면 true. 로그인 실패처럼 "세어야 할 사건"에만 부릅니다.
* - blocked(): 세지 않고 지금 막혀 있는지만 봅니다. 메일 서버(POP/IMAP/SMTP)는 로그인 전에 blocked() 로 확인하고 실패했을 때만 hit() 합니다.
* 예전에는 성공한 로그인까지 세어, 메일마다 새로 접속하는 메일 프로그램이 15분에 20통을 넘기면 정상 사용자도 차단됐습니다.
*/
export function memoryRateStore() {
const map = new Map();
return {
blocked(key, { limit = 5 } = {}) { const hit = map.get(key); return !!hit && hit.until >= Date.now() && hit.count >= limit; },
hit(key, { limit = 5, windowSec = 900 } = {}) {
const t = Date.now(); const hit = map.get(key);
if (!hit || hit.until < t) { map.set(key, { count: 1, until: t + windowSec * 1000 }); return true; }
if (hit.count >= limit) return false;
hit.count++; return true;
},
};
}
/** "이름 <주소>" 에서 표시 이름만(따옴표 제거). 이름이 없으면 ''. */
export function nameOf(value) { const s = String(value || '').trim(); const m = s.match(/^(.*?)\s*<[^<>]+>$/); return m ? m[1].trim().replace(/^"(.*)"$/, '$1').replace(/\\(["\\])/g, '$1') : ''; }
/**
* 보내는 사람 "이름 <주소>" 를 RFC 5322 에 맞게 만듭니다(0.9.0~). 줄바꿈·따옴표·꺾쇠는 이름에서 지우고, 쉼표·괄호·@ 같은 특수문자가 있으면
* 이름을 따옴표로 감쌉니다. 이름이 비어 있으면 주소만. 예전에는 계정 이름을 그대로 이어 붙여 "Lee, Director <a@x>" 처럼 깨진 헤더가 나갔습니다.
*/
export function formatSender(name, addr) {
const n = String(name || '').replace(/[\r\n"<>\\]/g, '').trim();
if (!n) return addr;
return /[,()@;:.\[\]]/.test(n) ? `"${n}" <${addr}>` : `${n} <${addr}>`;
}
/**
* 메일 도메인에서 화면 왼쪽 위에 보일 서비스 이름을 만듭니다(0.8.25~). 계정@ 바로 뒤의 첫 이름표를 대문자로 쓰고 " 메일" 을 붙입니다.
* heart-m.com → "HEART-M 메일", cafe.heart-m.com → "CAFE 메일", arihakdang.com → "ARIHAKDANG 메일"
* 예전에는 lang/ko.json 의 appName("아리학당 메일")이 모든 설치에 그대로 보였습니다. 도메인이 비어 있으면 기본 문구(fallback)를 돌려줍니다.
*/
export function siteName(domain, fallback = '메일') {
const label = String(domain || '').trim().toLowerCase().split('.')[0];
return label ? `${label.toUpperCase()} 메일` : fallback;
}