Last active
June 1, 2026 17:05
-
-
Save Igloczek/7bfc459109f4a01f7e046b591d2a842a to your computer and use it in GitHub Desktop.
Simple way to filter out most of the disposable / temporary / disposable / burner / fake / dead emails
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import dns from "node:dns/promises" | |
| import axios from "axios" | |
| import { TTLCache } from "@isaacs/ttlcache" | |
| const domainsCache = new TTLCache<string, boolean>({ | |
| ttl: 1000 * 60 * 60 * 1, | |
| }) | |
| const dnsCache = new TTLCache<string, boolean>({ | |
| ttl: 1000 * 60 * 60 * 1, | |
| }) | |
| const resolver = new dns.Resolver({ timeout: 5000, tries: 3 }) | |
| resolver.setServers(["1.1.1.1", "8.8.8.8"]) | |
| const TRANSIENT_ERRORS = new Set([ | |
| "ETIMEOUT", | |
| "ESERVFAIL", | |
| "ECONNREFUSED", | |
| "EREFUSED", | |
| ]) | |
| const KNOWN_PROVIDERS = new Set([ | |
| "gmail.com", | |
| "googlemail.com", | |
| "yahoo.com", | |
| "yahoo.co.uk", | |
| "hotmail.com", | |
| "hotmail.co.uk", | |
| "outlook.com", | |
| "icloud.com", | |
| "me.com", | |
| "mac.com", | |
| "live.com", | |
| "msn.com", | |
| "proton.me", | |
| "protonmail.com", | |
| "hey.com", | |
| "fastmail.com", | |
| "aol.com", | |
| ]) | |
| interface ListConfig { | |
| url: string | |
| type: "text" | "json" | |
| } | |
| const lists: ListConfig[] = [ | |
| { | |
| url: "https://deviceandbrowserinfo.com/api/emails/disposable", | |
| type: "json", | |
| }, | |
| { | |
| url: "https://raw.githubusercontent.com/Igloczek/burner-email-providers/master/emails.txt", | |
| type: "text", | |
| }, | |
| { | |
| url: "https://raw.githubusercontent.com/wesbos/burner-email-providers/master/emails.txt", | |
| type: "text", | |
| }, | |
| { | |
| url: "https://raw.githubusercontent.com/7c/fakefilter/main/txt/data.txt", | |
| type: "text", | |
| }, | |
| { | |
| url: "https://raw.githubusercontent.com/unkn0w/disposable-email-domain-list/main/domains.txt", | |
| type: "text", | |
| }, | |
| { | |
| url: "https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains_strict.txt", | |
| type: "text", | |
| }, | |
| { | |
| url: "https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/master/disposable_email_blocklist.conf", | |
| type: "text", | |
| }, | |
| ] | |
| async function loadDomains() { | |
| await Promise.allSettled( | |
| lists.map(async (list) => { | |
| const response = await axios.get(list.url) | |
| if (list.type === "json") { | |
| // Handle JSON response - assuming it's an array of domains | |
| const jsonData = Array.isArray(response.data) ? response.data : [] | |
| jsonData.forEach((domain) => { | |
| if (typeof domain === "string" && domain.trim() !== "") { | |
| domainsCache.set(domain.trim(), true) | |
| } | |
| }) | |
| } else { | |
| // Handle text response | |
| response.data.split("\n").forEach((item: string) => { | |
| const line = item.trim() | |
| if (line !== "" && !line.startsWith("#")) { | |
| domainsCache.set(line, true) | |
| } | |
| }) | |
| } | |
| }), | |
| ) | |
| } | |
| // warm up the cache | |
| await loadDomains() | |
| // automatically refresh lists every hour in the background | |
| setInterval(() => { | |
| loadDomains().catch(console.error) | |
| }, 1000 * 60 * 60 * 1) | |
| /** | |
| * Check if a domain can receive email, per RFC 5321: | |
| * 1. Try MX records first | |
| * 2. If no MX, fall back to A/AAAA records (implicit MX) | |
| * 3. Only mark as dead if the domain has none of the above | |
| */ | |
| async function checkDnsRecords(domain: string): Promise<boolean> { | |
| // Step 1: Check MX records | |
| try { | |
| const mx = await resolver.resolveMx(domain) | |
| if (mx && mx.length > 0) return true | |
| } catch (err) { | |
| const error = err as { code?: string } | |
| if (error.code && TRANSIENT_ERRORS.has(error.code)) return true | |
| if (error.code === "ENOTFOUND") return false | |
| // ENODATA = domain exists but no MX records, fall through to A/AAAA check | |
| } | |
| // Step 2: RFC 5321 fallback — check A/AAAA records (implicit MX) | |
| try { | |
| const a = await resolver.resolve4(domain) | |
| if (a && a.length > 0) return true | |
| } catch { | |
| // ignore | |
| } | |
| try { | |
| const aaaa = await resolver.resolve6(domain) | |
| if (aaaa && aaaa.length > 0) return true | |
| } catch { | |
| // ignore | |
| } | |
| return false | |
| } | |
| async function hasEmailDnsRecords(domain: string): Promise<boolean> { | |
| const cached = dnsCache.get(domain) | |
| if (cached !== undefined) { | |
| return cached | |
| } | |
| const result = await checkDnsRecords(domain) | |
| dnsCache.set(domain, result) | |
| return result | |
| } | |
| export async function verifyEmail(email: string) { | |
| const parts = email.split("@") | |
| if (parts.length !== 2) { | |
| return false | |
| } | |
| const domain = parts[1] | |
| if (KNOWN_PROVIDERS.has(domain)) { | |
| return true | |
| } | |
| const isDisposable = domainsCache.get(domain) === true | |
| if (isDisposable) { | |
| return false | |
| } | |
| const hasDnsRecords = await hasEmailDnsRecords(domain) | |
| if (!hasDnsRecords) { | |
| return false | |
| } | |
| return true | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment