linkedin-secret-sauce 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/enrichment/auth/index.d.ts +4 -0
- package/dist/enrichment/auth/index.js +12 -0
- package/dist/enrichment/auth/smartlead-auth.d.ts +50 -0
- package/dist/enrichment/auth/smartlead-auth.js +156 -0
- package/dist/enrichment/index.d.ts +44 -0
- package/dist/enrichment/index.js +244 -0
- package/dist/enrichment/orchestrator.d.ts +31 -0
- package/dist/enrichment/orchestrator.js +218 -0
- package/dist/enrichment/providers/apollo.d.ts +11 -0
- package/dist/enrichment/providers/apollo.js +136 -0
- package/dist/enrichment/providers/construct.d.ts +11 -0
- package/dist/enrichment/providers/construct.js +107 -0
- package/dist/enrichment/providers/dropcontact.d.ts +16 -0
- package/dist/enrichment/providers/dropcontact.js +37 -0
- package/dist/enrichment/providers/hunter.d.ts +11 -0
- package/dist/enrichment/providers/hunter.js +162 -0
- package/dist/enrichment/providers/index.d.ts +9 -0
- package/dist/enrichment/providers/index.js +18 -0
- package/dist/enrichment/providers/ldd.d.ts +11 -0
- package/dist/enrichment/providers/ldd.js +110 -0
- package/dist/enrichment/providers/smartprospect.d.ts +19 -0
- package/dist/enrichment/providers/smartprospect.js +333 -0
- package/dist/enrichment/types.d.ts +343 -0
- package/dist/enrichment/types.js +31 -0
- package/dist/enrichment/utils/disposable-domains.d.ts +24 -0
- package/dist/enrichment/utils/disposable-domains.js +1011 -0
- package/dist/enrichment/utils/index.d.ts +6 -0
- package/dist/enrichment/utils/index.js +22 -0
- package/dist/enrichment/utils/personal-domains.d.ts +31 -0
- package/dist/enrichment/utils/personal-domains.js +95 -0
- package/dist/enrichment/utils/validation.d.ts +42 -0
- package/dist/enrichment/utils/validation.js +130 -0
- package/dist/enrichment/verification/index.d.ts +4 -0
- package/dist/enrichment/verification/index.js +8 -0
- package/dist/enrichment/verification/mx.d.ts +16 -0
- package/dist/enrichment/verification/mx.js +168 -0
- package/dist/index.d.ts +18 -14
- package/dist/index.js +26 -1
- package/package.json +15 -16
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Email Enrichment Waterfall Orchestrator
|
|
4
|
+
*
|
|
5
|
+
* Runs providers in sequence until a verified email is found that meets
|
|
6
|
+
* the confidence threshold. Supports budget tracking and cost callbacks.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.enrichBusinessEmail = enrichBusinessEmail;
|
|
10
|
+
exports.enrichBatch = enrichBatch;
|
|
11
|
+
const personal_domains_1 = require("./utils/personal-domains");
|
|
12
|
+
const disposable_domains_1 = require("./utils/disposable-domains");
|
|
13
|
+
/**
|
|
14
|
+
* Default provider costs in USD per lookup
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_PROVIDER_COSTS = {
|
|
17
|
+
construct: 0,
|
|
18
|
+
ldd: 0,
|
|
19
|
+
smartprospect: 0.01,
|
|
20
|
+
hunter: 0.005,
|
|
21
|
+
apollo: 0,
|
|
22
|
+
dropcontact: 0.01,
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Normalize provider result to canonical format
|
|
26
|
+
*/
|
|
27
|
+
function normalizeProviderResult(providerName, raw) {
|
|
28
|
+
const iso = new Date().toISOString();
|
|
29
|
+
// No result from provider
|
|
30
|
+
if (!raw || !raw.email) {
|
|
31
|
+
return {
|
|
32
|
+
business_email: null,
|
|
33
|
+
business_email_source: providerName,
|
|
34
|
+
business_email_verified: false,
|
|
35
|
+
last_checked_at: iso,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const email = String(raw.email);
|
|
39
|
+
const confidence = typeof raw.score === 'number'
|
|
40
|
+
? raw.score
|
|
41
|
+
: typeof raw.confidence === 'number'
|
|
42
|
+
? raw.confidence
|
|
43
|
+
: undefined;
|
|
44
|
+
const verified = Boolean(raw.verified);
|
|
45
|
+
return {
|
|
46
|
+
business_email: email,
|
|
47
|
+
business_email_source: providerName,
|
|
48
|
+
business_email_verified: verified,
|
|
49
|
+
business_email_confidence: confidence,
|
|
50
|
+
last_checked_at: iso,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get provider name from function
|
|
55
|
+
*/
|
|
56
|
+
function getProviderName(provider, index) {
|
|
57
|
+
return provider.__name ?? `provider-${index}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get provider cost
|
|
61
|
+
*/
|
|
62
|
+
function getProviderCost(providerName) {
|
|
63
|
+
return DEFAULT_PROVIDER_COSTS[providerName] ?? 0;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Enrich a single candidate with business email
|
|
67
|
+
*/
|
|
68
|
+
async function enrichBusinessEmail(candidate, options) {
|
|
69
|
+
const { providers, maxCostPerEmail = Infinity, confidenceThreshold = 0, retryMs = 200, onCost, logger, } = options;
|
|
70
|
+
// Track remaining budget
|
|
71
|
+
let remaining = Number.isFinite(maxCostPerEmail) && maxCostPerEmail >= 0 ? maxCostPerEmail : Infinity;
|
|
72
|
+
logger?.debug?.('enrichment.start', {
|
|
73
|
+
providers: providers.length,
|
|
74
|
+
confidenceThreshold,
|
|
75
|
+
maxCostPerEmail,
|
|
76
|
+
});
|
|
77
|
+
for (let i = 0; i < providers.length; i++) {
|
|
78
|
+
const provider = providers[i];
|
|
79
|
+
const providerName = getProviderName(provider, i);
|
|
80
|
+
const stepCost = getProviderCost(providerName);
|
|
81
|
+
// Skip if cost exceeds remaining budget
|
|
82
|
+
if (stepCost > 0 && remaining < stepCost) {
|
|
83
|
+
logger?.debug?.('enrichment.skip_budget', {
|
|
84
|
+
provider: providerName,
|
|
85
|
+
cost: stepCost,
|
|
86
|
+
remaining,
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
let raw = null;
|
|
91
|
+
try {
|
|
92
|
+
logger?.debug?.('enrichment.provider.start', { provider: providerName });
|
|
93
|
+
raw = await provider(candidate);
|
|
94
|
+
logger?.debug?.('enrichment.provider.done', {
|
|
95
|
+
provider: providerName,
|
|
96
|
+
hasResult: !!raw?.email,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
logger?.warn?.('enrichment.provider.error', {
|
|
101
|
+
provider: providerName,
|
|
102
|
+
error: error instanceof Error ? error.message : String(error),
|
|
103
|
+
});
|
|
104
|
+
// Retry once after delay on transient errors
|
|
105
|
+
if (retryMs > 0) {
|
|
106
|
+
await new Promise((r) => setTimeout(r, retryMs));
|
|
107
|
+
try {
|
|
108
|
+
raw = await provider(candidate);
|
|
109
|
+
logger?.debug?.('enrichment.provider.retry.done', {
|
|
110
|
+
provider: providerName,
|
|
111
|
+
hasResult: !!raw?.email,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
raw = null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Deduct cost (even on failure - API was called)
|
|
120
|
+
if (stepCost > 0) {
|
|
121
|
+
remaining = Math.max(0, remaining - stepCost);
|
|
122
|
+
if (onCost) {
|
|
123
|
+
try {
|
|
124
|
+
await onCost(providerName, stepCost);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Ignore cost callback errors
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
logger?.debug?.('enrichment.cost.debit', {
|
|
131
|
+
provider: providerName,
|
|
132
|
+
cost: stepCost,
|
|
133
|
+
remaining,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// Normalize result
|
|
137
|
+
const normalized = normalizeProviderResult(providerName, raw);
|
|
138
|
+
// Skip if no email found
|
|
139
|
+
if (!normalized.business_email) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// Filter out personal emails
|
|
143
|
+
if ((0, personal_domains_1.isPersonalEmail)(normalized.business_email)) {
|
|
144
|
+
logger?.debug?.('enrichment.skip_personal', {
|
|
145
|
+
provider: providerName,
|
|
146
|
+
email: normalized.business_email,
|
|
147
|
+
});
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
// Filter out disposable emails
|
|
151
|
+
if ((0, disposable_domains_1.isDisposableEmail)(normalized.business_email)) {
|
|
152
|
+
logger?.debug?.('enrichment.skip_disposable', {
|
|
153
|
+
provider: providerName,
|
|
154
|
+
email: normalized.business_email,
|
|
155
|
+
});
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
// Check confidence threshold
|
|
159
|
+
const score = normalized.business_email_confidence;
|
|
160
|
+
if (confidenceThreshold > 0 && score !== undefined && score < confidenceThreshold) {
|
|
161
|
+
logger?.debug?.('enrichment.skip_low_confidence', {
|
|
162
|
+
provider: providerName,
|
|
163
|
+
score,
|
|
164
|
+
threshold: confidenceThreshold,
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
// Check if verified
|
|
169
|
+
if (!normalized.business_email_verified) {
|
|
170
|
+
logger?.debug?.('enrichment.skip_unverified', {
|
|
171
|
+
provider: providerName,
|
|
172
|
+
email: normalized.business_email,
|
|
173
|
+
});
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// Success!
|
|
177
|
+
logger?.info?.('enrichment.success', {
|
|
178
|
+
provider: providerName,
|
|
179
|
+
email: normalized.business_email,
|
|
180
|
+
confidence: normalized.business_email_confidence,
|
|
181
|
+
});
|
|
182
|
+
return normalized;
|
|
183
|
+
}
|
|
184
|
+
// No provider found a valid email
|
|
185
|
+
logger?.debug?.('enrichment.not_found', {
|
|
186
|
+
candidateHasName: !!(candidate.firstName || candidate.name),
|
|
187
|
+
candidateHasDomain: !!(candidate.domain || candidate.company),
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
business_email: null,
|
|
191
|
+
business_email_source: null,
|
|
192
|
+
business_email_verified: false,
|
|
193
|
+
last_checked_at: new Date().toISOString(),
|
|
194
|
+
status: 'not_found',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Enrich multiple candidates in batches
|
|
199
|
+
*/
|
|
200
|
+
async function enrichBatch(candidates, options) {
|
|
201
|
+
const results = [];
|
|
202
|
+
const batchSize = options.batchSize ?? 50;
|
|
203
|
+
const delayMs = options.delayMs ?? 200;
|
|
204
|
+
for (let i = 0; i < candidates.length; i += batchSize) {
|
|
205
|
+
const chunk = candidates.slice(i, i + batchSize);
|
|
206
|
+
// Process batch in parallel
|
|
207
|
+
const batchResults = await Promise.all(chunk.map((candidate) => enrichBusinessEmail(candidate, options)));
|
|
208
|
+
// Combine results with candidates
|
|
209
|
+
batchResults.forEach((result, idx) => {
|
|
210
|
+
results.push({ candidate: chunk[idx], ...result });
|
|
211
|
+
});
|
|
212
|
+
// Delay between batches to avoid rate limiting
|
|
213
|
+
if (i + batchSize < candidates.length && delayMs > 0) {
|
|
214
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return results;
|
|
218
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apollo.io Provider
|
|
3
|
+
*
|
|
4
|
+
* Apollo.io public API for email finding.
|
|
5
|
+
* Uses mixed_people/search endpoint.
|
|
6
|
+
*/
|
|
7
|
+
import type { EnrichmentCandidate, ProviderResult, ApolloConfig } from "../types";
|
|
8
|
+
/**
|
|
9
|
+
* Create the Apollo provider function
|
|
10
|
+
*/
|
|
11
|
+
export declare function createApolloProvider(config: ApolloConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Apollo.io Provider
|
|
4
|
+
*
|
|
5
|
+
* Apollo.io public API for email finding.
|
|
6
|
+
* Uses mixed_people/search endpoint.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createApolloProvider = createApolloProvider;
|
|
10
|
+
const API_BASE = "https://api.apollo.io/v1";
|
|
11
|
+
/**
|
|
12
|
+
* Delay helper for retry logic
|
|
13
|
+
*/
|
|
14
|
+
async function delay(ms) {
|
|
15
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Check if value is truthy
|
|
19
|
+
*/
|
|
20
|
+
function truthy(v) {
|
|
21
|
+
return v !== undefined && v !== null && String(v).length > 0;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Map Apollo verification status to boolean
|
|
25
|
+
*/
|
|
26
|
+
function mapVerified(status) {
|
|
27
|
+
if (!status)
|
|
28
|
+
return undefined;
|
|
29
|
+
const s = String(status).toLowerCase();
|
|
30
|
+
if (s === "verified" || s === "deliverable")
|
|
31
|
+
return true;
|
|
32
|
+
if (s === "invalid" || s === "undeliverable")
|
|
33
|
+
return false;
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* HTTP request with retry on 429 rate limit
|
|
38
|
+
*/
|
|
39
|
+
async function requestWithRetry(url, init, retries = 1, backoffMs = 200) {
|
|
40
|
+
let lastErr;
|
|
41
|
+
for (let i = 0; i <= retries; i++) {
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(url, init);
|
|
44
|
+
// Retry on rate limit
|
|
45
|
+
if (res?.status === 429 && i < retries) {
|
|
46
|
+
await delay(backoffMs * Math.pow(2, i));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!res || res.status >= 400) {
|
|
50
|
+
lastErr = new Error(`apollo_http_${res?.status ?? "error"}`);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
const json = (await res.json());
|
|
54
|
+
return json;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
lastErr = err;
|
|
58
|
+
if (i < retries) {
|
|
59
|
+
await delay(backoffMs * Math.pow(2, i));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw lastErr ?? new Error("apollo_http_error");
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extract inputs from candidate
|
|
68
|
+
*/
|
|
69
|
+
function extractInputs(candidate) {
|
|
70
|
+
const name = candidate.name || candidate.full_name || candidate.fullName || undefined;
|
|
71
|
+
const org = candidate.company || candidate.organization || candidate.org || undefined;
|
|
72
|
+
const domain = candidate.domain ||
|
|
73
|
+
candidate.companyDomain ||
|
|
74
|
+
candidate.company_domain ||
|
|
75
|
+
undefined;
|
|
76
|
+
return { name, org, domain };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create the Apollo provider function
|
|
80
|
+
*/
|
|
81
|
+
function createApolloProvider(config) {
|
|
82
|
+
const { apiKey } = config;
|
|
83
|
+
if (!apiKey) {
|
|
84
|
+
// Return a no-op provider if not configured
|
|
85
|
+
const noopProvider = async () => null;
|
|
86
|
+
noopProvider.__name = "apollo";
|
|
87
|
+
return noopProvider;
|
|
88
|
+
}
|
|
89
|
+
async function fetchEmail(candidate) {
|
|
90
|
+
const { name, org, domain } = extractInputs(candidate);
|
|
91
|
+
// Build request body
|
|
92
|
+
const body = { page: 1, per_page: 1 };
|
|
93
|
+
if (truthy(name))
|
|
94
|
+
body["q_person_name"] = String(name);
|
|
95
|
+
if (truthy(domain))
|
|
96
|
+
body["organization_domains"] = [String(domain)];
|
|
97
|
+
else if (truthy(org))
|
|
98
|
+
body["q_organization_name"] = String(org);
|
|
99
|
+
const url = `${API_BASE}/mixed_people/search`;
|
|
100
|
+
const init = {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: {
|
|
103
|
+
"content-type": "application/json",
|
|
104
|
+
"X-Api-Key": String(apiKey),
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify(body),
|
|
107
|
+
};
|
|
108
|
+
try {
|
|
109
|
+
const json = await requestWithRetry(url, init, 1, 100);
|
|
110
|
+
// Parse response - handle multiple possible shapes
|
|
111
|
+
const people = (json &&
|
|
112
|
+
(json.people || json.matches || json.data?.people));
|
|
113
|
+
if (!Array.isArray(people) || people.length === 0)
|
|
114
|
+
return null;
|
|
115
|
+
const p = people[0] || {};
|
|
116
|
+
const email = p.email ??
|
|
117
|
+
p.primary_email ??
|
|
118
|
+
p.emails?.[0]?.email ??
|
|
119
|
+
null;
|
|
120
|
+
if (!email)
|
|
121
|
+
return null;
|
|
122
|
+
const score = typeof p.email_confidence === "number"
|
|
123
|
+
? p.email_confidence
|
|
124
|
+
: Number(p.email_confidence ?? 0) || undefined;
|
|
125
|
+
const verified = mapVerified(p.email_status ??
|
|
126
|
+
p.emails?.[0]?.status);
|
|
127
|
+
return { email, verified, score };
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Mark provider name for orchestrator
|
|
134
|
+
fetchEmail.__name = "apollo";
|
|
135
|
+
return fetchEmail;
|
|
136
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email Construction Provider
|
|
3
|
+
*
|
|
4
|
+
* Generates email pattern candidates based on name + domain, then verifies via MX check.
|
|
5
|
+
* This is a FREE provider that doesn't require any API keys.
|
|
6
|
+
*/
|
|
7
|
+
import type { EnrichmentCandidate, ProviderResult, ConstructConfig } from '../types';
|
|
8
|
+
/**
|
|
9
|
+
* Create the construct provider function
|
|
10
|
+
*/
|
|
11
|
+
export declare function createConstructProvider(config?: ConstructConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Email Construction Provider
|
|
4
|
+
*
|
|
5
|
+
* Generates email pattern candidates based on name + domain, then verifies via MX check.
|
|
6
|
+
* This is a FREE provider that doesn't require any API keys.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createConstructProvider = createConstructProvider;
|
|
10
|
+
const mx_1 = require("../verification/mx");
|
|
11
|
+
const personal_domains_1 = require("../utils/personal-domains");
|
|
12
|
+
const validation_1 = require("../utils/validation");
|
|
13
|
+
/**
|
|
14
|
+
* Build all email pattern candidates for a person
|
|
15
|
+
*/
|
|
16
|
+
function buildCandidates(input) {
|
|
17
|
+
const domain = String(input.domain || '').toLowerCase();
|
|
18
|
+
const first = (0, validation_1.cleanNamePart)(input.first || '');
|
|
19
|
+
const last = (0, validation_1.cleanNamePart)(input.last || '');
|
|
20
|
+
const fl = first ? first[0] : '';
|
|
21
|
+
const locals = [];
|
|
22
|
+
// Patterns with both first and last name
|
|
23
|
+
if (first && last) {
|
|
24
|
+
locals.push(`${first}.${last}`); // john.doe
|
|
25
|
+
locals.push(`${fl}.${last}`); // j.doe
|
|
26
|
+
locals.push(`${first}${last[0] ?? ''}`); // johnd
|
|
27
|
+
locals.push(`${fl}${last}`); // jdoe
|
|
28
|
+
locals.push(`${first}_${last}`); // john_doe
|
|
29
|
+
locals.push(`${last}.${first}`); // doe.john
|
|
30
|
+
}
|
|
31
|
+
// Single name patterns
|
|
32
|
+
if (first)
|
|
33
|
+
locals.push(first); // john
|
|
34
|
+
if (last)
|
|
35
|
+
locals.push(last); // doe
|
|
36
|
+
// Deduplicate while preserving order
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
const emails = [];
|
|
39
|
+
for (const local of locals) {
|
|
40
|
+
if (!local)
|
|
41
|
+
continue;
|
|
42
|
+
const email = `${local}@${domain}`;
|
|
43
|
+
if (!seen.has(email)) {
|
|
44
|
+
seen.add(email);
|
|
45
|
+
emails.push(email);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return emails;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Extract name components from candidate
|
|
52
|
+
*/
|
|
53
|
+
function extractNames(candidate) {
|
|
54
|
+
const firstName = candidate.firstName ||
|
|
55
|
+
candidate.first_name ||
|
|
56
|
+
candidate.first ||
|
|
57
|
+
candidate.name?.split(' ')?.[0] ||
|
|
58
|
+
'';
|
|
59
|
+
const lastName = candidate.lastName ||
|
|
60
|
+
candidate.last_name ||
|
|
61
|
+
candidate.last ||
|
|
62
|
+
candidate.name?.split(' ')?.slice(1).join(' ') ||
|
|
63
|
+
'';
|
|
64
|
+
return { first: firstName, last: lastName };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extract domain from candidate
|
|
68
|
+
*/
|
|
69
|
+
function extractDomain(candidate) {
|
|
70
|
+
return candidate.domain || candidate.companyDomain || candidate.company_domain || '';
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create the construct provider function
|
|
74
|
+
*/
|
|
75
|
+
function createConstructProvider(config) {
|
|
76
|
+
const maxAttempts = config?.maxAttempts ?? 8;
|
|
77
|
+
const timeoutMs = config?.timeoutMs ?? 5000;
|
|
78
|
+
async function fetchEmail(candidate) {
|
|
79
|
+
const { first, last } = extractNames(candidate);
|
|
80
|
+
const domain = extractDomain(candidate);
|
|
81
|
+
// Skip if missing required fields
|
|
82
|
+
if (!first || !domain) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
// Skip personal domains
|
|
86
|
+
if ((0, personal_domains_1.isPersonalDomain)(domain)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const candidates = buildCandidates({ first, last, domain });
|
|
90
|
+
const max = Math.min(candidates.length, maxAttempts);
|
|
91
|
+
for (let i = 0; i < max; i++) {
|
|
92
|
+
const email = candidates[i];
|
|
93
|
+
const verification = await (0, mx_1.verifyEmailMx)(email, { timeoutMs });
|
|
94
|
+
if (verification.valid === true && verification.confidence >= 50) {
|
|
95
|
+
return {
|
|
96
|
+
email,
|
|
97
|
+
verified: true,
|
|
98
|
+
score: verification.confidence,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
// Mark provider name for orchestrator
|
|
105
|
+
fetchEmail.__name = 'construct';
|
|
106
|
+
return fetchEmail;
|
|
107
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dropcontact Provider
|
|
3
|
+
*
|
|
4
|
+
* Dropcontact API for email finding.
|
|
5
|
+
* This is a placeholder implementation - full implementation would call the Dropcontact API.
|
|
6
|
+
*/
|
|
7
|
+
import type { EnrichmentCandidate, ProviderResult, DropcontactConfig } from '../types';
|
|
8
|
+
/**
|
|
9
|
+
* Create the Dropcontact provider function
|
|
10
|
+
*
|
|
11
|
+
* Note: This is a placeholder. Full implementation would:
|
|
12
|
+
* 1. POST to https://api.dropcontact.io/batch with contacts
|
|
13
|
+
* 2. Poll for results
|
|
14
|
+
* 3. Return enriched email with verification status
|
|
15
|
+
*/
|
|
16
|
+
export declare function createDropcontactProvider(config: DropcontactConfig): (_candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dropcontact Provider
|
|
4
|
+
*
|
|
5
|
+
* Dropcontact API for email finding.
|
|
6
|
+
* This is a placeholder implementation - full implementation would call the Dropcontact API.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createDropcontactProvider = createDropcontactProvider;
|
|
10
|
+
/**
|
|
11
|
+
* Create the Dropcontact provider function
|
|
12
|
+
*
|
|
13
|
+
* Note: This is a placeholder. Full implementation would:
|
|
14
|
+
* 1. POST to https://api.dropcontact.io/batch with contacts
|
|
15
|
+
* 2. Poll for results
|
|
16
|
+
* 3. Return enriched email with verification status
|
|
17
|
+
*/
|
|
18
|
+
function createDropcontactProvider(config) {
|
|
19
|
+
const { apiKey } = config;
|
|
20
|
+
if (!apiKey) {
|
|
21
|
+
// Return a no-op provider if not configured
|
|
22
|
+
const noopProvider = async () => null;
|
|
23
|
+
noopProvider.__name = 'dropcontact';
|
|
24
|
+
return noopProvider;
|
|
25
|
+
}
|
|
26
|
+
async function fetchEmail(_candidate) {
|
|
27
|
+
// TODO: Implement Dropcontact API integration
|
|
28
|
+
// API: POST https://api.dropcontact.io/batch
|
|
29
|
+
// Headers: X-Access-Token: {apiKey}
|
|
30
|
+
// Body: { data: [{ first_name, last_name, company, website }], siren: false, language: "en" }
|
|
31
|
+
// For now, return null (skip this provider)
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
// Mark provider name for orchestrator
|
|
35
|
+
fetchEmail.__name = 'dropcontact';
|
|
36
|
+
return fetchEmail;
|
|
37
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hunter.io Provider
|
|
3
|
+
*
|
|
4
|
+
* Hunter.io public API for email finding.
|
|
5
|
+
* Uses email-finder endpoint with name+domain, falls back to domain-search.
|
|
6
|
+
*/
|
|
7
|
+
import type { EnrichmentCandidate, ProviderResult, HunterConfig } from "../types";
|
|
8
|
+
/**
|
|
9
|
+
* Create the Hunter provider function
|
|
10
|
+
*/
|
|
11
|
+
export declare function createHunterProvider(config: HunterConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
|