@visulima/email-verifier 0.0.1 → 1.0.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/CHANGELOG.md +29 -0
- package/LICENSE.md +27 -0
- package/README.md +134 -29
- package/dist/checks/character.d.ts +31 -0
- package/dist/checks/character.js +1 -0
- package/dist/checks/disposable.d.ts +1 -0
- package/dist/checks/disposable.js +1 -0
- package/dist/checks/free.d.ts +1 -0
- package/dist/checks/free.js +1 -0
- package/dist/checks/mx.d.ts +1 -0
- package/dist/checks/mx.js +1 -0
- package/dist/checks/role.d.ts +41 -0
- package/dist/checks/role.js +1 -0
- package/dist/checks/smtp.d.ts +93 -0
- package/dist/checks/smtp.js +7 -0
- package/dist/checks/symbol.d.ts +29 -0
- package/dist/checks/symbol.js +1 -0
- package/dist/checks/syntax.d.ts +19 -0
- package/dist/checks/syntax.js +1 -0
- package/dist/checks/tag.d.ts +31 -0
- package/dist/checks/tag.js +1 -0
- package/dist/enrich/name.d.ts +32 -0
- package/dist/enrich/name.js +1 -0
- package/dist/enrich/provider.d.ts +40 -0
- package/dist/enrich/provider.js +1 -0
- package/dist/enrich/typo.d.ts +59 -0
- package/dist/enrich/typo.js +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +1 -0
- package/dist/packem_shared/InMemoryCache-DyBUMeKq.js +1 -0
- package/dist/packem_shared/extractDomain-CYJf1_0N.js +1 -0
- package/dist/packem_shared/mx.d-NAesS5sg.d.ts +117 -0
- package/dist/packem_shared/score.d-BTcZnl2L.d.ts +134 -0
- package/dist/score.d.ts +8 -0
- package/dist/score.js +1 -0
- package/dist/verify-email.d.ts +62 -0
- package/dist/verify-email.js +1 -0
- package/package.json +122 -7
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
## @visulima/email-verifier 1.0.0 (2026-07-03)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* **email:** emailable-parity verification & enrichment packages ([#718](https://github.com/visulima/visulima/issues/718)) ([e2d8c1e](https://github.com/visulima/visulima/commit/e2d8c1e78e3e0c2548ece87b20d730dab60f677a))
|
|
6
|
+
|
|
7
|
+
### Miscellaneous Chores
|
|
8
|
+
|
|
9
|
+
* **release:** @visulima/email-verifier@1.0.0-alpha.1 [skip ci]\n\n## @visulima/email-verifier 1.0.0-alpha.1 (2026-06-30) ([2f54bf0](https://github.com/visulima/visulima/commit/2f54bf0d4eced72aea511f282df9c132e965aac7))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Dependencies
|
|
13
|
+
|
|
14
|
+
* **@visulima/disposable-email-domains:** upgraded to 1.0.0
|
|
15
|
+
* **@visulima/email-provider-mx:** upgraded to 1.0.0
|
|
16
|
+
* **@visulima/free-email-domains:** upgraded to 1.0.0
|
|
17
|
+
|
|
18
|
+
## @visulima/email-verifier 1.0.0-alpha.1 (2026-06-30)
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* **email:** emailable-parity verification & enrichment packages ([#718](https://github.com/visulima/visulima/issues/718)) ([e2d8c1e](https://github.com/visulima/visulima/commit/e2d8c1e78e3e0c2548ece87b20d730dab60f677a))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Dependencies
|
|
26
|
+
|
|
27
|
+
* **@visulima/disposable-email-domains:** upgraded to 1.0.0-alpha.18
|
|
28
|
+
* **@visulima/email-provider-mx:** upgraded to 1.0.0-alpha.1
|
|
29
|
+
* **@visulima/free-email-domains:** upgraded to 1.0.0-alpha.1
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 visulima
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
<!-- DEPENDENCIES -->
|
|
24
|
+
<!-- /DEPENDENCIES -->
|
|
25
|
+
|
|
26
|
+
<!-- TYPE_DEPENDENCIES -->
|
|
27
|
+
<!-- /TYPE_DEPENDENCIES -->
|
package/README.md
CHANGED
|
@@ -1,45 +1,150 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- START_PACKAGE_OG_IMAGE_PLACEHOLDER -->
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<a href="https://www.anolilab.com/open-source" align="center">
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<img src="__assets__/package-og.svg" alt="email-verifier" />
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
</a>
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
<h3 align="center">Mailer-free email address verification and enrichment: syntax, MX/SMTP probing, disposable/free/role detection, catch-all, provider & secure-email-gateway classification, typo suggestions, and a 0–100 quality score.</h3>
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `@visulima/email-verifier`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
11
|
+
<!-- END_PACKAGE_OG_IMAGE_PLACEHOLDER -->
|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
<br />
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
<div align="center">
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
[![typescript-image][typescript-badge]][typescript-url]
|
|
18
|
+
[![mit licence][license-badge]][license]
|
|
19
|
+
[![npm downloads][npm-downloads-badge]][npm-downloads]
|
|
20
|
+
[![Chat][chat-badge]][chat]
|
|
21
|
+
[![PRs Welcome][prs-welcome-badge]][prs-welcome]
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
</div>
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<div align="center">
|
|
28
|
+
<p>
|
|
29
|
+
<sup>
|
|
30
|
+
Daniel Bannert's open source work is supported by the community on <a href="https://github.com/sponsors/prisis">GitHub Sponsors</a>
|
|
31
|
+
</sup>
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
---
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
37
|
+
`@visulima/email-verifier` validates and enriches an email address without
|
|
38
|
+
pulling in a mail-sending stack. It mirrors the surface of commercial APIs like
|
|
39
|
+
[emailable](https://emailable.com): syntax + domain + SMTP verification,
|
|
40
|
+
catch-all / mailbox-full / greylist handling, disposable / free / role / no-reply
|
|
41
|
+
detection, sub-address (tag) parsing, character & Unicode-symbol analysis,
|
|
42
|
+
provider & Secure-Email-Gateway classification, misspelled-domain suggestions,
|
|
43
|
+
name parsing, and a transparent 0–100 quality score — all aggregated into one
|
|
44
|
+
`EmailVerificationReport`.
|
|
36
45
|
|
|
37
|
-
|
|
46
|
+
> It is **mailer-free**: it does not depend on `@visulima/email`, so a
|
|
47
|
+
> validation-only consumer never installs the 25 provider adapters.
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
- [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
49
|
+
## Install
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
```sh
|
|
52
|
+
npm install @visulima/email-verifier
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
yarn add @visulima/email-verifier
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
pnpm add @visulima/email-verifier
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { verifyEmail } from "@visulima/email-verifier";
|
|
67
|
+
|
|
68
|
+
const report = await verifyEmail("john.doe@gmail.com");
|
|
69
|
+
|
|
70
|
+
report.state; // "deliverable" | "risky" | "undeliverable" | "unknown"
|
|
71
|
+
report.reason; // e.g. "accepted_email"
|
|
72
|
+
report.score; // 0–100
|
|
73
|
+
report.free; // true
|
|
74
|
+
report.provider?.display; // "Google"
|
|
75
|
+
report.name; // { firstName: "John", lastName: "Doe", fullName: "John Doe", confidence: "high" }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
> **Network caveat:** SMTP verification connects to the recipient's mail server on
|
|
79
|
+
> port 25. Many networks block outbound 25 and many servers tarpit or refuse
|
|
80
|
+
> verification probes, so treat a `deferred`/`unknown` result as inconclusive
|
|
81
|
+
> rather than undeliverable. Pass `{ offline: true }` to skip all network checks.
|
|
82
|
+
|
|
83
|
+
### À la carte checks
|
|
84
|
+
|
|
85
|
+
Every check and enrichment is also exported on its own subpath so you only pull
|
|
86
|
+
in what you need:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { validateSyntax } from "@visulima/email-verifier/checks/syntax";
|
|
90
|
+
import { checkMxRecords } from "@visulima/email-verifier/checks/mx";
|
|
91
|
+
import { verifySmtp } from "@visulima/email-verifier/checks/smtp";
|
|
92
|
+
import { checkDisposable } from "@visulima/email-verifier/checks/disposable";
|
|
93
|
+
import { checkFree } from "@visulima/email-verifier/checks/free";
|
|
94
|
+
import { isRoleAccount, isNoReply } from "@visulima/email-verifier/checks/role";
|
|
95
|
+
import { detectTag } from "@visulima/email-verifier/checks/tag";
|
|
96
|
+
import { analyzeCharacters } from "@visulima/email-verifier/checks/character";
|
|
97
|
+
import { analyzeSymbols } from "@visulima/email-verifier/checks/symbol";
|
|
98
|
+
import { enrichProvider } from "@visulima/email-verifier/enrich/provider";
|
|
99
|
+
import { suggestEmailTypo } from "@visulima/email-verifier/enrich/typo";
|
|
100
|
+
import { parseName } from "@visulima/email-verifier/enrich/name";
|
|
101
|
+
import { scoreReport } from "@visulima/email-verifier/score";
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Scoring
|
|
105
|
+
|
|
106
|
+
The score is a transparent additive model over a 100-point baseline. Every weight
|
|
107
|
+
is overridable:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { verifyEmail } from "@visulima/email-verifier";
|
|
111
|
+
|
|
112
|
+
const report = await verifyEmail("info@example.com", {
|
|
113
|
+
weights: { role: 40, free: 0 },
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Related
|
|
118
|
+
|
|
119
|
+
- [`@visulima/disposable-email-domains`](https://github.com/visulima/visulima/tree/main/packages/email/disposable-email-domains) — disposable-domain list (used by `checks/disposable`).
|
|
120
|
+
- [`@visulima/free-email-domains`](https://github.com/visulima/visulima/tree/main/packages/email/free-email-domains) — free-provider domain list (used by `checks/free`).
|
|
121
|
+
- [`@visulima/email-provider-mx`](https://github.com/visulima/visulima/tree/main/packages/email/email-provider-mx) — MX-host → provider/SEG map (used by `enrich/provider`).
|
|
122
|
+
- [`@visulima/email`](https://github.com/visulima/visulima/tree/main/packages/email/email) — the mail-sending library (send-side validation only).
|
|
123
|
+
|
|
124
|
+
## Supported Node.js Versions
|
|
125
|
+
|
|
126
|
+
Libraries in this ecosystem make the best effort to track [Node.js' release schedule](https://github.com/nodejs/release#release-schedule).
|
|
127
|
+
|
|
128
|
+
## Contributing
|
|
129
|
+
|
|
130
|
+
If you would like to help take a look at the [list of issues](https://github.com/visulima/visulima/issues) and check our [Contributing](.github/CONTRIBUTING.md) guidelines.
|
|
131
|
+
|
|
132
|
+
## Credits
|
|
133
|
+
|
|
134
|
+
- [Daniel Bannert](https://github.com/prisis)
|
|
135
|
+
- [All Contributors](https://github.com/visulima/visulima/graphs/contributors)
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
The visulima email-verifier is open-sourced software licensed under the [MIT][license]
|
|
44
140
|
|
|
45
|
-
|
|
141
|
+
[typescript-badge]: https://img.shields.io/badge/TypeScript-3178C6.svg?style=for-the-badge&logo=typescript&logoColor=white "typescript"
|
|
142
|
+
[typescript-url]: https://www.typescriptlang.org/ "TypeScript"
|
|
143
|
+
[license-badge]: https://img.shields.io/npm/l/@visulima/email-verifier?color=blueviolet&style=for-the-badge "license"
|
|
144
|
+
[license]: LICENSE.md "license"
|
|
145
|
+
[npm-downloads-badge]: https://img.shields.io/npm/dm/@visulima/email-verifier?logo=npm&style=for-the-badge&labelColor=333333&color=476ad3 "npm downloads"
|
|
146
|
+
[npm-downloads]: https://www.npmjs.com/package/@visulima/email-verifier "npm downloads"
|
|
147
|
+
[chat-badge]: https://img.shields.io/discord/677851225179ita68409?color=7289da&label=Discord&logo=discord&logoColor=ffffff&style=for-the-badge "chat"
|
|
148
|
+
[chat]: https://discord.gg/J8GxgQ7Xv5 "chat"
|
|
149
|
+
[prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge "PRs welcome"
|
|
150
|
+
[prs-welcome]: https://github.com/visulima/visulima/blob/main/.github/CONTRIBUTING.md "PRs welcome"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The result of local-part character analysis.
|
|
3
|
+
*/
|
|
4
|
+
interface CharacterResult {
|
|
5
|
+
/** Ratio of digits to total characters in the local part (0–1). */
|
|
6
|
+
digitRatio: number;
|
|
7
|
+
/** True when the local part looks irregular / machine-generated / risky. */
|
|
8
|
+
irregular: boolean;
|
|
9
|
+
/** Length of the local part. */
|
|
10
|
+
length: number;
|
|
11
|
+
/** Human-readable reasons contributing to the `irregular` verdict. */
|
|
12
|
+
reasons: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Analyzes the local part of an email for irregular characteristics that
|
|
16
|
+
* correlate with low-quality, spam-trap, or machine-generated addresses.
|
|
17
|
+
*
|
|
18
|
+
* Heuristic and intentionally conservative — it flags *patterns* (excessive
|
|
19
|
+
* length, digit-heavy locals, long unpronounceable consonant runs, long repeats)
|
|
20
|
+
* rather than asserting invalidity.
|
|
21
|
+
* @param email The email address to analyze.
|
|
22
|
+
* @returns The character analysis result.
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { analyzeCharacters } from "@visulima/email-verifier/checks/character";
|
|
26
|
+
*
|
|
27
|
+
* analyzeCharacters("aaaaaaa@example.com").irregular; // long repeat ⇒ true
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
declare const analyzeCharacters: (email: string) => CharacterResult;
|
|
31
|
+
export { type CharacterResult, analyzeCharacters, analyzeCharacters as default };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{splitAddress as h}from"../packem_shared/extractDomain-CYJf1_0N.js";const g=30,l=.6,i=7,o=/\d/g,c=/[bcdfghj-np-tvwxz]{7,}/,u=/(.)\1{4,}/,p=a=>{const r=h(a);if(!r)return{digitRatio:0,irregular:!0,length:0,reasons:["invalid-format"]};const{localPart:t}=r,e=[],s=(t.match(o)??[]).length,n=t.length===0?0:s/t.length;return t.length>g&&e.push("excessive-length"),n>=l&&t.length>4&&e.push("digit-heavy"),c.test(t)&&e.push(`consonant-run-${String(i)}+`),u.test(t)&&e.push("repeated-character"),{digitRatio:n,irregular:e.length>0,length:t.length,reasons:e}};export{p as analyzeCharacters,p as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type DisposableEmailOptions, areDisposableEmails, isDisposableEmail as default, isDisposableDomain, isDisposableEmail, isListLoaded, preload, setDomains } from '@visulima/disposable-email-domains';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{areDisposableEmails as i,isDisposableEmail as e,isDisposableDomain as o,isDisposableEmail as l,isListLoaded as m,preload as p,setDomains as D}from"@visulima/disposable-email-domains";export{i as areDisposableEmails,e as default,o as isDisposableDomain,l as isDisposableEmail,m as isListLoaded,p as preload,D as setDomains};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type FreeEmailOptions, areFreeEmails, isFreeEmail as default, isFreeDomain, isFreeEmail, isListLoaded, preload, setDomains } from '@visulima/free-email-domains';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{areFreeEmails as i,isFreeEmail as s,isFreeDomain as r,isFreeEmail as m,isListLoaded as o,preload as l,setDomains as d}from"@visulima/free-email-domains";export{i as areFreeEmails,s as default,r as isFreeDomain,m as isFreeEmail,o as isListLoaded,l as preload,d as setDomains};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type C as Cache, type I as InMemoryCache, type M as MxCheckOptions, type b as MxCheckResult, type c as MxRecord, type d as MxResolution, e as checkMxRecords, e as default } from "../packem_shared/mx.d-NAesS5sg.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createRequire as d}from"node:module";const u=d(import.meta.url),o=typeof globalThis<"u"&&typeof globalThis.process<"u"?globalThis.process:process,f=e=>{if(typeof o<"u"&&o.versions&&o.versions.node){const[r,s]=o.versions.node.split(".").map(Number);if(r>22||r===22&&s>=3||r===20&&s>=16)return o.getBuiltinModule(e)}return u(e)},{resolveMx:v,resolve4:p,resolve6:h}=f("node:dns/promises"),c=async e=>{try{if((await p(e)).length>0)return!0}catch{}try{return(await h(e)).length>0}catch{return!1}},l=e=>({domainResolves:!0,records:[{exchange:e,priority:0}],resolvedVia:"address",valid:!0}),g=async(e,r)=>{try{const s=(await v(e)).map(t=>({exchange:t.exchange,priority:t.priority}));return s.length>0?{domainResolves:!0,records:s.toSorted((t,a)=>t.priority-a.priority),resolvedVia:"mx",valid:!0}:r&&await c(e)?l(e):{domainResolves:!1,records:[],resolvedVia:"none",valid:!1}}catch(s){return r&&await c(e)?l(e):{domainResolves:!1,error:s instanceof Error?s.message:String(s),resolvedVia:"none",valid:!1}}},y=async(e,r={})=>{const{fallbackToAddress:s=!0}=r,t=r.ttl??36e5,a=`${e}:${String(s)}`;if(r.cache){const n=await r.cache.get(a);if(n!==void 0)return n}const i=await g(e,s);return r.cache&&await r.cache.set(a,i,t),i};export{y as checkMxRecords,y as default};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-part prefixes that indicate a no-reply / do-not-reply mailbox.
|
|
3
|
+
* These are a strict subset of {@link ROLE_ACCOUNT_PREFIXES}.
|
|
4
|
+
*/
|
|
5
|
+
declare const NO_REPLY_PREFIXES: ReadonlySet<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Local-part prefixes that identify a role-based (shared, function) mailbox
|
|
8
|
+
* rather than a personal one. Superset of RFC 2142 plus common real-world roles.
|
|
9
|
+
*/
|
|
10
|
+
declare const ROLE_ACCOUNT_PREFIXES: ReadonlySet<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Determines whether an email address is a role-based account.
|
|
13
|
+
*
|
|
14
|
+
* Matches the whole local part, the part before any `+tag`, and each
|
|
15
|
+
* dot/dash/underscore-separated token (so `sales.john`, `info+news`, and
|
|
16
|
+
* `no-reply` are all caught).
|
|
17
|
+
* @param email The email address to check.
|
|
18
|
+
* @param customPrefixes Optional additional role prefixes to recognize.
|
|
19
|
+
* @returns True when the address is a role account.
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { isRoleAccount } from "@visulima/email-verifier/checks/role";
|
|
23
|
+
*
|
|
24
|
+
* isRoleAccount("info@example.com"); // true
|
|
25
|
+
* isRoleAccount("john.doe@example.com"); // false
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
declare const isRoleAccount: (email: string, customPrefixes?: Iterable<string>) => boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Determines whether an email address is a no-reply / do-not-reply mailbox.
|
|
31
|
+
* @param email The email address to check.
|
|
32
|
+
* @returns True when the address is a no-reply mailbox.
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { isNoReply } from "@visulima/email-verifier/checks/role";
|
|
36
|
+
*
|
|
37
|
+
* isNoReply("no-reply@example.com"); // true
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
declare const isNoReply: (email: string) => boolean;
|
|
41
|
+
export { NO_REPLY_PREFIXES, ROLE_ACCOUNT_PREFIXES, isRoleAccount as default, isNoReply, isRoleAccount };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{splitAddress as c}from"../packem_shared/extractDomain-CYJf1_0N.js";const l=new Set(["do-not-reply","do_not_reply","donotreply","no-reply","no_reply","noreply","notification","notifications","notify"]),n=new Set(["abuse","accounting","accounts","admin","administrator","all","billing","board","career","careers","ceo","cfo","compliance","contact","contacts","cto","customercare","customerservice","dev","developer","devnull","do-not-reply","do_not_reply","donotreply","enquiries","enquiry","everyone","feedback","finance","ftp","hello","help","helpdesk","hi","hostmaster","hr","info","information","inquiries","inquiry","investor","investors","it","jobs","legal","list","list-request","mail","mailer-daemon","mailerdaemon","majordomo","marketing","media","members","newsletter","no-reply","no_reply","noc","noreply","notification","notifications","notify","office","operations","order","orders","postmaster","press","privacy","purchasing","recruitment","register","registration","root","sales","secretary","security","service","services","shop","spam","staff","subscribe","support","sysadmin","team","tech","test","unsubscribe","usenet","uucp","webmaster","welcome","www"]),u=/[.\-_+]/,o=(t,e,r)=>{if(e.has(t))return!0;if(r){for(const s of r)if(s.toLowerCase()===t)return!0}return!1},m=(t,e)=>{const r=c(t);if(!r)return!1;const{localPart:s}=r,i=s.split("+")[0];if(o(s,n,e)||o(i,n,e))return!0;const a=i.split(u).filter(Boolean);return a.length>1&&a.some(p=>o(p,n,e))},d=t=>{const e=c(t);if(!e)return!1;const r=e.localPart.split("+")[0];return o(e.localPart,l)||o(r,l)};export{l as NO_REPLY_PREFIXES,n as ROLE_ACCOUNT_PREFIXES,m as default,d as isNoReply,m as isRoleAccount};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { b as MxCheckResult, C as Cache, c as MxRecord } from "../packem_shared/mx.d-NAesS5sg.js";
|
|
2
|
+
/**
|
|
3
|
+
* Options for SMTP verification.
|
|
4
|
+
*/
|
|
5
|
+
interface SmtpVerificationOptions {
|
|
6
|
+
/** Cache for the underlying MX lookup. */
|
|
7
|
+
cache?: Cache<MxCheckResult>;
|
|
8
|
+
/**
|
|
9
|
+
* Number of random, almost-certainly-nonexistent addresses to probe for
|
|
10
|
+
* catch-all detection. More probes reduce false positives from
|
|
11
|
+
* deferred-rejection servers. Set to 0 to disable.
|
|
12
|
+
* @default 1
|
|
13
|
+
*/
|
|
14
|
+
catchAllProbes?: number;
|
|
15
|
+
/**
|
|
16
|
+
* MAIL FROM address used during the probe; defaults to `verify@` followed by
|
|
17
|
+
* the HELO host.
|
|
18
|
+
*/
|
|
19
|
+
fromEmail?: string;
|
|
20
|
+
/**
|
|
21
|
+
* HELO/EHLO hostname presented to the remote server.
|
|
22
|
+
* @default the address domain
|
|
23
|
+
*/
|
|
24
|
+
heloHost?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Pre-resolved MX records to probe, in priority order. When provided, the
|
|
27
|
+
* probe skips its own DNS lookup — the orchestrator passes the records it
|
|
28
|
+
* already resolved so the domain is not queried twice.
|
|
29
|
+
*/
|
|
30
|
+
mxRecords?: MxRecord[];
|
|
31
|
+
port?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Number of times to retry the whole probe on a temporary (greylist) failure.
|
|
34
|
+
* @default 1
|
|
35
|
+
*/
|
|
36
|
+
retries?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Delay in milliseconds between greylist retries.
|
|
39
|
+
* @default 5000
|
|
40
|
+
*/
|
|
41
|
+
retryDelay?: number;
|
|
42
|
+
/** Cache for SMTP results. */
|
|
43
|
+
smtpCache?: Cache<SmtpVerificationResult>;
|
|
44
|
+
timeout?: number;
|
|
45
|
+
ttl?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Detailed result of an SMTP verification attempt.
|
|
49
|
+
*/
|
|
50
|
+
interface SmtpVerificationResult {
|
|
51
|
+
/** True when the server accepts a random, nonexistent recipient (catch-all). */
|
|
52
|
+
acceptAll?: boolean;
|
|
53
|
+
/** The numeric SMTP status code for the real RCPT TO. */
|
|
54
|
+
code?: number;
|
|
55
|
+
/** True when the result is inconclusive due to a temporary failure (greylisting). */
|
|
56
|
+
deferred?: boolean;
|
|
57
|
+
/** Final enhanced status code for the real RCPT (e.g. "5.1.1"), if present. */
|
|
58
|
+
enhancedCode?: string;
|
|
59
|
+
error?: string;
|
|
60
|
+
/** True when the mailbox exists but is over quota (452/552 / 4.2.2). */
|
|
61
|
+
mailboxFull?: boolean;
|
|
62
|
+
mxRecords?: MxRecord[];
|
|
63
|
+
smtpResponse?: string;
|
|
64
|
+
/** True when the real RCPT TO was accepted (250). */
|
|
65
|
+
valid: boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Verifies an email address over SMTP without sending a message.
|
|
69
|
+
*
|
|
70
|
+
* Performs the full handshake (greeting → EHLO/HELO → MAIL FROM → RCPT TO),
|
|
71
|
+
* then — when the real recipient is accepted — issues additional RCPT TO probes
|
|
72
|
+
* for random addresses to detect catch-all (accept-all) servers. Temporary
|
|
73
|
+
* (4xx) failures are surfaced as `deferred` and retried up to `retries` times to
|
|
74
|
+
* survive greylisting, and over-quota responses set `mailboxFull`.
|
|
75
|
+
*
|
|
76
|
+
* Note: many networks block outbound port 25 and many servers refuse or tarpit
|
|
77
|
+
* verification probes, so treat a `deferred`/errored result as inconclusive
|
|
78
|
+
* rather than undeliverable.
|
|
79
|
+
* @param email The email address to verify.
|
|
80
|
+
* @param options Verification options.
|
|
81
|
+
* @returns The detailed SMTP verification result.
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* import { verifySmtp } from "@visulima/email-verifier/checks/smtp";
|
|
85
|
+
*
|
|
86
|
+
* const result = await verifySmtp("user@example.com", { catchAllProbes: 2 });
|
|
87
|
+
* if (result.valid && !result.acceptAll) {
|
|
88
|
+
* console.log("Mailbox exists");
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
declare const verifySmtp: (email: string, options?: SmtpVerificationOptions) => Promise<SmtpVerificationResult>;
|
|
93
|
+
export { type SmtpVerificationOptions, type SmtpVerificationResult, verifySmtp as default, verifySmtp };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import{createRequire as x}from"node:module";import{splitAddress as M}from"../packem_shared/extractDomain-CYJf1_0N.js";import{checkMxRecords as j}from"./mx.js";const $=x(import.meta.url),h=typeof globalThis<"u"&&typeof globalThis.process<"u"?globalThis.process:process,R=r=>{if(typeof h<"u"&&h.versions&&h.versions.node){const[e,t]=h.versions.node.split(".").map(Number);if(e>22||e===22&&t>=3||e===20&&t>=16)return h.getBuiltinModule(r)}return $(r)},{Socket:T}=R("node:net"),y=/[\u0000-\u001F\u007F]/,k=/\b([245]\.\d{1,3}\.\d{1,3})\b/,C=/^(\d{3})(?: |$)/,E=/\r?\n/,g="abcdefghijklmnopqrstuvwxyz0123456789",P=(r=20)=>{let e="";for(let t=0;t<r;t+=1)e+=g[Math.floor(Math.random()*g.length)]??"";return e},S=async r=>new Promise(e=>{setTimeout(e,r)});class _{transcript="";buffer="";completed=[];failure;finished=!1;socket;waiter;constructor(e){this.socket=e,e.on("data",o=>{this.transcript+=o.toString(),this.buffer+=o.toString(),this.drain()});const t=o=>{this.finished=!0,this.failure=o??new Error("Connection closed by server"),this.completed.length===0&&this.waiter&&(this.waiter.reject(this.failure),this.waiter=void 0)};e.on("error",t),e.on("close",()=>{t()}),e.on("timeout",()=>{t(new Error("SMTP socket timeout")),e.destroy()})}async command(e){return this.transcript+=`${e}\r
|
|
2
|
+
`,this.socket.write(`${e}\r
|
|
3
|
+
`),this.readReply()}close(){try{this.socket.write(`QUIT\r
|
|
4
|
+
`)}catch{}this.socket.removeAllListeners(),this.socket.destroy()}async readReply(){if(this.completed.length>0)return this.completed.shift();if(this.finished)throw this.failure??new Error("Connection closed");return new Promise((e,t)=>{this.waiter={reject:t,resolve:e}})}drain(){const e=this.buffer.split(E);this.buffer=e.pop()??"";let t=[];for(const o of e){t.push(o);const a=C.exec(o);if(a){const i=t.join(`
|
|
5
|
+
`),c=k.exec(i)?.[1];this.completed.push({code:Number.parseInt(a[1],10),enhanced:c,text:i}),t=[]}}if(t.length>0&&(this.buffer=`${t.join(`
|
|
6
|
+
`)}
|
|
7
|
+
${this.buffer}`),this.waiter&&this.completed.length>0){const{resolve:o}=this.waiter;this.waiter=void 0,o(this.completed.shift())}}}const A=r=>{const{code:e,enhanced:t}=r;return e>=200&&e<300?{code:e,enhancedCode:t,valid:!0}:e===452||e===552||t==="4.2.2"||t==="5.2.2"?{code:e,deferred:e===452,enhancedCode:t,mailboxFull:!0,valid:!1}:e>=400&&e<500?{code:e,deferred:!0,enhancedCode:t,valid:!1}:{code:e,enhancedCode:t,valid:!1}},L=async(r,e,t)=>new Promise((o,a)=>{const i=new T;i.setTimeout(t);const c=f=>{i.removeAllListeners(),i.destroy(),a(f)},l=()=>{c(new Error("SMTP connection timeout"))};i.once("error",c),i.once("timeout",l),i.connect(e,r,()=>{i.removeListener("error",c),i.removeListener("timeout",l),o(new _(i))})}),H=async(r,e,t,o)=>{const{catchAllProbes:a=1,port:i=25,timeout:c=5e3}=o,l=o.heloHost??e,f=o.fromEmail??`verify@${l}`;let n;try{n=await L(t.exchange,i,c);const d=await n.readReply();if(d.code!==220)return{deferred:!0,error:`Unexpected greeting: ${d.text}`,valid:!1};let s=await n.command(`EHLO ${l}`);if(s.code!==250&&(s=await n.command(`HELO ${l}`),s.code!==250))return{deferred:!0,error:`HELO rejected: ${s.text}`,valid:!1};const u=await n.command(`MAIL FROM:<${f}>`);if(u.code!==250)return{deferred:!0,error:`MAIL FROM rejected: ${u.text}`,valid:!1};const b=await n.command(`RCPT TO:<${r}>`),p=A(b);let m;if(p.valid&&a>0){m=!0;for(let v=0;v<a;v+=1){const w=await n.command(`RCPT TO:<${P()}@${e}>`);if(w.code<200||w.code>=300){m=!1;break}}}return{...p,acceptAll:m,smtpResponse:n.transcript}}finally{n?.close()}},O=async(r,e,t,o)=>{let a="SMTP verification did not complete";for(const i of t)try{return await H(r,e,i,o)}catch(c){a=c instanceof Error?c.message:String(c)}return{deferred:!0,error:a,valid:!1}},I=r=>{if(r.heloHost!==void 0&&y.test(r.heloHost))return{error:"Invalid heloHost: control characters are not allowed",valid:!1};if(r.fromEmail!==void 0&&y.test(r.fromEmail))return{error:"Invalid fromEmail: control characters are not allowed",valid:!1}},B=async(r,e={})=>{const t=M(r);if(!t)return{error:"Invalid email format",valid:!1};const{domain:o}=t,a=I(e);if(a)return a;const{retries:i=1,retryDelay:c=5e3,ttl:l=36e5}=e,f=["smtp",t.address,e.fromEmail??"",String(e.port??25),e.heloHost??"",String(e.catchAllProbes??1),(e.mxRecords??[]).map(s=>s.exchange).join(",")].join("|");if(e.smtpCache){const s=await e.smtpCache.get(f);if(s!==void 0)return s}let n=e.mxRecords;if(!n||n.length===0){const s=await j(o,{cache:e.cache,fallbackToAddress:!1});if(!s.valid||!s.records||s.records.length===0)return{error:s.error??"No MX records found",mxRecords:s.records,valid:!1};n=s.records}let d={error:"SMTP verification did not complete",valid:!1};for(let s=0;s<=i&&(d=await O(t.address,o,n,e),!(!d.deferred||s===i));s+=1)await S(c);return d={...d,mxRecords:n},e.smtpCache&&!d.deferred&&await e.smtpCache.set(f,d,l),d};export{B as default,B as verifySmtp};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The result of symbol / Unicode analysis.
|
|
3
|
+
*/
|
|
4
|
+
interface SymbolResult {
|
|
5
|
+
/** True when the address mixes Unicode scripts (a homoglyph-spoofing signal). */
|
|
6
|
+
hasMixedScripts: boolean;
|
|
7
|
+
/** True when the address contains non-ASCII characters anywhere. */
|
|
8
|
+
hasNonAscii: boolean;
|
|
9
|
+
/** True when the local part or domain contains symbol/emoji characters. */
|
|
10
|
+
hasSymbols: boolean;
|
|
11
|
+
/** The distinct Unicode scripts detected (e.g. ["Latin", "Cyrillic"]). */
|
|
12
|
+
scripts: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Detects symbols, emoji, and mixed Unicode scripts in an email address.
|
|
16
|
+
*
|
|
17
|
+
* Mixed-script local parts (e.g. Latin + Cyrillic) are a classic homoglyph /
|
|
18
|
+
* spoofing signal; symbol or emoji characters indicate a non-standard address.
|
|
19
|
+
* @param email The email address to analyze.
|
|
20
|
+
* @returns The symbol analysis result.
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { analyzeSymbols } from "@visulima/email-verifier/checks/symbol";
|
|
24
|
+
*
|
|
25
|
+
* analyzeSymbols("раypal@example.com").hasMixedScripts; // true (Cyrillic "ра")
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
declare const analyzeSymbols: (email: string) => SymbolResult;
|
|
29
|
+
export { type SymbolResult, analyzeSymbols, analyzeSymbols as default };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{splitAddress as n}from"../packem_shared/extractDomain-CYJf1_0N.js";const l=new RegExp("\\P{ASCII}","u"),c=new RegExp("\\p{S}","u"),p=[["Latin",new RegExp("\\p{Script=Latin}","u")],["Cyrillic",new RegExp("\\p{Script=Cyrillic}","u")],["Greek",new RegExp("\\p{Script=Greek}","u")],["Han",new RegExp("\\p{Script=Han}","u")],["Hiragana",new RegExp("\\p{Script=Hiragana}","u")],["Katakana",new RegExp("\\p{Script=Katakana}","u")],["Hangul",new RegExp("\\p{Script=Hangul}","u")],["Arabic",new RegExp("\\p{Script=Arabic}","u")],["Hebrew",new RegExp("\\p{Script=Hebrew}","u")]],g=i=>{const e=n(i);if(!e)return{hasMixedScripts:!1,hasNonAscii:!1,hasSymbols:!1,scripts:[]};const t=`${e.localPart}${e.domain}`,r=p.filter(([,a])=>a.test(t)).map(([a])=>a);return{hasMixedScripts:[e.localPart,...e.domain.split(".").filter(Boolean)].some(a=>p.filter(([,s])=>s.test(a)).length>1),hasNonAscii:l.test(e.address),hasSymbols:c.test(t),scripts:r}};export{g as analyzeSymbols,g as default};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates an email address format according to basic RFC standards.
|
|
3
|
+
*
|
|
4
|
+
* This is intentionally pragmatic rather than a full RFC 5322 parser: it rejects
|
|
5
|
+
* the address shapes that never deliver (missing parts, consecutive dots, leading
|
|
6
|
+
* or trailing dots, hyphen-bounded domain labels) while accepting the long tail
|
|
7
|
+
* of legitimate real-world addresses.
|
|
8
|
+
* @param email The email address string to validate.
|
|
9
|
+
* @returns True if the email address is syntactically valid, false otherwise.
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { validateSyntax } from "@visulima/email-verifier/checks/syntax";
|
|
13
|
+
*
|
|
14
|
+
* validateSyntax("user@example.com"); // true
|
|
15
|
+
* validateSyntax("user@@example.com"); // false
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
declare const validateSyntax: (email: string) => boolean;
|
|
19
|
+
export { validateSyntax as default, validateSyntax };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{splitAddress as n}from"../packem_shared/extractDomain-CYJf1_0N.js";const r=/^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/,l=/^[\p{L}\p{N}-]+$/u,o=s=>{if(!s||typeof s!="string"||!r.test(s)||s.includes(".."))return!1;const a=n(s);if(!a)return!1;const{domain:t,localPart:i}=a;return i.startsWith(".")||i.endsWith(".")||!t.includes(".")||t.startsWith("-")||t.endsWith("-")?!1:t.split(".").every(e=>e.length>0&&!e.startsWith("-")&&!e.endsWith("-")&&l.test(e))};export{o as default,o as validateSyntax};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The result of sub-address (tag) detection.
|
|
3
|
+
*/
|
|
4
|
+
interface TagResult {
|
|
5
|
+
/** The local part with the tag and separator removed (the "real" mailbox). */
|
|
6
|
+
baseLocalPart: string;
|
|
7
|
+
/** True when a sub-address tag was found. */
|
|
8
|
+
hasTag: boolean;
|
|
9
|
+
/** The separator that introduced the tag (`+` or `-`), if any. */
|
|
10
|
+
separator?: string;
|
|
11
|
+
/** The detected tag value (everything after the separator), if any. */
|
|
12
|
+
tag?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Detects plus-addressing / sub-addressing tags in an email address.
|
|
16
|
+
*
|
|
17
|
+
* Most providers (Gmail, Outlook, iCloud, Proton, …) delimit the tag with `+`;
|
|
18
|
+
* a few (Fastmail, Yahoo) use `-`. The returned {@link TagResult.baseLocalPart}
|
|
19
|
+
* is the canonical mailbox with the tag stripped.
|
|
20
|
+
* @param email The email address to inspect.
|
|
21
|
+
* @returns The tag detection result.
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { detectTag } from "@visulima/email-verifier/checks/tag";
|
|
25
|
+
*
|
|
26
|
+
* detectTag("user+newsletter@gmail.com");
|
|
27
|
+
* // { hasTag: true, tag: "newsletter", separator: "+", baseLocalPart: "user" }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
declare const detectTag: (email: string) => TagResult;
|
|
31
|
+
export { type TagResult, detectTag as default, detectTag };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{splitAddress as c}from"../packem_shared/extractDomain-CYJf1_0N.js";const l=new Set(["fastmail.com","fastmail.fm","yahoo.co.uk","yahoo.com"]),f=r=>{const e=c(r);if(!e)return{baseLocalPart:"",hasTag:!1};const{domain:o,localPart:a}=e,s=a.indexOf("+");if(s>0)return{baseLocalPart:a.slice(0,s),hasTag:!0,separator:"+",tag:a.slice(s+1)};if(l.has(o)){const t=a.indexOf("-");if(t>0)return{baseLocalPart:a.slice(0,t),hasTag:!0,separator:"-",tag:a.slice(t+1)}}return{baseLocalPart:a,hasTag:!1}};export{f as default,f as detectTag};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A name parsed from an email's local part.
|
|
3
|
+
*/
|
|
4
|
+
interface NameResult {
|
|
5
|
+
/** Confidence in the parse: `high` (clear separator), `medium` (camelCase), `low` (heuristic), `none`. */
|
|
6
|
+
confidence: "high" | "low" | "medium" | "none";
|
|
7
|
+
/** The detected first name, capitalized, if any. */
|
|
8
|
+
firstName?: string;
|
|
9
|
+
/** The detected first + last name joined, if any. */
|
|
10
|
+
fullName?: string;
|
|
11
|
+
/** The detected last name, capitalized, if any. */
|
|
12
|
+
lastName?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Heuristically parses a person's name from the local part of an email address.
|
|
16
|
+
*
|
|
17
|
+
* Handles separator-delimited (`john.doe`), camelCase (`johnDoe`), and
|
|
18
|
+
* single-token local parts, stripping trailing digits. This is best-effort name
|
|
19
|
+
* parsing only — no gender detection — and confidence is reported so callers
|
|
20
|
+
* can decide how much to trust it.
|
|
21
|
+
* @param email The email address to parse.
|
|
22
|
+
* @returns The parsed name and a confidence level.
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { parseName } from "@visulima/email-verifier/enrich/name";
|
|
26
|
+
*
|
|
27
|
+
* parseName("john.doe@example.com");
|
|
28
|
+
* // { firstName: "John", lastName: "Doe", fullName: "John Doe", confidence: "high" }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
declare const parseName: (email: string) => NameResult;
|
|
32
|
+
export { type NameResult, parseName as default, parseName };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{splitAddress as p}from"../packem_shared/extractDomain-CYJf1_0N.js";const f=/[.\-_]/,u=/^[a-z]+(?:[A-Z][a-z]+)+$/,d=/\d+$/,N=/\d/,r=e=>e.length===0?e:e.charAt(0).toUpperCase()+e.slice(1).toLowerCase(),h=e=>e.replaceAll(/([a-z])([A-Z])/g,"$1 $2").split(" "),g=e=>{const s=p(e);if(!s)return{confidence:"none"};const i=e.trim().lastIndexOf("@"),t=(i>0?e.trim().slice(0,i):s.localPart).split("+")[0].replace(d,"");if(t.length===0)return{confidence:"none"};let a,o;if(f.test(t))a=t.split(f).filter(Boolean),o="high";else if(u.test(t))a=h(t),o="medium";else return N.test(t)?{confidence:"none"}:{confidence:"low",firstName:r(t),fullName:r(t)};const n=a.map(m=>m.replace(d,"")).filter(Boolean);if(n.length===0)return{confidence:"none"};const l=r(n[0]);if(n.length===1)return{confidence:"low",firstName:l,fullName:l};const c=r(n.at(-1));return{confidence:o,firstName:l,fullName:`${l} ${c}`,lastName:c}};export{g as default,g as parseName};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { MxProviderInfo } from '@visulima/email-provider-mx';
|
|
2
|
+
export { type MxProviderInfo, classifyMx, classifyMxRecords, isSecureEmailGateway } from '@visulima/email-provider-mx';
|
|
3
|
+
import { c as MxRecord, b as MxCheckResult, C as Cache } from "../packem_shared/mx.d-NAesS5sg.js";
|
|
4
|
+
/**
|
|
5
|
+
* The enriched provider details for an email address.
|
|
6
|
+
*/
|
|
7
|
+
interface ProviderDetails {
|
|
8
|
+
/** True when the resolving MX is a Secure Email Gateway (Proofpoint, Mimecast, …). */
|
|
9
|
+
isSecureEmailGateway: boolean;
|
|
10
|
+
/** The MX records the classification was derived from. */
|
|
11
|
+
mxRecords: MxRecord[];
|
|
12
|
+
/** The classified provider, or `undefined` when no MX matched a known provider. */
|
|
13
|
+
provider?: MxProviderInfo;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Options for provider enrichment.
|
|
17
|
+
*/
|
|
18
|
+
interface ProviderEnrichOptions {
|
|
19
|
+
cache?: Cache<MxCheckResult>;
|
|
20
|
+
ttl?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolves an email address's mailbox/SEG provider from its MX records.
|
|
24
|
+
*
|
|
25
|
+
* Looks up the domain's MX records, then classifies the highest-priority host
|
|
26
|
+
* via `@visulima/email-provider-mx` — emailable's "SMTP Provider Details" plus
|
|
27
|
+
* Secure-Email-Gateway detection.
|
|
28
|
+
* @param email The email address to enrich.
|
|
29
|
+
* @param options Caching options for the MX lookup.
|
|
30
|
+
* @returns The provider details, including the MX records used.
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { enrichProvider } from "@visulima/email-verifier/enrich/provider";
|
|
34
|
+
*
|
|
35
|
+
* const details = await enrichProvider("user@gmail.com");
|
|
36
|
+
* console.log(details.provider?.display); // "Google"
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
declare const enrichProvider: (email: string, options?: ProviderEnrichOptions) => Promise<ProviderDetails>;
|
|
40
|
+
export { type ProviderDetails, type ProviderEnrichOptions, enrichProvider as default, enrichProvider };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{classifyMxRecords as t}from"@visulima/email-provider-mx";import{classifyMx as y,classifyMxRecords as u,isSecureEmailGateway as p}from"@visulima/email-provider-mx";import{checkMxRecords as o}from"../checks/mx.js";import{extractDomain as i}from"../packem_shared/extractDomain-CYJf1_0N.js";const d=async(c,a={})=>{const r=i(c);if(!r)return{isSecureEmailGateway:!1,mxRecords:[]};const e=(await o(r,{cache:a.cache,fallbackToAddress:!1,ttl:a.ttl})).records??[];if(e.length===0)return{isSecureEmailGateway:!1,mxRecords:e};const s=t(e);return{isSecureEmailGateway:s?.type==="seg",mxRecords:e,provider:s}};export{y as classifyMx,u as classifyMxRecords,d as default,d as enrichProvider,p as isSecureEmailGateway};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A suggested correction for a likely-misspelled email address.
|
|
3
|
+
*/
|
|
4
|
+
interface TypoSuggestion {
|
|
5
|
+
/** The suggested corrected domain. */
|
|
6
|
+
domain: string;
|
|
7
|
+
/** The full suggested address (original local part + corrected domain). */
|
|
8
|
+
full: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Options for typo suggestion.
|
|
12
|
+
*/
|
|
13
|
+
interface TypoOptions {
|
|
14
|
+
/** Popular full domains to match against (e.g. `gmail.com`). */
|
|
15
|
+
domains?: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Distance threshold for the full-domain match.
|
|
18
|
+
* @default 2
|
|
19
|
+
*/
|
|
20
|
+
domainThreshold?: number;
|
|
21
|
+
/** Popular second-level domains to match against (e.g. `gmail`). */
|
|
22
|
+
secondLevelDomains?: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Distance threshold for the second-level match.
|
|
25
|
+
* @default 2
|
|
26
|
+
*/
|
|
27
|
+
secondLevelThreshold?: number;
|
|
28
|
+
/** Popular top-level domains to match against (e.g. `com`). */
|
|
29
|
+
topLevelDomains?: string[];
|
|
30
|
+
/**
|
|
31
|
+
* Distance threshold for the top-level match.
|
|
32
|
+
* @default 2
|
|
33
|
+
*/
|
|
34
|
+
topLevelThreshold?: number;
|
|
35
|
+
}
|
|
36
|
+
declare const sift3Distance: (s1: string, s2: string) => number;
|
|
37
|
+
/**
|
|
38
|
+
* Suggests a corrected domain for a likely-misspelled one, using the mailcheck
|
|
39
|
+
* algorithm (full-domain match, then second-level + top-level matching).
|
|
40
|
+
* @param domain The domain to check.
|
|
41
|
+
* @param options Override domain lists and thresholds.
|
|
42
|
+
* @returns The suggested domain, or `undefined` when the domain looks fine.
|
|
43
|
+
*/
|
|
44
|
+
declare const suggestDomain: (domain: string, options?: TypoOptions) => string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Suggests a corrected email address when the domain looks misspelled
|
|
47
|
+
* (e.g. `user@gmial.com` → `user@gmail.com`).
|
|
48
|
+
* @param email The email address to check.
|
|
49
|
+
* @param options Override domain lists and thresholds.
|
|
50
|
+
* @returns A {@link TypoSuggestion}, or `undefined` when nothing looks wrong.
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { suggestEmailTypo } from "@visulima/email-verifier/enrich/typo";
|
|
54
|
+
*
|
|
55
|
+
* suggestEmailTypo("user@gmial.com")?.full; // "user@gmail.com"
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
declare const suggestEmailTypo: (email: string, options?: TypoOptions) => TypoSuggestion | undefined;
|
|
59
|
+
export { type TypoOptions, type TypoSuggestion, suggestEmailTypo as default, sift3Distance, suggestDomain, suggestEmailTypo };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{splitAddress as d}from"../packem_shared/extractDomain-CYJf1_0N.js";const g=["aol.com","fastmail.com","gmail.com","googlemail.com","hotmail.com","hotmail.co.uk","icloud.com","live.com","mail.com","me.com","msn.com","outlook.com","proton.me","protonmail.com","yahoo.com","yahoo.co.uk","ymail.com","zoho.com"],v=["aol","fastmail","gmail","googlemail","hotmail","icloud","live","mail","me","msn","outlook","proton","protonmail","yahoo","ymail","zoho"],f=["co.uk","com","de","edu","fr","gov","info","io","me","net","org"],p=5,L=(n,o,e)=>{for(let l=0;l<p;l+=1){if(e+l<n.length&&n[e+l]===o[e])return{offset1:l,offset2:0};if(e+l<o.length&&n[e]===o[e+l])return{offset1:0,offset2:l}}return{offset1:0,offset2:0}},y=(n,o)=>{if(n.length===0)return o.length;if(o.length===0)return n.length;let e=0,l=0,t=0,i=0;for(;e+l<n.length&&e+t<o.length;)n[e+l]===o[e+t]?i+=1:{offset1:l,offset2:t}=L(n,o,e),e+=1;return(n.length+o.length)/2-i},c=(n,o,e)=>{let l=Number.POSITIVE_INFINITY,t;for(const i of o){if(n===i)return n;const s=y(n,i);s<l&&(l=s,t=i)}return l<=e?t:void 0},T=n=>{const o=n.split(".");return o.length>=3&&f.includes(o.slice(-2).join("."))?{secondLevel:o.slice(0,-2).join("."),topLevel:o.slice(-2).join(".")}:{secondLevel:o.slice(0,-1).join("."),topLevel:o.at(-1)??""}},k=(n,o={})=>{const e=n.toLowerCase().trim(),l=o.domains??g,t=o.secondLevelDomains??v,i=o.topLevelDomains??f;if(l.includes(e))return;const s=c(e,l,o.domainThreshold??2);if(s&&s!==e)return s;const{secondLevel:r,topLevel:m}=T(e);if(!r||!m)return;const u=c(r,t,o.secondLevelThreshold??2)??r,h=c(m,i,o.topLevelThreshold??2)??m,a=`${u}.${h}`;return a===e?void 0:a},I=(n,o={})=>{const e=d(n);if(!e)return;const l=k(e.domain,o);if(l)return{domain:l,full:`${e.localPart}@${l}`}};export{I as default,y as sift3Distance,k as suggestDomain,I as suggestEmailTypo};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export { type CharacterResult, analyzeCharacters } from "./checks/character.js";
|
|
2
|
+
export { type DisposableEmailOptions, areDisposableEmails, isDisposableDomain, isDisposableEmail } from '@visulima/disposable-email-domains';
|
|
3
|
+
export { type FreeEmailOptions, areFreeEmails, isFreeDomain, isFreeEmail } from '@visulima/free-email-domains';
|
|
4
|
+
export { type C as Cache, I as InMemoryCache, type a as InMemoryCacheOptions, type M as MxCheckOptions, type b as MxCheckResult, type c as MxRecord, type d as MxResolution, e as checkMxRecords } from "./packem_shared/mx.d-NAesS5sg.js";
|
|
5
|
+
export { NO_REPLY_PREFIXES, ROLE_ACCOUNT_PREFIXES, isNoReply, default as isRoleAccount } from "./checks/role.js";
|
|
6
|
+
export { type SmtpVerificationOptions, type SmtpVerificationResult, default as verifySmtp } from "./checks/smtp.js";
|
|
7
|
+
export { type SymbolResult, analyzeSymbols } from "./checks/symbol.js";
|
|
8
|
+
export { default as validateSyntax } from "./checks/syntax.js";
|
|
9
|
+
export { type TagResult, default as detectTag } from "./checks/tag.js";
|
|
10
|
+
export { type NameResult, default as parseName } from "./enrich/name.js";
|
|
11
|
+
export { type ProviderDetails, type ProviderEnrichOptions, default as enrichProvider } from "./enrich/provider.js";
|
|
12
|
+
export { type TypoOptions, type TypoSuggestion, sift3Distance, suggestDomain, default as suggestEmailTypo } from "./enrich/typo.js";
|
|
13
|
+
export { D as DEFAULT_WEIGHTS, type a as DomainReport, type E as EmailVerificationReport, type S as ScoreInput, type b as ScoreResult, type c as ScoreWeights, type V as VerificationState, s as scoreReport } from "./packem_shared/score.d-BTcZnl2L.js";
|
|
14
|
+
export { type VerifyEmailOptions, default as verifyEmail } from "./verify-email.js";
|
|
15
|
+
export { type MxProviderInfo, classifyMx, classifyMxRecords, isSecureEmailGateway } from '@visulima/email-provider-mx';
|
|
16
|
+
/**
|
|
17
|
+
* The split parts of an email address.
|
|
18
|
+
*/
|
|
19
|
+
interface AddressParts {
|
|
20
|
+
/** The full normalized (lowercased, trimmed) address. */
|
|
21
|
+
address: string;
|
|
22
|
+
/** The domain part, lowercased and with any trailing dot stripped. */
|
|
23
|
+
domain: string;
|
|
24
|
+
/** The local part (before the `@`), lowercased. */
|
|
25
|
+
localPart: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Splits an email address into its normalized local and domain parts.
|
|
29
|
+
*
|
|
30
|
+
* Uses the last `@` as the separator so quoted local parts containing `@` are
|
|
31
|
+
* handled the same way the SMTP/MX probes treat them. Inputs containing
|
|
32
|
+
* whitespace (after trimming) are rejected so a payload that embeds a CRLF
|
|
33
|
+
* followed by a second SMTP command cannot be smuggled into the SMTP dialogue
|
|
34
|
+
* by callers of the standalone probes.
|
|
35
|
+
* @param email The email address to split.
|
|
36
|
+
* @returns The normalized parts, or `undefined` if the address is structurally invalid.
|
|
37
|
+
*/
|
|
38
|
+
declare const splitAddress: (email: string) => AddressParts | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Extracts and normalizes just the domain from an email address.
|
|
41
|
+
* @param email The email address to extract the domain from.
|
|
42
|
+
* @returns The normalized domain, or `undefined` if invalid.
|
|
43
|
+
*/
|
|
44
|
+
declare const extractDomain: (email: string) => string | undefined;
|
|
45
|
+
export { type AddressParts, extractDomain, splitAddress };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{analyzeCharacters as a}from"./checks/character.js";import{checkMxRecords as s}from"./checks/mx.js";import{NO_REPLY_PREFIXES as m,ROLE_ACCOUNT_PREFIXES as f,isNoReply as i,default as l}from"./checks/role.js";import{default as x}from"./checks/smtp.js";import{analyzeSymbols as d}from"./checks/symbol.js";import{default as u}from"./checks/syntax.js";import{default as n}from"./checks/tag.js";import{default as R}from"./enrich/name.js";import{default as F}from"./enrich/provider.js";import{sift3Distance as T,suggestDomain as _,default as b}from"./enrich/typo.js";import{extractDomain as v,splitAddress as A}from"./packem_shared/extractDomain-CYJf1_0N.js";import{InMemoryCache as I}from"./packem_shared/InMemoryCache-DyBUMeKq.js";import{DEFAULT_WEIGHTS as N,default as P}from"./score.js";import{default as O}from"./verify-email.js";import{areDisposableEmails as G,isDisposableDomain as U,isDisposableEmail as X}from"@visulima/disposable-email-domains";import{areFreeEmails as w,isFreeDomain as H,isFreeEmail as W}from"@visulima/free-email-domains";import{classifyMx as j,classifyMxRecords as q,isSecureEmailGateway as B}from"@visulima/email-provider-mx";export{N as DEFAULT_WEIGHTS,I as InMemoryCache,m as NO_REPLY_PREFIXES,f as ROLE_ACCOUNT_PREFIXES,a as analyzeCharacters,d as analyzeSymbols,G as areDisposableEmails,w as areFreeEmails,s as checkMxRecords,j as classifyMx,q as classifyMxRecords,n as detectTag,F as enrichProvider,v as extractDomain,U as isDisposableDomain,X as isDisposableEmail,H as isFreeDomain,W as isFreeEmail,i as isNoReply,l as isRoleAccount,B as isSecureEmailGateway,R as parseName,P as scoreReport,T as sift3Distance,A as splitAddress,_ as suggestDomain,b as suggestEmailTypo,u as validateSyntax,O as verifyEmail,x as verifySmtp};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LRUCache as a}from"lru-cache";class l{cache;constructor(c={}){const{max:e=500,ttl:t=36e5}=c;this.cache=new a({max:e,ttl:t})}clear=async()=>{this.cache.clear()};delete=async c=>{this.cache.delete(c)};get=async c=>this.cache.get(c);set=async(c,e,t)=>{this.cache.set(c,e,{ttl:t})}}export{l as InMemoryCache};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const a=/\.$/,i=/\s/,o=s=>{if(!s||typeof s!="string")return;const e=s.trim().toLowerCase();if(i.test(e))return;const t=e.lastIndexOf("@");if(t<=0||t===e.length-1)return;const r=e.slice(0,t),n=e.slice(t+1).replace(a,"");if(!(!r||!n))return{address:e,domain:n,localPart:r}},c=s=>o(s)?.domain;export{c as extractDomain,o as splitAddress};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic cache interface for caching any type of data.
|
|
3
|
+
* Implementations can use in-memory cache, LRU cache, Redis, etc.
|
|
4
|
+
*/
|
|
5
|
+
interface Cache<T = unknown> {
|
|
6
|
+
/**
|
|
7
|
+
* Clears all cached entries.
|
|
8
|
+
*/
|
|
9
|
+
clear: () => Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Deletes a cached entry.
|
|
12
|
+
* @param key The cache key.
|
|
13
|
+
*/
|
|
14
|
+
delete: (key: string) => Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Gets a cached value.
|
|
17
|
+
* @param key The cache key.
|
|
18
|
+
* @returns The cached value or undefined if not found or expired.
|
|
19
|
+
*/
|
|
20
|
+
get: (key: string) => Promise<T | undefined>;
|
|
21
|
+
/**
|
|
22
|
+
* Sets a cached value.
|
|
23
|
+
* @param key The cache key.
|
|
24
|
+
* @param value The value to cache.
|
|
25
|
+
* @param ttl Time-to-live in milliseconds.
|
|
26
|
+
*/
|
|
27
|
+
set: (key: string, value: T, ttl: number) => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Options for creating an in-memory cache.
|
|
31
|
+
*/
|
|
32
|
+
interface InMemoryCacheOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Maximum number of entries in the cache.
|
|
35
|
+
* @default 500
|
|
36
|
+
*/
|
|
37
|
+
max?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Default TTL in milliseconds for entries.
|
|
40
|
+
* @default 3600000 (1 hour)
|
|
41
|
+
*/
|
|
42
|
+
ttl?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Default in-memory cache implementation using LRU cache.
|
|
46
|
+
*
|
|
47
|
+
* Shared by the MX and SMTP probes so repeated lookups against the same domain
|
|
48
|
+
* (e.g. when verifying a list) avoid redundant DNS/socket work.
|
|
49
|
+
*/
|
|
50
|
+
declare class InMemoryCache<T extends object = Record<string, unknown>> implements Cache<T> {
|
|
51
|
+
private readonly cache;
|
|
52
|
+
constructor(options?: InMemoryCacheOptions);
|
|
53
|
+
clear: () => Promise<void>;
|
|
54
|
+
delete: (key: string) => Promise<void>;
|
|
55
|
+
get: (key: string) => Promise<T | undefined>;
|
|
56
|
+
set: (key: string, value: T, ttl: number) => Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* MX record information.
|
|
60
|
+
*/
|
|
61
|
+
interface MxRecord {
|
|
62
|
+
exchange: string;
|
|
63
|
+
priority: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* How the domain's mail-acceptance was established.
|
|
67
|
+
*
|
|
68
|
+
* - `mx`: the domain published MX records.
|
|
69
|
+
* - `address`: the domain has no MX but resolves to an A/AAAA record, so by RFC 5321 §5.1 the address itself is treated as an implicit MX.
|
|
70
|
+
* - `none`: neither MX nor address records exist.
|
|
71
|
+
*/
|
|
72
|
+
type MxResolution = "address" | "mx" | "none";
|
|
73
|
+
/**
|
|
74
|
+
* Result of an MX/domain check.
|
|
75
|
+
*/
|
|
76
|
+
interface MxCheckResult {
|
|
77
|
+
/** True when the domain itself resolves but publishes no MX records. */
|
|
78
|
+
domainResolves: boolean;
|
|
79
|
+
error?: string;
|
|
80
|
+
records?: MxRecord[];
|
|
81
|
+
resolvedVia: MxResolution;
|
|
82
|
+
/** True when the domain can accept mail (has MX or A/AAAA records). */
|
|
83
|
+
valid: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Options for MX record checking.
|
|
87
|
+
*/
|
|
88
|
+
interface MxCheckOptions {
|
|
89
|
+
cache?: Cache<MxCheckResult>;
|
|
90
|
+
/**
|
|
91
|
+
* When true (the default), a domain with no MX records but a resolvable
|
|
92
|
+
* A/AAAA record is still considered able to accept mail (implicit MX).
|
|
93
|
+
*/
|
|
94
|
+
fallbackToAddress?: boolean;
|
|
95
|
+
ttl?: number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Checks MX records for a domain, with an optional A/AAAA fallback.
|
|
99
|
+
*
|
|
100
|
+
* Distinguishes three states emailable separates as "MX Record Detection" and
|
|
101
|
+
* "Domain Validation": records present (`mx`), no MX but the domain resolves
|
|
102
|
+
* (`address`, implicit MX), and nothing resolves (`none`).
|
|
103
|
+
* @param domain The domain to check.
|
|
104
|
+
* @param options Options including caching and the address fallback toggle.
|
|
105
|
+
* @returns The MX/domain check result.
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* import { checkMxRecords } from "@visulima/email-verifier/checks/mx";
|
|
109
|
+
*
|
|
110
|
+
* const result = await checkMxRecords("example.com");
|
|
111
|
+
* if (result.valid) {
|
|
112
|
+
* console.log(result.resolvedVia, result.records);
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
declare const checkMxRecords: (domain: string, options?: MxCheckOptions) => Promise<MxCheckResult>;
|
|
117
|
+
export { Cache as C, InMemoryCache as I, MxCheckOptions as M, InMemoryCacheOptions as a, MxCheckResult as b, MxRecord as c, MxResolution as d, checkMxRecords as e };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { CharacterResult } from "../checks/character.js";
|
|
2
|
+
import { c as MxRecord, d as MxResolution } from "./mx.d-NAesS5sg.js";
|
|
3
|
+
import { SmtpVerificationResult } from "../checks/smtp.js";
|
|
4
|
+
import { SymbolResult } from "../checks/symbol.js";
|
|
5
|
+
import { TagResult } from "../checks/tag.js";
|
|
6
|
+
import { NameResult } from "../enrich/name.js";
|
|
7
|
+
import { MxProviderInfo } from '@visulima/email-provider-mx';
|
|
8
|
+
/**
|
|
9
|
+
* The overall deliverability verdict for an address, mirroring emailable's states.
|
|
10
|
+
*
|
|
11
|
+
* - `deliverable`: the mailbox almost certainly exists and accepts mail.
|
|
12
|
+
* - `risky`: deliverable but lower-quality (catch-all, role, disposable, full mailbox).
|
|
13
|
+
* - `undeliverable`: syntax/domain/mailbox failure — do not send.
|
|
14
|
+
* - `unknown`: could not be determined (SMTP blocked, greylisted, or not probed).
|
|
15
|
+
*/
|
|
16
|
+
type VerificationState = "deliverable" | "risky" | "undeliverable" | "unknown";
|
|
17
|
+
/**
|
|
18
|
+
* The resolved domain portion of a verification report.
|
|
19
|
+
*/
|
|
20
|
+
interface DomainReport {
|
|
21
|
+
/** The MX records found (empty when resolved via A/AAAA or not at all). */
|
|
22
|
+
records: MxRecord[];
|
|
23
|
+
/** How mail-acceptance was established (`unchecked` when DNS was skipped). */
|
|
24
|
+
resolvedVia: MxResolution | "unchecked";
|
|
25
|
+
/** True when the domain can accept mail. */
|
|
26
|
+
valid: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A complete email verification + enrichment report.
|
|
30
|
+
*
|
|
31
|
+
* Aggregates every check and enrichment into a single object whose shape reads
|
|
32
|
+
* as a drop-in mental model for emailable's API response.
|
|
33
|
+
*/
|
|
34
|
+
interface EmailVerificationReport {
|
|
35
|
+
/** True when the SMTP server accepts any recipient (catch-all / accept-all). */
|
|
36
|
+
acceptAll: boolean;
|
|
37
|
+
/** Local-part character analysis. */
|
|
38
|
+
character: CharacterResult;
|
|
39
|
+
/** True when the SMTP result was inconclusive (greylisting / temporary failure). */
|
|
40
|
+
deferred: boolean;
|
|
41
|
+
/** Typo-corrected address suggestion, if the domain looked misspelled. */
|
|
42
|
+
didYouMean?: string;
|
|
43
|
+
/** True when the address uses a known disposable / throwaway domain. */
|
|
44
|
+
disposable: boolean;
|
|
45
|
+
/** The resolved domain details. */
|
|
46
|
+
domain: DomainReport;
|
|
47
|
+
/** The normalized (lowercased, trimmed) email address. */
|
|
48
|
+
email: string;
|
|
49
|
+
/** True when the address uses a known free mailbox provider. */
|
|
50
|
+
free: boolean;
|
|
51
|
+
/** True when the mailbox exists but is over quota. */
|
|
52
|
+
mailboxFull: boolean;
|
|
53
|
+
/** Name parsed from the local part (no gender detection). */
|
|
54
|
+
name: NameResult;
|
|
55
|
+
/** True when the address is a no-reply / do-not-reply mailbox. */
|
|
56
|
+
noReply: boolean;
|
|
57
|
+
/** The classified mailbox/SEG provider, if recognized. */
|
|
58
|
+
provider?: MxProviderInfo;
|
|
59
|
+
/** Human-readable explanation of the `state`. */
|
|
60
|
+
reason: string;
|
|
61
|
+
/** True when the address is a role-based (shared) mailbox. */
|
|
62
|
+
role: boolean;
|
|
63
|
+
/** Composite 0–100 quality score. */
|
|
64
|
+
score: number;
|
|
65
|
+
/** True when the resolving MX is a Secure Email Gateway. */
|
|
66
|
+
secureEmailGateway: boolean;
|
|
67
|
+
/** The raw SMTP probe result, when SMTP verification ran. */
|
|
68
|
+
smtp?: SmtpVerificationResult;
|
|
69
|
+
/** The overall deliverability verdict. */
|
|
70
|
+
state: VerificationState;
|
|
71
|
+
/** Symbol / Unicode-script analysis. */
|
|
72
|
+
symbol: SymbolResult;
|
|
73
|
+
/** True when the address passed the syntax check. */
|
|
74
|
+
syntaxValid: boolean;
|
|
75
|
+
/** Sub-address (tag) detection result. */
|
|
76
|
+
tag: TagResult;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The fields of a report the scorer consumes (everything except the derived
|
|
80
|
+
* `score`, `state`, and `reason`).
|
|
81
|
+
*/
|
|
82
|
+
type ScoreInput = Omit<EmailVerificationReport, "reason" | "score" | "state">;
|
|
83
|
+
/**
|
|
84
|
+
* Overridable penalty/bonus weights for the quality score.
|
|
85
|
+
*
|
|
86
|
+
* Each value is points applied to a 100-point baseline. Penalties are positive
|
|
87
|
+
* numbers that get subtracted; bonuses are added.
|
|
88
|
+
*/
|
|
89
|
+
interface ScoreWeights {
|
|
90
|
+
acceptAll: number;
|
|
91
|
+
character: number;
|
|
92
|
+
deferred: number;
|
|
93
|
+
didYouMean: number;
|
|
94
|
+
disposable: number;
|
|
95
|
+
free: number;
|
|
96
|
+
knownProvider: number;
|
|
97
|
+
mailboxFull: number;
|
|
98
|
+
mixedScripts: number;
|
|
99
|
+
noReply: number;
|
|
100
|
+
role: number;
|
|
101
|
+
smtpUnverified: number;
|
|
102
|
+
symbol: number;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The default scoring rubric. Transparent and additive so callers can reason
|
|
106
|
+
* about — and override — every weight.
|
|
107
|
+
*/
|
|
108
|
+
declare const DEFAULT_WEIGHTS: ScoreWeights;
|
|
109
|
+
/**
|
|
110
|
+
* The outcome of scoring: the numeric score plus the derived state and reason.
|
|
111
|
+
*/
|
|
112
|
+
interface ScoreResult {
|
|
113
|
+
reason: string;
|
|
114
|
+
score: number;
|
|
115
|
+
state: VerificationState;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Computes a 0–100 quality score and the deliverability state for a report.
|
|
119
|
+
*
|
|
120
|
+
* The state is derived from hard signals (syntax, domain, SMTP verdict); the
|
|
121
|
+
* score layers transparent penalties/bonuses on top so two `risky` addresses can
|
|
122
|
+
* still be ranked against each other.
|
|
123
|
+
* @param input The verification signals (a report without its derived fields).
|
|
124
|
+
* @param weights Optional weight overrides.
|
|
125
|
+
* @returns The score, state, and reason.
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* import { scoreReport } from "@visulima/email-verifier/score";
|
|
129
|
+
*
|
|
130
|
+
* const { score, state } = scoreReport(report);
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
declare const scoreReport: (input: ScoreInput, weights?: Partial<ScoreWeights>) => ScoreResult;
|
|
134
|
+
export { DEFAULT_WEIGHTS as D, EmailVerificationReport as E, ScoreInput as S, VerificationState as V, DomainReport as a, ScoreResult as b, ScoreWeights as c, scoreReport as s };
|
package/dist/score.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { D as DEFAULT_WEIGHTS, type S as ScoreInput, type b as ScoreResult, type c as ScoreWeights, s as default, s as scoreReport } from "./packem_shared/score.d-BTcZnl2L.js";
|
|
2
|
+
import "./checks/character.js";
|
|
3
|
+
import "./packem_shared/mx.d-NAesS5sg.js";
|
|
4
|
+
import "./checks/smtp.js";
|
|
5
|
+
import "./checks/symbol.js";
|
|
6
|
+
import "./checks/tag.js";
|
|
7
|
+
import "./enrich/name.js";
|
|
8
|
+
import '@visulima/email-provider-mx';
|
package/dist/score.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const t={acceptAll:25,character:15,deferred:15,didYouMean:20,disposable:60,free:5,knownProvider:5,mailboxFull:40,mixedScripts:30,noReply:25,role:25,smtpUnverified:10,symbol:10},i=e=>Math.max(0,Math.min(100,Math.round(e))),n=e=>e.smtp!==void 0&&!e.smtp.valid&&!e.smtp.deferred&&!e.mailboxFull&&(e.smtp.code??0)>=500,d=e=>e.disposable?{reason:"disposable_mailbox",state:"risky"}:e.mailboxFull?{reason:"mailbox_full",state:"risky"}:e.acceptAll?{reason:"accept_all",state:"risky"}:e.noReply?{reason:"no_reply",state:"risky"}:e.role?{reason:"role_account",state:"risky"}:{reason:"accepted_email",state:"deliverable"},m=e=>n(e)?{reason:"rejected_email",state:"undeliverable"}:e.smtp?.valid?d(e):e.mailboxFull?{reason:"mailbox_full",state:"risky"}:e.smtp?.deferred?{reason:"greylisted",state:"unknown"}:{reason:"unknown",state:"unknown"},c=e=>e.syntaxValid?e.domain.resolvedVia!=="unchecked"&&!e.domain.valid?{reason:e.domain.resolvedVia==="none"?"no_mx_records":"invalid_domain",state:"undeliverable"}:e.smtp?m(e):e.disposable?{reason:"disposable_mailbox",state:"risky"}:{reason:"smtp_not_checked",state:"unknown"}:{reason:"invalid_syntax",state:"undeliverable"},p=(e,o={})=>{const s={...t,...o},{reason:l,state:r}=c(e);if(r==="undeliverable")return{reason:l,score:0,state:r};let a=100;return e.smtp?.valid!==!0&&(a-=s.smtpUnverified),e.deferred&&(a-=s.deferred),e.disposable&&(a-=s.disposable),e.acceptAll&&(a-=s.acceptAll),e.mailboxFull&&(a-=s.mailboxFull),e.role&&(a-=s.role),e.noReply&&(a-=s.noReply),e.free&&(a-=s.free),e.character.irregular&&(a-=s.character),e.symbol.hasMixedScripts?a-=s.mixedScripts:e.symbol.hasSymbols&&(a-=s.symbol),e.didYouMean&&(a-=s.didYouMean),e.provider&&(a+=s.knownProvider),{reason:l,score:i(a),state:r}};export{t as DEFAULT_WEIGHTS,p as default,p as scoreReport};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { DisposableEmailOptions } from '@visulima/disposable-email-domains';
|
|
2
|
+
import { FreeEmailOptions } from '@visulima/free-email-domains';
|
|
3
|
+
import { b as MxCheckResult, C as Cache } from "./packem_shared/mx.d-NAesS5sg.js";
|
|
4
|
+
import { SmtpVerificationOptions } from "./checks/smtp.js";
|
|
5
|
+
import { TypoOptions } from "./enrich/typo.js";
|
|
6
|
+
import { c as ScoreWeights, E as EmailVerificationReport } from "./packem_shared/score.d-BTcZnl2L.js";
|
|
7
|
+
export type { V as VerificationState } from "./packem_shared/score.d-BTcZnl2L.js";
|
|
8
|
+
import "./checks/character.js";
|
|
9
|
+
import "./checks/symbol.js";
|
|
10
|
+
import "./checks/tag.js";
|
|
11
|
+
import "./enrich/name.js";
|
|
12
|
+
import '@visulima/email-provider-mx';
|
|
13
|
+
/**
|
|
14
|
+
* Options for `verifyEmail`.
|
|
15
|
+
*/
|
|
16
|
+
interface VerifyEmailOptions {
|
|
17
|
+
/** Shared cache for MX/SMTP lookups (dedupes work when verifying lists). */
|
|
18
|
+
cache?: Cache<MxCheckResult>;
|
|
19
|
+
/**
|
|
20
|
+
* Run live SMTP verification (catch-all, mailbox-full, greylist).
|
|
21
|
+
* @default true
|
|
22
|
+
*/
|
|
23
|
+
checkSmtp?: boolean;
|
|
24
|
+
/** Disposable-list overrides. */
|
|
25
|
+
disposable?: DisposableEmailOptions;
|
|
26
|
+
/** Free-list overrides. */
|
|
27
|
+
free?: FreeEmailOptions;
|
|
28
|
+
/**
|
|
29
|
+
* Skip all network checks (MX, SMTP, provider) and produce a syntax +
|
|
30
|
+
* heuristic-only report. Overrides `checkSmtp`.
|
|
31
|
+
* @default false
|
|
32
|
+
*/
|
|
33
|
+
offline?: boolean;
|
|
34
|
+
/** Additional role-account prefixes to recognize. */
|
|
35
|
+
roleCustomPrefixes?: Iterable<string>;
|
|
36
|
+
/** SMTP probe options (timeout, catch-all probes, retries, …). */
|
|
37
|
+
smtp?: SmtpVerificationOptions;
|
|
38
|
+
/** Typo-suggestion overrides. */
|
|
39
|
+
typo?: TypoOptions;
|
|
40
|
+
/** Score weight overrides. */
|
|
41
|
+
weights?: Partial<ScoreWeights>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Verifies and enriches a single email address end-to-end.
|
|
45
|
+
*
|
|
46
|
+
* Runs the offline checks (syntax, disposable, free, role, tag, character,
|
|
47
|
+
* symbol, name, typo) immediately, then — unless `offline` is set — resolves the
|
|
48
|
+
* domain's MX and runs the SMTP probe and provider classification concurrently.
|
|
49
|
+
* Everything is aggregated into one {@link EmailVerificationReport} and scored.
|
|
50
|
+
* @param email The email address to verify.
|
|
51
|
+
* @param options Verification options.
|
|
52
|
+
* @returns The complete verification report.
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* import { verifyEmail } from "@visulima/email-verifier";
|
|
56
|
+
*
|
|
57
|
+
* const report = await verifyEmail("user@gmail.com");
|
|
58
|
+
* console.log(report.state, report.score); // "deliverable" 95
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
declare const verifyEmail: (email: string, options?: VerifyEmailOptions) => Promise<EmailVerificationReport>;
|
|
62
|
+
export { type EmailVerificationReport, type VerifyEmailOptions, verifyEmail as default, verifyEmail };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{analyzeCharacters as h}from"./checks/character.js";import{checkMxRecords as v}from"./checks/mx.js";import x,{isNoReply as b}from"./checks/role.js";import g from"./checks/smtp.js";import{analyzeSymbols as w}from"./checks/symbol.js";import V from"./checks/syntax.js";import E from"./checks/tag.js";import R from"./enrich/name.js";import $ from"./enrich/typo.js";import{splitAddress as k}from"./packem_shared/extractDomain-CYJf1_0N.js";import f from"./score.js";import{classifyMxRecords as A}from"@visulima/email-provider-mx";import{isFreeEmail as F}from"@visulima/free-email-domains";import{isDisposableEmail as C}from"@visulima/disposable-email-domains";const p=(e,a)=>({character:h(e),didYouMean:$(e,a.typo)?.full,disposable:C(e,a.disposable),free:F(e,a.free),name:R(e),noReply:b(e),role:x(e,a.roleCustomPrefixes),symbol:w(e),tag:E(e)}),J=async(e,a={})=>{const{checkSmtp:n=!0,offline:y=!1}=a,o=k(e),t=o?.address??(typeof e=="string"?e.trim().toLowerCase():"");if(!V(e)||!o){const s={...p(t,a),acceptAll:!1,deferred:!1,domain:{records:[],resolvedVia:"unchecked",valid:!1},email:t,mailboxFull:!1,secureEmailGateway:!1,syntaxValid:!1};return{...s,...f(s,a.weights)}}const u=p(o.address,a);let m={records:[],resolvedVia:"unchecked",valid:!1},r,l,d=!1;if(!y){const s=await v(o.domain,{cache:a.cache});m={records:s.records??[],resolvedVia:s.resolvedVia,valid:s.valid};const i=s.records??[];s.valid&&i.length>0&&(l=A(i),d=l?.type==="seg",n&&(r=await g(o.address,{cache:a.cache,...a.smtp,mxRecords:i})))}const c={...u,acceptAll:r?.acceptAll??!1,deferred:r?.deferred??!1,domain:m,email:o.address,mailboxFull:r?.mailboxFull??!1,provider:l,secureEmailGateway:d,smtp:r,syntaxValid:!0};return{...c,...f(c,a.weights)}};export{J as default,J as verifyEmail};
|
package/package.json
CHANGED
|
@@ -1,10 +1,125 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@visulima/email-verifier",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Mailer-free email address verification and enrichment: syntax, MX/SMTP probing, disposable/free/role detection, catch-all, provider & secure-email-gateway classification, typo suggestions, and a 0–100 quality score.",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
"accept-all",
|
|
7
|
+
"catch-all",
|
|
8
|
+
"disposable-email",
|
|
9
|
+
"email-enrichment",
|
|
10
|
+
"email-quality-score",
|
|
11
|
+
"email-validation",
|
|
12
|
+
"email-verification",
|
|
13
|
+
"free-email",
|
|
14
|
+
"mx-records",
|
|
15
|
+
"role-account",
|
|
16
|
+
"secure-email-gateway",
|
|
17
|
+
"smtp-verification",
|
|
18
|
+
"visulima"
|
|
19
|
+
],
|
|
20
|
+
"homepage": "https://visulima.com/packages/email-verifier",
|
|
21
|
+
"bugs": "https://github.com/visulima/visulima/issues",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/visulima/visulima.git",
|
|
25
|
+
"directory": "packages/email/email-verifier"
|
|
26
|
+
},
|
|
27
|
+
"funding": [
|
|
28
|
+
{
|
|
29
|
+
"type": "github",
|
|
30
|
+
"url": "https://github.com/sponsors/prisis"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"type": "consulting",
|
|
34
|
+
"url": "https://anolilab.com/support"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"author": {
|
|
39
|
+
"name": "Daniel Bannert",
|
|
40
|
+
"email": "d.bannert@anolilab.de"
|
|
41
|
+
},
|
|
42
|
+
"sideEffects": false,
|
|
43
|
+
"type": "module",
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"types": "./dist/index.d.ts",
|
|
47
|
+
"default": "./dist/index.js"
|
|
48
|
+
},
|
|
49
|
+
"./verify-email": {
|
|
50
|
+
"types": "./dist/verify-email.d.ts",
|
|
51
|
+
"default": "./dist/verify-email.js"
|
|
52
|
+
},
|
|
53
|
+
"./score": {
|
|
54
|
+
"types": "./dist/score.d.ts",
|
|
55
|
+
"default": "./dist/score.js"
|
|
56
|
+
},
|
|
57
|
+
"./checks/syntax": {
|
|
58
|
+
"types": "./dist/checks/syntax.d.ts",
|
|
59
|
+
"default": "./dist/checks/syntax.js"
|
|
60
|
+
},
|
|
61
|
+
"./checks/mx": {
|
|
62
|
+
"types": "./dist/checks/mx.d.ts",
|
|
63
|
+
"default": "./dist/checks/mx.js"
|
|
64
|
+
},
|
|
65
|
+
"./checks/smtp": {
|
|
66
|
+
"types": "./dist/checks/smtp.d.ts",
|
|
67
|
+
"default": "./dist/checks/smtp.js"
|
|
68
|
+
},
|
|
69
|
+
"./checks/disposable": {
|
|
70
|
+
"types": "./dist/checks/disposable.d.ts",
|
|
71
|
+
"default": "./dist/checks/disposable.js"
|
|
72
|
+
},
|
|
73
|
+
"./checks/free": {
|
|
74
|
+
"types": "./dist/checks/free.d.ts",
|
|
75
|
+
"default": "./dist/checks/free.js"
|
|
76
|
+
},
|
|
77
|
+
"./checks/role": {
|
|
78
|
+
"types": "./dist/checks/role.d.ts",
|
|
79
|
+
"default": "./dist/checks/role.js"
|
|
80
|
+
},
|
|
81
|
+
"./checks/tag": {
|
|
82
|
+
"types": "./dist/checks/tag.d.ts",
|
|
83
|
+
"default": "./dist/checks/tag.js"
|
|
84
|
+
},
|
|
85
|
+
"./checks/character": {
|
|
86
|
+
"types": "./dist/checks/character.d.ts",
|
|
87
|
+
"default": "./dist/checks/character.js"
|
|
88
|
+
},
|
|
89
|
+
"./checks/symbol": {
|
|
90
|
+
"types": "./dist/checks/symbol.d.ts",
|
|
91
|
+
"default": "./dist/checks/symbol.js"
|
|
92
|
+
},
|
|
93
|
+
"./enrich/provider": {
|
|
94
|
+
"types": "./dist/enrich/provider.d.ts",
|
|
95
|
+
"default": "./dist/enrich/provider.js"
|
|
96
|
+
},
|
|
97
|
+
"./enrich/typo": {
|
|
98
|
+
"types": "./dist/enrich/typo.d.ts",
|
|
99
|
+
"default": "./dist/enrich/typo.js"
|
|
100
|
+
},
|
|
101
|
+
"./enrich/name": {
|
|
102
|
+
"types": "./dist/enrich/name.d.ts",
|
|
103
|
+
"default": "./dist/enrich/name.js"
|
|
104
|
+
},
|
|
105
|
+
"./package.json": "./package.json"
|
|
106
|
+
},
|
|
107
|
+
"files": [
|
|
108
|
+
"dist",
|
|
109
|
+
"README.md",
|
|
110
|
+
"CHANGELOG.md"
|
|
111
|
+
],
|
|
112
|
+
"dependencies": {
|
|
113
|
+
"@visulima/disposable-email-domains": "1.0.0",
|
|
114
|
+
"@visulima/email-provider-mx": "1.0.0",
|
|
115
|
+
"@visulima/free-email-domains": "1.0.0",
|
|
116
|
+
"lru-cache": "11.5.1"
|
|
117
|
+
},
|
|
118
|
+
"engines": {
|
|
119
|
+
"node": "^22.14.0 || >=24.10.0"
|
|
120
|
+
},
|
|
121
|
+
"publishConfig": {
|
|
122
|
+
"access": "public",
|
|
123
|
+
"provenance": true
|
|
124
|
+
}
|
|
125
|
+
}
|