diff --git a/README.md b/README.md index ca6aa75..fd21518 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,32 @@ # @xtr-dev/payload-mailing -📧 **Template-based email system with scheduling and job processing for PayloadCMS** +A template-based email system with scheduling and job processing for PayloadCMS 3.x. ⚠️ **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 +- 📧 Template-based emails with LiquidJS, Mustache, or custom engines +- ⏰ Email scheduling for future delivery +- 🔄 Automatic retry mechanism for failed sends +- 🎯 Full TypeScript support with generated Payload types +- 📋 Job queue integration via PayloadCMS +- 🔧 Uses Payload collections directly - no custom APIs ## Installation ```bash npm install @xtr-dev/payload-mailing +# or +pnpm add @xtr-dev/payload-mailing +# or +yarn add @xtr-dev/payload-mailing ``` ## Quick Start -### 1. Configure email in your Payload config and add the plugin - ```typescript -import { buildConfig } from 'payload/config' +import { buildConfig } from 'payload' import { mailingPlugin } from '@xtr-dev/payload-mailing' import { nodemailerAdapter } from '@payloadcms/email-nodemailer' @@ -55,467 +50,164 @@ export default buildConfig({ defaultFromName: 'Your Site Name', retryAttempts: 3, retryDelay: 300000, // 5 minutes - queue: 'email-queue', // optional }), ], }) ``` -### 2. Send emails with type-safe helper +## Imports ```typescript -// sendEmail is a primary export for easy access -import { sendEmail } from '@xtr-dev/payload-mailing' -import { Email } from './payload-types' // Your generated types +// Main plugin +import { mailingPlugin } from '@xtr-dev/payload-mailing' -// Option 1: Using templates with full type safety -const email = await sendEmail(payload, { +// Helper functions +import { sendEmail, renderTemplate, processEmails } from '@xtr-dev/payload-mailing' + +// Job tasks +import { sendTemplateEmailTask } from '@xtr-dev/payload-mailing' + +// Types +import type { MailingPluginConfig, SendEmailOptions } from '@xtr-dev/payload-mailing' +``` + +## Usage + +```typescript +import { sendEmail } from '@xtr-dev/payload-mailing' + +// Using templates +const email = await sendEmail(payload, { template: { slug: 'welcome-email', - variables: { - firstName: 'John', - welcomeUrl: 'https://yoursite.com/welcome' - } + variables: { firstName: 'John', welcomeUrl: 'https://example.com' } }, 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', + scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // Schedule for later + priority: 1 } }) -// 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({ +// Direct email +const directEmail = await payload.create({ collection: 'emails', data: { to: ['user@example.com'], - subject: 'Hello', - html: '

Hello World

', + subject: 'Welcome!', + html: '

Welcome!

', + text: 'Welcome!' } }) ``` -## 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: +## Template Engines ### 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" }}` +Modern template syntax with logic support: +```liquid +{% if user.isPremium %} + Welcome Premium Member {{user.name}}! +{% else %} + Welcome {{user.name}}! +{% endif %} +``` ### 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) +Logic-less templates: +```mustache +{{#user.isPremium}} + Welcome Premium Member {{user.name}}! +{{/user.isPremium}} +{{^user.isPremium}} + Welcome {{user.name}}! +{{/user.isPremium}} ``` -## PayloadCMS Integration - -The plugin provides PayloadCMS tasks for email processing: - -### 1. Add the task to your Payload config +### Simple Variables +Basic `{{variable}}` replacement: +```text +Welcome {{user.name}}! Your account expires on {{expireDate}}. +``` +### Custom Renderer +Bring your own template engine: ```typescript -import { buildConfig } from 'payload/config' -import { sendTemplateEmailTask } from '@xtr-dev/payload-mailing' - -export default buildConfig({ - // ... your config - jobs: { - tasks: [ - sendTemplateEmailTask, - // ... your other tasks - ] +mailingPlugin({ + templateRenderer: async (template, variables) => { + return handlebars.compile(template)(variables) } }) ``` -### 2. Queue emails from your code +## Templates -```typescript -import type { SendTemplateEmailInput } from '@xtr-dev/payload-mailing' +Use `{{}}` to insert data in templates: -// 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 -}) +- `{{user.name}}` - User data from variables +- `{{formatDate createdAt "short"}}` - Built-in date formatting +- `{{formatCurrency amount "USD"}}` - Currency formatting -// 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 - } -}) +Example template: +```liquid +Subject: Welcome {{user.name}}! + +{% if user.isPremium %} +Welcome Premium Member {{user.name}}! + +Your premium features are ready. +{% else %} +Welcome {{user.name}}! + +Upgrade to premium for more features. +{% endif %} + +Account created: {{formatDate user.createdAt "long"}} ``` -### 3. Use in admin panel workflows +## Requirements -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 - -### Immediate Processing - -The send email task now supports immediate processing. Enable the `processImmediately` option to send emails instantly: - -```typescript -await payload.jobs.queue({ - task: 'send-email', - input: { - processImmediately: true, // Send immediately (default: false) - templateSlug: 'welcome-email', - to: ['user@example.com'], - variables: { name: 'John' } - } -}) -``` - -**Benefits**: -- No separate workflow needed -- Unified task interface -- Optional immediate processing when needed +- PayloadCMS ^3.45.0 +- Node.js ^18.20.2 || >=20.9.0 +- pnpm ^9 || ^10 ## 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 emails using PayloadCMS jobs: ```typescript -// Queue the job for processing +import { sendTemplateEmailTask } from '@xtr-dev/payload-mailing' + +// Add to your Payload config +export default buildConfig({ + jobs: { + tasks: [sendTemplateEmailTask] + } +}) + +// Queue a template email await payload.jobs.queue({ - task: 'process-email-queue', - input: {} + task: 'send-template-email', + input: { + templateSlug: 'welcome-email', + to: ['user@example.com'], + variables: { firstName: 'John' }, + scheduledAt: new Date('2024-01-15T10:00:00Z').toISOString() + } }) ``` -## Email Status Tracking - -All emails are stored in the emails collection with these statuses: +## Email Status +Emails are tracked 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 +- `failed` - Failed to send (will retry automatically) ## Environment Variables ```bash -# Email configuration EMAIL_HOST=smtp.gmail.com EMAIL_PORT=587 EMAIL_USER=your-email@gmail.com @@ -523,263 +215,6 @@ 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