import axios from "axios"; import { getEnv } from "../../shared/config/env"; import { withRetry, isCircuitOpen, recordFailure, recordSuccess } from "../../shared/utils/retry"; import { hunterLimiter } from "../../shared/utils/rate-limiter"; import { logger } from "../../shared/utils/logger"; const PROVIDER = "hunter"; export interface HunterEmailResult { email: string | null; score: number; // Hunter confidence 0-100 source: "hunter"; firstName: string | null; lastName: string | null; } /** * Finds a professional email address using Hunter.io. * Falls through to pattern generation if not found. */ export async function findEmail( domain: string, firstName: string, lastName: string ): Promise { if (isCircuitOpen(PROVIDER)) return null; await hunterLimiter.consume(PROVIDER); try { const result = await withRetry( () => callHunterEmailFinder(domain, firstName, lastName), { provider: PROVIDER } ); recordSuccess(PROVIDER); return result; } catch (err) { recordFailure(PROVIDER); logger.warn({ domain, err }, "Hunter email find failed — will try pattern generation"); return null; } } /** * Searches all known emails for a domain (domain search). */ export async function searchDomain(domain: string): Promise { if (isCircuitOpen(PROVIDER)) return []; await hunterLimiter.consume(PROVIDER); try { const result = await withRetry( () => callHunterDomainSearch(domain), { provider: PROVIDER } ); recordSuccess(PROVIDER); return result; } catch (err) { recordFailure(PROVIDER); logger.warn({ domain, err }, "Hunter domain search failed"); return []; } } async function callHunterEmailFinder( domain: string, firstName: string, lastName: string ): Promise { const env = getEnv(); const response = await axios.get("https://api.hunter.io/v2/email-finder", { params: { domain, first_name: firstName, last_name: lastName, api_key: env.HUNTER_API_KEY, }, timeout: 8_000, }); const data = response.data?.data; if (!data?.email) return null; return { email: data.email, score: data.score ?? 0, source: "hunter", firstName: data.first_name ?? null, lastName: data.last_name ?? null, }; } async function callHunterDomainSearch(domain: string): Promise { const env = getEnv(); const response = await axios.get("https://api.hunter.io/v2/domain-search", { params: { domain, api_key: env.HUNTER_API_KEY, limit: 10 }, timeout: 8_000, }); const emails = response.data?.data?.emails ?? []; return emails .filter((e: { type: string }) => e.type === "professional") .map((e: { value: string; confidence: number; first_name: string; last_name: string }) => ({ email: e.value, score: e.confidence, source: "hunter" as const, firstName: e.first_name ?? null, lastName: e.last_name ?? null, })); } // ─── Aliases for contact-enricher.ts compatibility ────────── export type HunterContact = { value: string; // email first_name: string | null; last_name: string | null; position: string | null; seniority: string | null; confidence: number; }; /** * Search for contacts at a domain — used by contact-enricher. * Maps Hunter's domain-search response to HunterContact format. */ export async function searchHunterContacts(domain: string): Promise { if (isCircuitOpen(PROVIDER)) return []; await hunterLimiter.consume(PROVIDER); try { const env = getEnv(); const response = await axios.get("https://api.hunter.io/v2/domain-search", { params: { domain, api_key: env.HUNTER_API_KEY, limit: 10 }, timeout: 8_000, }); recordSuccess(PROVIDER); const emails = response.data?.data?.emails ?? []; return emails.map((e: Record) => ({ value: (e.value as string) ?? "", first_name: (e.first_name as string) ?? null, last_name: (e.last_name as string) ?? null, position: (e.position as string) ?? null, seniority: (e.seniority as string) ?? null, confidence: (e.confidence as number) ?? 0, })); } catch (err) { recordFailure(PROVIDER); logger.warn({ domain, err }, "Hunter domain search failed"); return []; } }