iDevBuddy
feat: Phase 1 — AI Client Acquisition System
bd28470
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<EmailVerification> {
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<EmailVerification> {
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<EmailVerification> {
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);
}