Compare commits

..

11 Commits

Author SHA1 Message Date
Bas
fde8eb538d Merge pull request #41 from xtr-dev/dev
Dev
2025-09-14 17:47:19 +02:00
845b379da3 Fix error handling and improve naming consistency
- Use native error chaining in workflow (Error constructor with cause option)
- Fix job scheduling to use 'task' instead of 'workflow' property
- Rename processEmailsJob.ts to processEmailsTask.ts for consistency
- Update all imports and references while maintaining backward compatibility
- Add processEmailsTask export with processEmailsJob alias
- Bump version to 0.3.1
2025-09-14 17:34:53 +02:00
dd205dba41 Make workflow documentation more concise
- Simplified workflow section to focus on key advantage
- Removed verbose comparison table and features list
- Kept essential usage example with processImmediately option
- More readable and focused on the main differentiator
2025-09-14 17:21:57 +02:00
a6564e2a29 Add sendEmail workflow with immediate processing option
- Create sendEmailWorkflow as a Payload workflow alternative to task
- Add processImmediately option (disabled by default) to send emails immediately
- Expose processEmailItem method in MailingService for individual email processing
- Add comprehensive input schema with conditional fields
- Update plugin to register both tasks and workflows
- Add detailed documentation comparing tasks vs workflows
- Includes status tracking and error handling
- Bump version to 0.3.0 (new feature)
2025-09-14 17:20:21 +02:00
8f200da449 Refactor and clean up job organization
- Properly encapsulate processEmailsJob in its own file with handler and definition
- Clean up index.ts to remove duplicate code and just export job definitions
- Add comprehensive JSDoc comments for better documentation
- Separate job handler logic from job definition for clarity
- Fix job scheduling to use correct field names
- Bump version to 0.2.1
2025-09-14 17:16:01 +02:00
Bas
ff94d72d49 Merge pull request #40 from xtr-dev/dev
BREAKING CHANGE: Remove custom transport support, use Payload's email…
2025-09-14 17:02:50 +02:00
ddee7d5a76 BREAKING CHANGE: Remove custom transport support, use Payload's email config
- Removed custom transport configuration from plugin
- Plugin now requires Payload email to be configured
- Simplified setup by relying on Payload's email adapter
- Updated README with new configuration requirements
- Bump version to 0.2.0 (breaking change)

Users must now configure email in their Payload config using an email adapter
like @payloadcms/email-nodemailer instead of configuring transport in the plugin.
2025-09-14 16:57:30 +02:00
Bas
0083e8e1fa Merge pull request #39 from xtr-dev/dev
Remove redundant queueName validation and debug log, bump version to …
2025-09-14 16:29:13 +02:00
63a7eef8d8 Remove redundant queueName validation and debug log, bump version to 0.1.24 2025-09-14 16:28:24 +02:00
Bas
6cf055178b Merge pull request #38 from xtr-dev/dev
Add debug log for email transporter configuration and bump version to…
2025-09-14 16:15:41 +02:00
aa978090fa Add debug log for email transporter configuration and bump version to 0.1.23 2025-09-14 16:14:56 +02:00
13 changed files with 483 additions and 153 deletions

View File

@@ -28,26 +28,31 @@ npm install @xtr-dev/payload-mailing
## Quick Start ## Quick Start
### 1. Add the plugin to your Payload config ### 1. Configure email in your Payload config and add the plugin
```typescript ```typescript
import { buildConfig } from 'payload/config' import { buildConfig } from 'payload/config'
import { mailingPlugin } from '@xtr-dev/payload-mailing' import { mailingPlugin } from '@xtr-dev/payload-mailing'
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
export default buildConfig({ export default buildConfig({
// ... your config // ... your config
email: nodemailerAdapter({
defaultFromAddress: 'noreply@yoursite.com',
defaultFromName: 'Your Site',
transport: {
host: 'smtp.gmail.com',
port: 587,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
},
}),
plugins: [ plugins: [
mailingPlugin({ mailingPlugin({
defaultFrom: 'noreply@yoursite.com', defaultFrom: 'noreply@yoursite.com',
transport: { defaultFromName: 'Your Site Name',
host: 'smtp.gmail.com',
port: 587,
secure: false,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
},
retryAttempts: 3, retryAttempts: 3,
retryDelay: 300000, // 5 minutes retryDelay: 300000, // 5 minutes
queue: 'email-queue', // optional queue: 'email-queue', // optional
@@ -119,13 +124,6 @@ mailingPlugin({
return yourCustomEngine.render(template, variables) return yourCustomEngine.render(template, variables)
}, },
// Email transport
transport: {
host: 'smtp.gmail.com',
port: 587,
auth: { user: '...', pass: '...' }
},
// Collection names (optional) // Collection names (optional)
collections: { collections: {
templates: 'email-templates', // default templates: 'email-templates', // default
@@ -382,7 +380,16 @@ await processEmails(payload)
await retryFailedEmails(payload) await retryFailedEmails(payload)
``` ```
## PayloadCMS Task Integration ## PayloadCMS Integration
The plugin provides both tasks and workflows for email processing:
### Tasks vs Workflows
- **Tasks**: Simple job execution, good for background processing
- **Workflows**: More advanced with UI, status tracking, and immediate processing options
### Task Integration
The plugin provides a ready-to-use PayloadCMS task for queuing template emails: The plugin provides a ready-to-use PayloadCMS task for queuing template emails:
@@ -457,6 +464,34 @@ The task can also be triggered from the Payload admin panel with a user-friendly
-**Error Handling**: Comprehensive error reporting -**Error Handling**: Comprehensive error reporting
-**Queue Management**: Leverage Payload's job queue system -**Queue Management**: Leverage Payload's job queue system
### Workflow Integration
For advanced features, use the workflow instead:
```typescript
import { sendEmailWorkflow } from '@xtr-dev/payload-mailing'
export default buildConfig({
jobs: {
workflows: [sendEmailWorkflow]
}
})
```
**Key advantage**: Optional `processImmediately` option to send emails instantly instead of queuing.
```typescript
await payload.workflows.queue({
workflow: 'send-email',
input: {
processImmediately: true, // Send immediately (default: false)
templateSlug: 'welcome-email',
to: ['user@example.com'],
variables: { name: 'John' }
}
})
```
## Job Processing ## Job Processing
The plugin automatically adds a unified email processing job to PayloadCMS: The plugin automatically adds a unified email processing job to PayloadCMS:

View File

@@ -135,15 +135,6 @@ const buildConfigWithMemoryDB = async () => {
mailingPlugin({ mailingPlugin({
defaultFrom: 'noreply@test.com', defaultFrom: 'noreply@test.com',
initOrder: 'after', initOrder: 'after',
transport: {
host: 'localhost',
port: 1025, // MailHog port for dev
secure: false,
auth: {
user: 'test',
pass: 'test',
},
},
retryAttempts: 3, retryAttempts: 3,
retryDelay: 60000, // 1 minute for dev retryDelay: 60000, // 1 minute for dev
queue: 'email-queue', queue: 'email-queue',

View File

@@ -1,6 +1,6 @@
{ {
"name": "@xtr-dev/payload-mailing", "name": "@xtr-dev/payload-mailing",
"version": "0.1.22", "version": "0.3.1",
"description": "Template-based email system with scheduling and job processing for PayloadCMS", "description": "Template-based email system with scheduling and job processing for PayloadCMS",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -15,6 +15,10 @@ export { default as Emails } from './collections/Emails.js'
export { mailingJobs, sendEmailJob } from './jobs/index.js' export { mailingJobs, sendEmailJob } from './jobs/index.js'
export type { SendEmailTaskInput } from './jobs/sendEmailTask.js' export type { SendEmailTaskInput } from './jobs/sendEmailTask.js'
// Workflows (includes the send email workflow)
export { mailingWorkflows, sendEmailWorkflow } from './workflows/index.js'
export type { SendEmailWorkflowInput } from './workflows/sendEmailWorkflow.js'
// Main email sending function // Main email sending function
export { sendEmail, type SendEmailOptions } from './sendEmail.js' export { sendEmail, type SendEmailOptions } from './sendEmail.js'
export { default as sendEmailDefault } from './sendEmail.js' export { default as sendEmailDefault } from './sendEmail.js'

View File

@@ -1,35 +1,14 @@
import { processEmailsJob, ProcessEmailsJobData } from './processEmailsJob.js' import { processEmailsJob } from './processEmailsTask.js'
import { sendEmailJob } from './sendEmailTask.js' import { sendEmailJob } from './sendEmailTask.js'
import { MailingService } from '../services/MailingService.js'
/**
* All mailing-related jobs that get registered with Payload
*/
export const mailingJobs = [ export const mailingJobs = [
{ processEmailsJob,
slug: 'processEmails',
handler: async ({ job, req }: { job: any; req: any }) => {
// Get mailing context from payload
const payload = (req as any).payload
const mailingContext = payload.mailing
if (!mailingContext) {
throw new Error('Mailing plugin not properly initialized')
}
// Use the existing mailing service from context
await processEmailsJob(
job as { data: ProcessEmailsJobData },
{ req, mailingService: mailingContext.service }
)
return {
output: {
success: true,
message: 'Email queue processing completed successfully'
}
}
},
interfaceName: 'ProcessEmailsJob',
},
sendEmailJob, sendEmailJob,
] ]
export * from './processEmailsJob.js' // Re-export everything from individual job files
export * from './sendEmailTask.js' export * from './processEmailsTask.js'
export * from './sendEmailTask.js'

View File

@@ -1,50 +0,0 @@
import type { PayloadRequest } from 'payload'
import { MailingService } from '../services/MailingService.js'
export interface ProcessEmailsJobData {
// No type needed - always processes both pending and failed emails
}
export const processEmailsJob = async (
job: { data: ProcessEmailsJobData },
context: { req: PayloadRequest; mailingService: MailingService }
) => {
const { mailingService } = context
try {
console.log('🔄 Processing email queue (pending + failed emails)...')
// Process pending emails first
await mailingService.processEmails()
// Then retry failed emails
await mailingService.retryFailedEmails()
console.log('✅ Email queue processing completed successfully (pending and failed emails)')
} catch (error) {
console.error('❌ Email queue processing failed:', error)
throw error
}
}
export const scheduleEmailsJob = async (
payload: any,
queueName: string,
delay?: number
) => {
if (!payload.jobs) {
console.warn('PayloadCMS jobs not configured - emails will not be processed automatically')
return
}
try {
await payload.jobs.queue({
queue: queueName,
task: 'processEmails',
input: {},
waitUntil: delay ? new Date(Date.now() + delay) : undefined,
})
} catch (error) {
console.error('Failed to schedule email processing job:', error)
}
}

View File

@@ -0,0 +1,95 @@
import type { PayloadRequest, Payload } from 'payload'
import type { MailingService } from '../services/MailingService.js'
/**
* Data passed to the process emails task
*/
export interface ProcessEmailsTaskData {
// Currently no data needed - always processes both pending and failed emails
}
/**
* Handler function for processing emails
* Used internally by the task definition
*/
export const processEmailsTaskHandler = async (
job: { data: ProcessEmailsTaskData },
context: { req: PayloadRequest; mailingService: MailingService }
) => {
const { mailingService } = context
try {
console.log('🔄 Processing email queue (pending + failed emails)...')
// Process pending emails first
await mailingService.processEmails()
// Then retry failed emails
await mailingService.retryFailedEmails()
console.log('✅ Email queue processing completed successfully')
} catch (error) {
console.error('❌ Email queue processing failed:', error)
throw error
}
}
/**
* Task definition for processing emails
* This is what gets registered with Payload's job system
*/
export const processEmailsTask = {
slug: 'process-emails',
handler: async ({ job, req }: { job: any; req: any }) => {
// Get mailing context from payload
const payload = (req as any).payload
const mailingContext = payload.mailing
if (!mailingContext) {
throw new Error('Mailing plugin not properly initialized')
}
// Use the existing mailing service from context
await processEmailsTaskHandler(
job as { data: ProcessEmailsTaskData },
{ req, mailingService: mailingContext.service }
)
return {
output: {
success: true,
message: 'Email queue processing completed successfully'
}
}
},
interfaceName: 'ProcessEmailsTask',
}
// For backward compatibility, export as processEmailsJob
export const processEmailsJob = processEmailsTask
/**
* Helper function to schedule an email processing job
* Used by the plugin during initialization and can be used by developers
*/
export const scheduleEmailsJob = async (
payload: Payload,
queueName: string,
delay?: number
) => {
if (!payload.jobs) {
console.warn('PayloadCMS jobs not configured - emails will not be processed automatically')
return
}
try {
await payload.jobs.queue({
queue: queueName,
task: 'process-emails',
input: {},
waitUntil: delay ? new Date(Date.now() + delay) : undefined,
} as any)
} catch (error) {
console.error('Failed to schedule email processing job:', error)
}
}

View File

@@ -64,6 +64,10 @@ function transformTaskInputToSendEmailOptions(taskInput: SendEmailTaskInput) {
return sendEmailOptions return sendEmailOptions
} }
/**
* Job definition for sending emails
* Can be used through Payload's job queue system to send emails programmatically
*/
export const sendEmailJob = { export const sendEmailJob = {
slug: 'send-email', slug: 'send-email',
label: 'Send Email', label: 'Send Email',

View File

@@ -4,17 +4,12 @@ import { MailingService } from './services/MailingService.js'
import { createEmailTemplatesCollection } from './collections/EmailTemplates.js' import { createEmailTemplatesCollection } from './collections/EmailTemplates.js'
import Emails from './collections/Emails.js' import Emails from './collections/Emails.js'
import { mailingJobs, scheduleEmailsJob } from './jobs/index.js' import { mailingJobs, scheduleEmailsJob } from './jobs/index.js'
import { mailingWorkflows } from './workflows/index.js'
export const mailingPlugin = (pluginConfig: MailingPluginConfig) => (config: Config): Config => { export const mailingPlugin = (pluginConfig: MailingPluginConfig) => (config: Config): Config => {
const queueName = pluginConfig.queue || 'default' const queueName = pluginConfig.queue || 'default'
// Validate queueName
if (!queueName || typeof queueName !== 'string') {
throw new Error('Invalid queue configuration: queue must be a non-empty string')
}
// Handle templates collection configuration // Handle templates collection configuration
const templatesConfig = pluginConfig.collections?.templates const templatesConfig = pluginConfig.collections?.templates
const templatesSlug = typeof templatesConfig === 'string' ? templatesConfig : 'email-templates' const templatesSlug = typeof templatesConfig === 'string' ? templatesConfig : 'email-templates'
@@ -92,6 +87,10 @@ export const mailingPlugin = (pluginConfig: MailingPluginConfig) => (config: Con
...(config.jobs?.tasks || []), ...(config.jobs?.tasks || []),
...mailingJobs, ...mailingJobs,
], ],
workflows: [
...(config.jobs?.workflows || []),
...mailingWorkflows,
],
}, },
onInit: async (payload: any) => { onInit: async (payload: any) => {
if (pluginConfig.initOrder === 'after' && config.onInit) { if (pluginConfig.initOrder === 'after' && config.onInit) {

View File

@@ -1,11 +1,9 @@
import { Payload } from 'payload' import { Payload } from 'payload'
import { Liquid } from 'liquidjs' import { Liquid } from 'liquidjs'
import nodemailer, { Transporter } from 'nodemailer'
import { import {
MailingPluginConfig, MailingPluginConfig,
TemplateVariables, TemplateVariables,
MailingService as IMailingService, MailingService as IMailingService,
MailingTransportConfig,
BaseEmail, BaseEmailTemplate, BaseEmailDocument, BaseEmailTemplateDocument BaseEmail, BaseEmailTemplate, BaseEmailDocument, BaseEmailTemplateDocument
} from '../types/index.js' } from '../types/index.js'
import { serializeRichTextToHTML, serializeRichTextToText } from '../utils/richTextSerializer.js' import { serializeRichTextToHTML, serializeRichTextToText } from '../utils/richTextSerializer.js'
@@ -13,11 +11,10 @@ import { serializeRichTextToHTML, serializeRichTextToText } from '../utils/richT
export class MailingService implements IMailingService { export class MailingService implements IMailingService {
public payload: Payload public payload: Payload
private config: MailingPluginConfig private config: MailingPluginConfig
private transporter!: Transporter | any private emailAdapter: any
private templatesCollection: string private templatesCollection: string
private emailsCollection: string private emailsCollection: string
private liquid: Liquid | null | false = null private liquid: Liquid | null | false = null
private transporterInitialized = false
constructor(payload: Payload, config: MailingPluginConfig) { constructor(payload: Payload, config: MailingPluginConfig) {
this.payload = payload this.payload = payload
@@ -29,34 +26,19 @@ export class MailingService implements IMailingService {
const emailsConfig = config.collections?.emails const emailsConfig = config.collections?.emails
this.emailsCollection = typeof emailsConfig === 'string' ? emailsConfig : 'emails' this.emailsCollection = typeof emailsConfig === 'string' ? emailsConfig : 'emails'
this.initializeTransporter() // Use Payload's configured email adapter
} if (!this.payload.email) {
throw new Error('Payload email configuration is required. Please configure email in your Payload config.')
private initializeTransporter(): void {
if (this.transporterInitialized) return
if (this.config.transport) {
if ('sendMail' in this.config.transport) {
this.transporter = this.config.transport
} else {
this.transporter = nodemailer.createTransport(this.config.transport as MailingTransportConfig)
}
} else if (this.payload.email && 'sendMail' in this.payload.email) {
// Use Payload's configured mailer (cast to any to handle different adapter types)
this.transporter = this.payload.email as any
} else {
throw new Error('Email transport configuration is required either in plugin config or Payload config')
} }
this.emailAdapter = this.payload.email
this.transporterInitialized = true
} }
private ensureInitialized(): void { private ensureInitialized(): void {
if (!this.payload || !this.payload.db) { if (!this.payload || !this.payload.db) {
throw new Error('MailingService payload not properly initialized') throw new Error('MailingService payload not properly initialized')
} }
if (!this.transporterInitialized) { if (!this.emailAdapter) {
this.initializeTransporter() throw new Error('Email adapter not configured. Please ensure Payload has email configured.')
} }
} }
@@ -243,7 +225,7 @@ export class MailingService implements IMailingService {
} }
} }
private async processEmailItem(emailId: string): Promise<void> { async processEmailItem(emailId: string): Promise<void> {
try { try {
await this.payload.update({ await this.payload.update({
collection: this.emailsCollection as any, collection: this.emailsCollection as any,
@@ -302,7 +284,8 @@ export class MailingService implements IMailingService {
} }
} }
await this.transporter.sendMail(mailOptions) // Send email using Payload's email adapter
await this.emailAdapter.sendEmail(mailOptions)
await this.payload.update({ await this.payload.update({
collection: this.emailsCollection as any, collection: this.emailsCollection as any,

View File

@@ -1,6 +1,5 @@
import { Payload } from 'payload' import { Payload } from 'payload'
import type { CollectionConfig, RichTextField } from 'payload' import type { CollectionConfig, RichTextField } from 'payload'
import { Transporter } from 'nodemailer'
// JSON value type that matches Payload's JSON field type // JSON value type that matches Payload's JSON field type
export type JSONValue = string | number | boolean | { [k: string]: unknown } | unknown[] | null | undefined export type JSONValue = string | number | boolean | { [k: string]: unknown } | unknown[] | null | undefined
@@ -70,7 +69,6 @@ export interface MailingPluginConfig {
} }
defaultFrom?: string defaultFrom?: string
defaultFromName?: string defaultFromName?: string
transport?: Transporter | MailingTransportConfig
queue?: string queue?: string
retryAttempts?: number retryAttempts?: number
retryDelay?: number retryDelay?: number
@@ -82,17 +80,6 @@ export interface MailingPluginConfig {
initOrder?: 'before' | 'after' initOrder?: 'before' | 'after'
} }
export interface MailingTransportConfig {
host: string
port: number
secure?: boolean
auth?: {
user: string
pass: string
}
}
export interface QueuedEmail { export interface QueuedEmail {
id: string id: string
template?: string | null template?: string | null
@@ -124,6 +111,7 @@ export interface TemplateVariables {
export interface MailingService { export interface MailingService {
processEmails(): Promise<void> processEmails(): Promise<void>
processEmailItem(emailId: string): Promise<void>
retryFailedEmails(): Promise<void> retryFailedEmails(): Promise<void>
renderTemplate(templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }> renderTemplate(templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }>
} }

11
src/workflows/index.ts Normal file
View File

@@ -0,0 +1,11 @@
import { sendEmailWorkflow } from './sendEmailWorkflow.js'
/**
* All mailing-related workflows that get registered with Payload
*/
export const mailingWorkflows = [
sendEmailWorkflow,
]
// Re-export everything from individual workflow files
export * from './sendEmailWorkflow.js'

View File

@@ -0,0 +1,291 @@
import { sendEmail } from '../sendEmail.js'
import { BaseEmailDocument } from '../types/index.js'
export interface SendEmailWorkflowInput {
// Template mode fields
templateSlug?: string
variables?: Record<string, any>
// Direct email mode fields
subject?: string
html?: string
text?: string
// Common fields
to: string | string[]
cc?: string | string[]
bcc?: string | string[]
from?: string
fromName?: string
replyTo?: string
scheduledAt?: string | Date // ISO date string or Date object
priority?: number
// Workflow-specific option
processImmediately?: boolean // If true, process the email immediately instead of waiting for the queue
// Allow any additional fields that users might have in their email collection
[key: string]: any
}
/**
* Transforms workflow input into sendEmail options by separating template and data fields
*/
function transformWorkflowInputToSendEmailOptions(workflowInput: SendEmailWorkflowInput) {
const sendEmailOptions: any = {
data: {}
}
// If using template mode, set template options
if (workflowInput.templateSlug) {
sendEmailOptions.template = {
slug: workflowInput.templateSlug,
variables: workflowInput.variables || {}
}
}
// Standard email fields that should be copied to data
const standardFields = ['to', 'cc', 'bcc', 'from', 'fromName', 'replyTo', 'subject', 'html', 'text', 'scheduledAt', 'priority']
// Fields that should not be copied to data
const excludedFields = ['templateSlug', 'variables', 'processImmediately']
// Copy standard fields to data
standardFields.forEach(field => {
if (workflowInput[field] !== undefined) {
sendEmailOptions.data[field] = workflowInput[field]
}
})
// Copy any additional custom fields
Object.keys(workflowInput).forEach(key => {
if (!excludedFields.includes(key) && !standardFields.includes(key)) {
sendEmailOptions.data[key] = workflowInput[key]
}
})
return sendEmailOptions
}
/**
* Workflow for sending emails with optional immediate processing
* Can be used through Payload's workflow system to send emails programmatically
*/
export const sendEmailWorkflow = {
slug: 'send-email',
label: 'Send Email',
inputSchema: [
{
name: 'processImmediately',
type: 'checkbox' as const,
label: 'Process Immediately',
defaultValue: false,
admin: {
description: 'Process and send the email immediately instead of waiting for the queue processor'
}
},
{
name: 'templateSlug',
type: 'text' as const,
label: 'Template Slug',
admin: {
description: 'Use a template (leave empty for direct email)',
condition: (data: any) => !data.subject && !data.html
}
},
{
name: 'variables',
type: 'json' as const,
label: 'Template Variables',
admin: {
description: 'JSON object with variables for template rendering',
condition: (data: any) => Boolean(data.templateSlug)
}
},
{
name: 'subject',
type: 'text' as const,
label: 'Subject',
admin: {
description: 'Email subject (required if not using template)',
condition: (data: any) => !data.templateSlug
}
},
{
name: 'html',
type: 'textarea' as const,
label: 'HTML Content',
admin: {
description: 'HTML email content (required if not using template)',
condition: (data: any) => !data.templateSlug
}
},
{
name: 'text',
type: 'textarea' as const,
label: 'Text Content',
admin: {
description: 'Plain text email content (optional)',
condition: (data: any) => !data.templateSlug
}
},
{
name: 'to',
type: 'text' as const,
required: true,
label: 'To (Email Recipients)',
admin: {
description: 'Comma-separated list of email addresses'
}
},
{
name: 'cc',
type: 'text' as const,
label: 'CC (Carbon Copy)',
admin: {
description: 'Optional comma-separated list of CC email addresses'
}
},
{
name: 'bcc',
type: 'text' as const,
label: 'BCC (Blind Carbon Copy)',
admin: {
description: 'Optional comma-separated list of BCC email addresses'
}
},
{
name: 'from',
type: 'text' as const,
label: 'From Email',
admin: {
description: 'Optional sender email address (uses default if not provided)'
}
},
{
name: 'fromName',
type: 'text' as const,
label: 'From Name',
admin: {
description: 'Optional sender display name (e.g., "John Doe")'
}
},
{
name: 'replyTo',
type: 'text' as const,
label: 'Reply To',
admin: {
description: 'Optional reply-to email address'
}
},
{
name: 'scheduledAt',
type: 'date' as const,
label: 'Schedule For',
admin: {
description: 'Optional date/time to schedule email for future delivery',
condition: (data: any) => !data.processImmediately
}
},
{
name: 'priority',
type: 'number' as const,
label: 'Priority',
min: 1,
max: 10,
defaultValue: 5,
admin: {
description: 'Email priority (1 = highest, 10 = lowest)'
}
}
],
handler: async ({ job, req }: any) => {
const { input, id, taskStatus } = job
const { payload } = req
// Cast input to our expected type
const workflowInput = input as SendEmailWorkflowInput
const shouldProcessImmediately = workflowInput.processImmediately || false
try {
console.log(`📧 Workflow ${id}: Creating email...`)
// Transform workflow input into sendEmail options
const sendEmailOptions = transformWorkflowInputToSendEmailOptions(workflowInput)
// Create the email in the database
const email = await sendEmail<BaseEmailDocument>(payload, sendEmailOptions)
console.log(`✅ Workflow ${id}: Email created with ID: ${email.id}`)
// Update task status with email ID
if (taskStatus) {
await taskStatus.update({
data: {
emailId: email.id,
status: 'created'
}
})
}
// If processImmediately is true, process the email now
if (shouldProcessImmediately) {
console.log(`⚡ Workflow ${id}: Processing email immediately...`)
// Get the mailing service from context
const mailingContext = payload.mailing
if (!mailingContext || !mailingContext.service) {
throw new Error('Mailing plugin not properly initialized')
}
// Process just this specific email
await mailingContext.service.processEmailItem(String(email.id))
console.log(`✅ Workflow ${id}: Email processed and sent immediately`)
// Update task status
if (taskStatus) {
await taskStatus.update({
data: {
emailId: email.id,
status: 'sent',
processedImmediately: true
}
})
}
} else {
// Update task status for queued email
if (taskStatus) {
await taskStatus.update({
data: {
emailId: email.id,
status: 'queued',
processedImmediately: false
}
})
}
}
} catch (error) {
console.error(`❌ Workflow ${id}: Failed to process email:`, error)
// Update task status with error
if (taskStatus) {
await taskStatus.update({
data: {
status: 'failed',
error: error instanceof Error ? error.message : String(error)
}
})
}
if (error instanceof Error) {
throw new Error(`Failed to process email: ${error.message}`, { cause: error })
} else {
throw new Error(`Failed to process email: ${String(error)}`)
}
}
}
}
export default sendEmailWorkflow