linkedin-secret-sauce 0.10.1 → 0.11.1
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.
- package/README.md +512 -232
- package/dist/enrichment/auth/smartlead-auth.d.ts +3 -3
- package/dist/enrichment/auth/smartlead-auth.js +25 -25
- package/dist/enrichment/index.d.ts +6 -4
- package/dist/enrichment/index.js +45 -13
- package/dist/enrichment/matching.d.ts +8 -3
- package/dist/enrichment/matching.js +7 -5
- package/dist/enrichment/orchestrator.js +48 -9
- package/dist/enrichment/providers/bouncer.d.ts +67 -0
- package/dist/enrichment/providers/bouncer.js +233 -0
- package/dist/enrichment/providers/construct.js +72 -14
- package/dist/enrichment/providers/hunter.js +6 -60
- package/dist/enrichment/providers/index.d.ts +2 -1
- package/dist/enrichment/providers/index.js +11 -3
- package/dist/enrichment/providers/ldd.js +5 -47
- package/dist/enrichment/providers/smartprospect.js +9 -14
- package/dist/enrichment/providers/snovio.d.ts +58 -0
- package/dist/enrichment/providers/snovio.js +286 -0
- package/dist/enrichment/types.d.ts +133 -10
- package/dist/enrichment/types.js +28 -6
- package/dist/enrichment/utils/http-retry.d.ts +96 -0
- package/dist/enrichment/utils/http-retry.js +162 -0
- package/dist/enrichment/verification/index.d.ts +1 -1
- package/dist/enrichment/verification/index.js +3 -1
- package/dist/enrichment/verification/mx.d.ts +33 -0
- package/dist/enrichment/verification/mx.js +367 -7
- package/dist/index.d.ts +196 -6
- package/dist/index.js +159 -12
- package/dist/parsers/search-parser.js +7 -3
- package/package.json +10 -3
|
@@ -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
|
+
}
|
|
@@ -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
|
-
//
|
|
47
|
-
|
|
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
|
-
//
|
|
117
|
-
const
|
|
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 =
|
|
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,
|