| /** | |
| * Email validation utilities for comma-separated email lists | |
| */ | |
| /** | |
| * Validates a single email address using a non-backtracking approach | |
| * to prevent ReDoS vulnerabilities | |
| */ | |
| export function isValidEmail(email: string): boolean { | |
| const trimmed = email.trim(); | |
| // Length limits per RFC 5321 | |
| if (trimmed.length === 0 || trimmed.length > 254) return false; | |
| const atIndex = trimmed.indexOf("@"); | |
| // Must have exactly one @ with content on both sides | |
| if (atIndex < 1 || atIndex !== trimmed.lastIndexOf("@")) return false; | |
| const local = trimmed.slice(0, atIndex); | |
| const domain = trimmed.slice(atIndex + 1); | |
| // Local part validation | |
| if (local.length > 64 || /\s/.test(local)) return false; | |
| // Domain validation: must have content, no spaces, and a valid TLD | |
| if (domain.length === 0 || domain.length > 253 || /\s/.test(domain)) | |
| return false; | |
| // Domain must have at least one dot with content on both sides | |
| const lastDotIndex = domain.lastIndexOf("."); | |
| if (lastDotIndex < 1 || lastDotIndex >= domain.length - 1) return false; | |
| return true; | |
| } | |
| /** | |
| * Parses a comma-separated email string into an array of trimmed emails | |
| */ | |
| export function parseEmailList(value: string | null | undefined): string[] { | |
| if (!value) return []; | |
| return value | |
| .split(",") | |
| .map((e) => e.trim()) | |
| .filter(Boolean); | |
| } | |
| /** | |
| * Validates a comma-separated email string | |
| * Returns true if empty/null or if all emails are valid and unique (case-insensitive) | |
| */ | |
| export function isValidEmailList(value: string | null | undefined): boolean { | |
| if (!value) return true; | |
| const emails = parseEmailList(value); | |
| // Check all emails are valid | |
| if (!emails.every((email) => isValidEmail(email))) return false; | |
| // Check for duplicates (case-insensitive) | |
| const uniqueEmails = new Set(emails.map((e) => e.toLowerCase())); | |
| return uniqueEmails.size === emails.length; | |
| } | |