108 lines
2.7 KiB
TypeScript
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 };
|
|
}
|