validate.js 1.97 KB
/**
 * 投递前基本校验(纯函数,零网络)。
 * 只做「畸形请求拦截」:非法邮箱、空 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 }
}