/** * 投递前基本校验(纯函数,零网络)。 * 只做「畸形请求拦截」:非法邮箱、空 subject。 * 不做业务规则(验证码、退订等不属于本包)。 */ import { EmailError } from './map-error.js' // 务实的邮箱形态校验:localpart@domain.tld,不含空白,domain 至少一个点。 // 不追求 RFC 5322 全量正确(那是投递通道的事),只拦明显畸形。 const EMAIL_RE = /^[^\s@"']+@[^\s@.]+(\.[^\s@.]+)+$/ /** * @param {string} addr * @returns {boolean} */ export function isValidEmail(addr) { return typeof addr === 'string' && addr.length <= 254 && EMAIL_RE.test(addr) } /** * 校验一条 SendMessage 的收件人与 subject。非法即抛 EmailError。 * @param {import('./index.js').SendMessage} msg * @param {string} provider 用于错误归属 * @returns {{ to: string|string[] }} */ export function assertSendable(msg, provider) { if (!msg || typeof msg !== 'object') { throw new EmailError({ provider, reason: 'invalidRecipient', retryable: false, message: 'message must be an object' }) } const recipients = msg.to == null ? [] : (Array.isArray(msg.to) ? msg.to : [msg.to]) if (recipients.length === 0) { throw new EmailError({ provider, reason: 'invalidRecipient', retryable: false, message: 'no recipient' }) } for (const r of recipients) { if (!isValidEmail(r)) { throw new EmailError({ provider, reason: 'invalidRecipient', retryable: false, message: `invalid recipient address: ${String(r)}`, }) } } if (typeof msg.subject !== 'string' || msg.subject.trim() === '') { throw new EmailError({ provider, reason: 'invalidRecipient', retryable: false, message: 'empty subject' }) } if ((msg.html == null || msg.html === '') && (msg.text == null || msg.text === '')) { throw new EmailError({ provider, reason: 'invalidRecipient', retryable: false, message: 'message has no html or text body' }) } return { to: msg.to } }