mirror of
https://github.com/xtr-dev/payload-mailing.git
synced 2025-12-10 00:03:23 +00:00
- Create centralized sanitization utilities in utils/helpers.ts - Add sanitizeDisplayName() with configurable quote escaping - Add sanitizeFromName() wrapper for consistent fromName handling - Replace duplicated sanitization logic in sendEmail.ts (9 lines → 1 line) - Replace duplicated sanitization logic in MailingService.ts (9 lines → 1 line) - Export new utilities from main index for external use - Maintain identical functionality while reducing maintenance overhead Benefits: - Single source of truth for email header sanitization - Consistent security handling across all email components - Easier to maintain and update sanitization logic - Configurable quote escaping for different use cases 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
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<void> => {
|
|
const mailing = getMailing(payload)
|
|
return mailing.service.processEmails()
|
|
}
|
|
|
|
export const retryFailedEmails = async (payload: Payload): Promise<void> => {
|
|
const mailing = getMailing(payload)
|
|
return mailing.service.retryFailedEmails()
|
|
} |