mirror of
https://github.com/xtr-dev/payload-mailing.git
synced 2025-12-10 08:13:23 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80d32674a9 | ||
| 243f7c96cf | |||
| 159a99a1ec | |||
| 860dd7e921 | |||
|
|
c7af628beb | ||
| fa54c5622c | |||
| 719b60b9ef |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xtr-dev/payload-mailing",
|
"name": "@xtr-dev/payload-mailing",
|
||||||
"version": "0.0.6",
|
"version": "0.0.8",
|
||||||
"description": "Template-based email system with scheduling and job processing for PayloadCMS",
|
"description": "Template-based email system with scheduling and job processing for PayloadCMS",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -4,9 +4,61 @@ import { MailingService } from './services/MailingService.js'
|
|||||||
import { createEmailTemplatesCollection } from './collections/EmailTemplates.js'
|
import { createEmailTemplatesCollection } from './collections/EmailTemplates.js'
|
||||||
import Emails from './collections/Emails.js'
|
import Emails from './collections/Emails.js'
|
||||||
|
|
||||||
|
// Helper function to schedule the email processing job
|
||||||
|
async function scheduleEmailProcessingJob(payload: any, queueName: string, delayMs: number = 60000): Promise<boolean> {
|
||||||
|
if (!queueName || typeof queueName !== 'string') {
|
||||||
|
throw new Error('Invalid queueName: must be a non-empty string')
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobSlug = 'process-email-queue'
|
||||||
|
|
||||||
|
// Check if there's already a scheduled job for this task
|
||||||
|
const existingJobs = await payload.find({
|
||||||
|
collection: 'payload-jobs',
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
taskSlug: {
|
||||||
|
equals: jobSlug,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hasCompleted: {
|
||||||
|
equals: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
// If no existing job, schedule a new one
|
||||||
|
if (existingJobs.docs.length === 0) {
|
||||||
|
await payload.create({
|
||||||
|
collection: 'payload-jobs',
|
||||||
|
data: {
|
||||||
|
taskSlug: jobSlug,
|
||||||
|
input: {},
|
||||||
|
queue: queueName,
|
||||||
|
waitUntil: new Date(Date.now() + delayMs),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
console.log(`🔄 Scheduled email processing job in queue: ${queueName}`)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
console.log(`✅ Email processing job already scheduled in queue: ${queueName}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const mailingPlugin = (pluginConfig: MailingPluginConfig) => (config: Config): Config => {
|
export const mailingPlugin = (pluginConfig: MailingPluginConfig) => (config: Config): Config => {
|
||||||
const queueName = pluginConfig.queue || 'default'
|
const queueName = pluginConfig.queue || 'default'
|
||||||
|
|
||||||
|
// Validate queueName
|
||||||
|
if (!queueName || typeof queueName !== 'string') {
|
||||||
|
throw new Error('Invalid queue configuration: queue must be a non-empty string')
|
||||||
|
}
|
||||||
|
|
||||||
// Handle templates collection configuration
|
// Handle templates collection configuration
|
||||||
const templatesConfig = pluginConfig.collections?.templates
|
const templatesConfig = pluginConfig.collections?.templates
|
||||||
const templatesSlug = typeof templatesConfig === 'string' ? templatesConfig : 'email-templates'
|
const templatesSlug = typeof templatesConfig === 'string' ? templatesConfig : 'email-templates'
|
||||||
@@ -80,8 +132,11 @@ export const mailingPlugin = (pluginConfig: MailingPluginConfig) => (config: Con
|
|||||||
{
|
{
|
||||||
slug: 'process-email-queue',
|
slug: 'process-email-queue',
|
||||||
handler: async ({ job, req }: { job: any; req: any }) => {
|
handler: async ({ job, req }: { job: any; req: any }) => {
|
||||||
|
const payload = (req as any).payload
|
||||||
|
let jobResult = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mailingService = new MailingService((req as any).payload, pluginConfig)
|
const mailingService = new MailingService(payload, pluginConfig)
|
||||||
|
|
||||||
console.log('🔄 Processing email queue (pending + failed emails)...')
|
console.log('🔄 Processing email queue (pending + failed emails)...')
|
||||||
|
|
||||||
@@ -91,19 +146,41 @@ export const mailingPlugin = (pluginConfig: MailingPluginConfig) => (config: Con
|
|||||||
// Then retry failed emails
|
// Then retry failed emails
|
||||||
await mailingService.retryFailedEmails()
|
await mailingService.retryFailedEmails()
|
||||||
|
|
||||||
return {
|
jobResult = {
|
||||||
output: {
|
output: {
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Email queue processed successfully (pending and failed emails)'
|
message: 'Email queue processed successfully (pending and failed emails)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('✅ Email queue processing completed successfully')
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error processing email queue:', error)
|
console.error('❌ Error processing email queue:', error)
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
|
||||||
// Properly fail the job by throwing the error
|
jobResult = new Error(`Email queue processing failed: ${errorMessage}`)
|
||||||
throw new Error(`Email queue processing failed: ${errorMessage}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always reschedule the next job (success or failure) using duplicate prevention
|
||||||
|
let rescheduled = false
|
||||||
|
try {
|
||||||
|
rescheduled = await scheduleEmailProcessingJob(payload, queueName, 300000) // Reschedule in 5 minutes
|
||||||
|
if (rescheduled) {
|
||||||
|
console.log(`🔄 Rescheduled next email processing job in ${queueName} queue`)
|
||||||
|
}
|
||||||
|
} catch (rescheduleError) {
|
||||||
|
console.error('❌ Failed to reschedule email processing job:', rescheduleError)
|
||||||
|
// If rescheduling fails, we should warn but not fail the current job
|
||||||
|
// since the email processing itself may have succeeded
|
||||||
|
console.warn('⚠️ Email processing completed but next job could not be scheduled')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the original result or throw the error
|
||||||
|
if (jobResult instanceof Error) {
|
||||||
|
throw jobResult
|
||||||
|
}
|
||||||
|
return jobResult
|
||||||
},
|
},
|
||||||
interfaceName: 'ProcessEmailQueueJob',
|
interfaceName: 'ProcessEmailQueueJob',
|
||||||
},
|
},
|
||||||
@@ -130,6 +207,13 @@ export const mailingPlugin = (pluginConfig: MailingPluginConfig) => (config: Con
|
|||||||
|
|
||||||
console.log('PayloadCMS Mailing Plugin initialized successfully')
|
console.log('PayloadCMS Mailing Plugin initialized successfully')
|
||||||
|
|
||||||
|
// Schedule the email processing job if not already scheduled
|
||||||
|
try {
|
||||||
|
await scheduleEmailProcessingJob(payload, queueName)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to schedule email processing job:', error)
|
||||||
|
}
|
||||||
|
|
||||||
// Call onReady callback if provided
|
// Call onReady callback if provided
|
||||||
if (pluginConfig.onReady) {
|
if (pluginConfig.onReady) {
|
||||||
await pluginConfig.onReady(payload)
|
await pluginConfig.onReady(payload)
|
||||||
|
|||||||
@@ -48,6 +48,20 @@ export class MailingService implements IMailingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDefaultFrom(): string {
|
||||||
|
const fromEmail = this.config.defaultFrom
|
||||||
|
const fromName = this.config.defaultFromName
|
||||||
|
|
||||||
|
// Check if fromName exists, is not empty after trimming, and fromEmail exists
|
||||||
|
if (fromName && fromName.trim() && fromEmail) {
|
||||||
|
// Escape quotes in the display name to prevent malformed headers
|
||||||
|
const escapedName = fromName.replace(/"/g, '\\"')
|
||||||
|
return `"${escapedName}" <${fromEmail}>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return fromEmail || ''
|
||||||
|
}
|
||||||
|
|
||||||
private registerHandlebarsHelpers(): void {
|
private registerHandlebarsHelpers(): void {
|
||||||
Handlebars.registerHelper('formatDate', (date: Date, format?: string) => {
|
Handlebars.registerHelper('formatDate', (date: Date, format?: string) => {
|
||||||
if (!date) return ''
|
if (!date) return ''
|
||||||
@@ -128,7 +142,7 @@ export class MailingService implements IMailingService {
|
|||||||
to: Array.isArray(options.to) ? options.to : [options.to],
|
to: Array.isArray(options.to) ? options.to : [options.to],
|
||||||
cc: options.cc ? (Array.isArray(options.cc) ? options.cc : [options.cc]) : undefined,
|
cc: options.cc ? (Array.isArray(options.cc) ? options.cc : [options.cc]) : undefined,
|
||||||
bcc: options.bcc ? (Array.isArray(options.bcc) ? options.bcc : [options.bcc]) : undefined,
|
bcc: options.bcc ? (Array.isArray(options.bcc) ? options.bcc : [options.bcc]) : undefined,
|
||||||
from: options.from || this.config.defaultFrom,
|
from: options.from || this.getDefaultFrom(),
|
||||||
replyTo: options.replyTo,
|
replyTo: options.replyTo,
|
||||||
subject: subject || options.subject,
|
subject: subject || options.subject,
|
||||||
html,
|
html,
|
||||||
@@ -245,7 +259,7 @@ export class MailingService implements IMailingService {
|
|||||||
}) as QueuedEmail
|
}) as QueuedEmail
|
||||||
|
|
||||||
let emailObject: EmailObject = {
|
let emailObject: EmailObject = {
|
||||||
from: email.from || this.config.defaultFrom,
|
from: email.from || this.getDefaultFrom(),
|
||||||
to: email.to,
|
to: email.to,
|
||||||
cc: email.cc || undefined,
|
cc: email.cc || undefined,
|
||||||
bcc: email.bcc || undefined,
|
bcc: email.bcc || undefined,
|
||||||
@@ -262,7 +276,7 @@ export class MailingService implements IMailingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mailOptions = {
|
const mailOptions = {
|
||||||
from: emailObject.from || this.config.defaultFrom,
|
from: emailObject.from,
|
||||||
to: emailObject.to,
|
to: emailObject.to,
|
||||||
cc: emailObject.cc || undefined,
|
cc: emailObject.cc || undefined,
|
||||||
bcc: emailObject.bcc || undefined,
|
bcc: emailObject.bcc || undefined,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface MailingPluginConfig {
|
|||||||
emails?: string | Partial<CollectionConfig>
|
emails?: string | Partial<CollectionConfig>
|
||||||
}
|
}
|
||||||
defaultFrom?: string
|
defaultFrom?: string
|
||||||
|
defaultFromName?: string
|
||||||
transport?: Transporter | MailingTransportConfig
|
transport?: Transporter | MailingTransportConfig
|
||||||
queue?: string
|
queue?: string
|
||||||
retryAttempts?: number
|
retryAttempts?: number
|
||||||
|
|||||||
Reference in New Issue
Block a user