linkedin-secret-sauce 0.12.1 → 0.12.2
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 +50 -21
- package/dist/cosiall-client.d.ts +1 -1
- package/dist/cosiall-client.js +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/enrichment/index.js +11 -2
- package/dist/enrichment/matching.d.ts +16 -2
- package/dist/enrichment/matching.js +387 -65
- package/dist/enrichment/providers/bounceban.d.ts +82 -0
- package/dist/enrichment/providers/bounceban.js +447 -0
- package/dist/enrichment/providers/bouncer.d.ts +1 -1
- package/dist/enrichment/providers/bouncer.js +19 -21
- package/dist/enrichment/providers/construct.d.ts +1 -1
- package/dist/enrichment/providers/construct.js +22 -38
- package/dist/enrichment/providers/cosiall.d.ts +1 -1
- package/dist/enrichment/providers/cosiall.js +3 -4
- package/dist/enrichment/providers/dropcontact.d.ts +15 -9
- package/dist/enrichment/providers/dropcontact.js +188 -19
- package/dist/enrichment/providers/hunter.d.ts +8 -1
- package/dist/enrichment/providers/hunter.js +52 -28
- package/dist/enrichment/providers/index.d.ts +2 -0
- package/dist/enrichment/providers/index.js +10 -1
- package/dist/enrichment/providers/ldd.d.ts +1 -10
- package/dist/enrichment/providers/ldd.js +20 -97
- package/dist/enrichment/providers/smartprospect.js +28 -48
- package/dist/enrichment/providers/snovio.d.ts +1 -1
- package/dist/enrichment/providers/snovio.js +29 -31
- package/dist/enrichment/providers/trykitt.d.ts +63 -0
- package/dist/enrichment/providers/trykitt.js +210 -0
- package/dist/enrichment/types.d.ts +210 -7
- package/dist/enrichment/types.js +16 -8
- package/dist/enrichment/utils/candidate-parser.d.ts +107 -0
- package/dist/enrichment/utils/candidate-parser.js +173 -0
- package/dist/enrichment/utils/noop-provider.d.ts +39 -0
- package/dist/enrichment/utils/noop-provider.js +37 -0
- package/dist/enrichment/utils/rate-limiter.d.ts +103 -0
- package/dist/enrichment/utils/rate-limiter.js +204 -0
- package/dist/enrichment/utils/validation.d.ts +75 -3
- package/dist/enrichment/utils/validation.js +164 -11
- package/dist/linkedin-api.d.ts +40 -1
- package/dist/linkedin-api.js +160 -27
- package/dist/types.d.ts +50 -1
- package/dist/utils/lru-cache.d.ts +105 -0
- package/dist/utils/lru-cache.js +175 -0
- package/package.json +25 -26
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* BounceBan Email Verification Provider
|
|
4
|
+
*
|
|
5
|
+
* Specializes in catch-all email verification with 85-95% accuracy
|
|
6
|
+
* using proprietary algorithms WITHOUT sending actual emails.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Catch-all domain verification (industry-leading)
|
|
10
|
+
* - DeepVerify mode for pattern-guessed emails (+15-25% accuracy)
|
|
11
|
+
* - Waterfall endpoint with FREE 30-min retry window
|
|
12
|
+
* - Single email verification is FREE
|
|
13
|
+
* - GDPR compliant (no emails sent)
|
|
14
|
+
*
|
|
15
|
+
* API Endpoints:
|
|
16
|
+
* - GET /v1/verify/single - Standard single verification
|
|
17
|
+
* - GET https://api-waterfall.bounceban.com/v1/verify/single - Waterfall (recommended)
|
|
18
|
+
* - POST /v1/verify/bulk - Bulk verification
|
|
19
|
+
*
|
|
20
|
+
* Result values:
|
|
21
|
+
* - deliverable: Email verified (score 70-100)
|
|
22
|
+
* - risky: May work, often catch-all (score 30-70)
|
|
23
|
+
* - undeliverable: Email doesn't exist (score 0-30)
|
|
24
|
+
* - unknown: Could not determine
|
|
25
|
+
*
|
|
26
|
+
* @see https://bounceban.com
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.createBounceBanProvider = createBounceBanProvider;
|
|
30
|
+
exports.verifyEmailWithBounceBan = verifyEmailWithBounceBan;
|
|
31
|
+
exports.verifyEmailsBatchWithBounceBan = verifyEmailsBatchWithBounceBan;
|
|
32
|
+
exports.checkCatchAllWithBounceBan = checkCatchAllWithBounceBan;
|
|
33
|
+
const http_retry_1 = require("../utils/http-retry");
|
|
34
|
+
const noop_provider_1 = require("../utils/noop-provider");
|
|
35
|
+
const DEFAULT_API_URL = "https://api.bounceban.com";
|
|
36
|
+
const DEFAULT_WATERFALL_API_URL = "https://api-waterfall.bounceban.com";
|
|
37
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
38
|
+
const DEFAULT_WATERFALL_TIMEOUT_MS = 80000;
|
|
39
|
+
/**
|
|
40
|
+
* Map BounceBan result to confidence score
|
|
41
|
+
*/
|
|
42
|
+
function resultToConfidence(result, score, isAcceptAll) {
|
|
43
|
+
// Use the score directly if provided (0-100)
|
|
44
|
+
if (score >= 0 && score <= 100) {
|
|
45
|
+
// Reduce confidence for catch-all domains
|
|
46
|
+
if (isAcceptAll && result === "deliverable") {
|
|
47
|
+
return Math.min(score, 75); // Cap at 75 for catch-all even if deliverable
|
|
48
|
+
}
|
|
49
|
+
return score;
|
|
50
|
+
}
|
|
51
|
+
// Fallback based on result type
|
|
52
|
+
switch (result) {
|
|
53
|
+
case "deliverable":
|
|
54
|
+
return isAcceptAll ? 70 : 95;
|
|
55
|
+
case "risky":
|
|
56
|
+
return isAcceptAll ? 50 : 60;
|
|
57
|
+
case "undeliverable":
|
|
58
|
+
return 10;
|
|
59
|
+
case "unknown":
|
|
60
|
+
default:
|
|
61
|
+
return 30;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Map BounceBan result to verified status
|
|
66
|
+
*/
|
|
67
|
+
function resultToVerified(result) {
|
|
68
|
+
switch (result) {
|
|
69
|
+
case "deliverable":
|
|
70
|
+
return true;
|
|
71
|
+
case "undeliverable":
|
|
72
|
+
return false;
|
|
73
|
+
case "risky":
|
|
74
|
+
case "unknown":
|
|
75
|
+
default:
|
|
76
|
+
return undefined; // Can't determine
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Extract emails to verify from candidate
|
|
81
|
+
* BounceBan is a VERIFICATION provider - it needs emails to verify
|
|
82
|
+
*/
|
|
83
|
+
function extractEmailsToVerify(candidate) {
|
|
84
|
+
const emails = [];
|
|
85
|
+
// Check if candidate has pre-generated email patterns in metadata
|
|
86
|
+
const metadata = candidate._emailCandidates;
|
|
87
|
+
if (Array.isArray(metadata)) {
|
|
88
|
+
emails.push(...metadata.filter((e) => typeof e === "string"));
|
|
89
|
+
}
|
|
90
|
+
// Also check for a single email field
|
|
91
|
+
const singleEmail = candidate.email;
|
|
92
|
+
if (typeof singleEmail === "string" && singleEmail.includes("@")) {
|
|
93
|
+
emails.push(singleEmail);
|
|
94
|
+
}
|
|
95
|
+
return [...new Set(emails)]; // Deduplicate
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Verify a single email via BounceBan standard endpoint
|
|
99
|
+
*/
|
|
100
|
+
async function verifyEmailStandard(email, apiKey, apiUrl, timeoutMs, useDeepVerify) {
|
|
101
|
+
const params = new URLSearchParams({
|
|
102
|
+
api_key: apiKey,
|
|
103
|
+
email: email,
|
|
104
|
+
});
|
|
105
|
+
if (useDeepVerify) {
|
|
106
|
+
params.set("mode", "deepverify");
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const response = await (0, http_retry_1.getWithRetry)(`${apiUrl}/v1/verify/single?${params.toString()}`, undefined, {
|
|
110
|
+
retries: 1,
|
|
111
|
+
backoffMs: 500,
|
|
112
|
+
timeoutMs,
|
|
113
|
+
retryOnStatus: [429, 500, 502, 503, 504],
|
|
114
|
+
});
|
|
115
|
+
return response;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Verify a single email via BounceBan waterfall endpoint
|
|
123
|
+
*
|
|
124
|
+
* The waterfall endpoint is optimized for:
|
|
125
|
+
* - Single HTTP request (no polling needed)
|
|
126
|
+
* - FREE retries within 30-min window if timeout
|
|
127
|
+
* - Better catch-all handling
|
|
128
|
+
*/
|
|
129
|
+
async function verifyEmailWaterfall(email, apiKey, waterfallApiUrl, timeoutMs, useDeepVerify) {
|
|
130
|
+
const params = new URLSearchParams({
|
|
131
|
+
api_key: apiKey,
|
|
132
|
+
email: email,
|
|
133
|
+
timeout: String(Math.floor(timeoutMs / 1000)), // Convert to seconds
|
|
134
|
+
});
|
|
135
|
+
if (useDeepVerify) {
|
|
136
|
+
params.set("mode", "deepverify");
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const response = await (0, http_retry_1.getWithRetry)(`${waterfallApiUrl}/v1/verify/single?${params.toString()}`, undefined, {
|
|
140
|
+
retries: 2, // Waterfall allows free retries within 30 mins
|
|
141
|
+
backoffMs: 5000, // Wait 5s between retries as recommended
|
|
142
|
+
timeoutMs: timeoutMs + 10000, // Add buffer for network
|
|
143
|
+
retryOnStatus: [408, 429, 500, 502, 503, 504], // Include 408 timeout
|
|
144
|
+
});
|
|
145
|
+
return response;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create the BounceBan verification provider
|
|
153
|
+
*
|
|
154
|
+
* This provider specializes in verifying emails on catch-all domains
|
|
155
|
+
* where traditional SMTP verification fails.
|
|
156
|
+
*
|
|
157
|
+
* Best used after construct provider generates email patterns.
|
|
158
|
+
*/
|
|
159
|
+
function createBounceBanProvider(config) {
|
|
160
|
+
const { apiKey, apiUrl = DEFAULT_API_URL, waterfallApiUrl = DEFAULT_WATERFALL_API_URL, timeoutMs, useDeepVerify = false, useWaterfall = true, } = config;
|
|
161
|
+
// Determine timeout based on endpoint
|
|
162
|
+
const effectiveTimeout = timeoutMs ??
|
|
163
|
+
(useWaterfall ? DEFAULT_WATERFALL_TIMEOUT_MS : DEFAULT_TIMEOUT_MS);
|
|
164
|
+
if (!apiKey) {
|
|
165
|
+
return (0, noop_provider_1.createNoOpProvider)("bounceban");
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Verify emails for a candidate
|
|
169
|
+
*/
|
|
170
|
+
async function verifyEmails(candidate) {
|
|
171
|
+
const emailsToVerify = extractEmailsToVerify(candidate);
|
|
172
|
+
// If no emails to verify, return null
|
|
173
|
+
if (emailsToVerify.length === 0) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const verifiedEmails = [];
|
|
177
|
+
// Verify each email
|
|
178
|
+
for (const email of emailsToVerify) {
|
|
179
|
+
// Add small delay between verifications to respect rate limits
|
|
180
|
+
if (verifiedEmails.length > 0) {
|
|
181
|
+
await (0, http_retry_1.delay)(200);
|
|
182
|
+
}
|
|
183
|
+
const result = useWaterfall
|
|
184
|
+
? await verifyEmailWaterfall(email, apiKey, waterfallApiUrl, effectiveTimeout, useDeepVerify)
|
|
185
|
+
: await verifyEmailStandard(email, apiKey, apiUrl, effectiveTimeout, useDeepVerify);
|
|
186
|
+
if (!result || result.status !== "success") {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const confidence = resultToConfidence(result.result, result.score, result.is_accept_all);
|
|
190
|
+
const verified = resultToVerified(result.result);
|
|
191
|
+
// Only include potentially deliverable emails
|
|
192
|
+
if (result.result === "deliverable" || result.result === "risky") {
|
|
193
|
+
verifiedEmails.push({
|
|
194
|
+
email: result.email,
|
|
195
|
+
verified,
|
|
196
|
+
confidence,
|
|
197
|
+
isCatchAll: result.is_accept_all,
|
|
198
|
+
metadata: {
|
|
199
|
+
bounceBanResult: result.result,
|
|
200
|
+
bounceBanScore: result.score,
|
|
201
|
+
smtpProvider: result.smtp_provider,
|
|
202
|
+
isDisposable: result.is_disposable,
|
|
203
|
+
isRole: result.is_role,
|
|
204
|
+
isFree: result.is_free,
|
|
205
|
+
mxRecords: result.mx_records,
|
|
206
|
+
mode: result.mode,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (verifiedEmails.length === 0) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
// Sort by confidence (highest first)
|
|
215
|
+
verifiedEmails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
|
|
216
|
+
return { emails: verifiedEmails };
|
|
217
|
+
}
|
|
218
|
+
// Mark provider name for orchestrator
|
|
219
|
+
verifyEmails.__name = "bounceban";
|
|
220
|
+
return verifyEmails;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Standalone function to verify a single email via BounceBan
|
|
224
|
+
*
|
|
225
|
+
* Uses the waterfall endpoint by default for best catch-all handling.
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```typescript
|
|
229
|
+
* const result = await verifyEmailWithBounceBan('john@catchall-domain.com', {
|
|
230
|
+
* apiKey: process.env.BOUNCEBAN_API_KEY,
|
|
231
|
+
* });
|
|
232
|
+
* console.log(result.result); // "deliverable" | "risky" | "undeliverable" | "unknown"
|
|
233
|
+
* console.log(result.score); // 0-100
|
|
234
|
+
* console.log(result.is_accept_all); // true (catch-all domain)
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
async function verifyEmailWithBounceBan(email, config) {
|
|
238
|
+
const { apiKey, waterfallApiUrl = DEFAULT_WATERFALL_API_URL, apiUrl = DEFAULT_API_URL, timeoutMs, useDeepVerify = false, useWaterfall = true, } = config;
|
|
239
|
+
if (!apiKey) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const effectiveTimeout = timeoutMs ??
|
|
243
|
+
(useWaterfall ? DEFAULT_WATERFALL_TIMEOUT_MS : DEFAULT_TIMEOUT_MS);
|
|
244
|
+
return useWaterfall
|
|
245
|
+
? verifyEmailWaterfall(email, apiKey, waterfallApiUrl, effectiveTimeout, useDeepVerify)
|
|
246
|
+
: verifyEmailStandard(email, apiKey, apiUrl, effectiveTimeout, useDeepVerify);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Submit emails for bulk verification via BounceBan
|
|
250
|
+
*
|
|
251
|
+
* @returns Task ID for polling status
|
|
252
|
+
*/
|
|
253
|
+
async function submitBulkVerification(emails, apiKey, apiUrl, useDeepVerify) {
|
|
254
|
+
try {
|
|
255
|
+
const response = await fetch(`${apiUrl}/v1/verify/bulk`, {
|
|
256
|
+
method: "POST",
|
|
257
|
+
headers: {
|
|
258
|
+
"Content-Type": "application/json",
|
|
259
|
+
},
|
|
260
|
+
body: JSON.stringify({
|
|
261
|
+
api_key: apiKey,
|
|
262
|
+
emails: emails,
|
|
263
|
+
mode: useDeepVerify ? "deepverify" : "regular",
|
|
264
|
+
}),
|
|
265
|
+
});
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const data = (await response.json());
|
|
270
|
+
return data.id || null;
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Check bulk verification task status
|
|
278
|
+
*/
|
|
279
|
+
async function checkBulkStatus(taskId, apiKey, apiUrl) {
|
|
280
|
+
try {
|
|
281
|
+
const params = new URLSearchParams({
|
|
282
|
+
api_key: apiKey,
|
|
283
|
+
id: taskId,
|
|
284
|
+
});
|
|
285
|
+
const response = await fetch(`${apiUrl}/v1/verify/bulk/status?${params.toString()}`);
|
|
286
|
+
if (!response.ok) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
return (await response.json());
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Fetch bulk verification results (paginated)
|
|
297
|
+
*/
|
|
298
|
+
async function fetchBulkResults(taskId, apiKey, apiUrl, page = 1, perPage = 100) {
|
|
299
|
+
try {
|
|
300
|
+
const response = await fetch(`${apiUrl}/v1/verify/bulk/dump`, {
|
|
301
|
+
method: "POST",
|
|
302
|
+
headers: {
|
|
303
|
+
"Content-Type": "application/json",
|
|
304
|
+
},
|
|
305
|
+
body: JSON.stringify({
|
|
306
|
+
api_key: apiKey,
|
|
307
|
+
id: taskId,
|
|
308
|
+
page: page,
|
|
309
|
+
per_page: perPage,
|
|
310
|
+
}),
|
|
311
|
+
});
|
|
312
|
+
if (!response.ok) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
return (await response.json());
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Verify multiple emails in batch via BounceBan Bulk API
|
|
323
|
+
*
|
|
324
|
+
* More cost-effective for large volumes ($0.003/email vs single free).
|
|
325
|
+
* Uses async bulk API with polling for results.
|
|
326
|
+
*
|
|
327
|
+
* For small batches (< 5 emails), falls back to sequential single verification.
|
|
328
|
+
*
|
|
329
|
+
* @example
|
|
330
|
+
* ```typescript
|
|
331
|
+
* const results = await verifyEmailsBatchWithBounceBan(
|
|
332
|
+
* ['john@example.com', 'jane@example.com'],
|
|
333
|
+
* { apiKey: process.env.BOUNCEBAN_API_KEY }
|
|
334
|
+
* );
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
337
|
+
async function verifyEmailsBatchWithBounceBan(emails, config) {
|
|
338
|
+
const results = new Map();
|
|
339
|
+
const { apiKey, apiUrl = DEFAULT_API_URL, useDeepVerify = false } = config;
|
|
340
|
+
if (!apiKey || emails.length === 0) {
|
|
341
|
+
return results;
|
|
342
|
+
}
|
|
343
|
+
// For small batches, use sequential single verification (it's free)
|
|
344
|
+
if (emails.length < 5) {
|
|
345
|
+
for (const email of emails) {
|
|
346
|
+
const result = await verifyEmailWithBounceBan(email, config);
|
|
347
|
+
results.set(email, result);
|
|
348
|
+
// Small delay between requests
|
|
349
|
+
if (emails.indexOf(email) < emails.length - 1) {
|
|
350
|
+
await (0, http_retry_1.delay)(200);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return results;
|
|
354
|
+
}
|
|
355
|
+
// For larger batches, use bulk API
|
|
356
|
+
const taskId = await submitBulkVerification(emails, apiKey, apiUrl, useDeepVerify);
|
|
357
|
+
if (!taskId) {
|
|
358
|
+
// Fallback to sequential if bulk submit fails
|
|
359
|
+
for (const email of emails) {
|
|
360
|
+
const result = await verifyEmailWithBounceBan(email, config);
|
|
361
|
+
results.set(email, result);
|
|
362
|
+
if (emails.indexOf(email) < emails.length - 1) {
|
|
363
|
+
await (0, http_retry_1.delay)(200);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return results;
|
|
367
|
+
}
|
|
368
|
+
// Poll for completion (max 5 minutes)
|
|
369
|
+
const maxPollTime = 5 * 60 * 1000;
|
|
370
|
+
const pollInterval = 2000;
|
|
371
|
+
const startTime = Date.now();
|
|
372
|
+
while (Date.now() - startTime < maxPollTime) {
|
|
373
|
+
await (0, http_retry_1.delay)(pollInterval);
|
|
374
|
+
const status = await checkBulkStatus(taskId, apiKey, apiUrl);
|
|
375
|
+
if (!status) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (status.status === "error") {
|
|
379
|
+
// Bulk failed, return empty results
|
|
380
|
+
return results;
|
|
381
|
+
}
|
|
382
|
+
if (status.status === "finished") {
|
|
383
|
+
// Fetch all results with pagination
|
|
384
|
+
let page = 1;
|
|
385
|
+
const perPage = 100;
|
|
386
|
+
let hasMore = true;
|
|
387
|
+
while (hasMore) {
|
|
388
|
+
const dumpResponse = await fetchBulkResults(taskId, apiKey, apiUrl, page, perPage);
|
|
389
|
+
if (!dumpResponse || dumpResponse.results.length === 0) {
|
|
390
|
+
hasMore = false;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
// Map results to our response format
|
|
394
|
+
for (const item of dumpResponse.results) {
|
|
395
|
+
const response = {
|
|
396
|
+
id: taskId,
|
|
397
|
+
status: "success",
|
|
398
|
+
email: item.email,
|
|
399
|
+
result: item.result,
|
|
400
|
+
score: item.score,
|
|
401
|
+
is_disposable: item.is_disposable,
|
|
402
|
+
is_accept_all: item.is_accept_all,
|
|
403
|
+
is_role: item.is_role,
|
|
404
|
+
is_free: item.is_free,
|
|
405
|
+
mx_records: item.mx_records,
|
|
406
|
+
smtp_provider: item.smtp_provider,
|
|
407
|
+
};
|
|
408
|
+
results.set(item.email, response);
|
|
409
|
+
}
|
|
410
|
+
// Check if there are more pages
|
|
411
|
+
if (dumpResponse.results.length < perPage) {
|
|
412
|
+
hasMore = false;
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
page++;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return results;
|
|
419
|
+
}
|
|
420
|
+
// Still processing, continue polling
|
|
421
|
+
}
|
|
422
|
+
// Timeout - return whatever we have
|
|
423
|
+
return results;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Check if a domain is catch-all via BounceBan
|
|
427
|
+
*
|
|
428
|
+
* Verifies a random non-existent email to detect catch-all behavior.
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* ```typescript
|
|
432
|
+
* const isCatchAll = await checkCatchAllWithBounceBan('example.com', {
|
|
433
|
+
* apiKey: process.env.BOUNCEBAN_API_KEY,
|
|
434
|
+
* });
|
|
435
|
+
* ```
|
|
436
|
+
*/
|
|
437
|
+
async function checkCatchAllWithBounceBan(domain, config) {
|
|
438
|
+
// Generate a random email that almost certainly doesn't exist
|
|
439
|
+
const randomLocal = `catchall-test-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
440
|
+
const testEmail = `${randomLocal}@${domain}`;
|
|
441
|
+
const result = await verifyEmailWithBounceBan(testEmail, config);
|
|
442
|
+
if (!result) {
|
|
443
|
+
return null; // Could not determine
|
|
444
|
+
}
|
|
445
|
+
// If the random email is "deliverable" or "risky", the domain is catch-all
|
|
446
|
+
return result.is_accept_all || result.result === "deliverable";
|
|
447
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @see https://docs.usebouncer.com
|
|
15
15
|
*/
|
|
16
|
-
import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, BouncerConfig, BouncerVerifyResponse } from
|
|
16
|
+
import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, BouncerConfig, BouncerVerifyResponse } from "../types";
|
|
17
17
|
/**
|
|
18
18
|
* Create the Bouncer verification provider
|
|
19
19
|
*
|
|
@@ -19,14 +19,15 @@ exports.createBouncerProvider = createBouncerProvider;
|
|
|
19
19
|
exports.verifyEmailWithBouncer = verifyEmailWithBouncer;
|
|
20
20
|
exports.checkCatchAllDomain = checkCatchAllDomain;
|
|
21
21
|
exports.verifyEmailsBatch = verifyEmailsBatch;
|
|
22
|
-
const
|
|
22
|
+
const noop_provider_1 = require("../utils/noop-provider");
|
|
23
|
+
const DEFAULT_API_URL = "https://api.usebouncer.com/v1.1";
|
|
23
24
|
const DEFAULT_TIMEOUT_MS = 30000;
|
|
24
25
|
/**
|
|
25
26
|
* Map Bouncer status to confidence score
|
|
26
27
|
*/
|
|
27
28
|
function statusToConfidence(status, response) {
|
|
28
29
|
switch (status) {
|
|
29
|
-
case
|
|
30
|
+
case "deliverable":
|
|
30
31
|
// High confidence - email verified as deliverable
|
|
31
32
|
// Reduce slightly if catch-all or high toxicity
|
|
32
33
|
let score = 95;
|
|
@@ -37,13 +38,13 @@ function statusToConfidence(status, response) {
|
|
|
37
38
|
if (response.role)
|
|
38
39
|
score -= 5; // Role accounts slightly lower
|
|
39
40
|
return Math.max(50, score);
|
|
40
|
-
case
|
|
41
|
+
case "risky":
|
|
41
42
|
// Medium confidence - risky but might work
|
|
42
43
|
return response.acceptAll ? 40 : 50;
|
|
43
|
-
case
|
|
44
|
+
case "undeliverable":
|
|
44
45
|
// Email does not exist
|
|
45
46
|
return 0;
|
|
46
|
-
case
|
|
47
|
+
case "unknown":
|
|
47
48
|
// Could not verify
|
|
48
49
|
return 30;
|
|
49
50
|
default:
|
|
@@ -59,7 +60,7 @@ function extractEmailsToVerify(candidate) {
|
|
|
59
60
|
// Check if candidate has pre-generated email patterns in metadata
|
|
60
61
|
const metadata = candidate._emailCandidates;
|
|
61
62
|
if (Array.isArray(metadata)) {
|
|
62
|
-
emails.push(...metadata.filter((e) => typeof e ===
|
|
63
|
+
emails.push(...metadata.filter((e) => typeof e === "string"));
|
|
63
64
|
}
|
|
64
65
|
return emails;
|
|
65
66
|
}
|
|
@@ -72,10 +73,10 @@ async function verifyEmail(email, apiKey, apiUrl, timeoutMs) {
|
|
|
72
73
|
try {
|
|
73
74
|
const url = `${apiUrl}/email/verify?email=${encodeURIComponent(email)}`;
|
|
74
75
|
const response = await fetch(url, {
|
|
75
|
-
method:
|
|
76
|
+
method: "GET",
|
|
76
77
|
headers: {
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
"x-api-key": apiKey,
|
|
79
|
+
Accept: "application/json",
|
|
79
80
|
},
|
|
80
81
|
signal: controller.signal,
|
|
81
82
|
});
|
|
@@ -83,7 +84,7 @@ async function verifyEmail(email, apiKey, apiUrl, timeoutMs) {
|
|
|
83
84
|
// All HTTP errors return null - let caller decide how to handle
|
|
84
85
|
return null;
|
|
85
86
|
}
|
|
86
|
-
const data = await response.json();
|
|
87
|
+
const data = (await response.json());
|
|
87
88
|
return data;
|
|
88
89
|
}
|
|
89
90
|
catch {
|
|
@@ -106,12 +107,9 @@ async function verifyEmail(email, apiKey, apiUrl, timeoutMs) {
|
|
|
106
107
|
* 3. Only verified emails are returned
|
|
107
108
|
*/
|
|
108
109
|
function createBouncerProvider(config) {
|
|
109
|
-
const { apiKey, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS } = config;
|
|
110
|
+
const { apiKey, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS, } = config;
|
|
110
111
|
if (!apiKey) {
|
|
111
|
-
|
|
112
|
-
const noop = async () => null;
|
|
113
|
-
noop.__name = 'bouncer';
|
|
114
|
-
return noop;
|
|
112
|
+
return (0, noop_provider_1.createNoOpProvider)("bouncer");
|
|
115
113
|
}
|
|
116
114
|
async function verifyEmails(candidate) {
|
|
117
115
|
const emailsToVerify = extractEmailsToVerify(candidate);
|
|
@@ -128,10 +126,10 @@ function createBouncerProvider(config) {
|
|
|
128
126
|
if (result) {
|
|
129
127
|
const confidence = statusToConfidence(result.status, result);
|
|
130
128
|
// Only include emails that are potentially deliverable
|
|
131
|
-
if (result.status ===
|
|
129
|
+
if (result.status === "deliverable" || result.status === "risky") {
|
|
132
130
|
verifiedEmails.push({
|
|
133
131
|
email: result.email,
|
|
134
|
-
verified: result.status ===
|
|
132
|
+
verified: result.status === "deliverable",
|
|
135
133
|
confidence,
|
|
136
134
|
isCatchAll: result.acceptAll,
|
|
137
135
|
metadata: {
|
|
@@ -159,7 +157,7 @@ function createBouncerProvider(config) {
|
|
|
159
157
|
return { emails: verifiedEmails };
|
|
160
158
|
}
|
|
161
159
|
// Mark provider name for orchestrator
|
|
162
|
-
verifyEmails.__name =
|
|
160
|
+
verifyEmails.__name = "bouncer";
|
|
163
161
|
return verifyEmails;
|
|
164
162
|
}
|
|
165
163
|
/**
|
|
@@ -176,7 +174,7 @@ function createBouncerProvider(config) {
|
|
|
176
174
|
* ```
|
|
177
175
|
*/
|
|
178
176
|
async function verifyEmailWithBouncer(email, config) {
|
|
179
|
-
const { apiKey, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS } = config;
|
|
177
|
+
const { apiKey, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS, } = config;
|
|
180
178
|
return verifyEmail(email, apiKey, apiUrl, timeoutMs);
|
|
181
179
|
}
|
|
182
180
|
/**
|
|
@@ -192,7 +190,7 @@ async function verifyEmailWithBouncer(email, config) {
|
|
|
192
190
|
* ```
|
|
193
191
|
*/
|
|
194
192
|
async function checkCatchAllDomain(domain, config) {
|
|
195
|
-
const { apiKey, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS } = config;
|
|
193
|
+
const { apiKey, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS, } = config;
|
|
196
194
|
// Generate a random email that almost certainly doesn't exist
|
|
197
195
|
const randomLocal = `bounce-test-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
198
196
|
const testEmail = `${randomLocal}@${domain}`;
|
|
@@ -215,7 +213,7 @@ async function checkCatchAllDomain(domain, config) {
|
|
|
215
213
|
* ```
|
|
216
214
|
*/
|
|
217
215
|
async function verifyEmailsBatch(emails, config) {
|
|
218
|
-
const { apiKey, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS } = config;
|
|
216
|
+
const { apiKey, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS, } = config;
|
|
219
217
|
const results = new Map();
|
|
220
218
|
// Verify in parallel with concurrency limit
|
|
221
219
|
const CONCURRENCY = 5;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Generates email pattern candidates based on name + domain, then verifies via MX check.
|
|
5
5
|
* This is a FREE provider that doesn't require any API keys.
|
|
6
6
|
*/
|
|
7
|
-
import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, ConstructConfig } from
|
|
7
|
+
import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, ConstructConfig } from "../types";
|
|
8
8
|
/**
|
|
9
9
|
* Create the construct provider function
|
|
10
10
|
*/
|