export type ValidationResult = { valid: boolean; errors: string[]; warnings: string[]; }; export class ContentValidationError extends Error { constructor( message: string, public readonly validation: ValidationResult, ) { super(message); this.name = 'ContentValidationError'; } } const PLACEHOLDER_PATTERNS = [ { pattern: /\[.*?\]/i, message: 'Bracket placeholder detected' }, { pattern: /\{\{.*?\}\}/, message: 'Mustache placeholder detected' }, { pattern: /\[Your .*?\]/i, message: 'Template placeholder detected' }, { pattern: /\[Director .*?\]/i, message: 'Template placeholder detected' }, { pattern: /\[Company .*?\]/i, message: 'Template placeholder detected' }, { pattern: /INSERT .* HERE/i, message: 'Template placeholder detected' }, { pattern: /\bTODO\b/, message: 'TODO placeholder detected' }, { pattern: /lorem ipsum/i, message: 'Lorem ipsum placeholder detected' }, ]; const TIER1_SPAM = [ 'act now', '100% free', 'risk-free', 'free money', 'click here', 'limited time', 'winner', 'viagra', 'casino', 'lottery', 'guarantee', ]; const TIER2_SPAM = [ 'urgent', 'limited', 'offer', 'discount', 'sale', 'promotion', 'buy now', ]; const URL_PATTERN = /https?:\/\//i; function wordCount(text: string): number { return text.trim().split(/\s+/).filter(Boolean).length; } export type ValidateOptions = { isReply?: boolean; failOnWarnings?: boolean; }; export function validateEmailContent( subject: string, body: string, options: ValidateOptions = {}, ): ValidationResult { const errors: string[] = []; const warnings: string[] = []; const combined = `${subject}\n${body}`; for (const { pattern, message } of PLACEHOLDER_PATTERNS) { if (pattern.test(combined)) { errors.push(message); } } if (URL_PATTERN.test(combined)) { errors.push('URLs are not allowed in warmup content'); } if (subject.length > 60) { errors.push('Subject exceeds 60 characters'); } const minWords = options.isReply ? 20 : 40; const maxWords = options.isReply ? 120 : 200; const words = wordCount(body); if (words < minWords || words > maxWords) { errors.push(`Body word count ${words} outside allowed range ${minWords}-${maxWords}`); } const lower = combined.toLowerCase(); for (const phrase of TIER1_SPAM) { if (lower.includes(phrase)) { errors.push(`Spam trigger phrase: "${phrase}"`); } } for (const phrase of TIER2_SPAM) { if (lower.includes(phrase)) { warnings.push(`Soft spam phrase: "${phrase}"`); } } const valid = errors.length === 0 && (!options.failOnWarnings || warnings.length === 0); return { valid, errors, warnings }; }