Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
70 lines
5.3 KiB
JavaScript
70 lines
5.3 KiB
JavaScript
/**
|
|
* 메일 서비스 모듈 선택(1.0.0, 요구사항 2) — 발송·수신 "길" 을 설정(config.json 의 inbound / outbound)으로 고릅니다.
|
|
*
|
|
* 발신(outbound)
|
|
* resend : Resend REST API(providers/resend.mjs). 예전과 같은 기본값.
|
|
* haraka : 내장 Haraka 의 로컬 제출 포트(127.0.0.1:HARAKA_RELAY_PORT, 암호화·인증 없음)로 넘기면 Haraka 가 받는 쪽 MX 로 직접 배달합니다.
|
|
* smtp : 외부 SMTP 중계 서버(SMTP_RELAY_*)로 넘깁니다(회사 메일 서버, 다른 발송 서비스의 SMTP 등).
|
|
* 수신(inbound)
|
|
* resend : Resend 웹훅 + 목록 동기화(store.mjs).
|
|
* haraka : 내장 Haraka 가 25 포트로 받은 메일을 로컬 HTTP(/api/inbound/haraka)로 넘겨 줍니다(store.mjs ingestRaw).
|
|
*
|
|
* 발송 호출 지점은 모두 sendMail(env, payload, key) 하나입니다. payload 는 Resend 요청과 같은 모양(from, to, cc, bcc, reply_to, subject,
|
|
* text, html, headers, attachments[{filename, content(base64), content_type, content_id}])이고 돌려주는 값은 { id } 입니다.
|
|
* 새 발송 서비스를 더하려면 providers/<이름>.mjs 에 같은 모양으로 send 를 만들고 여기 outboundMode·sendMail·outboundStatus 에 한 줄씩 더합니다.
|
|
*/
|
|
import { resend, providerStatus as resendStatus } from './resend.mjs';
|
|
import { sendViaSmtp } from './smtp.mjs';
|
|
import { smtpProbe } from './smtp-client.mjs';
|
|
import { now } from '../util.mjs';
|
|
|
|
export const OUTBOUND_MODES = ['resend', 'haraka', 'smtp'], INBOUND_MODES = ['resend', 'haraka'];
|
|
/** 설정된 발신 방식(알 수 없는 값은 resend). env.MAIL_OUTBOUND 는 config.mjs 의 applyConfig 가 채웁니다. */
|
|
export const outboundMode = (env) => (OUTBOUND_MODES.includes(env.MAIL_OUTBOUND) ? env.MAIL_OUTBOUND : 'resend');
|
|
/** 설정된 수신 방식(알 수 없는 값은 resend). */
|
|
export const inboundMode = (env) => (INBOUND_MODES.includes(env.MAIL_INBOUND) ? env.MAIL_INBOUND : 'resend');
|
|
/** Resend 를 어느 쪽으로든 쓰는지(동기화·웹훅·도메인 상태 확인이 의미 있는지). */
|
|
export const usesResend = (env) => inboundMode(env) === 'resend' || outboundMode(env) === 'resend';
|
|
/** 내장 Haraka 를 어느 쪽으로든 쓰는지(haraka/run.mjs 가 Haraka 를 띄울지). */
|
|
export const usesHaraka = (env) => inboundMode(env) === 'haraka' || outboundMode(env) === 'haraka';
|
|
|
|
/** 발신 방식에 따른 중계 서버 접속 정보. haraka 는 로컬 고정, smtp 는 SMTP_RELAY_* 설정. */
|
|
export function relaySettings(env, mode = outboundMode(env)) {
|
|
// haraka: 서버에 하나뿐인 Haraka 의 제출 포트는 기본 도메인의 설정(HARAKA_RELAY_PORT)이 정합니다(1.0.7: 추가 도메인도 같은 포트로 넘김).
|
|
if (mode === 'haraka') { const primary = env.TENANTS?.primary?.env || env; return { host: '127.0.0.1', port: Number(primary.HARAKA_RELAY_PORT || 2525), security: 'none', user: '', pass: '' }; }
|
|
if (mode === 'smtp') return { host: String(env.SMTP_RELAY_HOST || '').trim(), port: Number(env.SMTP_RELAY_PORT || 587), security: ['none', 'starttls', 'tls'].includes(env.SMTP_RELAY_SECURITY) ? env.SMTP_RELAY_SECURITY : 'starttls', user: String(env.SMTP_RELAY_USER || ''), pass: String(env.SMTP_RELAY_PASS || ''), rejectUnauthorized: env.SMTP_RELAY_INSECURE !== '1' };
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 메일 한 통 발송. 어느 모듈을 쓰든 { id } 를 돌려주고, 실패는 공통 오류 코드(mail_not_configured / mail_auth_failed / mail_rejected /
|
|
* mail_rate_limited / mail_provider_unavailable)의 Error 로 던집니다(providerMessage 에 서비스 응답).
|
|
* @param {string} [key] 멱등 키(Resend 만 씀)
|
|
*/
|
|
export async function sendMail(env, payload, key) {
|
|
const mode = outboundMode(env);
|
|
if (mode === 'resend') return resend(env, '/emails', 'POST', payload, key);
|
|
return sendViaSmtp(env, payload, relaySettings(env, mode));
|
|
}
|
|
|
|
// 상태 캐시는 env(= DB) 마다 따로 둡니다(시험처럼 한 프로세스에 여러 env 가 있을 때 섞이지 않게). 운영은 env 가 하나라 전역 캐시와 같습니다.
|
|
const statusCaches = new WeakMap();
|
|
/**
|
|
* 발신 서비스 상태(설정 화면·상태 화면). resend 는 도메인 검증 상태까지, haraka/smtp 는 중계 서버 접속·EHLO(·AUTH) 확인입니다.
|
|
* 60초 캐시(force 로 즉시). 돌려주는 모양은 예전 providerStatus 와 같고 mode 와 relay 를 더합니다.
|
|
* @returns {Promise<{mode:string, provider:string, detail?:string, domain?:object, relay?:object, checkedAt:string}>}
|
|
*/
|
|
export async function outboundStatus(env, { force = false } = {}) {
|
|
const mode = outboundMode(env), key = mode + JSON.stringify(relaySettings(env, mode) || {}) + (env.RESEND_API_KEY ? 'k' : '');
|
|
const statusCache = statusCaches.get(env.DB || env) || { at: 0, key: '', value: null };
|
|
if (!force && statusCache.value && statusCache.key === key && Date.now() - statusCache.at < 60000) return statusCache.value;
|
|
let value;
|
|
if (mode === 'resend') value = { mode, ...(await resendStatus(env, { force })) };
|
|
else {
|
|
const relay = relaySettings(env, mode);
|
|
const probe = await smtpProbe({ ...relay, hostname: env.PUBLIC_HOST || 'localhost' });
|
|
value = { mode, provider: probe.ok ? 'ok' : probe.error, detail: probe.detail, relay: { host: relay.host, port: relay.port, security: relay.security, auth: !!relay.user }, checkedAt: now() };
|
|
}
|
|
statusCaches.set(env.DB || env, { at: Date.now(), key, value }); return value;
|
|
}
|