diff --git a/README.md b/README.md index 0ceab05..2381749 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ export default buildConfig({ ### 2. Send emails with type-safe helper ```typescript +// sendEmail is a primary export for easy access import { sendEmail } from '@xtr-dev/payload-mailing' import { Email } from './payload-types' // Your generated types diff --git a/dev/README.md b/dev/README.md index bcea813..5e7f8e2 100644 --- a/dev/README.md +++ b/dev/README.md @@ -184,25 +184,43 @@ The plugin automatically processes the outbox every 5 minutes and retries failed ## Plugin API Usage ```javascript -import { sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing' +import { sendEmail } from '@xtr-dev/payload-mailing' -// Send immediate email -const emailId = await sendEmail(payload, { - templateId: 'welcome-template-id', - to: 'user@example.com', - variables: { - firstName: 'John', - siteName: 'My App' +// Send immediate email with template +const email = await sendEmail(payload, { + template: { + slug: 'welcome-email', + variables: { + firstName: 'John', + siteName: 'My App' + } + }, + data: { + to: 'user@example.com', } }) -// Schedule email -const scheduledId = await scheduleEmail(payload, { - templateId: 'reminder-template-id', - to: 'user@example.com', - scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours - variables: { - eventName: 'Product Launch' +// Schedule email for later +const scheduledEmail = await sendEmail(payload, { + template: { + slug: 'reminder', + variables: { + eventName: 'Product Launch' + } + }, + data: { + to: 'user@example.com', + scheduledAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours + } +}) + +// Send direct HTML email (no template) +const directEmail = await sendEmail(payload, { + data: { + to: 'user@example.com', + subject: 'Direct Email', + html: '

Hello World

', + text: 'Hello World' } }) ``` \ No newline at end of file diff --git a/dev/app/api/test-email/route.ts b/dev/app/api/test-email/route.ts index 3572ca4..bf16533 100644 --- a/dev/app/api/test-email/route.ts +++ b/dev/app/api/test-email/route.ts @@ -1,37 +1,50 @@ import { getPayload } from 'payload' import config from '@payload-config' -import { sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing' +import { sendEmail } from '@xtr-dev/payload-mailing' export async function POST(request: Request) { try { const payload = await getPayload({ config }) const body = await request.json() - const { type = 'send', templateSlug, to, variables, scheduledAt } = body + const { type = 'send', templateSlug, to, variables, scheduledAt, subject, html, text } = body - let result - if (type === 'send') { - // Send immediately - result = await sendEmail(payload, { - templateSlug, + // Use the new sendEmail API + const emailOptions: any = { + data: { to, - variables, - }) - } else if (type === 'schedule') { - // Schedule for later - result = await scheduleEmail(payload, { - templateSlug, - to, - variables, - scheduledAt: scheduledAt ? new Date(scheduledAt) : new Date(Date.now() + 60000), // Default to 1 minute - }) - } else { - return Response.json({ error: 'Invalid type. Use "send" or "schedule"' }, { status: 400 }) + } } + // Add template if provided + if (templateSlug) { + emailOptions.template = { + slug: templateSlug, + variables: variables || {} + } + } else if (subject && html) { + // Direct email without template + emailOptions.data.subject = subject + emailOptions.data.html = html + if (text) { + emailOptions.data.text = text + } + } else { + return Response.json({ + error: 'Either templateSlug or subject+html must be provided' + }, { status: 400 }) + } + + // Add scheduling if needed + if (type === 'schedule' || scheduledAt) { + emailOptions.data.scheduledAt = scheduledAt ? new Date(scheduledAt) : new Date(Date.now() + 60000) + } + + const result = await sendEmail(payload, emailOptions) + return Response.json({ success: true, - emailId: result, - message: type === 'send' ? 'Email sent successfully' : 'Email scheduled successfully', + emailId: result.id, + message: scheduledAt ? 'Email scheduled successfully' : 'Email queued successfully', }) } catch (error) { console.error('Test email error:', error) diff --git a/dev/test-plugin.mjs b/dev/test-plugin.mjs index 6d7cf34..0fe1382 100644 --- a/dev/test-plugin.mjs +++ b/dev/test-plugin.mjs @@ -1,10 +1,10 @@ // Simple test to verify plugin can be imported and initialized -import { mailingPlugin, sendEmail, scheduleEmail } from '@xtr-dev/payload-mailing' +import { mailingPlugin, sendEmail, renderTemplate } from '@xtr-dev/payload-mailing' console.log('✅ Plugin imports successfully') console.log('✅ mailingPlugin:', typeof mailingPlugin) -console.log('✅ sendEmail:', typeof sendEmail) -console.log('✅ scheduleEmail:', typeof scheduleEmail) +console.log('✅ sendEmail:', typeof sendEmail) +console.log('✅ renderTemplate:', typeof renderTemplate) // Test plugin configuration try { diff --git a/package.json b/package.json index 89ace5f..64734c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/payload-mailing", - "version": "0.1.5", + "version": "0.1.6", "description": "Template-based email system with scheduling and job processing for PayloadCMS", "type": "module", "main": "dist/index.js", diff --git a/src/index.ts b/src/index.ts index 70288ff..3606914 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,13 +15,15 @@ export { default as Emails } from './collections/Emails.js' export { mailingJobs, sendEmailJob } from './jobs/index.js' export type { SendEmailTaskInput } from './jobs/sendEmailTask.js' +// Main email sending function +export { sendEmail, type BaseEmailData, type SendEmailOptions } from './sendEmail.js' +export { default as sendEmailDefault } from './sendEmail.js' + // Utility functions for developers export { getMailing, renderTemplate, processEmails, retryFailedEmails, - sendEmail, - type BaseEmailData, - type SendEmailOptions, + parseAndValidateEmails, } from './utils/helpers.js' \ No newline at end of file diff --git a/src/jobs/sendEmailTask.ts b/src/jobs/sendEmailTask.ts index a0e8042..474867a 100644 --- a/src/jobs/sendEmailTask.ts +++ b/src/jobs/sendEmailTask.ts @@ -1,4 +1,4 @@ -import { sendEmail, type BaseEmailData } from '../utils/helpers.js' +import { sendEmail, type BaseEmailData } from '../sendEmail.js' export interface SendEmailTaskInput { // Template mode fields diff --git a/src/sendEmail.ts b/src/sendEmail.ts new file mode 100644 index 0000000..7f514de --- /dev/null +++ b/src/sendEmail.ts @@ -0,0 +1,110 @@ +import { Payload } from 'payload' +import { getMailing, renderTemplate, parseAndValidateEmails } from './utils/helpers.js' + +// Base type for email data that all emails must have +export interface BaseEmailData { + to: string | string[] + cc?: string | string[] + bcc?: string | string[] + subject?: string + html?: string + text?: string + scheduledAt?: string | Date + priority?: number + [key: string]: any +} + +// Options for sending emails +export interface SendEmailOptions { + // Template-based email + template?: { + slug: string + variables?: Record + } + // Direct email data + data?: Partial + // Common options + collectionSlug?: string // defaults to 'emails' +} + +/** + * Send an email with full type safety + * + * @example + * ```typescript + * // With your generated Email type + * import { Email } from './payload-types' + * + * const email = await sendEmail(payload, { + * template: { + * slug: 'welcome', + * variables: { name: 'John' } + * }, + * data: { + * to: 'user@example.com', + * customField: 'value' // Your custom fields are type-safe! + * } + * }) + * ``` + */ +export const sendEmail = async ( + payload: Payload, + options: SendEmailOptions +): Promise => { + const mailing = getMailing(payload) + const collectionSlug = options.collectionSlug || mailing.collections.emails || 'emails' + + let emailData: Partial = { ...options.data } as Partial + + // If using a template, render it first + if (options.template) { + const { html, text, subject } = await renderTemplate( + payload, + options.template.slug, + options.template.variables || {} + ) + + // Template values take precedence over data values + emailData = { + ...emailData, + subject, + html, + text, + } as Partial + } + + // Validate required fields + if (!emailData.to) { + throw new Error('Field "to" is required for sending emails') + } + + if (!emailData.subject || !emailData.html) { + throw new Error('Fields "subject" and "html" are required when not using a template') + } + + // Process email addresses using shared validation + if (emailData.to) { + emailData.to = parseAndValidateEmails(emailData.to as string | string[]) + } + if (emailData.cc) { + emailData.cc = parseAndValidateEmails(emailData.cc as string | string[]) + } + if (emailData.bcc) { + emailData.bcc = parseAndValidateEmails(emailData.bcc as string | string[]) + } + + // Convert scheduledAt to ISO string if it's a Date + if (emailData.scheduledAt instanceof Date) { + emailData.scheduledAt = emailData.scheduledAt.toISOString() + } + + // Create the email in the collection + const email = await payload.create({ + collection: collectionSlug as any, + data: emailData as any + }) + + return email as unknown as T +} + +export default sendEmail \ No newline at end of file diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index a2a70b9..deec373 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,32 +1,6 @@ import { Payload } from 'payload' import { TemplateVariables } from '../types/index.js' -// Base type for email data that all emails must have -export interface BaseEmailData { - to: string | string[] - cc?: string | string[] - bcc?: string | string[] - subject?: string - html?: string - text?: string - scheduledAt?: string | Date - priority?: number - [key: string]: any -} - -// Options for sending emails -export interface SendEmailOptions { - // Template-based email - template?: { - slug: string - variables?: Record - } - // Direct email data - data?: Partial - // Common options - collectionSlug?: string // defaults to 'emails' -} - /** * Parse and validate email addresses * @internal @@ -72,84 +46,4 @@ export const processEmails = async (payload: Payload): Promise => { export const retryFailedEmails = async (payload: Payload): Promise => { const mailing = getMailing(payload) return mailing.service.retryFailedEmails() -} - -/** - * Send an email with full type safety - * - * @example - * ```typescript - * // With your generated Email type - * import { Email } from './payload-types' - * - * const email = await sendEmail(payload, { - * template: { - * slug: 'welcome', - * variables: { name: 'John' } - * }, - * data: { - * to: 'user@example.com', - * customField: 'value' // Your custom fields are type-safe! - * } - * }) - * ``` - */ -export const sendEmail = async ( - payload: Payload, - options: SendEmailOptions -): Promise => { - const mailing = getMailing(payload) - const collectionSlug = options.collectionSlug || mailing.collections.emails || 'emails' - - let emailData: Partial = { ...options.data } as Partial - - // If using a template, render it first - if (options.template) { - const { html, text, subject } = await renderTemplate( - payload, - options.template.slug, - options.template.variables || {} - ) - - // Template values take precedence over data values - emailData = { - ...emailData, - subject, - html, - text, - } as Partial - } - - // Validate required fields - if (!emailData.to) { - throw new Error('Field "to" is required for sending emails') - } - - if (!emailData.subject || !emailData.html) { - throw new Error('Fields "subject" and "html" are required when not using a template') - } - - // Process email addresses using shared validation - if (emailData.to) { - emailData.to = parseAndValidateEmails(emailData.to as string | string[]) - } - if (emailData.cc) { - emailData.cc = parseAndValidateEmails(emailData.cc as string | string[]) - } - if (emailData.bcc) { - emailData.bcc = parseAndValidateEmails(emailData.bcc as string | string[]) - } - - // Convert scheduledAt to ISO string if it's a Date - if (emailData.scheduledAt instanceof Date) { - emailData.scheduledAt = emailData.scheduledAt.toISOString() - } - - // Create the email in the collection - const email = await payload.create({ - collection: collectionSlug as any, - data: emailData as any - }) - - return email as unknown as T } \ No newline at end of file