Spaces:
Running
Running
File size: 3,124 Bytes
bd28470 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | 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);
}
|