import { Payload } from 'payload' import { TemplateVariables } from '../types/index.js' /** * Parse and validate email addresses * @internal */ export const parseAndValidateEmails = (emails: string | string[] | null | undefined): string[] | undefined => { if (!emails || emails === null) return undefined let emailList: string[] if (Array.isArray(emails)) { emailList = emails } else { emailList = emails.split(',').map(email => email.trim()).filter(Boolean) } // RFC 5322 compliant email validation const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ const invalidEmails = emailList.filter(email => { // Check basic format if (!emailRegex.test(email)) return true // Check for common invalid patterns if (email.includes('..') || email.startsWith('.') || email.endsWith('.')) return true if (email.includes('@.') || email.includes('.@')) return true // Check domain has at least one dot const parts = email.split('@') if (parts.length !== 2 || !parts[1].includes('.')) return true return false }) if (invalidEmails.length > 0) { throw new Error(`Invalid email addresses: ${invalidEmails.join(', ')}`) } return emailList } /** * Sanitize display names to prevent email header injection * Removes newlines, carriage returns, and control characters * @param displayName - The display name to sanitize * @param escapeQuotes - Whether to escape quotes (for email headers) * @returns Sanitized display name */ export const sanitizeDisplayName = (displayName: string, escapeQuotes = false): string => { if (!displayName) return displayName let sanitized = displayName .trim() // Remove/replace newlines and carriage returns to prevent header injection .replace(/[\r\n]/g, ' ') // Remove control characters (except space and printable characters) .replace(/[\x00-\x1F\x7F-\x9F]/g, '') // Escape quotes if needed (for email headers) if (escapeQuotes) { sanitized = sanitized.replace(/"/g, '\\"') } return sanitized } /** * Sanitize and validate fromName for emails * Wrapper around sanitizeDisplayName for consistent fromName handling * @param fromName - The fromName to sanitize * @returns Sanitized fromName or undefined if empty after sanitization */ export const sanitizeFromName = (fromName: string | null | undefined): string | undefined => { if (!fromName) return undefined const sanitized = sanitizeDisplayName(fromName, false) return sanitized.length > 0 ? sanitized : undefined } export const getMailing = (payload: Payload) => { const mailing = (payload as any).mailing if (!mailing) { throw new Error('Mailing plugin not initialized. Make sure you have added the mailingPlugin to your Payload config.') } return mailing } export const renderTemplate = async (payload: Payload, templateSlug: string, variables: TemplateVariables): Promise<{ html: string; text: string; subject: string }> => { const mailing = getMailing(payload) return mailing.service.renderTemplate(templateSlug, variables) } export const processEmails = async (payload: Payload): Promise => { const mailing = getMailing(payload) return mailing.service.processEmails() } export const retryFailedEmails = async (payload: Payload): Promise => { const mailing = getMailing(payload) return mailing.service.retryFailedEmails() }