linkedin-secret-sauce 0.8.0 → 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.
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/enrichment/matching.d.ts +44 -16
- package/dist/enrichment/matching.js +281 -124
- package/package.json +1 -1
|
@@ -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, getEmailsForLinkedInContact, getEmailsForLinkedInContactsBatch, type LinkedInContact, type MatchResult, type MatchOptions, type LinkedInEnrichmentResult, type LinkedInEnrichmentOptions, type EmailResult, type GetEmailsResult, type GetEmailsConfig, type GetEmailsOptions, } 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";
|
|
@@ -239,6 +239,10 @@ 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';
|
|
242
246
|
/**
|
|
243
247
|
* Email result from unified lookup
|
|
244
248
|
*/
|
|
@@ -246,7 +250,7 @@ export interface EmailResult {
|
|
|
246
250
|
/** The email address */
|
|
247
251
|
email: string;
|
|
248
252
|
/** Which provider found this email */
|
|
249
|
-
source:
|
|
253
|
+
source: EmailSource;
|
|
250
254
|
/** Confidence score (0-100) */
|
|
251
255
|
confidence: number;
|
|
252
256
|
/** Email type classification */
|
|
@@ -255,6 +259,8 @@ export interface EmailResult {
|
|
|
255
259
|
verified: boolean;
|
|
256
260
|
/** Email deliverability score (0-1) for SmartProspect emails */
|
|
257
261
|
deliverability?: number;
|
|
262
|
+
/** Whether this is a catch-all domain */
|
|
263
|
+
isCatchAll?: boolean;
|
|
258
264
|
/** Additional metadata */
|
|
259
265
|
metadata?: Record<string, unknown>;
|
|
260
266
|
}
|
|
@@ -269,18 +275,28 @@ export interface GetEmailsResult {
|
|
|
269
275
|
/** Numeric LinkedIn ID extracted from objectUrn */
|
|
270
276
|
numericLinkedInId: string | null;
|
|
271
277
|
/** Which providers were queried */
|
|
272
|
-
providersQueried:
|
|
273
|
-
/** Error message if failed */
|
|
274
|
-
|
|
278
|
+
providersQueried: EmailSource[];
|
|
279
|
+
/** Error message if any provider failed */
|
|
280
|
+
errors?: string[];
|
|
275
281
|
}
|
|
276
282
|
/**
|
|
277
283
|
* Configuration for unified email lookup
|
|
278
284
|
*/
|
|
279
285
|
export interface GetEmailsConfig {
|
|
280
|
-
/** LDD configuration (
|
|
286
|
+
/** LDD configuration (FREE - your ~500M database) */
|
|
281
287
|
ldd?: LddConfig;
|
|
282
|
-
/** SmartProspect configuration (
|
|
288
|
+
/** SmartProspect configuration (FREE for FlexIQ - already paying monthly) */
|
|
283
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
|
+
};
|
|
284
300
|
}
|
|
285
301
|
/**
|
|
286
302
|
* Options for unified email lookup
|
|
@@ -290,19 +306,24 @@ export interface GetEmailsOptions {
|
|
|
290
306
|
skipLdd?: boolean;
|
|
291
307
|
/** Skip SmartProspect lookup (default: false) */
|
|
292
308
|
skipSmartProspect?: boolean;
|
|
309
|
+
/** Skip email pattern guessing (default: false) */
|
|
310
|
+
skipPatternGuessing?: boolean;
|
|
311
|
+
/** Skip paid providers Hunter/Apollo (default: false) */
|
|
312
|
+
skipPaidProviders?: boolean;
|
|
293
313
|
/** Minimum match confidence for SmartProspect (default: 60) */
|
|
294
314
|
minMatchConfidence?: number;
|
|
315
|
+
/** Minimum confidence to skip paid providers (default: 80) */
|
|
316
|
+
paidProviderThreshold?: number;
|
|
295
317
|
/** Include company in SmartProspect search (default: true) */
|
|
296
318
|
includeCompany?: boolean;
|
|
297
319
|
}
|
|
298
320
|
/**
|
|
299
321
|
* Get emails for a LinkedIn contact - UNIFIED FUNCTION
|
|
300
322
|
*
|
|
301
|
-
*
|
|
302
|
-
* 1.
|
|
303
|
-
* 2.
|
|
304
|
-
* 3.
|
|
305
|
-
* 4. Returns ALL emails with unified scoring
|
|
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)
|
|
306
327
|
*
|
|
307
328
|
* @example
|
|
308
329
|
* ```ts
|
|
@@ -310,10 +331,13 @@ export interface GetEmailsOptions {
|
|
|
310
331
|
*
|
|
311
332
|
* // From your LinkedIn Sales Nav search result
|
|
312
333
|
* const contact = {
|
|
313
|
-
* objectUrn: 'urn:li:member:307567', //
|
|
334
|
+
* objectUrn: 'urn:li:member:307567', // Stable ID for LDD
|
|
314
335
|
* firstName: 'Jim',
|
|
315
336
|
* lastName: 'DeMaio',
|
|
316
|
-
* currentPositions: [{
|
|
337
|
+
* currentPositions: [{
|
|
338
|
+
* companyName: 'Acme Corp',
|
|
339
|
+
* companyUrnResolutionResult: { website: 'acme.com' }
|
|
340
|
+
* }]
|
|
317
341
|
* };
|
|
318
342
|
*
|
|
319
343
|
* const result = await getEmailsForLinkedInContact(contact, {
|
|
@@ -325,13 +349,15 @@ export interface GetEmailsOptions {
|
|
|
325
349
|
* email: process.env.SMARTLEAD_EMAIL,
|
|
326
350
|
* password: process.env.SMARTLEAD_PASSWORD,
|
|
327
351
|
* },
|
|
352
|
+
* // companyDomain extracted automatically from currentPositions
|
|
328
353
|
* });
|
|
329
354
|
*
|
|
330
355
|
* if (result.success) {
|
|
356
|
+
* console.log('Providers queried:', result.providersQueried);
|
|
331
357
|
* console.log('Found emails:', result.emails);
|
|
332
358
|
* // [
|
|
333
359
|
* // { email: 'jim@acme.com', source: 'ldd', confidence: 95, type: 'business' },
|
|
334
|
-
* // { email: '
|
|
360
|
+
* // { email: 'j.demaio@acme.com', source: 'smartprospect', confidence: 85, type: 'business' },
|
|
335
361
|
* // ]
|
|
336
362
|
* }
|
|
337
363
|
* ```
|
|
@@ -340,11 +366,13 @@ export declare function getEmailsForLinkedInContact(contact: LinkedInContact, co
|
|
|
340
366
|
/**
|
|
341
367
|
* Get emails for multiple LinkedIn contacts in batch
|
|
342
368
|
*
|
|
343
|
-
* Processes contacts
|
|
369
|
+
* Processes contacts with controlled concurrency to balance speed and rate limits.
|
|
344
370
|
*/
|
|
345
371
|
export declare function getEmailsForLinkedInContactsBatch(contacts: LinkedInContact[], config: GetEmailsConfig, options?: GetEmailsOptions & {
|
|
346
|
-
/** Delay between requests in ms (default:
|
|
372
|
+
/** Delay between requests in ms (default: 100) */
|
|
347
373
|
delayMs?: number;
|
|
374
|
+
/** Max concurrent lookups (default: 3) */
|
|
375
|
+
concurrency?: number;
|
|
348
376
|
/** Callback for progress updates */
|
|
349
377
|
onProgress?: (completed: number, total: number, result: GetEmailsResult) => void;
|
|
350
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;
|
|
@@ -629,11 +662,10 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
|
629
662
|
/**
|
|
630
663
|
* Get emails for a LinkedIn contact - UNIFIED FUNCTION
|
|
631
664
|
*
|
|
632
|
-
*
|
|
633
|
-
* 1.
|
|
634
|
-
* 2.
|
|
635
|
-
* 3.
|
|
636
|
-
* 4. Returns ALL emails with unified scoring
|
|
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)
|
|
637
669
|
*
|
|
638
670
|
* @example
|
|
639
671
|
* ```ts
|
|
@@ -641,10 +673,13 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
|
641
673
|
*
|
|
642
674
|
* // From your LinkedIn Sales Nav search result
|
|
643
675
|
* const contact = {
|
|
644
|
-
* objectUrn: 'urn:li:member:307567', //
|
|
676
|
+
* objectUrn: 'urn:li:member:307567', // Stable ID for LDD
|
|
645
677
|
* firstName: 'Jim',
|
|
646
678
|
* lastName: 'DeMaio',
|
|
647
|
-
* currentPositions: [{
|
|
679
|
+
* currentPositions: [{
|
|
680
|
+
* companyName: 'Acme Corp',
|
|
681
|
+
* companyUrnResolutionResult: { website: 'acme.com' }
|
|
682
|
+
* }]
|
|
648
683
|
* };
|
|
649
684
|
*
|
|
650
685
|
* const result = await getEmailsForLinkedInContact(contact, {
|
|
@@ -656,164 +691,286 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
|
656
691
|
* email: process.env.SMARTLEAD_EMAIL,
|
|
657
692
|
* password: process.env.SMARTLEAD_PASSWORD,
|
|
658
693
|
* },
|
|
694
|
+
* // companyDomain extracted automatically from currentPositions
|
|
659
695
|
* });
|
|
660
696
|
*
|
|
661
697
|
* if (result.success) {
|
|
698
|
+
* console.log('Providers queried:', result.providersQueried);
|
|
662
699
|
* console.log('Found emails:', result.emails);
|
|
663
700
|
* // [
|
|
664
701
|
* // { email: 'jim@acme.com', source: 'ldd', confidence: 95, type: 'business' },
|
|
665
|
-
* // { email: '
|
|
702
|
+
* // { email: 'j.demaio@acme.com', source: 'smartprospect', confidence: 85, type: 'business' },
|
|
666
703
|
* // ]
|
|
667
704
|
* }
|
|
668
705
|
* ```
|
|
669
706
|
*/
|
|
670
707
|
async function getEmailsForLinkedInContact(contact, config, options = {}) {
|
|
671
|
-
const { skipLdd = false, skipSmartProspect = false, minMatchConfidence = 60, includeCompany = true, } = options;
|
|
708
|
+
const { skipLdd = false, skipSmartProspect = false, skipPatternGuessing = false, skipPaidProviders = false, minMatchConfidence = 60, paidProviderThreshold = 80, includeCompany = true, } = options;
|
|
672
709
|
// Extract numeric ID from objectUrn
|
|
673
710
|
const numericLinkedInId = (0, ldd_1.extractNumericLinkedInId)(contact.objectUrn);
|
|
711
|
+
// Extract company domain from contact
|
|
712
|
+
const companyDomain = config.companyDomain || extractCompanyDomain(contact);
|
|
674
713
|
const result = {
|
|
675
714
|
success: false,
|
|
676
715
|
emails: [],
|
|
677
716
|
numericLinkedInId,
|
|
678
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
|
+
}
|
|
679
728
|
};
|
|
680
729
|
// ==========================================================================
|
|
681
|
-
//
|
|
730
|
+
// Phase 1: FREE providers in PARALLEL (LDD + SmartProspect)
|
|
682
731
|
// ==========================================================================
|
|
732
|
+
const freeProviderPromises = [];
|
|
733
|
+
// LDD lookup (FREE)
|
|
683
734
|
if (!skipLdd && config.ldd?.apiUrl && config.ldd?.apiToken) {
|
|
684
735
|
result.providersQueried.push('ldd');
|
|
685
|
-
|
|
686
|
-
const lddProvider = (0, ldd_1.createLddProvider)(config.ldd);
|
|
687
|
-
const lddResult = await lddProvider({
|
|
688
|
-
numericLinkedInId: numericLinkedInId || undefined,
|
|
689
|
-
linkedinUsername: undefined, // Let LDD provider extract from contact if needed
|
|
690
|
-
firstName: contact.firstName,
|
|
691
|
-
lastName: contact.lastName,
|
|
692
|
-
});
|
|
693
|
-
if (lddResult && 'emails' in lddResult && lddResult.emails.length > 0) {
|
|
694
|
-
// LDD found emails!
|
|
695
|
-
for (const emailData of lddResult.emails) {
|
|
696
|
-
result.emails.push({
|
|
697
|
-
email: emailData.email,
|
|
698
|
-
source: 'ldd',
|
|
699
|
-
confidence: emailData.confidence ?? 90,
|
|
700
|
-
type: emailData.metadata?.emailTypeClassified || 'unknown',
|
|
701
|
-
verified: emailData.verified ?? true,
|
|
702
|
-
metadata: emailData.metadata,
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
result.success = true;
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
catch (err) {
|
|
709
|
-
// LDD failed, continue to SmartProspect
|
|
710
|
-
result.error = `LDD error: ${err instanceof Error ? err.message : 'Unknown error'}`;
|
|
711
|
-
}
|
|
736
|
+
freeProviderPromises.push(queryLdd(contact, config.ldd, numericLinkedInId, addEmail, result));
|
|
712
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;
|
|
713
749
|
// ==========================================================================
|
|
714
|
-
//
|
|
750
|
+
// Phase 2: Email pattern guessing with MX verification (FREE)
|
|
715
751
|
// ==========================================================================
|
|
716
|
-
if (!
|
|
717
|
-
result.providersQueried.push('
|
|
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
|
|
718
805
|
try {
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
+
});
|
|
723
837
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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) {
|
|
731
868
|
if (includeCompany && filters.companyName) {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const broaderResponse = await client.search({
|
|
740
|
-
firstName: filters.firstName,
|
|
741
|
-
lastName: filters.lastName,
|
|
742
|
-
limit: 25,
|
|
743
|
-
});
|
|
744
|
-
if (broaderResponse.success && broaderResponse.data.list.length > 0) {
|
|
745
|
-
searchResponse.data.list = broaderResponse.data.list;
|
|
746
|
-
}
|
|
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;
|
|
747
876
|
}
|
|
748
877
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
smartProspectId: enrichedContact.id,
|
|
783
|
-
company: enrichedContact.company?.name,
|
|
784
|
-
title: enrichedContact.title,
|
|
785
|
-
},
|
|
786
|
-
});
|
|
787
|
-
result.success = true;
|
|
788
|
-
}
|
|
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
|
+
});
|
|
789
911
|
}
|
|
790
912
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|
+
}
|
|
795
943
|
}
|
|
796
944
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
945
|
+
catch (err) {
|
|
946
|
+
result.errors?.push(`Pattern: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
947
|
+
}
|
|
800
948
|
}
|
|
801
949
|
/**
|
|
802
950
|
* Get emails for multiple LinkedIn contacts in batch
|
|
803
951
|
*
|
|
804
|
-
* Processes contacts
|
|
952
|
+
* Processes contacts with controlled concurrency to balance speed and rate limits.
|
|
805
953
|
*/
|
|
806
954
|
async function getEmailsForLinkedInContactsBatch(contacts, config, options = {}) {
|
|
807
|
-
const { delayMs =
|
|
808
|
-
const results =
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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) {
|
|
817
974
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
818
975
|
}
|
|
819
976
|
}
|