UNPKG

@xtr-dev/payload-mailing

Version:

Template-based email system with scheduling and job processing for PayloadCMS

119 lines (118 loc) 4.46 kB
import { getMailing, renderTemplateWithId, parseAndValidateEmails, sanitizeFromName } from './utils/helpers.js'; import { processJobById } from './utils/emailProcessor.js'; import { createContextLogger } from './utils/logger.js'; import { pollForJobId } from './utils/jobPolling.js'; /** * Send an email with full type safety * * @example * ```typescript * // With your generated Email type * import { Email } from './payload-types' * * const email = await sendEmail<Email>(payload, { * template: { * slug: 'welcome', * variables: { name: 'John' } * }, * data: { * to: 'user@example.com', * customField: 'value' // Your custom fields are type-safe! * } * }) * ``` */ export const sendEmail = async (payload, options) => { const mailingConfig = getMailing(payload); const collectionSlug = options.collectionSlug || mailingConfig.collections.emails || 'emails'; let emailData = { ...options.data }; if (options.template) { // Look up and render the template in a single operation to avoid duplicate lookups const { html, text, subject, templateId } = await renderTemplateWithId(payload, options.template.slug, options.template.variables || {}); emailData = { ...emailData, template: templateId, subject, html, text, }; } // Validate required fields if (!emailData.to) { throw new Error('Field "to" is required for sending emails'); } if (options.template) { if (!emailData.subject || !emailData.html) { throw new Error(`Template rendering failed: template "${options.template.slug}" did not provide required subject and html content`); } } else { if (!emailData.subject || !emailData.html) { throw new Error('Fields "subject" and "html" are required when sending direct emails without a template'); } } if (emailData.to) { emailData.to = parseAndValidateEmails(emailData.to); } if (emailData.cc) { emailData.cc = parseAndValidateEmails(emailData.cc); } if (emailData.bcc) { emailData.bcc = parseAndValidateEmails(emailData.bcc); } if (emailData.replyTo) { const validated = parseAndValidateEmails(emailData.replyTo); emailData.replyTo = validated && validated.length > 0 ? validated[0] : undefined; } if (emailData.from) { const validated = parseAndValidateEmails(emailData.from); emailData.from = validated && validated.length > 0 ? validated[0] : undefined; } emailData.fromName = sanitizeFromName(emailData.fromName); if (emailData.scheduledAt instanceof Date) { emailData.scheduledAt = emailData.scheduledAt.toISOString(); } if (emailData.sentAt instanceof Date) { emailData.sentAt = emailData.sentAt.toISOString(); } if (emailData.lastAttemptAt instanceof Date) { emailData.lastAttemptAt = emailData.lastAttemptAt.toISOString(); } if (emailData.createdAt instanceof Date) { emailData.createdAt = emailData.createdAt.toISOString(); } if (emailData.updatedAt instanceof Date) { emailData.updatedAt = emailData.updatedAt.toISOString(); } const email = await payload.create({ collection: collectionSlug, data: emailData }); if (!email || typeof email !== 'object' || !email.id) { throw new Error('Failed to create email: invalid response from database'); } if (options.processImmediately) { const logger = createContextLogger(payload, 'IMMEDIATE'); if (!payload.jobs) { throw new Error('PayloadCMS jobs not configured - cannot process email immediately'); } // Poll for the job ID using configurable polling mechanism const { jobId } = await pollForJobId({ payload, collectionSlug, emailId: email.id, config: mailingConfig.jobPolling, logger, }); try { await processJobById(payload, jobId); logger.debug(`Successfully processed email ${email.id} immediately`); } catch (error) { logger.error(`Failed to process email ${email.id} immediately:`, error); throw new Error(`Failed to process email ${email.id} immediately: ${String(error)}`); } } return email; }; export default sendEmail;