diff --git a/README.md b/README.md
index 8dc2225..4a1b760 100644
--- a/README.md
+++ b/README.md
@@ -359,14 +359,127 @@ EMAIL_PASS=your-app-password
EMAIL_FROM=noreply@yoursite.com
```
+## Security and Access Control
+
+### Collection Access Restrictions
+
+By default, both email templates and emails collections allow full access (`read/create/update/delete: () => true`). For production use, you should configure proper access restrictions using collection overrides:
+
+```typescript
+mailingPlugin({
+ // ... other config
+ collections: {
+ 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'
+ },
+ }
+ },
+ emails: {
+ 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'
+ },
+ }
+ }
+ }
+})
+```
+
+### Collection Overrides
+
+You can override any collection configuration using the `collections.templates` or `collections.emails` options. This includes:
+
+- **Access controls** - Restrict who can read/create/update/delete
+- **Admin UI settings** - Customize admin interface appearance
+- **Field modifications** - Add custom fields or modify existing ones
+- **Hooks** - Add custom validation or processing logic
+
+Example with additional custom fields:
+
+```typescript
+mailingPlugin({
+ // ... other config
+ collections: {
+ templates: {
+ admin: {
+ group: 'Custom Marketing',
+ description: 'Custom email templates with enhanced features'
+ },
+ fields: [
+ // Plugin's default fields are preserved
+ {
+ name: 'category',
+ type: 'select',
+ options: [
+ { label: 'Marketing', value: 'marketing' },
+ { label: 'Transactional', value: 'transactional' },
+ { label: 'System', value: 'system' }
+ ],
+ admin: {
+ position: 'sidebar'
+ }
+ },
+ {
+ name: 'tags',
+ type: 'text',
+ hasMany: true,
+ admin: {
+ description: 'Tags for organizing templates'
+ }
+ }
+ ],
+ hooks: {
+ beforeChange: [
+ ({ data, req }) => {
+ // Custom validation logic
+ if (data.category === 'system' && req.user?.role !== 'admin') {
+ throw new Error('Only admins can create system templates')
+ }
+ return data
+ }
+ ]
+ }
+ }
+ }
+})
+```
+
## TypeScript Support
The plugin includes full TypeScript definitions. Import types as needed:
```typescript
-import {
- MailingPluginConfig,
- SendEmailOptions,
+import {
+ MailingPluginConfig,
+ SendEmailOptions,
EmailTemplate,
QueuedEmail,
EmailObject,
diff --git a/dev/payload.config.ts b/dev/payload.config.ts
index 57ac215..8e96282 100644
--- a/dev/payload.config.ts
+++ b/dev/payload.config.ts
@@ -146,6 +146,123 @@ const buildConfigWithMemoryDB = async () => {
retryDelay: 60000, // 1 minute for dev
queue: 'email-queue',
+ // 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({
diff --git a/src/plugin.ts b/src/plugin.ts
index 085fe9b..8b94155 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -52,7 +52,10 @@ export const mailingPlugin = (pluginConfig: MailingPluginConfig) => (config: Con
},
// Update relationship fields to point to correct templates collection
fields: (emailsOverrides.fields || Emails.fields).map((field: any) => {
- if (field.name === 'template' && field.type === 'relationship') {
+ if (field &&
+ typeof field === 'object' &&
+ field.name === 'template' &&
+ field.type === 'relationship') {
return {
...field,
relationTo: templatesSlug,
@@ -95,12 +98,10 @@ export const mailingPlugin = (pluginConfig: MailingPluginConfig) => (config: Con
}
} catch (error) {
console.error('❌ Error processing email queue:', error)
- return {
- output: {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error'
- }
- }
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
+
+ // Properly fail the job by throwing the error
+ throw new Error(`Email queue processing failed: ${errorMessage}`)
}
},
interfaceName: 'ProcessEmailQueueJob',
diff --git a/src/utils/richTextSerializer.ts b/src/utils/richTextSerializer.ts
index f8a0391..b98dcf4 100644
--- a/src/utils/richTextSerializer.ts
+++ b/src/utils/richTextSerializer.ts
@@ -1,6 +1,20 @@
-// Using any type for now since Lexical types have import issues
-// import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
-type SerializedEditorState = any
+// Proper type definitions for Lexical serialization
+interface SerializedEditorState {
+ root: {
+ children: SerializedLexicalNode[]
+ }
+}
+
+interface SerializedLexicalNode {
+ type: string
+ children?: SerializedLexicalNode[]
+ text?: string
+ format?: number
+ tag?: string
+ listType?: 'number' | 'bullet'
+ url?: string
+ newTab?: boolean
+}
/**
* Converts Lexical richtext content to HTML
@@ -24,11 +38,11 @@ export function serializeRichTextToText(richTextData: SerializedEditorState): st
return serializeNodesToText(richTextData.root.children)
}
-function serializeNodesToHTML(nodes: any[]): string {
+function serializeNodesToHTML(nodes: SerializedLexicalNode[]): string {
return nodes.map(node => serializeNodeToHTML(node)).join('')
}
-function serializeNodeToHTML(node: any): string {
+function serializeNodeToHTML(node: SerializedLexicalNode): string {
if (!node) return ''
switch (node.type) {
@@ -43,16 +57,25 @@ function serializeNodeToHTML(node: any): string {
case 'text':
let text = node.text || ''
-
- // Apply text formatting
+
+ // Apply text formatting using proper nesting order
if (node.format) {
- if (node.format & 1) text = `${text}` // Bold
- if (node.format & 2) text = `${text}` // Italic
- if (node.format & 4) text = `${text}` // Strikethrough
- if (node.format & 8) text = `${text}` // Underline
- if (node.format & 16) text = `${text}` // Code
+ const formatFlags = {
+ bold: (node.format & 1) !== 0,
+ italic: (node.format & 2) !== 0,
+ strikethrough: (node.format & 4) !== 0,
+ underline: (node.format & 8) !== 0,
+ code: (node.format & 16) !== 0,
+ }
+
+ // Apply formatting in proper nesting order: code > bold > italic > underline > strikethrough
+ if (formatFlags.code) text = `${text}`
+ if (formatFlags.bold) text = `${text}`
+ if (formatFlags.italic) text = `${text}`
+ if (formatFlags.underline) text = `${text}`
+ if (formatFlags.strikethrough) text = `${text}`
}
-
+
return text
case 'linebreak':
@@ -89,11 +112,11 @@ function serializeNodeToHTML(node: any): string {
}
}
-function serializeNodesToText(nodes: any[]): string {
+function serializeNodesToText(nodes: SerializedLexicalNode[]): string {
return nodes.map(node => serializeNodeToText(node)).join('')
}
-function serializeNodeToText(node: any): string {
+function serializeNodeToText(node: SerializedLexicalNode): string {
if (!node) return ''
switch (node.type) {