linkedin-secret-sauce 0.10.0 → 0.11.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.
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/enrichment/index.js +32 -1
- package/dist/enrichment/matching.js +1 -1
- package/dist/enrichment/orchestrator.js +9 -0
- package/dist/enrichment/providers/bouncer.d.ts +67 -0
- package/dist/enrichment/providers/bouncer.js +233 -0
- package/dist/enrichment/providers/construct.js +20 -8
- package/dist/enrichment/providers/index.d.ts +2 -0
- package/dist/enrichment/providers/index.js +11 -1
- package/dist/enrichment/providers/snovio.d.ts +58 -0
- package/dist/enrichment/providers/snovio.js +286 -0
- package/dist/enrichment/types.d.ts +125 -1
- package/dist/enrichment/types.js +21 -0
- package/package.json +21 -22
|
@@ -39,7 +39,7 @@ export { isPersonalEmail, isBusinessEmail, isPersonalDomain, PERSONAL_DOMAINS, }
|
|
|
39
39
|
export { isDisposableEmail, isDisposableDomain, DISPOSABLE_DOMAINS, } from "./utils/disposable-domains";
|
|
40
40
|
export { isValidEmailSyntax, isRoleAccount, asciiFold, cleanNamePart, hostnameFromUrl, extractLinkedInUsername, } from "./utils/validation";
|
|
41
41
|
export { verifyEmailMx } from "./verification/mx";
|
|
42
|
-
export { createConstructProvider, createLddProvider, createSmartProspectProvider, createHunterProvider, createApolloProvider, createDropcontactProvider, } from "./providers";
|
|
42
|
+
export { createConstructProvider, createLddProvider, createSmartProspectProvider, createHunterProvider, createApolloProvider, createDropcontactProvider, createBouncerProvider, createSnovioProvider, verifyEmailWithBouncer, checkCatchAllDomain, verifyEmailsBatch, findEmailsWithSnovio, verifyEmailWithSnovio, clearSnovioTokenCache, } from "./providers";
|
|
43
43
|
export { extractNumericLinkedInId } from "./providers/ldd";
|
|
44
44
|
export { createSmartProspectClient, type SmartProspectClient, type SmartProspectLocationOptions, } from "./providers/smartprospect";
|
|
45
45
|
export { enrichBusinessEmail, enrichBatch, enrichAllEmails, enrichAllBatch } from "./orchestrator";
|
package/dist/enrichment/index.js
CHANGED
|
@@ -42,7 +42,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
42
42
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
43
43
|
};
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.
|
|
45
|
+
exports.enrichLinkedInContact = exports.parseLinkedInSearchResponse = exports.buildSmartProspectFiltersFromLinkedIn = 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.createApolloProvider = exports.createHunterProvider = exports.createSmartProspectProvider = exports.createLddProvider = exports.createConstructProvider = 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 = void 0;
|
|
46
|
+
exports.salesLeadToContact = exports.getEmailsForLinkedInContactsBatch = exports.getEmailsForLinkedInContact = exports.createLinkedInEnricher = exports.enrichLinkedInContactsBatch = void 0;
|
|
46
47
|
exports.createEnrichmentClient = createEnrichmentClient;
|
|
47
48
|
const orchestrator_1 = require("./orchestrator");
|
|
48
49
|
const construct_1 = require("./providers/construct");
|
|
@@ -51,13 +52,27 @@ const smartprospect_1 = require("./providers/smartprospect");
|
|
|
51
52
|
const hunter_1 = require("./providers/hunter");
|
|
52
53
|
const apollo_1 = require("./providers/apollo");
|
|
53
54
|
const dropcontact_1 = require("./providers/dropcontact");
|
|
55
|
+
const bouncer_1 = require("./providers/bouncer");
|
|
56
|
+
const snovio_1 = require("./providers/snovio");
|
|
54
57
|
/**
|
|
55
58
|
* Default provider order
|
|
59
|
+
*
|
|
60
|
+
* Strategy:
|
|
61
|
+
* 1. construct - FREE pattern guessing with MX check
|
|
62
|
+
* 2. bouncer - SMTP verification of construct results ($0.006/email)
|
|
63
|
+
* 3. ldd - FREE LinkedIn data dump lookup
|
|
64
|
+
* 4. smartprospect - Paid SmartLead lookup ($0.01/email)
|
|
65
|
+
* 5. snovio - Email finder for catch-all domains ($0.02/email)
|
|
66
|
+
* 6. hunter - Hunter.io API ($0.005/email)
|
|
67
|
+
* 7. apollo - FREE Apollo.io lookup
|
|
68
|
+
* 8. dropcontact - Dropcontact API ($0.01/email)
|
|
56
69
|
*/
|
|
57
70
|
const DEFAULT_ORDER = [
|
|
58
71
|
"construct",
|
|
72
|
+
"bouncer",
|
|
59
73
|
"ldd",
|
|
60
74
|
"smartprospect",
|
|
75
|
+
"snovio",
|
|
61
76
|
"hunter",
|
|
62
77
|
"apollo",
|
|
63
78
|
"dropcontact",
|
|
@@ -90,6 +105,12 @@ function createEnrichmentClient(config) {
|
|
|
90
105
|
if (providerConfigs.dropcontact) {
|
|
91
106
|
providerFuncs.set("dropcontact", (0, dropcontact_1.createDropcontactProvider)(providerConfigs.dropcontact));
|
|
92
107
|
}
|
|
108
|
+
if (providerConfigs.bouncer) {
|
|
109
|
+
providerFuncs.set("bouncer", (0, bouncer_1.createBouncerProvider)(providerConfigs.bouncer));
|
|
110
|
+
}
|
|
111
|
+
if (providerConfigs.snovio) {
|
|
112
|
+
providerFuncs.set("snovio", (0, snovio_1.createSnovioProvider)(providerConfigs.snovio));
|
|
113
|
+
}
|
|
93
114
|
// Build ordered provider list
|
|
94
115
|
const providerOrder = options.providerOrder ?? DEFAULT_ORDER;
|
|
95
116
|
const orderedProviders = [];
|
|
@@ -250,6 +271,16 @@ Object.defineProperty(exports, "createSmartProspectProvider", { enumerable: true
|
|
|
250
271
|
Object.defineProperty(exports, "createHunterProvider", { enumerable: true, get: function () { return providers_1.createHunterProvider; } });
|
|
251
272
|
Object.defineProperty(exports, "createApolloProvider", { enumerable: true, get: function () { return providers_1.createApolloProvider; } });
|
|
252
273
|
Object.defineProperty(exports, "createDropcontactProvider", { enumerable: true, get: function () { return providers_1.createDropcontactProvider; } });
|
|
274
|
+
Object.defineProperty(exports, "createBouncerProvider", { enumerable: true, get: function () { return providers_1.createBouncerProvider; } });
|
|
275
|
+
Object.defineProperty(exports, "createSnovioProvider", { enumerable: true, get: function () { return providers_1.createSnovioProvider; } });
|
|
276
|
+
// Bouncer utilities
|
|
277
|
+
Object.defineProperty(exports, "verifyEmailWithBouncer", { enumerable: true, get: function () { return providers_1.verifyEmailWithBouncer; } });
|
|
278
|
+
Object.defineProperty(exports, "checkCatchAllDomain", { enumerable: true, get: function () { return providers_1.checkCatchAllDomain; } });
|
|
279
|
+
Object.defineProperty(exports, "verifyEmailsBatch", { enumerable: true, get: function () { return providers_1.verifyEmailsBatch; } });
|
|
280
|
+
// Snov.io utilities
|
|
281
|
+
Object.defineProperty(exports, "findEmailsWithSnovio", { enumerable: true, get: function () { return providers_1.findEmailsWithSnovio; } });
|
|
282
|
+
Object.defineProperty(exports, "verifyEmailWithSnovio", { enumerable: true, get: function () { return providers_1.verifyEmailWithSnovio; } });
|
|
283
|
+
Object.defineProperty(exports, "clearSnovioTokenCache", { enumerable: true, get: function () { return providers_1.clearSnovioTokenCache; } });
|
|
253
284
|
// Re-export LDD utilities
|
|
254
285
|
var ldd_2 = require("./providers/ldd");
|
|
255
286
|
Object.defineProperty(exports, "extractNumericLinkedInId", { enumerable: true, get: function () { return ldd_2.extractNumericLinkedInId; } });
|
|
@@ -1060,7 +1060,7 @@ async function queryPatternGuessing(contact, companyDomain, addEmail, result) {
|
|
|
1060
1060
|
try {
|
|
1061
1061
|
// Import construct provider dynamically to avoid circular deps
|
|
1062
1062
|
const { createConstructProvider } = await Promise.resolve().then(() => __importStar(require('./providers/construct')));
|
|
1063
|
-
const constructProvider = createConstructProvider({ maxAttempts:
|
|
1063
|
+
const constructProvider = createConstructProvider({ maxAttempts: 12, timeoutMs: 3000 });
|
|
1064
1064
|
const constructResult = await constructProvider({
|
|
1065
1065
|
firstName: contact.firstName,
|
|
1066
1066
|
lastName: contact.lastName,
|
|
@@ -15,6 +15,13 @@ const disposable_domains_1 = require("./utils/disposable-domains");
|
|
|
15
15
|
const validation_1 = require("./utils/validation");
|
|
16
16
|
/**
|
|
17
17
|
* Default provider costs in USD per lookup
|
|
18
|
+
*
|
|
19
|
+
* Costs based on 2025 pricing:
|
|
20
|
+
* - Bouncer: $0.006/email at scale (best accuracy 99%+)
|
|
21
|
+
* - Snov.io: $0.02/email (email finding + verification)
|
|
22
|
+
* - Hunter: $0.005/email
|
|
23
|
+
* - SmartProspect: $0.01/email
|
|
24
|
+
* - Dropcontact: $0.01/email
|
|
18
25
|
*/
|
|
19
26
|
const _PROVIDER_COSTS = {
|
|
20
27
|
construct: 0,
|
|
@@ -23,6 +30,8 @@ const _PROVIDER_COSTS = {
|
|
|
23
30
|
hunter: 0.005,
|
|
24
31
|
apollo: 0,
|
|
25
32
|
dropcontact: 0.01,
|
|
33
|
+
bouncer: 0.006,
|
|
34
|
+
snovio: 0.02,
|
|
26
35
|
};
|
|
27
36
|
/**
|
|
28
37
|
* Normalize provider result to canonical format
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bouncer.io Email Verification Provider
|
|
3
|
+
*
|
|
4
|
+
* Provides SMTP-level email verification with 99%+ accuracy.
|
|
5
|
+
* Best for verifying pattern-guessed emails on non-catch-all domains.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - SMTP verification (checks if mailbox exists)
|
|
9
|
+
* - Catch-all domain detection
|
|
10
|
+
* - Disposable email detection
|
|
11
|
+
* - Role account detection
|
|
12
|
+
* - Toxicity scoring (0-5)
|
|
13
|
+
*
|
|
14
|
+
* @see https://docs.usebouncer.com
|
|
15
|
+
*/
|
|
16
|
+
import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, BouncerConfig, BouncerVerifyResponse } from '../types';
|
|
17
|
+
/**
|
|
18
|
+
* Create the Bouncer verification provider
|
|
19
|
+
*
|
|
20
|
+
* NOTE: This provider is a VERIFIER, not a FINDER. It verifies emails
|
|
21
|
+
* that were generated by the construct provider or passed in the candidate.
|
|
22
|
+
*
|
|
23
|
+
* Usage in the enrichment flow:
|
|
24
|
+
* 1. construct provider generates email patterns
|
|
25
|
+
* 2. bouncer provider verifies which patterns are deliverable
|
|
26
|
+
* 3. Only verified emails are returned
|
|
27
|
+
*/
|
|
28
|
+
export declare function createBouncerProvider(config: BouncerConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | ProviderMultiResult | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Standalone function to verify a single email via Bouncer
|
|
31
|
+
*
|
|
32
|
+
* Useful for ad-hoc verification outside the enrichment flow.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const result = await verifyEmailWithBouncer('test@example.com', {
|
|
37
|
+
* apiKey: process.env.BOUNCER_API_KEY,
|
|
38
|
+
* });
|
|
39
|
+
* console.log(result.status); // 'deliverable' | 'undeliverable' | 'risky' | 'unknown'
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare function verifyEmailWithBouncer(email: string, config: BouncerConfig): Promise<BouncerVerifyResponse | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Check if a domain is catch-all via Bouncer
|
|
45
|
+
*
|
|
46
|
+
* Sends a verification request for a random email and checks acceptAll flag.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const isCatchAll = await checkCatchAllDomain('example.com', {
|
|
51
|
+
* apiKey: process.env.BOUNCER_API_KEY,
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function checkCatchAllDomain(domain: string, config: BouncerConfig): Promise<boolean | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Verify multiple emails in batch via Bouncer
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const results = await verifyEmailsBatch(
|
|
62
|
+
* ['john@example.com', 'jane@example.com'],
|
|
63
|
+
* { apiKey: process.env.BOUNCER_API_KEY }
|
|
64
|
+
* );
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function verifyEmailsBatch(emails: string[], config: BouncerConfig): Promise<Map<string, BouncerVerifyResponse | null>>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bouncer.io Email Verification Provider
|
|
4
|
+
*
|
|
5
|
+
* Provides SMTP-level email verification with 99%+ accuracy.
|
|
6
|
+
* Best for verifying pattern-guessed emails on non-catch-all domains.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - SMTP verification (checks if mailbox exists)
|
|
10
|
+
* - Catch-all domain detection
|
|
11
|
+
* - Disposable email detection
|
|
12
|
+
* - Role account detection
|
|
13
|
+
* - Toxicity scoring (0-5)
|
|
14
|
+
*
|
|
15
|
+
* @see https://docs.usebouncer.com
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.createBouncerProvider = createBouncerProvider;
|
|
19
|
+
exports.verifyEmailWithBouncer = verifyEmailWithBouncer;
|
|
20
|
+
exports.checkCatchAllDomain = checkCatchAllDomain;
|
|
21
|
+
exports.verifyEmailsBatch = verifyEmailsBatch;
|
|
22
|
+
const DEFAULT_API_URL = 'https://api.usebouncer.com/v1.1';
|
|
23
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
24
|
+
/**
|
|
25
|
+
* Map Bouncer status to confidence score
|
|
26
|
+
*/
|
|
27
|
+
function statusToConfidence(status, response) {
|
|
28
|
+
switch (status) {
|
|
29
|
+
case 'deliverable':
|
|
30
|
+
// High confidence - email verified as deliverable
|
|
31
|
+
// Reduce slightly if catch-all or high toxicity
|
|
32
|
+
let score = 95;
|
|
33
|
+
if (response.acceptAll)
|
|
34
|
+
score -= 20; // Catch-all reduces confidence
|
|
35
|
+
if (response.toxicity && response.toxicity >= 3)
|
|
36
|
+
score -= 10;
|
|
37
|
+
if (response.role)
|
|
38
|
+
score -= 5; // Role accounts slightly lower
|
|
39
|
+
return Math.max(50, score);
|
|
40
|
+
case 'risky':
|
|
41
|
+
// Medium confidence - risky but might work
|
|
42
|
+
return response.acceptAll ? 40 : 50;
|
|
43
|
+
case 'undeliverable':
|
|
44
|
+
// Email does not exist
|
|
45
|
+
return 0;
|
|
46
|
+
case 'unknown':
|
|
47
|
+
// Could not verify
|
|
48
|
+
return 30;
|
|
49
|
+
default:
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Extract email candidates from the enrichment candidate
|
|
55
|
+
* Bouncer is a VERIFICATION provider, so it needs emails to verify
|
|
56
|
+
*/
|
|
57
|
+
function extractEmailsToVerify(candidate) {
|
|
58
|
+
const emails = [];
|
|
59
|
+
// Check if candidate has pre-generated email patterns in metadata
|
|
60
|
+
const metadata = candidate._emailCandidates;
|
|
61
|
+
if (Array.isArray(metadata)) {
|
|
62
|
+
emails.push(...metadata.filter((e) => typeof e === 'string'));
|
|
63
|
+
}
|
|
64
|
+
return emails;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Verify a single email via Bouncer API
|
|
68
|
+
*/
|
|
69
|
+
async function verifyEmail(email, apiKey, apiUrl, timeoutMs) {
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
72
|
+
try {
|
|
73
|
+
const url = `${apiUrl}/email/verify?email=${encodeURIComponent(email)}`;
|
|
74
|
+
const response = await fetch(url, {
|
|
75
|
+
method: 'GET',
|
|
76
|
+
headers: {
|
|
77
|
+
'x-api-key': apiKey,
|
|
78
|
+
'Accept': 'application/json',
|
|
79
|
+
},
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
// All HTTP errors return null - let caller decide how to handle
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const data = await response.json();
|
|
87
|
+
return data;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// All errors (network, timeout, etc.) return null
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
clearTimeout(timeoutId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Create the Bouncer verification provider
|
|
99
|
+
*
|
|
100
|
+
* NOTE: This provider is a VERIFIER, not a FINDER. It verifies emails
|
|
101
|
+
* that were generated by the construct provider or passed in the candidate.
|
|
102
|
+
*
|
|
103
|
+
* Usage in the enrichment flow:
|
|
104
|
+
* 1. construct provider generates email patterns
|
|
105
|
+
* 2. bouncer provider verifies which patterns are deliverable
|
|
106
|
+
* 3. Only verified emails are returned
|
|
107
|
+
*/
|
|
108
|
+
function createBouncerProvider(config) {
|
|
109
|
+
const { apiKey, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS } = config;
|
|
110
|
+
if (!apiKey) {
|
|
111
|
+
// Return no-op provider if not configured
|
|
112
|
+
const noop = async () => null;
|
|
113
|
+
noop.__name = 'bouncer';
|
|
114
|
+
return noop;
|
|
115
|
+
}
|
|
116
|
+
async function verifyEmails(candidate) {
|
|
117
|
+
const emailsToVerify = extractEmailsToVerify(candidate);
|
|
118
|
+
// If no emails to verify, return null
|
|
119
|
+
// The orchestrator will need to pass emails from construct
|
|
120
|
+
if (emailsToVerify.length === 0) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const verifiedEmails = [];
|
|
124
|
+
// Verify each email candidate
|
|
125
|
+
for (const email of emailsToVerify) {
|
|
126
|
+
try {
|
|
127
|
+
const result = await verifyEmail(email, apiKey, apiUrl, timeoutMs);
|
|
128
|
+
if (result) {
|
|
129
|
+
const confidence = statusToConfidence(result.status, result);
|
|
130
|
+
// Only include emails that are potentially deliverable
|
|
131
|
+
if (result.status === 'deliverable' || result.status === 'risky') {
|
|
132
|
+
verifiedEmails.push({
|
|
133
|
+
email: result.email,
|
|
134
|
+
verified: result.status === 'deliverable',
|
|
135
|
+
confidence,
|
|
136
|
+
isCatchAll: result.acceptAll,
|
|
137
|
+
metadata: {
|
|
138
|
+
bouncerStatus: result.status,
|
|
139
|
+
bouncerReason: result.reason,
|
|
140
|
+
toxicity: result.toxicity,
|
|
141
|
+
isDisposable: result.disposable,
|
|
142
|
+
isRole: result.role,
|
|
143
|
+
isFreeProvider: result.free,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Continue with other emails on error
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (verifiedEmails.length === 0) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
// Sort by confidence
|
|
158
|
+
verifiedEmails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
|
|
159
|
+
return { emails: verifiedEmails };
|
|
160
|
+
}
|
|
161
|
+
// Mark provider name for orchestrator
|
|
162
|
+
verifyEmails.__name = 'bouncer';
|
|
163
|
+
return verifyEmails;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Standalone function to verify a single email via Bouncer
|
|
167
|
+
*
|
|
168
|
+
* Useful for ad-hoc verification outside the enrichment flow.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```typescript
|
|
172
|
+
* const result = await verifyEmailWithBouncer('test@example.com', {
|
|
173
|
+
* apiKey: process.env.BOUNCER_API_KEY,
|
|
174
|
+
* });
|
|
175
|
+
* console.log(result.status); // 'deliverable' | 'undeliverable' | 'risky' | 'unknown'
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
async function verifyEmailWithBouncer(email, config) {
|
|
179
|
+
const { apiKey, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS } = config;
|
|
180
|
+
return verifyEmail(email, apiKey, apiUrl, timeoutMs);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Check if a domain is catch-all via Bouncer
|
|
184
|
+
*
|
|
185
|
+
* Sends a verification request for a random email and checks acceptAll flag.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* const isCatchAll = await checkCatchAllDomain('example.com', {
|
|
190
|
+
* apiKey: process.env.BOUNCER_API_KEY,
|
|
191
|
+
* });
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
async function checkCatchAllDomain(domain, config) {
|
|
195
|
+
const { apiKey, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS } = config;
|
|
196
|
+
// Generate a random email that almost certainly doesn't exist
|
|
197
|
+
const randomLocal = `bounce-test-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
198
|
+
const testEmail = `${randomLocal}@${domain}`;
|
|
199
|
+
const result = await verifyEmail(testEmail, apiKey, apiUrl, timeoutMs);
|
|
200
|
+
if (!result) {
|
|
201
|
+
return null; // Could not determine
|
|
202
|
+
}
|
|
203
|
+
// If acceptAll is true, the domain accepts all emails (catch-all)
|
|
204
|
+
return result.acceptAll;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Verify multiple emails in batch via Bouncer
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```typescript
|
|
211
|
+
* const results = await verifyEmailsBatch(
|
|
212
|
+
* ['john@example.com', 'jane@example.com'],
|
|
213
|
+
* { apiKey: process.env.BOUNCER_API_KEY }
|
|
214
|
+
* );
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
async function verifyEmailsBatch(emails, config) {
|
|
218
|
+
const { apiKey, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS } = config;
|
|
219
|
+
const results = new Map();
|
|
220
|
+
// Verify in parallel with concurrency limit
|
|
221
|
+
const CONCURRENCY = 5;
|
|
222
|
+
for (let i = 0; i < emails.length; i += CONCURRENCY) {
|
|
223
|
+
const batch = emails.slice(i, i + CONCURRENCY);
|
|
224
|
+
const batchResults = await Promise.all(batch.map(async (email) => {
|
|
225
|
+
const result = await verifyEmail(email, apiKey, apiUrl, timeoutMs);
|
|
226
|
+
return { email, result };
|
|
227
|
+
}));
|
|
228
|
+
for (const { email, result } of batchResults) {
|
|
229
|
+
results.set(email, result);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return results;
|
|
233
|
+
}
|
|
@@ -12,27 +12,39 @@ const personal_domains_1 = require("../utils/personal-domains");
|
|
|
12
12
|
const validation_1 = require("../utils/validation");
|
|
13
13
|
/**
|
|
14
14
|
* Build all email pattern candidates for a person
|
|
15
|
+
*
|
|
16
|
+
* Patterns are ordered by commonality based on real-world email conventions:
|
|
17
|
+
* 1. firstname (most common for small companies)
|
|
18
|
+
* 2. firstname.lastname (most common for larger companies)
|
|
19
|
+
* 3. f.lastname, flastname, etc.
|
|
15
20
|
*/
|
|
16
21
|
function buildCandidates(input) {
|
|
17
22
|
const domain = String(input.domain || '').toLowerCase();
|
|
18
23
|
const first = (0, validation_1.cleanNamePart)(input.first || '');
|
|
19
24
|
const last = (0, validation_1.cleanNamePart)(input.last || '');
|
|
20
25
|
const fl = first ? first[0] : '';
|
|
26
|
+
const ll = last ? last[0] : '';
|
|
21
27
|
const locals = [];
|
|
22
|
-
//
|
|
28
|
+
// Simple first name pattern (very common, especially for small companies/startups)
|
|
29
|
+
if (first)
|
|
30
|
+
locals.push(first); // john
|
|
31
|
+
// Common combined patterns
|
|
23
32
|
if (first && last) {
|
|
24
|
-
locals.push(`${first}.${last}`); // john.doe
|
|
25
|
-
locals.push(`${fl}.${last}`); // j.doe
|
|
26
|
-
locals.push(`${first}${last[0] ?? ''}`); // johnd
|
|
33
|
+
locals.push(`${first}.${last}`); // john.doe (most common corporate pattern)
|
|
27
34
|
locals.push(`${fl}${last}`); // jdoe
|
|
35
|
+
locals.push(`${first}${ll}`); // johnd
|
|
36
|
+
locals.push(`${fl}.${last}`); // j.doe
|
|
37
|
+
locals.push(`${first}${last}`); // johndoe
|
|
28
38
|
locals.push(`${first}_${last}`); // john_doe
|
|
29
39
|
locals.push(`${last}.${first}`); // doe.john
|
|
40
|
+
locals.push(`${last}${first}`); // doejohn
|
|
41
|
+
locals.push(`${last}`); // doe (last name only)
|
|
42
|
+
locals.push(`${fl}${ll}`); // jd (initials)
|
|
30
43
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
locals.push(first); // john
|
|
34
|
-
if (last)
|
|
44
|
+
else if (last) {
|
|
45
|
+
// Only last name available
|
|
35
46
|
locals.push(last); // doe
|
|
47
|
+
}
|
|
36
48
|
// Deduplicate while preserving order
|
|
37
49
|
const seen = new Set();
|
|
38
50
|
const emails = [];
|
|
@@ -7,3 +7,5 @@ export { createSmartProspectProvider } from './smartprospect';
|
|
|
7
7
|
export { createHunterProvider } from './hunter';
|
|
8
8
|
export { createApolloProvider } from './apollo';
|
|
9
9
|
export { createDropcontactProvider } from './dropcontact';
|
|
10
|
+
export { createBouncerProvider, verifyEmailWithBouncer, checkCatchAllDomain, verifyEmailsBatch } from './bouncer';
|
|
11
|
+
export { createSnovioProvider, findEmailsWithSnovio, verifyEmailWithSnovio, clearSnovioTokenCache } from './snovio';
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Email Enrichment Providers
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.createDropcontactProvider = exports.createApolloProvider = exports.createHunterProvider = exports.createSmartProspectProvider = exports.createLddProvider = exports.createConstructProvider = void 0;
|
|
6
|
+
exports.clearSnovioTokenCache = exports.verifyEmailWithSnovio = exports.findEmailsWithSnovio = exports.createSnovioProvider = exports.verifyEmailsBatch = exports.checkCatchAllDomain = exports.verifyEmailWithBouncer = exports.createBouncerProvider = exports.createDropcontactProvider = exports.createApolloProvider = exports.createHunterProvider = exports.createSmartProspectProvider = exports.createLddProvider = exports.createConstructProvider = void 0;
|
|
7
7
|
var construct_1 = require("./construct");
|
|
8
8
|
Object.defineProperty(exports, "createConstructProvider", { enumerable: true, get: function () { return construct_1.createConstructProvider; } });
|
|
9
9
|
var ldd_1 = require("./ldd");
|
|
@@ -16,3 +16,13 @@ var apollo_1 = require("./apollo");
|
|
|
16
16
|
Object.defineProperty(exports, "createApolloProvider", { enumerable: true, get: function () { return apollo_1.createApolloProvider; } });
|
|
17
17
|
var dropcontact_1 = require("./dropcontact");
|
|
18
18
|
Object.defineProperty(exports, "createDropcontactProvider", { enumerable: true, get: function () { return dropcontact_1.createDropcontactProvider; } });
|
|
19
|
+
var bouncer_1 = require("./bouncer");
|
|
20
|
+
Object.defineProperty(exports, "createBouncerProvider", { enumerable: true, get: function () { return bouncer_1.createBouncerProvider; } });
|
|
21
|
+
Object.defineProperty(exports, "verifyEmailWithBouncer", { enumerable: true, get: function () { return bouncer_1.verifyEmailWithBouncer; } });
|
|
22
|
+
Object.defineProperty(exports, "checkCatchAllDomain", { enumerable: true, get: function () { return bouncer_1.checkCatchAllDomain; } });
|
|
23
|
+
Object.defineProperty(exports, "verifyEmailsBatch", { enumerable: true, get: function () { return bouncer_1.verifyEmailsBatch; } });
|
|
24
|
+
var snovio_1 = require("./snovio");
|
|
25
|
+
Object.defineProperty(exports, "createSnovioProvider", { enumerable: true, get: function () { return snovio_1.createSnovioProvider; } });
|
|
26
|
+
Object.defineProperty(exports, "findEmailsWithSnovio", { enumerable: true, get: function () { return snovio_1.findEmailsWithSnovio; } });
|
|
27
|
+
Object.defineProperty(exports, "verifyEmailWithSnovio", { enumerable: true, get: function () { return snovio_1.verifyEmailWithSnovio; } });
|
|
28
|
+
Object.defineProperty(exports, "clearSnovioTokenCache", { enumerable: true, get: function () { return snovio_1.clearSnovioTokenCache; } });
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snov.io Email Finder Provider
|
|
3
|
+
*
|
|
4
|
+
* Provides email finding with 98% delivery rate and built-in verification.
|
|
5
|
+
* Best for finding emails when pattern guessing fails or on catch-all domains.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Email finding by name + domain
|
|
9
|
+
* - Built-in email verification
|
|
10
|
+
* - Database of 70M+ contacts
|
|
11
|
+
* - LinkedIn profile support
|
|
12
|
+
*
|
|
13
|
+
* @see https://snov.io/api
|
|
14
|
+
*/
|
|
15
|
+
import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, SnovioConfig, SnovioGetEmailsResponse, SnovioVerificationStatus } from '../types';
|
|
16
|
+
/**
|
|
17
|
+
* Create the Snov.io email finder provider
|
|
18
|
+
*
|
|
19
|
+
* This provider uses Snov.io's database to find verified emails
|
|
20
|
+
* when pattern guessing fails or for catch-all domains.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createSnovioProvider(config: SnovioConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | ProviderMultiResult | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Standalone function to find emails via Snov.io
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const result = await findEmailsWithSnovio(
|
|
29
|
+
* 'John',
|
|
30
|
+
* 'Doe',
|
|
31
|
+
* 'example.com',
|
|
32
|
+
* {
|
|
33
|
+
* userId: process.env.SNOVIO_USER_ID,
|
|
34
|
+
* apiSecret: process.env.SNOVIO_API_SECRET,
|
|
35
|
+
* }
|
|
36
|
+
* );
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function findEmailsWithSnovio(firstName: string, lastName: string, domain: string, config: SnovioConfig): Promise<SnovioGetEmailsResponse | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Verify a single email via Snov.io
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const result = await verifyEmailWithSnovio('test@example.com', {
|
|
46
|
+
* userId: process.env.SNOVIO_USER_ID,
|
|
47
|
+
* apiSecret: process.env.SNOVIO_API_SECRET,
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function verifyEmailWithSnovio(email: string, config: SnovioConfig): Promise<{
|
|
52
|
+
email: string;
|
|
53
|
+
status: SnovioVerificationStatus;
|
|
54
|
+
} | null>;
|
|
55
|
+
/**
|
|
56
|
+
* Clear the cached access token (useful for testing)
|
|
57
|
+
*/
|
|
58
|
+
export declare function clearSnovioTokenCache(): void;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Snov.io Email Finder Provider
|
|
4
|
+
*
|
|
5
|
+
* Provides email finding with 98% delivery rate and built-in verification.
|
|
6
|
+
* Best for finding emails when pattern guessing fails or on catch-all domains.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Email finding by name + domain
|
|
10
|
+
* - Built-in email verification
|
|
11
|
+
* - Database of 70M+ contacts
|
|
12
|
+
* - LinkedIn profile support
|
|
13
|
+
*
|
|
14
|
+
* @see https://snov.io/api
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.createSnovioProvider = createSnovioProvider;
|
|
18
|
+
exports.findEmailsWithSnovio = findEmailsWithSnovio;
|
|
19
|
+
exports.verifyEmailWithSnovio = verifyEmailWithSnovio;
|
|
20
|
+
exports.clearSnovioTokenCache = clearSnovioTokenCache;
|
|
21
|
+
const DEFAULT_API_URL = 'https://api.snov.io';
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
23
|
+
// Token cache
|
|
24
|
+
let cachedAccessToken = null;
|
|
25
|
+
let tokenExpiresAt = 0;
|
|
26
|
+
/**
|
|
27
|
+
* Map Snov.io verification status to confidence score
|
|
28
|
+
*/
|
|
29
|
+
function statusToConfidence(status) {
|
|
30
|
+
switch (status) {
|
|
31
|
+
case 'valid':
|
|
32
|
+
return 95; // High confidence - verified email
|
|
33
|
+
case 'catch_all':
|
|
34
|
+
return 60; // Medium confidence - catch-all domain
|
|
35
|
+
case 'unverifiable':
|
|
36
|
+
return 40; // Low confidence - could not verify
|
|
37
|
+
case 'not_valid':
|
|
38
|
+
return 0; // Invalid email
|
|
39
|
+
case 'unknown':
|
|
40
|
+
default:
|
|
41
|
+
return 30;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Extract name and domain from candidate
|
|
46
|
+
*/
|
|
47
|
+
function extractNameAndDomain(candidate) {
|
|
48
|
+
const firstName = candidate.firstName ||
|
|
49
|
+
candidate.first_name ||
|
|
50
|
+
candidate.first ||
|
|
51
|
+
candidate.name?.split(' ')?.[0] ||
|
|
52
|
+
'';
|
|
53
|
+
const lastName = candidate.lastName ||
|
|
54
|
+
candidate.last_name ||
|
|
55
|
+
candidate.last ||
|
|
56
|
+
candidate.name?.split(' ')?.slice(1).join(' ') ||
|
|
57
|
+
'';
|
|
58
|
+
const domain = candidate.domain ||
|
|
59
|
+
candidate.companyDomain ||
|
|
60
|
+
candidate.company_domain ||
|
|
61
|
+
'';
|
|
62
|
+
if (!firstName || !domain) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return { firstName, lastName, domain };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get OAuth access token from Snov.io
|
|
69
|
+
*/
|
|
70
|
+
async function getAccessToken(userId, apiSecret, apiUrl, timeoutMs) {
|
|
71
|
+
// Return cached token if valid
|
|
72
|
+
if (cachedAccessToken && Date.now() < tokenExpiresAt) {
|
|
73
|
+
return cachedAccessToken;
|
|
74
|
+
}
|
|
75
|
+
const controller = new AbortController();
|
|
76
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(`${apiUrl}/v1/oauth/access_token`, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'application/json',
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
grant_type: 'client_credentials',
|
|
85
|
+
client_id: userId,
|
|
86
|
+
client_secret: apiSecret,
|
|
87
|
+
}),
|
|
88
|
+
signal: controller.signal,
|
|
89
|
+
});
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const data = await response.json();
|
|
94
|
+
// Cache token with 5 minute buffer
|
|
95
|
+
cachedAccessToken = data.access_token;
|
|
96
|
+
tokenExpiresAt = Date.now() + (data.expires_in - 300) * 1000;
|
|
97
|
+
return data.access_token;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
clearTimeout(timeoutId);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Find emails using Snov.io's get-emails-from-name API
|
|
108
|
+
*/
|
|
109
|
+
async function findEmailsByName(firstName, lastName, domain, accessToken, apiUrl, timeoutMs) {
|
|
110
|
+
const controller = new AbortController();
|
|
111
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
112
|
+
try {
|
|
113
|
+
const params = new URLSearchParams({
|
|
114
|
+
access_token: accessToken,
|
|
115
|
+
firstName,
|
|
116
|
+
lastName,
|
|
117
|
+
domain,
|
|
118
|
+
});
|
|
119
|
+
const response = await fetch(`${apiUrl}/v1/get-emails-from-names?${params}`, {
|
|
120
|
+
method: 'GET',
|
|
121
|
+
headers: {
|
|
122
|
+
'Accept': 'application/json',
|
|
123
|
+
},
|
|
124
|
+
signal: controller.signal,
|
|
125
|
+
});
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
// Handle rate limiting
|
|
128
|
+
if (response.status === 429) {
|
|
129
|
+
throw new Error('Rate limited by Snov.io API');
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const data = await response.json();
|
|
134
|
+
return data;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
138
|
+
return null; // Timeout
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
clearTimeout(timeoutId);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Create the Snov.io email finder provider
|
|
148
|
+
*
|
|
149
|
+
* This provider uses Snov.io's database to find verified emails
|
|
150
|
+
* when pattern guessing fails or for catch-all domains.
|
|
151
|
+
*/
|
|
152
|
+
function createSnovioProvider(config) {
|
|
153
|
+
const { userId, apiSecret, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS, } = config;
|
|
154
|
+
if (!userId || !apiSecret) {
|
|
155
|
+
// Return no-op provider if not configured
|
|
156
|
+
const noop = async () => null;
|
|
157
|
+
noop.__name = 'snovio';
|
|
158
|
+
return noop;
|
|
159
|
+
}
|
|
160
|
+
async function findEmails(candidate) {
|
|
161
|
+
const nameAndDomain = extractNameAndDomain(candidate);
|
|
162
|
+
if (!nameAndDomain) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const { firstName, lastName, domain } = nameAndDomain;
|
|
166
|
+
// Get access token
|
|
167
|
+
const accessToken = await getAccessToken(userId, apiSecret, apiUrl, timeoutMs);
|
|
168
|
+
if (!accessToken) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
// Find emails
|
|
172
|
+
const result = await findEmailsByName(firstName, lastName, domain, accessToken, apiUrl, timeoutMs);
|
|
173
|
+
if (!result || !result.success || !result.data?.emails?.length) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
// Convert to provider multi-result format
|
|
177
|
+
const emails = result.data.emails
|
|
178
|
+
.filter((e) => e.email && e.emailStatus !== 'not_valid')
|
|
179
|
+
.map((e) => ({
|
|
180
|
+
email: e.email,
|
|
181
|
+
verified: e.emailStatus === 'valid',
|
|
182
|
+
confidence: statusToConfidence(e.emailStatus),
|
|
183
|
+
isCatchAll: e.emailStatus === 'catch_all',
|
|
184
|
+
metadata: {
|
|
185
|
+
snovioStatus: e.emailStatus,
|
|
186
|
+
firstName: e.firstName,
|
|
187
|
+
lastName: e.lastName,
|
|
188
|
+
position: e.position,
|
|
189
|
+
companyName: e.companyName,
|
|
190
|
+
sourcePage: e.sourcePage,
|
|
191
|
+
type: e.type,
|
|
192
|
+
},
|
|
193
|
+
}));
|
|
194
|
+
if (emails.length === 0) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
// Sort by confidence
|
|
198
|
+
emails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
|
|
199
|
+
return { emails };
|
|
200
|
+
}
|
|
201
|
+
// Mark provider name for orchestrator
|
|
202
|
+
findEmails.__name = 'snovio';
|
|
203
|
+
return findEmails;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Standalone function to find emails via Snov.io
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```typescript
|
|
210
|
+
* const result = await findEmailsWithSnovio(
|
|
211
|
+
* 'John',
|
|
212
|
+
* 'Doe',
|
|
213
|
+
* 'example.com',
|
|
214
|
+
* {
|
|
215
|
+
* userId: process.env.SNOVIO_USER_ID,
|
|
216
|
+
* apiSecret: process.env.SNOVIO_API_SECRET,
|
|
217
|
+
* }
|
|
218
|
+
* );
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
async function findEmailsWithSnovio(firstName, lastName, domain, config) {
|
|
222
|
+
const { userId, apiSecret, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS, } = config;
|
|
223
|
+
const accessToken = await getAccessToken(userId, apiSecret, apiUrl, timeoutMs);
|
|
224
|
+
if (!accessToken) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return findEmailsByName(firstName, lastName, domain, accessToken, apiUrl, timeoutMs);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Verify a single email via Snov.io
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```typescript
|
|
234
|
+
* const result = await verifyEmailWithSnovio('test@example.com', {
|
|
235
|
+
* userId: process.env.SNOVIO_USER_ID,
|
|
236
|
+
* apiSecret: process.env.SNOVIO_API_SECRET,
|
|
237
|
+
* });
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
async function verifyEmailWithSnovio(email, config) {
|
|
241
|
+
const { userId, apiSecret, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS, } = config;
|
|
242
|
+
const accessToken = await getAccessToken(userId, apiSecret, apiUrl, timeoutMs);
|
|
243
|
+
if (!accessToken) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const controller = new AbortController();
|
|
247
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
248
|
+
try {
|
|
249
|
+
const params = new URLSearchParams({
|
|
250
|
+
access_token: accessToken,
|
|
251
|
+
email,
|
|
252
|
+
});
|
|
253
|
+
const response = await fetch(`${apiUrl}/v1/email-verifier?${params}`, {
|
|
254
|
+
method: 'GET',
|
|
255
|
+
headers: {
|
|
256
|
+
'Accept': 'application/json',
|
|
257
|
+
},
|
|
258
|
+
signal: controller.signal,
|
|
259
|
+
});
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const data = await response.json();
|
|
264
|
+
if (!data.success || !data.data?.emails?.length) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const result = data.data.emails[0];
|
|
268
|
+
return {
|
|
269
|
+
email: result.email,
|
|
270
|
+
status: result.result,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
clearTimeout(timeoutId);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Clear the cached access token (useful for testing)
|
|
282
|
+
*/
|
|
283
|
+
function clearSnovioTokenCache() {
|
|
284
|
+
cachedAccessToken = null;
|
|
285
|
+
tokenExpiresAt = 0;
|
|
286
|
+
}
|
|
@@ -166,6 +166,109 @@ export interface LddConfig {
|
|
|
166
166
|
export interface DropcontactConfig {
|
|
167
167
|
apiKey: string;
|
|
168
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Bouncer.io provider configuration
|
|
171
|
+
*
|
|
172
|
+
* Bouncer provides SMTP email verification with 99%+ accuracy.
|
|
173
|
+
* Best for verifying pattern-guessed emails on non-catch-all domains.
|
|
174
|
+
*
|
|
175
|
+
* @see https://docs.usebouncer.com
|
|
176
|
+
*/
|
|
177
|
+
export interface BouncerConfig {
|
|
178
|
+
/** Bouncer API key */
|
|
179
|
+
apiKey: string;
|
|
180
|
+
/** API URL override (default: https://api.usebouncer.com/v1.1) */
|
|
181
|
+
apiUrl?: string;
|
|
182
|
+
/** Timeout in ms (default: 30000) */
|
|
183
|
+
timeoutMs?: number;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Bouncer verification result status
|
|
187
|
+
*/
|
|
188
|
+
export type BouncerStatus = 'deliverable' | 'undeliverable' | 'risky' | 'unknown';
|
|
189
|
+
/**
|
|
190
|
+
* Bouncer verification result reason
|
|
191
|
+
*/
|
|
192
|
+
export type BouncerReason = 'accepted_email' | 'rejected_email' | 'invalid_domain' | 'invalid_email' | 'unavailable_smtp' | 'dns_error' | 'low_deliverability' | 'low_quality' | 'catch_all' | 'full_mailbox' | 'role_account' | 'disposable' | 'timeout' | 'unknown';
|
|
193
|
+
/**
|
|
194
|
+
* Bouncer API response for single email verification
|
|
195
|
+
*/
|
|
196
|
+
export interface BouncerVerifyResponse {
|
|
197
|
+
status: BouncerStatus;
|
|
198
|
+
reason: BouncerReason;
|
|
199
|
+
email: string;
|
|
200
|
+
domain: string;
|
|
201
|
+
account: string;
|
|
202
|
+
/** Whether this is a free email provider (Gmail, Yahoo, etc.) */
|
|
203
|
+
free: boolean;
|
|
204
|
+
/** Whether this is a disposable email */
|
|
205
|
+
disposable: boolean;
|
|
206
|
+
/** Whether this is a role-based email */
|
|
207
|
+
role: boolean;
|
|
208
|
+
/** Whether domain is catch-all (accepts all emails) */
|
|
209
|
+
acceptAll: boolean;
|
|
210
|
+
/** Did you mean suggestion for typos */
|
|
211
|
+
didYouMean?: string;
|
|
212
|
+
/** DNS information */
|
|
213
|
+
dns?: {
|
|
214
|
+
type: string;
|
|
215
|
+
record: string;
|
|
216
|
+
};
|
|
217
|
+
/** Toxicity score (0-5, higher = more risky) */
|
|
218
|
+
toxicity?: number;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Snov.io provider configuration
|
|
222
|
+
*
|
|
223
|
+
* Snov.io provides email finding with 98% delivery rate and built-in verification.
|
|
224
|
+
* Best for finding emails when pattern guessing fails or on catch-all domains.
|
|
225
|
+
*
|
|
226
|
+
* @see https://snov.io/api
|
|
227
|
+
*/
|
|
228
|
+
export interface SnovioConfig {
|
|
229
|
+
/** Snov.io API user ID */
|
|
230
|
+
userId: string;
|
|
231
|
+
/** Snov.io API secret */
|
|
232
|
+
apiSecret: string;
|
|
233
|
+
/** API URL override (default: https://api.snov.io) */
|
|
234
|
+
apiUrl?: string;
|
|
235
|
+
/** Timeout in ms (default: 30000) */
|
|
236
|
+
timeoutMs?: number;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Snov.io email verification status
|
|
240
|
+
*/
|
|
241
|
+
export type SnovioVerificationStatus = 'valid' | 'not_valid' | 'catch_all' | 'unverifiable' | 'unknown';
|
|
242
|
+
/**
|
|
243
|
+
* Snov.io email result
|
|
244
|
+
*/
|
|
245
|
+
export interface SnovioEmailResult {
|
|
246
|
+
email: string;
|
|
247
|
+
emailStatus: SnovioVerificationStatus;
|
|
248
|
+
firstName?: string;
|
|
249
|
+
lastName?: string;
|
|
250
|
+
position?: string;
|
|
251
|
+
sourcePage?: string;
|
|
252
|
+
companyName?: string;
|
|
253
|
+
type?: 'prospect' | 'personal';
|
|
254
|
+
status?: string;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Snov.io get-emails-from-name API response
|
|
258
|
+
*/
|
|
259
|
+
export interface SnovioGetEmailsResponse {
|
|
260
|
+
success: boolean;
|
|
261
|
+
data: {
|
|
262
|
+
firstName: string;
|
|
263
|
+
lastName: string;
|
|
264
|
+
emails: SnovioEmailResult[];
|
|
265
|
+
};
|
|
266
|
+
params?: {
|
|
267
|
+
firstName: string;
|
|
268
|
+
lastName: string;
|
|
269
|
+
domain: string;
|
|
270
|
+
};
|
|
271
|
+
}
|
|
169
272
|
/**
|
|
170
273
|
* Email construction provider configuration (no API key needed)
|
|
171
274
|
*/
|
|
@@ -185,6 +288,10 @@ export interface ProvidersConfig {
|
|
|
185
288
|
hunter?: HunterConfig;
|
|
186
289
|
apollo?: ApolloConfig;
|
|
187
290
|
dropcontact?: DropcontactConfig;
|
|
291
|
+
/** Bouncer.io for SMTP email verification (99%+ accuracy) */
|
|
292
|
+
bouncer?: BouncerConfig;
|
|
293
|
+
/** Snov.io for email finding (98% delivery rate) */
|
|
294
|
+
snovio?: SnovioConfig;
|
|
188
295
|
}
|
|
189
296
|
/**
|
|
190
297
|
* Options for enrichment operations
|
|
@@ -263,13 +370,30 @@ export interface EnrichmentClient {
|
|
|
263
370
|
/**
|
|
264
371
|
* Available provider names
|
|
265
372
|
*/
|
|
266
|
-
export type ProviderName = "construct" | "ldd" | "smartprospect" | "hunter" | "apollo" | "dropcontact";
|
|
373
|
+
export type ProviderName = "construct" | "ldd" | "smartprospect" | "hunter" | "apollo" | "dropcontact" | "bouncer" | "snovio";
|
|
267
374
|
/**
|
|
268
375
|
* Default provider order
|
|
376
|
+
*
|
|
377
|
+
* Strategy:
|
|
378
|
+
* 1. construct - FREE pattern guessing with MX check
|
|
379
|
+
* 2. bouncer - SMTP verification of construct results ($0.006/email)
|
|
380
|
+
* 3. ldd - FREE LinkedIn data dump lookup
|
|
381
|
+
* 4. smartprospect - Paid SmartLead lookup ($0.01/email)
|
|
382
|
+
* 5. snovio - Email finder for catch-all domains ($0.02/email)
|
|
383
|
+
* 6. hunter - Hunter.io API ($0.005/email)
|
|
384
|
+
* 7. apollo - FREE Apollo.io lookup
|
|
385
|
+
* 8. dropcontact - Dropcontact API ($0.01/email)
|
|
269
386
|
*/
|
|
270
387
|
export declare const DEFAULT_PROVIDER_ORDER: ProviderName[];
|
|
271
388
|
/**
|
|
272
389
|
* Provider costs in USD per lookup
|
|
390
|
+
*
|
|
391
|
+
* Costs based on 2025 pricing:
|
|
392
|
+
* - Bouncer: $0.006/email at scale (best accuracy 99%+)
|
|
393
|
+
* - Snov.io: $0.02/email (email finding + verification)
|
|
394
|
+
* - Hunter: $0.005/email
|
|
395
|
+
* - SmartProspect: $0.01/email
|
|
396
|
+
* - Dropcontact: $0.01/email
|
|
273
397
|
*/
|
|
274
398
|
export declare const PROVIDER_COSTS: Record<ProviderName, number>;
|
|
275
399
|
/**
|
package/dist/enrichment/types.js
CHANGED
|
@@ -9,17 +9,36 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
9
9
|
exports.SMARTPROSPECT_SUB_INDUSTRIES = exports.PROVIDER_COSTS = exports.DEFAULT_PROVIDER_ORDER = void 0;
|
|
10
10
|
/**
|
|
11
11
|
* Default provider order
|
|
12
|
+
*
|
|
13
|
+
* Strategy:
|
|
14
|
+
* 1. construct - FREE pattern guessing with MX check
|
|
15
|
+
* 2. bouncer - SMTP verification of construct results ($0.006/email)
|
|
16
|
+
* 3. ldd - FREE LinkedIn data dump lookup
|
|
17
|
+
* 4. smartprospect - Paid SmartLead lookup ($0.01/email)
|
|
18
|
+
* 5. snovio - Email finder for catch-all domains ($0.02/email)
|
|
19
|
+
* 6. hunter - Hunter.io API ($0.005/email)
|
|
20
|
+
* 7. apollo - FREE Apollo.io lookup
|
|
21
|
+
* 8. dropcontact - Dropcontact API ($0.01/email)
|
|
12
22
|
*/
|
|
13
23
|
exports.DEFAULT_PROVIDER_ORDER = [
|
|
14
24
|
"construct",
|
|
25
|
+
"bouncer",
|
|
15
26
|
"ldd",
|
|
16
27
|
"smartprospect",
|
|
28
|
+
"snovio",
|
|
17
29
|
"hunter",
|
|
18
30
|
"apollo",
|
|
19
31
|
"dropcontact",
|
|
20
32
|
];
|
|
21
33
|
/**
|
|
22
34
|
* Provider costs in USD per lookup
|
|
35
|
+
*
|
|
36
|
+
* Costs based on 2025 pricing:
|
|
37
|
+
* - Bouncer: $0.006/email at scale (best accuracy 99%+)
|
|
38
|
+
* - Snov.io: $0.02/email (email finding + verification)
|
|
39
|
+
* - Hunter: $0.005/email
|
|
40
|
+
* - SmartProspect: $0.01/email
|
|
41
|
+
* - Dropcontact: $0.01/email
|
|
23
42
|
*/
|
|
24
43
|
exports.PROVIDER_COSTS = {
|
|
25
44
|
construct: 0,
|
|
@@ -28,6 +47,8 @@ exports.PROVIDER_COSTS = {
|
|
|
28
47
|
hunter: 0.005,
|
|
29
48
|
apollo: 0,
|
|
30
49
|
dropcontact: 0.01,
|
|
50
|
+
bouncer: 0.006,
|
|
51
|
+
snovio: 0.02,
|
|
31
52
|
};
|
|
32
53
|
/**
|
|
33
54
|
* SmartProspect Sub-Industry values (exact API values - partial list)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linkedin-secret-sauce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Private LinkedIn Sales Navigator client with automatic cookie management",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,26 +10,6 @@
|
|
|
10
10
|
"publishConfig": {
|
|
11
11
|
"registry": "https://registry.npmjs.org/"
|
|
12
12
|
},
|
|
13
|
-
"scripts": {
|
|
14
|
-
"dev:playground": "pnpm -C apps/playground dev",
|
|
15
|
-
"search": "node scripts/rg-fast.mjs",
|
|
16
|
-
"rg:fast": "node scripts/rg-fast.mjs",
|
|
17
|
-
"build": "tsc -p tsconfig.json",
|
|
18
|
-
"typecheck": "tsc --noEmit",
|
|
19
|
-
"typecheck:playground": "pnpm -C apps/playground typecheck",
|
|
20
|
-
"typecheck:all": "pnpm typecheck && pnpm typecheck:playground",
|
|
21
|
-
"lint": "eslint \"src/**/*.ts\" --max-warnings=0",
|
|
22
|
-
"lint:playground": "eslint \"apps/playground/src/**/*.{ts,tsx}\" \"apps/playground/server/**/*.ts\" --max-warnings=0",
|
|
23
|
-
"lint:all": "eslint \"src/**/*.ts\" \"apps/playground/src/**/*.{ts,tsx}\" \"apps/playground/server/**/*.ts\" --max-warnings=0",
|
|
24
|
-
"lint:fix": "eslint \"src/**/*.ts\" \"apps/playground/src/**/*.{ts,tsx}\" \"apps/playground/server/**/*.ts\" --fix",
|
|
25
|
-
"check": "pnpm typecheck:all && pnpm lint:all",
|
|
26
|
-
"dev": "tsc -w -p tsconfig.json",
|
|
27
|
-
"test": "vitest run",
|
|
28
|
-
"prepublishOnly": "npm run build",
|
|
29
|
-
"release:patch": "npm version patch && git push --follow-tags",
|
|
30
|
-
"release:minor": "npm version minor && git push --follow-tags",
|
|
31
|
-
"release:major": "npm version major && git push --follow-tags"
|
|
32
|
-
},
|
|
33
13
|
"keywords": [
|
|
34
14
|
"linkedin",
|
|
35
15
|
"sales-navigator",
|
|
@@ -59,5 +39,24 @@
|
|
|
59
39
|
"husky": "^9.0.11",
|
|
60
40
|
"typescript": "^5.9.3",
|
|
61
41
|
"vitest": "^1.6.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"dev:playground": "pnpm -C apps/playground dev",
|
|
45
|
+
"search": "node scripts/rg-fast.mjs",
|
|
46
|
+
"rg:fast": "node scripts/rg-fast.mjs",
|
|
47
|
+
"build": "tsc -p tsconfig.json",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"typecheck:playground": "pnpm -C apps/playground typecheck",
|
|
50
|
+
"typecheck:all": "pnpm typecheck && pnpm typecheck:playground",
|
|
51
|
+
"lint": "eslint \"src/**/*.ts\" --max-warnings=0",
|
|
52
|
+
"lint:playground": "eslint \"apps/playground/src/**/*.{ts,tsx}\" \"apps/playground/server/**/*.ts\" --max-warnings=0",
|
|
53
|
+
"lint:all": "eslint \"src/**/*.ts\" \"apps/playground/src/**/*.{ts,tsx}\" \"apps/playground/server/**/*.ts\" --max-warnings=0",
|
|
54
|
+
"lint:fix": "eslint \"src/**/*.ts\" \"apps/playground/src/**/*.{ts,tsx}\" \"apps/playground/server/**/*.ts\" --fix",
|
|
55
|
+
"check": "pnpm typecheck:all && pnpm lint:all",
|
|
56
|
+
"dev": "tsc -w -p tsconfig.json",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"release:patch": "npm version patch && git push --follow-tags",
|
|
59
|
+
"release:minor": "npm version minor && git push --follow-tags",
|
|
60
|
+
"release:major": "npm version major && git push --follow-tags"
|
|
62
61
|
}
|
|
63
|
-
}
|
|
62
|
+
}
|