@xtr-dev/payload-mailing
Version:
Template-based email system with scheduling and job processing for PayloadCMS
119 lines (118 loc) • 4.46 kB
JavaScript
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;