# @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 LiquidJS, Mustache, or simple variables ✅ **Type Safety**: Full TypeScript support using your generated Payload types ✅ **Flexible Template Engines**: LiquidJS, Mustache, or bring your own template renderer ✅ **Email Scheduling**: Schedule emails for future delivery using Payload collections ✅ **Job Integration**: Automatic processing via PayloadCMS jobs queue ✅ **Retry Failed Sends**: Automatic retry mechanism for failed emails ✅ **Payload Native**: Uses Payload collections directly - no custom APIs to learn ## Installation ```bash npm install @xtr-dev/payload-mailing ``` ## Quick Start ### 1. Configure email in your Payload config and add the plugin ```typescript import { buildConfig } from 'payload/config' import { mailingPlugin } from '@xtr-dev/payload-mailing' import { nodemailerAdapter } from '@payloadcms/email-nodemailer' export default buildConfig({ // ... your config email: nodemailerAdapter({ defaultFromAddress: 'noreply@yoursite.com', defaultFromName: 'Your Site', transport: { host: 'smtp.gmail.com', port: 587, auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASS, }, }, }), plugins: [ mailingPlugin({ defaultFrom: 'noreply@yoursite.com', defaultFromName: 'Your Site Name', retryAttempts: 3, retryDelay: 300000, // 5 minutes queue: 'email-queue', // optional }), ], }) ``` ### 2. Send emails with type-safe helper ```typescript // sendEmail is a primary export for easy access import { sendEmail } from '@xtr-dev/payload-mailing' import { Email } from './payload-types' // Your generated types // Option 1: Using templates with full type safety const email = await sendEmail(payload, { template: { slug: 'welcome-email', variables: { firstName: 'John', welcomeUrl: 'https://yoursite.com/welcome' } }, data: { to: 'user@example.com', // Schedule for later (optional) scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000), priority: 1, // Your custom fields are type-safe! customField: 'your-value', } }) // Option 2: Direct HTML email (no template) const directEmail = await sendEmail(payload, { data: { to: ['user@example.com', 'another@example.com'], subject: 'Welcome!', html: '

Welcome John!

Thanks for joining!

', text: 'Welcome John! Thanks for joining!', // All your custom fields work with TypeScript autocomplete! customField: 'value', } }) // Option 3: Use payload.create() directly for full control const manualEmail = await payload.create({ collection: 'emails', data: { to: ['user@example.com'], subject: 'Hello', html: '

Hello World

', } }) ``` ## Configuration ### Plugin Options ```typescript mailingPlugin({ // Template engine (optional) templateEngine: 'liquidjs', // 'liquidjs' | 'mustache' | 'simple' // Custom template renderer (optional) templateRenderer: async (template: string, variables: Record) => { return yourCustomEngine.render(template, variables) }, // Collection names (optional) collections: { templates: 'email-templates', // default emails: 'emails' // default }, // Sending options defaultFrom: 'noreply@yoursite.com', defaultFromName: 'Your Site', retryAttempts: 3, // default retryDelay: 300000, // 5 minutes (default) // Advanced options 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 } }) ``` ### Template Engine Options Choose your preferred template engine: ```typescript // LiquidJS (default) - Modern syntax with logic mailingPlugin({ templateEngine: 'liquidjs' // {% if user.isPremium %}Premium!{% endif %} }) // Mustache - Logic-less templates mailingPlugin({ templateEngine: 'mustache' // {{#user.isPremium}}Premium!{{/user.isPremium}} }) // Simple variable replacement mailingPlugin({ templateEngine: 'simple' // Just {{variable}} replacement }) // Custom template renderer mailingPlugin({ templateRenderer: async (template, variables) => { return handlebars.compile(template)(variables) // Bring your own! } }) ``` ### Transport Configuration You can provide either a Nodemailer transporter instance or configuration: ```typescript // 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 • Queue and schedule emails effortlessly Your account was created on {{formatDate createdAt "long"}}. Best regards, The {{siteName}} Team ``` ## Advanced Features ### Custom Rich Text Editor Override the rich text editor used for templates: ```typescript 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 ], }) }) ``` ### 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: ```typescript 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) } }) ``` ## Template Syntax Reference Depending on your chosen template engine, you can use different syntax: ### LiquidJS (Default) - Variables: `{{ user.name }}` - Logic: `{% if user.isPremium %}Premium content{% endif %}` - Loops: `{% for item in items %}{{ item.name }}{% endfor %}` - Filters: `{{ amount | formatCurrency }}`, `{{ date | formatDate: "short" }}` ### Mustache - Variables: `{{ user.name }}` - Logic: `{{#user.isPremium}}Premium content{{/user.isPremium}}` - Loops: `{{#items}}{{ name }}{{/items}}` - No built-in filters (use variables with pre-formatted data) ### Simple - Variables only: `{{ user.name }}`, `{{ amount }}`, `{{ date }}` ### Built-in Filters (LiquidJS only) - `formatDate` - Format dates: `{{ createdAt | formatDate: "short" }}` - `formatCurrency` - Format currency: `{{ amount | formatCurrency: "USD" }}` - `capitalize` - Capitalize first letter: `{{ name | capitalize }}` ## Available Helper Functions ```typescript import { renderTemplate, // Render email templates with variables processEmails, // Process pending emails manually retryFailedEmails, // Retry failed emails getMailing // Get mailing service instance } from '@xtr-dev/payload-mailing' // Render a template const { html, text, subject } = await renderTemplate(payload, 'welcome', { name: 'John', activationUrl: 'https://example.com/activate' }) // Process emails manually await processEmails(payload) // Retry failed emails await retryFailedEmails(payload) ``` ## PayloadCMS Integration The plugin provides both tasks and workflows for email processing: ### Tasks vs Workflows - **Tasks**: Simple job execution, good for background processing - **Workflows**: More advanced with UI, status tracking, and immediate processing options ### Task Integration The plugin provides a ready-to-use PayloadCMS task for queuing template emails: ### 1. Add the task to your Payload config ```typescript import { buildConfig } from 'payload/config' import { sendTemplateEmailTask } from '@xtr-dev/payload-mailing' export default buildConfig({ // ... your config jobs: { tasks: [ sendTemplateEmailTask, // ... your other tasks ] } }) ``` ### 2. Queue emails from your code ```typescript import type { SendTemplateEmailInput } from '@xtr-dev/payload-mailing' // Queue a template email const result = await payload.jobs.queue({ task: 'send-template-email', input: { templateSlug: 'welcome-email', to: ['user@example.com'], cc: ['manager@example.com'], variables: { firstName: 'John', activationUrl: 'https://example.com/activate/123' }, priority: 1, // Add any custom fields from your email collection customField: 'value' } as SendTemplateEmailInput }) // Queue a scheduled email await payload.jobs.queue({ task: 'send-template-email', input: { templateSlug: 'reminder-email', to: ['user@example.com'], variables: { eventName: 'Product Launch' }, scheduledAt: new Date('2024-01-15T10:00:00Z').toISOString(), priority: 3 } }) ``` ### 3. Use in admin panel workflows The task can also be triggered from the Payload admin panel with a user-friendly form interface that includes: - Template slug selection - Email recipients (to, cc, bcc) - Template variables as JSON - Optional scheduling - Priority setting - Any custom fields you've added to your email collection ### Task Benefits - ✅ **Easy Integration**: Just add to your tasks array - ✅ **Type Safety**: Full TypeScript support with `SendTemplateEmailInput` - ✅ **Admin UI**: Ready-to-use form interface - ✅ **Flexible**: Supports all your custom email collection fields - ✅ **Error Handling**: Comprehensive error reporting - ✅ **Queue Management**: Leverage Payload's job queue system ### Workflow Integration For advanced features, use the workflow instead: ```typescript import { sendEmailWorkflow } from '@xtr-dev/payload-mailing' export default buildConfig({ jobs: { workflows: [sendEmailWorkflow] } }) ``` **Key advantage**: Optional `processImmediately` option to send emails instantly instead of queuing. ```typescript await payload.workflows.queue({ workflow: 'send-email', input: { processImmediately: true, // Send immediately (default: false) templateSlug: 'welcome-email', to: ['user@example.com'], variables: { name: 'John' } } }) ``` ## 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: ```typescript // 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 ```bash # 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: ```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, EmailTemplate, QueuedEmail, EmailObject, EmailWrapperHook } from '@xtr-dev/payload-mailing' ``` ## API Reference ### `sendEmail(payload, options)` Type-safe email sending with automatic template rendering and validation. ```typescript import { sendEmail } from '@xtr-dev/payload-mailing' import { Email } from './payload-types' const email = await sendEmail(payload, { template: { slug: 'template-slug', variables: { /* template variables */ } }, data: { to: 'user@example.com', // Your custom fields are type-safe here! } }) ``` **Type Parameters:** - `T extends BaseEmailData` - Your generated Email type for full type safety **Options:** - `template.slug` - Template slug to render - `template.variables` - Variables to pass to template - `data` - Email data (merged with template output) - `collectionSlug` - Custom collection name (defaults to 'emails') ### `renderTemplate(payload, slug, variables)` Render an email template without sending. ```typescript const { html, text, subject } = await renderTemplate( payload, 'welcome-email', { name: 'John' } ) ``` ### Helper Functions - `getMailing(payload)` - Get mailing context - `processEmails(payload)` - Manually trigger email processing - `retryFailedEmails(payload)` - Manually retry failed emails ## Migration Guide (v0.0.x → v0.1.0) **🚨 BREAKING CHANGES**: The API has been simplified to use Payload collections directly. ### Before (v0.0.x) ```typescript import { sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing' // Old way const emailId = await sendEmail(payload, { templateSlug: 'welcome', to: 'user@example.com', variables: { name: 'John' } }) const scheduledId = await scheduleEmail(payload, { templateSlug: 'reminder', to: 'user@example.com', scheduledAt: new Date('2024-01-15T10:00:00Z'), variables: { eventName: 'Launch' } }) ``` ### After (v0.1.0+) ```typescript import { renderTemplate } from '@xtr-dev/payload-mailing' // New way - render template first const { html, text, subject } = await renderTemplate(payload, 'welcome', { name: 'John' }) // Then create email using Payload collections (full type safety!) const email = await payload.create({ collection: 'emails', data: { to: ['user@example.com'], subject, html, text, // For scheduling scheduledAt: new Date('2024-01-15T10:00:00Z'), // Add any custom fields from your collection customField: 'value', } }) ``` ### Benefits of Migration - ✅ **Full TypeScript support** with your generated Payload types - ✅ **Use any custom fields** you add to your email collection - ✅ **Leverage Payload's features**: validation, hooks, access control - ✅ **One consistent API** - just use `payload.create()` - ✅ **No wrapper methods** - direct access to Payload's power ## Recent Changes ### v0.1.0 (Latest - Breaking Changes) **🚀 Major API Simplification:** - **REMOVED**: `sendEmail()` and `scheduleEmail()` wrapper methods - **REMOVED**: `SendEmailOptions` custom types - **ADDED**: Direct Payload collection usage with full type safety - **ADDED**: `renderTemplate()` helper for template rendering - **ADDED**: Support for LiquidJS, Mustache, and custom template engines - **IMPROVED**: Webpack compatibility with proper dynamic imports **Template Engine Enhancements:** - **NEW**: LiquidJS support (default) with modern syntax and logic - **NEW**: Mustache support for logic-less templates - **NEW**: Custom template renderer hook for maximum flexibility - **NEW**: Simple variable replacement as fallback - **FIXED**: All webpack compatibility issues resolved **Developer Experience:** - **IMPROVED**: Full TypeScript inference using generated Payload types - **IMPROVED**: Comprehensive migration guide and documentation - **IMPROVED**: Better error handling and async patterns - **SIMPLIFIED**: Cleaner codebase with fewer abstractions ## License MIT ## Contributing Issues and pull requests welcome at [GitHub repository](https://github.com/xtr-dev/payload-mailing)