Files
AriWeb-Suite/modules/arimail/server/providers/smtp.mjs
T

47 lines
3.4 KiB
JavaScript
Raw Normal View History

/**
* SMTP 중계 발송 모듈(1.0.0) — 발신 방식이 haraka(내장 Haraka 의 로컬 제출 포트) 또는 smtp(외부 중계 서버)일 때 쓰입니다.
*
* 입력은 Resend 발송 요청과 같은 모양의 메시지 객체입니다(providers/index.mjs 참고). 그래서 store.mjs(웹 발송)·smtp.mjs(제출 서버)·
* automation.mjs(전달·자동 응답)는 어느 발송 모듈을 쓰는지 몰라도 됩니다. 이 모듈은 그 객체를 RFC 5322 원본(.eml)으로 만들어
* (mime.mjs 의 buildEml 재사용) 봉투(MAIL FROM / RCPT TO)와 함께 중계 서버에 넘깁니다.
*
* 주의: Bcc 는 헤더에 쓰지 않고 봉투에만 넣습니다(buildEml 에 bcc 를 넘기면 헤더가 생기므로 넘기지 않음).
* 멱등 키(key)는 SMTP 에 해당 개념이 없어 쓰지 않습니다. 발송 전에 저장소가 send_key 로 중복을 막고 있습니다.
*/
import { randomUUID } from 'node:crypto';
import { buildEml } from '../mime.mjs';
import { address, addresses } from '../util.mjs';
import { smtpSend } from './smtp-client.mjs';
/**
* Resend 형식 메시지 객체 → { eml, envelope }. 시험과 보낸 메일함 저장이 같은 변환을 쓸 수 있게 따로 두었습니다.
* @param {object} payload { from, to[], cc?[], bcc?[], reply_to?[], subject, text?, html?, headers?{}, attachments?[{filename, content(base64), content_type, content_id?}] }
* @param {string} domain Message-ID 의 @ 뒤 도메인
*/
export function payloadToEml(payload, domain) {
const messageId = `<${randomUUID()}@${domain || 'localhost'}>`;
const eml = buildEml({
from: payload.from, to: payload.to || [], cc: payload.cc || [], replyTo: payload.reply_to || [], subject: payload.subject || '',
text: payload.text, html: payload.html, headers: payload.headers || {}, messageId,
attachments: (payload.attachments || []).map((a) => ({ filename: a.filename, contentType: a.content_type, bytes: Buffer.from(a.content || '', 'base64'), contentId: a.content_id || undefined, disposition: a.content_id ? 'inline' : 'attachment' })),
});
const envelope = { from: address(payload.from), to: addresses([...(payload.to || []), ...(payload.cc || []), ...(payload.bcc || [])]) };
return { eml, envelope, messageId };
}
/**
* 메시지 객체를 중계 서버로 보냅니다.
* @param {object} env SMTP_RELAY_* 가 아니라 호출자가 고른 relay 설정을 씁니다(env 는 도메인·호스트 이름용)
* @param {object} payload Resend 형식 메시지
* @param {object} relay { host, port, security, user, pass, rejectUnauthorized? }
* @returns {Promise<{id:string, accepted:string[], rejected:object[], response:string}>} id = 생성한 Message-ID(괄호 없이). messages.provider_id 에 저장됩니다.
*/
export async function sendViaSmtp(env, payload, relay) {
const domain = String(env.MAILBOX || 'admin@arihakdang.com').split('@')[1] || 'localhost';
const { eml, envelope, messageId } = payloadToEml(payload, domain);
if (!envelope.to.length) { const e = new Error('mail_rejected'); e.providerMessage = 'no recipients'; throw e; }
const result = await smtpSend({ ...relay, hostname: env.PUBLIC_HOST || 'localhost', from: envelope.from, to: envelope.to, eml });
if (result.rejected.length) console.warn('SMTP 중계: 일부 받는 사람 거부', result.rejected.map((r) => `${r.address} (${r.reply})`).join(', '));
return { id: messageId.replace(/^<|>$/g, ''), ...result };
}