From ea7d8dfdd5abc07d507ad5a390843614855f03c6 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sun, 14 Sep 2025 12:27:43 +0200 Subject: [PATCH] Add validation for beforeSend hook to ensure required properties remain intact - Validate that 'from' field is not removed - Validate that 'to' field is not removed or emptied - Validate that 'subject' field is not removed - Validate that at least 'html' or 'text' content exists - Throw clear error messages if validation fails - Bump version to 0.1.21 --- dev/payload-types.ts | 21 +++++- dev/test-hook-validation.ts | 113 +++++++++++++++++++++++++++++++++ package.json | 2 +- src/services/MailingService.ts | 14 ++++ 4 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 dev/test-hook-validation.ts diff --git a/dev/payload-types.ts b/dev/payload-types.ts index 6c034a3..f40e95b 100644 --- a/dev/payload-types.ts +++ b/dev/payload-types.ts @@ -248,6 +248,10 @@ export interface Email { * Sender email address (optional, uses default if not provided) */ from?: string | null; + /** + * Sender display name (optional, e.g., "John Doe" for "John Doe ") + */ + fromName?: string | null; /** * Reply-to email address */ @@ -543,6 +547,7 @@ export interface EmailsSelect { cc?: T; bcc?: T; from?: T; + fromName?: T; replyTo?: T; subject?: T; html?: T; @@ -675,6 +680,18 @@ export interface TaskSendEmail { * Optional comma-separated list of BCC email addresses */ bcc?: string | null; + /** + * Optional sender email address (uses default if not provided) + */ + from?: string | null; + /** + * Optional sender display name (e.g., "John Doe") + */ + fromName?: string | null; + /** + * Optional reply-to email address + */ + replyTo?: string | null; /** * Optional date/time to schedule email for future delivery */ @@ -684,7 +701,9 @@ export interface TaskSendEmail { */ priority?: number | null; }; - output?: unknown; + output: { + id?: string | null; + }; } /** * This interface was referenced by `Config`'s JSON-Schema diff --git a/dev/test-hook-validation.ts b/dev/test-hook-validation.ts new file mode 100644 index 0000000..9179d7f --- /dev/null +++ b/dev/test-hook-validation.ts @@ -0,0 +1,113 @@ +// Test hook validation in the dev environment +import { getPayload } from 'payload' +import config from './payload.config.js' + +async function testHookValidation() { + const payload = await getPayload({ config: await config }) + + console.log('\n๐Ÿงช Testing beforeSend hook validation...\n') + + // Test 1: Create an email to process + const email = await payload.create({ + collection: 'emails', + data: { + to: ['test@example.com'], + subject: 'Test Email for Validation', + html: '

Testing hook validation

', + text: 'Testing hook validation', + status: 'pending' + } + }) + + console.log('โœ… Test email created:', email.id) + + // Get the mailing service + const mailingService = (payload as any).mailing.service + + // Test 2: Temporarily replace the config with a bad hook + const originalBeforeSend = mailingService.config.beforeSend + + console.log('\n๐Ÿ“ Test: Hook that removes "from" field...') + mailingService.config.beforeSend = async (options: any, email: any) => { + delete options.from + return options + } + + try { + await mailingService.processEmails() + console.log('โŒ Should have thrown error for missing "from"') + } catch (error: any) { + if (error.message.includes('must not remove the "from" property')) { + console.log('โœ… Correctly caught missing "from" field') + } else { + console.log('โŒ Unexpected error:', error.message) + } + } + + console.log('\n๐Ÿ“ Test: Hook that empties "to" array...') + mailingService.config.beforeSend = async (options: any, email: any) => { + options.to = [] + return options + } + + try { + await mailingService.processEmails() + console.log('โŒ Should have thrown error for empty "to"') + } catch (error: any) { + if (error.message.includes('must not remove or empty the "to" property')) { + console.log('โœ… Correctly caught empty "to" array') + } else { + console.log('โŒ Unexpected error:', error.message) + } + } + + console.log('\n๐Ÿ“ Test: Hook that removes "subject"...') + mailingService.config.beforeSend = async (options: any, email: any) => { + delete options.subject + return options + } + + try { + await mailingService.processEmails() + console.log('โŒ Should have thrown error for missing "subject"') + } catch (error: any) { + if (error.message.includes('must not remove the "subject" property')) { + console.log('โœ… Correctly caught missing "subject" field') + } else { + console.log('โŒ Unexpected error:', error.message) + } + } + + console.log('\n๐Ÿ“ Test: Hook that removes both "html" and "text"...') + mailingService.config.beforeSend = async (options: any, email: any) => { + delete options.html + delete options.text + return options + } + + try { + await mailingService.processEmails() + console.log('โŒ Should have thrown error for missing content') + } catch (error: any) { + if (error.message.includes('must not remove both "html" and "text" properties')) { + console.log('โœ… Correctly caught missing content fields') + } else { + console.log('โŒ Unexpected error:', error.message) + } + } + + // Restore original hook + mailingService.config.beforeSend = originalBeforeSend + + console.log('\nโœ… All validation tests completed!\n') + + // Clean up + await payload.delete({ + collection: 'emails', + id: email.id + }) + + process.exit(0) +} + +testHookValidation().catch(console.error) \ No newline at end of file diff --git a/package.json b/package.json index 2d3485e..d8ab084 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/payload-mailing", - "version": "0.1.20", + "version": "0.1.21", "description": "Template-based email system with scheduling and job processing for PayloadCMS", "type": "module", "main": "dist/index.js", diff --git a/src/services/MailingService.ts b/src/services/MailingService.ts index e3eb792..f71aabf 100644 --- a/src/services/MailingService.ts +++ b/src/services/MailingService.ts @@ -285,6 +285,20 @@ export class MailingService implements IMailingService { if (this.config.beforeSend) { try { mailOptions = await this.config.beforeSend(mailOptions, email) + + // Validate required properties remain intact after hook execution + if (!mailOptions.from) { + throw new Error('beforeSend hook must not remove the "from" property') + } + if (!mailOptions.to || (Array.isArray(mailOptions.to) && mailOptions.to.length === 0)) { + throw new Error('beforeSend hook must not remove or empty the "to" property') + } + if (!mailOptions.subject) { + throw new Error('beforeSend hook must not remove the "subject" property') + } + if (!mailOptions.html && !mailOptions.text) { + throw new Error('beforeSend hook must not remove both "html" and "text" properties') + } } catch (error) { console.error('Error in beforeSend hook:', error) throw new Error(`beforeSend hook failed: ${error instanceof Error ? error.message : 'Unknown error'}`)