mirror of
https://github.com/xtr-dev/payload-mailing.git
synced 2025-12-10 08:13:23 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
685875d1b9 | ||
| 79044b7bc3 | |||
| e7304fe1a2 | |||
| 790eedfee7 | |||
| 9520ec5ed1 | |||
|
|
768b70a003 | ||
| e91ab7e54e | |||
| 06f9c2cb5b | |||
|
|
21b22a033a | ||
| 6ad90874cf |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xtr-dev/payload-mailing",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.12",
|
||||
"description": "Template-based email system with scheduling and job processing for PayloadCMS",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sendEmail } from '../sendEmail.js'
|
||||
import { Email } from '../payload-types.js'
|
||||
import {Email, EmailTemplate} from '../payload-types.js'
|
||||
|
||||
export interface SendEmailTaskInput {
|
||||
// Template mode fields
|
||||
@@ -22,6 +22,45 @@ export interface SendEmailTaskInput {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms task input into sendEmail options by separating template and data fields
|
||||
*/
|
||||
function transformTaskInputToSendEmailOptions(taskInput: SendEmailTaskInput) {
|
||||
const sendEmailOptions: any = {
|
||||
data: {}
|
||||
}
|
||||
|
||||
// If using template mode, set template options
|
||||
if (taskInput.templateSlug) {
|
||||
sendEmailOptions.template = {
|
||||
slug: taskInput.templateSlug,
|
||||
variables: taskInput.variables || {}
|
||||
}
|
||||
}
|
||||
|
||||
// Standard email fields that should be copied to data
|
||||
const standardFields = ['to', 'cc', 'bcc', 'subject', 'html', 'text', 'scheduledAt', 'priority']
|
||||
|
||||
// Template-specific fields that should not be copied to data
|
||||
const templateFields = ['templateSlug', 'variables']
|
||||
|
||||
// Copy standard fields to data
|
||||
standardFields.forEach(field => {
|
||||
if (taskInput[field] !== undefined) {
|
||||
sendEmailOptions.data[field] = taskInput[field]
|
||||
}
|
||||
})
|
||||
|
||||
// Copy any additional custom fields that aren't template or standard fields
|
||||
Object.keys(taskInput).forEach(key => {
|
||||
if (!templateFields.includes(key) && !standardFields.includes(key)) {
|
||||
sendEmailOptions.data[key] = taskInput[key]
|
||||
}
|
||||
})
|
||||
|
||||
return sendEmailOptions
|
||||
}
|
||||
|
||||
export const sendEmailJob = {
|
||||
slug: 'send-email',
|
||||
label: 'Send Email',
|
||||
@@ -116,42 +155,19 @@ export const sendEmailJob = {
|
||||
}
|
||||
}
|
||||
],
|
||||
outputSchema: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'text' as const
|
||||
}
|
||||
],
|
||||
handler: async ({ input, payload }: any) => {
|
||||
// Cast input to our expected type
|
||||
const taskInput = input as SendEmailTaskInput
|
||||
|
||||
try {
|
||||
// Prepare options for sendEmail based on task input
|
||||
const sendEmailOptions: any = {
|
||||
data: {}
|
||||
}
|
||||
|
||||
// If using template mode
|
||||
if (taskInput.templateSlug) {
|
||||
sendEmailOptions.template = {
|
||||
slug: taskInput.templateSlug,
|
||||
variables: taskInput.variables || {}
|
||||
}
|
||||
}
|
||||
|
||||
// Build data object from task input
|
||||
const dataFields = ['to', 'cc', 'bcc', 'subject', 'html', 'text', 'scheduledAt', 'priority']
|
||||
const additionalFields: string[] = []
|
||||
|
||||
// Copy standard fields
|
||||
dataFields.forEach(field => {
|
||||
if (taskInput[field] !== undefined) {
|
||||
sendEmailOptions.data[field] = taskInput[field]
|
||||
}
|
||||
})
|
||||
|
||||
// Copy any additional custom fields
|
||||
Object.keys(taskInput).forEach(key => {
|
||||
if (!['templateSlug', 'variables', ...dataFields].includes(key)) {
|
||||
sendEmailOptions.data[key] = taskInput[key]
|
||||
additionalFields.push(key)
|
||||
}
|
||||
})
|
||||
// Transform task input into sendEmail options using helper function
|
||||
const sendEmailOptions = transformTaskInputToSendEmailOptions(taskInput)
|
||||
|
||||
// Use the sendEmail helper to create the email
|
||||
const email = await sendEmail<Email>(payload, sendEmailOptions)
|
||||
@@ -159,21 +175,22 @@ export const sendEmailJob = {
|
||||
return {
|
||||
output: {
|
||||
success: true,
|
||||
emailId: email.id,
|
||||
message: `Email queued successfully with ID: ${email.id}`,
|
||||
mode: taskInput.templateSlug ? 'template' : 'direct',
|
||||
templateSlug: taskInput.templateSlug || null,
|
||||
subject: email.subject,
|
||||
recipients: Array.isArray(email.to) ? email.to.length : 1,
|
||||
scheduledAt: email.scheduledAt || null
|
||||
id: email.id,
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
throw new Error(`Failed to queue email: ${errorMessage}`)
|
||||
if (error instanceof Error) {
|
||||
// Preserve original error and stack trace
|
||||
const wrappedError = new Error(`Failed to queue email: ${error.message}`)
|
||||
wrappedError.stack = error.stack
|
||||
wrappedError.cause = error
|
||||
throw wrappedError
|
||||
} else {
|
||||
throw new Error(`Failed to queue email: ${String(error)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default sendEmailJob
|
||||
export default sendEmailJob
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Payload } from 'payload'
|
||||
import { getMailing, renderTemplate, parseAndValidateEmails } from './utils/helpers.js'
|
||||
import {Email} from "./payload-types.js"
|
||||
import {Email, EmailTemplate} from "./payload-types.js"
|
||||
|
||||
// Options for sending emails
|
||||
export interface SendEmailOptions<T extends Email = Email> {
|
||||
@@ -35,14 +35,14 @@ export interface SendEmailOptions<T extends Email = Email> {
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const sendEmail = async <T extends Email = Email>(
|
||||
export const sendEmail = async <TEmail extends Email = Email>(
|
||||
payload: Payload,
|
||||
options: SendEmailOptions<T>
|
||||
): Promise<T> => {
|
||||
options: SendEmailOptions<TEmail>
|
||||
): Promise<TEmail> => {
|
||||
const mailing = getMailing(payload)
|
||||
const collectionSlug = options.collectionSlug || mailing.collections.emails || 'emails'
|
||||
|
||||
let emailData: Partial<T> = { ...options.data } as Partial<T>
|
||||
let emailData: Partial<TEmail> = { ...options.data } as Partial<TEmail>
|
||||
|
||||
// If using a template, render it first
|
||||
if (options.template) {
|
||||
@@ -58,7 +58,7 @@ export const sendEmail = async <T extends Email = Email>(
|
||||
subject,
|
||||
html,
|
||||
text,
|
||||
} as Partial<T>
|
||||
} as Partial<TEmail>
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
@@ -96,7 +96,12 @@ export const sendEmail = async <T extends Email = Email>(
|
||||
data: emailData
|
||||
})
|
||||
|
||||
return email as T
|
||||
// Validate that the created email has the expected structure
|
||||
if (!email || typeof email !== 'object' || !email.id) {
|
||||
throw new Error('Failed to create email: invalid response from database')
|
||||
}
|
||||
|
||||
return email as TEmail
|
||||
}
|
||||
|
||||
export default sendEmail
|
||||
|
||||
@@ -5,10 +5,8 @@ import {
|
||||
MailingPluginConfig,
|
||||
TemplateVariables,
|
||||
MailingService as IMailingService,
|
||||
EmailTemplate,
|
||||
QueuedEmail,
|
||||
MailingTransportConfig,
|
||||
BaseEmail
|
||||
BaseEmail, BaseEmailTemplate
|
||||
} from '../types/index.js'
|
||||
import { serializeRichTextToHTML, serializeRichTextToText } from '../utils/richTextSerializer.js'
|
||||
|
||||
@@ -287,7 +285,7 @@ export class MailingService implements IMailingService {
|
||||
const email = await this.payload.findByID({
|
||||
collection: this.emailsCollection as any,
|
||||
id: emailId,
|
||||
}) as QueuedEmail
|
||||
}) as BaseEmail
|
||||
|
||||
const newAttempts = (email.attempts || 0) + 1
|
||||
|
||||
@@ -302,7 +300,7 @@ export class MailingService implements IMailingService {
|
||||
return newAttempts
|
||||
}
|
||||
|
||||
private async getTemplateBySlug(templateSlug: string): Promise<EmailTemplate | null> {
|
||||
private async getTemplateBySlug(templateSlug: string): Promise<BaseEmailTemplate | null> {
|
||||
try {
|
||||
const { docs } = await this.payload.find({
|
||||
collection: this.templatesCollection as any,
|
||||
@@ -314,7 +312,7 @@ export class MailingService implements IMailingService {
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
return docs.length > 0 ? docs[0] as EmailTemplate : null
|
||||
return docs.length > 0 ? docs[0] as BaseEmailTemplate : null
|
||||
} catch (error) {
|
||||
console.error(`Template with slug '${templateSlug}' not found:`, error)
|
||||
return null
|
||||
@@ -379,7 +377,7 @@ export class MailingService implements IMailingService {
|
||||
})
|
||||
}
|
||||
|
||||
private async renderEmailTemplate(template: EmailTemplate, variables: Record<string, any> = {}): Promise<{ html: string; text: string }> {
|
||||
private async renderEmailTemplate(template: BaseEmailTemplate, variables: Record<string, any> = {}): Promise<{ html: string; text: string }> {
|
||||
if (!template.content) {
|
||||
return { html: '', text: '' }
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Payload } from 'payload'
|
||||
import type { CollectionConfig, RichTextField } from 'payload'
|
||||
import { Transporter } from 'nodemailer'
|
||||
import {Email} from "../payload-types.js"
|
||||
import {Email, EmailTemplate} from "../payload-types.js"
|
||||
|
||||
export type BaseEmail<TEmail = Email, TEmailTemplate = EmailTemplate> = Omit<TEmail, 'id' | 'template'> & {template: Omit<TEmailTemplate, 'id'>}
|
||||
export type BaseEmail<TEmail extends Email = Email, TEmailTemplate extends EmailTemplate = EmailTemplate> = Omit<TEmail, 'id' | 'template'> & {template: Omit<TEmailTemplate, 'id'> | TEmailTemplate['id'] | undefined | null}
|
||||
|
||||
export type BaseEmailTemplate<TEmailTemplate extends EmailTemplate = EmailTemplate> = Omit<TEmailTemplate, 'id'>
|
||||
|
||||
export type TemplateRendererHook = (template: string, variables: Record<string, any>) => string | Promise<string>
|
||||
|
||||
@@ -37,16 +39,6 @@ export interface MailingTransportConfig {
|
||||
}
|
||||
}
|
||||
|
||||
export interface EmailTemplate {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
subject: string
|
||||
content: any // Lexical editor state
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
|
||||
export interface QueuedEmail {
|
||||
id: string
|
||||
|
||||
Reference in New Issue
Block a user