@konfeature/ap-email-guessr 0.1.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/README.md +128 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +21 -0
- package/dist/src/lib/actions/find-email.d.ts +10 -0
- package/dist/src/lib/actions/find-email.js +90 -0
- package/dist/src/lib/actions/generate-emails.d.ts +8 -0
- package/dist/src/lib/actions/generate-emails.js +56 -0
- package/dist/src/lib/common/dns.d.ts +14 -0
- package/dist/src/lib/common/dns.js +37 -0
- package/dist/src/lib/common/find-email.d.ts +39 -0
- package/dist/src/lib/common/find-email.js +78 -0
- package/dist/src/lib/common/find-person.d.ts +31 -0
- package/dist/src/lib/common/find-person.js +102 -0
- package/dist/src/lib/common/http.d.ts +24 -0
- package/dist/src/lib/common/http.js +67 -0
- package/dist/src/lib/common/mx-fingerprint.d.ts +30 -0
- package/dist/src/lib/common/mx-fingerprint.js +114 -0
- package/dist/src/lib/common/patterns.d.ts +28 -0
- package/dist/src/lib/common/patterns.js +105 -0
- package/dist/src/lib/common/providers/o365.d.ts +51 -0
- package/dist/src/lib/common/providers/o365.js +171 -0
- package/dist/src/lib/common/verifier.d.ts +22 -0
- package/dist/src/lib/common/verifier.js +10 -0
- package/dist/src/lib/common/verifiers/no2bounce-verifier.d.ts +34 -0
- package/dist/src/lib/common/verifiers/no2bounce-verifier.js +123 -0
- package/dist/src/lib/common/verifiers/o365-verifier.d.ts +9 -0
- package/dist/src/lib/common/verifiers/o365-verifier.js +30 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# ap-email-guessr
|
|
2
|
+
|
|
3
|
+
An [Activepieces](https://www.activepieces.com) community piece that finds a person's email
|
|
4
|
+
address from their name and company domain. It ships two actions:
|
|
5
|
+
|
|
6
|
+
| Action | What it does |
|
|
7
|
+
| --- | --- |
|
|
8
|
+
| **Generate Email List** | Builds likely addresses from a first name, last name and domain, ordered by real-world corporate naming frequency (`john.doe@`, `jdoe@`, `john@`, ...). |
|
|
9
|
+
| **Find Email** | Generates the candidates, then verifies them top-down — stopping at the first hit or on a catch-all domain — routing Microsoft domains through a free O365 check and everything else through [no2bounce](https://www.no2bounce.com). Returns the found address, a verdict and the credits spent. |
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
The piece auth is an optional **no2bounce API Token** (Settings → API in no2bounce). It is only
|
|
14
|
+
needed to verify **non-Microsoft** domains; Microsoft-backed domains are verified for free via the
|
|
15
|
+
O365 account check, so the token can be left empty if you only target those.
|
|
16
|
+
|
|
17
|
+
## Actions
|
|
18
|
+
|
|
19
|
+
### Generate Email List
|
|
20
|
+
|
|
21
|
+
Inputs: `firstName`, `lastName`, `domain`, plus optional `patternSet` (`common` ≈ 10 patterns /
|
|
22
|
+
`all` ≈ 20, default `all`) and `maxVariants` (hard cap).
|
|
23
|
+
|
|
24
|
+
Output — candidates ordered most-likely first:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{ "emails": ["john.doe@example.com", "jdoe@example.com", "john@example.com"], "count": 3 }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Names are normalized (accents stripped, lowercased, non-alphanumerics removed) and the domain is
|
|
31
|
+
cleaned of protocol/path/leading `@`.
|
|
32
|
+
|
|
33
|
+
### Find Email
|
|
34
|
+
|
|
35
|
+
Inputs:
|
|
36
|
+
|
|
37
|
+
- `firstName`, `lastName`, `domain` (required).
|
|
38
|
+
- `patternSet` (optional, default `common`) — `common` checks only the ~10 highest-probability
|
|
39
|
+
patterns; `all` considers the full ~20.
|
|
40
|
+
- `maxChecks` (optional) — hard ceiling on how many candidates are verified before giving up. This
|
|
41
|
+
is the per-person credit cap.
|
|
42
|
+
- `useO365` (optional, default `true`) — verify Microsoft-backed domains with the free Azure AD
|
|
43
|
+
account check instead of spending no2bounce credits.
|
|
44
|
+
- `timeout` (optional, default `15`) — per-request timeout in seconds.
|
|
45
|
+
|
|
46
|
+
Output:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"found": true,
|
|
51
|
+
"email": "jessie.han@cupshe.com",
|
|
52
|
+
"status": "valid",
|
|
53
|
+
"confidence": 0.9,
|
|
54
|
+
"catchAll": false,
|
|
55
|
+
"domain": "cupshe.com",
|
|
56
|
+
"mxProvider": "microsoft",
|
|
57
|
+
"verifier": "o365",
|
|
58
|
+
"reason": "o365_valid",
|
|
59
|
+
"creditsUsed": 0,
|
|
60
|
+
"candidatesChecked": 1,
|
|
61
|
+
"candidatesConsidered": 10,
|
|
62
|
+
"attempts": [{ "email": "jessie.han@cupshe.com", "verdict": "valid", "reason": "account_exists" }]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## How verification works
|
|
67
|
+
|
|
68
|
+
**Routing.** Per domain the piece resolves MX and fingerprints the provider:
|
|
69
|
+
|
|
70
|
+
- **Microsoft-backed** (Microsoft/consumer MX, or a `Managed` Azure AD realm on a domain that is
|
|
71
|
+
*not* a known non-Microsoft mailbox host) → the **free O365 check** (`GetCredentialType`). No port
|
|
72
|
+
25, no credits. The "Managed realm on a Google/Zoho domain" case is deliberately excluded — that
|
|
73
|
+
is a leftover tenant, not Microsoft mail, and trusting it produces false negatives.
|
|
74
|
+
- **Everything else** → **no2bounce**. no2bounce is async: submit a 1-email batch to get a
|
|
75
|
+
`trackingId` (under `data`), then poll `?trackingId=` until the top-level `overallStatus` is
|
|
76
|
+
`Completed`. For a single email the top-level counts — `Deliverable`, `Undeliverable`,
|
|
77
|
+
`Deliverable/AcceptAll`, `UnDeliverable/AcceptAll`, `Risky/AcceptAll` — are that email's verdict.
|
|
78
|
+
|
|
79
|
+
**Credit minimization.** Candidates are verified in likelihood order and the search:
|
|
80
|
+
|
|
81
|
+
1. **uses no2bounce's per-address catch-all scoring** — on an accept-all domain a predicted-deliverable
|
|
82
|
+
address counts as a hit (with reduced confidence), a predicted-undeliverable one is skipped, and a
|
|
83
|
+
genuinely `Risky/AcceptAll` result stops the search;
|
|
84
|
+
2. **verifies non-Microsoft candidates in concurrent rounds of 5 and stops at the first round with a
|
|
85
|
+
hit** (early-stop) — checking 5 at a time is faster than one-by-one, but since no2bounce bills per
|
|
86
|
+
email with no refund a round is paid for in full once submitted (a hit at candidate #3 still costs
|
|
87
|
+
the first round of 5), so the search stops as soon as a round yields a valid address or a catch-all
|
|
88
|
+
rather than walking the whole list;
|
|
89
|
+
3. uses the **`common` pattern set** by default, capping the not-found tail at ~10 instead of ~20.
|
|
90
|
+
|
|
91
|
+
no2bounce bills **1 credit per verified email**; the O365 path is **0 credits**. The actual spend is
|
|
92
|
+
returned as `creditsUsed`.
|
|
93
|
+
|
|
94
|
+
## Verdicts
|
|
95
|
+
|
|
96
|
+
| `status` | Meaning |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `valid` | The mailbox exists (`found: true`, with `email`). |
|
|
99
|
+
| `invalid` | No candidate verified, or null MX / no MX. |
|
|
100
|
+
| `catch_all` | Domain accepts everything — the specific address can't be confirmed. |
|
|
101
|
+
| `unknown` | Verification was inconclusive (e.g. no2bounce token missing, poll timeout, O365 throttled). |
|
|
102
|
+
|
|
103
|
+
> **Note.** The no2bounce wire format above is confirmed against the live API (the published docs were
|
|
104
|
+
> out of date). The O365 (`login.microsoftonline.com`) check and DNS use only Node built-ins; the only
|
|
105
|
+
> external dependency is the no2bounce HTTP API.
|
|
106
|
+
|
|
107
|
+
## Build
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
npm install # or: bun install
|
|
111
|
+
npm run build # tsc -> dist/
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The compiled entry point is `dist/src/index.js` (referenced by `main` in `package.json`).
|
|
115
|
+
|
|
116
|
+
## Publish & install in self-hosted Activepieces
|
|
117
|
+
|
|
118
|
+
1. Set a unique package name in `package.json` if `ap-email-guessr` is taken on npm, then publish:
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
npm publish --access public
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
2. In your self-hosted Activepieces instance, add the piece from npm
|
|
125
|
+
(**Platform Admin → Pieces → Install**, or set `AP_DEV_PIECES` when developing) using the
|
|
126
|
+
published package name and version.
|
|
127
|
+
|
|
128
|
+
See the Activepieces docs for details: <https://www.activepieces.com/docs/build-pieces/building-pieces/piece-definition>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emailGuessr = void 0;
|
|
4
|
+
const pieces_framework_1 = require("@activepieces/pieces-framework");
|
|
5
|
+
const find_email_1 = require("./lib/actions/find-email");
|
|
6
|
+
const generate_emails_1 = require("./lib/actions/generate-emails");
|
|
7
|
+
exports.emailGuessr = (0, pieces_framework_1.createPiece)({
|
|
8
|
+
displayName: "Email Guessr",
|
|
9
|
+
description: "Generate likely email addresses from a name and domain, then find the real one — free Microsoft 365 checks plus no2bounce verification, with early-stop to minimize credits.",
|
|
10
|
+
auth: pieces_framework_1.PieceAuth.SecretText({
|
|
11
|
+
displayName: "no2bounce API Token",
|
|
12
|
+
description: "API token from no2bounce, used to verify non-Microsoft domains. Optional: Microsoft-backed domains are verified for free via the O365 account check.",
|
|
13
|
+
required: false,
|
|
14
|
+
}),
|
|
15
|
+
minimumSupportedRelease: "0.82.0",
|
|
16
|
+
logoUrl: "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/2709.png",
|
|
17
|
+
authors: ["frak"],
|
|
18
|
+
actions: [generate_emails_1.generateEmails, find_email_1.findEmailAction],
|
|
19
|
+
triggers: [],
|
|
20
|
+
});
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const findEmailAction: import("@activepieces/pieces-framework").IAction<import("@activepieces/pieces-framework").PieceAuthProperty, {
|
|
2
|
+
firstName: import("@activepieces/pieces-framework").ShortTextProperty<true>;
|
|
3
|
+
lastName: import("@activepieces/pieces-framework").ShortTextProperty<true>;
|
|
4
|
+
domain: import("@activepieces/pieces-framework").ShortTextProperty<true>;
|
|
5
|
+
patternSet: import("@activepieces/pieces-framework").StaticDropdownProperty<string, false>;
|
|
6
|
+
maxChecks: import("@activepieces/pieces-framework").NumberProperty<false>;
|
|
7
|
+
useO365: import("@activepieces/pieces-framework").CheckboxProperty<false>;
|
|
8
|
+
timeout: import("@activepieces/pieces-framework").NumberProperty<false>;
|
|
9
|
+
}>;
|
|
10
|
+
//# sourceMappingURL=find-email.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findEmailAction = void 0;
|
|
4
|
+
const pieces_framework_1 = require("@activepieces/pieces-framework");
|
|
5
|
+
const find_person_1 = require("../common/find-person");
|
|
6
|
+
exports.findEmailAction = (0, pieces_framework_1.createAction)({
|
|
7
|
+
name: "find_email",
|
|
8
|
+
displayName: "Find Email",
|
|
9
|
+
description: "Find a person's email from their name and company domain. Generates likely addresses (ordered by real-world frequency), then verifies them top-down — stopping at the first hit or on a catch-all domain — routing Microsoft domains through the free O365 check and everything else through no2bounce.",
|
|
10
|
+
props: {
|
|
11
|
+
firstName: pieces_framework_1.Property.ShortText({
|
|
12
|
+
displayName: "First Name",
|
|
13
|
+
description: "e.g. John (accents and casing are normalized).",
|
|
14
|
+
required: true,
|
|
15
|
+
}),
|
|
16
|
+
lastName: pieces_framework_1.Property.ShortText({
|
|
17
|
+
displayName: "Last Name",
|
|
18
|
+
description: "e.g. Doe (accents and casing are normalized).",
|
|
19
|
+
required: true,
|
|
20
|
+
}),
|
|
21
|
+
domain: pieces_framework_1.Property.ShortText({
|
|
22
|
+
displayName: "Domain",
|
|
23
|
+
description: "Company domain, e.g. example.com.",
|
|
24
|
+
required: true,
|
|
25
|
+
}),
|
|
26
|
+
patternSet: pieces_framework_1.Property.StaticDropdown({
|
|
27
|
+
displayName: "Pattern Set",
|
|
28
|
+
description: "Common checks only the ~10 highest-probability patterns (fewer credits); All considers the full ~20.",
|
|
29
|
+
required: false,
|
|
30
|
+
defaultValue: "common",
|
|
31
|
+
options: {
|
|
32
|
+
options: [
|
|
33
|
+
{ label: "Common only (~10)", value: "common" },
|
|
34
|
+
{ label: "All patterns (~20)", value: "all" },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
maxChecks: pieces_framework_1.Property.Number({
|
|
39
|
+
displayName: "Max Verifications (credit cap)",
|
|
40
|
+
description: "Hard ceiling on how many candidates are verified before giving up. Caps no2bounce credits spent per person.",
|
|
41
|
+
required: false,
|
|
42
|
+
}),
|
|
43
|
+
useO365: pieces_framework_1.Property.Checkbox({
|
|
44
|
+
displayName: "Use O365 Check",
|
|
45
|
+
description: "Verify Microsoft-backed domains via the free Azure AD account check instead of spending no2bounce credits.",
|
|
46
|
+
required: false,
|
|
47
|
+
defaultValue: true,
|
|
48
|
+
}),
|
|
49
|
+
timeout: pieces_framework_1.Property.Number({
|
|
50
|
+
displayName: "Timeout (seconds)",
|
|
51
|
+
description: "Per-request timeout for verification calls (1-120).",
|
|
52
|
+
required: false,
|
|
53
|
+
defaultValue: 15,
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
async run(context) {
|
|
57
|
+
const { firstName, lastName, domain, patternSet, maxChecks, useO365 } = context.propsValue;
|
|
58
|
+
const timeoutSeconds = context.propsValue.timeout ?? 15;
|
|
59
|
+
const timeoutMs = Math.min(Math.max(Number(timeoutSeconds) || 15, 1), 120) * 1000;
|
|
60
|
+
const token = context.auth || undefined;
|
|
61
|
+
const result = await (0, find_person_1.findPersonEmail)(firstName, lastName, domain, {
|
|
62
|
+
patternSet: patternSet ?? "common",
|
|
63
|
+
maxChecks: maxChecks != null ? Math.max(1, Number(maxChecks)) : undefined,
|
|
64
|
+
stopOnFirstHit: true,
|
|
65
|
+
timeoutMs,
|
|
66
|
+
useO365: useO365 ?? true,
|
|
67
|
+
no2bounceToken: token,
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
found: result.found,
|
|
71
|
+
email: result.email,
|
|
72
|
+
status: result.verdict,
|
|
73
|
+
confidence: result.confidence,
|
|
74
|
+
catchAll: result.catchAll,
|
|
75
|
+
domain: result.domain,
|
|
76
|
+
mxProvider: result.mxProvider,
|
|
77
|
+
verifier: result.verifierUsed,
|
|
78
|
+
reason: result.reason,
|
|
79
|
+
creditsUsed: result.creditsUsed,
|
|
80
|
+
candidatesChecked: result.candidatesChecked,
|
|
81
|
+
candidatesConsidered: result.candidatesConsidered,
|
|
82
|
+
attempts: result.attempts.map((attempt) => ({
|
|
83
|
+
email: attempt.email,
|
|
84
|
+
verdict: attempt.verdict,
|
|
85
|
+
reason: attempt.reason,
|
|
86
|
+
})),
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=find-email.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const generateEmails: import("@activepieces/pieces-framework").IAction<import("@activepieces/pieces-framework").PieceAuthProperty, {
|
|
2
|
+
firstName: import("@activepieces/pieces-framework").ShortTextProperty<true>;
|
|
3
|
+
lastName: import("@activepieces/pieces-framework").ShortTextProperty<true>;
|
|
4
|
+
domain: import("@activepieces/pieces-framework").ShortTextProperty<true>;
|
|
5
|
+
patternSet: import("@activepieces/pieces-framework").StaticDropdownProperty<string, false>;
|
|
6
|
+
maxVariants: import("@activepieces/pieces-framework").NumberProperty<false>;
|
|
7
|
+
}>;
|
|
8
|
+
//# sourceMappingURL=generate-emails.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateEmails = void 0;
|
|
4
|
+
const pieces_framework_1 = require("@activepieces/pieces-framework");
|
|
5
|
+
const patterns_1 = require("../common/patterns");
|
|
6
|
+
exports.generateEmails = (0, pieces_framework_1.createAction)({
|
|
7
|
+
name: "generate_emails",
|
|
8
|
+
displayName: "Generate Email List",
|
|
9
|
+
description: "Build a list of likely email addresses from a first name, last name and domain, ordered by real-world corporate naming frequency (most likely first).",
|
|
10
|
+
props: {
|
|
11
|
+
firstName: pieces_framework_1.Property.ShortText({
|
|
12
|
+
displayName: "First Name",
|
|
13
|
+
description: "e.g. John (accents and casing are normalized).",
|
|
14
|
+
required: true,
|
|
15
|
+
}),
|
|
16
|
+
lastName: pieces_framework_1.Property.ShortText({
|
|
17
|
+
displayName: "Last Name",
|
|
18
|
+
description: "e.g. Doe (accents and casing are normalized).",
|
|
19
|
+
required: true,
|
|
20
|
+
}),
|
|
21
|
+
domain: pieces_framework_1.Property.ShortText({
|
|
22
|
+
displayName: "Domain",
|
|
23
|
+
description: "Company domain, e.g. example.com.",
|
|
24
|
+
required: true,
|
|
25
|
+
}),
|
|
26
|
+
patternSet: pieces_framework_1.Property.StaticDropdown({
|
|
27
|
+
displayName: "Pattern Set",
|
|
28
|
+
description: "Common keeps only the ~10 highest-probability patterns (cheaper to verify); All returns the full ~20 ordered patterns.",
|
|
29
|
+
required: false,
|
|
30
|
+
defaultValue: "all",
|
|
31
|
+
options: {
|
|
32
|
+
options: [
|
|
33
|
+
{ label: "All patterns (~20)", value: "all" },
|
|
34
|
+
{ label: "Common only (~10)", value: "common" },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
maxVariants: pieces_framework_1.Property.Number({
|
|
39
|
+
displayName: "Max Variants",
|
|
40
|
+
description: "Optional hard cap on the number of candidates returned, applied after ordering.",
|
|
41
|
+
required: false,
|
|
42
|
+
}),
|
|
43
|
+
},
|
|
44
|
+
async run(context) {
|
|
45
|
+
const { firstName, lastName, domain, patternSet, maxVariants } = context.propsValue;
|
|
46
|
+
const emails = (0, patterns_1.generateEmailCandidates)({
|
|
47
|
+
firstName,
|
|
48
|
+
lastName,
|
|
49
|
+
domain,
|
|
50
|
+
patternSet: patternSet ?? "all",
|
|
51
|
+
maxVariants: maxVariants != null ? Number(maxVariants) : undefined,
|
|
52
|
+
});
|
|
53
|
+
return { emails, count: emails.length };
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
//# sourceMappingURL=generate-emails.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain mail-routing resolution (DNS only, Node built-ins).
|
|
3
|
+
*
|
|
4
|
+
* Used by the verifier router to fingerprint the provider and detect domains
|
|
5
|
+
* that cannot receive mail (no MX, or an explicit RFC 7505 null MX).
|
|
6
|
+
*/
|
|
7
|
+
export interface MailRouting {
|
|
8
|
+
/** MX hosts ordered by priority (or the domain itself for implicit MX). */
|
|
9
|
+
hosts: string[];
|
|
10
|
+
/** RFC 7505 null MX: the domain explicitly accepts no mail. */
|
|
11
|
+
nullMx: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveMail(domain: string): Promise<MailRouting>;
|
|
14
|
+
//# sourceMappingURL=dns.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Domain mail-routing resolution (DNS only, Node built-ins).
|
|
4
|
+
*
|
|
5
|
+
* Used by the verifier router to fingerprint the provider and detect domains
|
|
6
|
+
* that cannot receive mail (no MX, or an explicit RFC 7505 null MX).
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.resolveMail = resolveMail;
|
|
10
|
+
const node_dns_1 = require("node:dns");
|
|
11
|
+
async function resolveMail(domain) {
|
|
12
|
+
try {
|
|
13
|
+
const records = await node_dns_1.promises.resolveMx(domain);
|
|
14
|
+
const real = records.filter((record) => record.exchange && record.exchange !== ".");
|
|
15
|
+
// Records exist but every exchange is empty/"." → explicit null MX.
|
|
16
|
+
if (records.length > 0 && real.length === 0) {
|
|
17
|
+
return { hosts: [], nullMx: true };
|
|
18
|
+
}
|
|
19
|
+
if (real.length > 0) {
|
|
20
|
+
const hosts = real
|
|
21
|
+
.sort((a, b) => a.priority - b.priority)
|
|
22
|
+
.map((record) => record.exchange);
|
|
23
|
+
return { hosts, nullMx: false };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// No MX records; fall back to the implicit-MX (A/AAAA) lookup below.
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
await node_dns_1.promises.lookup(domain);
|
|
31
|
+
return { hosts: [domain], nullMx: false };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { hosts: [], nullMx: false };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=dns.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit-minimizing email search (layers 1 + 2).
|
|
3
|
+
*
|
|
4
|
+
* Candidates arrive pre-ordered by likelihood and are verified top-down in
|
|
5
|
+
* rounds of `batchSize` (1 = strictly sequential). Each completed round is read
|
|
6
|
+
* back in candidate order, so the verdict is identical to checking one at a time:
|
|
7
|
+
* - stop at the first `valid` hit (layer 2: early-stop), and
|
|
8
|
+
* - stop immediately on a `catch_all` verdict, since a catch-all domain the
|
|
9
|
+
* verifier can't score further can't disambiguate individual mailboxes —
|
|
10
|
+
* continuing would just burn credits for no extra signal (layer 1).
|
|
11
|
+
*
|
|
12
|
+
* no2bounce bills per email with no refund, so a round is paid for the moment it
|
|
13
|
+
* is submitted: `creditsUsed` counts every email in the rounds we started, even
|
|
14
|
+
* when an early candidate in the final round is the hit. A larger `batchSize` is
|
|
15
|
+
* thus faster (fewer submit/poll cycles) but coarser on credits. `maxChecks` is a
|
|
16
|
+
* hard ceiling; a `valid` hit on an accept-all domain is reported with reduced
|
|
17
|
+
* confidence and `catchAll: true`.
|
|
18
|
+
*/
|
|
19
|
+
import type { Verifier, VerifyOutcome, VerifyVerdict } from "./verifier";
|
|
20
|
+
export interface FindEmailOptions {
|
|
21
|
+
/** Hard ceiling on verifications (credit cap). Defaults to all candidates. */
|
|
22
|
+
maxChecks?: number;
|
|
23
|
+
/** Stop at the first valid hit. Defaults to true. */
|
|
24
|
+
stopOnFirstHit?: boolean;
|
|
25
|
+
/** Candidates verified concurrently per round. Defaults to 1 (sequential). */
|
|
26
|
+
batchSize?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface FindEmailResult {
|
|
29
|
+
found: boolean;
|
|
30
|
+
email: string | null;
|
|
31
|
+
verdict: VerifyVerdict;
|
|
32
|
+
confidence: number;
|
|
33
|
+
catchAll: boolean;
|
|
34
|
+
candidatesChecked: number;
|
|
35
|
+
creditsUsed: number;
|
|
36
|
+
attempts: VerifyOutcome[];
|
|
37
|
+
}
|
|
38
|
+
export declare function findEmail(candidates: string[], verifier: Verifier, options?: FindEmailOptions): Promise<FindEmailResult>;
|
|
39
|
+
//# sourceMappingURL=find-email.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Credit-minimizing email search (layers 1 + 2).
|
|
4
|
+
*
|
|
5
|
+
* Candidates arrive pre-ordered by likelihood and are verified top-down in
|
|
6
|
+
* rounds of `batchSize` (1 = strictly sequential). Each completed round is read
|
|
7
|
+
* back in candidate order, so the verdict is identical to checking one at a time:
|
|
8
|
+
* - stop at the first `valid` hit (layer 2: early-stop), and
|
|
9
|
+
* - stop immediately on a `catch_all` verdict, since a catch-all domain the
|
|
10
|
+
* verifier can't score further can't disambiguate individual mailboxes —
|
|
11
|
+
* continuing would just burn credits for no extra signal (layer 1).
|
|
12
|
+
*
|
|
13
|
+
* no2bounce bills per email with no refund, so a round is paid for the moment it
|
|
14
|
+
* is submitted: `creditsUsed` counts every email in the rounds we started, even
|
|
15
|
+
* when an early candidate in the final round is the hit. A larger `batchSize` is
|
|
16
|
+
* thus faster (fewer submit/poll cycles) but coarser on credits. `maxChecks` is a
|
|
17
|
+
* hard ceiling; a `valid` hit on an accept-all domain is reported with reduced
|
|
18
|
+
* confidence and `catchAll: true`.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.findEmail = findEmail;
|
|
22
|
+
const CONFIDENCE = {
|
|
23
|
+
valid: 0.9,
|
|
24
|
+
catch_all: 0.5,
|
|
25
|
+
invalid: 0.05,
|
|
26
|
+
unknown: 0.5,
|
|
27
|
+
};
|
|
28
|
+
function result(found, email, verdict, catchAll, attempts, creditsUsed) {
|
|
29
|
+
// An accept-all "hit" is probabilistic, so trust it less than a clean one.
|
|
30
|
+
const confidence = verdict === "valid" && catchAll ? 0.6 : CONFIDENCE[verdict];
|
|
31
|
+
return {
|
|
32
|
+
found,
|
|
33
|
+
email,
|
|
34
|
+
verdict,
|
|
35
|
+
confidence,
|
|
36
|
+
catchAll,
|
|
37
|
+
candidatesChecked: attempts.length,
|
|
38
|
+
creditsUsed,
|
|
39
|
+
attempts,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async function findEmail(candidates, verifier, options = {}) {
|
|
43
|
+
const stopOnFirstHit = options.stopOnFirstHit ?? true;
|
|
44
|
+
const batchSize = Math.max(1, options.batchSize ?? 1);
|
|
45
|
+
const limit = Math.min(options.maxChecks ?? candidates.length, candidates.length);
|
|
46
|
+
const attempts = [];
|
|
47
|
+
let creditsUsed = 0;
|
|
48
|
+
let firstValid = null;
|
|
49
|
+
for (let start = 0; start < limit; start += batchSize) {
|
|
50
|
+
const round = candidates.slice(start, Math.min(start + batchSize, limit));
|
|
51
|
+
const outcomes = await Promise.all(round.map((email) => verifier.verify(email)));
|
|
52
|
+
// The whole round is billed on submit (no2bounce charges per email with
|
|
53
|
+
// no refund), so count every result before deciding the verdict.
|
|
54
|
+
for (const outcome of outcomes) {
|
|
55
|
+
attempts.push(outcome);
|
|
56
|
+
creditsUsed += outcome.creditsUsed;
|
|
57
|
+
}
|
|
58
|
+
for (const outcome of outcomes) {
|
|
59
|
+
if (outcome.verdict === "catch_all") {
|
|
60
|
+
return result(false, null, "catch_all", true, attempts, creditsUsed);
|
|
61
|
+
}
|
|
62
|
+
if (outcome.verdict === "valid") {
|
|
63
|
+
if (!firstValid) {
|
|
64
|
+
firstValid = outcome;
|
|
65
|
+
}
|
|
66
|
+
if (stopOnFirstHit) {
|
|
67
|
+
return result(true, outcome.email, "valid", outcome.catchAll ?? false, attempts, creditsUsed);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (firstValid) {
|
|
73
|
+
return result(true, firstValid.email, "valid", firstValid.catchAll ?? false, attempts, creditsUsed);
|
|
74
|
+
}
|
|
75
|
+
const sawUnknown = attempts.some((a) => a.verdict === "unknown");
|
|
76
|
+
return result(false, null, sawUnknown ? "unknown" : "invalid", attempts.some((a) => a.catchAll === true), attempts, creditsUsed);
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=find-email.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-person email finder: generate ordered candidates, pick the right verifier
|
|
3
|
+
* for the domain, then run the credit-minimizing early-stop search.
|
|
4
|
+
*
|
|
5
|
+
* Routing: use the free O365 account check when mail genuinely routes through
|
|
6
|
+
* Microsoft (Microsoft/consumer MX, or a "Managed" realm on a domain that is not
|
|
7
|
+
* a known non-Microsoft mailbox host). Everything else goes to no2bounce. The
|
|
8
|
+
* "Managed realm but Google/Zoho MX" exclusion is the louyetu.fr false-negative
|
|
9
|
+
* fix: a leftover Azure AD tenant must not be trusted as Microsoft mail.
|
|
10
|
+
*/
|
|
11
|
+
import { type FindEmailResult } from "./find-email";
|
|
12
|
+
import { type PatternSet } from "./patterns";
|
|
13
|
+
export interface FindPersonOptions {
|
|
14
|
+
patternSet: PatternSet;
|
|
15
|
+
maxVariants?: number;
|
|
16
|
+
/** Hard ceiling on verifications (credit cap). */
|
|
17
|
+
maxChecks?: number;
|
|
18
|
+
stopOnFirstHit: boolean;
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
useO365: boolean;
|
|
21
|
+
no2bounceToken?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface FindPersonResult extends FindEmailResult {
|
|
24
|
+
reason: string;
|
|
25
|
+
domain: string;
|
|
26
|
+
mxProvider: string;
|
|
27
|
+
verifierUsed: string;
|
|
28
|
+
candidatesConsidered: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function findPersonEmail(firstName: string, lastName: string, domain: string, options: FindPersonOptions): Promise<FindPersonResult>;
|
|
31
|
+
//# sourceMappingURL=find-person.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Per-person email finder: generate ordered candidates, pick the right verifier
|
|
4
|
+
* for the domain, then run the credit-minimizing early-stop search.
|
|
5
|
+
*
|
|
6
|
+
* Routing: use the free O365 account check when mail genuinely routes through
|
|
7
|
+
* Microsoft (Microsoft/consumer MX, or a "Managed" realm on a domain that is not
|
|
8
|
+
* a known non-Microsoft mailbox host). Everything else goes to no2bounce. The
|
|
9
|
+
* "Managed realm but Google/Zoho MX" exclusion is the louyetu.fr false-negative
|
|
10
|
+
* fix: a leftover Azure AD tenant must not be trusted as Microsoft mail.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.findPersonEmail = findPersonEmail;
|
|
14
|
+
const dns_1 = require("./dns");
|
|
15
|
+
const find_email_1 = require("./find-email");
|
|
16
|
+
const mx_fingerprint_1 = require("./mx-fingerprint");
|
|
17
|
+
const o365_1 = require("./providers/o365");
|
|
18
|
+
const patterns_1 = require("./patterns");
|
|
19
|
+
const no2bounce_verifier_1 = require("./verifiers/no2bounce-verifier");
|
|
20
|
+
const o365_verifier_1 = require("./verifiers/o365-verifier");
|
|
21
|
+
const NO2BOUNCE_BATCH_SIZE = 5;
|
|
22
|
+
function earlyExit(candidatesConsidered, domain, mxProvider, verdict, confidence, reason) {
|
|
23
|
+
return {
|
|
24
|
+
found: false,
|
|
25
|
+
email: null,
|
|
26
|
+
verdict,
|
|
27
|
+
confidence,
|
|
28
|
+
catchAll: false,
|
|
29
|
+
candidatesChecked: 0,
|
|
30
|
+
creditsUsed: 0,
|
|
31
|
+
attempts: [],
|
|
32
|
+
reason,
|
|
33
|
+
domain,
|
|
34
|
+
mxProvider,
|
|
35
|
+
verifierUsed: "none",
|
|
36
|
+
candidatesConsidered,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function findPersonEmail(firstName, lastName, domain, options) {
|
|
40
|
+
const candidates = (0, patterns_1.generateEmailCandidates)({
|
|
41
|
+
firstName,
|
|
42
|
+
lastName,
|
|
43
|
+
domain,
|
|
44
|
+
patternSet: options.patternSet,
|
|
45
|
+
maxVariants: options.maxVariants,
|
|
46
|
+
});
|
|
47
|
+
const cleanDomain = (0, patterns_1.normalizeDomain)(domain);
|
|
48
|
+
if (candidates.length === 0) {
|
|
49
|
+
return earlyExit(0, cleanDomain, "none", "invalid", 0.03, "no_candidates");
|
|
50
|
+
}
|
|
51
|
+
const routing = await (0, dns_1.resolveMail)(cleanDomain);
|
|
52
|
+
const provider = (0, mx_fingerprint_1.fingerprintMx)(routing.hosts);
|
|
53
|
+
if (routing.nullMx) {
|
|
54
|
+
return earlyExit(candidates.length, cleanDomain, provider.id, "invalid", 0.02, "null_mx");
|
|
55
|
+
}
|
|
56
|
+
if (routing.hosts.length === 0) {
|
|
57
|
+
return earlyExit(candidates.length, cleanDomain, provider.id, "invalid", 0.03, "no_mx_record");
|
|
58
|
+
}
|
|
59
|
+
const consumer = (0, o365_1.isMicrosoftConsumerDomain)(cleanDomain);
|
|
60
|
+
let namespace = null;
|
|
61
|
+
let microsoftBacked = consumer || provider.microsoftBacked;
|
|
62
|
+
// Only spend a realm lookup on ambiguous MX (not obviously Microsoft, not a
|
|
63
|
+
// known foreign mailbox host) — that's where O365-behind-a-gateway hides.
|
|
64
|
+
if (options.useO365 &&
|
|
65
|
+
!microsoftBacked &&
|
|
66
|
+
!(0, mx_fingerprint_1.hostsNonMicrosoftMailboxes)(provider)) {
|
|
67
|
+
namespace = await (0, o365_1.getUserRealm)(cleanDomain, options.timeoutMs);
|
|
68
|
+
if (namespace === "Managed") {
|
|
69
|
+
microsoftBacked = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let verifier;
|
|
73
|
+
let verifierUsed;
|
|
74
|
+
if (options.useO365 && microsoftBacked) {
|
|
75
|
+
verifier = (0, o365_verifier_1.createO365Verifier)(namespace ?? "Managed", true, options.timeoutMs);
|
|
76
|
+
verifierUsed = "o365";
|
|
77
|
+
}
|
|
78
|
+
else if (options.no2bounceToken) {
|
|
79
|
+
verifier = (0, no2bounce_verifier_1.createNo2BounceVerifier)({
|
|
80
|
+
apitoken: options.no2bounceToken,
|
|
81
|
+
timeoutMs: options.timeoutMs,
|
|
82
|
+
});
|
|
83
|
+
verifierUsed = "no2bounce";
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
return earlyExit(candidates.length, cleanDomain, provider.id, "unknown", 0.5, "no2bounce_token_required");
|
|
87
|
+
}
|
|
88
|
+
const result = await (0, find_email_1.findEmail)(candidates, verifier, {
|
|
89
|
+
maxChecks: options.maxChecks,
|
|
90
|
+
stopOnFirstHit: options.stopOnFirstHit,
|
|
91
|
+
batchSize: verifierUsed === "no2bounce" ? NO2BOUNCE_BATCH_SIZE : 1,
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
...result,
|
|
95
|
+
reason: `${verifierUsed}_${result.verdict}`,
|
|
96
|
+
domain: cleanDomain,
|
|
97
|
+
mxProvider: provider.id,
|
|
98
|
+
verifierUsed,
|
|
99
|
+
candidatesConsidered: candidates.length,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=find-person.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal HTTPS client built on Node's `node:https` only (no dependencies).
|
|
3
|
+
* Used by the provider signal collectors (O365, Gravatar). Designed to fail
|
|
4
|
+
* soft: callers treat any network/timeout error as a neutral signal rather
|
|
5
|
+
* than a hard verdict.
|
|
6
|
+
*/
|
|
7
|
+
export interface HttpResponse {
|
|
8
|
+
status: number;
|
|
9
|
+
body: string;
|
|
10
|
+
}
|
|
11
|
+
export interface HttpRequestOptions {
|
|
12
|
+
url: string;
|
|
13
|
+
method?: "GET" | "HEAD" | "POST";
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
body?: string;
|
|
16
|
+
timeoutMs: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Perform a single HTTPS request. Resolves with the status code and (for
|
|
20
|
+
* non-HEAD requests) the response body. Rejects only on transport failure or
|
|
21
|
+
* timeout so callers can map those to a neutral signal.
|
|
22
|
+
*/
|
|
23
|
+
export declare function httpRequest(options: HttpRequestOptions): Promise<HttpResponse>;
|
|
24
|
+
//# sourceMappingURL=http.d.ts.map
|