| const DISPOSABLE_DOMAINS = new Set<string>([ |
| 'guerrillamail.com', 'guerrillamail.de', 'guerrillamail.net', 'guerrillamail.org', |
| 'guerrillamailblock.com', 'grr.la', 'sharklasers.com', 'spam4.me', |
| 'tempmail.com', 'temp-mail.org', 'temp-mail.io', |
| 'throwaway.email', 'throwaway.com', |
| 'mailinator.com', 'mailnesia.com', 'maildrop.cc', |
| 'yopmail.com', 'yopmail.fr', 'yopmail.net', |
| 'trashmail.com', 'trashmail.me', 'trashmail.net', |
| 'dispostable.com', 'discard.email', |
| 'fakeinbox.com', 'fakemail.net', |
| 'getnada.com', 'nada.email', |
| 'tempinbox.com', 'tempr.email', 'tempmailaddress.com', |
| 'emailondeck.com', '33mail.com', |
| 'mohmal.com', 'mohmal.im', 'mohmal.in', |
| 'harakirimail.com', 'crazymailing.com', |
| 'inboxbear.com', 'mailcatch.com', |
| 'mintemail.com', 'mt2015.com', |
| 'spamgourmet.com', 'spamgourmet.net', |
| 'mailexpire.com', 'mailforspam.com', |
| 'safetymail.info', 'trashymail.com', |
| 'mytemp.email', 'tempail.com', |
| 'burnermail.io', |
| 'passinbox.com', 'passmail.net', 'passmail.com', |
| 'silomails.com', 'slmail.me', |
| 'spam.me', 'spambox.us', |
| ]); |
|
|
| const OFFENSIVE_RE = /(nigger|faggot|fuckfaggot)/i; |
|
|
| const TYPO_TLDS = new Set<string>(['con', 'coma', 'comhade', 'gmai', 'gmial']); |
|
|
| export type EmailValidationResult = { valid: true } | { valid: false; reason: string }; |
|
|
| async function hasMxRecords(domain: string): Promise<boolean> { |
| try { |
| const res = await fetch( |
| `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(domain)}&type=MX`, |
| { headers: { Accept: 'application/dns-json' }, signal: AbortSignal.timeout(3000) }, |
| ); |
| if (!res.ok) return true; |
| const data = (await res.json()) as { Answer?: unknown[] }; |
| return Array.isArray(data.Answer) && data.Answer.length > 0; |
| } catch { |
| return true; |
| } |
| } |
|
|
| export async function validateEmail(email: string): Promise<EmailValidationResult> { |
| const normalized = email.trim().toLowerCase(); |
| const atIdx = normalized.indexOf('@'); |
| if (atIdx < 1) return { valid: false, reason: 'Invalid email format' }; |
|
|
| const domain = normalized.slice(atIdx + 1); |
| const localPart = normalized.slice(0, atIdx); |
|
|
| if (OFFENSIVE_RE.test(localPart) || OFFENSIVE_RE.test(domain)) { |
| return { valid: false, reason: 'Email address not accepted' }; |
| } |
|
|
| if (DISPOSABLE_DOMAINS.has(domain)) { |
| return { valid: false, reason: 'Disposable email addresses are not allowed. Please use a permanent email.' }; |
| } |
|
|
| const tld = domain.split('.').pop(); |
| if (tld && TYPO_TLDS.has(tld)) { |
| return { valid: false, reason: 'This email domain looks like a typo. Please check the ending.' }; |
| } |
|
|
| const mx = await hasMxRecords(domain); |
| if (!mx) { |
| return { valid: false, reason: 'This email domain does not accept mail. Please check for typos.' }; |
| } |
|
|
| return { valid: true }; |
| } |
|
|