diff --git a/dev/payload.config.ts b/dev/payload.config.ts index f53fc40..11eb050 100644 --- a/dev/payload.config.ts +++ b/dev/payload.config.ts @@ -123,123 +123,6 @@ export default buildConfig({ retryDelay: 60000, // 1 minute for dev queue: 'default', - // Example: Collection overrides for customization - // Uncomment and modify as needed for your use case - /* - collections: { - templates: { - // Custom access controls - restrict who can manage templates - access: { - read: ({ req: { user } }) => { - if (!user) return false - return user.role === 'admin' || user.permissions?.includes('mailing:read') - }, - create: ({ req: { user } }) => { - if (!user) return false - return user.role === 'admin' || user.permissions?.includes('mailing:create') - }, - update: ({ req: { user } }) => { - if (!user) return false - return user.role === 'admin' || user.permissions?.includes('mailing:update') - }, - delete: ({ req: { user } }) => { - if (!user) return false - return user.role === 'admin' - }, - }, - // Custom admin UI settings - admin: { - group: 'Marketing', - description: 'Email templates with enhanced security and categorization' - }, - // Add custom fields to templates - fields: [ - // Default plugin fields are automatically included - { - name: 'category', - type: 'select', - options: [ - { label: 'Marketing', value: 'marketing' }, - { label: 'Transactional', value: 'transactional' }, - { label: 'System Notifications', value: 'system' } - ], - defaultValue: 'transactional', - admin: { - position: 'sidebar', - description: 'Template category for organization' - } - }, - { - name: 'tags', - type: 'text', - hasMany: true, - admin: { - position: 'sidebar', - description: 'Tags for easy template filtering' - } - }, - { - name: 'isActive', - type: 'checkbox', - defaultValue: true, - admin: { - position: 'sidebar', - description: 'Only active templates can be used' - } - } - ], - // Custom validation hooks - hooks: { - beforeChange: [ - ({ data, req }) => { - // Example: Only admins can create system templates - if (data.category === 'system' && req.user?.role !== 'admin') { - throw new Error('Only administrators can create system notification templates') - } - - // Example: Auto-generate slug if not provided - if (!data.slug && data.name) { - data.slug = data.name.toLowerCase() - .replace(/[^a-z0-9]/g, '-') - .replace(/-+/g, '-') - .replace(/^-|-$/g, '') - } - - return data - } - ] - } - }, - emails: { - // Restrict access to emails collection - access: { - read: ({ req: { user } }) => { - if (!user) return false - return user.role === 'admin' || user.permissions?.includes('mailing:read') - }, - create: ({ req: { user } }) => { - if (!user) return false - return user.role === 'admin' || user.permissions?.includes('mailing:create') - }, - update: ({ req: { user } }) => { - if (!user) return false - return user.role === 'admin' || user.permissions?.includes('mailing:update') - }, - delete: ({ req: { user } }) => { - if (!user) return false - return user.role === 'admin' - }, - }, - // Custom admin configuration for emails - admin: { - group: 'Marketing', - description: 'Email delivery tracking and management', - defaultColumns: ['subject', 'to', 'status', 'priority', 'scheduledAt'], - } - } - }, - */ - // Optional: Custom rich text editor configuration // Comment out to use default lexical editor richTextEditor: lexicalEditor({ @@ -256,12 +139,6 @@ export default buildConfig({ // etc. ], }), - - - // Called after mailing plugin is fully initialized - onReady: async (payload) => { - await seedUser(payload) - }, }), ], secret: process.env.PAYLOAD_SECRET || 'test-secret_key', diff --git a/package.json b/package.json index 5c52251..da85819 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/payload-mailing", - "version": "0.4.13", + "version": "0.4.14", "description": "Template-based email system with scheduling and job processing for PayloadCMS", "type": "module", "main": "dist/index.js", diff --git a/src/collections/Emails.ts b/src/collections/Emails.ts index 9bff505..19c99f5 100644 --- a/src/collections/Emails.ts +++ b/src/collections/Emails.ts @@ -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', diff --git a/src/sendEmail.ts b/src/sendEmail.ts index 710b809..1309780 100644 --- a/src/sendEmail.ts +++ b/src/sendEmail.ts @@ -48,7 +48,6 @@ export const sendEmail = async = { ...options.data } as Partial - // 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 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 maxTotalTime) { throw new Error( `Job polling timed out after ${maxTotalTime}ms for email ${email.id}. ` + @@ -162,41 +144,31 @@ export const sendEmail = async 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)`