@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 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,2 @@
1
+ export declare const emailGuessr: import("@activepieces/pieces-framework").Piece<import("@activepieces/pieces-framework").SecretTextProperty<false>>;
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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