diff --git a/README.md b/README.md index 3f01fe3..9540532 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,18 @@ mailingPlugin({ richTextEditor: lexicalEditor(), // optional custom editor onReady: async (payload) => { // optional initialization hook console.log('Mailing plugin ready!') + }, + + // beforeSend hook - modify emails before sending + beforeSend: async (options, email) => { + // Add attachments, modify headers, etc. + options.attachments = [ + { filename: 'invoice.pdf', content: pdfBuffer } + ] + options.headers = { + 'X-Campaign-ID': email.campaignId + } + return options } }) ``` @@ -255,6 +267,56 @@ mailingPlugin({ }) ``` +### beforeSend Hook + +Modify emails before they are sent to add attachments, custom headers, or make other changes: + +```typescript +mailingPlugin({ + // ... other config + beforeSend: async (options, email) => { + // Add attachments dynamically + if (email.invoiceId) { + const invoice = await generateInvoicePDF(email.invoiceId) + options.attachments = [ + { + filename: `invoice-${email.invoiceId}.pdf`, + content: invoice.buffer, + contentType: 'application/pdf' + } + ] + } + + // Add custom headers + options.headers = { + 'X-Campaign-ID': email.campaignId, + 'X-Customer-ID': email.customerId, + 'X-Priority': email.priority === 1 ? 'High' : 'Normal' + } + + // Modify recipients based on conditions + if (process.env.NODE_ENV === 'development') { + // Redirect all emails to test address in dev + options.to = ['test@example.com'] + options.subject = `[TEST] ${options.subject}` + } + + // Add BCC for compliance + if (email.requiresAudit) { + options.bcc = ['audit@company.com'] + } + + return options + } +}) +``` + +The `beforeSend` hook receives: +- `options`: The nodemailer mail options that will be sent +- `email`: The full email document from the database + +You must return the modified options object. + ### Initialization Hooks Control plugin initialization order and add post-initialization logic: @@ -266,7 +328,7 @@ mailingPlugin({ onReady: async (payload) => { // Called after plugin is fully initialized console.log('Mailing plugin ready!') - + // Custom initialization logic here await setupCustomEmailSettings(payload) } diff --git a/package.json b/package.json index 8448d49..2d3485e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/payload-mailing", - "version": "0.1.19", + "version": "0.1.20", "description": "Template-based email system with scheduling and job processing for PayloadCMS", "type": "module", "main": "dist/index.js", diff --git a/src/sendEmail.ts b/src/sendEmail.ts index 12862a4..f7ed142 100644 --- a/src/sendEmail.ts +++ b/src/sendEmail.ts @@ -83,25 +83,25 @@ export const sendEmail = async 0 ? validated[0] : undefined } - if (emailData.from && emailData.from !== null) { + 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 - if (emailData.fromName && emailData.fromName !== null) { + if (emailData.fromName) { emailData.fromName = emailData.fromName .trim() // Remove/replace newlines and carriage returns to prevent header injection diff --git a/src/services/MailingService.ts b/src/services/MailingService.ts index b88b9a6..e3eb792 100644 --- a/src/services/MailingService.ts +++ b/src/services/MailingService.ts @@ -270,7 +270,7 @@ export class MailingService implements IMailingService { fromField = this.getDefaultFrom() } - const mailOptions = { + let mailOptions: any = { from: fromField, to: email.to, cc: email.cc || undefined, @@ -281,6 +281,16 @@ export class MailingService implements IMailingService { text: email.text || undefined, } + // Call beforeSend hook if configured + if (this.config.beforeSend) { + try { + mailOptions = await this.config.beforeSend(mailOptions, email) + } catch (error) { + console.error('Error in beforeSend hook:', error) + throw new Error(`beforeSend hook failed: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + } + await this.transporter.sendMail(mailOptions) await this.payload.update({ diff --git a/src/types/index.ts b/src/types/index.ts index 88e44cb..8944721 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -48,6 +48,21 @@ export type TemplateRendererHook = (template: string, variables: Record BeforeSendMailOptions | Promise + export interface MailingPluginConfig { collections?: { templates?: string | Partial @@ -62,6 +77,7 @@ export interface MailingPluginConfig { templateRenderer?: TemplateRendererHook templateEngine?: TemplateEngine richTextEditor?: RichTextField['editor'] + beforeSend?: BeforeSendHook onReady?: (payload: any) => Promise initOrder?: 'before' | 'after' }