mirror of
https://github.com/xtr-dev/payload-mailing.git
synced 2025-12-10 00:03:23 +00:00
Update README for improved clarity and reduced redundancy.
This commit is contained in:
789
README.md
789
README.md
@@ -1,37 +1,32 @@
|
|||||||
# @xtr-dev/payload-mailing
|
# @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.
|
⚠️ **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
|
## Features
|
||||||
|
|
||||||
✅ **Template System**: Create reusable email templates with LiquidJS, Mustache, or simple variables
|
- 📧 Template-based emails with LiquidJS, Mustache, or custom engines
|
||||||
|
- ⏰ Email scheduling for future delivery
|
||||||
✅ **Type Safety**: Full TypeScript support using your generated Payload types
|
- 🔄 Automatic retry mechanism for failed sends
|
||||||
|
- 🎯 Full TypeScript support with generated Payload types
|
||||||
✅ **Flexible Template Engines**: LiquidJS, Mustache, or bring your own template renderer
|
- 📋 Job queue integration via PayloadCMS
|
||||||
|
- 🔧 Uses Payload collections directly - no custom APIs
|
||||||
✅ **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
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @xtr-dev/payload-mailing
|
npm install @xtr-dev/payload-mailing
|
||||||
|
# or
|
||||||
|
pnpm add @xtr-dev/payload-mailing
|
||||||
|
# or
|
||||||
|
yarn add @xtr-dev/payload-mailing
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### 1. Configure email in your Payload config and add the plugin
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { buildConfig } from 'payload/config'
|
import { buildConfig } from 'payload'
|
||||||
import { mailingPlugin } from '@xtr-dev/payload-mailing'
|
import { mailingPlugin } from '@xtr-dev/payload-mailing'
|
||||||
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||||
|
|
||||||
@@ -55,467 +50,164 @@ export default buildConfig({
|
|||||||
defaultFromName: 'Your Site Name',
|
defaultFromName: 'Your Site Name',
|
||||||
retryAttempts: 3,
|
retryAttempts: 3,
|
||||||
retryDelay: 300000, // 5 minutes
|
retryDelay: 300000, // 5 minutes
|
||||||
queue: 'email-queue', // optional
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Send emails with type-safe helper
|
## Imports
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// sendEmail is a primary export for easy access
|
// Main plugin
|
||||||
import { sendEmail } from '@xtr-dev/payload-mailing'
|
import { mailingPlugin } from '@xtr-dev/payload-mailing'
|
||||||
import { Email } from './payload-types' // Your generated types
|
|
||||||
|
|
||||||
// Option 1: Using templates with full type safety
|
// Helper functions
|
||||||
const email = await sendEmail<Email>(payload, {
|
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: {
|
template: {
|
||||||
slug: 'welcome-email',
|
slug: 'welcome-email',
|
||||||
variables: {
|
variables: { firstName: 'John', welcomeUrl: 'https://example.com' }
|
||||||
firstName: 'John',
|
|
||||||
welcomeUrl: 'https://yoursite.com/welcome'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
to: 'user@example.com',
|
to: 'user@example.com',
|
||||||
// Schedule for later (optional)
|
scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // Schedule for later
|
||||||
scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
priority: 1
|
||||||
priority: 1,
|
|
||||||
// Your custom fields are type-safe!
|
|
||||||
customField: 'your-value',
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Option 2: Direct HTML email (no template)
|
// Direct email
|
||||||
const directEmail = await sendEmail<Email>(payload, {
|
const directEmail = await payload.create({
|
||||||
data: {
|
|
||||||
to: ['user@example.com', 'another@example.com'],
|
|
||||||
subject: 'Welcome!',
|
|
||||||
html: '<h1>Welcome John!</h1><p>Thanks for joining!</p>',
|
|
||||||
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',
|
collection: 'emails',
|
||||||
data: {
|
data: {
|
||||||
to: ['user@example.com'],
|
to: ['user@example.com'],
|
||||||
subject: 'Hello',
|
subject: 'Welcome!',
|
||||||
html: '<p>Hello World</p>',
|
html: '<h1>Welcome!</h1>',
|
||||||
|
text: 'Welcome!'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Template Engines
|
||||||
|
|
||||||
### Plugin Options
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
mailingPlugin({
|
|
||||||
// Template engine (optional)
|
|
||||||
templateEngine: 'liquidjs', // 'liquidjs' | 'mustache' | 'simple'
|
|
||||||
|
|
||||||
// Custom template renderer (optional)
|
|
||||||
templateRenderer: async (template: string, variables: Record<string, any>) => {
|
|
||||||
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)
|
### LiquidJS (Default)
|
||||||
- Variables: `{{ user.name }}`
|
Modern template syntax with logic support:
|
||||||
- Logic: `{% if user.isPremium %}Premium content{% endif %}`
|
```liquid
|
||||||
- Loops: `{% for item in items %}{{ item.name }}{% endfor %}`
|
{% if user.isPremium %}
|
||||||
- Filters: `{{ amount | formatCurrency }}`, `{{ date | formatDate: "short" }}`
|
Welcome Premium Member {{user.name}}!
|
||||||
|
{% else %}
|
||||||
|
Welcome {{user.name}}!
|
||||||
|
{% endif %}
|
||||||
|
```
|
||||||
|
|
||||||
### Mustache
|
### Mustache
|
||||||
- Variables: `{{ user.name }}`
|
Logic-less templates:
|
||||||
- Logic: `{{#user.isPremium}}Premium content{{/user.isPremium}}`
|
```mustache
|
||||||
- Loops: `{{#items}}{{ name }}{{/items}}`
|
{{#user.isPremium}}
|
||||||
- No built-in filters (use variables with pre-formatted data)
|
Welcome Premium Member {{user.name}}!
|
||||||
|
{{/user.isPremium}}
|
||||||
### Simple
|
{{^user.isPremium}}
|
||||||
- Variables only: `{{ user.name }}`, `{{ amount }}`, `{{ date }}`
|
Welcome {{user.name}}!
|
||||||
|
{{/user.isPremium}}
|
||||||
### 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
|
### Simple Variables
|
||||||
|
Basic `{{variable}}` replacement:
|
||||||
The plugin provides PayloadCMS tasks for email processing:
|
```text
|
||||||
|
Welcome {{user.name}}! Your account expires on {{expireDate}}.
|
||||||
### 1. Add the task to your Payload config
|
```
|
||||||
|
|
||||||
|
### Custom Renderer
|
||||||
|
Bring your own template engine:
|
||||||
```typescript
|
```typescript
|
||||||
import { buildConfig } from 'payload/config'
|
mailingPlugin({
|
||||||
import { sendTemplateEmailTask } from '@xtr-dev/payload-mailing'
|
templateRenderer: async (template, variables) => {
|
||||||
|
return handlebars.compile(template)(variables)
|
||||||
export default buildConfig({
|
|
||||||
// ... your config
|
|
||||||
jobs: {
|
|
||||||
tasks: [
|
|
||||||
sendTemplateEmailTask,
|
|
||||||
// ... your other tasks
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Queue emails from your code
|
## Templates
|
||||||
|
|
||||||
```typescript
|
Use `{{}}` to insert data in templates:
|
||||||
import type { SendTemplateEmailInput } from '@xtr-dev/payload-mailing'
|
|
||||||
|
|
||||||
// Queue a template email
|
- `{{user.name}}` - User data from variables
|
||||||
const result = await payload.jobs.queue({
|
- `{{formatDate createdAt "short"}}` - Built-in date formatting
|
||||||
task: 'send-template-email',
|
- `{{formatCurrency amount "USD"}}` - Currency formatting
|
||||||
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
|
Example template:
|
||||||
await payload.jobs.queue({
|
```liquid
|
||||||
task: 'send-template-email',
|
Subject: Welcome {{user.name}}!
|
||||||
input: {
|
|
||||||
templateSlug: 'reminder-email',
|
{% if user.isPremium %}
|
||||||
to: ['user@example.com'],
|
Welcome Premium Member {{user.name}}!
|
||||||
variables: { eventName: 'Product Launch' },
|
|
||||||
scheduledAt: new Date('2024-01-15T10:00:00Z').toISOString(),
|
Your premium features are ready.
|
||||||
priority: 3
|
{% 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:
|
- PayloadCMS ^3.45.0
|
||||||
- Template slug selection
|
- Node.js ^18.20.2 || >=20.9.0
|
||||||
- Email recipients (to, cc, bcc)
|
- pnpm ^9 || ^10
|
||||||
- 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
|
|
||||||
|
|
||||||
## Job Processing
|
## Job Processing
|
||||||
|
|
||||||
The plugin automatically adds a unified email processing job to PayloadCMS:
|
Queue emails using PayloadCMS jobs:
|
||||||
|
|
||||||
- **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
|
```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({
|
await payload.jobs.queue({
|
||||||
task: 'process-email-queue',
|
task: 'send-template-email',
|
||||||
input: {}
|
input: {
|
||||||
|
templateSlug: 'welcome-email',
|
||||||
|
to: ['user@example.com'],
|
||||||
|
variables: { firstName: 'John' },
|
||||||
|
scheduledAt: new Date('2024-01-15T10:00:00Z').toISOString()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
## Email Status Tracking
|
## Email Status
|
||||||
|
|
||||||
All emails are stored in the emails collection with these statuses:
|
|
||||||
|
|
||||||
|
Emails are tracked with these statuses:
|
||||||
- `pending` - Waiting to be sent
|
- `pending` - Waiting to be sent
|
||||||
- `processing` - Currently being sent
|
- `processing` - Currently being sent
|
||||||
- `sent` - Successfully delivered
|
- `sent` - Successfully delivered
|
||||||
- `failed` - Failed to send (will retry if attempts < retryAttempts)
|
- `failed` - Failed to send (will retry automatically)
|
||||||
|
|
||||||
## 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
|
## Environment Variables
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Email configuration
|
|
||||||
EMAIL_HOST=smtp.gmail.com
|
EMAIL_HOST=smtp.gmail.com
|
||||||
EMAIL_PORT=587
|
EMAIL_PORT=587
|
||||||
EMAIL_USER=your-email@gmail.com
|
EMAIL_USER=your-email@gmail.com
|
||||||
@@ -523,263 +215,6 @@ EMAIL_PASS=your-app-password
|
|||||||
EMAIL_FROM=noreply@yoursite.com
|
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<T>(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<Email>(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
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
Reference in New Issue
Block a user