From 0198821ff3530e69311c4b82e4b38d0e5ffea6d4 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sun, 14 Sep 2025 23:22:46 +0200 Subject: [PATCH 1/3] Update README for improved clarity and reduced redundancy. --- README.md | 789 ++++++++---------------------------------------------- 1 file changed, 112 insertions(+), 677 deletions(-) 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 From fe8c4d194ea460c9ccc8eb417c979023ad21b72d Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sun, 14 Sep 2025 23:30:14 +0200 Subject: [PATCH 2/3] Bump version to 0.4.9 and add comprehensive plugin configuration details to README. --- README.md | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fd21518..fb3fe5a 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,72 @@ Upgrade to premium for more features. Account created: {{formatDate user.createdAt "long"}} ``` +## Configuration + +### Plugin Options + +```typescript +mailingPlugin({ + // Template engine + templateEngine: 'liquidjs', // 'liquidjs' | 'mustache' | 'simple' + + // Custom template renderer + templateRenderer: async (template: string, variables: Record) => { + return yourCustomEngine.render(template, variables) + }, + + // Email settings + defaultFrom: 'noreply@yoursite.com', + defaultFromName: 'Your Site', + retryAttempts: 3, // Number of retry attempts + retryDelay: 300000, // 5 minutes between retries + + // Collection customization + collections: { + templates: 'email-templates', // Custom collection name + emails: 'emails' // Custom collection name + }, + + // Hooks + beforeSend: async (options, email) => { + // Modify email before sending + options.headers = { 'X-Campaign-ID': email.campaignId } + return options + }, + + onReady: async (payload) => { + // Plugin initialization complete + console.log('Mailing plugin ready!') + } +}) +``` + +### Collection Overrides + +Customize collections with access controls and custom fields: + +```typescript +mailingPlugin({ + collections: { + emails: { + access: { + read: ({ req: { user } }) => user?.role === 'admin', + create: ({ req: { user } }) => !!user, + update: ({ req: { user } }) => user?.role === 'admin', + delete: ({ req: { user } }) => user?.role === 'admin' + }, + fields: [ + { + name: 'campaignId', + type: 'text', + admin: { position: 'sidebar' } + } + ] + } + } +}) +``` + ## Requirements - PayloadCMS ^3.45.0 @@ -215,6 +281,87 @@ EMAIL_PASS=your-app-password EMAIL_FROM=noreply@yoursite.com ``` +## API Reference + +### `sendEmail(payload, options)` + +Send emails with full type safety using your generated Payload types. + +```typescript +import { sendEmail } from '@xtr-dev/payload-mailing' +import type { Email } from './payload-types' + +const email = await sendEmail(payload, { + template?: { + slug: string // Template slug + variables: Record // Template variables + }, + data: { + to: string | string[] // Recipients + cc?: string | string[] // CC recipients + bcc?: string | string[] // BCC recipients + subject?: string // Email subject (overrides template) + html?: string // HTML content (overrides template) + text?: string // Text content (overrides template) + scheduledAt?: Date // Schedule for later + priority?: number // Priority (1-5, 1 = highest) + // ... your custom fields from Email collection + }, + collectionSlug?: string // Custom collection name (default: 'emails') +}) +``` + +### `renderTemplate(payload, slug, variables)` + +Render a template without sending an email. + +```typescript +import { renderTemplate } from '@xtr-dev/payload-mailing' + +const result = await renderTemplate( + payload: Payload, + slug: string, + variables: Record +): Promise<{ + html: string // Rendered HTML content + text: string // Rendered text content + subject: string // Rendered subject line +}> +``` + +### Helper Functions + +```typescript +import { processEmails, retryFailedEmails, getMailing } from '@xtr-dev/payload-mailing' + +// Process pending emails manually +await processEmails(payload: Payload): Promise + +// Retry failed emails manually +await retryFailedEmails(payload: Payload): Promise + +// Get mailing service instance +const mailing = getMailing(payload: Payload): MailingService +``` + +### Job Task Types + +```typescript +import type { SendTemplateEmailInput } from '@xtr-dev/payload-mailing' + +interface SendTemplateEmailInput { + templateSlug: string // Template to use + to: string[] // Recipients + cc?: string[] // CC recipients + bcc?: string[] // BCC recipients + variables: Record // Template variables + scheduledAt?: string // ISO date string for scheduling + priority?: number // Priority (1-5) + processImmediately?: boolean // Send immediately (default: false) + [key: string]: any // Your custom email collection fields +} +``` + ## License MIT diff --git a/package.json b/package.json index ab5b1f0..f159ae4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/payload-mailing", - "version": "0.4.8", + "version": "0.4.9", "description": "Template-based email system with scheduling and job processing for PayloadCMS", "type": "module", "main": "dist/index.js", From ae3865346693f12ab333596f715a5f3d42ca6d98 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sun, 14 Sep 2025 23:33:14 +0200 Subject: [PATCH 3/3] Expand and clarify README documentation: - Provide detailed examples of email template structure and rendering. - Add guidance on job scheduling and direct email sending use cases. - Enhance troubleshooting section with common issues and solutions. - Introduce bulk operations, email monitoring, and query examples. - Update plugin configuration requirements and clarify environment variables. Improves overall usability and onboarding for developers. --- README.md | 314 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 300 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index fb3fe5a..7350a86 100644 --- a/README.md +++ b/README.md @@ -148,21 +148,66 @@ Use `{{}}` to insert data in templates: - `{{formatDate createdAt "short"}}` - Built-in date formatting - `{{formatCurrency amount "USD"}}` - Currency formatting -Example template: +### Template Structure + +Templates include both subject and body content: + ```liquid -Subject: Welcome {{user.name}}! + +Welcome {{user.name}} to {{siteName}}! + + +# Hello {{user.name}}! 👋 {% if user.isPremium %} -Welcome Premium Member {{user.name}}! +**Welcome Premium Member!** -Your premium features are ready. +Your premium features are now active: +- Priority support +- Advanced analytics +- Custom integrations {% else %} -Welcome {{user.name}}! +Welcome to {{siteName}}! -Upgrade to premium for more features. +**Ready to get started?** +- Complete your profile +- Explore our features +- [Upgrade to Premium]({{upgradeUrl}}) {% endif %} -Account created: {{formatDate user.createdAt "long"}} +--- +**Account Details:** +- Created: {{formatDate user.createdAt "long"}} +- Email: {{user.email}} +- Plan: {{user.plan | capitalize}} + +Need help? Reply to this email or visit our [help center]({{helpUrl}}). + +Best regards, +The {{siteName}} Team +``` + +### Example Usage + +```typescript +// Create template in admin panel, then use: +const { html, text, subject } = await renderTemplate(payload, 'welcome-email', { + user: { + name: 'John Doe', + email: 'john@example.com', + isPremium: false, + plan: 'free', + createdAt: new Date() + }, + siteName: 'MyApp', + upgradeUrl: 'https://myapp.com/upgrade', + helpUrl: 'https://myapp.com/help' +}) + +// Results in: +// subject: "Welcome John Doe to MyApp!" +// html: "

Hello John Doe! 👋

Welcome to MyApp!

..." +// text: "Hello John Doe! Welcome to MyApp! Ready to get started?..." ``` ## Configuration @@ -233,37 +278,98 @@ mailingPlugin({ ## Requirements -- PayloadCMS ^3.45.0 +- PayloadCMS ^3.0.0 - Node.js ^18.20.2 || >=20.9.0 - pnpm ^9 || ^10 ## Job Processing -Queue emails using PayloadCMS jobs: +### When to Use Jobs vs Direct Sending + +**Use Jobs for:** +- Bulk email campaigns (performance) +- Scheduled emails (future delivery) +- Background processing (non-blocking) +- Retry handling (automatic retries) +- High-volume sending (queue management) + +**Use Direct Sending for:** +- Immediate transactional emails +- Single recipient emails +- Simple use cases +- When you need immediate feedback + +### Setup ```typescript import { sendTemplateEmailTask } from '@xtr-dev/payload-mailing' -// Add to your Payload config export default buildConfig({ jobs: { tasks: [sendTemplateEmailTask] } }) +``` -// Queue a template email +### Queue Template Emails + +```typescript +// Basic template email await payload.jobs.queue({ task: 'send-template-email', input: { templateSlug: 'welcome-email', to: ['user@example.com'], - variables: { firstName: 'John' }, - scheduledAt: new Date('2024-01-15T10:00:00Z').toISOString() + variables: { firstName: 'John' } + } +}) + +// 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: 1 + } +}) + +// Immediate processing (bypasses queue) +await payload.jobs.queue({ + task: 'send-template-email', + input: { + processImmediately: true, + templateSlug: 'urgent-notification', + to: ['admin@example.com'], + variables: { alertMessage: 'System critical error' } } }) ``` -## Email Status +### Bulk Operations + +```typescript +// Send to multiple recipients efficiently +const recipients = ['user1@example.com', 'user2@example.com', 'user3@example.com'] + +for (const email of recipients) { + await payload.jobs.queue({ + task: 'send-template-email', + input: { + templateSlug: 'newsletter', + to: [email], + variables: { unsubscribeUrl: `https://example.com/unsubscribe/${email}` }, + priority: 3 // Lower priority for bulk emails + } + }) +} +``` + +## Email Status & Monitoring + +### Status Types Emails are tracked with these statuses: - `pending` - Waiting to be sent @@ -271,6 +377,54 @@ Emails are tracked with these statuses: - `sent` - Successfully delivered - `failed` - Failed to send (will retry automatically) +### Query Email Status + +```typescript +// Check specific email status +const email = await payload.findByID({ + collection: 'emails', + id: 'email-id' +}) +console.log(`Email status: ${email.status}`) + +// Find emails by status +const pendingEmails = await payload.find({ + collection: 'emails', + where: { + status: { equals: 'pending' } + }, + sort: 'createdAt' +}) + +// Find failed emails for retry +const failedEmails = await payload.find({ + collection: 'emails', + where: { + status: { equals: 'failed' }, + attemptCount: { less_than: 3 } + } +}) + +// Monitor scheduled emails +const scheduledEmails = await payload.find({ + collection: 'emails', + where: { + scheduledAt: { greater_than: new Date() }, + status: { equals: 'pending' } + } +}) +``` + +### Admin Panel Monitoring + +Navigate to **Mailing > Emails** in your Payload admin to: +- View email delivery status and timestamps +- See error messages for failed deliveries +- Track retry attempts and next retry times +- Monitor scheduled email queue +- Filter by status, recipient, or date range +- Export email reports for analysis + ## Environment Variables ```bash @@ -362,10 +516,142 @@ interface SendTemplateEmailInput { } ``` +## Troubleshooting + +### Common Issues + +#### Templates not rendering +```typescript +// Check template exists +const template = await payload.findByID({ + collection: 'email-templates', + id: 'your-template-slug' +}) + +// Verify template engine configuration +mailingPlugin({ + templateEngine: 'liquidjs', // Ensure correct engine +}) +``` + +#### Emails stuck in pending status +```typescript +// Manually process email queue +import { processEmails } from '@xtr-dev/payload-mailing' +await processEmails(payload) + +// Check for processing errors +const pendingEmails = await payload.find({ + collection: 'emails', + where: { status: { equals: 'pending' } } +}) +``` + +#### SMTP connection errors +```bash +# Verify environment variables +EMAIL_HOST=smtp.gmail.com +EMAIL_PORT=587 +EMAIL_USER=your-email@gmail.com +EMAIL_PASS=your-app-password # Use app password, not regular password + +# Test connection +curl -v telnet://smtp.gmail.com:587 +``` + +#### Template variables not working +```typescript +// Ensure variables match template syntax +const variables = { + user: { name: 'John' }, // For {{user.name}} + welcomeUrl: 'https://...' // For {{welcomeUrl}} +} + +// Check for typos in template +{% if user.isPremium %} +{% if user.isPremimum %} +``` + +### Error Handling + +```typescript +try { + const email = await sendEmail(payload, { + template: { slug: 'welcome', variables: { name: 'John' } }, + data: { to: 'user@example.com' } + }) +} catch (error) { + if (error.message.includes('Template not found')) { + // Handle missing template + console.error('Template does not exist:', error.templateSlug) + } else if (error.message.includes('SMTP')) { + // Handle email delivery issues + console.error('Email delivery failed:', error.details) + } else { + // Handle other errors + console.error('Unexpected error:', error) + } +} +``` + +### Debug Mode + +Enable detailed logging: + +```bash +# Set environment variable +PAYLOAD_AUTOMATION_LOG_LEVEL=debug npm run dev + +# Or in your code +mailingPlugin({ + onReady: async (payload) => { + console.log('Mailing plugin initialized') + }, + beforeSend: async (options, email) => { + console.log('Sending email:', { to: options.to, subject: options.subject }) + return options + } +}) +``` + ## License MIT ## Contributing +### Development Setup + +```bash +# Clone the repository +git clone https://github.com/xtr-dev/payload-mailing.git +cd payload-mailing + +# Install dependencies +pnpm install + +# Build the package +pnpm build + +# Run tests +pnpm test + +# Link for local development +pnpm link --global +``` + +### Testing + +```bash +# Run unit tests +pnpm test + +# Run integration tests +pnpm test:integration + +# Test with different PayloadCMS versions +pnpm test:payload-3.0 +pnpm test:payload-latest +``` + Issues and pull requests welcome at [GitHub repository](https://github.com/xtr-dev/payload-mailing)