linkedin-secret-sauce 0.12.1 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -21
- package/dist/cosiall-client.d.ts +1 -1
- package/dist/cosiall-client.js +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/enrichment/index.js +11 -2
- package/dist/enrichment/matching.d.ts +16 -2
- package/dist/enrichment/matching.js +387 -65
- package/dist/enrichment/providers/bounceban.d.ts +82 -0
- package/dist/enrichment/providers/bounceban.js +447 -0
- package/dist/enrichment/providers/bouncer.d.ts +1 -1
- package/dist/enrichment/providers/bouncer.js +19 -21
- package/dist/enrichment/providers/construct.d.ts +1 -1
- package/dist/enrichment/providers/construct.js +22 -38
- package/dist/enrichment/providers/cosiall.d.ts +1 -1
- package/dist/enrichment/providers/cosiall.js +3 -4
- package/dist/enrichment/providers/dropcontact.d.ts +15 -9
- package/dist/enrichment/providers/dropcontact.js +188 -19
- package/dist/enrichment/providers/hunter.d.ts +8 -1
- package/dist/enrichment/providers/hunter.js +52 -28
- package/dist/enrichment/providers/index.d.ts +2 -0
- package/dist/enrichment/providers/index.js +10 -1
- package/dist/enrichment/providers/ldd.d.ts +1 -10
- package/dist/enrichment/providers/ldd.js +20 -97
- package/dist/enrichment/providers/smartprospect.js +28 -48
- package/dist/enrichment/providers/snovio.d.ts +1 -1
- package/dist/enrichment/providers/snovio.js +29 -31
- package/dist/enrichment/providers/trykitt.d.ts +63 -0
- package/dist/enrichment/providers/trykitt.js +210 -0
- package/dist/enrichment/types.d.ts +210 -7
- package/dist/enrichment/types.js +16 -8
- package/dist/enrichment/utils/candidate-parser.d.ts +107 -0
- package/dist/enrichment/utils/candidate-parser.js +173 -0
- package/dist/enrichment/utils/noop-provider.d.ts +39 -0
- package/dist/enrichment/utils/noop-provider.js +37 -0
- package/dist/enrichment/utils/rate-limiter.d.ts +103 -0
- package/dist/enrichment/utils/rate-limiter.js +204 -0
- package/dist/enrichment/utils/validation.d.ts +75 -3
- package/dist/enrichment/utils/validation.js +164 -11
- package/dist/linkedin-api.d.ts +40 -1
- package/dist/linkedin-api.js +160 -27
- package/dist/types.d.ts +50 -1
- package/dist/utils/lru-cache.d.ts +105 -0
- package/dist/utils/lru-cache.js +175 -0
- package/package.json +25 -26
|
@@ -780,7 +780,7 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
|
780
780
|
async function getEmailsForLinkedInContact(contactOrLead, config, options = {}) {
|
|
781
781
|
// Normalize input - accept either LinkedInContact or raw SalesLeadSearchResult
|
|
782
782
|
const contact = normalizeToContact(contactOrLead);
|
|
783
|
-
const { skipLdd = false, skipSmartProspect = false, skipCosiall = false, skipPatternGuessing = false, skipPaidProviders = false, minMatchConfidence = 60, paidProviderThreshold = 80, includeCompany = true, } = options;
|
|
783
|
+
const { skipLdd = false, skipSmartProspect = false, skipCosiall = false, skipTryKitt = false, skipPatternGuessing = false, skipBounceBan = false, skipPaidProviders = false, minMatchConfidence = 60, paidProviderThreshold = 80, includeCompany = true, } = options;
|
|
784
784
|
// Extract numeric ID from objectUrn
|
|
785
785
|
const numericLinkedInId = (0, ldd_1.extractNumericLinkedInId)(contact.objectUrn);
|
|
786
786
|
// Extract company domain from contact (LinkedIn data - often missing)
|
|
@@ -792,28 +792,75 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
792
792
|
providersQueried: [],
|
|
793
793
|
errors: [],
|
|
794
794
|
};
|
|
795
|
-
// Track
|
|
796
|
-
const
|
|
795
|
+
// Track all emails with their sources for confidence boosting
|
|
796
|
+
const emailsByAddress = new Map();
|
|
797
797
|
const addEmail = (emailResult) => {
|
|
798
798
|
const lower = emailResult.email.toLowerCase();
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
799
|
+
const existing = emailsByAddress.get(lower);
|
|
800
|
+
if (existing) {
|
|
801
|
+
// Email already found from another source - track for confidence boosting
|
|
802
|
+
existing.results.push(emailResult);
|
|
803
|
+
existing.sources.add(emailResult.source);
|
|
802
804
|
}
|
|
805
|
+
else {
|
|
806
|
+
emailsByAddress.set(lower, {
|
|
807
|
+
results: [emailResult],
|
|
808
|
+
sources: new Set([emailResult.source]),
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
/**
|
|
813
|
+
* Merge emails and boost confidence when found from multiple sources
|
|
814
|
+
* - 2 sources: boost to at least 95% confidence
|
|
815
|
+
* - 3+ sources: boost to 100% confidence
|
|
816
|
+
*/
|
|
817
|
+
const mergeAndBoostEmails = () => {
|
|
818
|
+
const mergedEmails = [];
|
|
819
|
+
for (const [_email, { results, sources }] of emailsByAddress) {
|
|
820
|
+
// Find the best result (highest confidence)
|
|
821
|
+
const bestResult = results.reduce((best, current) => current.confidence > best.confidence ? current : best);
|
|
822
|
+
// Boost confidence based on number of sources
|
|
823
|
+
let boostedConfidence = bestResult.confidence;
|
|
824
|
+
if (sources.size >= 3) {
|
|
825
|
+
boostedConfidence = 100;
|
|
826
|
+
}
|
|
827
|
+
else if (sources.size === 2) {
|
|
828
|
+
boostedConfidence = Math.max(boostedConfidence, 95);
|
|
829
|
+
}
|
|
830
|
+
// Create merged result
|
|
831
|
+
const mergedResult = {
|
|
832
|
+
...bestResult,
|
|
833
|
+
confidence: boostedConfidence,
|
|
834
|
+
metadata: {
|
|
835
|
+
...bestResult.metadata,
|
|
836
|
+
foundBySources: Array.from(sources),
|
|
837
|
+
sourceCount: sources.size,
|
|
838
|
+
confidenceBoosted: sources.size > 1,
|
|
839
|
+
originalConfidence: bestResult.confidence,
|
|
840
|
+
},
|
|
841
|
+
};
|
|
842
|
+
// If verified by any source, mark as verified
|
|
843
|
+
if (results.some((r) => r.verified)) {
|
|
844
|
+
mergedResult.verified = true;
|
|
845
|
+
}
|
|
846
|
+
mergedEmails.push(mergedResult);
|
|
847
|
+
}
|
|
848
|
+
return mergedEmails;
|
|
803
849
|
};
|
|
804
850
|
// Track company domain discovered from SmartProspect (since LinkedIn doesn't provide it)
|
|
805
851
|
let discoveredCompanyDomain = null;
|
|
806
852
|
// ==========================================================================
|
|
807
|
-
// Phase 1: FREE providers in PARALLEL (
|
|
853
|
+
// Phase 1: FREE database providers in PARALLEL (no LinkedIn API calls)
|
|
854
|
+
// LDD + SmartProspect + Cosiall - these are your own databases or already paid
|
|
808
855
|
// ==========================================================================
|
|
809
856
|
const freeProviderPromises = [];
|
|
810
|
-
// LDD lookup (FREE)
|
|
857
|
+
// LDD lookup (FREE - your ~500M database)
|
|
811
858
|
if (!skipLdd && config.ldd?.apiUrl && config.ldd?.apiToken) {
|
|
812
859
|
result.providersQueried.push("ldd");
|
|
813
860
|
freeProviderPromises.push(queryLdd(contact, config.ldd, numericLinkedInId, addEmail, result));
|
|
814
861
|
}
|
|
815
|
-
// SmartProspect lookup (FREE for FlexIQ)
|
|
816
|
-
// This also extracts company domain for pattern guessing
|
|
862
|
+
// SmartProspect lookup (FREE for FlexIQ - already paying monthly)
|
|
863
|
+
// This also extracts company domain for pattern guessing (no extra API call)
|
|
817
864
|
if (!skipSmartProspect && config.smartprospect) {
|
|
818
865
|
result.providersQueried.push("smartprospect");
|
|
819
866
|
freeProviderPromises.push(querySmartProspect(contact, config.smartprospect, minMatchConfidence, includeCompany, addEmail, result).then((domain) => {
|
|
@@ -822,63 +869,86 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
822
869
|
}
|
|
823
870
|
}));
|
|
824
871
|
}
|
|
825
|
-
// Cosiall Profile Emails lookup (FREE)
|
|
872
|
+
// Cosiall Profile Emails lookup (FREE - your database)
|
|
826
873
|
if (!skipCosiall && config.cosiall?.enabled !== false) {
|
|
827
874
|
result.providersQueried.push("cosiall");
|
|
828
875
|
freeProviderPromises.push(queryCosiall(contact, addEmail, result));
|
|
829
876
|
}
|
|
830
|
-
//
|
|
877
|
+
// TryKitt.ai lookup (FREE for individuals - unlimited)
|
|
878
|
+
// AI-powered email finder with enterprise identity server catch-all verification
|
|
879
|
+
if (!skipTryKitt && config.trykitt?.apiKey) {
|
|
880
|
+
result.providersQueried.push("trykitt");
|
|
881
|
+
freeProviderPromises.push(queryTryKitt(contact, config.trykitt, linkedInCompanyDomain || discoveredCompanyDomain, addEmail, result));
|
|
882
|
+
}
|
|
883
|
+
// Wait for all Phase 1 free providers
|
|
831
884
|
await Promise.all(freeProviderPromises);
|
|
832
|
-
//
|
|
833
|
-
|
|
885
|
+
// Merge emails and calculate best confidence after Phase 1
|
|
886
|
+
result.emails = mergeAndBoostEmails();
|
|
887
|
+
const phase1BestConfidence = result.emails.length > 0
|
|
834
888
|
? Math.max(...result.emails.map((e) => e.confidence))
|
|
835
889
|
: 0;
|
|
836
890
|
// ==========================================================================
|
|
837
|
-
// Phase 2: Domain Discovery
|
|
891
|
+
// Phase 2: Domain Discovery + Pattern Guessing
|
|
892
|
+
// ONLY if Phase 1 confidence < threshold (to minimize LinkedIn API calls)
|
|
838
893
|
// ==========================================================================
|
|
839
|
-
// Priority: LinkedIn contact data > SmartProspect > LinkedIn Company API
|
|
840
894
|
let companyDomain = linkedInCompanyDomain || discoveredCompanyDomain;
|
|
841
|
-
//
|
|
842
|
-
if (
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
895
|
+
// Skip Phase 2 entirely if we already have high confidence emails
|
|
896
|
+
if (phase1BestConfidence < paidProviderThreshold) {
|
|
897
|
+
// Try LinkedIn Company Lookup for domain discovery (EXPENSIVE - uses LinkedIn API)
|
|
898
|
+
// Only if we don't have a domain yet
|
|
899
|
+
if (!companyDomain &&
|
|
900
|
+
!skipPatternGuessing &&
|
|
901
|
+
!options.skipLinkedInCompanyLookup &&
|
|
902
|
+
config.linkedInCompanyLookup) {
|
|
903
|
+
const companyUrn = contact.currentPositions?.[0]?.companyUrn;
|
|
904
|
+
if (companyUrn) {
|
|
905
|
+
const companyId = extractCompanyIdFromUrn(companyUrn);
|
|
906
|
+
if (companyId) {
|
|
907
|
+
result.providersQueried.push("linkedin");
|
|
908
|
+
try {
|
|
909
|
+
const company = await config.linkedInCompanyLookup(companyId);
|
|
910
|
+
if (company?.websiteUrl) {
|
|
911
|
+
companyDomain = extractDomainFromUrl(company.websiteUrl);
|
|
912
|
+
if (companyDomain) {
|
|
913
|
+
discoveredCompanyDomain = companyDomain;
|
|
914
|
+
}
|
|
859
915
|
}
|
|
860
916
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
917
|
+
catch (err) {
|
|
918
|
+
result.errors?.push(`LinkedIn: ${err instanceof Error ? err.message : "Company lookup failed"}`);
|
|
919
|
+
}
|
|
864
920
|
}
|
|
865
921
|
}
|
|
866
922
|
}
|
|
923
|
+
// Store discovered domain in result for visibility
|
|
924
|
+
if (discoveredCompanyDomain && !linkedInCompanyDomain) {
|
|
925
|
+
result.discoveredCompanyDomain = discoveredCompanyDomain;
|
|
926
|
+
}
|
|
927
|
+
// Run pattern guessing if we have a domain (from LinkedIn contact, SmartProspect, or LinkedIn API)
|
|
928
|
+
if (!skipPatternGuessing && companyDomain) {
|
|
929
|
+
result.providersQueried.push("pattern");
|
|
930
|
+
await queryPatternGuessing(contact, companyDomain, addEmail, result);
|
|
931
|
+
// Re-merge after pattern guessing
|
|
932
|
+
result.emails = mergeAndBoostEmails();
|
|
933
|
+
}
|
|
934
|
+
// BounceBan catch-all verification (FREE single emails)
|
|
935
|
+
// Use BounceBan to verify pattern-guessed emails, especially on catch-all domains
|
|
936
|
+
if (!skipBounceBan &&
|
|
937
|
+
config.bounceban?.apiKey &&
|
|
938
|
+
result.emails.length > 0) {
|
|
939
|
+
// Only verify emails that are from pattern guessing or have low confidence
|
|
940
|
+
const emailsToVerify = result.emails
|
|
941
|
+
.filter((e) => e.source === "pattern" || (e.confidence < 80 && !e.verified))
|
|
942
|
+
.slice(0, 3); // Verify top 3 to save credits
|
|
943
|
+
if (emailsToVerify.length > 0) {
|
|
944
|
+
result.providersQueried.push("bounceban");
|
|
945
|
+
await queryBounceBan(emailsToVerify, config.bounceban, addEmail, result);
|
|
946
|
+
// Re-merge after BounceBan verification
|
|
947
|
+
result.emails = mergeAndBoostEmails();
|
|
948
|
+
}
|
|
949
|
+
}
|
|
867
950
|
}
|
|
868
|
-
//
|
|
869
|
-
if (discoveredCompanyDomain && !linkedInCompanyDomain) {
|
|
870
|
-
result.discoveredCompanyDomain = discoveredCompanyDomain;
|
|
871
|
-
}
|
|
872
|
-
// ==========================================================================
|
|
873
|
-
// Phase 3: Email pattern guessing with MX verification (FREE)
|
|
874
|
-
// ==========================================================================
|
|
875
|
-
if (!skipPatternGuessing &&
|
|
876
|
-
companyDomain &&
|
|
877
|
-
bestConfidenceAfterFreeProviders < paidProviderThreshold) {
|
|
878
|
-
result.providersQueried.push("pattern");
|
|
879
|
-
await queryPatternGuessing(contact, companyDomain, addEmail, result);
|
|
880
|
-
}
|
|
881
|
-
// Recalculate best confidence
|
|
951
|
+
// Calculate final best confidence
|
|
882
952
|
const finalBestConfidence = result.emails.length > 0
|
|
883
953
|
? Math.max(...result.emails.map((e) => e.confidence))
|
|
884
954
|
: 0;
|
|
@@ -887,27 +957,55 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
887
957
|
// ==========================================================================
|
|
888
958
|
if (!skipPaidProviders && finalBestConfidence < paidProviderThreshold) {
|
|
889
959
|
// Only use paid providers if we have low confidence or no results
|
|
890
|
-
//
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
//
|
|
896
|
-
|
|
897
|
-
//
|
|
898
|
-
//
|
|
960
|
+
// Hunter.io - Email Finder ($0.005/lookup)
|
|
961
|
+
if (config.hunter?.apiKey) {
|
|
962
|
+
result.providersQueried.push("hunter");
|
|
963
|
+
// Extract linkedin handle from contact if available
|
|
964
|
+
// Note: For best results, caller should fetch profile with getSalesNavigatorProfileFull
|
|
965
|
+
// and extract handle using extractLinkedInHandle(profile.flagshipProfileUrl)
|
|
966
|
+
let linkedinHandle = null;
|
|
967
|
+
// Try to extract from entityUrn or any linkedin URL fields
|
|
968
|
+
// (These won't typically have the vanity, but check anyway)
|
|
969
|
+
const contactAny = contact;
|
|
970
|
+
if (contactAny.linkedinHandle) {
|
|
971
|
+
linkedinHandle = String(contactAny.linkedinHandle);
|
|
972
|
+
}
|
|
973
|
+
else if (contactAny.flagshipProfileUrl) {
|
|
974
|
+
// Extract handle from flagship URL
|
|
975
|
+
const match = String(contactAny.flagshipProfileUrl).match(/linkedin\.com\/in\/([^\/\?]+)/i);
|
|
976
|
+
if (match)
|
|
977
|
+
linkedinHandle = match[1];
|
|
978
|
+
}
|
|
979
|
+
await queryHunter(contact, config.hunter, companyDomain || null, linkedinHandle, addEmail, result);
|
|
980
|
+
// Re-merge after Hunter
|
|
981
|
+
result.emails = mergeAndBoostEmails();
|
|
982
|
+
}
|
|
983
|
+
// Snovio - Email Finder ($0.02/lookup) - best for catch-all domains
|
|
984
|
+
if (config.snovio?.userId && config.snovio?.apiSecret && companyDomain) {
|
|
985
|
+
result.providersQueried.push("snovio");
|
|
986
|
+
await querySnovio(contact, config.snovio, companyDomain, addEmail, result);
|
|
987
|
+
// Re-merge after Snovio
|
|
988
|
+
result.emails = mergeAndBoostEmails();
|
|
899
989
|
}
|
|
990
|
+
// Note: Bouncer is a VERIFICATION provider (verifies existing emails).
|
|
991
|
+
// It's automatically used by the construct provider for SMTP verification.
|
|
992
|
+
// No need to call it separately here since pattern guessing already uses
|
|
993
|
+
// the built-in mx verification which is similar to Bouncer.
|
|
900
994
|
}
|
|
995
|
+
// Final merge (in case paid providers added emails) and sort
|
|
996
|
+
result.emails = mergeAndBoostEmails();
|
|
901
997
|
// Sort emails by confidence (highest first), then by source priority
|
|
902
998
|
const sourcePriority = {
|
|
903
999
|
ldd: 0, // Highest priority - your own data
|
|
904
1000
|
smartprospect: 1,
|
|
905
1001
|
cosiall: 2, // Cosiall Profile Emails (FREE)
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1002
|
+
trykitt: 3, // TryKitt.ai (FREE for individuals)
|
|
1003
|
+
linkedin: 4, // LinkedIn company lookup (for domain discovery, doesn't provide emails)
|
|
1004
|
+
pattern: 5,
|
|
1005
|
+
bounceban: 6, // BounceBan catch-all verification (FREE single)
|
|
1006
|
+
hunter: 7,
|
|
1007
|
+
bouncer: 8,
|
|
1008
|
+
snovio: 9,
|
|
911
1009
|
};
|
|
912
1010
|
result.emails.sort((a, b) => {
|
|
913
1011
|
if (b.confidence !== a.confidence) {
|
|
@@ -1041,6 +1139,117 @@ async function queryCosiall(contact, addEmail, result) {
|
|
|
1041
1139
|
}
|
|
1042
1140
|
}
|
|
1043
1141
|
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Query TryKitt.ai provider (FREE for individuals)
|
|
1144
|
+
* AI-powered email finder with enterprise identity server catch-all verification
|
|
1145
|
+
*/
|
|
1146
|
+
async function queryTryKitt(contact, trykittConfig, companyDomain, addEmail, result) {
|
|
1147
|
+
try {
|
|
1148
|
+
// Import TryKitt provider dynamically
|
|
1149
|
+
const { createTryKittProvider } = await Promise.resolve().then(() => __importStar(require("./providers/trykitt")));
|
|
1150
|
+
const trykittProvider = createTryKittProvider(trykittConfig);
|
|
1151
|
+
// Build candidate with available data
|
|
1152
|
+
const fullName = contact.fullName || `${contact.firstName} ${contact.lastName}`.trim();
|
|
1153
|
+
// Need both name and domain to search
|
|
1154
|
+
if (!fullName || !companyDomain) {
|
|
1155
|
+
return; // Silently skip - missing required data
|
|
1156
|
+
}
|
|
1157
|
+
const candidate = {
|
|
1158
|
+
fullName,
|
|
1159
|
+
firstName: contact.firstName,
|
|
1160
|
+
lastName: contact.lastName,
|
|
1161
|
+
domain: companyDomain,
|
|
1162
|
+
};
|
|
1163
|
+
// Add LinkedIn URL if available (improves TryKitt accuracy)
|
|
1164
|
+
if (contact.entityUrn) {
|
|
1165
|
+
// entityUrn is like "urn:li:fs_salesProfile:..." - not useful for TryKitt
|
|
1166
|
+
}
|
|
1167
|
+
const trykittResult = await trykittProvider(candidate);
|
|
1168
|
+
if (trykittResult) {
|
|
1169
|
+
// Single result
|
|
1170
|
+
if ("email" in trykittResult && trykittResult.email) {
|
|
1171
|
+
addEmail({
|
|
1172
|
+
email: trykittResult.email,
|
|
1173
|
+
source: "trykitt",
|
|
1174
|
+
confidence: trykittResult.score ?? 80,
|
|
1175
|
+
type: "business",
|
|
1176
|
+
verified: trykittResult.verified ?? false,
|
|
1177
|
+
metadata: {
|
|
1178
|
+
provider: "trykitt",
|
|
1179
|
+
aiPowered: true,
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
// Multi result
|
|
1184
|
+
else if ("emails" in trykittResult) {
|
|
1185
|
+
const multiResult = trykittResult;
|
|
1186
|
+
for (const emailData of multiResult.emails) {
|
|
1187
|
+
addEmail({
|
|
1188
|
+
email: emailData.email,
|
|
1189
|
+
source: "trykitt",
|
|
1190
|
+
confidence: emailData.confidence ?? 70,
|
|
1191
|
+
type: "business",
|
|
1192
|
+
verified: emailData.verified ?? false,
|
|
1193
|
+
isCatchAll: emailData.isCatchAll,
|
|
1194
|
+
metadata: emailData.metadata,
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
catch (err) {
|
|
1201
|
+
result.errors?.push(`TryKitt: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Query BounceBan for catch-all verification
|
|
1206
|
+
* Verifies pattern-guessed emails with 85-95% accuracy on catch-all domains
|
|
1207
|
+
*/
|
|
1208
|
+
async function queryBounceBan(emailsToVerify, bouncebanConfig, addEmail, result) {
|
|
1209
|
+
try {
|
|
1210
|
+
// Import BounceBan provider dynamically
|
|
1211
|
+
const { verifyEmailWithBounceBan } = await Promise.resolve().then(() => __importStar(require("./providers/bounceban")));
|
|
1212
|
+
for (const emailResult of emailsToVerify) {
|
|
1213
|
+
const verification = await verifyEmailWithBounceBan(emailResult.email, {
|
|
1214
|
+
apiKey: bouncebanConfig.apiKey,
|
|
1215
|
+
useDeepVerify: bouncebanConfig.useDeepVerify ?? false,
|
|
1216
|
+
useWaterfall: bouncebanConfig.useWaterfall ?? true,
|
|
1217
|
+
});
|
|
1218
|
+
if (verification && verification.status === "success") {
|
|
1219
|
+
// Map BounceBan result to confidence
|
|
1220
|
+
let confidence = verification.score;
|
|
1221
|
+
const verified = verification.result === "deliverable";
|
|
1222
|
+
// Reduce confidence for catch-all domains even if deliverable
|
|
1223
|
+
if (verification.is_accept_all && verified) {
|
|
1224
|
+
confidence = Math.min(confidence, 75);
|
|
1225
|
+
}
|
|
1226
|
+
// Only add if it's potentially deliverable
|
|
1227
|
+
if (verification.result === "deliverable" ||
|
|
1228
|
+
verification.result === "risky") {
|
|
1229
|
+
addEmail({
|
|
1230
|
+
email: verification.email,
|
|
1231
|
+
source: "bounceban",
|
|
1232
|
+
confidence,
|
|
1233
|
+
type: "business",
|
|
1234
|
+
verified,
|
|
1235
|
+
isCatchAll: verification.is_accept_all,
|
|
1236
|
+
metadata: {
|
|
1237
|
+
bounceBanResult: verification.result,
|
|
1238
|
+
bounceBanScore: verification.score,
|
|
1239
|
+
smtpProvider: verification.smtp_provider,
|
|
1240
|
+
isDisposable: verification.is_disposable,
|
|
1241
|
+
isRole: verification.is_role,
|
|
1242
|
+
originalSource: emailResult.source,
|
|
1243
|
+
},
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
catch (err) {
|
|
1250
|
+
result.errors?.push(`BounceBan: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1044
1253
|
/**
|
|
1045
1254
|
* Query SmartProspect provider
|
|
1046
1255
|
* Returns the company domain if found (for pattern guessing fallback)
|
|
@@ -1167,6 +1376,119 @@ async function queryPatternGuessing(contact, companyDomain, addEmail, result) {
|
|
|
1167
1376
|
result.errors?.push(`Pattern: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
1168
1377
|
}
|
|
1169
1378
|
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Query Hunter.io Email Finder (PAID provider)
|
|
1381
|
+
*
|
|
1382
|
+
* Supports multiple modes:
|
|
1383
|
+
* 1. linkedin_handle - Most accurate (requires getSalesNavigatorProfileFull to get handle)
|
|
1384
|
+
* 2. domain + first_name + last_name
|
|
1385
|
+
* 3. company + first_name + last_name
|
|
1386
|
+
*/
|
|
1387
|
+
async function queryHunter(contact, hunterConfig, companyDomain, linkedinHandle, addEmail, result) {
|
|
1388
|
+
try {
|
|
1389
|
+
// Import Hunter provider dynamically
|
|
1390
|
+
const { createHunterProvider } = await Promise.resolve().then(() => __importStar(require("./providers/hunter")));
|
|
1391
|
+
const hunterProvider = createHunterProvider(hunterConfig);
|
|
1392
|
+
// Build candidate with all available data
|
|
1393
|
+
const candidate = {
|
|
1394
|
+
firstName: contact.firstName,
|
|
1395
|
+
lastName: contact.lastName,
|
|
1396
|
+
};
|
|
1397
|
+
// Add linkedin_handle if available (best for Hunter)
|
|
1398
|
+
if (linkedinHandle) {
|
|
1399
|
+
candidate.linkedinHandle = linkedinHandle;
|
|
1400
|
+
}
|
|
1401
|
+
// Add domain if available
|
|
1402
|
+
if (companyDomain) {
|
|
1403
|
+
candidate.domain = companyDomain;
|
|
1404
|
+
}
|
|
1405
|
+
// Add company name if available
|
|
1406
|
+
const companyName = contact.currentPositions?.[0]?.companyName;
|
|
1407
|
+
if (companyName) {
|
|
1408
|
+
candidate.company = companyName;
|
|
1409
|
+
}
|
|
1410
|
+
const hunterResult = await hunterProvider(candidate);
|
|
1411
|
+
if (hunterResult) {
|
|
1412
|
+
// Single result
|
|
1413
|
+
if ("email" in hunterResult && hunterResult.email) {
|
|
1414
|
+
addEmail({
|
|
1415
|
+
email: hunterResult.email,
|
|
1416
|
+
source: "hunter",
|
|
1417
|
+
confidence: hunterResult.score ?? 70,
|
|
1418
|
+
type: "business",
|
|
1419
|
+
verified: hunterResult.verified ?? false,
|
|
1420
|
+
metadata: {
|
|
1421
|
+
usedLinkedInHandle: !!linkedinHandle,
|
|
1422
|
+
usedDomain: !!companyDomain,
|
|
1423
|
+
usedCompany: !!companyName && !companyDomain && !linkedinHandle,
|
|
1424
|
+
},
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
// Multi result (from domain-search)
|
|
1428
|
+
else if ("emails" in hunterResult && hunterResult.emails.length > 0) {
|
|
1429
|
+
for (const emailData of hunterResult.emails) {
|
|
1430
|
+
addEmail({
|
|
1431
|
+
email: emailData.email,
|
|
1432
|
+
source: "hunter",
|
|
1433
|
+
confidence: emailData.confidence ?? 60,
|
|
1434
|
+
type: "business",
|
|
1435
|
+
verified: emailData.verified ?? false,
|
|
1436
|
+
metadata: emailData.metadata,
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
catch (err) {
|
|
1443
|
+
result.errors?.push(`Hunter: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Query Snovio for email addresses
|
|
1448
|
+
*/
|
|
1449
|
+
async function querySnovio(contact, snovioConfig, companyDomain, addEmail, result) {
|
|
1450
|
+
try {
|
|
1451
|
+
// Import Snovio provider dynamically
|
|
1452
|
+
const { createSnovioProvider } = await Promise.resolve().then(() => __importStar(require("./providers/snovio")));
|
|
1453
|
+
const snovioProvider = createSnovioProvider(snovioConfig);
|
|
1454
|
+
// Build candidate with available data
|
|
1455
|
+
const candidate = {
|
|
1456
|
+
firstName: contact.firstName,
|
|
1457
|
+
lastName: contact.lastName,
|
|
1458
|
+
domain: companyDomain,
|
|
1459
|
+
};
|
|
1460
|
+
const snovioResult = await snovioProvider(candidate);
|
|
1461
|
+
if (snovioResult) {
|
|
1462
|
+
// Single result
|
|
1463
|
+
if ("email" in snovioResult && snovioResult.email) {
|
|
1464
|
+
addEmail({
|
|
1465
|
+
email: snovioResult.email,
|
|
1466
|
+
source: "snovio",
|
|
1467
|
+
confidence: snovioResult.score ?? 70,
|
|
1468
|
+
type: "business",
|
|
1469
|
+
verified: snovioResult.verified ?? false,
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
// Multi result
|
|
1473
|
+
else if ("emails" in snovioResult && snovioResult.emails.length > 0) {
|
|
1474
|
+
for (const emailData of snovioResult.emails) {
|
|
1475
|
+
addEmail({
|
|
1476
|
+
email: emailData.email,
|
|
1477
|
+
source: "snovio",
|
|
1478
|
+
confidence: emailData.confidence ?? 60,
|
|
1479
|
+
type: "business",
|
|
1480
|
+
verified: emailData.verified ?? false,
|
|
1481
|
+
isCatchAll: emailData.isCatchAll,
|
|
1482
|
+
metadata: emailData.metadata,
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
catch (err) {
|
|
1489
|
+
result.errors?.push(`Snovio: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1170
1492
|
/**
|
|
1171
1493
|
* Get emails for multiple LinkedIn contacts in batch
|
|
1172
1494
|
*
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BounceBan Email Verification Provider
|
|
3
|
+
*
|
|
4
|
+
* Specializes in catch-all email verification with 85-95% accuracy
|
|
5
|
+
* using proprietary algorithms WITHOUT sending actual emails.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Catch-all domain verification (industry-leading)
|
|
9
|
+
* - DeepVerify mode for pattern-guessed emails (+15-25% accuracy)
|
|
10
|
+
* - Waterfall endpoint with FREE 30-min retry window
|
|
11
|
+
* - Single email verification is FREE
|
|
12
|
+
* - GDPR compliant (no emails sent)
|
|
13
|
+
*
|
|
14
|
+
* API Endpoints:
|
|
15
|
+
* - GET /v1/verify/single - Standard single verification
|
|
16
|
+
* - GET https://api-waterfall.bounceban.com/v1/verify/single - Waterfall (recommended)
|
|
17
|
+
* - POST /v1/verify/bulk - Bulk verification
|
|
18
|
+
*
|
|
19
|
+
* Result values:
|
|
20
|
+
* - deliverable: Email verified (score 70-100)
|
|
21
|
+
* - risky: May work, often catch-all (score 30-70)
|
|
22
|
+
* - undeliverable: Email doesn't exist (score 0-30)
|
|
23
|
+
* - unknown: Could not determine
|
|
24
|
+
*
|
|
25
|
+
* @see https://bounceban.com
|
|
26
|
+
*/
|
|
27
|
+
import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, BounceBanConfig, BounceBanVerifyResponse } from "../types";
|
|
28
|
+
/**
|
|
29
|
+
* Create the BounceBan verification provider
|
|
30
|
+
*
|
|
31
|
+
* This provider specializes in verifying emails on catch-all domains
|
|
32
|
+
* where traditional SMTP verification fails.
|
|
33
|
+
*
|
|
34
|
+
* Best used after construct provider generates email patterns.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createBounceBanProvider(config: BounceBanConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | ProviderMultiResult | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Standalone function to verify a single email via BounceBan
|
|
39
|
+
*
|
|
40
|
+
* Uses the waterfall endpoint by default for best catch-all handling.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* const result = await verifyEmailWithBounceBan('john@catchall-domain.com', {
|
|
45
|
+
* apiKey: process.env.BOUNCEBAN_API_KEY,
|
|
46
|
+
* });
|
|
47
|
+
* console.log(result.result); // "deliverable" | "risky" | "undeliverable" | "unknown"
|
|
48
|
+
* console.log(result.score); // 0-100
|
|
49
|
+
* console.log(result.is_accept_all); // true (catch-all domain)
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function verifyEmailWithBounceBan(email: string, config: BounceBanConfig): Promise<BounceBanVerifyResponse | null>;
|
|
53
|
+
/**
|
|
54
|
+
* Verify multiple emails in batch via BounceBan Bulk API
|
|
55
|
+
*
|
|
56
|
+
* More cost-effective for large volumes ($0.003/email vs single free).
|
|
57
|
+
* Uses async bulk API with polling for results.
|
|
58
|
+
*
|
|
59
|
+
* For small batches (< 5 emails), falls back to sequential single verification.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* const results = await verifyEmailsBatchWithBounceBan(
|
|
64
|
+
* ['john@example.com', 'jane@example.com'],
|
|
65
|
+
* { apiKey: process.env.BOUNCEBAN_API_KEY }
|
|
66
|
+
* );
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare function verifyEmailsBatchWithBounceBan(emails: string[], config: BounceBanConfig): Promise<Map<string, BounceBanVerifyResponse | null>>;
|
|
70
|
+
/**
|
|
71
|
+
* Check if a domain is catch-all via BounceBan
|
|
72
|
+
*
|
|
73
|
+
* Verifies a random non-existent email to detect catch-all behavior.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const isCatchAll = await checkCatchAllWithBounceBan('example.com', {
|
|
78
|
+
* apiKey: process.env.BOUNCEBAN_API_KEY,
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function checkCatchAllWithBounceBan(domain: string, config: BounceBanConfig): Promise<boolean | null>;
|