mirror of
https://github.com/xtr-dev/payload-mailing.git
synced 2025-12-10 08:13:23 +00:00
- Update QueuedEmail interface to include `| null` for optional fields to match BaseEmailDocument - Add missing null checks for replyTo and from fields in sendEmail processing - Add proper email validation for replyTo and from fields (single email addresses) - Ensure type consistency across all email-related interfaces Fixes potential type conflicts between QueuedEmail and BaseEmailDocument, and ensures all nullable email fields are properly validated. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
118 lines
3.8 KiB
TypeScript
118 lines
3.8 KiB
TypeScript
import { Payload } from 'payload'
|
|
import { getMailing, renderTemplate, parseAndValidateEmails } from './utils/helpers.js'
|
|
import { BaseEmailDocument } from './types/index.js'
|
|
|
|
// Options for sending emails
|
|
export interface SendEmailOptions<T extends BaseEmailDocument = BaseEmailDocument> {
|
|
// Template-based email
|
|
template?: {
|
|
slug: string
|
|
variables?: Record<string, any>
|
|
}
|
|
// Direct email data
|
|
data?: Partial<T>
|
|
// 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<Email>(payload, {
|
|
* template: {
|
|
* slug: 'welcome',
|
|
* variables: { name: 'John' }
|
|
* },
|
|
* data: {
|
|
* to: 'user@example.com',
|
|
* customField: 'value' // Your custom fields are type-safe!
|
|
* }
|
|
* })
|
|
* ```
|
|
*/
|
|
export const sendEmail = async <TEmail extends BaseEmailDocument = BaseEmailDocument>(
|
|
payload: Payload,
|
|
options: SendEmailOptions<TEmail>
|
|
): Promise<TEmail> => {
|
|
const mailing = getMailing(payload)
|
|
const collectionSlug = options.collectionSlug || mailing.collections.emails || 'emails'
|
|
|
|
let emailData: Partial<TEmail> = { ...options.data } as Partial<TEmail>
|
|
|
|
// 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<TEmail>
|
|
}
|
|
|
|
// Validate required fields
|
|
if (!emailData.to) {
|
|
throw new Error('Field "to" is required for sending emails')
|
|
}
|
|
|
|
// Validate required fields based on whether template was used
|
|
if (options.template) {
|
|
// When using template, subject and html should have been set by renderTemplate
|
|
if (!emailData.subject || !emailData.html) {
|
|
throw new Error(`Template rendering failed: template "${options.template.slug}" did not provide required subject and html content`)
|
|
}
|
|
} else {
|
|
// When not using template, user must provide subject and html directly
|
|
if (!emailData.subject || !emailData.html) {
|
|
throw new Error('Fields "subject" and "html" are required when sending direct emails without a template')
|
|
}
|
|
}
|
|
|
|
// Process email addresses using shared validation (handle null values)
|
|
if (emailData.to) {
|
|
emailData.to = parseAndValidateEmails(emailData.to as string | string[])
|
|
}
|
|
if (emailData.cc && emailData.cc !== null) {
|
|
emailData.cc = parseAndValidateEmails(emailData.cc as string | string[])
|
|
}
|
|
if (emailData.bcc && emailData.bcc !== null) {
|
|
emailData.bcc = parseAndValidateEmails(emailData.bcc as string | string[])
|
|
}
|
|
if (emailData.replyTo && emailData.replyTo !== null) {
|
|
const validated = parseAndValidateEmails(emailData.replyTo as string | string[])
|
|
// replyTo should be a single email, so take the first one if array
|
|
emailData.replyTo = validated && validated.length > 0 ? validated[0] : undefined
|
|
}
|
|
if (emailData.from && emailData.from !== null) {
|
|
const validated = parseAndValidateEmails(emailData.from as string | string[])
|
|
// from should be a single email, so take the first one if array
|
|
emailData.from = validated && validated.length > 0 ? validated[0] : undefined
|
|
}
|
|
|
|
// Create the email in the collection with proper typing
|
|
const email = await payload.create({
|
|
collection: collectionSlug,
|
|
data: emailData
|
|
})
|
|
|
|
// 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
|