email-delivery-log.js 2.21 KB
import { createHash, randomUUID } from 'node:crypto'

/**
 * @param {import('knex').Knex|undefined} db
 * @param {{
 *   id?: string,
 *   provider?: string,
 *   messageId?: string|null,
 *   purpose: string,
 *   toEmail?: string,
 *   status: string,
 *   template?: string,
 *   errorMessage?: string,
 *   metadata?: any
 * }} record
 */
export async function recordEmailDelivery(db, record) {
  if (!db || typeof db !== 'function') throw new Error('recordEmailDelivery requires db')
  if (!(await db.schema.hasTable('email_delivery_logs'))) throw new Error('recordEmailDelivery requires email_delivery_logs table')
  const toEmail = requiredString(record.toEmail, 'toEmail')
  const purpose = requiredString(record.purpose, 'purpose')
  const status = requiredString(record.status, 'status')
  const row = {
    id: record.id || deliveryLogId(record),
    provider: record.provider || null,
    message_id: record.messageId || null,
    purpose,
    to_email: toEmail,
    status,
    template: record.template || null,
    error_message: record.errorMessage || null,
    metadata_json: record.metadata ? JSON.stringify(record.metadata) : null,
    updated_at: db.fn.now(),
  }
  await db('email_delivery_logs')
    .insert(row)
    .onConflict('id')
    .merge({
      provider: row.provider,
      message_id: row.message_id,
      purpose: row.purpose,
      to_email: row.to_email,
      status: row.status,
      template: row.template,
      error_message: row.error_message,
      metadata_json: row.metadata_json,
      updated_at: row.updated_at,
    })
}

/**
 * @param {{ purpose: string, toEmail?: string }} record
 */
function deliveryLogId(record) {
  const purpose = String(record.purpose || 'email').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 32)
  const toEmail = String(record.toEmail || '').trim().toLowerCase()
  const entropy = `${Date.now()}:${randomUUID()}`
  const digest = createHash('sha1').update(`${purpose}:${toEmail}:${entropy}`).digest('hex').slice(0, 24)
  return `email_${purpose}_${digest}`
}

/**
 * @param {unknown} value
 * @param {string} field
 */
function requiredString(value, field) {
  if (typeof value === 'string' && value.trim().length > 0) return value
  throw new Error(`recordEmailDelivery requires ${field}`)
}