linkedin-secret-sauce 0.12.4 → 0.13.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/matching.js +161 -81
- package/dist/enrichment/providers/bounceban.js +19 -10
- package/dist/enrichment/providers/construct.js +97 -89
- package/dist/enrichment/providers/cosiall.js +2 -2
- package/dist/enrichment/providers/hunter.js +2 -1
- package/dist/enrichment/providers/ldd.js +4 -2
- package/dist/enrichment/providers/smartprospect.d.ts +5 -2
- package/dist/enrichment/providers/smartprospect.js +64 -27
- package/dist/enrichment/providers/snovio.js +7 -3
- package/dist/enrichment/providers/trykitt.d.ts +2 -2
- package/dist/enrichment/providers/trykitt.js +86 -21
- package/dist/index.d.ts +1 -1
- package/dist/linkedin-api.d.ts +113 -1
- package/dist/linkedin-api.js +836 -3
- package/dist/parsers/profile-parser.js +1 -0
- package/dist/types.d.ts +164 -0
- package/docs/api/assets/hierarchy.js +1 -1
- package/docs/api/assets/navigation.js +1 -1
- package/docs/api/assets/search.js +1 -1
- package/docs/api/classes/LinkedInClientError.html +4 -4
- package/docs/api/functions/_testGetAccountCookies.html +2 -2
- package/docs/api/functions/_testGetAccountEntry.html +2 -2
- package/docs/api/functions/_testGetAllAccountIds.html +2 -2
- package/docs/api/functions/_testGetPoolState.html +2 -2
- package/docs/api/functions/adminResetAccount.html +1 -1
- package/docs/api/functions/adminSetCooldown.html +1 -1
- package/docs/api/functions/analyzeProfilePosts.html +14 -0
- package/docs/api/functions/buildCookieHeader.html +1 -1
- package/docs/api/functions/clearAllSmartLeadTokens.html +2 -2
- package/docs/api/functions/clearRequestHistory.html +1 -1
- package/docs/api/functions/clearSessionAccount.html +1 -1
- package/docs/api/functions/clearSmartLeadToken.html +2 -2
- package/docs/api/functions/createEnrichmentClient.html +2 -2
- package/docs/api/functions/extractCsrfToken.html +1 -1
- package/docs/api/functions/extractLinkedInHandle.html +2 -2
- package/docs/api/functions/fetchCookiesFromCosiall.html +2 -2
- package/docs/api/functions/fetchProfileEmailsFromCosiall.html +2 -2
- package/docs/api/functions/forceRefreshCookies.html +1 -1
- package/docs/api/functions/getAccountForSession.html +1 -1
- package/docs/api/functions/getAccountsSummary.html +1 -1
- package/docs/api/functions/getCompaniesBatch.html +2 -2
- package/docs/api/functions/getCompanyById.html +2 -2
- package/docs/api/functions/getCompanyByUrl.html +1 -1
- package/docs/api/functions/getConfig.html +1 -1
- package/docs/api/functions/getCookiePoolHealth.html +1 -1
- package/docs/api/functions/getPostComments.html +11 -0
- package/docs/api/functions/getPostHistory.html +12 -0
- package/docs/api/functions/getPostReactions.html +12 -0
- package/docs/api/functions/getProfileByUrn.html +2 -2
- package/docs/api/functions/getProfileByVanity.html +2 -2
- package/docs/api/functions/getProfilesBatch.html +1 -1
- package/docs/api/functions/getRequestHistory.html +1 -1
- package/docs/api/functions/getSalesNavigatorProfileDetails.html +1 -1
- package/docs/api/functions/getSalesNavigatorProfileFull.html +2 -2
- package/docs/api/functions/getSmartLeadToken.html +1 -1
- package/docs/api/functions/getSmartLeadTokenCacheStats.html +2 -2
- package/docs/api/functions/getSmartLeadUser.html +2 -2
- package/docs/api/functions/getSnapshot.html +1 -1
- package/docs/api/functions/getYearsAtCompanyOptions.html +2 -2
- package/docs/api/functions/getYearsInPositionOptions.html +2 -2
- package/docs/api/functions/getYearsOfExperienceOptions.html +2 -2
- package/docs/api/functions/incrementMetric.html +1 -1
- package/docs/api/functions/initializeCookiePool.html +1 -1
- package/docs/api/functions/initializeLinkedInClient.html +1 -1
- package/docs/api/functions/isBusinessEmail.html +2 -2
- package/docs/api/functions/isDisposableDomain.html +2 -2
- package/docs/api/functions/isDisposableEmail.html +2 -2
- package/docs/api/functions/isPersonalDomain.html +2 -2
- package/docs/api/functions/isPersonalEmail.html +2 -2
- package/docs/api/functions/isRoleAccount.html +2 -2
- package/docs/api/functions/isValidEmailSyntax.html +2 -2
- package/docs/api/functions/parseFullProfile.html +2 -2
- package/docs/api/functions/parseSalesSearchResults.html +1 -1
- package/docs/api/functions/reportAccountFailure.html +1 -1
- package/docs/api/functions/reportAccountSuccess.html +1 -1
- package/docs/api/functions/resolveCompanyUniversalName.html +1 -1
- package/docs/api/functions/searchSalesLeads.html +2 -2
- package/docs/api/functions/selectAccountForRequest.html +1 -1
- package/docs/api/functions/setAccountForSession.html +1 -1
- package/docs/api/functions/typeahead.html +1 -1
- package/docs/api/functions/verifyEmailMx.html +1 -1
- package/docs/api/hierarchy.html +1 -1
- package/docs/api/index.html +2 -2
- package/docs/api/interfaces/AccountCookies.html +2 -2
- package/docs/api/interfaces/AnalyzedPost.html +8 -0
- package/docs/api/interfaces/BatchEnrichmentOptions.html +8 -8
- package/docs/api/interfaces/CacheAdapter.html +4 -4
- package/docs/api/interfaces/CanonicalEmail.html +8 -8
- package/docs/api/interfaces/CommentAuthor.html +8 -0
- package/docs/api/interfaces/Company.html +2 -2
- package/docs/api/interfaces/ConstructConfig.html +5 -5
- package/docs/api/interfaces/CosiallProfileEmailsResponse.html +6 -6
- package/docs/api/interfaces/EnrichmentCandidate.html +4 -4
- package/docs/api/interfaces/EnrichmentClient.html +6 -6
- package/docs/api/interfaces/EnrichmentClientConfig.html +7 -7
- package/docs/api/interfaces/EnrichmentLogger.html +3 -3
- package/docs/api/interfaces/EnrichmentOptions.html +6 -6
- package/docs/api/interfaces/HunterConfig.html +3 -3
- package/docs/api/interfaces/LddConfig.html +3 -3
- package/docs/api/interfaces/LddProfileData.html +2 -2
- package/docs/api/interfaces/LinkedInClientConfig.html +2 -2
- package/docs/api/interfaces/LinkedInCookie.html +2 -2
- package/docs/api/interfaces/LinkedInPosition.html +2 -2
- package/docs/api/interfaces/LinkedInProfile.html +2 -2
- package/docs/api/interfaces/LinkedInSpotlightBadge.html +2 -2
- package/docs/api/interfaces/LinkedInTenure.html +2 -2
- package/docs/api/interfaces/Metrics.html +2 -2
- package/docs/api/interfaces/MetricsSnapshot.html +2 -2
- package/docs/api/interfaces/PostAnalytics.html +11 -0
- package/docs/api/interfaces/PostComment.html +8 -0
- package/docs/api/interfaces/PostCommentsResult.html +7 -0
- package/docs/api/interfaces/PostHistoryResult.html +4 -0
- package/docs/api/interfaces/PostReaction.html +5 -0
- package/docs/api/interfaces/PostReactionsResult.html +5 -0
- package/docs/api/interfaces/ProfileAnalysisOptions.html +14 -0
- package/docs/api/interfaces/ProfileAnalysisResult.html +9 -0
- package/docs/api/interfaces/ProfileEducation.html +2 -2
- package/docs/api/interfaces/ProfileEmailsLookupOptions.html +5 -5
- package/docs/api/interfaces/ProfilePosition.html +2 -2
- package/docs/api/interfaces/ProfilePost.html +14 -0
- package/docs/api/interfaces/ProfileSkill.html +2 -2
- package/docs/api/interfaces/ProviderResult.html +6 -6
- package/docs/api/interfaces/ProvidersConfig.html +6 -6
- package/docs/api/interfaces/ReactionActor.html +9 -0
- package/docs/api/interfaces/RequestHistoryEntry.html +2 -2
- package/docs/api/interfaces/SalesLeadSearchResult.html +2 -2
- package/docs/api/interfaces/SalesNavigatorContactInfo.html +2 -2
- package/docs/api/interfaces/SalesNavigatorPosition.html +2 -2
- package/docs/api/interfaces/SalesNavigatorProfile.html +2 -2
- package/docs/api/interfaces/SalesNavigatorProfileFull.html +4 -4
- package/docs/api/interfaces/SearchSalesResult.html +2 -2
- package/docs/api/interfaces/SmartLeadAuthConfig.html +4 -4
- package/docs/api/interfaces/SmartLeadCredentials.html +2 -2
- package/docs/api/interfaces/SmartLeadLoginResponse.html +2 -2
- package/docs/api/interfaces/SmartLeadUser.html +2 -2
- package/docs/api/interfaces/SmartProspectConfig.html +8 -8
- package/docs/api/interfaces/SmartProspectContact.html +2 -2
- package/docs/api/interfaces/SmartProspectSearchFilters.html +21 -21
- package/docs/api/interfaces/TypeaheadItem.html +2 -2
- package/docs/api/interfaces/TypeaheadResult.html +2 -2
- package/docs/api/interfaces/VerificationResult.html +9 -9
- package/docs/api/types/CostCallback.html +2 -2
- package/docs/api/types/Geo.html +2 -2
- package/docs/api/types/LddApiResponse.html +1 -1
- package/docs/api/types/LinkedInReactionType.html +2 -0
- package/docs/api/types/ProviderFunc.html +2 -2
- package/docs/api/types/ProviderName.html +2 -2
- package/docs/api/types/SalesSearchFilters.html +2 -2
- package/docs/api/types/TypeaheadType.html +1 -1
- package/docs/api/variables/COMPANY_SIZE_OPTIONS.html +1 -1
- package/docs/api/variables/DEFAULT_PROVIDER_ORDER.html +2 -2
- package/docs/api/variables/DISPOSABLE_DOMAINS.html +2 -2
- package/docs/api/variables/FUNCTION_OPTIONS.html +1 -1
- package/docs/api/variables/INDUSTRY_OPTIONS.html +1 -1
- package/docs/api/variables/LANGUAGE_OPTIONS.html +1 -1
- package/docs/api/variables/PERSONAL_DOMAINS.html +2 -2
- package/docs/api/variables/PROVIDER_COSTS.html +2 -2
- package/docs/api/variables/REGION_OPTIONS.html +1 -1
- package/docs/api/variables/SENIORITY_OPTIONS.html +2 -2
- package/docs/api/variables/YEARS_OPTIONS.html +1 -1
- package/package.json +1 -1
|
@@ -813,12 +813,41 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
813
813
|
* Merge emails and boost confidence when found from multiple sources
|
|
814
814
|
* - 2 sources: boost to at least 95% confidence
|
|
815
815
|
* - 3+ sources: boost to 100% confidence
|
|
816
|
+
*
|
|
817
|
+
* IMPORTANT: When multiple sources find the same email, we pick the
|
|
818
|
+
* most authoritative source (not just highest confidence), then boost.
|
|
819
|
+
* Source priority: ldd > smartprospect > cosiall > trykitt > bounceban > hunter > snovio > pattern
|
|
816
820
|
*/
|
|
817
821
|
const mergeAndBoostEmails = () => {
|
|
818
822
|
const mergedEmails = [];
|
|
823
|
+
// Source priority for merging (lower = higher priority)
|
|
824
|
+
const mergePriority = {
|
|
825
|
+
ldd: 0, // Highest priority - your own data
|
|
826
|
+
smartprospect: 1, // Very reliable - verified database
|
|
827
|
+
cosiall: 2, // LinkedIn profile data
|
|
828
|
+
trykitt: 3, // AI-powered finder
|
|
829
|
+
bounceban: 4, // Verification provider
|
|
830
|
+
hunter: 5, // Paid finder
|
|
831
|
+
snovio: 6, // Paid finder
|
|
832
|
+
linkedin: 7, // Company lookup only
|
|
833
|
+
pattern: 8, // Pattern guessing - lowest priority
|
|
834
|
+
};
|
|
819
835
|
for (const [_email, { results, sources }] of emailsByAddress) {
|
|
820
|
-
// Find the best result
|
|
821
|
-
|
|
836
|
+
// Find the best result by SOURCE PRIORITY first, then confidence
|
|
837
|
+
// This ensures SmartProspect/LDD are preferred over pattern guessing
|
|
838
|
+
const bestResult = results.reduce((best, current) => {
|
|
839
|
+
const bestPriority = mergePriority[best.source] ?? 99;
|
|
840
|
+
const currentPriority = mergePriority[current.source] ?? 99;
|
|
841
|
+
// Pick by source priority first
|
|
842
|
+
if (currentPriority < bestPriority) {
|
|
843
|
+
return current;
|
|
844
|
+
}
|
|
845
|
+
if (currentPriority > bestPriority) {
|
|
846
|
+
return best;
|
|
847
|
+
}
|
|
848
|
+
// Same source priority - pick higher confidence
|
|
849
|
+
return current.confidence > best.confidence ? current : best;
|
|
850
|
+
});
|
|
822
851
|
// Boost confidence based on number of sources
|
|
823
852
|
let boostedConfidence = bestResult.confidence;
|
|
824
853
|
if (sources.size >= 3) {
|
|
@@ -874,90 +903,110 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
874
903
|
result.providersQueried.push("cosiall");
|
|
875
904
|
freeProviderPromises.push(queryCosiall(contact, addEmail, result));
|
|
876
905
|
}
|
|
877
|
-
// TryKitt.ai
|
|
878
|
-
//
|
|
879
|
-
if (!skipTryKitt && config.trykitt?.apiKey) {
|
|
880
|
-
result.providersQueried.push("trykitt");
|
|
881
|
-
freeProviderPromises.push(queryTryKitt(contact, config.trykitt, linkedInCompanyDomain || discoveredCompanyDomain, addEmail, result));
|
|
882
|
-
}
|
|
906
|
+
// TryKitt.ai is moved to Phase 2 (after domain discovery)
|
|
907
|
+
// It needs name + domain to find emails
|
|
883
908
|
// Wait for all Phase 1 free providers
|
|
884
909
|
await Promise.all(freeProviderPromises);
|
|
885
|
-
// Merge emails
|
|
910
|
+
// Merge emails after Phase 1
|
|
886
911
|
result.emails = mergeAndBoostEmails();
|
|
887
|
-
const phase1BestConfidence = result.emails.length > 0
|
|
888
|
-
? Math.max(...result.emails.map((e) => e.confidence))
|
|
889
|
-
: 0;
|
|
890
912
|
// ==========================================================================
|
|
891
|
-
// Phase 2: Domain Discovery + Pattern Guessing
|
|
892
|
-
//
|
|
913
|
+
// Phase 2: Domain Discovery + FREE Email Finding (TryKitt, Pattern Guessing)
|
|
914
|
+
// FREE providers ALWAYS run - no reason to skip them
|
|
893
915
|
// ==========================================================================
|
|
894
916
|
let companyDomain = linkedInCompanyDomain || discoveredCompanyDomain;
|
|
895
|
-
//
|
|
896
|
-
if
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
const
|
|
904
|
-
if (
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
if (
|
|
911
|
-
|
|
912
|
-
if (companyDomain) {
|
|
913
|
-
discoveredCompanyDomain = companyDomain;
|
|
914
|
-
}
|
|
917
|
+
// Try LinkedIn Company Lookup for domain discovery
|
|
918
|
+
// Only if we don't have a domain yet
|
|
919
|
+
if (!companyDomain &&
|
|
920
|
+
!skipPatternGuessing &&
|
|
921
|
+
!options.skipLinkedInCompanyLookup &&
|
|
922
|
+
config.linkedInCompanyLookup) {
|
|
923
|
+
const companyUrn = contact.currentPositions?.[0]?.companyUrn;
|
|
924
|
+
if (companyUrn) {
|
|
925
|
+
const companyId = extractCompanyIdFromUrn(companyUrn);
|
|
926
|
+
if (companyId) {
|
|
927
|
+
result.providersQueried.push("linkedin");
|
|
928
|
+
try {
|
|
929
|
+
const company = await config.linkedInCompanyLookup(companyId);
|
|
930
|
+
if (company?.websiteUrl) {
|
|
931
|
+
companyDomain = extractDomainFromUrl(company.websiteUrl);
|
|
932
|
+
if (companyDomain) {
|
|
933
|
+
discoveredCompanyDomain = companyDomain;
|
|
915
934
|
}
|
|
916
935
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
}
|
|
936
|
+
}
|
|
937
|
+
catch (err) {
|
|
938
|
+
result.errors?.push(`LinkedIn: ${err instanceof Error ? err.message : "Company lookup failed"}`);
|
|
920
939
|
}
|
|
921
940
|
}
|
|
922
941
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
942
|
+
}
|
|
943
|
+
// Store discovered domain in result for visibility
|
|
944
|
+
if (discoveredCompanyDomain && !linkedInCompanyDomain) {
|
|
945
|
+
result.discoveredCompanyDomain = discoveredCompanyDomain;
|
|
946
|
+
}
|
|
947
|
+
// TryKitt.ai lookup (FREE for individuals - unlimited)
|
|
948
|
+
// AI-powered email finder - runs after domain discovery
|
|
949
|
+
if (!skipTryKitt && config.trykitt?.apiKey && companyDomain) {
|
|
950
|
+
result.providersQueried.push("trykitt");
|
|
951
|
+
await queryTryKitt(contact, config.trykitt, companyDomain, addEmail, result);
|
|
952
|
+
// Re-merge after TryKitt
|
|
953
|
+
result.emails = mergeAndBoostEmails();
|
|
954
|
+
}
|
|
955
|
+
// Run pattern guessing if we have a domain (from LinkedIn contact, SmartProspect, or LinkedIn API)
|
|
956
|
+
if (!skipPatternGuessing && companyDomain) {
|
|
957
|
+
result.providersQueried.push("pattern");
|
|
958
|
+
await queryPatternGuessing(contact, companyDomain, addEmail, result);
|
|
959
|
+
// Re-merge after pattern guessing
|
|
960
|
+
result.emails = mergeAndBoostEmails();
|
|
961
|
+
}
|
|
962
|
+
// ==========================================================================
|
|
963
|
+
// Check if we already have a verified BUSINESS email from a trusted source
|
|
964
|
+
// If so, skip BounceBan and paid providers entirely
|
|
965
|
+
//
|
|
966
|
+
// NOTE: LDD and Cosiall can return personal emails (Gmail, Yahoo, etc.)
|
|
967
|
+
// Don't stop searching if we only have personal emails - keep looking for business.
|
|
968
|
+
// ==========================================================================
|
|
969
|
+
const hasVerifiedBusinessEmail = result.emails.some((e) => e.verified &&
|
|
970
|
+
e.type === "business" &&
|
|
971
|
+
(e.source === "smartprospect" ||
|
|
972
|
+
e.source === "ldd" ||
|
|
973
|
+
e.source === "cosiall" ||
|
|
974
|
+
e.source === "trykitt"));
|
|
975
|
+
const phase2BestConfidence = result.emails.length > 0
|
|
976
|
+
? Math.max(...result.emails.map((e) => e.confidence))
|
|
977
|
+
: 0;
|
|
978
|
+
// BounceBan catch-all verification (costs $0.008/lookup)
|
|
979
|
+
// SKIP if we already have a verified BUSINESS email from SmartProspect/Cosiall/TryKitt
|
|
980
|
+
// Only run to verify pattern-guessed emails when no trusted source found a business email
|
|
981
|
+
// NOTE: LDD personal emails don't count - we still want to find the business email
|
|
982
|
+
if (!skipBounceBan &&
|
|
983
|
+
config.bounceban?.apiKey &&
|
|
984
|
+
result.emails.length > 0 &&
|
|
985
|
+
!hasVerifiedBusinessEmail && // Skip if we have a verified BUSINESS email
|
|
986
|
+
phase2BestConfidence < paidProviderThreshold) {
|
|
987
|
+
// Only verify pattern-guessed emails (the ones we're uncertain about)
|
|
988
|
+
const emailsToVerify = result.emails.filter((e) => e.source === "pattern" && !e.verified);
|
|
989
|
+
if (emailsToVerify.length > 0) {
|
|
990
|
+
result.providersQueried.push("bounceban");
|
|
991
|
+
await queryBounceBan(emailsToVerify, config.bounceban, addEmail, result);
|
|
992
|
+
// Re-merge after BounceBan verification
|
|
932
993
|
result.emails = mergeAndBoostEmails();
|
|
933
994
|
}
|
|
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
|
-
}
|
|
950
995
|
}
|
|
951
996
|
// Calculate final best confidence
|
|
952
997
|
const finalBestConfidence = result.emails.length > 0
|
|
953
998
|
? Math.max(...result.emails.map((e) => e.confidence))
|
|
954
999
|
: 0;
|
|
955
1000
|
// ==========================================================================
|
|
956
|
-
// Phase 3: PAID providers as
|
|
1001
|
+
// Phase 3: PAID providers as LAST RESORT (Hunter then Snovio)
|
|
1002
|
+
// SKIP if we already have a verified BUSINESS email
|
|
1003
|
+
// NOTE: LDD personal emails don't count - we still want to find the business email
|
|
957
1004
|
// ==========================================================================
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
//
|
|
1005
|
+
const hasAnyVerifiedBusinessEmail = result.emails.some((e) => e.verified && e.type === "business");
|
|
1006
|
+
if (!skipPaidProviders &&
|
|
1007
|
+
!hasAnyVerifiedBusinessEmail && // Skip if we have a verified BUSINESS email
|
|
1008
|
+
finalBestConfidence < paidProviderThreshold) {
|
|
1009
|
+
// Hunter.io - Email Finder ($0.005/lookup) - try first
|
|
961
1010
|
if (config.hunter?.apiKey) {
|
|
962
1011
|
result.providersQueried.push("hunter");
|
|
963
1012
|
// Extract linkedin handle from contact if available
|
|
@@ -980,10 +1029,15 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
980
1029
|
// Re-merge after Hunter
|
|
981
1030
|
result.emails = mergeAndBoostEmails();
|
|
982
1031
|
}
|
|
983
|
-
//
|
|
1032
|
+
// Check if Hunter found a verified email - if so, skip Snovio
|
|
1033
|
+
const hunterFoundVerified = result.emails.some((e) => e.source === "hunter" && e.verified);
|
|
1034
|
+
// Snovio - Email Finder ($0.02/lookup) - ONLY if Hunter didn't find anything
|
|
1035
|
+
// This is the absolute last resort since it's the most expensive
|
|
984
1036
|
if (config.snovio?.clientId &&
|
|
985
1037
|
config.snovio?.clientSecret &&
|
|
986
|
-
companyDomain
|
|
1038
|
+
companyDomain &&
|
|
1039
|
+
!hunterFoundVerified // Skip if Hunter already found a verified email
|
|
1040
|
+
) {
|
|
987
1041
|
result.providersQueried.push("snovio");
|
|
988
1042
|
await querySnovio(contact, config.snovio, companyDomain, addEmail, result);
|
|
989
1043
|
// Re-merge after Snovio
|
|
@@ -1217,20 +1271,26 @@ async function queryBounceBan(emailsToVerify, bouncebanConfig, addEmail, result)
|
|
|
1217
1271
|
useWaterfall: bouncebanConfig.useWaterfall ?? true,
|
|
1218
1272
|
});
|
|
1219
1273
|
if (verification && verification.status === "success") {
|
|
1220
|
-
//
|
|
1221
|
-
|
|
1274
|
+
// BounceBan specializes in catch-all verification with 97%+ accuracy
|
|
1275
|
+
// Trust the score they return - don't artificially cap it
|
|
1276
|
+
const confidence = verification.score;
|
|
1222
1277
|
const verified = verification.result === "deliverable";
|
|
1223
|
-
//
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1278
|
+
// For catch-all domains:
|
|
1279
|
+
// - "deliverable" = BounceBan's algorithm determined this specific email exists (97%+ accuracy)
|
|
1280
|
+
// - "risky" = Could not determine with high confidence
|
|
1281
|
+
// - "undeliverable" = Email definitely doesn't exist
|
|
1227
1282
|
// Only add if it's potentially deliverable
|
|
1228
1283
|
if (verification.result === "deliverable" ||
|
|
1229
1284
|
verification.result === "risky") {
|
|
1285
|
+
// For "deliverable" on catch-all, BounceBan has verified this is the REAL email
|
|
1286
|
+
// For "risky", we keep lower confidence as BounceBan couldn't determine
|
|
1287
|
+
const finalConfidence = verification.result === "deliverable"
|
|
1288
|
+
? Math.max(confidence, 85) // Deliverable = high confidence (BounceBan verified it)
|
|
1289
|
+
: Math.min(confidence, 60); // Risky = lower confidence
|
|
1230
1290
|
addEmail({
|
|
1231
1291
|
email: verification.email,
|
|
1232
1292
|
source: "bounceban",
|
|
1233
|
-
confidence,
|
|
1293
|
+
confidence: finalConfidence,
|
|
1234
1294
|
type: "business",
|
|
1235
1295
|
verified,
|
|
1236
1296
|
isCatchAll: verification.is_accept_all,
|
|
@@ -1309,20 +1369,39 @@ async function querySmartProspect(contact, smartProspectConfig, minMatchConfiden
|
|
|
1309
1369
|
.split("/")[0]
|
|
1310
1370
|
.toLowerCase();
|
|
1311
1371
|
}
|
|
1312
|
-
// Fetch email for matched contact
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
]);
|
|
1372
|
+
// Fetch email for matched contact (pass filter_id from search for polling)
|
|
1373
|
+
const searchFilterId = searchResponse.data.filter_id;
|
|
1374
|
+
const fetchResponse = await client.fetch([matchResult.smartProspectContact.id], searchFilterId);
|
|
1316
1375
|
if (fetchResponse.success && fetchResponse.data.list.length > 0) {
|
|
1317
1376
|
const enrichedContact = fetchResponse.data.list[0];
|
|
1318
1377
|
if (enrichedContact.email) {
|
|
1378
|
+
// Calculate email confidence based on verification status AND match quality
|
|
1379
|
+
// SmartProspect verification status: "valid", "catch_all", "invalid", etc.
|
|
1380
|
+
const isVerified = enrichedContact.verificationStatus === "valid" ||
|
|
1381
|
+
enrichedContact.verificationStatus === "verified";
|
|
1382
|
+
const isCatchAll = enrichedContact.verificationStatus === "catch_all";
|
|
1383
|
+
// Base confidence from match quality (60-100%)
|
|
1384
|
+
let emailConfidence = matchResult.confidence;
|
|
1385
|
+
// Boost confidence for verified emails from SmartProspect
|
|
1386
|
+
// A verified email from SmartProspect is more reliable than pattern guessing
|
|
1387
|
+
if (isVerified) {
|
|
1388
|
+
emailConfidence = Math.max(emailConfidence, 90); // Verified = at least 90%
|
|
1389
|
+
}
|
|
1390
|
+
else if (isCatchAll) {
|
|
1391
|
+
emailConfidence = Math.max(emailConfidence, 80); // Catch-all = at least 80%
|
|
1392
|
+
}
|
|
1393
|
+
else if (enrichedContact.emailDeliverability > 0) {
|
|
1394
|
+
// Use deliverability score if available
|
|
1395
|
+
emailConfidence = Math.max(emailConfidence, enrichedContact.emailDeliverability * 100);
|
|
1396
|
+
}
|
|
1319
1397
|
addEmail({
|
|
1320
1398
|
email: enrichedContact.email,
|
|
1321
1399
|
source: "smartprospect",
|
|
1322
|
-
confidence:
|
|
1400
|
+
confidence: emailConfidence,
|
|
1323
1401
|
type: "business",
|
|
1324
|
-
verified:
|
|
1402
|
+
verified: isVerified || isCatchAll,
|
|
1325
1403
|
deliverability: enrichedContact.emailDeliverability,
|
|
1404
|
+
isCatchAll,
|
|
1326
1405
|
metadata: {
|
|
1327
1406
|
matchQuality: matchResult.quality,
|
|
1328
1407
|
matchedFields: matchResult.matchedFields,
|
|
@@ -1330,6 +1409,7 @@ async function querySmartProspect(contact, smartProspectConfig, minMatchConfiden
|
|
|
1330
1409
|
company: enrichedContact.company?.name,
|
|
1331
1410
|
companyWebsite: companyWebsite,
|
|
1332
1411
|
title: enrichedContact.title,
|
|
1412
|
+
verificationStatus: enrichedContact.verificationStatus,
|
|
1333
1413
|
},
|
|
1334
1414
|
});
|
|
1335
1415
|
}
|
|
@@ -99,14 +99,16 @@ function extractEmailsToVerify(candidate) {
|
|
|
99
99
|
*/
|
|
100
100
|
async function verifyEmailStandard(email, apiKey, apiUrl, timeoutMs, useDeepVerify) {
|
|
101
101
|
const params = new URLSearchParams({
|
|
102
|
-
api_key: apiKey,
|
|
103
102
|
email: email,
|
|
104
103
|
});
|
|
105
104
|
if (useDeepVerify) {
|
|
106
105
|
params.set("mode", "deepverify");
|
|
107
106
|
}
|
|
107
|
+
const url = `${apiUrl}/v1/verify/single?${params.toString()}`;
|
|
108
108
|
try {
|
|
109
|
-
const response = await (0, http_retry_1.getWithRetry)(
|
|
109
|
+
const response = await (0, http_retry_1.getWithRetry)(url, {
|
|
110
|
+
Authorization: apiKey,
|
|
111
|
+
}, {
|
|
110
112
|
retries: 1,
|
|
111
113
|
backoffMs: 500,
|
|
112
114
|
timeoutMs,
|
|
@@ -114,7 +116,8 @@ async function verifyEmailStandard(email, apiKey, apiUrl, timeoutMs, useDeepVeri
|
|
|
114
116
|
});
|
|
115
117
|
return response;
|
|
116
118
|
}
|
|
117
|
-
catch {
|
|
119
|
+
catch (err) {
|
|
120
|
+
console.error("[BounceBan] Standard verification failed:", err instanceof Error ? err.message : err);
|
|
118
121
|
return null;
|
|
119
122
|
}
|
|
120
123
|
}
|
|
@@ -128,15 +131,17 @@ async function verifyEmailStandard(email, apiKey, apiUrl, timeoutMs, useDeepVeri
|
|
|
128
131
|
*/
|
|
129
132
|
async function verifyEmailWaterfall(email, apiKey, waterfallApiUrl, timeoutMs, useDeepVerify) {
|
|
130
133
|
const params = new URLSearchParams({
|
|
131
|
-
api_key: apiKey,
|
|
132
134
|
email: email,
|
|
133
135
|
timeout: String(Math.floor(timeoutMs / 1000)), // Convert to seconds
|
|
134
136
|
});
|
|
135
137
|
if (useDeepVerify) {
|
|
136
138
|
params.set("mode", "deepverify");
|
|
137
139
|
}
|
|
140
|
+
const url = `${waterfallApiUrl}/v1/verify/single?${params.toString()}`;
|
|
138
141
|
try {
|
|
139
|
-
const response = await (0, http_retry_1.getWithRetry)(
|
|
142
|
+
const response = await (0, http_retry_1.getWithRetry)(url, {
|
|
143
|
+
Authorization: apiKey,
|
|
144
|
+
}, {
|
|
140
145
|
retries: 2, // Waterfall allows free retries within 30 mins
|
|
141
146
|
backoffMs: 5000, // Wait 5s between retries as recommended
|
|
142
147
|
timeoutMs: timeoutMs + 10000, // Add buffer for network
|
|
@@ -144,7 +149,8 @@ async function verifyEmailWaterfall(email, apiKey, waterfallApiUrl, timeoutMs, u
|
|
|
144
149
|
});
|
|
145
150
|
return response;
|
|
146
151
|
}
|
|
147
|
-
catch {
|
|
152
|
+
catch (err) {
|
|
153
|
+
console.error("[BounceBan] Waterfall verification failed:", err instanceof Error ? err.message : err);
|
|
148
154
|
return null;
|
|
149
155
|
}
|
|
150
156
|
}
|
|
@@ -256,9 +262,9 @@ async function submitBulkVerification(emails, apiKey, apiUrl, useDeepVerify) {
|
|
|
256
262
|
method: "POST",
|
|
257
263
|
headers: {
|
|
258
264
|
"Content-Type": "application/json",
|
|
265
|
+
Authorization: apiKey,
|
|
259
266
|
},
|
|
260
267
|
body: JSON.stringify({
|
|
261
|
-
api_key: apiKey,
|
|
262
268
|
emails: emails,
|
|
263
269
|
mode: useDeepVerify ? "deepverify" : "regular",
|
|
264
270
|
}),
|
|
@@ -279,10 +285,13 @@ async function submitBulkVerification(emails, apiKey, apiUrl, useDeepVerify) {
|
|
|
279
285
|
async function checkBulkStatus(taskId, apiKey, apiUrl) {
|
|
280
286
|
try {
|
|
281
287
|
const params = new URLSearchParams({
|
|
282
|
-
api_key: apiKey,
|
|
283
288
|
id: taskId,
|
|
284
289
|
});
|
|
285
|
-
const response = await fetch(`${apiUrl}/v1/verify/bulk/status?${params.toString()}
|
|
290
|
+
const response = await fetch(`${apiUrl}/v1/verify/bulk/status?${params.toString()}`, {
|
|
291
|
+
headers: {
|
|
292
|
+
Authorization: apiKey,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
286
295
|
if (!response.ok) {
|
|
287
296
|
return null;
|
|
288
297
|
}
|
|
@@ -301,9 +310,9 @@ async function fetchBulkResults(taskId, apiKey, apiUrl, page = 1, perPage = 100)
|
|
|
301
310
|
method: "POST",
|
|
302
311
|
headers: {
|
|
303
312
|
"Content-Type": "application/json",
|
|
313
|
+
Authorization: apiKey,
|
|
304
314
|
},
|
|
305
315
|
body: JSON.stringify({
|
|
306
|
-
api_key: apiKey,
|
|
307
316
|
id: taskId,
|
|
308
317
|
page: page,
|
|
309
318
|
per_page: perPage,
|
|
@@ -68,104 +68,112 @@ function createConstructProvider(config) {
|
|
|
68
68
|
const timeoutMs = config?.timeoutMs ?? 5000;
|
|
69
69
|
const smtpVerifyDelayMs = config?.smtpVerifyDelayMs ?? 2000; // Delay between SMTP checks
|
|
70
70
|
async function fetchEmail(candidate) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const result = results[0];
|
|
113
|
-
if (result.exists === true) {
|
|
114
|
-
// Email confirmed to exist!
|
|
115
|
-
attemptedPatterns.push({ email, status: "exists" });
|
|
116
|
-
validEmails.push({
|
|
117
|
-
email: result.email,
|
|
118
|
-
verified: true,
|
|
119
|
-
confidence: 95, // High confidence - SMTP verified
|
|
120
|
-
isCatchAll: false,
|
|
121
|
-
metadata: {
|
|
122
|
-
pattern: result.email.split("@")[0],
|
|
123
|
-
mxRecords: catchAllResult.mxRecords,
|
|
124
|
-
smtpVerified: true,
|
|
125
|
-
attemptedPatterns, // Include what was tried
|
|
126
|
-
},
|
|
71
|
+
try {
|
|
72
|
+
const { firstName: first, lastName: last } = (0, candidate_parser_1.extractName)(candidate);
|
|
73
|
+
const { domain } = (0, candidate_parser_1.extractCompany)(candidate);
|
|
74
|
+
// Skip if missing required fields
|
|
75
|
+
if (!first || !domain) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
// Skip personal domains
|
|
79
|
+
if ((0, personal_domains_1.isPersonalDomain)(domain)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const candidates = buildCandidates({ first, last, domain });
|
|
83
|
+
const max = Math.min(candidates.length, maxAttempts);
|
|
84
|
+
// First, check if domain is catch-all
|
|
85
|
+
const catchAllResult = await (0, mx_1.checkDomainCatchAll)(domain, {
|
|
86
|
+
timeoutMs: 10000,
|
|
87
|
+
});
|
|
88
|
+
const isCatchAll = catchAllResult.isCatchAll;
|
|
89
|
+
// Collect ALL valid email patterns (not just first match)
|
|
90
|
+
const validEmails = [];
|
|
91
|
+
// Track all attempted patterns for debugging
|
|
92
|
+
const attemptedPatterns = [];
|
|
93
|
+
// If NOT catch-all, we can verify each email via SMTP
|
|
94
|
+
if (isCatchAll === false) {
|
|
95
|
+
// Verify emails one by one, stop when we find a valid one
|
|
96
|
+
const emailsToVerify = candidates.slice(0, max);
|
|
97
|
+
for (let i = 0; i < emailsToVerify.length; i++) {
|
|
98
|
+
const email = emailsToVerify[i];
|
|
99
|
+
// If we already found a valid email, skip the rest
|
|
100
|
+
if (validEmails.length > 0) {
|
|
101
|
+
attemptedPatterns.push({ email, status: "skipped" });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// Add delay between checks (except first one)
|
|
105
|
+
if (i > 0) {
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, smtpVerifyDelayMs));
|
|
107
|
+
}
|
|
108
|
+
// Verify single email
|
|
109
|
+
const results = await (0, mx_1.verifyEmailsExist)([email], {
|
|
110
|
+
delayMs: 0,
|
|
111
|
+
timeoutMs,
|
|
127
112
|
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
113
|
+
const result = results[0];
|
|
114
|
+
if (result.exists === true) {
|
|
115
|
+
// Email confirmed to exist!
|
|
116
|
+
attemptedPatterns.push({ email, status: "exists" });
|
|
117
|
+
validEmails.push({
|
|
118
|
+
email: result.email,
|
|
119
|
+
verified: true,
|
|
120
|
+
confidence: 80, // Good confidence - SMTP verified pattern guess
|
|
121
|
+
// Note: 80% is appropriate for pattern guessing with SMTP verification
|
|
122
|
+
// Higher-priority sources like SmartProspect (90%) should rank above
|
|
123
|
+
isCatchAll: false,
|
|
124
|
+
metadata: {
|
|
125
|
+
pattern: result.email.split("@")[0],
|
|
126
|
+
mxRecords: catchAllResult.mxRecords,
|
|
127
|
+
smtpVerified: true,
|
|
128
|
+
attemptedPatterns, // Include what was tried
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
// Found one! Stop checking more
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
else if (result.exists === false) {
|
|
135
|
+
attemptedPatterns.push({ email, status: "not_found" });
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
attemptedPatterns.push({ email, status: "unknown" });
|
|
139
|
+
}
|
|
133
140
|
}
|
|
134
|
-
|
|
135
|
-
|
|
141
|
+
// If no valid email found, include attempted patterns in metadata
|
|
142
|
+
if (validEmails.length === 0 && attemptedPatterns.length > 0) {
|
|
143
|
+
// Return null but could add metadata about attempts
|
|
136
144
|
}
|
|
137
145
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
mxRecords: verification.mxRecords,
|
|
157
|
-
smtpVerified: false,
|
|
158
|
-
},
|
|
159
|
-
});
|
|
146
|
+
else {
|
|
147
|
+
// Catch-all or unknown - fall back to MX verification only
|
|
148
|
+
for (let i = 0; i < max; i++) {
|
|
149
|
+
const email = candidates[i];
|
|
150
|
+
const verification = await (0, mx_1.verifyEmailMx)(email, { timeoutMs });
|
|
151
|
+
if (verification.valid === true && verification.confidence >= 50) {
|
|
152
|
+
validEmails.push({
|
|
153
|
+
email,
|
|
154
|
+
verified: true,
|
|
155
|
+
confidence: verification.confidence,
|
|
156
|
+
isCatchAll: isCatchAll ?? undefined,
|
|
157
|
+
metadata: {
|
|
158
|
+
pattern: email.split("@")[0],
|
|
159
|
+
mxRecords: verification.mxRecords,
|
|
160
|
+
smtpVerified: false,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
160
164
|
}
|
|
161
165
|
}
|
|
166
|
+
if (validEmails.length === 0) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
// Sort by confidence
|
|
170
|
+
validEmails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
|
|
171
|
+
return { emails: validEmails };
|
|
162
172
|
}
|
|
163
|
-
|
|
173
|
+
catch (err) {
|
|
174
|
+
console.error("[Construct] Email pattern generation failed:", err instanceof Error ? err.message : err);
|
|
164
175
|
return null;
|
|
165
176
|
}
|
|
166
|
-
// Sort by confidence
|
|
167
|
-
validEmails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
|
|
168
|
-
return { emails: validEmails };
|
|
169
177
|
}
|
|
170
178
|
// Mark provider name for orchestrator
|
|
171
179
|
fetchEmail.__name = "construct";
|
|
@@ -98,8 +98,8 @@ function createCosiallProvider(config) {
|
|
|
98
98
|
}));
|
|
99
99
|
return { emails };
|
|
100
100
|
}
|
|
101
|
-
catch {
|
|
102
|
-
|
|
101
|
+
catch (err) {
|
|
102
|
+
console.error("[Cosiall] Profile email lookup failed:", err instanceof Error ? err.message : err);
|
|
103
103
|
return null;
|
|
104
104
|
}
|
|
105
105
|
}
|