@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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal HTTPS client built on Node's `node:https` only (no dependencies).
|
|
4
|
+
* Used by the provider signal collectors (O365, Gravatar). Designed to fail
|
|
5
|
+
* soft: callers treat any network/timeout error as a neutral signal rather
|
|
6
|
+
* than a hard verdict.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.httpRequest = httpRequest;
|
|
10
|
+
const node_https_1 = require("node:https");
|
|
11
|
+
const DEFAULT_USER_AGENT = "Mozilla/5.0 (compatible; ap-email-guessr/0.2; +https://www.activepieces.com)";
|
|
12
|
+
/**
|
|
13
|
+
* Perform a single HTTPS request. Resolves with the status code and (for
|
|
14
|
+
* non-HEAD requests) the response body. Rejects only on transport failure or
|
|
15
|
+
* timeout so callers can map those to a neutral signal.
|
|
16
|
+
*/
|
|
17
|
+
function httpRequest(options) {
|
|
18
|
+
const method = options.method ?? "GET";
|
|
19
|
+
const url = new URL(options.url);
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const req = (0, node_https_1.request)({
|
|
22
|
+
protocol: url.protocol,
|
|
23
|
+
hostname: url.hostname,
|
|
24
|
+
port: url.port || 443,
|
|
25
|
+
path: `${url.pathname}${url.search}`,
|
|
26
|
+
method,
|
|
27
|
+
headers: {
|
|
28
|
+
"User-Agent": DEFAULT_USER_AGENT,
|
|
29
|
+
Accept: "*/*",
|
|
30
|
+
...options.headers,
|
|
31
|
+
},
|
|
32
|
+
}, (res) => {
|
|
33
|
+
const status = res.statusCode ?? 0;
|
|
34
|
+
// HEAD has no body; drain and resolve immediately.
|
|
35
|
+
if (method === "HEAD") {
|
|
36
|
+
res.resume();
|
|
37
|
+
res.on("end", () => resolve({ status, body: "" }));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let bytes = 0;
|
|
42
|
+
// Guard against unexpectedly large bodies (e.g. an avatar image
|
|
43
|
+
// if a caller forgets d=404). 256 KiB is ample for JSON APIs.
|
|
44
|
+
const maxBytes = 256 * 1024;
|
|
45
|
+
res.on("data", (chunk) => {
|
|
46
|
+
bytes += chunk.length;
|
|
47
|
+
if (bytes <= maxBytes) {
|
|
48
|
+
chunks.push(chunk);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
res.destroy();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
res.on("end", () => resolve({ status, body: Buffer.concat(chunks).toString("utf8") }));
|
|
55
|
+
res.on("error", reject);
|
|
56
|
+
});
|
|
57
|
+
req.setTimeout(options.timeoutMs, () => {
|
|
58
|
+
req.destroy(new Error("http_timeout"));
|
|
59
|
+
});
|
|
60
|
+
req.on("error", reject);
|
|
61
|
+
if (options.body !== undefined && method === "POST") {
|
|
62
|
+
req.write(options.body);
|
|
63
|
+
}
|
|
64
|
+
req.end();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fingerprint a domain's mail provider from its MX hostnames.
|
|
3
|
+
*
|
|
4
|
+
* Two uses:
|
|
5
|
+
* 1. Routing — knowing a domain is Microsoft-backed tells us the O365 account
|
|
6
|
+
* check is authoritative.
|
|
7
|
+
* 2. Confidence weighting — "security gateways" (Proofpoint, Mimecast, ...)
|
|
8
|
+
* sit in front of the real mailserver and routinely accept every RCPT TO
|
|
9
|
+
* before filtering later, so an SMTP "accepted" from them is weak evidence.
|
|
10
|
+
*/
|
|
11
|
+
export type MxProviderId = "google" | "microsoft" | "zoho" | "yahoo" | "yandex" | "fastmail" | "proofpoint" | "mimecast" | "barracuda" | "cisco_ironport" | "other" | "none";
|
|
12
|
+
export interface MxProvider {
|
|
13
|
+
id: MxProviderId;
|
|
14
|
+
name: string;
|
|
15
|
+
/** Security gateway that tends to accept-all at RCPT time. */
|
|
16
|
+
isGateway: boolean;
|
|
17
|
+
/** Domain mailboxes are (likely) hosted on Microsoft 365 / Exchange Online. */
|
|
18
|
+
microsoftBacked: boolean;
|
|
19
|
+
/** Domain mailboxes are (likely) hosted on Google Workspace / Gmail. */
|
|
20
|
+
googleBacked: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** Classify a domain's mail provider from its ordered MX hostnames. */
|
|
23
|
+
export declare function fingerprintMx(mxHosts: string[]): MxProvider;
|
|
24
|
+
/**
|
|
25
|
+
* True when the provider hosts its own non-Microsoft mailboxes, so a "Managed"
|
|
26
|
+
* Azure AD namespace there is only a leftover tenant artifact and the O365
|
|
27
|
+
* account check must NOT be trusted. Gateways are excluded: O365 sits behind them.
|
|
28
|
+
*/
|
|
29
|
+
export declare function hostsNonMicrosoftMailboxes(provider: MxProvider): boolean;
|
|
30
|
+
//# sourceMappingURL=mx-fingerprint.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Fingerprint a domain's mail provider from its MX hostnames.
|
|
4
|
+
*
|
|
5
|
+
* Two uses:
|
|
6
|
+
* 1. Routing — knowing a domain is Microsoft-backed tells us the O365 account
|
|
7
|
+
* check is authoritative.
|
|
8
|
+
* 2. Confidence weighting — "security gateways" (Proofpoint, Mimecast, ...)
|
|
9
|
+
* sit in front of the real mailserver and routinely accept every RCPT TO
|
|
10
|
+
* before filtering later, so an SMTP "accepted" from them is weak evidence.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.fingerprintMx = fingerprintMx;
|
|
14
|
+
exports.hostsNonMicrosoftMailboxes = hostsNonMicrosoftMailboxes;
|
|
15
|
+
// Ordered: gateways first so that, when a domain fronts its real provider with
|
|
16
|
+
// a gateway, the gateway (the connection we actually probe) wins.
|
|
17
|
+
const RULES = [
|
|
18
|
+
{
|
|
19
|
+
id: "proofpoint",
|
|
20
|
+
name: "Proofpoint",
|
|
21
|
+
needles: ["pphosted.com", "ppe-hosted.com", "proofpoint.com"],
|
|
22
|
+
isGateway: true,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "mimecast",
|
|
26
|
+
name: "Mimecast",
|
|
27
|
+
needles: ["mimecast.com", "mimecast.co.za", "mimecast-offshore.com"],
|
|
28
|
+
isGateway: true,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "barracuda",
|
|
32
|
+
name: "Barracuda",
|
|
33
|
+
needles: ["barracudanetworks.com", "cudaops.com", "cudamail.com"],
|
|
34
|
+
isGateway: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "cisco_ironport",
|
|
38
|
+
name: "Cisco IronPort",
|
|
39
|
+
needles: ["iphmx.com", "ironport.com"],
|
|
40
|
+
isGateway: true,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "microsoft",
|
|
44
|
+
name: "Microsoft 365",
|
|
45
|
+
needles: ["mail.protection.outlook.com", "olc.protection.outlook.com"],
|
|
46
|
+
microsoftBacked: true,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "google",
|
|
50
|
+
name: "Google Workspace",
|
|
51
|
+
needles: [
|
|
52
|
+
"aspmx.l.google.com",
|
|
53
|
+
"googlemail.com",
|
|
54
|
+
"aspmx.google.com",
|
|
55
|
+
"psmtp.com",
|
|
56
|
+
],
|
|
57
|
+
googleBacked: true,
|
|
58
|
+
},
|
|
59
|
+
{ id: "zoho", name: "Zoho Mail", needles: ["zoho.com", "zoho.eu", "zohomail"] },
|
|
60
|
+
{ id: "yahoo", name: "Yahoo", needles: ["yahoodns.net"] },
|
|
61
|
+
{ id: "yandex", name: "Yandex", needles: ["mx.yandex", "yandex.net"] },
|
|
62
|
+
{
|
|
63
|
+
id: "fastmail",
|
|
64
|
+
name: "Fastmail",
|
|
65
|
+
needles: ["messagingengine.com", "fastmail.com"],
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
const NONE = {
|
|
69
|
+
id: "none",
|
|
70
|
+
name: "None",
|
|
71
|
+
isGateway: false,
|
|
72
|
+
microsoftBacked: false,
|
|
73
|
+
googleBacked: false,
|
|
74
|
+
};
|
|
75
|
+
const OTHER = {
|
|
76
|
+
id: "other",
|
|
77
|
+
name: "Other / Unknown",
|
|
78
|
+
isGateway: false,
|
|
79
|
+
microsoftBacked: false,
|
|
80
|
+
googleBacked: false,
|
|
81
|
+
};
|
|
82
|
+
/** Classify a domain's mail provider from its ordered MX hostnames. */
|
|
83
|
+
function fingerprintMx(mxHosts) {
|
|
84
|
+
if (mxHosts.length === 0) {
|
|
85
|
+
return NONE;
|
|
86
|
+
}
|
|
87
|
+
const hosts = mxHosts.map((host) => host.toLowerCase().replace(/\.$/, ""));
|
|
88
|
+
for (const rule of RULES) {
|
|
89
|
+
const hit = hosts.some((host) => rule.needles.some((needle) => host.includes(needle)));
|
|
90
|
+
if (hit) {
|
|
91
|
+
return {
|
|
92
|
+
id: rule.id,
|
|
93
|
+
name: rule.name,
|
|
94
|
+
isGateway: rule.isGateway ?? false,
|
|
95
|
+
microsoftBacked: rule.microsoftBacked ?? false,
|
|
96
|
+
googleBacked: rule.googleBacked ?? false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return OTHER;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* True when the provider hosts its own non-Microsoft mailboxes, so a "Managed"
|
|
104
|
+
* Azure AD namespace there is only a leftover tenant artifact and the O365
|
|
105
|
+
* account check must NOT be trusted. Gateways are excluded: O365 sits behind them.
|
|
106
|
+
*/
|
|
107
|
+
function hostsNonMicrosoftMailboxes(provider) {
|
|
108
|
+
return (provider.googleBacked ||
|
|
109
|
+
provider.id === "zoho" ||
|
|
110
|
+
provider.id === "yahoo" ||
|
|
111
|
+
provider.id === "yandex" ||
|
|
112
|
+
provider.id === "fastmail");
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=mx-fingerprint.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a person's name and build candidate email addresses ordered by
|
|
3
|
+
* real-world corporate frequency.
|
|
4
|
+
*
|
|
5
|
+
* The order is load-bearing: the verifier checks candidates top-down and stops
|
|
6
|
+
* on the first hit, so the most likely patterns must come first to minimize
|
|
7
|
+
* paid verifications. "common" keeps only the leading high-probability slice.
|
|
8
|
+
*/
|
|
9
|
+
/** Lowercase, strip accents and keep only [a-z0-9]. */
|
|
10
|
+
export declare function normalizeNamePart(value: string): string;
|
|
11
|
+
/** Strip protocol, path, leading "@" and lowercase a domain input. */
|
|
12
|
+
export declare function normalizeDomain(value: string): string;
|
|
13
|
+
export type PatternSet = "common" | "all";
|
|
14
|
+
export interface GenerateEmailsParams {
|
|
15
|
+
firstName: string;
|
|
16
|
+
lastName: string;
|
|
17
|
+
domain: string;
|
|
18
|
+
/** "common" keeps only the top, highest-probability patterns. Default "all". */
|
|
19
|
+
patternSet?: PatternSet;
|
|
20
|
+
/** Hard cap on candidates returned, applied after ordering. */
|
|
21
|
+
maxVariants?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generate a de-duplicated list of candidate emails ordered by likelihood.
|
|
25
|
+
* Returns an empty array when the domain or both name parts are missing.
|
|
26
|
+
*/
|
|
27
|
+
export declare function generateEmailCandidates(params: GenerateEmailsParams): string[];
|
|
28
|
+
//# sourceMappingURL=patterns.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Normalize a person's name and build candidate email addresses ordered by
|
|
4
|
+
* real-world corporate frequency.
|
|
5
|
+
*
|
|
6
|
+
* The order is load-bearing: the verifier checks candidates top-down and stops
|
|
7
|
+
* on the first hit, so the most likely patterns must come first to minimize
|
|
8
|
+
* paid verifications. "common" keeps only the leading high-probability slice.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.normalizeNamePart = normalizeNamePart;
|
|
12
|
+
exports.normalizeDomain = normalizeDomain;
|
|
13
|
+
exports.generateEmailCandidates = generateEmailCandidates;
|
|
14
|
+
function stripDiacritics(input) {
|
|
15
|
+
let result = "";
|
|
16
|
+
for (const char of input.normalize("NFKD")) {
|
|
17
|
+
const code = char.codePointAt(0) ?? 0;
|
|
18
|
+
// Drop combining diacritical marks (U+0300–U+036F).
|
|
19
|
+
if (code >= 0x0300 && code <= 0x036f) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
result += char;
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
/** Lowercase, strip accents and keep only [a-z0-9]. */
|
|
27
|
+
function normalizeNamePart(value) {
|
|
28
|
+
return stripDiacritics(value ?? "")
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/[^a-z0-9]/g, "");
|
|
31
|
+
}
|
|
32
|
+
/** Strip protocol, path, leading "@" and lowercase a domain input. */
|
|
33
|
+
function normalizeDomain(value) {
|
|
34
|
+
return (value ?? "")
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/^https?:\/\//, "")
|
|
38
|
+
.replace(/^@/, "")
|
|
39
|
+
.replace(/\/.*$/, "")
|
|
40
|
+
.replace(/\s+/g, "");
|
|
41
|
+
}
|
|
42
|
+
const LOCAL_PART_REGEX = /^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/;
|
|
43
|
+
/** Leading patterns that cover the large majority of real corporate addresses. */
|
|
44
|
+
const COMMON_LIMIT = 10;
|
|
45
|
+
const join = (a, sep, b) => a && b ? `${a}${sep}${b}` : "";
|
|
46
|
+
// Ordered by approximate real-world B2B frequency (most common first). Each
|
|
47
|
+
// trailing comment names the produced local-part so the terse builders stay
|
|
48
|
+
// readable. This order drives how many paid verifications an early-stop search
|
|
49
|
+
// performs, so changing it changes credit cost.
|
|
50
|
+
const PATTERNS = [
|
|
51
|
+
(p) => join(p.first, ".", p.last), // first.last
|
|
52
|
+
(p) => join(p.fi, "", p.last), // flast
|
|
53
|
+
(p) => p.first, // first
|
|
54
|
+
(p) => join(p.first, "", p.last), // firstlast
|
|
55
|
+
(p) => join(p.first, "_", p.last), // first_last
|
|
56
|
+
(p) => join(p.fi, ".", p.last), // f.last
|
|
57
|
+
(p) => join(p.first, "-", p.last), // first-last
|
|
58
|
+
(p) => p.last, // last
|
|
59
|
+
(p) => join(p.last, ".", p.first), // last.first
|
|
60
|
+
(p) => join(p.first, "", p.li), // firstl
|
|
61
|
+
(p) => join(p.fi, "", p.li), // fl
|
|
62
|
+
(p) => join(p.last, "", p.first), // lastfirst
|
|
63
|
+
(p) => join(p.last, "_", p.first), // last_first
|
|
64
|
+
(p) => join(p.last, "-", p.first), // last-first
|
|
65
|
+
(p) => join(p.fi, "_", p.last), // f_last
|
|
66
|
+
(p) => join(p.fi, "-", p.last), // f-last
|
|
67
|
+
(p) => join(p.last, "", p.fi), // lastf
|
|
68
|
+
(p) => join(p.last, ".", p.fi), // last.f
|
|
69
|
+
(p) => join(p.li, "", p.fi), // lf
|
|
70
|
+
(p) => join(p.first, ".", p.li), // first.l
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* Generate a de-duplicated list of candidate emails ordered by likelihood.
|
|
74
|
+
* Returns an empty array when the domain or both name parts are missing.
|
|
75
|
+
*/
|
|
76
|
+
function generateEmailCandidates(params) {
|
|
77
|
+
const first = normalizeNamePart(params.firstName);
|
|
78
|
+
const last = normalizeNamePart(params.lastName);
|
|
79
|
+
const domain = normalizeDomain(params.domain);
|
|
80
|
+
if (!domain || (!first && !last)) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const parts = {
|
|
84
|
+
first,
|
|
85
|
+
last,
|
|
86
|
+
fi: first.charAt(0),
|
|
87
|
+
li: last.charAt(0),
|
|
88
|
+
};
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
const locals = [];
|
|
91
|
+
for (const build of PATTERNS) {
|
|
92
|
+
const local = build(parts);
|
|
93
|
+
if (!local || seen.has(local) || !LOCAL_PART_REGEX.test(local)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
seen.add(local);
|
|
97
|
+
locals.push(local);
|
|
98
|
+
}
|
|
99
|
+
const limit = params.maxVariants ??
|
|
100
|
+
(params.patternSet === "common" ? COMMON_LIMIT : locals.length);
|
|
101
|
+
return locals
|
|
102
|
+
.slice(0, Math.max(0, limit))
|
|
103
|
+
.map((local) => `${local}@${domain}`);
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=patterns.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Microsoft 365 / Azure AD account-existence check.
|
|
3
|
+
*
|
|
4
|
+
* Two unauthenticated endpoints, used widely for O365 user enumeration:
|
|
5
|
+
*
|
|
6
|
+
* 1. getuserrealm.srf — classifies the domain's namespace:
|
|
7
|
+
* Managed → mailboxes live in Azure AD / Exchange Online (the account
|
|
8
|
+
* check below is authoritative).
|
|
9
|
+
* Federated → auth is delegated to on-prem ADFS/Okta/etc.; the account
|
|
10
|
+
* check is unreliable, so we skip the verdict.
|
|
11
|
+
* Unknown → not a Microsoft domain.
|
|
12
|
+
*
|
|
13
|
+
* 2. GetCredentialType — returns `IfExistsResult`, which reveals whether a
|
|
14
|
+
* given Azure AD / Microsoft account exists *without* SMTP. This is the
|
|
15
|
+
* single most valuable non-SMTP signal because a large share of corporate
|
|
16
|
+
* domains run on O365, including many that sit behind a security gateway.
|
|
17
|
+
*
|
|
18
|
+
* Everything fails soft: any network/throttle/parse problem yields an
|
|
19
|
+
* inconclusive result rather than a false verdict.
|
|
20
|
+
*/
|
|
21
|
+
export type O365Namespace = "Managed" | "Federated" | "Unknown" | null;
|
|
22
|
+
export interface O365Result {
|
|
23
|
+
/** Whether GetCredentialType produced an interpretable verdict. */
|
|
24
|
+
checked: boolean;
|
|
25
|
+
/** true = account exists, false = does not exist, null = inconclusive. */
|
|
26
|
+
exists: boolean | null;
|
|
27
|
+
namespace: O365Namespace;
|
|
28
|
+
ifExistsResult: number | null;
|
|
29
|
+
reason: string;
|
|
30
|
+
}
|
|
31
|
+
export declare const O365_UNCHECKED: O365Result;
|
|
32
|
+
export declare function isMicrosoftConsumerDomain(domain: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Map an `IfExistsResult` code to an existence verdict.
|
|
35
|
+
* 0 → exists
|
|
36
|
+
* 1 → does not exist
|
|
37
|
+
* 6 → exists in a different Microsoft tenant (still a real account)
|
|
38
|
+
* 5 → different identity provider (ambiguous for this domain) → inconclusive
|
|
39
|
+
* other → inconclusive
|
|
40
|
+
*/
|
|
41
|
+
export declare function interpretIfExists(code: number | null): boolean | null;
|
|
42
|
+
/** Classify a domain's Microsoft namespace. Returns null on any failure. */
|
|
43
|
+
export declare function getUserRealm(domain: string, timeoutMs: number): Promise<O365Namespace>;
|
|
44
|
+
/**
|
|
45
|
+
* Run the account-existence check for one email, given a pre-resolved namespace
|
|
46
|
+
* for its domain. `applicable` should be true only when the domain is known to
|
|
47
|
+
* be Microsoft-backed (Managed namespace, Microsoft MX, or a consumer domain);
|
|
48
|
+
* otherwise GetCredentialType returns noise for unrelated domains.
|
|
49
|
+
*/
|
|
50
|
+
export declare function checkO365Account(email: string, namespace: O365Namespace, applicable: boolean, timeoutMs: number): Promise<O365Result>;
|
|
51
|
+
//# sourceMappingURL=o365.d.ts.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Microsoft 365 / Azure AD account-existence check.
|
|
4
|
+
*
|
|
5
|
+
* Two unauthenticated endpoints, used widely for O365 user enumeration:
|
|
6
|
+
*
|
|
7
|
+
* 1. getuserrealm.srf — classifies the domain's namespace:
|
|
8
|
+
* Managed → mailboxes live in Azure AD / Exchange Online (the account
|
|
9
|
+
* check below is authoritative).
|
|
10
|
+
* Federated → auth is delegated to on-prem ADFS/Okta/etc.; the account
|
|
11
|
+
* check is unreliable, so we skip the verdict.
|
|
12
|
+
* Unknown → not a Microsoft domain.
|
|
13
|
+
*
|
|
14
|
+
* 2. GetCredentialType — returns `IfExistsResult`, which reveals whether a
|
|
15
|
+
* given Azure AD / Microsoft account exists *without* SMTP. This is the
|
|
16
|
+
* single most valuable non-SMTP signal because a large share of corporate
|
|
17
|
+
* domains run on O365, including many that sit behind a security gateway.
|
|
18
|
+
*
|
|
19
|
+
* Everything fails soft: any network/throttle/parse problem yields an
|
|
20
|
+
* inconclusive result rather than a false verdict.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.O365_UNCHECKED = void 0;
|
|
24
|
+
exports.isMicrosoftConsumerDomain = isMicrosoftConsumerDomain;
|
|
25
|
+
exports.interpretIfExists = interpretIfExists;
|
|
26
|
+
exports.getUserRealm = getUserRealm;
|
|
27
|
+
exports.checkO365Account = checkO365Account;
|
|
28
|
+
const http_1 = require("../http");
|
|
29
|
+
exports.O365_UNCHECKED = {
|
|
30
|
+
checked: false,
|
|
31
|
+
exists: null,
|
|
32
|
+
namespace: null,
|
|
33
|
+
ifExistsResult: null,
|
|
34
|
+
reason: "not_checked",
|
|
35
|
+
};
|
|
36
|
+
// Consumer Microsoft domains (MSA). getuserrealm often reports these as
|
|
37
|
+
// "Unknown", but GetCredentialType is still authoritative for them.
|
|
38
|
+
const CONSUMER_DOMAINS = new Set([
|
|
39
|
+
"outlook.com",
|
|
40
|
+
"hotmail.com",
|
|
41
|
+
"live.com",
|
|
42
|
+
"msn.com",
|
|
43
|
+
"passport.com",
|
|
44
|
+
"windowslive.com",
|
|
45
|
+
"hotmail.co.uk",
|
|
46
|
+
"hotmail.fr",
|
|
47
|
+
"hotmail.de",
|
|
48
|
+
"hotmail.it",
|
|
49
|
+
"hotmail.es",
|
|
50
|
+
"live.co.uk",
|
|
51
|
+
"live.fr",
|
|
52
|
+
"live.de",
|
|
53
|
+
"outlook.fr",
|
|
54
|
+
"outlook.de",
|
|
55
|
+
"outlook.es",
|
|
56
|
+
"outlook.jp",
|
|
57
|
+
]);
|
|
58
|
+
function isMicrosoftConsumerDomain(domain) {
|
|
59
|
+
return CONSUMER_DOMAINS.has(domain.toLowerCase());
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Map an `IfExistsResult` code to an existence verdict.
|
|
63
|
+
* 0 → exists
|
|
64
|
+
* 1 → does not exist
|
|
65
|
+
* 6 → exists in a different Microsoft tenant (still a real account)
|
|
66
|
+
* 5 → different identity provider (ambiguous for this domain) → inconclusive
|
|
67
|
+
* other → inconclusive
|
|
68
|
+
*/
|
|
69
|
+
function interpretIfExists(code) {
|
|
70
|
+
if (code === 0 || code === 6) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (code === 1) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/** Classify a domain's Microsoft namespace. Returns null on any failure. */
|
|
79
|
+
async function getUserRealm(domain, timeoutMs) {
|
|
80
|
+
const login = encodeURIComponent(`probe@${domain}`);
|
|
81
|
+
try {
|
|
82
|
+
const res = await (0, http_1.httpRequest)({
|
|
83
|
+
url: `https://login.microsoftonline.com/getuserrealm.srf?login=${login}&json=1`,
|
|
84
|
+
method: "GET",
|
|
85
|
+
timeoutMs,
|
|
86
|
+
});
|
|
87
|
+
if (res.status !== 200) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const data = JSON.parse(res.body);
|
|
91
|
+
const ns = data.NameSpaceType;
|
|
92
|
+
if (ns === "Managed" || ns === "Federated" || ns === "Unknown") {
|
|
93
|
+
return ns;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Query GetCredentialType for a single address. Returns null code on failure. */
|
|
102
|
+
async function probeCredentialType(email, timeoutMs) {
|
|
103
|
+
try {
|
|
104
|
+
const res = await (0, http_1.httpRequest)({
|
|
105
|
+
url: "https://login.microsoftonline.com/common/GetCredentialType?mkt=en-US",
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { "Content-Type": "application/json; charset=UTF-8" },
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
Username: email,
|
|
110
|
+
isOtherIdpSupported: true,
|
|
111
|
+
}),
|
|
112
|
+
timeoutMs,
|
|
113
|
+
});
|
|
114
|
+
if (res.status !== 200) {
|
|
115
|
+
return { ifExistsResult: null, throttled: false };
|
|
116
|
+
}
|
|
117
|
+
const data = JSON.parse(res.body);
|
|
118
|
+
return {
|
|
119
|
+
ifExistsResult: typeof data.IfExistsResult === "number"
|
|
120
|
+
? data.IfExistsResult
|
|
121
|
+
: null,
|
|
122
|
+
throttled: (data.ThrottleStatus ?? 0) !== 0,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return { ifExistsResult: null, throttled: false };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Run the account-existence check for one email, given a pre-resolved namespace
|
|
131
|
+
* for its domain. `applicable` should be true only when the domain is known to
|
|
132
|
+
* be Microsoft-backed (Managed namespace, Microsoft MX, or a consumer domain);
|
|
133
|
+
* otherwise GetCredentialType returns noise for unrelated domains.
|
|
134
|
+
*/
|
|
135
|
+
async function checkO365Account(email, namespace, applicable, timeoutMs) {
|
|
136
|
+
if (!applicable) {
|
|
137
|
+
return { ...exports.O365_UNCHECKED, namespace, reason: "not_microsoft_domain" };
|
|
138
|
+
}
|
|
139
|
+
const probe = await probeCredentialType(email, timeoutMs);
|
|
140
|
+
if (probe.throttled) {
|
|
141
|
+
return {
|
|
142
|
+
checked: false,
|
|
143
|
+
exists: null,
|
|
144
|
+
namespace,
|
|
145
|
+
ifExistsResult: probe.ifExistsResult,
|
|
146
|
+
reason: "throttled",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (probe.ifExistsResult === null) {
|
|
150
|
+
return {
|
|
151
|
+
checked: false,
|
|
152
|
+
exists: null,
|
|
153
|
+
namespace,
|
|
154
|
+
ifExistsResult: null,
|
|
155
|
+
reason: "no_result",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const exists = interpretIfExists(probe.ifExistsResult);
|
|
159
|
+
return {
|
|
160
|
+
checked: exists !== null,
|
|
161
|
+
exists,
|
|
162
|
+
namespace,
|
|
163
|
+
ifExistsResult: probe.ifExistsResult,
|
|
164
|
+
reason: exists === true
|
|
165
|
+
? "account_exists"
|
|
166
|
+
: exists === false
|
|
167
|
+
? "account_not_found"
|
|
168
|
+
: "inconclusive",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=o365.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-agnostic email-existence verifier.
|
|
3
|
+
*
|
|
4
|
+
* Both the direct O365 account check and the no2bounce API implement this, so
|
|
5
|
+
* the credit-saving search logic (early-stop, catch-all-stop) in `find-email`
|
|
6
|
+
* is identical regardless of which provider actually answers.
|
|
7
|
+
*/
|
|
8
|
+
export type VerifyVerdict = "valid" | "invalid" | "catch_all" | "unknown";
|
|
9
|
+
export interface VerifyOutcome {
|
|
10
|
+
email: string;
|
|
11
|
+
verdict: VerifyVerdict;
|
|
12
|
+
/** Credits this single verification consumed (0 for the free O365 check). */
|
|
13
|
+
creditsUsed: number;
|
|
14
|
+
reason: string;
|
|
15
|
+
catchAll?: boolean;
|
|
16
|
+
raw?: unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface Verifier {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
verify(email: string): Promise<VerifyOutcome>;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=verifier.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Provider-agnostic email-existence verifier.
|
|
4
|
+
*
|
|
5
|
+
* Both the direct O365 account check and the no2bounce API implement this, so
|
|
6
|
+
* the credit-saving search logic (early-stop, catch-all-stop) in `find-email`
|
|
7
|
+
* is identical regardless of which provider actually answers.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
//# sourceMappingURL=verifier.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifier adapter for the no2bounce API (wire format confirmed against the
|
|
3
|
+
* live API).
|
|
4
|
+
*
|
|
5
|
+
* Async flow: POST a 1-email batch to get a `trackingId` (under `data`), then
|
|
6
|
+
* GET-poll `?trackingId=` until the *top-level* `overallStatus` is "Completed".
|
|
7
|
+
* For a single email the top-level count fields are that email's verdict.
|
|
8
|
+
* Billing is 1 credit per email (`creditDebited`), with no refund on undeliverable.
|
|
9
|
+
*/
|
|
10
|
+
import type { Verifier, VerifyVerdict } from "../verifier";
|
|
11
|
+
export interface No2BounceOptions {
|
|
12
|
+
apitoken: string;
|
|
13
|
+
timeoutMs: number;
|
|
14
|
+
pollIntervalMs?: number;
|
|
15
|
+
maxWaitMs?: number;
|
|
16
|
+
}
|
|
17
|
+
interface PollResult {
|
|
18
|
+
overallStatus?: string;
|
|
19
|
+
percent?: number;
|
|
20
|
+
creditDebited?: number;
|
|
21
|
+
Deliverable?: number;
|
|
22
|
+
Undeliverable?: number;
|
|
23
|
+
"Deliverable/AcceptAll"?: number;
|
|
24
|
+
"UnDeliverable/AcceptAll"?: number;
|
|
25
|
+
"Risky/AcceptAll"?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface DecodedResult {
|
|
28
|
+
verdict: VerifyVerdict;
|
|
29
|
+
catchAll: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function decodeSingle(r: PollResult): DecodedResult;
|
|
32
|
+
export declare function createNo2BounceVerifier(options: No2BounceOptions): Verifier;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=no2bounce-verifier.d.ts.map
|