import axios from "axios"; import dns from "dns/promises"; import { getEnv } from "../../shared/config/env"; import { withRetry, isCircuitOpen, recordFailure, recordSuccess } from "../../shared/utils/retry"; import { reoonLimiter } from "../../shared/utils/rate-limiter"; import { logger } from "../../shared/utils/logger"; const PROVIDER = "reoon"; export type VerifyResult = "valid" | "invalid" | "catch_all" | "unknown"; export interface EmailVerification { email: string; result: VerifyResult; isDeliverable: boolean; isCatchAll: boolean; mxFound: boolean; } /** * Verifies email deliverability via Reoon API with MX record fallback. * Order: Reoon API → local MX check → pattern heuristic */ export async function verifyEmail(email: string): Promise { const domain = email.split("@")[1]; if (!domain) return makeResult(email, "invalid", false, false, false); // Try Reoon API first if (!isCircuitOpen(PROVIDER)) { await reoonLimiter.consume(PROVIDER); try { const result = await withRetry(() => callReoon(email), { provider: PROVIDER }); recordSuccess(PROVIDER); return result; } catch (err) { recordFailure(PROVIDER); logger.warn({ email, err }, "Reoon verify failed — falling back to MX check"); } } // Fallback: local MX record check return mxFallback(email, domain); } async function callReoon(email: string): Promise { const env = getEnv(); const response = await axios.get("https://emailverifier.reoon.com/api/v1/verify", { params: { email, key: env.REOON_API_KEY, mode: "quick" }, timeout: 10_000, }); const data = response.data; const result: VerifyResult = data.status === "valid" ? "valid" : data.status === "catch_all" ? "catch_all" : "invalid"; return makeResult( email, result, data.is_deliverable ?? result === "valid", data.is_catch_all ?? false, data.has_mx_record ?? true ); } async function mxFallback(email: string, domain: string): Promise { try { const records = await dns.resolveMx(domain); const mxFound = records.length > 0; return makeResult(email, mxFound ? "catch_all" : "invalid", mxFound, mxFound, mxFound); } catch { return makeResult(email, "unknown", false, false, false); } } function makeResult( email: string, result: VerifyResult, isDeliverable: boolean, isCatchAll: boolean, mxFound: boolean ): EmailVerification { return { email, result, isDeliverable, isCatchAll, mxFound }; } /** * Generates email pattern candidates for a name + domain. * Returns ordered list from most to least common pattern. */ export function generateEmailPatterns( firstName: string, lastName: string, domain: string ): string[] { const f = firstName.toLowerCase().replace(/[^a-z]/g, ""); const l = lastName.toLowerCase().replace(/[^a-z]/g, ""); return [ `${f}.${l}@${domain}`, `${f}${l}@${domain}`, `${f[0]}${l}@${domain}`, `${f}@${domain}`, `${f[0]}.${l}@${domain}`, `${l}.${f}@${domain}`, ].filter(Boolean); }