// Security-focused validation example
interface SecureCampaignRequest {
name: string;
subject: string;
content: EmailContent;
recipients: EmailRecipient[];
}
interface EmailContent {
html: string;
text: string;
templateId?: string;
}
interface EmailRecipient {
email: string;
name?: string;
personalization?: Record<string, string | number | boolean>;
}
interface ValidationResult {
isValid: boolean;
errors: string[];
}
interface PersonalizationSanitizer {
[key: string]: string | number | boolean;
}
/**
* Security-focused validation service
*/
class SecureCampaignValidationService {
private readonly emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
private readonly invalidNamePattern = /[<>\"']/;
private readonly invalidSubjectPattern = /[\r\n]/;
/**
* Validate campaign request with security considerations
*/
validateCampaignRequest(request: SecureCampaignRequest): ValidationResult {
const errors: string[] = [];
// Validate name
const nameValidation = this.validateName(request.name);
if (!nameValidation.isValid) {
errors.push(...nameValidation.errors);
}
// Validate subject
const subjectValidation = this.validateSubject(request.subject);
if (!subjectValidation.isValid) {
errors.push(...subjectValidation.errors);
}
// Validate content
const contentValidation = this.validateContent(request.content);
if (!contentValidation.isValid) {
errors.push(...contentValidation.errors);
}
// Validate recipients
const recipientsValidation = this.validateRecipients(request.recipients);
if (!recipientsValidation.isValid) {
errors.push(...recipientsValidation.errors);
}
return {
isValid: errors.length === 0,
errors
};
}
private validateName(name: string): ValidationResult {
const errors: string[] = [];
if (!name || name.trim().length === 0) {
errors.push("Name is required");
return { isValid: false, errors };
}
if (name.length > 100) {
errors.push("Name must be less than 100 characters");
}
if (this.invalidNamePattern.test(name)) {
errors.push("Name contains invalid characters");
}
return { isValid: errors.length === 0, errors };
}
private validateSubject(subject: string): ValidationResult {
const errors: string[] = [];
if (!subject || subject.trim().length === 0) {
errors.push("Subject is required");
return { isValid: false, errors };
}
if (subject.length > 200) {
errors.push("Subject must be less than 200 characters");
}
if (this.invalidSubjectPattern.test(subject)) {
errors.push("Subject contains invalid characters");
}
return { isValid: errors.length === 0, errors };
}
private validateContent(content: EmailContent): ValidationResult {
const errors: string[] = [];
if (!content.html || content.html.trim().length === 0) {
errors.push("HTML content is required");
}
if (!content.text || content.text.trim().length === 0) {
errors.push("Text content is required");
}
return { isValid: errors.length === 0, errors };
}
private validateRecipients(recipients: EmailRecipient[]): ValidationResult {
const errors: string[] = [];
if (!recipients || recipients.length === 0) {
errors.push("At least one recipient is required");
return { isValid: false, errors };
}
if (recipients.length > 10000) {
errors.push("Maximum 10,000 recipients allowed");
return { isValid: false, errors };
}
for (let i = 0; i < recipients.length; i++) {
const recipient = recipients[i];
const recipientErrors = this.validateRecipient(recipient, i);
errors.push(...recipientErrors);
}
return { isValid: errors.length === 0, errors };
}
private validateRecipient(recipient: EmailRecipient, index: number): string[] {
const errors: string[] = [];
const prefix = `Recipient ${index + 1}`;
if (!recipient.email) {
errors.push(`${prefix}: Email is required`);
return errors;
}
if (!this.emailPattern.test(recipient.email)) {
errors.push(`${prefix}: Invalid email address: ${recipient.email}`);
}
if (recipient.personalization) {
const sanitized = this.sanitizePersonalization(recipient.personalization);
if (Object.keys(sanitized).length !== Object.keys(recipient.personalization).length) {
errors.push(`${prefix}: Personalization contains invalid data`);
}
}
return errors;
}
/**
* Sanitize personalization data to prevent injection
*/
private sanitizePersonalization(data: PersonalizationSanitizer): PersonalizationSanitizer {
if (!data || typeof data !== 'object') {
return {};
}
const sanitized: PersonalizationSanitizer = {};
for (const [key, value] of Object.entries(data)) {
// Only allow safe string/number/boolean values
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
if (typeof value === 'string') {
// Basic XSS prevention - escape HTML
sanitized[key] = value
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
} else {
sanitized[key] = value;
}
}
}
return sanitized;
}
}