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);
}