linkedin-secret-sauce 0.7.2 → 0.9.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.
|
@@ -44,4 +44,4 @@ 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";
|
|
46
46
|
export { getSmartLeadToken, getSmartLeadUser, clearSmartLeadToken, clearAllSmartLeadTokens, getSmartLeadTokenCacheStats, enableFileCache, disableFileCache, isFileCacheEnabled, clearFileCache, type SmartLeadCredentials, type SmartLeadAuthConfig, type SmartLeadUser, type SmartLeadLoginResponse, } from "./auth";
|
|
47
|
-
export { calculateMatchConfidence, classifyMatchQuality, findBestMatch, matchContacts, buildSmartProspectFiltersFromLinkedIn, parseLinkedInSearchResponse, enrichLinkedInContact, enrichLinkedInContactsBatch, createLinkedInEnricher, type LinkedInContact, type MatchResult, type MatchOptions, type LinkedInEnrichmentResult, type LinkedInEnrichmentOptions, } from "./matching";
|
|
47
|
+
export { calculateMatchConfidence, classifyMatchQuality, findBestMatch, matchContacts, buildSmartProspectFiltersFromLinkedIn, parseLinkedInSearchResponse, enrichLinkedInContact, enrichLinkedInContactsBatch, createLinkedInEnricher, getEmailsForLinkedInContact, getEmailsForLinkedInContactsBatch, type LinkedInContact, type MatchResult, type MatchOptions, type LinkedInEnrichmentResult, type LinkedInEnrichmentOptions, type EmailSource, type EmailResult, type GetEmailsResult, type GetEmailsConfig, type GetEmailsOptions, } from "./matching";
|
package/dist/enrichment/index.js
CHANGED
|
@@ -42,7 +42,7 @@ 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.createLinkedInEnricher = exports.enrichLinkedInContactsBatch = 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.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;
|
|
45
|
+
exports.getEmailsForLinkedInContactsBatch = exports.getEmailsForLinkedInContact = exports.createLinkedInEnricher = exports.enrichLinkedInContactsBatch = 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.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
46
|
exports.createEnrichmentClient = createEnrichmentClient;
|
|
47
47
|
const orchestrator_1 = require("./orchestrator");
|
|
48
48
|
const construct_1 = require("./providers/construct");
|
|
@@ -287,3 +287,6 @@ Object.defineProperty(exports, "parseLinkedInSearchResponse", { enumerable: true
|
|
|
287
287
|
Object.defineProperty(exports, "enrichLinkedInContact", { enumerable: true, get: function () { return matching_1.enrichLinkedInContact; } });
|
|
288
288
|
Object.defineProperty(exports, "enrichLinkedInContactsBatch", { enumerable: true, get: function () { return matching_1.enrichLinkedInContactsBatch; } });
|
|
289
289
|
Object.defineProperty(exports, "createLinkedInEnricher", { enumerable: true, get: function () { return matching_1.createLinkedInEnricher; } });
|
|
290
|
+
// UNIFIED email lookup (recommended for most use cases)
|
|
291
|
+
Object.defineProperty(exports, "getEmailsForLinkedInContact", { enumerable: true, get: function () { return matching_1.getEmailsForLinkedInContact; } });
|
|
292
|
+
Object.defineProperty(exports, "getEmailsForLinkedInContactsBatch", { enumerable: true, get: function () { return matching_1.getEmailsForLinkedInContactsBatch; } });
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Matches contacts between LinkedIn Sales Navigator and SmartProspect
|
|
5
5
|
* to find the same person across both platforms.
|
|
6
6
|
*/
|
|
7
|
-
import type { SmartProspectContact, SmartProspectConfig, SmartProspectFetchResponse } from './types';
|
|
7
|
+
import type { SmartProspectContact, SmartProspectConfig, SmartProspectFetchResponse, LddConfig } from './types';
|
|
8
8
|
import { type SmartProspectClient } from './providers/smartprospect';
|
|
9
9
|
/**
|
|
10
10
|
* LinkedIn Sales Navigator contact (simplified from API response)
|
|
@@ -239,3 +239,140 @@ export declare function createLinkedInEnricher(smartProspectConfig: SmartProspec
|
|
|
239
239
|
/** Fetch email for a specific SmartProspect contact ID (COSTS CREDITS) */
|
|
240
240
|
fetchEmail: (contactId: string) => Promise<SmartProspectFetchResponse>;
|
|
241
241
|
} | null;
|
|
242
|
+
/**
|
|
243
|
+
* Email source - where the email was found
|
|
244
|
+
*/
|
|
245
|
+
export type EmailSource = 'ldd' | 'smartprospect' | 'pattern' | 'hunter' | 'apollo';
|
|
246
|
+
/**
|
|
247
|
+
* Email result from unified lookup
|
|
248
|
+
*/
|
|
249
|
+
export interface EmailResult {
|
|
250
|
+
/** The email address */
|
|
251
|
+
email: string;
|
|
252
|
+
/** Which provider found this email */
|
|
253
|
+
source: EmailSource;
|
|
254
|
+
/** Confidence score (0-100) */
|
|
255
|
+
confidence: number;
|
|
256
|
+
/** Email type classification */
|
|
257
|
+
type: 'business' | 'personal' | 'unknown';
|
|
258
|
+
/** Whether the email was verified */
|
|
259
|
+
verified: boolean;
|
|
260
|
+
/** Email deliverability score (0-1) for SmartProspect emails */
|
|
261
|
+
deliverability?: number;
|
|
262
|
+
/** Whether this is a catch-all domain */
|
|
263
|
+
isCatchAll?: boolean;
|
|
264
|
+
/** Additional metadata */
|
|
265
|
+
metadata?: Record<string, unknown>;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Result from unified email lookup
|
|
269
|
+
*/
|
|
270
|
+
export interface GetEmailsResult {
|
|
271
|
+
/** Whether lookup was successful (found at least one email) */
|
|
272
|
+
success: boolean;
|
|
273
|
+
/** All found emails, sorted by confidence (highest first) */
|
|
274
|
+
emails: EmailResult[];
|
|
275
|
+
/** Numeric LinkedIn ID extracted from objectUrn */
|
|
276
|
+
numericLinkedInId: string | null;
|
|
277
|
+
/** Which providers were queried */
|
|
278
|
+
providersQueried: EmailSource[];
|
|
279
|
+
/** Error message if any provider failed */
|
|
280
|
+
errors?: string[];
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Configuration for unified email lookup
|
|
284
|
+
*/
|
|
285
|
+
export interface GetEmailsConfig {
|
|
286
|
+
/** LDD configuration (FREE - your ~500M database) */
|
|
287
|
+
ldd?: LddConfig;
|
|
288
|
+
/** SmartProspect configuration (FREE for FlexIQ - already paying monthly) */
|
|
289
|
+
smartprospect?: SmartProspectConfig;
|
|
290
|
+
/** Company domain for email pattern guessing (optional) */
|
|
291
|
+
companyDomain?: string;
|
|
292
|
+
/** Hunter configuration (PAID - last resort) */
|
|
293
|
+
hunter?: {
|
|
294
|
+
apiKey: string;
|
|
295
|
+
};
|
|
296
|
+
/** Apollo configuration (PAID - last resort) */
|
|
297
|
+
apollo?: {
|
|
298
|
+
apiKey: string;
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Options for unified email lookup
|
|
303
|
+
*/
|
|
304
|
+
export interface GetEmailsOptions {
|
|
305
|
+
/** Skip LDD lookup (default: false) */
|
|
306
|
+
skipLdd?: boolean;
|
|
307
|
+
/** Skip SmartProspect lookup (default: false) */
|
|
308
|
+
skipSmartProspect?: boolean;
|
|
309
|
+
/** Skip email pattern guessing (default: false) */
|
|
310
|
+
skipPatternGuessing?: boolean;
|
|
311
|
+
/** Skip paid providers Hunter/Apollo (default: false) */
|
|
312
|
+
skipPaidProviders?: boolean;
|
|
313
|
+
/** Minimum match confidence for SmartProspect (default: 60) */
|
|
314
|
+
minMatchConfidence?: number;
|
|
315
|
+
/** Minimum confidence to skip paid providers (default: 80) */
|
|
316
|
+
paidProviderThreshold?: number;
|
|
317
|
+
/** Include company in SmartProspect search (default: true) */
|
|
318
|
+
includeCompany?: boolean;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Get emails for a LinkedIn contact - UNIFIED FUNCTION
|
|
322
|
+
*
|
|
323
|
+
* Strategy (optimized for FlexIQ):
|
|
324
|
+
* 1. Query LDD + SmartProspect in PARALLEL (both FREE for you)
|
|
325
|
+
* 2. Try email pattern guessing with MX verification (FREE)
|
|
326
|
+
* 3. Only use Hunter/Apollo if confidence is below threshold (PAID - last resort)
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```ts
|
|
330
|
+
* import { getEmailsForLinkedInContact } from 'linkedin-secret-sauce';
|
|
331
|
+
*
|
|
332
|
+
* // From your LinkedIn Sales Nav search result
|
|
333
|
+
* const contact = {
|
|
334
|
+
* objectUrn: 'urn:li:member:307567', // Stable ID for LDD
|
|
335
|
+
* firstName: 'Jim',
|
|
336
|
+
* lastName: 'DeMaio',
|
|
337
|
+
* currentPositions: [{
|
|
338
|
+
* companyName: 'Acme Corp',
|
|
339
|
+
* companyUrnResolutionResult: { website: 'acme.com' }
|
|
340
|
+
* }]
|
|
341
|
+
* };
|
|
342
|
+
*
|
|
343
|
+
* const result = await getEmailsForLinkedInContact(contact, {
|
|
344
|
+
* ldd: {
|
|
345
|
+
* apiUrl: process.env.LDD_API_URL,
|
|
346
|
+
* apiToken: process.env.LDD_API_TOKEN,
|
|
347
|
+
* },
|
|
348
|
+
* smartprospect: {
|
|
349
|
+
* email: process.env.SMARTLEAD_EMAIL,
|
|
350
|
+
* password: process.env.SMARTLEAD_PASSWORD,
|
|
351
|
+
* },
|
|
352
|
+
* // companyDomain extracted automatically from currentPositions
|
|
353
|
+
* });
|
|
354
|
+
*
|
|
355
|
+
* if (result.success) {
|
|
356
|
+
* console.log('Providers queried:', result.providersQueried);
|
|
357
|
+
* console.log('Found emails:', result.emails);
|
|
358
|
+
* // [
|
|
359
|
+
* // { email: 'jim@acme.com', source: 'ldd', confidence: 95, type: 'business' },
|
|
360
|
+
* // { email: 'j.demaio@acme.com', source: 'smartprospect', confidence: 85, type: 'business' },
|
|
361
|
+
* // ]
|
|
362
|
+
* }
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
export declare function getEmailsForLinkedInContact(contact: LinkedInContact, config: GetEmailsConfig, options?: GetEmailsOptions): Promise<GetEmailsResult>;
|
|
366
|
+
/**
|
|
367
|
+
* Get emails for multiple LinkedIn contacts in batch
|
|
368
|
+
*
|
|
369
|
+
* Processes contacts with controlled concurrency to balance speed and rate limits.
|
|
370
|
+
*/
|
|
371
|
+
export declare function getEmailsForLinkedInContactsBatch(contacts: LinkedInContact[], config: GetEmailsConfig, options?: GetEmailsOptions & {
|
|
372
|
+
/** Delay between requests in ms (default: 100) */
|
|
373
|
+
delayMs?: number;
|
|
374
|
+
/** Max concurrent lookups (default: 3) */
|
|
375
|
+
concurrency?: number;
|
|
376
|
+
/** Callback for progress updates */
|
|
377
|
+
onProgress?: (completed: number, total: number, result: GetEmailsResult) => void;
|
|
378
|
+
}): Promise<GetEmailsResult[]>;
|
|
@@ -5,6 +5,39 @@
|
|
|
5
5
|
* Matches contacts between LinkedIn Sales Navigator and SmartProspect
|
|
6
6
|
* to find the same person across both platforms.
|
|
7
7
|
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
8
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
42
|
exports.calculateMatchConfidence = calculateMatchConfidence;
|
|
10
43
|
exports.classifyMatchQuality = classifyMatchQuality;
|
|
@@ -15,6 +48,8 @@ exports.parseLinkedInSearchResponse = parseLinkedInSearchResponse;
|
|
|
15
48
|
exports.enrichLinkedInContact = enrichLinkedInContact;
|
|
16
49
|
exports.enrichLinkedInContactsBatch = enrichLinkedInContactsBatch;
|
|
17
50
|
exports.createLinkedInEnricher = createLinkedInEnricher;
|
|
51
|
+
exports.getEmailsForLinkedInContact = getEmailsForLinkedInContact;
|
|
52
|
+
exports.getEmailsForLinkedInContactsBatch = getEmailsForLinkedInContactsBatch;
|
|
18
53
|
const smartprospect_1 = require("./providers/smartprospect");
|
|
19
54
|
const ldd_1 = require("./providers/ldd");
|
|
20
55
|
// =============================================================================
|
|
@@ -624,3 +659,320 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
|
624
659
|
},
|
|
625
660
|
};
|
|
626
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* Get emails for a LinkedIn contact - UNIFIED FUNCTION
|
|
664
|
+
*
|
|
665
|
+
* Strategy (optimized for FlexIQ):
|
|
666
|
+
* 1. Query LDD + SmartProspect in PARALLEL (both FREE for you)
|
|
667
|
+
* 2. Try email pattern guessing with MX verification (FREE)
|
|
668
|
+
* 3. Only use Hunter/Apollo if confidence is below threshold (PAID - last resort)
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* ```ts
|
|
672
|
+
* import { getEmailsForLinkedInContact } from 'linkedin-secret-sauce';
|
|
673
|
+
*
|
|
674
|
+
* // From your LinkedIn Sales Nav search result
|
|
675
|
+
* const contact = {
|
|
676
|
+
* objectUrn: 'urn:li:member:307567', // Stable ID for LDD
|
|
677
|
+
* firstName: 'Jim',
|
|
678
|
+
* lastName: 'DeMaio',
|
|
679
|
+
* currentPositions: [{
|
|
680
|
+
* companyName: 'Acme Corp',
|
|
681
|
+
* companyUrnResolutionResult: { website: 'acme.com' }
|
|
682
|
+
* }]
|
|
683
|
+
* };
|
|
684
|
+
*
|
|
685
|
+
* const result = await getEmailsForLinkedInContact(contact, {
|
|
686
|
+
* ldd: {
|
|
687
|
+
* apiUrl: process.env.LDD_API_URL,
|
|
688
|
+
* apiToken: process.env.LDD_API_TOKEN,
|
|
689
|
+
* },
|
|
690
|
+
* smartprospect: {
|
|
691
|
+
* email: process.env.SMARTLEAD_EMAIL,
|
|
692
|
+
* password: process.env.SMARTLEAD_PASSWORD,
|
|
693
|
+
* },
|
|
694
|
+
* // companyDomain extracted automatically from currentPositions
|
|
695
|
+
* });
|
|
696
|
+
*
|
|
697
|
+
* if (result.success) {
|
|
698
|
+
* console.log('Providers queried:', result.providersQueried);
|
|
699
|
+
* console.log('Found emails:', result.emails);
|
|
700
|
+
* // [
|
|
701
|
+
* // { email: 'jim@acme.com', source: 'ldd', confidence: 95, type: 'business' },
|
|
702
|
+
* // { email: 'j.demaio@acme.com', source: 'smartprospect', confidence: 85, type: 'business' },
|
|
703
|
+
* // ]
|
|
704
|
+
* }
|
|
705
|
+
* ```
|
|
706
|
+
*/
|
|
707
|
+
async function getEmailsForLinkedInContact(contact, config, options = {}) {
|
|
708
|
+
const { skipLdd = false, skipSmartProspect = false, skipPatternGuessing = false, skipPaidProviders = false, minMatchConfidence = 60, paidProviderThreshold = 80, includeCompany = true, } = options;
|
|
709
|
+
// Extract numeric ID from objectUrn
|
|
710
|
+
const numericLinkedInId = (0, ldd_1.extractNumericLinkedInId)(contact.objectUrn);
|
|
711
|
+
// Extract company domain from contact
|
|
712
|
+
const companyDomain = config.companyDomain || extractCompanyDomain(contact);
|
|
713
|
+
const result = {
|
|
714
|
+
success: false,
|
|
715
|
+
emails: [],
|
|
716
|
+
numericLinkedInId,
|
|
717
|
+
providersQueried: [],
|
|
718
|
+
errors: [],
|
|
719
|
+
};
|
|
720
|
+
// Track seen emails to deduplicate
|
|
721
|
+
const seenEmails = new Set();
|
|
722
|
+
const addEmail = (emailResult) => {
|
|
723
|
+
const lower = emailResult.email.toLowerCase();
|
|
724
|
+
if (!seenEmails.has(lower)) {
|
|
725
|
+
seenEmails.add(lower);
|
|
726
|
+
result.emails.push(emailResult);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
// ==========================================================================
|
|
730
|
+
// Phase 1: FREE providers in PARALLEL (LDD + SmartProspect)
|
|
731
|
+
// ==========================================================================
|
|
732
|
+
const freeProviderPromises = [];
|
|
733
|
+
// LDD lookup (FREE)
|
|
734
|
+
if (!skipLdd && config.ldd?.apiUrl && config.ldd?.apiToken) {
|
|
735
|
+
result.providersQueried.push('ldd');
|
|
736
|
+
freeProviderPromises.push(queryLdd(contact, config.ldd, numericLinkedInId, addEmail, result));
|
|
737
|
+
}
|
|
738
|
+
// SmartProspect lookup (FREE for FlexIQ)
|
|
739
|
+
if (!skipSmartProspect && config.smartprospect) {
|
|
740
|
+
result.providersQueried.push('smartprospect');
|
|
741
|
+
freeProviderPromises.push(querySmartProspect(contact, config.smartprospect, minMatchConfidence, includeCompany, addEmail, result));
|
|
742
|
+
}
|
|
743
|
+
// Wait for both free providers
|
|
744
|
+
await Promise.all(freeProviderPromises);
|
|
745
|
+
// Check if we have good enough results
|
|
746
|
+
const bestConfidence = result.emails.length > 0
|
|
747
|
+
? Math.max(...result.emails.map(e => e.confidence))
|
|
748
|
+
: 0;
|
|
749
|
+
// ==========================================================================
|
|
750
|
+
// Phase 2: Email pattern guessing with MX verification (FREE)
|
|
751
|
+
// ==========================================================================
|
|
752
|
+
if (!skipPatternGuessing && companyDomain && bestConfidence < paidProviderThreshold) {
|
|
753
|
+
result.providersQueried.push('pattern');
|
|
754
|
+
await queryPatternGuessing(contact, companyDomain, addEmail, result);
|
|
755
|
+
}
|
|
756
|
+
// Recalculate best confidence
|
|
757
|
+
const finalBestConfidence = result.emails.length > 0
|
|
758
|
+
? Math.max(...result.emails.map(e => e.confidence))
|
|
759
|
+
: 0;
|
|
760
|
+
// ==========================================================================
|
|
761
|
+
// Phase 3: PAID providers as last resort (Hunter/Apollo)
|
|
762
|
+
// ==========================================================================
|
|
763
|
+
if (!skipPaidProviders && finalBestConfidence < paidProviderThreshold) {
|
|
764
|
+
// Only use paid providers if we have low confidence or no results
|
|
765
|
+
// TODO: Implement Hunter and Apollo providers when needed
|
|
766
|
+
// For now, just mark that we would have queried them
|
|
767
|
+
if (config.hunter?.apiKey || config.apollo?.apiKey) {
|
|
768
|
+
// result.providersQueried.push('hunter');
|
|
769
|
+
// result.providersQueried.push('apollo');
|
|
770
|
+
// await queryPaidProviders(contact, config, addEmail, result);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
// Sort emails by confidence (highest first), then by source priority
|
|
774
|
+
const sourcePriority = {
|
|
775
|
+
ldd: 0, // Highest priority - your own data
|
|
776
|
+
smartprospect: 1,
|
|
777
|
+
pattern: 2,
|
|
778
|
+
hunter: 3,
|
|
779
|
+
apollo: 4,
|
|
780
|
+
};
|
|
781
|
+
result.emails.sort((a, b) => {
|
|
782
|
+
if (b.confidence !== a.confidence) {
|
|
783
|
+
return b.confidence - a.confidence;
|
|
784
|
+
}
|
|
785
|
+
return sourcePriority[a.source] - sourcePriority[b.source];
|
|
786
|
+
});
|
|
787
|
+
result.success = result.emails.length > 0;
|
|
788
|
+
// Clean up errors array if empty
|
|
789
|
+
if (result.errors && result.errors.length === 0) {
|
|
790
|
+
delete result.errors;
|
|
791
|
+
}
|
|
792
|
+
return result;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Extract company domain from LinkedIn contact
|
|
796
|
+
*/
|
|
797
|
+
function extractCompanyDomain(contact) {
|
|
798
|
+
const pos = contact.currentPositions?.[0];
|
|
799
|
+
if (!pos)
|
|
800
|
+
return null;
|
|
801
|
+
// Try to get website from company resolution
|
|
802
|
+
const website = pos.companyUrnResolutionResult?.website;
|
|
803
|
+
if (website) {
|
|
804
|
+
// Extract domain from URL
|
|
805
|
+
try {
|
|
806
|
+
const url = website.startsWith('http') ? website : `https://${website}`;
|
|
807
|
+
return new URL(url).hostname.replace(/^www\./, '');
|
|
808
|
+
}
|
|
809
|
+
catch {
|
|
810
|
+
// If URL parsing fails, try as-is
|
|
811
|
+
return website.replace(/^(https?:\/\/)?(www\.)?/, '').split('/')[0];
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Query LDD provider
|
|
818
|
+
*/
|
|
819
|
+
async function queryLdd(contact, lddConfig, numericLinkedInId, addEmail, result) {
|
|
820
|
+
try {
|
|
821
|
+
const lddProvider = (0, ldd_1.createLddProvider)(lddConfig);
|
|
822
|
+
const lddResult = await lddProvider({
|
|
823
|
+
numericLinkedInId: numericLinkedInId || undefined,
|
|
824
|
+
firstName: contact.firstName,
|
|
825
|
+
lastName: contact.lastName,
|
|
826
|
+
});
|
|
827
|
+
if (lddResult && 'emails' in lddResult && lddResult.emails.length > 0) {
|
|
828
|
+
for (const emailData of lddResult.emails) {
|
|
829
|
+
addEmail({
|
|
830
|
+
email: emailData.email,
|
|
831
|
+
source: 'ldd',
|
|
832
|
+
confidence: emailData.confidence ?? 90,
|
|
833
|
+
type: emailData.metadata?.emailTypeClassified || 'unknown',
|
|
834
|
+
verified: emailData.verified ?? true,
|
|
835
|
+
metadata: emailData.metadata,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
catch (err) {
|
|
841
|
+
result.errors?.push(`LDD: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Query SmartProspect provider
|
|
846
|
+
*/
|
|
847
|
+
async function querySmartProspect(contact, smartProspectConfig, minMatchConfidence, includeCompany, addEmail, result) {
|
|
848
|
+
try {
|
|
849
|
+
const client = (0, smartprospect_1.createSmartProspectClient)(smartProspectConfig);
|
|
850
|
+
if (!client) {
|
|
851
|
+
result.errors?.push('SmartProspect: Failed to create client');
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
// Build search filters
|
|
855
|
+
const filters = buildSmartProspectFiltersFromLinkedIn(contact);
|
|
856
|
+
const searchFilters = {
|
|
857
|
+
firstName: filters.firstName,
|
|
858
|
+
lastName: filters.lastName,
|
|
859
|
+
limit: 25,
|
|
860
|
+
};
|
|
861
|
+
if (includeCompany && filters.companyName) {
|
|
862
|
+
searchFilters.companyName = filters.companyName;
|
|
863
|
+
}
|
|
864
|
+
// Search for matching contacts
|
|
865
|
+
let searchResponse = await client.search(searchFilters);
|
|
866
|
+
// Try broader search if no results
|
|
867
|
+
if (!searchResponse.success || searchResponse.data.list.length === 0) {
|
|
868
|
+
if (includeCompany && filters.companyName) {
|
|
869
|
+
const broaderResponse = await client.search({
|
|
870
|
+
firstName: filters.firstName,
|
|
871
|
+
lastName: filters.lastName,
|
|
872
|
+
limit: 25,
|
|
873
|
+
});
|
|
874
|
+
if (broaderResponse.success && broaderResponse.data.list.length > 0) {
|
|
875
|
+
searchResponse = broaderResponse;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (searchResponse.data.list.length === 0) {
|
|
880
|
+
return; // No candidates found, but not an error
|
|
881
|
+
}
|
|
882
|
+
// Find best match
|
|
883
|
+
const matchResult = findBestMatch(contact, searchResponse.data.list, {
|
|
884
|
+
minConfidence: minMatchConfidence,
|
|
885
|
+
fuzzyNames: true,
|
|
886
|
+
fuzzyCompany: true,
|
|
887
|
+
});
|
|
888
|
+
if (!matchResult) {
|
|
889
|
+
return; // No good match, but not an error
|
|
890
|
+
}
|
|
891
|
+
// Fetch email for matched contact
|
|
892
|
+
const fetchResponse = await client.fetch([matchResult.smartProspectContact.id]);
|
|
893
|
+
if (fetchResponse.success && fetchResponse.data.list.length > 0) {
|
|
894
|
+
const enrichedContact = fetchResponse.data.list[0];
|
|
895
|
+
if (enrichedContact.email) {
|
|
896
|
+
addEmail({
|
|
897
|
+
email: enrichedContact.email,
|
|
898
|
+
source: 'smartprospect',
|
|
899
|
+
confidence: matchResult.confidence,
|
|
900
|
+
type: 'business',
|
|
901
|
+
verified: enrichedContact.verificationStatus === 'verified',
|
|
902
|
+
deliverability: enrichedContact.emailDeliverability,
|
|
903
|
+
metadata: {
|
|
904
|
+
matchQuality: matchResult.quality,
|
|
905
|
+
matchedFields: matchResult.matchedFields,
|
|
906
|
+
smartProspectId: enrichedContact.id,
|
|
907
|
+
company: enrichedContact.company?.name,
|
|
908
|
+
title: enrichedContact.title,
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
catch (err) {
|
|
915
|
+
result.errors?.push(`SmartProspect: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Query pattern guessing with MX verification
|
|
920
|
+
*/
|
|
921
|
+
async function queryPatternGuessing(contact, companyDomain, addEmail, result) {
|
|
922
|
+
try {
|
|
923
|
+
// Import construct provider dynamically to avoid circular deps
|
|
924
|
+
const { createConstructProvider } = await Promise.resolve().then(() => __importStar(require('./providers/construct')));
|
|
925
|
+
const constructProvider = createConstructProvider({ maxAttempts: 6, timeoutMs: 3000 });
|
|
926
|
+
const constructResult = await constructProvider({
|
|
927
|
+
firstName: contact.firstName,
|
|
928
|
+
lastName: contact.lastName,
|
|
929
|
+
domain: companyDomain,
|
|
930
|
+
});
|
|
931
|
+
if (constructResult && 'emails' in constructResult && constructResult.emails.length > 0) {
|
|
932
|
+
for (const emailData of constructResult.emails) {
|
|
933
|
+
addEmail({
|
|
934
|
+
email: emailData.email,
|
|
935
|
+
source: 'pattern',
|
|
936
|
+
confidence: emailData.confidence ?? 50,
|
|
937
|
+
type: 'business',
|
|
938
|
+
verified: emailData.verified ?? false,
|
|
939
|
+
isCatchAll: emailData.isCatchAll,
|
|
940
|
+
metadata: emailData.metadata,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
catch (err) {
|
|
946
|
+
result.errors?.push(`Pattern: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Get emails for multiple LinkedIn contacts in batch
|
|
951
|
+
*
|
|
952
|
+
* Processes contacts with controlled concurrency to balance speed and rate limits.
|
|
953
|
+
*/
|
|
954
|
+
async function getEmailsForLinkedInContactsBatch(contacts, config, options = {}) {
|
|
955
|
+
const { delayMs = 100, concurrency = 3, onProgress, ...lookupOptions } = options;
|
|
956
|
+
const results = new Array(contacts.length);
|
|
957
|
+
let completed = 0;
|
|
958
|
+
// Process in batches with concurrency limit
|
|
959
|
+
for (let i = 0; i < contacts.length; i += concurrency) {
|
|
960
|
+
const batch = contacts.slice(i, i + concurrency);
|
|
961
|
+
const batchPromises = batch.map(async (contact, batchIndex) => {
|
|
962
|
+
const result = await getEmailsForLinkedInContact(contact, config, lookupOptions);
|
|
963
|
+
const globalIndex = i + batchIndex;
|
|
964
|
+
results[globalIndex] = result;
|
|
965
|
+
completed++;
|
|
966
|
+
if (onProgress) {
|
|
967
|
+
onProgress(completed, contacts.length, result);
|
|
968
|
+
}
|
|
969
|
+
return result;
|
|
970
|
+
});
|
|
971
|
+
await Promise.all(batchPromises);
|
|
972
|
+
// Delay between batches (except for last batch)
|
|
973
|
+
if (i + concurrency < contacts.length && delayMs > 0) {
|
|
974
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return results;
|
|
978
|
+
}
|