linkedin-secret-sauce 0.11.0 → 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 +25 -24
- package/dist/enrichment/matching.d.ts +8 -3
- package/dist/enrichment/matching.js +7 -5
- package/dist/enrichment/orchestrator.js +44 -14
- package/dist/enrichment/providers/construct.js +72 -14
- package/dist/enrichment/providers/hunter.js +6 -60
- package/dist/enrichment/providers/index.d.ts +0 -1
- package/dist/enrichment/providers/index.js +1 -3
- package/dist/enrichment/providers/ldd.js +5 -47
- package/dist/enrichment/providers/smartprospect.js +9 -14
- package/dist/enrichment/types.d.ts +23 -24
- package/dist/enrichment/types.js +22 -21
- 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 +30 -22
|
@@ -64,10 +64,10 @@ export declare function getSmartLeadTokenCacheStats(): {
|
|
|
64
64
|
* // In your test setup
|
|
65
65
|
* import { enableFileCache } from 'linkedin-secret-sauce/enrichment';
|
|
66
66
|
*
|
|
67
|
-
* enableFileCache(); // Now tokens persist between test runs
|
|
67
|
+
* await enableFileCache(); // Now tokens persist between test runs
|
|
68
68
|
* ```
|
|
69
69
|
*/
|
|
70
|
-
export declare function enableFileCache(): void
|
|
70
|
+
export declare function enableFileCache(): Promise<void>;
|
|
71
71
|
/**
|
|
72
72
|
* Disable file-based token caching
|
|
73
73
|
*/
|
|
@@ -79,4 +79,4 @@ export declare function isFileCacheEnabled(): boolean;
|
|
|
79
79
|
/**
|
|
80
80
|
* Clear the file cache
|
|
81
81
|
*/
|
|
82
|
-
export declare function clearFileCache(): void
|
|
82
|
+
export declare function clearFileCache(): Promise<void>;
|
|
@@ -51,9 +51,10 @@ exports.enableFileCache = enableFileCache;
|
|
|
51
51
|
exports.disableFileCache = disableFileCache;
|
|
52
52
|
exports.isFileCacheEnabled = isFileCacheEnabled;
|
|
53
53
|
exports.clearFileCache = clearFileCache;
|
|
54
|
-
const fs = __importStar(require("fs"));
|
|
54
|
+
const fs = __importStar(require("fs/promises"));
|
|
55
55
|
const path = __importStar(require("path"));
|
|
56
56
|
const os = __importStar(require("os"));
|
|
57
|
+
const http_retry_1 = require("../utils/http-retry");
|
|
57
58
|
const DEFAULT_LOGIN_URL = 'https://server.smartlead.ai/api/auth/login';
|
|
58
59
|
const DEFAULT_REFRESH_BUFFER_MS = 5 * 60 * 1000; // 5 minutes
|
|
59
60
|
const TOKEN_LIFETIME_MS = 24 * 60 * 60 * 1000; // Assume 24h JWT lifetime (conservative)
|
|
@@ -213,34 +214,32 @@ function getFileCachePath() {
|
|
|
213
214
|
return path.join(homeDir, FILE_CACHE_NAME);
|
|
214
215
|
}
|
|
215
216
|
/**
|
|
216
|
-
* Load tokens from file cache
|
|
217
|
+
* Load tokens from file cache (async)
|
|
217
218
|
*/
|
|
218
|
-
function loadFileCache() {
|
|
219
|
+
async function loadFileCache() {
|
|
219
220
|
const cache = new Map();
|
|
220
221
|
if (!fileCacheEnabled)
|
|
221
222
|
return cache;
|
|
222
223
|
try {
|
|
223
224
|
const filePath = getFileCachePath();
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
cache.set(key, value);
|
|
231
|
-
}
|
|
225
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
226
|
+
const data = (0, http_retry_1.safeJsonParse)(content, {});
|
|
227
|
+
for (const [key, value] of Object.entries(data)) {
|
|
228
|
+
// Validate structure
|
|
229
|
+
if (value && value.token && value.expiresAt && value.obtainedAt) {
|
|
230
|
+
cache.set(key, value);
|
|
232
231
|
}
|
|
233
232
|
}
|
|
234
233
|
}
|
|
235
234
|
catch {
|
|
236
|
-
// Ignore file read errors
|
|
235
|
+
// Ignore file read errors (file may not exist)
|
|
237
236
|
}
|
|
238
237
|
return cache;
|
|
239
238
|
}
|
|
240
239
|
/**
|
|
241
|
-
* Save tokens to file cache
|
|
240
|
+
* Save tokens to file cache (async, fire-and-forget)
|
|
242
241
|
*/
|
|
243
|
-
function saveFileCache() {
|
|
242
|
+
async function saveFileCache() {
|
|
244
243
|
if (!fileCacheEnabled)
|
|
245
244
|
return;
|
|
246
245
|
try {
|
|
@@ -249,7 +248,7 @@ function saveFileCache() {
|
|
|
249
248
|
for (const [key, value] of tokenCache.entries()) {
|
|
250
249
|
data[key] = value;
|
|
251
250
|
}
|
|
252
|
-
fs.
|
|
251
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
253
252
|
}
|
|
254
253
|
catch {
|
|
255
254
|
// Ignore file write errors
|
|
@@ -266,13 +265,13 @@ function saveFileCache() {
|
|
|
266
265
|
* // In your test setup
|
|
267
266
|
* import { enableFileCache } from 'linkedin-secret-sauce/enrichment';
|
|
268
267
|
*
|
|
269
|
-
* enableFileCache(); // Now tokens persist between test runs
|
|
268
|
+
* await enableFileCache(); // Now tokens persist between test runs
|
|
270
269
|
* ```
|
|
271
270
|
*/
|
|
272
|
-
function enableFileCache() {
|
|
271
|
+
async function enableFileCache() {
|
|
273
272
|
fileCacheEnabled = true;
|
|
274
273
|
// Load existing tokens from file into memory cache
|
|
275
|
-
const fileTokens = loadFileCache();
|
|
274
|
+
const fileTokens = await loadFileCache();
|
|
276
275
|
for (const [key, value] of fileTokens.entries()) {
|
|
277
276
|
// Only load if not already in memory and still valid
|
|
278
277
|
if (!tokenCache.has(key) && isTokenValid(value, DEFAULT_REFRESH_BUFFER_MS)) {
|
|
@@ -295,25 +294,26 @@ function isFileCacheEnabled() {
|
|
|
295
294
|
/**
|
|
296
295
|
* Clear the file cache
|
|
297
296
|
*/
|
|
298
|
-
function clearFileCache() {
|
|
297
|
+
async function clearFileCache() {
|
|
299
298
|
try {
|
|
300
299
|
const filePath = getFileCachePath();
|
|
301
|
-
|
|
302
|
-
fs.unlinkSync(filePath);
|
|
303
|
-
}
|
|
300
|
+
await fs.unlink(filePath);
|
|
304
301
|
}
|
|
305
302
|
catch {
|
|
306
|
-
// Ignore errors
|
|
303
|
+
// Ignore errors (file may not exist)
|
|
307
304
|
}
|
|
308
305
|
}
|
|
309
306
|
// We need to modify the token caching behavior to persist to file
|
|
310
307
|
// This is done by wrapping the cache set operation
|
|
311
|
-
// Override tokenCache.set to also persist to file
|
|
308
|
+
// Override tokenCache.set to also persist to file (fire-and-forget async)
|
|
312
309
|
const originalSet = tokenCache.set.bind(tokenCache);
|
|
313
310
|
tokenCache.set = function (key, value) {
|
|
314
311
|
const result = originalSet(key, value);
|
|
315
312
|
if (fileCacheEnabled) {
|
|
316
|
-
|
|
313
|
+
// Fire-and-forget: don't await, just let it run in background
|
|
314
|
+
saveFileCache().catch(() => {
|
|
315
|
+
// Ignore save errors silently
|
|
316
|
+
});
|
|
317
317
|
}
|
|
318
318
|
return result;
|
|
319
319
|
};
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* const enricher = createEnrichmentClient({
|
|
12
12
|
* providers: {
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* ldd: { apiUrl: process.env.LDD_API_URL, apiToken: process.env.LDD_API_TOKEN },
|
|
14
|
+
* smartprospect: { email: process.env.SMARTLEAD_EMAIL, password: process.env.SMARTLEAD_PASSWORD },
|
|
15
|
+
* bouncer: { apiKey: process.env.BOUNCER_API_KEY },
|
|
15
16
|
* },
|
|
16
17
|
* options: {
|
|
17
18
|
* maxCostPerEmail: 0.05,
|
|
@@ -35,11 +36,12 @@ import type { EnrichmentClientConfig, EnrichmentClient } from "./types";
|
|
|
35
36
|
*/
|
|
36
37
|
export declare function createEnrichmentClient(config: EnrichmentClientConfig): EnrichmentClient;
|
|
37
38
|
export * from "./types";
|
|
39
|
+
export { PROVIDER_COSTS, DEFAULT_PROVIDER_ORDER } from "./types";
|
|
38
40
|
export { isPersonalEmail, isBusinessEmail, isPersonalDomain, PERSONAL_DOMAINS, } from "./utils/personal-domains";
|
|
39
41
|
export { isDisposableEmail, isDisposableDomain, DISPOSABLE_DOMAINS, } from "./utils/disposable-domains";
|
|
40
42
|
export { isValidEmailSyntax, isRoleAccount, asciiFold, cleanNamePart, hostnameFromUrl, extractLinkedInUsername, } from "./utils/validation";
|
|
41
|
-
export { verifyEmailMx } from "./verification/mx";
|
|
42
|
-
export { createConstructProvider, createLddProvider, createSmartProspectProvider, createHunterProvider,
|
|
43
|
+
export { verifyEmailMx, checkDomainCatchAll, verifyEmailsExist } from "./verification/mx";
|
|
44
|
+
export { createConstructProvider, createLddProvider, createSmartProspectProvider, createHunterProvider, createDropcontactProvider, createBouncerProvider, createSnovioProvider, verifyEmailWithBouncer, checkCatchAllDomain, verifyEmailsBatch, findEmailsWithSnovio, verifyEmailWithSnovio, clearSnovioTokenCache, } from "./providers";
|
|
43
45
|
export { extractNumericLinkedInId } from "./providers/ldd";
|
|
44
46
|
export { createSmartProspectClient, type SmartProspectClient, type SmartProspectLocationOptions, } from "./providers/smartprospect";
|
|
45
47
|
export { enrichBusinessEmail, enrichBatch, enrichAllEmails, enrichAllBatch } from "./orchestrator";
|
package/dist/enrichment/index.js
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
*
|
|
12
12
|
* const enricher = createEnrichmentClient({
|
|
13
13
|
* providers: {
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* ldd: { apiUrl: process.env.LDD_API_URL, apiToken: process.env.LDD_API_TOKEN },
|
|
15
|
+
* smartprospect: { email: process.env.SMARTLEAD_EMAIL, password: process.env.SMARTLEAD_PASSWORD },
|
|
16
|
+
* bouncer: { apiKey: process.env.BOUNCER_API_KEY },
|
|
16
17
|
* },
|
|
17
18
|
* options: {
|
|
18
19
|
* maxCostPerEmail: 0.05,
|
|
@@ -42,40 +43,39 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
42
43
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
43
44
|
};
|
|
44
45
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.
|
|
46
|
-
exports.salesLeadToContact = exports.getEmailsForLinkedInContactsBatch = exports.getEmailsForLinkedInContact = exports.createLinkedInEnricher = exports.enrichLinkedInContactsBatch = void 0;
|
|
46
|
+
exports.matchContacts = exports.findBestMatch = exports.classifyMatchQuality = exports.calculateMatchConfidence = exports.clearFileCache = exports.isFileCacheEnabled = exports.disableFileCache = exports.enableFileCache = exports.getSmartLeadTokenCacheStats = exports.clearAllSmartLeadTokens = exports.clearSmartLeadToken = exports.getSmartLeadUser = exports.getSmartLeadToken = exports.enrichAllBatch = exports.enrichAllEmails = exports.enrichBatch = exports.enrichBusinessEmail = exports.createSmartProspectClient = exports.extractNumericLinkedInId = exports.clearSnovioTokenCache = exports.verifyEmailWithSnovio = exports.findEmailsWithSnovio = exports.verifyEmailsBatch = exports.checkCatchAllDomain = exports.verifyEmailWithBouncer = exports.createSnovioProvider = exports.createBouncerProvider = exports.createDropcontactProvider = exports.createHunterProvider = exports.createSmartProspectProvider = exports.createLddProvider = exports.createConstructProvider = exports.verifyEmailsExist = exports.checkDomainCatchAll = exports.verifyEmailMx = exports.extractLinkedInUsername = exports.hostnameFromUrl = exports.cleanNamePart = exports.asciiFold = exports.isRoleAccount = exports.isValidEmailSyntax = exports.DISPOSABLE_DOMAINS = exports.isDisposableDomain = exports.isDisposableEmail = exports.PERSONAL_DOMAINS = exports.isPersonalDomain = exports.isBusinessEmail = exports.isPersonalEmail = exports.DEFAULT_PROVIDER_ORDER = exports.PROVIDER_COSTS = void 0;
|
|
47
|
+
exports.salesLeadToContact = exports.getEmailsForLinkedInContactsBatch = exports.getEmailsForLinkedInContact = exports.createLinkedInEnricher = exports.enrichLinkedInContactsBatch = exports.enrichLinkedInContact = exports.parseLinkedInSearchResponse = exports.buildSmartProspectFiltersFromLinkedIn = void 0;
|
|
47
48
|
exports.createEnrichmentClient = createEnrichmentClient;
|
|
48
49
|
const orchestrator_1 = require("./orchestrator");
|
|
49
50
|
const construct_1 = require("./providers/construct");
|
|
50
51
|
const ldd_1 = require("./providers/ldd");
|
|
51
52
|
const smartprospect_1 = require("./providers/smartprospect");
|
|
52
53
|
const hunter_1 = require("./providers/hunter");
|
|
53
|
-
const apollo_1 = require("./providers/apollo");
|
|
54
54
|
const dropcontact_1 = require("./providers/dropcontact");
|
|
55
55
|
const bouncer_1 = require("./providers/bouncer");
|
|
56
56
|
const snovio_1 = require("./providers/snovio");
|
|
57
57
|
/**
|
|
58
|
-
* Default provider order
|
|
58
|
+
* Default provider order - 2-Phase Strategy
|
|
59
59
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
60
|
+
* PHASE 1 - Free lookups (real data):
|
|
61
|
+
* - ldd: LinkedIn Data Dump - real verified emails (FREE)
|
|
62
|
+
* - smartprospect: SmartLead API - real verified emails (FREE with subscription)
|
|
63
|
+
* - construct: Pattern guessing + MX check (FREE)
|
|
64
|
+
*
|
|
65
|
+
* PHASE 2 - Paid verification/finding (only if needed):
|
|
66
|
+
* - bouncer: SMTP verify constructed emails ($0.006/email)
|
|
67
|
+
* - snovio: Email finder for catch-all domains ($0.02/email)
|
|
68
|
+
* - hunter: Hunter.io fallback ($0.005/email)
|
|
69
|
+
*
|
|
70
|
+
* Note: dropcontact available but not in default order
|
|
69
71
|
*/
|
|
70
72
|
const DEFAULT_ORDER = [
|
|
71
|
-
"construct",
|
|
72
|
-
"bouncer",
|
|
73
73
|
"ldd",
|
|
74
74
|
"smartprospect",
|
|
75
|
+
"construct",
|
|
76
|
+
"bouncer",
|
|
75
77
|
"snovio",
|
|
76
78
|
"hunter",
|
|
77
|
-
"apollo",
|
|
78
|
-
"dropcontact",
|
|
79
79
|
];
|
|
80
80
|
/**
|
|
81
81
|
* Create an enrichment client with the given configuration
|
|
@@ -99,9 +99,6 @@ function createEnrichmentClient(config) {
|
|
|
99
99
|
if (providerConfigs.hunter) {
|
|
100
100
|
providerFuncs.set("hunter", (0, hunter_1.createHunterProvider)(providerConfigs.hunter));
|
|
101
101
|
}
|
|
102
|
-
if (providerConfigs.apollo) {
|
|
103
|
-
providerFuncs.set("apollo", (0, apollo_1.createApolloProvider)(providerConfigs.apollo));
|
|
104
|
-
}
|
|
105
102
|
if (providerConfigs.dropcontact) {
|
|
106
103
|
providerFuncs.set("dropcontact", (0, dropcontact_1.createDropcontactProvider)(providerConfigs.dropcontact));
|
|
107
104
|
}
|
|
@@ -241,8 +238,11 @@ function buildCacheKey(candidate) {
|
|
|
241
238
|
}
|
|
242
239
|
return parts.join("|");
|
|
243
240
|
}
|
|
244
|
-
// Re-export types
|
|
241
|
+
// Re-export types and constants
|
|
245
242
|
__exportStar(require("./types"), exports);
|
|
243
|
+
var types_1 = require("./types");
|
|
244
|
+
Object.defineProperty(exports, "PROVIDER_COSTS", { enumerable: true, get: function () { return types_1.PROVIDER_COSTS; } });
|
|
245
|
+
Object.defineProperty(exports, "DEFAULT_PROVIDER_ORDER", { enumerable: true, get: function () { return types_1.DEFAULT_PROVIDER_ORDER; } });
|
|
246
246
|
// Re-export utilities
|
|
247
247
|
var personal_domains_1 = require("./utils/personal-domains");
|
|
248
248
|
Object.defineProperty(exports, "isPersonalEmail", { enumerable: true, get: function () { return personal_domains_1.isPersonalEmail; } });
|
|
@@ -263,13 +263,14 @@ Object.defineProperty(exports, "extractLinkedInUsername", { enumerable: true, ge
|
|
|
263
263
|
// Re-export verification
|
|
264
264
|
var mx_1 = require("./verification/mx");
|
|
265
265
|
Object.defineProperty(exports, "verifyEmailMx", { enumerable: true, get: function () { return mx_1.verifyEmailMx; } });
|
|
266
|
+
Object.defineProperty(exports, "checkDomainCatchAll", { enumerable: true, get: function () { return mx_1.checkDomainCatchAll; } });
|
|
267
|
+
Object.defineProperty(exports, "verifyEmailsExist", { enumerable: true, get: function () { return mx_1.verifyEmailsExist; } });
|
|
266
268
|
// Re-export providers (for advanced usage)
|
|
267
269
|
var providers_1 = require("./providers");
|
|
268
270
|
Object.defineProperty(exports, "createConstructProvider", { enumerable: true, get: function () { return providers_1.createConstructProvider; } });
|
|
269
271
|
Object.defineProperty(exports, "createLddProvider", { enumerable: true, get: function () { return providers_1.createLddProvider; } });
|
|
270
272
|
Object.defineProperty(exports, "createSmartProspectProvider", { enumerable: true, get: function () { return providers_1.createSmartProspectProvider; } });
|
|
271
273
|
Object.defineProperty(exports, "createHunterProvider", { enumerable: true, get: function () { return providers_1.createHunterProvider; } });
|
|
272
|
-
Object.defineProperty(exports, "createApolloProvider", { enumerable: true, get: function () { return providers_1.createApolloProvider; } });
|
|
273
274
|
Object.defineProperty(exports, "createDropcontactProvider", { enumerable: true, get: function () { return providers_1.createDropcontactProvider; } });
|
|
274
275
|
Object.defineProperty(exports, "createBouncerProvider", { enumerable: true, get: function () { return providers_1.createBouncerProvider; } });
|
|
275
276
|
Object.defineProperty(exports, "createSnovioProvider", { enumerable: true, get: function () { return providers_1.createSnovioProvider; } });
|
|
@@ -258,7 +258,7 @@ export declare function createLinkedInEnricher(smartProspectConfig: SmartProspec
|
|
|
258
258
|
/**
|
|
259
259
|
* Email source - where the email was found
|
|
260
260
|
*/
|
|
261
|
-
export type EmailSource = 'ldd' | 'smartprospect' | 'linkedin' | 'pattern' | 'hunter' | '
|
|
261
|
+
export type EmailSource = 'ldd' | 'smartprospect' | 'linkedin' | 'pattern' | 'hunter' | 'bouncer' | 'snovio';
|
|
262
262
|
/**
|
|
263
263
|
* Email result from unified lookup
|
|
264
264
|
*/
|
|
@@ -319,10 +319,15 @@ export interface GetEmailsConfig {
|
|
|
319
319
|
hunter?: {
|
|
320
320
|
apiKey: string;
|
|
321
321
|
};
|
|
322
|
-
/**
|
|
323
|
-
|
|
322
|
+
/** Bouncer configuration (PAID - SMTP verification) */
|
|
323
|
+
bouncer?: {
|
|
324
324
|
apiKey: string;
|
|
325
325
|
};
|
|
326
|
+
/** Snov.io configuration (PAID - email finder) */
|
|
327
|
+
snovio?: {
|
|
328
|
+
userId: string;
|
|
329
|
+
apiSecret: string;
|
|
330
|
+
};
|
|
326
331
|
}
|
|
327
332
|
/**
|
|
328
333
|
* Options for unified email lookup
|
|
@@ -863,15 +863,16 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
863
863
|
? Math.max(...result.emails.map(e => e.confidence))
|
|
864
864
|
: 0;
|
|
865
865
|
// ==========================================================================
|
|
866
|
-
// Phase 3: PAID providers as last resort (Hunter/
|
|
866
|
+
// Phase 3: PAID providers as last resort (Hunter/Bouncer/Snovio)
|
|
867
867
|
// ==========================================================================
|
|
868
868
|
if (!skipPaidProviders && finalBestConfidence < paidProviderThreshold) {
|
|
869
869
|
// Only use paid providers if we have low confidence or no results
|
|
870
|
-
// TODO: Implement Hunter
|
|
870
|
+
// TODO: Implement Hunter, Bouncer, Snovio providers when needed
|
|
871
871
|
// For now, just mark that we would have queried them
|
|
872
|
-
if (config.hunter?.apiKey || config.
|
|
872
|
+
if (config.hunter?.apiKey || config.bouncer?.apiKey || config.snovio?.userId) {
|
|
873
873
|
// result.providersQueried.push('hunter');
|
|
874
|
-
// result.providersQueried.push('
|
|
874
|
+
// result.providersQueried.push('bouncer');
|
|
875
|
+
// result.providersQueried.push('snovio');
|
|
875
876
|
// await queryPaidProviders(contact, config, addEmail, result);
|
|
876
877
|
}
|
|
877
878
|
}
|
|
@@ -882,7 +883,8 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
882
883
|
linkedin: 2, // LinkedIn company lookup (for domain discovery, doesn't provide emails)
|
|
883
884
|
pattern: 3,
|
|
884
885
|
hunter: 4,
|
|
885
|
-
|
|
886
|
+
bouncer: 5,
|
|
887
|
+
snovio: 6,
|
|
886
888
|
};
|
|
887
889
|
result.emails.sort((a, b) => {
|
|
888
890
|
if (b.confidence !== a.confidence) {
|
|
@@ -17,18 +17,19 @@ const validation_1 = require("./utils/validation");
|
|
|
17
17
|
* Default provider costs in USD per lookup
|
|
18
18
|
*
|
|
19
19
|
* Costs based on 2025 pricing:
|
|
20
|
-
* -
|
|
21
|
-
* -
|
|
22
|
-
* -
|
|
23
|
-
* -
|
|
24
|
-
* -
|
|
20
|
+
* - ldd: FREE (subscription-based)
|
|
21
|
+
* - smartprospect: FREE (included in SmartLead subscription)
|
|
22
|
+
* - construct: FREE (pattern guessing + MX check)
|
|
23
|
+
* - bouncer: $0.006/email (SMTP verification, 99%+ accuracy)
|
|
24
|
+
* - snovio: $0.02/email (email finding + verification)
|
|
25
|
+
* - hunter: $0.005/email
|
|
26
|
+
* - dropcontact: $0.01/email (not in default order)
|
|
25
27
|
*/
|
|
26
28
|
const _PROVIDER_COSTS = {
|
|
27
29
|
construct: 0,
|
|
28
30
|
ldd: 0,
|
|
29
|
-
smartprospect: 0
|
|
31
|
+
smartprospect: 0,
|
|
30
32
|
hunter: 0.005,
|
|
31
|
-
apollo: 0,
|
|
32
33
|
dropcontact: 0.01,
|
|
33
34
|
bouncer: 0.006,
|
|
34
35
|
snovio: 0.02,
|
|
@@ -235,11 +236,25 @@ async function enrichBatch(candidates, options) {
|
|
|
235
236
|
const delayMs = options.delayMs ?? 200;
|
|
236
237
|
for (let i = 0; i < candidates.length; i += batchSize) {
|
|
237
238
|
const chunk = candidates.slice(i, i + batchSize);
|
|
238
|
-
// Process batch in parallel
|
|
239
|
-
const batchResults = await Promise.
|
|
240
|
-
// Combine results with candidates
|
|
239
|
+
// Process batch in parallel (use allSettled for resilience)
|
|
240
|
+
const batchResults = await Promise.allSettled(chunk.map((candidate) => enrichBusinessEmail(candidate, options)));
|
|
241
|
+
// Combine results with candidates (handle both fulfilled and rejected)
|
|
241
242
|
batchResults.forEach((result, idx) => {
|
|
242
|
-
|
|
243
|
+
const candidate = chunk[idx];
|
|
244
|
+
if (result.status === 'fulfilled') {
|
|
245
|
+
results.push({ candidate, ...result.value });
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
// On error, return empty result for this candidate
|
|
249
|
+
results.push({
|
|
250
|
+
candidate,
|
|
251
|
+
business_email: null,
|
|
252
|
+
business_email_source: null,
|
|
253
|
+
business_email_verified: false,
|
|
254
|
+
last_checked_at: new Date().toISOString(),
|
|
255
|
+
status: `error: ${result.reason instanceof Error ? result.reason.message : 'unknown'}`,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
243
258
|
});
|
|
244
259
|
// Delay between batches to avoid rate limiting
|
|
245
260
|
if (i + batchSize < candidates.length && delayMs > 0) {
|
|
@@ -432,9 +447,24 @@ async function enrichAllBatch(candidates, options) {
|
|
|
432
447
|
const delayMs = options.delayMs ?? 200;
|
|
433
448
|
for (let i = 0; i < candidates.length; i += batchSize) {
|
|
434
449
|
const chunk = candidates.slice(i, i + batchSize);
|
|
435
|
-
// Process batch in parallel
|
|
436
|
-
const batchResults = await Promise.
|
|
437
|
-
results
|
|
450
|
+
// Process batch in parallel (use allSettled for resilience)
|
|
451
|
+
const batchResults = await Promise.allSettled(chunk.map((candidate) => enrichAllEmails(candidate, options)));
|
|
452
|
+
// Handle both fulfilled and rejected results
|
|
453
|
+
batchResults.forEach((result, idx) => {
|
|
454
|
+
if (result.status === 'fulfilled') {
|
|
455
|
+
results.push(result.value);
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
// On error, return empty result for this candidate
|
|
459
|
+
results.push({
|
|
460
|
+
emails: [],
|
|
461
|
+
candidate: chunk[idx],
|
|
462
|
+
totalCost: 0,
|
|
463
|
+
providersQueried: [],
|
|
464
|
+
completedAt: new Date().toISOString(),
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
});
|
|
438
468
|
// Delay between batches to avoid rate limiting
|
|
439
469
|
if (i + batchSize < candidates.length && delayMs > 0) {
|
|
440
470
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
@@ -87,6 +87,7 @@ function extractDomain(candidate) {
|
|
|
87
87
|
function createConstructProvider(config) {
|
|
88
88
|
const maxAttempts = config?.maxAttempts ?? 8;
|
|
89
89
|
const timeoutMs = config?.timeoutMs ?? 5000;
|
|
90
|
+
const smtpVerifyDelayMs = config?.smtpVerifyDelayMs ?? 2000; // Delay between SMTP checks
|
|
90
91
|
async function fetchEmail(candidate) {
|
|
91
92
|
const { first, last } = extractNames(candidate);
|
|
92
93
|
const domain = extractDomain(candidate);
|
|
@@ -100,22 +101,79 @@ function createConstructProvider(config) {
|
|
|
100
101
|
}
|
|
101
102
|
const candidates = buildCandidates({ first, last, domain });
|
|
102
103
|
const max = Math.min(candidates.length, maxAttempts);
|
|
104
|
+
// First, check if domain is catch-all
|
|
105
|
+
const catchAllResult = await (0, mx_1.checkDomainCatchAll)(domain, { timeoutMs: 10000 });
|
|
106
|
+
const isCatchAll = catchAllResult.isCatchAll;
|
|
103
107
|
// Collect ALL valid email patterns (not just first match)
|
|
104
108
|
const validEmails = [];
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
// Track all attempted patterns for debugging
|
|
110
|
+
const attemptedPatterns = [];
|
|
111
|
+
// If NOT catch-all, we can verify each email via SMTP
|
|
112
|
+
if (isCatchAll === false) {
|
|
113
|
+
// Verify emails one by one, stop when we find a valid one
|
|
114
|
+
const emailsToVerify = candidates.slice(0, max);
|
|
115
|
+
for (let i = 0; i < emailsToVerify.length; i++) {
|
|
116
|
+
const email = emailsToVerify[i];
|
|
117
|
+
// If we already found a valid email, skip the rest
|
|
118
|
+
if (validEmails.length > 0) {
|
|
119
|
+
attemptedPatterns.push({ email, status: 'skipped' });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// Add delay between checks (except first one)
|
|
123
|
+
if (i > 0) {
|
|
124
|
+
await new Promise((resolve) => setTimeout(resolve, smtpVerifyDelayMs));
|
|
125
|
+
}
|
|
126
|
+
// Verify single email
|
|
127
|
+
const results = await (0, mx_1.verifyEmailsExist)([email], { delayMs: 0, timeoutMs });
|
|
128
|
+
const result = results[0];
|
|
129
|
+
if (result.exists === true) {
|
|
130
|
+
// Email confirmed to exist!
|
|
131
|
+
attemptedPatterns.push({ email, status: 'exists' });
|
|
132
|
+
validEmails.push({
|
|
133
|
+
email: result.email,
|
|
134
|
+
verified: true,
|
|
135
|
+
confidence: 95, // High confidence - SMTP verified
|
|
136
|
+
isCatchAll: false,
|
|
137
|
+
metadata: {
|
|
138
|
+
pattern: result.email.split('@')[0],
|
|
139
|
+
mxRecords: catchAllResult.mxRecords,
|
|
140
|
+
smtpVerified: true,
|
|
141
|
+
attemptedPatterns, // Include what was tried
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
// Found one! Stop checking more
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
else if (result.exists === false) {
|
|
148
|
+
attemptedPatterns.push({ email, status: 'not_found' });
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
attemptedPatterns.push({ email, status: 'unknown' });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// If no valid email found, include attempted patterns in metadata
|
|
155
|
+
if (validEmails.length === 0 && attemptedPatterns.length > 0) {
|
|
156
|
+
// Return null but could add metadata about attempts
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Catch-all or unknown - fall back to MX verification only
|
|
161
|
+
for (let i = 0; i < max; i++) {
|
|
162
|
+
const email = candidates[i];
|
|
163
|
+
const verification = await (0, mx_1.verifyEmailMx)(email, { timeoutMs });
|
|
164
|
+
if (verification.valid === true && verification.confidence >= 50) {
|
|
165
|
+
validEmails.push({
|
|
166
|
+
email,
|
|
167
|
+
verified: true,
|
|
168
|
+
confidence: verification.confidence,
|
|
169
|
+
isCatchAll: isCatchAll ?? undefined,
|
|
170
|
+
metadata: {
|
|
171
|
+
pattern: email.split('@')[0],
|
|
172
|
+
mxRecords: verification.mxRecords,
|
|
173
|
+
smtpVerified: false,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
119
177
|
}
|
|
120
178
|
}
|
|
121
179
|
if (validEmails.length === 0) {
|
|
@@ -7,62 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.createHunterProvider = createHunterProvider;
|
|
10
|
+
const http_retry_1 = require("../utils/http-retry");
|
|
10
11
|
const API_BASE = "https://api.hunter.io/v2";
|
|
11
|
-
/**
|
|
12
|
-
* Delay helper for retry logic
|
|
13
|
-
*/
|
|
14
|
-
async function delay(ms) {
|
|
15
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Check if value is truthy
|
|
19
|
-
*/
|
|
20
|
-
function truthy(v) {
|
|
21
|
-
return v !== undefined && v !== null && String(v).length > 0;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Map Hunter verification status to boolean
|
|
25
|
-
*/
|
|
26
|
-
function mapVerified(status) {
|
|
27
|
-
if (!status)
|
|
28
|
-
return undefined;
|
|
29
|
-
const s = String(status).toLowerCase();
|
|
30
|
-
if (s === "valid" || s === "deliverable")
|
|
31
|
-
return true;
|
|
32
|
-
if (s === "invalid" || s === "undeliverable")
|
|
33
|
-
return false;
|
|
34
|
-
return undefined; // catch-all/unknown/webmail -> leave undefined
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* HTTP request with retry on 429 rate limit
|
|
38
|
-
*/
|
|
39
|
-
async function requestWithRetry(url, retries = 1, backoffMs = 200) {
|
|
40
|
-
let lastErr;
|
|
41
|
-
for (let i = 0; i <= retries; i++) {
|
|
42
|
-
try {
|
|
43
|
-
const res = await fetch(url);
|
|
44
|
-
// Retry on rate limit
|
|
45
|
-
if (res?.status === 429 && i < retries) {
|
|
46
|
-
await delay(backoffMs * Math.pow(2, i));
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (!res || res.status >= 400) {
|
|
50
|
-
lastErr = new Error(`hunter_http_${res?.status ?? "error"}`);
|
|
51
|
-
break;
|
|
52
|
-
}
|
|
53
|
-
const json = (await res.json());
|
|
54
|
-
return json;
|
|
55
|
-
}
|
|
56
|
-
catch (err) {
|
|
57
|
-
lastErr = err;
|
|
58
|
-
if (i < retries) {
|
|
59
|
-
await delay(backoffMs * Math.pow(2, i));
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
throw lastErr ?? new Error("hunter_http_error");
|
|
65
|
-
}
|
|
66
12
|
/**
|
|
67
13
|
* Extract name and domain from candidate
|
|
68
14
|
*/
|
|
@@ -99,7 +45,7 @@ function createHunterProvider(config) {
|
|
|
99
45
|
let url = null;
|
|
100
46
|
let isEmailFinder = false;
|
|
101
47
|
// Use email-finder if we have name components
|
|
102
|
-
if (truthy(first) && truthy(last) && truthy(domain)) {
|
|
48
|
+
if ((0, http_retry_1.truthy)(first) && (0, http_retry_1.truthy)(last) && (0, http_retry_1.truthy)(domain)) {
|
|
103
49
|
const qs = new URLSearchParams({
|
|
104
50
|
api_key: String(apiKey),
|
|
105
51
|
domain: String(domain),
|
|
@@ -110,7 +56,7 @@ function createHunterProvider(config) {
|
|
|
110
56
|
isEmailFinder = true;
|
|
111
57
|
}
|
|
112
58
|
// Fall back to domain-search if only domain available (can return multiple)
|
|
113
|
-
else if (truthy(domain)) {
|
|
59
|
+
else if ((0, http_retry_1.truthy)(domain)) {
|
|
114
60
|
const qs = new URLSearchParams({
|
|
115
61
|
api_key: String(apiKey),
|
|
116
62
|
domain: String(domain),
|
|
@@ -122,7 +68,7 @@ function createHunterProvider(config) {
|
|
|
122
68
|
return null; // Can't search without domain
|
|
123
69
|
}
|
|
124
70
|
try {
|
|
125
|
-
const json = await
|
|
71
|
+
const json = await (0, http_retry_1.getWithRetry)(url, undefined, { retries: 1, backoffMs: 100 });
|
|
126
72
|
// Parse email-finder response shape (single result)
|
|
127
73
|
if (isEmailFinder) {
|
|
128
74
|
const ef = (json && (json.data || json.result));
|
|
@@ -131,7 +77,7 @@ function createHunterProvider(config) {
|
|
|
131
77
|
const score = typeof ef.score === "number"
|
|
132
78
|
? ef.score
|
|
133
79
|
: Number(ef.score ?? 0) || undefined;
|
|
134
|
-
const verified =
|
|
80
|
+
const verified = (0, http_retry_1.mapVerifiedStatus)(ef?.verification?.status ?? ef?.status);
|
|
135
81
|
if (!email)
|
|
136
82
|
return null;
|
|
137
83
|
return { email, verified, score };
|
|
@@ -158,7 +104,7 @@ function createHunterProvider(config) {
|
|
|
158
104
|
const score = typeof hit?.confidence === "number"
|
|
159
105
|
? hit.confidence
|
|
160
106
|
: Number(hit?.confidence ?? 0) || 50;
|
|
161
|
-
const verified =
|
|
107
|
+
const verified = (0, http_retry_1.mapVerifiedStatus)(hit?.verification?.status ?? hit?.status);
|
|
162
108
|
emails.push({
|
|
163
109
|
email,
|
|
164
110
|
verified,
|
|
@@ -5,7 +5,6 @@ export { createConstructProvider } from './construct';
|
|
|
5
5
|
export { createLddProvider } from './ldd';
|
|
6
6
|
export { createSmartProspectProvider } from './smartprospect';
|
|
7
7
|
export { createHunterProvider } from './hunter';
|
|
8
|
-
export { createApolloProvider } from './apollo';
|
|
9
8
|
export { createDropcontactProvider } from './dropcontact';
|
|
10
9
|
export { createBouncerProvider, verifyEmailWithBouncer, checkCatchAllDomain, verifyEmailsBatch } from './bouncer';
|
|
11
10
|
export { createSnovioProvider, findEmailsWithSnovio, verifyEmailWithSnovio, clearSnovioTokenCache } from './snovio';
|