const EMAIL_SUFFIX_TOKEN_SPLIT_RE = /[\s,,]+/ const EMAIL_SUFFIX_INVALID_CHAR_RE = /[^a-z0-9.-]/g const EMAIL_SUFFIX_INVALID_CHAR_CHECK_RE = /[^a-z0-9.-]/ const EMAIL_SUFFIX_PREFIX_RE = /^@+/ const EMAIL_SUFFIX_WILDCARD_PREFIX = '*.' const EMAIL_SUFFIX_MESSAGE_VISIBLE_LIMIT = 5 const EMAIL_SUFFIX_DOMAIN_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/ // normalizeRegistrationEmailSuffixDomain converts raw input into a canonical domain token. // Exact domains are returned without "@"; wildcard domains keep the "*." prefix. export function normalizeRegistrationEmailSuffixDomain(raw: string): string { let value = String(raw || '').trim().toLowerCase() if (!value) { return '' } value = value.replace(EMAIL_SUFFIX_PREFIX_RE, '') return normalizeRegistrationEmailSuffixToken(value, false) } export function normalizeRegistrationEmailSuffixDomains( items: string[] | null | undefined ): string[] { if (!items || items.length === 0) { return [] } const seen = new Set() const normalized: string[] = [] for (const item of items) { const domain = normalizeRegistrationEmailSuffixDomain(item) if (!isRegistrationEmailSuffixDomainValid(domain) || seen.has(domain)) { continue } seen.add(domain) normalized.push(domain) } return normalized } export function parseRegistrationEmailSuffixWhitelistInput(input: string): string[] { if (!input || !input.trim()) { return [] } const seen = new Set() const normalized: string[] = [] for (const token of input.split(EMAIL_SUFFIX_TOKEN_SPLIT_RE)) { const domain = normalizeRegistrationEmailSuffixDomainStrict(token) if (!isRegistrationEmailSuffixDomainValid(domain) || seen.has(domain)) { continue } seen.add(domain) normalized.push(domain) } return normalized } export function normalizeRegistrationEmailSuffixWhitelist( items: string[] | null | undefined ): string[] { return normalizeRegistrationEmailSuffixDomains(items).map(toCanonicalRegistrationEmailSuffix) } function extractRegistrationEmailDomain(email: string): string { const raw = String(email || '').trim().toLowerCase() if (!raw) { return '' } const atIndex = raw.indexOf('@') if (atIndex <= 0 || atIndex >= raw.length - 1) { return '' } if (raw.indexOf('@', atIndex + 1) !== -1) { return '' } return raw.slice(atIndex + 1) } export function isRegistrationEmailSuffixAllowed( email: string, whitelist: string[] | null | undefined ): boolean { const normalizedWhitelist = normalizeRegistrationEmailSuffixWhitelist(whitelist) if (normalizedWhitelist.length === 0) { return true } const emailDomain = extractRegistrationEmailDomain(email) if (!emailDomain) { return false } const emailSuffix = `@${emailDomain}` return normalizedWhitelist.some((allowed) => { if (allowed.startsWith('@')) { return allowed === emailSuffix } if (allowed.startsWith(EMAIL_SUFFIX_WILDCARD_PREFIX)) { const base = allowed.slice(EMAIL_SUFFIX_WILDCARD_PREFIX.length) return emailDomain === base || emailDomain.endsWith(`.${base}`) } return false }) } export function formatRegistrationEmailSuffixWhitelistForMessage( whitelist: string[] | null | undefined, options: { separator: string more: (count: number) => string } ): string { const normalizedWhitelist = normalizeRegistrationEmailSuffixWhitelist(whitelist) const visible = normalizedWhitelist.slice(0, EMAIL_SUFFIX_MESSAGE_VISIBLE_LIMIT) const hiddenCount = normalizedWhitelist.length - visible.length if (hiddenCount > 0) { visible.push(options.more(hiddenCount)) } return visible.join(options.separator) } // Pasted domains should be strict: any invalid character drops the whole token. function normalizeRegistrationEmailSuffixDomainStrict(raw: string): string { let value = String(raw || '').trim().toLowerCase() if (!value) { return '' } value = value.replace(EMAIL_SUFFIX_PREFIX_RE, '') return normalizeRegistrationEmailSuffixToken(value, true) } export function isRegistrationEmailSuffixDomainValid(domain: string): boolean { if (!domain) { return false } if (domain.startsWith(EMAIL_SUFFIX_WILDCARD_PREFIX)) { return EMAIL_SUFFIX_DOMAIN_PATTERN.test(domain.slice(EMAIL_SUFFIX_WILDCARD_PREFIX.length)) } return !domain.includes('*') && EMAIL_SUFFIX_DOMAIN_PATTERN.test(domain) } function normalizeRegistrationEmailSuffixToken(value: string, strict: boolean): string { if (value.startsWith(EMAIL_SUFFIX_WILDCARD_PREFIX)) { const domain = value.slice(EMAIL_SUFFIX_WILDCARD_PREFIX.length) if (strict && (!domain || EMAIL_SUFFIX_INVALID_CHAR_CHECK_RE.test(domain))) { return '' } return `${EMAIL_SUFFIX_WILDCARD_PREFIX}${domain.replace(EMAIL_SUFFIX_INVALID_CHAR_RE, '')}` } if (value === '*') { return strict ? '' : value } if (strict && EMAIL_SUFFIX_INVALID_CHAR_CHECK_RE.test(value)) { return '' } return value.replace(/[*]/g, '').replace(EMAIL_SUFFIX_INVALID_CHAR_RE, '') } function toCanonicalRegistrationEmailSuffix(domain: string): string { return domain.startsWith(EMAIL_SUFFIX_WILDCARD_PREFIX) ? domain : `@${domain}` }