Eliminate code duplication in email sanitization

- 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>
This commit is contained in:
2025-09-14 21:52:55 +02:00
parent 8135ff61c2
commit e4a16094d6
4 changed files with 45 additions and 19 deletions

View File

@@ -26,6 +26,8 @@ export {
processEmails, processEmails,
retryFailedEmails, retryFailedEmails,
parseAndValidateEmails, parseAndValidateEmails,
sanitizeDisplayName,
sanitizeFromName,
} from './utils/helpers.js' } from './utils/helpers.js'
// Email processing utilities // Email processing utilities

View File

@@ -1,5 +1,5 @@
import { Payload } from 'payload' import { Payload } from 'payload'
import { getMailing, renderTemplate, parseAndValidateEmails } from './utils/helpers.js' import { getMailing, renderTemplate, parseAndValidateEmails, sanitizeFromName } from './utils/helpers.js'
import { BaseEmailDocument } from './types/index.js' import { BaseEmailDocument } from './types/index.js'
import { processJobById } from './utils/emailProcessor.js' import { processJobById } from './utils/emailProcessor.js'
@@ -104,15 +104,7 @@ export const sendEmail = async <TEmail extends BaseEmailDocument = BaseEmailDocu
} }
// Sanitize fromName to prevent header injection // Sanitize fromName to prevent header injection
if (emailData.fromName) { emailData.fromName = sanitizeFromName(emailData.fromName as string)
emailData.fromName = emailData.fromName
.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, '')
// Note: We don't escape quotes here as that's handled in MailingService
}
// Normalize Date objects to ISO strings for consistent database storage // Normalize Date objects to ISO strings for consistent database storage
if (emailData.scheduledAt instanceof Date) { if (emailData.scheduledAt instanceof Date) {

View File

@@ -7,6 +7,7 @@ import {
BaseEmail, BaseEmailTemplate, BaseEmailDocument, BaseEmailTemplateDocument BaseEmail, BaseEmailTemplate, BaseEmailDocument, BaseEmailTemplateDocument
} from '../types/index.js' } from '../types/index.js'
import { serializeRichTextToHTML, serializeRichTextToText } from '../utils/richTextSerializer.js' import { serializeRichTextToHTML, serializeRichTextToText } from '../utils/richTextSerializer.js'
import { sanitizeDisplayName } from '../utils/helpers.js'
export class MailingService implements IMailingService { export class MailingService implements IMailingService {
public payload: Payload public payload: Payload
@@ -44,17 +45,10 @@ export class MailingService implements IMailingService {
/** /**
* Sanitizes a display name for use in email headers to prevent header injection * Sanitizes a display name for use in email headers to prevent header injection
* and ensure proper formatting * Uses the centralized sanitization utility with quote escaping for headers
*/ */
private sanitizeDisplayName(name: string): string { private sanitizeDisplayName(name: string): string {
return name return sanitizeDisplayName(name, true) // escapeQuotes = true for email headers
.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 to prevent malformed headers
.replace(/"/g, '\\"')
} }
/** /**

View File

@@ -36,6 +36,44 @@ export const parseAndValidateEmails = (emails: string | string[] | null | undefi
return emailList 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) => { export const getMailing = (payload: Payload) => {
const mailing = (payload as any).mailing const mailing = (payload as any).mailing
if (!mailing) { if (!mailing) {