email-domain-verifier
Version:
Verifies email domains
157 lines (135 loc) • 4.85 kB
text/typescript
import { resolveMx } from 'dns'
import { createConnection } from 'net'
import { EmailVerificationOptions, EnsuredEmailVerificationOptions } from './types/EmailVerificationOptions'
import { EmailVerificationResult, EmailVerificationDetails } from './types/EmailVerificationResult'
const LRU = require('lru-cache')
const cache = new LRU({ max: 1000, ttl: 24 * 60 * 60 * 1000 })
const verifyEmailDomain = async (emailOrDomain: string, options: EmailVerificationOptions = {}): Promise<EmailVerificationResult> => {
const ensuredOptions = verifier.ensureOptions(options)
const {
mxVerificationSucceed,
smtpVerificationSucceed
} = await verifier.verify(verifier.parseDomainFromEmail(emailOrDomain), ensuredOptions)
if (ensuredOptions.requireSmtpOrMx) {
return {
verified: mxVerificationSucceed || (smtpVerificationSucceed ?? false),
mxVerificationSucceed,
smtpVerificationSucceed
}
}
return {
verified: (mxVerificationSucceed || ensuredOptions.mxNotRequired) && (smtpVerificationSucceed || ensuredOptions.smtpNotRequired),
mxVerificationSucceed,
smtpVerificationSucceed
}
}
const ensureOptions = (options: EmailVerificationOptions): EnsuredEmailVerificationOptions => {
return {
smtpNotRequired: ('smtpNotRequired' in options && options.smtpNotRequired) ?? false,
mxNotRequired: ('mxNotRequired' in options && options.mxNotRequired) ?? false,
requireSmtpOrMx: ('requireSmtpOrMx' in options && options.requireSmtpOrMx) ?? false,
smtpConnectionTimeout: options.smtpConnectionTimeout ?? 1000,
useCache: options.useCache ?? true
}
}
const parseDomainFromEmail = (emailOrDomain: string): string => {
const sepCount = (emailOrDomain.match(/@/g) || []).length
if (sepCount === 0) {
return emailOrDomain
}
return emailOrDomain.split('@')[sepCount]
}
const verify = async (domain: string, options: EnsuredEmailVerificationOptions): Promise<EmailVerificationDetails> => {
if (options.useCache) {
const result = cache.get(getCacheKey(domain, options))
if (result) {
return result as EmailVerificationDetails
}
}
const exchangeServers = await verifier.getExchangeServers(domain)
const mxVerificationSucceed = Boolean(exchangeServers.length)
if ((options.requireSmtpOrMx && mxVerificationSucceed) || options.smtpNotRequired) {
const result = {
mxVerificationSucceed
}
if (mxVerificationSucceed) {
cache.set(getCacheKey(domain, options), result)
}
return result
}
const smtpHosts = verifier.generateSmtpHostList(domain, exchangeServers)
const smtpVerificationSucceed = await verifier.isSmtpServerRunning(smtpHosts, options.smtpConnectionTimeout ?? 1000)
const result = {
mxVerificationSucceed,
smtpVerificationSucceed
}
if (result.mxVerificationSucceed && result.smtpVerificationSucceed) {
cache.set(getCacheKey(domain, options), result)
}
return result
}
const getCacheKey = (domain: string, options: EnsuredEmailVerificationOptions): string => {
return `${domain}-${options.requireSmtpOrMx}-${options.mxNotRequired}-${options.smtpNotRequired}`
}
const getExchangeServers = (domain: string): Promise<string[]> => {
return new Promise<string[]>((resolve, reject) => {
resolveMx(domain, (err, addresses) => {
if (err) {
const expectedErrors = ['ESERVFAIL', 'ENODATA', 'ENOTFOUND']
if (err.code && expectedErrors.includes(err.code)) {
return resolve([])
}
return reject(err)
}
return resolve(
addresses
.sort((a, b) => a.priority - b.priority)
.map(a => a.exchange)
)
})
})
}
const generateSmtpHostList = (domain: string, exchangeServers: string[]): string[] => {
return [...exchangeServers, `smtp.${domain}`, domain]
}
const isSmtpServerRunning = async (hosts: string[], timeout: number): Promise<boolean> => {
const portsToCheck = [25, 465, 587, 2525]
for (const host of hosts) {
for (const port of portsToCheck) {
const res = await verifier.isPortOpen(host, port, timeout)
if (res) {
return true
}
}
}
return false
}
const isPortOpen = (host: string, port: number, timeout: number): Promise<boolean> => {
return new Promise<boolean>(function (resolve, reject) {
const timer = setTimeout(() => {
resolve(false)
socket.destroy()
}, timeout)
const socket = createConnection(port, host, () => {
clearTimeout(timer)
resolve(true)
socket.destroy()
})
socket.on('error', function (err) {
clearTimeout(timer)
resolve(false)
socket.destroy()
})
})
}
const verifier = {
verifyEmailDomain,
ensureOptions,
parseDomainFromEmail,
verify,
getExchangeServers,
generateSmtpHostList,
isSmtpServerRunning,
isPortOpen
}
export default verifier