73 lines
2.1 KiB
TypeScript
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, "\\$&");
|
|
}
|