Compare commits

..

15 Commits

Author SHA1 Message Date
Bas
0083e8e1fa Merge pull request #39 from xtr-dev/dev
Remove redundant queueName validation and debug log, bump version to …
2025-09-14 16:29:13 +02:00
63a7eef8d8 Remove redundant queueName validation and debug log, bump version to 0.1.24 2025-09-14 16:28:24 +02:00
Bas
6cf055178b Merge pull request #38 from xtr-dev/dev
Add debug log for email transporter configuration and bump version to…
2025-09-14 16:15:41 +02:00
aa978090fa Add debug log for email transporter configuration and bump version to 0.1.23 2025-09-14 16:14:56 +02:00
Bas
556d910e30 Merge pull request #37 from xtr-dev/dev
Remove conditional transporter initialization and bump version to 0.1.22
2025-09-14 13:53:32 +02:00
b4bad70634 Remove conditional transporter initialization and bump version to 0.1.22 2025-09-14 13:52:49 +02:00
Bas
efdfaf5889 Merge pull request #36 from xtr-dev/dev
Add beforeSend hook for email customization
2025-09-14 12:37:38 +02:00
ea7d8dfdd5 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
2025-09-14 12:27:43 +02:00
0d6d07de85 Add beforeSend hook for email customization
- Add BeforeSendHook type and BeforeSendMailOptions interface
- Implement hook execution in MailingService before sending emails
- Hook allows adding attachments, headers, and modifying email options
- Add comprehensive documentation with examples
- Bump version to 0.1.20
2025-09-14 12:19:52 +02:00
Bas
f12ac8172e Merge pull request #35 from xtr-dev/dev
Fix model overwrite error when plugin is initialized multiple times
2025-09-14 10:24:58 +02:00
347cd33e13 Fix model overwrite error when plugin is initialized multiple times
- Filter out existing collections with same slugs before adding plugin collections
- Prevents 'Cannot overwrite model once compiled' errors in Next.js apps
- Fixes issue during hot reload and multiple getPayload() calls
- Bump version to 0.1.19
2025-09-14 10:22:34 +02:00
Bas
672ab3236a Merge pull request #34 from xtr-dev/dev
Add fromName field support to emails collection
2025-09-14 00:10:22 +02:00
c7db65980a Fix security vulnerabilities in fromName field handling
- Add sanitizeDisplayName() method to prevent header injection attacks
- Remove newlines, carriage returns, and control characters from display names
- Fix quote escaping inconsistency between getDefaultFrom() and processEmailItem()
- Create formatEmailAddress() helper method for consistent email formatting
- Add fromName sanitization in sendEmail() function for input validation
- Prevent malformed email headers and potential security issues

Security improvements:
- Header injection prevention (removes \r\n and control characters)
- Consistent quote escaping across all display name usage
- Proper sanitization at both input and output stages

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-14 00:07:53 +02:00
624dc12471 Bump package version to 0.1.18 in package.json. 2025-09-14 00:06:14 +02:00
e20ebe27bf Add fromName field support to emails collection
- Add fromName field to Emails collection schema for sender display name
- Update BaseEmailDocument and QueuedEmail interfaces to include fromName
- Add SendEmailTaskInput support for fromName field in job tasks
- Update MailingService to combine fromName and from into proper "Name <email>" format
- Add fromName, from, and replyTo fields to job input schema for admin UI
- Update field copying logic to handle new sender-related fields

Users can now specify a display name for emails (e.g., "John Doe <john@example.com>").

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-14 00:03:04 +02:00
12 changed files with 338 additions and 119 deletions

View File

@@ -1,95 +0,0 @@
# Using Custom ID Types
The mailing plugin now supports both `string` and `number` ID types. By default, it works with the generic `BaseEmailDocument` interface, but you can provide your own types for full type safety.
## Usage with Your Generated Types
When you have your own generated Payload types (e.g., from `payload generate:types`), you can use them with the mailing plugin:
```typescript
import { sendEmail, BaseEmailDocument } from '@xtr-dev/payload-mailing'
import { Email } from './payload-types' // Your generated types
// Option 1: Use your specific Email type
const email = await sendEmail<Email>(payload, {
template: {
slug: 'welcome',
variables: { name: 'John' }
},
data: {
to: 'user@example.com',
// All your custom fields are now type-safe
}
})
// Option 2: Extend BaseEmailDocument for custom fields
interface MyEmail extends BaseEmailDocument {
customField: string
anotherField?: number
}
const customEmail = await sendEmail<MyEmail>(payload, {
data: {
to: 'user@example.com',
subject: 'Hello',
html: '<p>Hello World</p>',
customField: 'my value', // Type-safe!
}
})
```
## Compatibility
The plugin works with:
- **String IDs**: `id: string`
- **Number IDs**: `id: number`
- **Nullable fields**: Fields can be `null`, `undefined`, or have values
- **Date fields**: Timestamp fields support both `Date` objects and `string` (ISO) formats
- **JSON variables**: Variables field supports any JSON-compatible value type
- **Generated types**: Works with `payload generate:types` output
Your Payload configuration determines which types are used. The plugin automatically adapts to your setup.
## Type Definitions
The base interfaces provided by the plugin:
```typescript
// JSON value type that matches Payload's JSON field type
type JSONValue = string | number | boolean | { [k: string]: unknown } | unknown[] | null | undefined
interface BaseEmailDocument {
id: string | number
template?: any
to: string[]
cc?: string[] | null
bcc?: string[] | null
from?: string | null
replyTo?: string | null
subject: string
html: string
text?: string | null
variables?: JSONValue // Supports any JSON-compatible value
scheduledAt?: string | Date | null
sentAt?: string | Date | null
status?: 'pending' | 'processing' | 'sent' | 'failed' | null
attempts?: number | null
lastAttemptAt?: string | Date | null
error?: string | null
priority?: number | null
createdAt?: string | Date | null
updatedAt?: string | Date | null
}
interface BaseEmailTemplateDocument {
id: string | number
name: string
slug: string
subject?: string | null
content?: any
createdAt?: string | Date | null
updatedAt?: string | Date | null
}
```
These provide a foundation that works with any ID type while maintaining type safety for the core email functionality.

View File

@@ -142,6 +142,18 @@ mailingPlugin({
richTextEditor: lexicalEditor(), // optional custom editor richTextEditor: lexicalEditor(), // optional custom editor
onReady: async (payload) => { // optional initialization hook onReady: async (payload) => { // optional initialization hook
console.log('Mailing plugin ready!') 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 ### Initialization Hooks
Control plugin initialization order and add post-initialization logic: Control plugin initialization order and add post-initialization logic:
@@ -266,7 +328,7 @@ mailingPlugin({
onReady: async (payload) => { onReady: async (payload) => {
// Called after plugin is fully initialized // Called after plugin is fully initialized
console.log('Mailing plugin ready!') console.log('Mailing plugin ready!')
// Custom initialization logic here // Custom initialization logic here
await setupCustomEmailSettings(payload) await setupCustomEmailSettings(payload)
} }

View File

@@ -248,6 +248,10 @@ export interface Email {
* Sender email address (optional, uses default if not provided) * Sender email address (optional, uses default if not provided)
*/ */
from?: string | null; from?: string | null;
/**
* Sender display name (optional, e.g., "John Doe" for "John Doe <john@example.com>")
*/
fromName?: string | null;
/** /**
* Reply-to email address * Reply-to email address
*/ */
@@ -543,6 +547,7 @@ export interface EmailsSelect<T extends boolean = true> {
cc?: T; cc?: T;
bcc?: T; bcc?: T;
from?: T; from?: T;
fromName?: T;
replyTo?: T; replyTo?: T;
subject?: T; subject?: T;
html?: T; html?: T;
@@ -675,6 +680,18 @@ export interface TaskSendEmail {
* Optional comma-separated list of BCC email addresses * Optional comma-separated list of BCC email addresses
*/ */
bcc?: string | null; 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 * Optional date/time to schedule email for future delivery
*/ */
@@ -684,7 +701,9 @@ export interface TaskSendEmail {
*/ */
priority?: number | null; priority?: number | null;
}; };
output?: unknown; output: {
id?: string | null;
};
} }
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema

113
dev/test-hook-validation.ts Normal file
View File

@@ -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: '<p>Testing hook validation</p>',
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)

View File

@@ -1,6 +1,6 @@
{ {
"name": "@xtr-dev/payload-mailing", "name": "@xtr-dev/payload-mailing",
"version": "0.1.17", "version": "0.1.24",
"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",

View File

@@ -49,6 +49,13 @@ const Emails: CollectionConfig = {
description: 'Sender email address (optional, uses default if not provided)', description: 'Sender email address (optional, uses default if not provided)',
}, },
}, },
{
name: 'fromName',
type: 'text',
admin: {
description: 'Sender display name (optional, e.g., "John Doe" for "John Doe <john@example.com>")',
},
},
{ {
name: 'replyTo', name: 'replyTo',
type: 'text', type: 'text',

View File

@@ -15,6 +15,9 @@ export interface SendEmailTaskInput {
to: string | string[] to: string | string[]
cc?: string | string[] cc?: string | string[]
bcc?: string | string[] bcc?: string | string[]
from?: string
fromName?: string
replyTo?: string
scheduledAt?: string | Date // ISO date string or Date object scheduledAt?: string | Date // ISO date string or Date object
priority?: number priority?: number
@@ -39,7 +42,7 @@ function transformTaskInputToSendEmailOptions(taskInput: SendEmailTaskInput) {
} }
// Standard email fields that should be copied to data // Standard email fields that should be copied to data
const standardFields = ['to', 'cc', 'bcc', 'subject', 'html', 'text', 'scheduledAt', 'priority'] const standardFields = ['to', 'cc', 'bcc', 'from', 'fromName', 'replyTo', 'subject', 'html', 'text', 'scheduledAt', 'priority']
// Template-specific fields that should not be copied to data // Template-specific fields that should not be copied to data
const templateFields = ['templateSlug', 'variables'] const templateFields = ['templateSlug', 'variables']
@@ -135,6 +138,30 @@ export const sendEmailJob = {
description: 'Optional comma-separated list of BCC email addresses' description: 'Optional comma-separated list of BCC email addresses'
} }
}, },
{
name: 'from',
type: 'text' as const,
label: 'From Email',
admin: {
description: 'Optional sender email address (uses default if not provided)'
}
},
{
name: 'fromName',
type: 'text' as const,
label: 'From Name',
admin: {
description: 'Optional sender display name (e.g., "John Doe")'
}
},
{
name: 'replyTo',
type: 'text' as const,
label: 'Reply To',
admin: {
description: 'Optional reply-to email address'
}
},
{ {
name: 'scheduledAt', name: 'scheduledAt',
type: 'date' as const, type: 'date' as const,

View File

@@ -143,6 +143,10 @@ export interface Email {
* Sender email address (optional, uses default if not provided) * Sender email address (optional, uses default if not provided)
*/ */
from?: string | null; from?: string | null;
/**
* Sender display name (optional, e.g., "John Doe" for "John Doe <john@example.com>")
*/
fromName?: string | null;
/** /**
* Reply-to email address * Reply-to email address
*/ */
@@ -336,6 +340,7 @@ export interface EmailsSelect<T extends boolean = true> {
cc?: T; cc?: T;
bcc?: T; bcc?: T;
from?: T; from?: T;
fromName?: T;
replyTo?: T; replyTo?: T;
subject?: T; subject?: T;
html?: T; html?: T;

View File

@@ -9,12 +9,6 @@ import { mailingJobs, scheduleEmailsJob } from './jobs/index.js'
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'
@@ -74,10 +68,15 @@ export const mailingPlugin = (pluginConfig: MailingPluginConfig) => (config: Con
}), }),
} satisfies CollectionConfig } satisfies CollectionConfig
// Filter out any existing collections with the same slugs to prevent duplicates
const existingCollections = (config.collections || []).filter(
(collection) => collection.slug !== templatesSlug && collection.slug !== emailsSlug
)
return { return {
...config, ...config,
collections: [ collections: [
...(config.collections || []), ...existingCollections,
templatesCollection, templatesCollection,
emailsCollection, emailsCollection,
], ],

View File

@@ -83,23 +83,34 @@ export const sendEmail = async <TEmail extends BaseEmailDocument = BaseEmailDocu
if (emailData.to) { if (emailData.to) {
emailData.to = parseAndValidateEmails(emailData.to as string | string[]) emailData.to = parseAndValidateEmails(emailData.to as string | string[])
} }
if (emailData.cc && emailData.cc !== null) { if (emailData.cc) {
emailData.cc = parseAndValidateEmails(emailData.cc as string | string[]) emailData.cc = parseAndValidateEmails(emailData.cc as string | string[])
} }
if (emailData.bcc && emailData.bcc !== null) { if (emailData.bcc) {
emailData.bcc = parseAndValidateEmails(emailData.bcc as string | string[]) emailData.bcc = parseAndValidateEmails(emailData.bcc as string | string[])
} }
if (emailData.replyTo && emailData.replyTo !== null) { if (emailData.replyTo) {
const validated = parseAndValidateEmails(emailData.replyTo as string | string[]) const validated = parseAndValidateEmails(emailData.replyTo as string | string[])
// replyTo should be a single email, so take the first one if array // replyTo should be a single email, so take the first one if array
emailData.replyTo = validated && validated.length > 0 ? validated[0] : undefined emailData.replyTo = validated && validated.length > 0 ? validated[0] : undefined
} }
if (emailData.from && emailData.from !== null) { if (emailData.from) {
const validated = parseAndValidateEmails(emailData.from as string | string[]) const validated = parseAndValidateEmails(emailData.from as string | string[])
// from should be a single email, so take the first one if array // from should be a single email, so take the first one if array
emailData.from = validated && validated.length > 0 ? validated[0] : undefined emailData.from = validated && validated.length > 0 ? validated[0] : undefined
} }
// Sanitize fromName to prevent header injection
if (emailData.fromName) {
emailData.fromName = emailData.fromName
.trim()
// Remove/replace newlines and carriage returns to prevent header injection
.replace(/[\r\n]/g, ' ')
// Remove control characters (except space and printable characters)
.replace(/[\x00-\x1F\x7F-\x9F]/g, '')
// Note: We don't escape quotes here as that's handled in MailingService
}
// Normalize Date objects to ISO strings for consistent database storage // Normalize Date objects to ISO strings for consistent database storage
if (emailData.scheduledAt instanceof Date) { if (emailData.scheduledAt instanceof Date) {
emailData.scheduledAt = emailData.scheduledAt.toISOString() emailData.scheduledAt = emailData.scheduledAt.toISOString()

View File

@@ -29,10 +29,7 @@ export class MailingService implements IMailingService {
const emailsConfig = config.collections?.emails const emailsConfig = config.collections?.emails
this.emailsCollection = typeof emailsConfig === 'string' ? emailsConfig : 'emails' this.emailsCollection = typeof emailsConfig === 'string' ? emailsConfig : 'emails'
// Only initialize transporter if payload is properly set this.initializeTransporter()
if (payload && payload.db) {
this.initializeTransporter()
}
} }
private initializeTransporter(): void { private initializeTransporter(): void {
@@ -63,15 +60,39 @@ export class MailingService implements IMailingService {
} }
} }
/**
* Sanitizes a display name for use in email headers to prevent header injection
* and ensure proper formatting
*/
private sanitizeDisplayName(name: string): string {
return name
.trim()
// Remove/replace newlines and carriage returns to prevent header injection
.replace(/[\r\n]/g, ' ')
// Remove control characters (except space and printable characters)
.replace(/[\x00-\x1F\x7F-\x9F]/g, '')
// Escape quotes to prevent malformed headers
.replace(/"/g, '\\"')
}
/**
* Formats an email address with optional display name
*/
private formatEmailAddress(email: string, displayName?: string | null): string {
if (displayName && displayName.trim()) {
const sanitizedName = this.sanitizeDisplayName(displayName)
return `"${sanitizedName}" <${email}>`
}
return email
}
private getDefaultFrom(): string { private getDefaultFrom(): string {
const fromEmail = this.config.defaultFrom const fromEmail = this.config.defaultFrom
const fromName = this.config.defaultFromName const fromName = this.config.defaultFromName
// Check if fromName exists, is not empty after trimming, and fromEmail exists // Check if fromName exists, is not empty after trimming, and fromEmail exists
if (fromName && fromName.trim() && fromEmail) { if (fromName && fromName.trim() && fromEmail) {
// Escape quotes in the display name to prevent malformed headers return this.formatEmailAddress(fromEmail, fromName)
const escapedName = fromName.replace(/"/g, '\\"')
return `"${escapedName}" <${fromEmail}>`
} }
return fromEmail || '' return fromEmail || ''
@@ -238,8 +259,16 @@ export class MailingService implements IMailingService {
id: emailId, id: emailId,
}) as BaseEmailDocument }) as BaseEmailDocument
const mailOptions = { // Combine from and fromName for nodemailer using proper sanitization
from: email.from, let fromField: string
if (email.from) {
fromField = this.formatEmailAddress(email.from, email.fromName)
} else {
fromField = this.getDefaultFrom()
}
let mailOptions: any = {
from: fromField,
to: email.to, to: email.to,
cc: email.cc || undefined, cc: email.cc || undefined,
bcc: email.bcc || undefined, bcc: email.bcc || undefined,
@@ -249,6 +278,30 @@ export class MailingService implements IMailingService {
text: email.text || undefined, text: email.text || undefined,
} }
// Call beforeSend hook if configured
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'}`)
}
}
await this.transporter.sendMail(mailOptions) await this.transporter.sendMail(mailOptions)
await this.payload.update({ await this.payload.update({

View File

@@ -13,6 +13,7 @@ export interface BaseEmailDocument {
cc?: string[] | null cc?: string[] | null
bcc?: string[] | null bcc?: string[] | null
from?: string | null from?: string | null
fromName?: string | null
replyTo?: string | null replyTo?: string | null
subject: string subject: string
html: string html: string
@@ -47,6 +48,21 @@ export type TemplateRendererHook = (template: string, variables: Record<string,
export type TemplateEngine = 'liquidjs' | 'mustache' | 'simple' export type TemplateEngine = 'liquidjs' | 'mustache' | 'simple'
export interface BeforeSendMailOptions {
from: string
to: string[]
cc?: string[]
bcc?: string[]
replyTo?: string
subject: string
html: string
text?: string
attachments?: any[]
[key: string]: any
}
export type BeforeSendHook = (options: BeforeSendMailOptions, email: BaseEmailDocument) => BeforeSendMailOptions | Promise<BeforeSendMailOptions>
export interface MailingPluginConfig { export interface MailingPluginConfig {
collections?: { collections?: {
templates?: string | Partial<CollectionConfig> templates?: string | Partial<CollectionConfig>
@@ -61,6 +77,7 @@ export interface MailingPluginConfig {
templateRenderer?: TemplateRendererHook templateRenderer?: TemplateRendererHook
templateEngine?: TemplateEngine templateEngine?: TemplateEngine
richTextEditor?: RichTextField['editor'] richTextEditor?: RichTextField['editor']
beforeSend?: BeforeSendHook
onReady?: (payload: any) => Promise<void> onReady?: (payload: any) => Promise<void>
initOrder?: 'before' | 'after' initOrder?: 'before' | 'after'
} }
@@ -83,6 +100,7 @@ export interface QueuedEmail {
cc?: string[] | null cc?: string[] | null
bcc?: string[] | null bcc?: string[] | null
from?: string | null from?: string | null
fromName?: string | null
replyTo?: string | null replyTo?: string | null
subject: string subject: string
html: string html: string