warmbox/apps/api/src/services/content-validator.service.ts

108 lines
2.7 KiB
TypeScript

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 };
}