Files
2026-07-13 13:32:57 +08:00

73 lines
2.1 KiB
TypeScript

/**
* Match an email against an operator-supplied pattern (ADMIN_EMAILS /
* WHITELISTED_EMAILS), anchored to the whole address with `^(?:...)$` so the
* pattern matches the entire email rather than a substring.
*
* The non-capturing group keeps top-level alternation working
* (`a@x.com|b@x.com` stays two whole-string alternatives). Patterns that
* already carry their own `^`/`$` anchors remain equivalent. A top-level
* alternative that is just `@domain.tld` is expanded to "any mailbox at exactly
* that domain".
*
* Dependency-free so it can be tested directly; callers pass the pattern from `env`.
*/
export function emailMatchesPattern(pattern: string, email: string): boolean {
return new RegExp(`^(?:${expandDomainShorthand(pattern)})$`).test(email);
}
function expandDomainShorthand(pattern: string): string {
return splitTopLevelAlternatives(pattern)
.map((alternative) => {
const domain = alternative.match(/^@([A-Za-z0-9.-]+)$/)?.[1];
return domain ? `[^@]+@${escapeRegExp(domain)}` : alternative;
})
.join("|");
}
function splitTopLevelAlternatives(pattern: string): string[] {
const alternatives: string[] = [];
let current = "";
let escaped = false;
let depth = 0;
let inCharacterClass = false;
for (const char of pattern) {
if (escaped) {
current += char;
escaped = false;
continue;
}
if (char === "\\") {
current += char;
escaped = true;
continue;
}
if (char === "[" && !inCharacterClass) {
inCharacterClass = true;
} else if (char === "]" && inCharacterClass) {
inCharacterClass = false;
} else if (!inCharacterClass && char === "(") {
depth++;
} else if (!inCharacterClass && char === ")" && depth > 0) {
depth--;
}
if (char === "|" && depth === 0 && !inCharacterClass) {
alternatives.push(current);
current = "";
continue;
}
current += char;
}
alternatives.push(current);
return alternatives;
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}