Clean up sendEmail.ts and bump version to 0.4.14

- Remove duplicate nested if statement at line 188
- Remove redundant comments throughout the file
- Simplify code structure for better readability
- Bump patch version from 0.4.13 to 0.4.14

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-06 22:59:55 +02:00
parent c62a364d9c
commit f303eda652
4 changed files with 26 additions and 157 deletions

View File

@@ -10,6 +10,26 @@ const Emails: CollectionConfig = {
group: 'Mailing',
description: 'Email delivery and status tracking',
},
defaultPopulate: {
template: true,
to: true,
cc: true,
bcc: true,
from: true,
replyTo: true,
jobs: true,
status: true,
attempts: true,
lastAttemptAt: true,
error: true,
priority: true,
scheduledAt: true,
sentAt: true,
variables: true,
html: true,
text: true,
createdAt: true,
},
fields: [
{
name: 'template',

View File

@@ -48,7 +48,6 @@ export const sendEmail = async <TEmail extends BaseEmailDocument = BaseEmailDocu
let emailData: Partial<TEmail> = { ...options.data } as Partial<TEmail>
// If using a template, render it first
if (options.template) {
const { html, text, subject } = await renderTemplate(
payload,
@@ -56,7 +55,6 @@ export const sendEmail = async <TEmail extends BaseEmailDocument = BaseEmailDocu
options.template.variables || {}
)
// Template values take precedence over data values
emailData = {
...emailData,
subject,
@@ -70,20 +68,16 @@ export const sendEmail = async <TEmail extends BaseEmailDocument = BaseEmailDocu
throw new Error('Field "to" is required for sending emails')
}
// Validate required fields based on whether template was used
if (options.template) {
// When using template, subject and html should have been set by renderTemplate
if (!emailData.subject || !emailData.html) {
throw new Error(`Template rendering failed: template "${options.template.slug}" did not provide required subject and html content`)
}
} else {
// When not using template, user must provide subject and html directly
if (!emailData.subject || !emailData.html) {
throw new Error('Fields "subject" and "html" are required when sending direct emails without a template')
}
}
// Process email addresses using shared validation (handle null values)
if (emailData.to) {
emailData.to = parseAndValidateEmails(emailData.to as string | string[])
}
@@ -95,19 +89,15 @@ export const sendEmail = async <TEmail extends BaseEmailDocument = BaseEmailDocu
}
if (emailData.replyTo) {
const validated = parseAndValidateEmails(emailData.replyTo as string | string[])
// replyTo should be a single email, so take the first one if array
emailData.replyTo = validated && validated.length > 0 ? validated[0] : undefined
}
if (emailData.from) {
const validated = parseAndValidateEmails(emailData.from as string | string[])
// from should be a single email, so take the first one if array
emailData.from = validated && validated.length > 0 ? validated[0] : undefined
}
// Sanitize fromName to prevent header injection
emailData.fromName = sanitizeFromName(emailData.fromName as string)
// Normalize Date objects to ISO strings for consistent database storage
if (emailData.scheduledAt instanceof Date) {
emailData.scheduledAt = emailData.scheduledAt.toISOString()
}
@@ -124,19 +114,15 @@ export const sendEmail = async <TEmail extends BaseEmailDocument = BaseEmailDocu
emailData.updatedAt = emailData.updatedAt.toISOString()
}
// Create the email in the collection with proper typing
// The hooks will automatically create and populate the job relationship
const email = await payload.create({
collection: collectionSlug,
data: emailData
})
// Validate that the created email has the expected structure
if (!email || typeof email !== 'object' || !email.id) {
throw new Error('Failed to create email: invalid response from database')
}
// If processImmediately is true, get the job from the relationship and process it now
if (options.processImmediately) {
const logger = createContextLogger(payload, 'IMMEDIATE')
@@ -144,17 +130,13 @@ export const sendEmail = async <TEmail extends BaseEmailDocument = BaseEmailDocu
throw new Error('PayloadCMS jobs not configured - cannot process email immediately')
}
// Poll for the job with optimized backoff and timeout protection
// This handles the async nature of hooks and ensures we wait for job creation
const maxAttempts = 5 // Reduced from 10 to minimize delay
const initialDelay = 25 // Reduced from 50ms for faster response
const maxTotalTime = 3000 // 3 second total timeout
const maxAttempts = 5
const initialDelay = 25
const maxTotalTime = 3000
const startTime = Date.now()
let jobId: string | undefined
for (let attempt = 0; attempt < maxAttempts; attempt++) {
// Check total timeout before continuing
if (Date.now() - startTime > maxTotalTime) {
throw new Error(
`Job polling timed out after ${maxTotalTime}ms for email ${email.id}. ` +
@@ -162,41 +144,31 @@ export const sendEmail = async <TEmail extends BaseEmailDocument = BaseEmailDocu
)
}
// Calculate delay with exponential backoff (25ms, 50ms, 100ms, 200ms, 400ms)
// Cap at 400ms per attempt for better responsiveness
const delay = Math.min(initialDelay * Math.pow(2, attempt), 400)
if (attempt > 0) {
await new Promise(resolve => setTimeout(resolve, delay))
}
// Refetch the email to check for jobs
const emailWithJobs = await payload.findByID({
collection: collectionSlug,
id: email.id,
})
if (emailWithJobs.jobs && emailWithJobs.jobs.length > 0) {
// Job found! Get the first job ID (should only be one for a new email)
const firstJob = Array.isArray(emailWithJobs.jobs) ? emailWithJobs.jobs[0] : emailWithJobs.jobs
jobId = typeof firstJob === 'string' ? firstJob : String(firstJob.id || firstJob)
break
}
// Log on later attempts to help with debugging (reduced threshold)
if (attempt >= 1) {
if (attempt >= 2) {
logger.debug(`Waiting for job creation for email ${email.id}, attempt ${attempt + 1}/${maxAttempts}`)
}
if (attempt >= 2) {
logger.debug(`Waiting for job creation for email ${email.id}, attempt ${attempt + 1}/${maxAttempts}`)
}
}
if (!jobId) {
// Distinguish between different failure scenarios for better error handling
const timeoutMsg = Date.now() - startTime >= maxTotalTime
const errorType = timeoutMsg ? 'POLLING_TIMEOUT' : 'JOB_NOT_FOUND'
const baseMessage = timeoutMsg
? `Job polling timed out after ${maxTotalTime}ms for email ${email.id}`
: `No processing job found for email ${email.id} after ${maxAttempts} attempts (${Date.now() - startTime}ms)`