2025-09-13 14:38:07 +02:00

@xtr-dev/payload-mailing

📧 Template-based email system with scheduling and job processing for PayloadCMS

⚠️ Pre-release Warning: This package is currently in active development (v0.0.x). Breaking changes may occur before v1.0.0. Not recommended for production use.

Features

Template System: Create reusable email templates with Handlebars syntax
Outbox Scheduling: Schedule emails for future delivery
Job Integration: Automatic processing via PayloadCMS jobs queue
Retry Failed Sends: Automatic retry mechanism for failed emails
Template Variables: Dynamic content with validation
Developer API: Simple methods for sending emails programmatically

Installation

npm install @xtr-dev/payload-mailing

Quick Start

1. Add the plugin to your Payload config

import { buildConfig } from 'payload/config'
import { mailingPlugin } from '@xtr-dev/payload-mailing'

export default buildConfig({
  // ... your config
  plugins: [
    mailingPlugin({
      defaultFrom: 'noreply@yoursite.com',
      transport: {
        host: 'smtp.gmail.com',
        port: 587,
        secure: false,
        auth: {
          user: process.env.EMAIL_USER,
          pass: process.env.EMAIL_PASS,
        },
      },
      retryAttempts: 3,
      retryDelay: 300000, // 5 minutes
      queue: 'email-queue', // optional
    }),
  ],
})

2. Send emails in your code

import { sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing'

// Send immediately using template slug
const emailId = await sendEmail(payload, {
  templateSlug: 'welcome-email',
  to: 'user@example.com',
  variables: {
    firstName: 'John',
    welcomeUrl: 'https://yoursite.com/welcome'
  }
})

// Schedule for later
const scheduledId = await scheduleEmail(payload, {
  templateSlug: 'reminder-email',
  to: 'user@example.com',
  scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
  variables: {
    eventName: 'Product Launch',
    eventDate: new Date('2024-01-15')
  }
})

Configuration

Plugin Options

interface MailingPluginConfig {
  collections?: {
    templates?: string    // default: 'email-templates'
    emails?: string      // default: 'emails'
  }
  defaultFrom?: string
  transport?: Transporter | MailingTransportConfig
  queue?: string         // default: 'default'
  retryAttempts?: number // default: 3
  retryDelay?: number    // default: 300000 (5 minutes)
  emailWrapper?: EmailWrapperHook // optional email layout wrapper
  richTextEditor?: RichTextField['editor'] // optional custom rich text editor
  onReady?: (payload: any) => Promise<void> // optional callback after plugin initialization
  initOrder?: 'before' | 'after' // default: 'before'
}

Transport Configuration

You can provide either a Nodemailer transporter instance or configuration:

// Using configuration object
{
  transport: {
    host: 'smtp.gmail.com',
    port: 587,
    secure: false,
    auth: {
      user: process.env.EMAIL_USER,
      pass: process.env.EMAIL_PASS,
    },
  }
}

// Or using a transporter instance
import nodemailer from 'nodemailer'
{
  transport: nodemailer.createTransporter({
    // your config
  })
}

Creating Email Templates

  1. Go to your Payload admin panel
  2. Navigate to Mailing > Email Templates
  3. Create a new template with:
    • Name: Descriptive name for the template
    • Slug: Unique identifier for the template (auto-generated)
    • Subject: Email subject (supports Handlebars)
    • Content: Rich text editor with Handlebars syntax (automatically generates HTML and text versions)

Template Example

Subject: Welcome to {{siteName}}, {{firstName}}!

Content (using rich text editor with Handlebars):

# Welcome {{firstName}}! 🎉

Thanks for joining {{siteName}}. We're excited to have you!

**What you can do:**
• Create beautiful emails with rich text formatting
• Use the emailWrapper hook to add custom layouts  
• Queue and schedule emails effortlessly

Your account was created on {{formatDate createdAt "long"}}.

Best regards,
The {{siteName}} Team

Advanced Features

Email Wrapper Hook

Use the emailWrapper hook to apply consistent layouts to all emails:

mailingPlugin({
  // ... other config
  emailWrapper: (email) => {
    const wrappedHtml = `
      <!DOCTYPE html>
      <html>
      <head>
        <meta charset="utf-8">
        <title>${email.subject}</title>
        <style>
          body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
          .container { max-width: 600px; margin: 0 auto; background: white; }
          .header { background: #007bff; color: white; padding: 20px; }
          .content { padding: 30px; }
          .footer { background: #f8f9fa; padding: 15px; text-align: center; }
        </style>
      </head>
      <body>
        <div class="container">
          <div class="header">
            <h1>My Company</h1>
          </div>
          <div class="content">
            ${email.html}
          </div>
          <div class="footer">
            © 2024 My Company. All rights reserved.
          </div>
        </div>
      </body>
      </html>
    `

    return {
      ...email,
      html: wrappedHtml,
      text: `MY COMPANY\n\n${email.text}\n\n© 2024 My Company`
    }
  }
})

Custom Rich Text Editor

Override the rich text editor used for templates:

import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { FixedToolbarFeature, HeadingFeature } from '@payloadcms/richtext-lexical'

mailingPlugin({
  // ... other config  
  richTextEditor: lexicalEditor({
    features: ({ defaultFeatures }) => [
      ...defaultFeatures,
      FixedToolbarFeature(),
      HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3'] }),
      // Add more features as needed
    ],
  })
})

Initialization Hooks

Control plugin initialization order and add post-initialization logic:

mailingPlugin({
  // ... other config
  initOrder: 'after', // Initialize after main Payload onInit
  onReady: async (payload) => {
    // Called after plugin is fully initialized
    console.log('Mailing plugin ready!')
    
    // Custom initialization logic here
    await setupCustomEmailSettings(payload)
  }
})

Handlebars Helpers

The plugin includes several built-in helpers:

  • {{formatDate date 'short'}} - Format dates (short, long, or default)
  • {{formatCurrency amount 'USD'}} - Format currency
  • {{capitalize string}} - Capitalize first letter
  • {{#ifEquals value1 value2}}...{{/ifEquals}} - Conditional equality

API Methods

sendEmail(payload, options)

Send an email immediately:

const emailId = await sendEmail(payload, {
  templateSlug: 'order-confirmation', // optional - use template slug
  to: ['customer@example.com'],       // string or array of emails
  cc: ['manager@example.com'],        // optional - array of emails
  bcc: ['archive@example.com'],       // optional - array of emails  
  from: 'orders@yoursite.com',        // optional, uses default
  replyTo: 'support@yoursite.com',    // optional
  subject: 'Custom subject',          // required if no template
  html: '<h1>Custom HTML</h1>',       // required if no template
  text: 'Custom text version',        // optional
  variables: {                        // template variables
    orderNumber: '12345',
    customerName: 'John Doe'
  },
  priority: 1                         // optional, 1-10 (1 = highest)
})

scheduleEmail(payload, options)

Schedule an email for later delivery:

const emailId = await scheduleEmail(payload, {
  templateSlug: 'newsletter',
  to: ['user1@example.com', 'user2@example.com'],
  scheduledAt: new Date('2024-01-15T10:00:00Z'),
  variables: {
    month: 'January',
    highlights: ['Feature A', 'Feature B']
  }
})

processEmails(payload)

Manually process pending emails:

import { processEmails } from '@xtr-dev/payload-mailing'
await processEmails(payload)

retryFailedEmails(payload)

Manually retry failed emails:

import { retryFailedEmails } from '@xtr-dev/payload-mailing'
await retryFailedEmails(payload)

Job Processing

The plugin automatically adds a unified email processing job to PayloadCMS:

  • Job Name: process-email-queue
  • Function: Processes both pending emails and retries failed emails
  • Trigger: Manual via admin panel or API call

The job is automatically registered when the plugin initializes. To trigger it manually:

// Queue the job for processing
await payload.jobs.queue({
  task: 'process-email-queue',
  input: {}
})

Email Status Tracking

All emails are stored in the emails collection with these statuses:

  • pending - Waiting to be sent
  • processing - Currently being sent
  • sent - Successfully delivered
  • failed - Failed to send (will retry if attempts < retryAttempts)

Monitoring

Check the Mailing > Emails collection in your admin panel to:

  • View email delivery status
  • See error messages for failed sends
  • Track retry attempts
  • Monitor scheduled emails

Environment Variables

# Email configuration
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=your-email@gmail.com
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:

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:

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:

import {
  MailingPluginConfig,
  SendEmailOptions,
  EmailTemplate,
  QueuedEmail,
  EmailObject,
  EmailWrapperHook
} from '@xtr-dev/payload-mailing'

Recent Changes

v0.0.x (Latest)

🔄 Breaking Changes:

  • Removed email layouts system in favor of emailWrapper hook for better flexibility
  • Email fields (to, cc, bcc) now use hasMany: true for proper array handling
  • Templates now use slug-based lookup instead of ID-based for developer-friendly API
  • Email collection renamed from "outbox" to "emails"
  • Unified job processing: single process-email-queue job handles both pending and failed emails

New Features:

  • Rich text editor with automatic HTML/text conversion
  • Template slugs for easier template reference
  • emailWrapper hook for consistent email layouts
  • Custom rich text editor configuration support
  • Initialization hooks (onReady, initOrder) for better plugin lifecycle control
  • Improved Handlebars variable interpolation with defensive programming

🐛 Bug Fixes:

  • Fixed text version uppercase conversion in headings
  • Fixed Handlebars interpolation issues in text version
  • Improved plugin initialization order to prevent timing issues

💡 Improvements:

  • Better admin UI with proper array input controls
  • More robust error handling and logging
  • Enhanced TypeScript definitions
  • Simplified template creation workflow

License

MIT

Contributing

Issues and pull requests welcome at GitHub repository

Description
No description provided
Readme 599 KiB
Languages
TypeScript 96.8%
JavaScript 3.2%