linkedin-secret-sauce 0.4.0 → 0.5.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.
Files changed (39) hide show
  1. package/dist/enrichment/auth/index.d.ts +4 -0
  2. package/dist/enrichment/auth/index.js +12 -0
  3. package/dist/enrichment/auth/smartlead-auth.d.ts +50 -0
  4. package/dist/enrichment/auth/smartlead-auth.js +156 -0
  5. package/dist/enrichment/index.d.ts +44 -0
  6. package/dist/enrichment/index.js +244 -0
  7. package/dist/enrichment/orchestrator.d.ts +31 -0
  8. package/dist/enrichment/orchestrator.js +218 -0
  9. package/dist/enrichment/providers/apollo.d.ts +11 -0
  10. package/dist/enrichment/providers/apollo.js +136 -0
  11. package/dist/enrichment/providers/construct.d.ts +11 -0
  12. package/dist/enrichment/providers/construct.js +107 -0
  13. package/dist/enrichment/providers/dropcontact.d.ts +16 -0
  14. package/dist/enrichment/providers/dropcontact.js +37 -0
  15. package/dist/enrichment/providers/hunter.d.ts +11 -0
  16. package/dist/enrichment/providers/hunter.js +162 -0
  17. package/dist/enrichment/providers/index.d.ts +9 -0
  18. package/dist/enrichment/providers/index.js +18 -0
  19. package/dist/enrichment/providers/ldd.d.ts +11 -0
  20. package/dist/enrichment/providers/ldd.js +110 -0
  21. package/dist/enrichment/providers/smartprospect.d.ts +19 -0
  22. package/dist/enrichment/providers/smartprospect.js +333 -0
  23. package/dist/enrichment/types.d.ts +343 -0
  24. package/dist/enrichment/types.js +31 -0
  25. package/dist/enrichment/utils/disposable-domains.d.ts +24 -0
  26. package/dist/enrichment/utils/disposable-domains.js +1011 -0
  27. package/dist/enrichment/utils/index.d.ts +6 -0
  28. package/dist/enrichment/utils/index.js +22 -0
  29. package/dist/enrichment/utils/personal-domains.d.ts +31 -0
  30. package/dist/enrichment/utils/personal-domains.js +95 -0
  31. package/dist/enrichment/utils/validation.d.ts +42 -0
  32. package/dist/enrichment/utils/validation.js +130 -0
  33. package/dist/enrichment/verification/index.d.ts +4 -0
  34. package/dist/enrichment/verification/index.js +8 -0
  35. package/dist/enrichment/verification/mx.d.ts +16 -0
  36. package/dist/enrichment/verification/mx.js +168 -0
  37. package/dist/index.d.ts +18 -14
  38. package/dist/index.js +26 -1
  39. package/package.json +15 -16
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ /**
3
+ * Hunter.io Provider
4
+ *
5
+ * Hunter.io public API for email finding.
6
+ * Uses email-finder endpoint with name+domain, falls back to domain-search.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createHunterProvider = createHunterProvider;
10
+ 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
+ /**
67
+ * Extract name and domain from candidate
68
+ */
69
+ function extractInputs(candidate) {
70
+ const name = candidate.name || candidate.fullName || candidate.full_name;
71
+ const first = candidate.first ||
72
+ candidate.first_name ||
73
+ candidate.firstName ||
74
+ name?.split?.(" ")?.[0];
75
+ const last = candidate.last ||
76
+ candidate.last_name ||
77
+ candidate.lastName ||
78
+ name?.split?.(" ")?.slice(1).join(" ") ||
79
+ undefined;
80
+ const domain = candidate.domain ||
81
+ candidate.companyDomain ||
82
+ candidate.company_domain ||
83
+ undefined;
84
+ return { first, last, domain };
85
+ }
86
+ /**
87
+ * Create the Hunter provider function
88
+ */
89
+ function createHunterProvider(config) {
90
+ const { apiKey } = config;
91
+ if (!apiKey) {
92
+ // Return a no-op provider if not configured
93
+ const noopProvider = async () => null;
94
+ noopProvider.__name = "hunter";
95
+ return noopProvider;
96
+ }
97
+ async function fetchEmail(candidate) {
98
+ const { first, last, domain } = extractInputs(candidate);
99
+ let url = null;
100
+ // Use email-finder if we have name components
101
+ if (truthy(first) && truthy(last) && truthy(domain)) {
102
+ const qs = new URLSearchParams({
103
+ api_key: String(apiKey),
104
+ domain: String(domain),
105
+ first_name: String(first),
106
+ last_name: String(last),
107
+ });
108
+ url = `${API_BASE}/email-finder?${qs.toString()}`;
109
+ }
110
+ // Fall back to domain-search if only domain available
111
+ else if (truthy(domain)) {
112
+ const qs = new URLSearchParams({
113
+ api_key: String(apiKey),
114
+ domain: String(domain),
115
+ limit: "1",
116
+ });
117
+ url = `${API_BASE}/domain-search?${qs.toString()}`;
118
+ }
119
+ else {
120
+ return null; // Can't search without domain
121
+ }
122
+ try {
123
+ const json = await requestWithRetry(url, 1, 100);
124
+ // Parse email-finder response shape
125
+ const ef = (json && (json.data || json.result));
126
+ if (ef && (ef.email || ef.score !== undefined)) {
127
+ const email = ef.email ?? null;
128
+ const score = typeof ef.score === "number"
129
+ ? ef.score
130
+ : Number(ef.score ?? 0) || undefined;
131
+ const verified = mapVerified(ef?.verification?.status ?? ef?.status);
132
+ if (!email)
133
+ return null;
134
+ return { email, verified, score };
135
+ }
136
+ // Parse domain-search response shape
137
+ const ds = (json &&
138
+ json.data &&
139
+ Array.isArray(json.data.emails)
140
+ ? json.data.emails
141
+ : null);
142
+ if (ds && ds.length > 0) {
143
+ const firstHit = ds[0];
144
+ const email = firstHit?.value || firstHit?.email || null;
145
+ const score = typeof firstHit?.confidence === "number"
146
+ ? firstHit.confidence
147
+ : Number(firstHit?.confidence ?? 0) || undefined;
148
+ const verified = mapVerified(firstHit?.verification?.status ?? firstHit?.status);
149
+ if (!email)
150
+ return null;
151
+ return { email, verified, score };
152
+ }
153
+ return null;
154
+ }
155
+ catch {
156
+ return null;
157
+ }
158
+ }
159
+ // Mark provider name for orchestrator
160
+ fetchEmail.__name = "hunter";
161
+ return fetchEmail;
162
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Email Enrichment Providers
3
+ */
4
+ export { createConstructProvider } from './construct';
5
+ export { createLddProvider } from './ldd';
6
+ export { createSmartProspectProvider } from './smartprospect';
7
+ export { createHunterProvider } from './hunter';
8
+ export { createApolloProvider } from './apollo';
9
+ export { createDropcontactProvider } from './dropcontact';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ /**
3
+ * Email Enrichment Providers
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createDropcontactProvider = exports.createApolloProvider = exports.createHunterProvider = exports.createSmartProspectProvider = exports.createLddProvider = exports.createConstructProvider = void 0;
7
+ var construct_1 = require("./construct");
8
+ Object.defineProperty(exports, "createConstructProvider", { enumerable: true, get: function () { return construct_1.createConstructProvider; } });
9
+ var ldd_1 = require("./ldd");
10
+ Object.defineProperty(exports, "createLddProvider", { enumerable: true, get: function () { return ldd_1.createLddProvider; } });
11
+ var smartprospect_1 = require("./smartprospect");
12
+ Object.defineProperty(exports, "createSmartProspectProvider", { enumerable: true, get: function () { return smartprospect_1.createSmartProspectProvider; } });
13
+ var hunter_1 = require("./hunter");
14
+ Object.defineProperty(exports, "createHunterProvider", { enumerable: true, get: function () { return hunter_1.createHunterProvider; } });
15
+ var apollo_1 = require("./apollo");
16
+ Object.defineProperty(exports, "createApolloProvider", { enumerable: true, get: function () { return apollo_1.createApolloProvider; } });
17
+ var dropcontact_1 = require("./dropcontact");
18
+ Object.defineProperty(exports, "createDropcontactProvider", { enumerable: true, get: function () { return dropcontact_1.createDropcontactProvider; } });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * LinkedIn Data Dump (LDD) Provider
3
+ *
4
+ * YOUR private database of ~500M scraped LinkedIn profiles with emails.
5
+ * This is FREE and unlimited - it's your own service.
6
+ */
7
+ import type { EnrichmentCandidate, ProviderResult, LddConfig } from "../types";
8
+ /**
9
+ * Create the LDD provider function
10
+ */
11
+ export declare function createLddProvider(config: LddConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ /**
3
+ * LinkedIn Data Dump (LDD) Provider
4
+ *
5
+ * YOUR private database of ~500M scraped LinkedIn profiles with emails.
6
+ * This is FREE and unlimited - it's your own service.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createLddProvider = createLddProvider;
10
+ const validation_1 = require("../utils/validation");
11
+ /**
12
+ * Delay helper for retry logic
13
+ */
14
+ async function delay(ms) {
15
+ return new Promise((r) => setTimeout(r, ms));
16
+ }
17
+ /**
18
+ * HTTP request with retry on 429 rate limit
19
+ */
20
+ async function requestWithRetry(url, token, retries = 1, backoffMs = 200) {
21
+ let lastErr;
22
+ for (let i = 0; i <= retries; i++) {
23
+ try {
24
+ const res = await fetch(url, {
25
+ method: "GET",
26
+ headers: {
27
+ Authorization: `Bearer ${token}`,
28
+ "Content-Type": "application/json",
29
+ },
30
+ });
31
+ // Retry on rate limit
32
+ if (res?.status === 429 && i < retries) {
33
+ await delay(backoffMs * Math.pow(2, i));
34
+ continue;
35
+ }
36
+ return res;
37
+ }
38
+ catch (err) {
39
+ lastErr = err;
40
+ if (i < retries) {
41
+ await delay(backoffMs * Math.pow(2, i));
42
+ continue;
43
+ }
44
+ }
45
+ }
46
+ throw lastErr ?? new Error("ldd_http_error");
47
+ }
48
+ /**
49
+ * Extract LinkedIn username from candidate
50
+ */
51
+ function extractUsername(candidate) {
52
+ // Direct username
53
+ const directUsername = candidate.linkedinUsername || candidate.linkedin_username;
54
+ if (directUsername)
55
+ return directUsername;
56
+ // Extract from URL
57
+ const url = candidate.linkedinUrl || candidate.linkedin_url;
58
+ if (url) {
59
+ const extracted = (0, validation_1.extractLinkedInUsername)(url);
60
+ if (extracted)
61
+ return extracted;
62
+ }
63
+ return null;
64
+ }
65
+ /**
66
+ * Create the LDD provider function
67
+ */
68
+ function createLddProvider(config) {
69
+ const { apiUrl, apiToken } = config;
70
+ if (!apiUrl || !apiToken) {
71
+ // Return a no-op provider if not configured
72
+ const noopProvider = async () => null;
73
+ noopProvider.__name = "ldd";
74
+ return noopProvider;
75
+ }
76
+ async function fetchEmail(candidate) {
77
+ const username = extractUsername(candidate);
78
+ if (!username) {
79
+ return null;
80
+ }
81
+ try {
82
+ const endpoint = `${apiUrl}/api/v1/profiles/by-username/${encodeURIComponent(username)}`;
83
+ const response = await requestWithRetry(endpoint, apiToken, 1, 100);
84
+ if (!response.ok) {
85
+ return null;
86
+ }
87
+ const data = (await response.json());
88
+ if (!data.success || !data.data) {
89
+ return null;
90
+ }
91
+ // Find first valid email
92
+ const emails = data.data.emails || [];
93
+ const validEmail = emails.find((e) => e.email_address && e.email_address.includes("@"));
94
+ if (!validEmail) {
95
+ return null;
96
+ }
97
+ return {
98
+ email: validEmail.email_address,
99
+ verified: true, // LDD data is pre-verified
100
+ score: 90, // High confidence from your own database
101
+ };
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ }
107
+ // Mark provider name for orchestrator
108
+ fetchEmail.__name = "ldd";
109
+ return fetchEmail;
110
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * SmartProspect/Smartlead Provider
3
+ *
4
+ * Smartlead's prospect database - a REVERSE-ENGINEERED private API.
5
+ * Two-phase process: search (free) then fetch (costs credits).
6
+ *
7
+ * Supports two authentication methods:
8
+ * 1. Direct token: Pass `apiToken` directly (for pre-authenticated scenarios)
9
+ * 2. Credentials: Pass `email` and `password` for automatic login with token caching
10
+ */
11
+ import type { EnrichmentCandidate, ProviderResult, SmartProspectConfig } from "../types";
12
+ /**
13
+ * Create the SmartProspect provider function
14
+ *
15
+ * Supports two auth methods:
16
+ * 1. Direct token: Pass `apiToken` in config
17
+ * 2. Credentials: Pass `email` and `password` in config for auto-login
18
+ */
19
+ export declare function createSmartProspectProvider(config: SmartProspectConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
@@ -0,0 +1,333 @@
1
+ "use strict";
2
+ /**
3
+ * SmartProspect/Smartlead Provider
4
+ *
5
+ * Smartlead's prospect database - a REVERSE-ENGINEERED private API.
6
+ * Two-phase process: search (free) then fetch (costs credits).
7
+ *
8
+ * Supports two authentication methods:
9
+ * 1. Direct token: Pass `apiToken` directly (for pre-authenticated scenarios)
10
+ * 2. Credentials: Pass `email` and `password` for automatic login with token caching
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.createSmartProspectProvider = createSmartProspectProvider;
14
+ const smartlead_auth_1 = require("../auth/smartlead-auth");
15
+ const DEFAULT_API_URL = "https://prospect-api.smartlead.ai/api/search-email-leads";
16
+ /**
17
+ * Delay helper for retry logic
18
+ */
19
+ async function delay(ms) {
20
+ return new Promise((r) => setTimeout(r, ms));
21
+ }
22
+ /**
23
+ * HTTP request with retry on 429 rate limit
24
+ */
25
+ async function requestWithRetry(url, options, retries = 2, backoffMs = 300) {
26
+ let lastErr;
27
+ for (let i = 0; i <= retries; i++) {
28
+ try {
29
+ const res = await fetch(url, options);
30
+ // Retry on rate limit
31
+ if (res.status === 429 && i < retries) {
32
+ await delay(backoffMs * Math.pow(2, i));
33
+ continue;
34
+ }
35
+ if (!res.ok) {
36
+ const errorText = await res.text().catch(() => "");
37
+ throw new Error(`SmartProspect API error: ${res.status} - ${errorText}`);
38
+ }
39
+ return (await res.json());
40
+ }
41
+ catch (err) {
42
+ lastErr = err;
43
+ if (i < retries) {
44
+ await delay(backoffMs * Math.pow(2, i));
45
+ continue;
46
+ }
47
+ }
48
+ }
49
+ throw lastErr ?? new Error("smartprospect_http_error");
50
+ }
51
+ /**
52
+ * Calculate match score between search input and contact result
53
+ */
54
+ function calculateMatchScore(contact, searchName, company, title) {
55
+ let score = 0;
56
+ // Name match scoring (0-50 points)
57
+ const contactName = contact.fullName?.toLowerCase() || "";
58
+ const searchNameLower = searchName.toLowerCase();
59
+ if (contactName === searchNameLower) {
60
+ score += 50; // Exact match
61
+ }
62
+ else if (contactName.includes(searchNameLower) ||
63
+ searchNameLower.includes(contactName)) {
64
+ score += 30; // Partial match
65
+ }
66
+ // Company match scoring (0-30 points)
67
+ if (company) {
68
+ const contactCompany = contact.company?.name?.toLowerCase() || "";
69
+ const searchCompany = company.toLowerCase();
70
+ if (contactCompany === searchCompany) {
71
+ score += 30; // Exact match
72
+ }
73
+ else if (contactCompany.includes(searchCompany) ||
74
+ searchCompany.includes(contactCompany)) {
75
+ score += 15; // Partial match
76
+ }
77
+ }
78
+ // Title match scoring (0-10 points)
79
+ if (title) {
80
+ const contactTitle = contact.title?.toLowerCase() || "";
81
+ const searchTitle = title.toLowerCase();
82
+ if (contactTitle.includes(searchTitle) ||
83
+ searchTitle.includes(contactTitle)) {
84
+ score += 10;
85
+ }
86
+ }
87
+ // Deliverability score bonus (0-10 points)
88
+ if (contact.emailDeliverability > 0) {
89
+ score += contact.emailDeliverability * 10;
90
+ }
91
+ return score;
92
+ }
93
+ /**
94
+ * Extract name components from candidate
95
+ */
96
+ function extractNames(candidate) {
97
+ const firstName = candidate.firstName ||
98
+ candidate.first_name ||
99
+ candidate.first ||
100
+ candidate.name?.split(" ")?.[0] ||
101
+ "";
102
+ const lastName = candidate.lastName ||
103
+ candidate.last_name ||
104
+ candidate.last ||
105
+ candidate.name?.split(" ")?.slice(1).join(" ") ||
106
+ "";
107
+ return { firstName, lastName };
108
+ }
109
+ /**
110
+ * Create the SmartProspect provider function
111
+ *
112
+ * Supports two auth methods:
113
+ * 1. Direct token: Pass `apiToken` in config
114
+ * 2. Credentials: Pass `email` and `password` in config for auto-login
115
+ */
116
+ function createSmartProspectProvider(config) {
117
+ const { apiToken, email, password, apiUrl = DEFAULT_API_URL, loginUrl, eagerInit = true, } = config;
118
+ // Check if we have valid auth config
119
+ const hasDirectToken = !!apiToken;
120
+ const hasCredentials = !!email && !!password;
121
+ if (!hasDirectToken && !hasCredentials) {
122
+ // Return a no-op provider if not configured
123
+ const noopProvider = async () => null;
124
+ noopProvider.__name = "smartprospect";
125
+ return noopProvider;
126
+ }
127
+ /**
128
+ * Get the current auth token (either direct or via login)
129
+ */
130
+ async function getAuthToken() {
131
+ if (hasDirectToken) {
132
+ return apiToken;
133
+ }
134
+ // Use credentials-based auth with token caching
135
+ return (0, smartlead_auth_1.getSmartLeadToken)({
136
+ credentials: { email: email, password: password },
137
+ loginUrl,
138
+ });
139
+ }
140
+ // Eager initialization: pre-fetch token so it's ready for first request
141
+ if (eagerInit && hasCredentials) {
142
+ // Fire and forget - don't block provider creation
143
+ getAuthToken().catch(() => {
144
+ // Silently ignore errors during eager init - will retry on first request
145
+ });
146
+ }
147
+ /**
148
+ * Handle 401 errors by clearing cached token
149
+ */
150
+ async function handleAuthError() {
151
+ if (hasCredentials) {
152
+ (0, smartlead_auth_1.clearSmartLeadToken)(email);
153
+ }
154
+ }
155
+ /**
156
+ * Search for contacts matching filters (FREE - no credits used)
157
+ */
158
+ async function searchContacts(filters) {
159
+ const makeRequest = async (token) => {
160
+ return requestWithRetry(`${apiUrl}/search-contacts`, {
161
+ method: "POST",
162
+ headers: {
163
+ Authorization: `Bearer ${token}`,
164
+ "Content-Type": "application/json",
165
+ Accept: "application/json",
166
+ },
167
+ body: JSON.stringify({
168
+ ...filters,
169
+ dontDisplayOwnedContact: filters.dontDisplayOwnedContact ?? true,
170
+ limit: filters.limit ?? 10,
171
+ titleExactMatch: filters.titleExactMatch ?? false,
172
+ }),
173
+ });
174
+ };
175
+ try {
176
+ const token = await getAuthToken();
177
+ return await makeRequest(token);
178
+ }
179
+ catch (err) {
180
+ // On 401, clear token and retry once with fresh token
181
+ if (err instanceof Error &&
182
+ err.message.includes("401") &&
183
+ hasCredentials) {
184
+ await handleAuthError();
185
+ try {
186
+ const freshToken = await getAuthToken();
187
+ return await makeRequest(freshToken);
188
+ }
189
+ catch {
190
+ // Retry failed too
191
+ }
192
+ }
193
+ return {
194
+ success: false,
195
+ message: "Search failed",
196
+ data: { list: [], total_count: 0 },
197
+ };
198
+ }
199
+ }
200
+ /**
201
+ * Fetch/enrich emails for specific contact IDs (COSTS CREDITS)
202
+ */
203
+ async function fetchContacts(contactIds) {
204
+ const makeRequest = async (token) => {
205
+ return requestWithRetry(`${apiUrl}/fetch-contacts`, {
206
+ method: "POST",
207
+ headers: {
208
+ Authorization: `Bearer ${token}`,
209
+ "Content-Type": "application/json",
210
+ Accept: "application/json",
211
+ },
212
+ body: JSON.stringify({ contactIds }),
213
+ });
214
+ };
215
+ try {
216
+ const token = await getAuthToken();
217
+ return await makeRequest(token);
218
+ }
219
+ catch (err) {
220
+ // On 401, clear token and retry once with fresh token
221
+ if (err instanceof Error &&
222
+ err.message.includes("401") &&
223
+ hasCredentials) {
224
+ await handleAuthError();
225
+ try {
226
+ const freshToken = await getAuthToken();
227
+ return await makeRequest(freshToken);
228
+ }
229
+ catch {
230
+ // Retry failed too
231
+ }
232
+ }
233
+ return {
234
+ success: false,
235
+ message: "Fetch failed",
236
+ data: {
237
+ list: [],
238
+ total_count: 0,
239
+ metrics: {
240
+ totalContacts: 0,
241
+ totalEmails: 0,
242
+ noEmailFound: 0,
243
+ invalidEmails: 0,
244
+ catchAllEmails: 0,
245
+ verifiedEmails: 0,
246
+ completed: 0,
247
+ },
248
+ leads_found: 0,
249
+ email_fetched: 0,
250
+ verification_status_list: [],
251
+ },
252
+ };
253
+ }
254
+ }
255
+ async function fetchEmail(candidate) {
256
+ const { firstName, lastName } = extractNames(candidate);
257
+ if (!firstName) {
258
+ return null; // Minimum requirement
259
+ }
260
+ const fullName = `${firstName} ${lastName}`.trim();
261
+ const company = candidate.company ||
262
+ candidate.currentCompany ||
263
+ candidate.organization ||
264
+ undefined;
265
+ const title = candidate.title ||
266
+ candidate.currentRole ||
267
+ candidate.current_role ||
268
+ undefined;
269
+ const domain = candidate.domain ||
270
+ candidate.companyDomain ||
271
+ candidate.company_domain ||
272
+ undefined;
273
+ // Build search filters
274
+ const filters = {
275
+ name: [fullName],
276
+ limit: 5, // Get top 5 to find best match
277
+ };
278
+ if (company) {
279
+ filters.company = [company];
280
+ }
281
+ if (domain) {
282
+ filters.domain = [domain];
283
+ }
284
+ // Step 1: Search for contacts (FREE)
285
+ const searchResult = await searchContacts(filters);
286
+ if (!searchResult.success || searchResult.data.list.length === 0) {
287
+ return null;
288
+ }
289
+ // Find best match by scoring
290
+ const matches = searchResult.data.list;
291
+ let bestMatch = null;
292
+ let bestScore = 0;
293
+ for (const contact of matches) {
294
+ const score = calculateMatchScore(contact, fullName, company, title);
295
+ if (score > bestScore) {
296
+ bestScore = score;
297
+ bestMatch = contact;
298
+ }
299
+ }
300
+ if (!bestMatch) {
301
+ return null;
302
+ }
303
+ // Step 2: Fetch email for best match (COSTS CREDITS)
304
+ const fetchResult = await fetchContacts([bestMatch.id]);
305
+ if (!fetchResult.success || fetchResult.data.list.length === 0) {
306
+ return null;
307
+ }
308
+ const enrichedContact = fetchResult.data.list[0];
309
+ const email = enrichedContact?.email;
310
+ if (!email) {
311
+ return null;
312
+ }
313
+ // Calculate verification confidence
314
+ const isVerified = enrichedContact.verificationStatus === "valid";
315
+ const isCatchAll = enrichedContact.verificationStatus === "catch_all";
316
+ const deliverability = enrichedContact.emailDeliverability || 0;
317
+ let confidence = deliverability * 100;
318
+ if (isVerified) {
319
+ confidence = Math.max(confidence, 90);
320
+ }
321
+ else if (isCatchAll) {
322
+ confidence = Math.min(confidence, 75);
323
+ }
324
+ return {
325
+ email,
326
+ verified: isVerified || isCatchAll, // Accept catch-all as semi-verified
327
+ score: confidence,
328
+ };
329
+ }
330
+ // Mark provider name for orchestrator
331
+ fetchEmail.__name = "smartprospect";
332
+ return fetchEmail;
333
+ }