linkedin-secret-sauce 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ /**
3
+ * HTTP Retry Utilities
4
+ *
5
+ * Shared utilities for HTTP requests with retry logic, rate limit handling,
6
+ * and exponential backoff. Used across all enrichment providers.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.delay = delay;
10
+ exports.truthy = truthy;
11
+ exports.mapVerifiedStatus = mapVerifiedStatus;
12
+ exports.fetchWithRetry = fetchWithRetry;
13
+ exports.getWithRetry = getWithRetry;
14
+ exports.postWithRetry = postWithRetry;
15
+ exports.safeJsonParse = safeJsonParse;
16
+ /**
17
+ * Delay execution for specified milliseconds
18
+ */
19
+ async function delay(ms) {
20
+ return new Promise((r) => setTimeout(r, ms));
21
+ }
22
+ /**
23
+ * Check if a value is truthy (not null, undefined, or empty string)
24
+ */
25
+ function truthy(v) {
26
+ return v !== undefined && v !== null && String(v).length > 0;
27
+ }
28
+ /**
29
+ * Map common verification status strings to boolean
30
+ */
31
+ function mapVerifiedStatus(status) {
32
+ if (!status)
33
+ return undefined;
34
+ const s = String(status).toLowerCase();
35
+ if (s === 'valid' || s === 'verified' || s === 'deliverable')
36
+ return true;
37
+ if (s === 'invalid' || s === 'unverified' || s === 'undeliverable')
38
+ return false;
39
+ return undefined; // catch-all/unknown/webmail -> leave undefined
40
+ }
41
+ /**
42
+ * HTTP request with retry on rate limit and transient errors
43
+ *
44
+ * Features:
45
+ * - Exponential backoff on rate limit (429)
46
+ * - Retry on server errors (502, 503, 504)
47
+ * - Configurable timeout
48
+ * - Returns typed response
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const response = await fetchWithRetry<MyApiResponse>(
53
+ * 'https://api.example.com/data',
54
+ * {
55
+ * method: 'POST',
56
+ * headers: { 'Content-Type': 'application/json' },
57
+ * body: JSON.stringify({ query: 'test' }),
58
+ * },
59
+ * { retries: 2, backoffMs: 300 }
60
+ * );
61
+ * ```
62
+ */
63
+ async function fetchWithRetry(url, init, options) {
64
+ const { retries = 1, backoffMs = 200, timeoutMs = 30000, retryOnStatus = [429, 502, 503, 504], } = options ?? {};
65
+ let lastErr = null;
66
+ for (let attempt = 0; attempt <= retries; attempt++) {
67
+ const controller = new AbortController();
68
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
69
+ try {
70
+ const res = await fetch(url, {
71
+ ...init,
72
+ signal: controller.signal,
73
+ });
74
+ clearTimeout(timeoutId);
75
+ // Check if we should retry based on status
76
+ if (retryOnStatus.includes(res.status) && attempt < retries) {
77
+ const waitMs = backoffMs * Math.pow(2, attempt);
78
+ await delay(waitMs);
79
+ continue;
80
+ }
81
+ // Handle non-OK responses
82
+ if (!res.ok) {
83
+ const errorText = await res.text().catch(() => '');
84
+ lastErr = new Error(`HTTP ${res.status}: ${res.statusText}${errorText ? ` - ${errorText.slice(0, 200)}` : ''}`);
85
+ break;
86
+ }
87
+ const json = await res.json();
88
+ return json;
89
+ }
90
+ catch (err) {
91
+ clearTimeout(timeoutId);
92
+ // Handle abort/timeout
93
+ if (err instanceof Error && err.name === 'AbortError') {
94
+ lastErr = new Error(`Request timeout after ${timeoutMs}ms`);
95
+ }
96
+ else {
97
+ lastErr = err instanceof Error ? err : new Error(String(err));
98
+ }
99
+ // Retry on network errors
100
+ if (attempt < retries) {
101
+ const waitMs = backoffMs * Math.pow(2, attempt);
102
+ await delay(waitMs);
103
+ continue;
104
+ }
105
+ }
106
+ }
107
+ throw lastErr ?? new Error('Request failed');
108
+ }
109
+ /**
110
+ * Simpler GET request with retry (no request body)
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * const data = await getWithRetry<UserData>(
115
+ * `https://api.example.com/users/${id}`,
116
+ * { 'Authorization': `Bearer ${token}` }
117
+ * );
118
+ * ```
119
+ */
120
+ async function getWithRetry(url, headers, options) {
121
+ return fetchWithRetry(url, {
122
+ method: 'GET',
123
+ headers: {
124
+ 'Accept': 'application/json',
125
+ ...headers,
126
+ },
127
+ }, options);
128
+ }
129
+ /**
130
+ * POST request with JSON body and retry
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * const result = await postWithRetry<SearchResult>(
135
+ * 'https://api.example.com/search',
136
+ * { query: 'test', limit: 10 },
137
+ * { 'X-Api-Key': apiKey }
138
+ * );
139
+ * ```
140
+ */
141
+ async function postWithRetry(url, body, headers, options) {
142
+ return fetchWithRetry(url, {
143
+ method: 'POST',
144
+ headers: {
145
+ 'Content-Type': 'application/json',
146
+ 'Accept': 'application/json',
147
+ ...headers,
148
+ },
149
+ body: JSON.stringify(body),
150
+ }, options);
151
+ }
152
+ /**
153
+ * Safe JSON parse with fallback
154
+ */
155
+ function safeJsonParse(str, fallback) {
156
+ try {
157
+ return JSON.parse(str);
158
+ }
159
+ catch {
160
+ return fallback;
161
+ }
162
+ }
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Email Verification Module
3
3
  */
4
- export { verifyEmailMx } from './mx';
4
+ export { verifyEmailMx, checkDomainCatchAll, verifyEmailsExist } from './mx';
@@ -3,6 +3,8 @@
3
3
  * Email Verification Module
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.verifyEmailMx = void 0;
6
+ exports.verifyEmailsExist = exports.checkDomainCatchAll = exports.verifyEmailMx = void 0;
7
7
  var mx_1 = require("./mx");
8
8
  Object.defineProperty(exports, "verifyEmailMx", { enumerable: true, get: function () { return mx_1.verifyEmailMx; } });
9
+ Object.defineProperty(exports, "checkDomainCatchAll", { enumerable: true, get: function () { return mx_1.checkDomainCatchAll; } });
10
+ Object.defineProperty(exports, "verifyEmailsExist", { enumerable: true, get: function () { return mx_1.verifyEmailsExist; } });
@@ -2,8 +2,25 @@
2
2
  * MX Record Resolution and Email Verification
3
3
  *
4
4
  * Provides MX record resolution with timeout support and confidence scoring.
5
+ * Includes SMTP-based catch-all detection using RCPT TO verification.
5
6
  */
6
7
  import type { VerificationResult } from '../types';
8
+ /**
9
+ * Verify multiple email addresses with rate limiting
10
+ *
11
+ * @param emails - Array of email addresses to verify
12
+ * @param options - Options including delay between checks
13
+ * @returns Array of verification results
14
+ */
15
+ export declare function verifyEmailsExist(emails: string[], options?: {
16
+ delayMs?: number;
17
+ timeoutMs?: number;
18
+ }): Promise<Array<{
19
+ email: string;
20
+ exists: boolean | null;
21
+ mxHost: string | null;
22
+ error?: string;
23
+ }>>;
7
24
  /**
8
25
  * Verify an email address via MX record lookup
9
26
  *
@@ -11,6 +28,22 @@ import type { VerificationResult } from '../types';
11
28
  * @param options - Verification options
12
29
  * @returns Verification result with confidence score
13
30
  */
31
+ /**
32
+ * Check if a domain is a catch-all (accepts all email addresses)
33
+ *
34
+ * @param domain - Domain to check
35
+ * @param options - Options including timeout
36
+ * @returns Object with isCatchAll status and MX records
37
+ */
38
+ export declare function checkDomainCatchAll(domain: string, options?: {
39
+ timeoutMs?: number;
40
+ }): Promise<{
41
+ isCatchAll: boolean | null;
42
+ mxRecords: string[];
43
+ mxHost: string | null;
44
+ error?: string;
45
+ }>;
14
46
  export declare function verifyEmailMx(email: string, options?: {
15
47
  timeoutMs?: number;
48
+ checkCatchAll?: boolean;
16
49
  }): Promise<VerificationResult>;
@@ -3,12 +3,52 @@
3
3
  * MX Record Resolution and Email Verification
4
4
  *
5
5
  * Provides MX record resolution with timeout support and confidence scoring.
6
+ * Includes SMTP-based catch-all detection using RCPT TO verification.
6
7
  */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
7
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.verifyEmailsExist = verifyEmailsExist;
43
+ exports.checkDomainCatchAll = checkDomainCatchAll;
8
44
  exports.verifyEmailMx = verifyEmailMx;
9
45
  const promises_1 = require("node:dns/promises");
46
+ const net = __importStar(require("node:net"));
10
47
  const disposable_domains_1 = require("../utils/disposable-domains");
11
48
  const validation_1 = require("../utils/validation");
49
+ // Cache for catch-all results (domain -> isCatchAll)
50
+ const catchAllCache = new Map();
51
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
12
52
  /**
13
53
  * Resolve MX records with timeout
14
54
  *
@@ -30,6 +70,273 @@ async function resolveMxWithTimeout(domain, timeoutMs) {
30
70
  clearTimeout(timeoutId);
31
71
  }
32
72
  }
73
+ /**
74
+ * Generate a random string for catch-all testing
75
+ */
76
+ function generateRandomLocalPart() {
77
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
78
+ let result = 'catchalltest_';
79
+ for (let i = 0; i < 16; i++) {
80
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
81
+ }
82
+ return result;
83
+ }
84
+ /**
85
+ * Detect if a domain is a catch-all by testing with a random email address
86
+ * Uses SMTP RCPT TO verification
87
+ *
88
+ * @param domain - Domain to test
89
+ * @param mxHost - MX server hostname
90
+ * @param timeoutMs - Timeout in milliseconds
91
+ * @returns true if catch-all, false if not, null if unable to determine
92
+ */
93
+ async function detectCatchAll(domain, mxHost, timeoutMs = 10000) {
94
+ // Check cache first
95
+ const cached = catchAllCache.get(domain);
96
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
97
+ return cached.isCatchAll;
98
+ }
99
+ return new Promise((resolve) => {
100
+ const randomEmail = `${generateRandomLocalPart()}@${domain}`;
101
+ const socket = new net.Socket();
102
+ let step = 0;
103
+ let responseBuffer = '';
104
+ const cleanup = () => {
105
+ socket.removeAllListeners();
106
+ socket.destroy();
107
+ };
108
+ const timeout = setTimeout(() => {
109
+ cleanup();
110
+ resolve(null); // Unable to determine
111
+ }, timeoutMs);
112
+ socket.on('error', () => {
113
+ clearTimeout(timeout);
114
+ cleanup();
115
+ resolve(null); // Unable to determine
116
+ });
117
+ socket.on('close', () => {
118
+ clearTimeout(timeout);
119
+ cleanup();
120
+ });
121
+ socket.on('data', (data) => {
122
+ responseBuffer += data.toString();
123
+ // Process complete lines
124
+ const lines = responseBuffer.split('\r\n');
125
+ responseBuffer = lines.pop() || ''; // Keep incomplete line in buffer
126
+ for (const line of lines) {
127
+ if (!line)
128
+ continue;
129
+ const code = parseInt(line.substring(0, 3), 10);
130
+ switch (step) {
131
+ case 0: // Waiting for greeting
132
+ if (code === 220) {
133
+ step = 1;
134
+ socket.write(`HELO verify.local\r\n`);
135
+ }
136
+ else {
137
+ cleanup();
138
+ resolve(null);
139
+ }
140
+ break;
141
+ case 1: // Waiting for HELO response
142
+ if (code === 250) {
143
+ step = 2;
144
+ socket.write(`MAIL FROM:<test@verify.local>\r\n`);
145
+ }
146
+ else {
147
+ cleanup();
148
+ resolve(null);
149
+ }
150
+ break;
151
+ case 2: // Waiting for MAIL FROM response
152
+ if (code === 250) {
153
+ step = 3;
154
+ socket.write(`RCPT TO:<${randomEmail}>\r\n`);
155
+ }
156
+ else {
157
+ cleanup();
158
+ resolve(null);
159
+ }
160
+ break;
161
+ case 3: // Waiting for RCPT TO response - this is the key!
162
+ clearTimeout(timeout);
163
+ socket.write('QUIT\r\n');
164
+ // 250 = accepted (catch-all)
165
+ // 550, 551, 552, 553 = rejected (not catch-all)
166
+ // Other codes = unable to determine
167
+ const isCatchAll = code === 250;
168
+ const isRejected = code >= 550 && code <= 559;
169
+ if (isCatchAll || isRejected) {
170
+ // Cache the result
171
+ catchAllCache.set(domain, {
172
+ isCatchAll,
173
+ timestamp: Date.now(),
174
+ });
175
+ cleanup();
176
+ resolve(isCatchAll);
177
+ }
178
+ else {
179
+ cleanup();
180
+ resolve(null);
181
+ }
182
+ break;
183
+ }
184
+ }
185
+ });
186
+ // Connect to MX server on port 25
187
+ socket.connect(25, mxHost);
188
+ });
189
+ }
190
+ /**
191
+ * Verify if a specific email address exists via SMTP RCPT TO
192
+ *
193
+ * @param email - Email address to verify
194
+ * @param mxHost - MX server hostname
195
+ * @param timeoutMs - Timeout in milliseconds
196
+ * @returns true if exists, false if not, null if unable to determine
197
+ */
198
+ async function verifyEmailExists(email, mxHost, timeoutMs = 10000) {
199
+ return new Promise((resolve) => {
200
+ const socket = new net.Socket();
201
+ let step = 0;
202
+ let responseBuffer = '';
203
+ const cleanup = () => {
204
+ socket.removeAllListeners();
205
+ socket.destroy();
206
+ };
207
+ const timeout = setTimeout(() => {
208
+ cleanup();
209
+ resolve(null);
210
+ }, timeoutMs);
211
+ socket.on('error', () => {
212
+ clearTimeout(timeout);
213
+ cleanup();
214
+ resolve(null);
215
+ });
216
+ socket.on('close', () => {
217
+ clearTimeout(timeout);
218
+ cleanup();
219
+ });
220
+ socket.on('data', (data) => {
221
+ responseBuffer += data.toString();
222
+ const lines = responseBuffer.split('\r\n');
223
+ responseBuffer = lines.pop() || '';
224
+ for (const line of lines) {
225
+ if (!line)
226
+ continue;
227
+ const code = parseInt(line.substring(0, 3), 10);
228
+ switch (step) {
229
+ case 0: // Waiting for greeting
230
+ if (code === 220) {
231
+ step = 1;
232
+ socket.write(`HELO verify.local\r\n`);
233
+ }
234
+ else {
235
+ cleanup();
236
+ resolve(null);
237
+ }
238
+ break;
239
+ case 1: // Waiting for HELO response
240
+ if (code === 250) {
241
+ step = 2;
242
+ socket.write(`MAIL FROM:<test@verify.local>\r\n`);
243
+ }
244
+ else {
245
+ cleanup();
246
+ resolve(null);
247
+ }
248
+ break;
249
+ case 2: // Waiting for MAIL FROM response
250
+ if (code === 250) {
251
+ step = 3;
252
+ socket.write(`RCPT TO:<${email}>\r\n`);
253
+ }
254
+ else {
255
+ cleanup();
256
+ resolve(null);
257
+ }
258
+ break;
259
+ case 3: // Waiting for RCPT TO response
260
+ clearTimeout(timeout);
261
+ socket.write('QUIT\r\n');
262
+ // 250 = accepted (email exists or catch-all)
263
+ // 550, 551, 552, 553 = rejected (email doesn't exist)
264
+ const exists = code === 250;
265
+ const rejected = code >= 550 && code <= 559;
266
+ if (exists || rejected) {
267
+ cleanup();
268
+ resolve(exists);
269
+ }
270
+ else {
271
+ cleanup();
272
+ resolve(null);
273
+ }
274
+ break;
275
+ }
276
+ }
277
+ });
278
+ socket.connect(25, mxHost);
279
+ });
280
+ }
281
+ /**
282
+ * Verify multiple email addresses with rate limiting
283
+ *
284
+ * @param emails - Array of email addresses to verify
285
+ * @param options - Options including delay between checks
286
+ * @returns Array of verification results
287
+ */
288
+ async function verifyEmailsExist(emails, options) {
289
+ const delayMs = options?.delayMs ?? 3000; // 3 second delay between checks
290
+ const timeoutMs = options?.timeoutMs ?? 10000;
291
+ const results = [];
292
+ // Group emails by domain to reuse MX lookups
293
+ const emailsByDomain = new Map();
294
+ for (const email of emails) {
295
+ const domain = email.split('@')[1]?.toLowerCase();
296
+ if (domain) {
297
+ const existing = emailsByDomain.get(domain) || [];
298
+ existing.push(email);
299
+ emailsByDomain.set(domain, existing);
300
+ }
301
+ else {
302
+ results.push({ email, exists: null, mxHost: null, error: 'Invalid email format' });
303
+ }
304
+ }
305
+ // Process each domain
306
+ for (const [domain, domainEmails] of emailsByDomain) {
307
+ // Get MX records for domain
308
+ const mx = await resolveMxWithTimeout(domain, 5000);
309
+ if (!mx || mx.length === 0) {
310
+ for (const email of domainEmails) {
311
+ results.push({ email, exists: null, mxHost: null, error: 'No MX records found' });
312
+ }
313
+ continue;
314
+ }
315
+ const sortedMx = mx.sort((a, b) => a.priority - b.priority);
316
+ const mxHost = sortedMx[0].exchange;
317
+ // Verify each email with delay
318
+ for (let i = 0; i < domainEmails.length; i++) {
319
+ const email = domainEmails[i];
320
+ // Add delay between checks (except for first one)
321
+ if (i > 0) {
322
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
323
+ }
324
+ try {
325
+ const exists = await verifyEmailExists(email, mxHost, timeoutMs);
326
+ results.push({ email, exists, mxHost });
327
+ }
328
+ catch (err) {
329
+ results.push({
330
+ email,
331
+ exists: null,
332
+ mxHost,
333
+ error: err instanceof Error ? err.message : 'Verification failed',
334
+ });
335
+ }
336
+ }
337
+ }
338
+ return results;
339
+ }
33
340
  /**
34
341
  * Calculate confidence score based on verification factors
35
342
  */
@@ -43,9 +350,14 @@ function calculateConfidence(params) {
43
350
  score += 20;
44
351
  else if (mxCount === 1)
45
352
  score += 10;
46
- // Non-catch-all bonus (we assume catch-all if MX exists but can't verify)
47
- if (!isCatchAll)
353
+ // Catch-all scoring:
354
+ // - Not catch-all (false): +30 bonus
355
+ // - Is catch-all (true): no bonus
356
+ // - Unknown (null): +15 (partial bonus)
357
+ if (isCatchAll === false)
48
358
  score += 30;
359
+ else if (isCatchAll === null)
360
+ score += 15;
49
361
  // Non-role account bonus
50
362
  if (!isRoleAccount)
51
363
  score += 20;
@@ -61,8 +373,50 @@ function calculateConfidence(params) {
61
373
  * @param options - Verification options
62
374
  * @returns Verification result with confidence score
63
375
  */
376
+ /**
377
+ * Check if a domain is a catch-all (accepts all email addresses)
378
+ *
379
+ * @param domain - Domain to check
380
+ * @param options - Options including timeout
381
+ * @returns Object with isCatchAll status and MX records
382
+ */
383
+ async function checkDomainCatchAll(domain, options) {
384
+ const timeoutMs = options?.timeoutMs ?? 10000;
385
+ try {
386
+ // Get MX records
387
+ const mx = await resolveMxWithTimeout(domain, 5000);
388
+ if (!mx || mx.length === 0) {
389
+ return {
390
+ isCatchAll: null,
391
+ mxRecords: [],
392
+ mxHost: null,
393
+ error: 'No MX records found',
394
+ };
395
+ }
396
+ // Sort by priority and get primary
397
+ const sortedMx = mx.sort((a, b) => a.priority - b.priority);
398
+ const primaryMxHost = sortedMx[0].exchange;
399
+ const mxRecords = sortedMx.map((m) => m.exchange);
400
+ // Detect catch-all
401
+ const isCatchAll = await detectCatchAll(domain, primaryMxHost, timeoutMs);
402
+ return {
403
+ isCatchAll,
404
+ mxRecords,
405
+ mxHost: primaryMxHost,
406
+ };
407
+ }
408
+ catch (err) {
409
+ return {
410
+ isCatchAll: null,
411
+ mxRecords: [],
412
+ mxHost: null,
413
+ error: err instanceof Error ? err.message : 'Unknown error',
414
+ };
415
+ }
416
+ }
64
417
  async function verifyEmailMx(email, options) {
65
418
  const timeoutMs = options?.timeoutMs ?? 5000;
419
+ const checkCatchAll = options?.checkCatchAll ?? false;
66
420
  // Step 1: Syntax validation
67
421
  if (!(0, validation_1.isValidEmailSyntax)(email)) {
68
422
  return {
@@ -113,8 +467,14 @@ async function verifyEmailMx(email, options) {
113
467
  mxRecords: [],
114
468
  };
115
469
  }
116
- // Conservative: assume catch-all if MX exists (full detection requires SMTP handshake)
117
- const catchAll = true;
470
+ // Sort MX records by priority (lowest first)
471
+ const sortedMx = mx.sort((a, b) => a.priority - b.priority);
472
+ const primaryMxHost = sortedMx[0].exchange;
473
+ // Detect catch-all if requested (uses SMTP RCPT TO verification)
474
+ let catchAll = null;
475
+ if (checkCatchAll) {
476
+ catchAll = await detectCatchAll(domain, primaryMxHost, timeoutMs);
477
+ }
118
478
  // Calculate confidence
119
479
  const confidence = calculateConfidence({
120
480
  mxValid: true,
@@ -127,17 +487,17 @@ async function verifyEmailMx(email, options) {
127
487
  // Determine reason
128
488
  let reason = 'valid';
129
489
  if (!valid) {
130
- if (catchAll)
490
+ if (catchAll === true)
131
491
  reason = 'catch_all';
132
492
  else if (roleAccount)
133
493
  reason = 'role_account';
134
494
  }
135
- const mxRecords = mx.map((m) => m.exchange);
495
+ const mxRecords = sortedMx.map((m) => m.exchange);
136
496
  return {
137
497
  valid,
138
498
  confidence,
139
499
  reason,
140
- isCatchAll: catchAll,
500
+ isCatchAll: catchAll ?? false, // Default to false if unknown
141
501
  isRoleAccount: roleAccount,
142
502
  isDisposable: false,
143
503
  mxRecords,