@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
168 lines • 6.17 kB
JavaScript
;
/**
* IDN (Internationalized Domain Names) utilities
* Uses Node's built-in Punycode via url.domainToASCII (Node >= 18).
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.IDNValidationError = void 0;
exports.domainToPunycode = domainToPunycode;
exports.emailToPunycode = emailToPunycode;
exports.validateInternationalEmail = validateInternationalEmail;
const node_url_1 = require("node:url");
/**
* Convert domain to Punycode / ASCII format.
*/
function domainToPunycode(domain) {
const lower = domain.toLowerCase();
try {
const ascii = (0, node_url_1.domainToASCII)(lower);
// domainToASCII returns '' for invalid input — fall back to lowercased original
return ascii || lower;
}
catch {
return lower;
}
}
/**
* Convert email address's domain to Punycode.
*/
function emailToPunycode(email) {
const atIndex = email.lastIndexOf('@');
if (atIndex === -1)
return email;
const local = email.slice(0, atIndex);
const domain = email.slice(atIndex + 1);
if (!domain)
return email;
return `${local}@${domainToPunycode(domain)}`;
}
/**
* Error codes for IDN validation
*/
var IDNValidationError;
(function (IDNValidationError) {
IDNValidationError["MISSING_INPUT"] = "MISSING_INPUT";
IDNValidationError["EMAIL_TOO_LONG"] = "EMAIL_TOO_LONG";
IDNValidationError["MISSING_AT_SYMBOL"] = "MISSING_AT_SYMBOL";
IDNValidationError["LOCAL_PART_EMPTY"] = "LOCAL_PART_EMPTY";
IDNValidationError["LOCAL_PART_TOO_LONG"] = "LOCAL_PART_TOO_LONG";
IDNValidationError["LOCAL_PART_INVALID"] = "LOCAL_PART_INVALID";
IDNValidationError["DOMAIN_EMPTY"] = "DOMAIN_EMPTY";
IDNValidationError["DOMAIN_TOO_LONG"] = "DOMAIN_TOO_LONG";
IDNValidationError["DOMAIN_INVALID_FORMAT"] = "DOMAIN_INVALID_FORMAT";
IDNValidationError["MISSING_TLD"] = "MISSING_TLD";
IDNValidationError["NUMERIC_TLD"] = "NUMERIC_TLD";
IDNValidationError["INVALID_ENCODING"] = "INVALID_ENCODING";
})(IDNValidationError || (exports.IDNValidationError = IDNValidationError = {}));
/**
* Validates an email address according to international standards (IDNA).
*/
function validateInternationalEmail(email) {
if (!email || typeof email !== 'string') {
return {
type: 'IDN_VALIDATION_ERROR',
code: IDNValidationError.MISSING_INPUT,
message: 'The email field cannot be empty'
};
}
const atIndex = email.lastIndexOf('@');
if (atIndex === -1) {
return {
type: 'IDN_VALIDATION_ERROR',
code: IDNValidationError.MISSING_AT_SYMBOL,
message: 'The email address must contain an @ symbol'
};
}
const local = email.slice(0, atIndex);
const domain = email.slice(atIndex + 1);
if (email.length > 254) {
return {
type: 'IDN_VALIDATION_ERROR',
code: IDNValidationError.EMAIL_TOO_LONG,
message: 'The email address is too long'
};
}
if (domain.length === 0) {
return {
type: 'IDN_VALIDATION_ERROR',
code: IDNValidationError.DOMAIN_EMPTY,
message: 'The domain part of the email cannot be empty'
};
}
if (domain.length > 255) {
return {
type: 'IDN_VALIDATION_ERROR',
code: IDNValidationError.DOMAIN_TOO_LONG,
message: 'The domain part of the email is too long'
};
}
if (local.length === 0) {
return {
type: 'IDN_VALIDATION_ERROR',
code: IDNValidationError.LOCAL_PART_EMPTY,
message: 'The username part of the email cannot be empty'
};
}
if (local.length > 64) {
return {
type: 'IDN_VALIDATION_ERROR',
code: IDNValidationError.LOCAL_PART_TOO_LONG,
message: 'The username part of the email is too long'
};
}
if (!/^[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]([a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~.]*[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~])?$/.test(local) || local.includes('..')) {
return {
type: 'IDN_VALIDATION_ERROR',
code: IDNValidationError.LOCAL_PART_INVALID,
message: 'The username contains invalid characters or dots in wrong places'
};
}
try {
if (/[\uD800-\uDFFF]/.test(domain) || /[\u0000-\u001F\u007F]/.test(domain)) {
return {
type: 'IDN_VALIDATION_ERROR',
code: IDNValidationError.INVALID_ENCODING,
message: 'The domain contains invalid characters or encoding'
};
}
if (/[^\p{L}\p{M}\p{N}.\-]/u.test(domain)) {
return {
type: 'IDN_VALIDATION_ERROR',
code: IDNValidationError.DOMAIN_INVALID_FORMAT,
message: 'The domain format is invalid'
};
}
const punycodeDomain = domainToPunycode(domain);
if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/.test(punycodeDomain)) {
return {
type: 'IDN_VALIDATION_ERROR',
code: IDNValidationError.DOMAIN_INVALID_FORMAT,
message: 'The domain format is invalid'
};
}
if (!punycodeDomain.includes('.')) {
return {
type: 'IDN_VALIDATION_ERROR',
code: IDNValidationError.MISSING_TLD,
message: 'The email domain must include a top-level domain (like .com or .org)'
};
}
const tld = punycodeDomain.split('.').pop();
if (/^[0-9]+$/.test(tld)) {
return {
type: 'IDN_VALIDATION_ERROR',
code: IDNValidationError.NUMERIC_TLD,
message: 'The top-level domain cannot be all numbers'
};
}
}
catch {
return {
type: 'IDN_VALIDATION_ERROR',
code: IDNValidationError.INVALID_ENCODING,
message: 'The domain contains invalid characters or encoding'
};
}
return undefined;
}
//# sourceMappingURL=idn.js.map