@mikkelscheike/email-provider-links
Version:
TypeScript library for email provider detection with 140 providers (259 domains), concurrent DNS resolution, alias normalization, and HTTPS login URL validation for login and password reset flows
161 lines • 5.18 kB
JavaScript
;
/**
* Email Alias Detection Module
*
* Detects and normalizes email aliases using provider-specific rules for
* case, plus-addressing, and dots. Domain part is always lowercased (RFC 5321).
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectEmailAlias = detectEmailAlias;
exports.normalizeEmail = normalizeEmail;
exports.emailsMatch = emailsMatch;
const provider_loader_1 = require("./provider-loader");
const idn_1 = require("./idn");
function assertValidEmail(email) {
if (!email || typeof email !== 'string') {
throw new Error('Invalid email format');
}
const trimmed = email.trim();
if (!trimmed) {
throw new Error('Invalid email format');
}
const atIndex = trimmed.lastIndexOf('@');
if (atIndex <= 0 || atIndex === trimmed.length - 1) {
throw new Error('Invalid email format');
}
const local = trimmed.slice(0, atIndex);
const domain = trimmed.slice(atIndex + 1);
// Local part: allow consecutive dots (Gmail accepts/strips them). Reject whitespace/@.
if (!local || /\s|@/.test(local) || local.length > 64) {
throw new Error('Invalid email format');
}
const domainError = (0, idn_1.validateInternationalEmail)(`a@${domain}`);
if (domainError) {
throw new Error('Invalid email format');
}
}
function getProviderDomainMap() {
const result = (0, provider_loader_1.loadProviders)();
if (result.domainMap) {
return result.domainMap;
}
const domainMap = new Map();
for (const provider of result.providers) {
for (const domain of provider.domains) {
domainMap.set(domain.toLowerCase(), provider);
}
}
return domainMap;
}
/**
* Detects and analyzes email aliases according to provider-specific rules.
*/
function detectEmailAlias(email, options = {}) {
if (!options.alreadyValidated) {
assertValidEmail(email);
}
const originalEmail = email.trim();
const atIndex = originalEmail.lastIndexOf('@');
const username = originalEmail.slice(0, atIndex).toLowerCase();
const domain = options.punycodeDomain ||
(0, idn_1.domainToPunycode)(originalEmail.slice(atIndex + 1).toLowerCase());
if (!username || !domain) {
throw new Error('Invalid email format - missing username or domain');
}
const domainMap = getProviderDomainMap();
const provider = domainMap.get(domain);
const result = {
canonical: `${username}@${domain}`,
original: originalEmail,
isAlias: false,
aliasType: 'none'
};
if (!provider?.alias) {
return result;
}
result.provider = domain;
let normalizedUsername = username;
let isAlias = false;
let aliasType = 'none';
let aliasPart;
if (provider.alias?.case?.ignore) {
if (provider.alias.case?.strip) {
normalizedUsername = normalizedUsername.toLowerCase();
}
}
if (provider.alias?.plus?.ignore) {
const plusIndex = username.indexOf('+');
if (plusIndex !== -1) {
aliasPart = username.substring(plusIndex + 1);
isAlias = true;
aliasType = 'plus';
if (provider.alias.plus?.strip) {
normalizedUsername = username.slice(0, plusIndex);
}
}
}
if (provider.alias?.dots?.ignore) {
const hasDots = username.includes('.');
if (hasDots) {
if (!isAlias) {
aliasPart = username;
isAlias = true;
aliasType = 'dot';
}
if (provider.alias.dots?.strip) {
normalizedUsername = normalizedUsername.replace(/\./g, '');
}
}
}
result.canonical = `${normalizedUsername}@${domain}`;
result.isAlias = isAlias;
result.aliasType = aliasType;
if (aliasPart !== undefined) {
result.aliasPart = aliasPart;
}
return result;
}
/**
* Normalizes an email address to its canonical form.
*/
function normalizeEmail(email, options = {}) {
if (email == null || typeof email !== 'string') {
// Preserve null/undefined for edge-case compatibility
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return email;
}
const trimmed = email.trim();
if (trimmed === '') {
return '';
}
try {
return detectEmailAlias(trimmed, options).canonical;
}
catch (error) {
if (error instanceof Error && error.message.includes('Invalid email format')) {
return trimmed;
}
return trimmed.toLowerCase();
}
}
/**
* Checks if two email addresses are the same when normalized.
*/
function emailsMatch(email1, email2) {
if (email1 == null || email2 == null) {
return false;
}
if (typeof email1 !== 'string' || typeof email2 !== 'string') {
return false;
}
if (email1.trim() === '' || email2.trim() === '') {
return false;
}
try {
return normalizeEmail(email1) === normalizeEmail(email2);
}
catch {
return false;
}
}
//# sourceMappingURL=alias-detection.js.map