phasmid 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/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Copyright <2026> <Smiduweorc>
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4
+
5
+ THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Phasmid: provider-aware email normalization and canonicalization.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { normalizeEmail, isSameEmail } from "Phasmid";
7
+ *
8
+ * normalizeEmail("John.Doe+newsletter@googlemail.com"); // "johndoe@gmail.com"
9
+ * isSameEmail("a.b@gmail.com", "ab@gmail.com"); // true
10
+ * ```
11
+ */
12
+ export { normalizeEmail, normalizeEmailDetailed, getEmailProvider, isSameEmail, } from "./src/normalize.js";
13
+ export { DEFAULT_PROVIDERS } from "./src/providers.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACN,cAAc,EACd,sBAAsB,EACtB,gBAAgB,EAChB,WAAW,GACX,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,149 @@
1
+ import { DEFAULT_PROVIDERS } from "./providers.js";
2
+ /** Conservative fallback used for domains that match no provider. */
3
+ const CONSERVATIVE_DEFAULT = {
4
+ lowercaseLocal: false,
5
+ removeDots: false,
6
+ subaddressSeparators: [],
7
+ };
8
+ /** A basic domain shape: labels separated by dots, with a final TLD label. */
9
+ const DOMAIN_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/i;
10
+ /** Lazily-built registry for the default-only case (the common path). */
11
+ let defaultRegistry = null;
12
+ function buildRegistry(providers) {
13
+ const map = new Map();
14
+ for (const provider of providers) {
15
+ for (const domain of provider.domains) {
16
+ map.set(domain.toLowerCase(), provider);
17
+ }
18
+ }
19
+ return map;
20
+ }
21
+ function getRegistry(options) {
22
+ if (!options.providers && !options.replaceDefaultProviders) {
23
+ return (defaultRegistry ??= buildRegistry(DEFAULT_PROVIDERS));
24
+ }
25
+ const base = options.replaceDefaultProviders ? [] : DEFAULT_PROVIDERS;
26
+ // Later entries override earlier ones, so user providers take precedence.
27
+ return buildRegistry([...base, ...(options.providers ?? [])]);
28
+ }
29
+ /** Split on the last `@`, the way most parsers treat the local/domain boundary. */
30
+ function splitEmail(email) {
31
+ const at = email.lastIndexOf("@");
32
+ if (at <= 0 || at === email.length - 1)
33
+ return null;
34
+ return { local: email.slice(0, at), domain: email.slice(at + 1) };
35
+ }
36
+ /** A quoted local part (`"a b"@x.com`) is preserved verbatim. */
37
+ function isQuoted(local) {
38
+ return local.length >= 2 && local.startsWith("\"") && local.endsWith("\"");
39
+ }
40
+ function stripSubaddress(local, separators) {
41
+ let cut = -1;
42
+ let cutLen = 0;
43
+ for (const sep of separators) {
44
+ if (!sep)
45
+ continue;
46
+ const idx = local.indexOf(sep);
47
+ // Ignore a separator at index 0; cutting there yields an empty local part.
48
+ if (idx > 0 && (cut === -1 || idx < cut)) {
49
+ cut = idx;
50
+ cutLen = sep.length;
51
+ }
52
+ }
53
+ if (cut === -1)
54
+ return { local, subaddress: null };
55
+ return { local: local.slice(0, cut), subaddress: local.slice(cut + cutLen) };
56
+ }
57
+ function isValidLocal(local) {
58
+ if (local.length === 0)
59
+ return false;
60
+ if (isQuoted(local))
61
+ return true;
62
+ return !/[\s@]/.test(local);
63
+ }
64
+ /**
65
+ * Normalize an email address and return the full structured result, including
66
+ * the matched provider, the stripped sub-address, and a validity flag.
67
+ *
68
+ * @throws {TypeError} if `email` is not a string.
69
+ */
70
+ export function normalizeEmailDetailed(email, options = {}) {
71
+ if (typeof email !== "string") {
72
+ throw new TypeError(`Expected email to be a string, received ${typeof email}`);
73
+ }
74
+ const lowercaseDomain = options.lowercaseDomain ?? true;
75
+ const trimmed = email.trim();
76
+ const parts = splitEmail(trimmed);
77
+ if (!parts) {
78
+ return {
79
+ normalized: trimmed,
80
+ local: trimmed,
81
+ domain: "",
82
+ providerId: null,
83
+ subaddress: null,
84
+ valid: false,
85
+ };
86
+ }
87
+ const lookupDomain = parts.domain.toLowerCase();
88
+ const provider = getRegistry(options).get(lookupDomain) ?? null;
89
+ const rule = provider ?? options.defaultRule ?? CONSERVATIVE_DEFAULT;
90
+ let local = parts.local;
91
+ let subaddress = null;
92
+ if (!isQuoted(local)) {
93
+ const stripped = stripSubaddress(local, rule.subaddressSeparators ?? []);
94
+ local = stripped.local;
95
+ subaddress = stripped.subaddress;
96
+ if (rule.removeDots)
97
+ local = local.replace(/\./g, "");
98
+ if (rule.lowercaseLocal)
99
+ local = local.toLowerCase();
100
+ }
101
+ let domain = lowercaseDomain ? lookupDomain : parts.domain;
102
+ if (provider?.canonicalDomain) {
103
+ domain = lowercaseDomain
104
+ ? provider.canonicalDomain.toLowerCase()
105
+ : provider.canonicalDomain;
106
+ }
107
+ const valid = isValidLocal(local) && DOMAIN_RE.test(domain);
108
+ return {
109
+ normalized: `${local}@${domain}`,
110
+ local,
111
+ domain,
112
+ providerId: provider?.id ?? null,
113
+ subaddress,
114
+ valid,
115
+ };
116
+ }
117
+ /**
118
+ * Normalize an email address to its canonical form.
119
+ *
120
+ * Applies the matching provider's rules (sub-address stripping, dot removal,
121
+ * case-folding, alias-domain collapsing) and returns the canonical string.
122
+ * Unknown domains get a conservative treatment: the domain is lowercased and
123
+ * the local part is left untouched.
124
+ *
125
+ * @throws {TypeError} if `email` is not a string.
126
+ */
127
+ export function normalizeEmail(email, options) {
128
+ return normalizeEmailDetailed(email, options).normalized;
129
+ }
130
+ /**
131
+ * Return the id of the provider that handles the given address's domain, or
132
+ * `null` if no provider matches (or the input is not a valid address).
133
+ */
134
+ export function getEmailProvider(email, options = {}) {
135
+ if (typeof email !== "string")
136
+ return null;
137
+ const parts = splitEmail(email.trim());
138
+ if (!parts)
139
+ return null;
140
+ return getRegistry(options).get(parts.domain.toLowerCase())?.id ?? null;
141
+ }
142
+ /**
143
+ * Returns `true` if two addresses normalize to the same canonical address,
144
+ * i.e. they deliver to the same mailbox under the configured rules.
145
+ */
146
+ export function isSameEmail(a, b, options) {
147
+ return normalizeEmail(a, options) === normalizeEmail(b, options);
148
+ }
149
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../src/normalize.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Built-in provider rules.
3
+ *
4
+ * These encode the well-documented, widely-relied-upon behaviors of major mail
5
+ * providers. They are deliberately conservative: domains are only collapsed
6
+ * where aliases truly deliver to the same mailbox (Gmail), and local parts are
7
+ * only lowercased for providers known to be case-insensitive.
8
+ *
9
+ * Sources cross-checked while compiling this list:
10
+ * - Wikipedia, "Email address" (sub-addressing section).
11
+ * - aaronbassett's "Email sub addressing for different providers" gist.
12
+ * - validator.js `normalizeEmail` provider conventions.
13
+ * - Fastmail / Microsoft Learn / Proton provider documentation.
14
+ *
15
+ * Consumers can extend or override any of these via
16
+ * {@link NormalizeOptions.providers}.
17
+ */
18
+ export const DEFAULT_PROVIDERS = Object.freeze([
19
+ {
20
+ // Gmail ignores dots, supports `+` tagging, and treats googlemail.com as
21
+ // an alias of gmail.com.
22
+ id: "gmail",
23
+ domains: ["gmail.com", "googlemail.com"],
24
+ canonicalDomain: "gmail.com",
25
+ lowercaseLocal: true,
26
+ removeDots: true,
27
+ subaddressSeparators: ["+"],
28
+ },
29
+ {
30
+ // Microsoft consumer mail (Outlook.com / Hotmail / Live / MSN). Dots are
31
+ // significant; `+` tagging is supported. The domains are distinct
32
+ // mailboxes, so they are not collapsed.
33
+ id: "microsoft",
34
+ domains: [
35
+ "outlook.com",
36
+ "outlook.com.au",
37
+ "outlook.co.uk",
38
+ "outlook.fr",
39
+ "outlook.de",
40
+ "outlook.es",
41
+ "outlook.it",
42
+ "outlook.jp",
43
+ "hotmail.com",
44
+ "hotmail.co.uk",
45
+ "hotmail.com.au",
46
+ "hotmail.fr",
47
+ "hotmail.de",
48
+ "hotmail.it",
49
+ "hotmail.es",
50
+ "hotmail.ca",
51
+ "live.com",
52
+ "live.com.au",
53
+ "live.co.uk",
54
+ "live.fr",
55
+ "live.de",
56
+ "live.ca",
57
+ "msn.com",
58
+ "windowslive.com",
59
+ "passport.com",
60
+ ],
61
+ lowercaseLocal: true,
62
+ subaddressSeparators: ["+"],
63
+ },
64
+ {
65
+ // Yahoo historically uses a hyphen as its sub-address separator (its
66
+ // "disposable addresses" feature), not `+`.
67
+ id: "yahoo",
68
+ domains: [
69
+ "yahoo.com",
70
+ "yahoo.co.uk",
71
+ "yahoo.ie",
72
+ "yahoo.fr",
73
+ "yahoo.de",
74
+ "yahoo.es",
75
+ "yahoo.it",
76
+ "yahoo.ca",
77
+ "yahoo.in",
78
+ "yahoo.com.au",
79
+ "yahoo.com.br",
80
+ "yahoo.com.mx",
81
+ "yahoo.com.ar",
82
+ "yahoo.co.jp",
83
+ "ymail.com",
84
+ "rocketmail.com",
85
+ ],
86
+ lowercaseLocal: true,
87
+ subaddressSeparators: ["-"],
88
+ },
89
+ {
90
+ // Apple iCloud. me.com and mac.com forward to the same Apple ID, but we
91
+ // keep them distinct by default to avoid surprising collapses.
92
+ id: "icloud",
93
+ domains: ["icloud.com", "me.com", "mac.com"],
94
+ lowercaseLocal: true,
95
+ subaddressSeparators: ["+"],
96
+ },
97
+ {
98
+ // Fastmail also supports subdomain addressing
99
+ // (user+tag@fastmail.com == tag@user.fastmail.com), which is not
100
+ // resolved here because it depends on the account's domain layout.
101
+ id: "fastmail",
102
+ domains: ["fastmail.com", "fastmail.fm"],
103
+ lowercaseLocal: true,
104
+ subaddressSeparators: ["+"],
105
+ },
106
+ {
107
+ id: "proton",
108
+ domains: ["protonmail.com", "protonmail.ch", "proton.me", "pm.me"],
109
+ lowercaseLocal: true,
110
+ subaddressSeparators: ["+"],
111
+ },
112
+ {
113
+ id: "yandex",
114
+ domains: [
115
+ "yandex.com",
116
+ "yandex.ru",
117
+ "ya.ru",
118
+ "yandex.by",
119
+ "yandex.kz",
120
+ "yandex.ua",
121
+ ],
122
+ lowercaseLocal: true,
123
+ subaddressSeparators: ["+"],
124
+ },
125
+ {
126
+ id: "zoho",
127
+ domains: ["zoho.com", "zohomail.com", "zoho.eu"],
128
+ lowercaseLocal: true,
129
+ subaddressSeparators: ["+"],
130
+ },
131
+ {
132
+ id: "mailfence",
133
+ domains: ["mailfence.com"],
134
+ lowercaseLocal: true,
135
+ subaddressSeparators: ["+"],
136
+ },
137
+ {
138
+ id: "runbox",
139
+ domains: ["runbox.com"],
140
+ lowercaseLocal: true,
141
+ subaddressSeparators: ["+"],
142
+ },
143
+ {
144
+ // Pobox is part of the Fastmail family.
145
+ id: "pobox",
146
+ domains: ["pobox.com"],
147
+ lowercaseLocal: true,
148
+ subaddressSeparators: ["+"],
149
+ },
150
+ {
151
+ id: "tutanota",
152
+ domains: [
153
+ "tutanota.com",
154
+ "tutanota.de",
155
+ "tutamail.com",
156
+ "tuta.com",
157
+ "tuta.io",
158
+ "keemail.me",
159
+ ],
160
+ lowercaseLocal: true,
161
+ subaddressSeparators: ["+"],
162
+ },
163
+ {
164
+ id: "posteo",
165
+ domains: ["posteo.de", "posteo.net"],
166
+ lowercaseLocal: true,
167
+ subaddressSeparators: ["+"],
168
+ },
169
+ {
170
+ id: "mailbox",
171
+ domains: ["mailbox.org"],
172
+ lowercaseLocal: true,
173
+ subaddressSeparators: ["+"],
174
+ },
175
+ {
176
+ // AOL is case-insensitive but does not support `+` tagging.
177
+ id: "aol",
178
+ domains: ["aol.com", "aim.com"],
179
+ lowercaseLocal: true,
180
+ },
181
+ ]);
182
+ //# sourceMappingURL=providers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"providers.js","sourceRoot":"","sources":["../../src/providers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Phasmid: provider-aware email normalization and canonicalization.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { normalizeEmail, isSameEmail } from "Phasmid";
7
+ *
8
+ * normalizeEmail("John.Doe+newsletter@googlemail.com"); // "johndoe@gmail.com"
9
+ * isSameEmail("a.b@gmail.com", "ab@gmail.com"); // true
10
+ * ```
11
+ */
12
+ export { normalizeEmail, normalizeEmailDetailed, getEmailProvider, isSameEmail, } from "./src/normalize.js";
13
+ export { DEFAULT_PROVIDERS } from "./src/providers.js";
14
+ export type { ProviderRule, DefaultRule, LocalPartRules, NormalizeOptions, NormalizedEmail, } from "./src/types.js";
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACN,cAAc,EACd,sBAAsB,EACtB,gBAAgB,EAChB,WAAW,GACX,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,YAAY,EACX,YAAY,EACZ,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,eAAe,GACf,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { NormalizeOptions, NormalizedEmail } from "./types.js";
2
+ /**
3
+ * Normalize an email address and return the full structured result, including
4
+ * the matched provider, the stripped sub-address, and a validity flag.
5
+ *
6
+ * @throws {TypeError} if `email` is not a string.
7
+ */
8
+ export declare function normalizeEmailDetailed(email: string, options?: NormalizeOptions): NormalizedEmail;
9
+ /**
10
+ * Normalize an email address to its canonical form.
11
+ *
12
+ * Applies the matching provider's rules (sub-address stripping, dot removal,
13
+ * case-folding, alias-domain collapsing) and returns the canonical string.
14
+ * Unknown domains get a conservative treatment: the domain is lowercased and
15
+ * the local part is left untouched.
16
+ *
17
+ * @throws {TypeError} if `email` is not a string.
18
+ */
19
+ export declare function normalizeEmail(email: string, options?: NormalizeOptions): string;
20
+ /**
21
+ * Return the id of the provider that handles the given address's domain, or
22
+ * `null` if no provider matches (or the input is not a valid address).
23
+ */
24
+ export declare function getEmailProvider(email: string, options?: NormalizeOptions): string | null;
25
+ /**
26
+ * Returns `true` if two addresses normalize to the same canonical address,
27
+ * i.e. they deliver to the same mailbox under the configured rules.
28
+ */
29
+ export declare function isSameEmail(a: string, b: string, options?: NormalizeOptions): boolean;
30
+ //# sourceMappingURL=normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../../src/normalize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEX,gBAAgB,EAChB,eAAe,EAEf,MAAM,YAAY,CAAC;AA0EpB;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACrC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,gBAAqB,GAC5B,eAAe,CAsDjB;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC7B,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,gBAAgB,GACxB,MAAM,CAER;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC/B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,gBAAqB,GAC5B,MAAM,GAAG,IAAI,CAKf;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAC1B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,OAAO,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAET"}
@@ -0,0 +1,20 @@
1
+ import type { ProviderRule } from "./types.js";
2
+ /**
3
+ * Built-in provider rules.
4
+ *
5
+ * These encode the well-documented, widely-relied-upon behaviors of major mail
6
+ * providers. They are deliberately conservative: domains are only collapsed
7
+ * where aliases truly deliver to the same mailbox (Gmail), and local parts are
8
+ * only lowercased for providers known to be case-insensitive.
9
+ *
10
+ * Sources cross-checked while compiling this list:
11
+ * - Wikipedia, "Email address" (sub-addressing section).
12
+ * - aaronbassett's "Email sub addressing for different providers" gist.
13
+ * - validator.js `normalizeEmail` provider conventions.
14
+ * - Fastmail / Microsoft Learn / Proton provider documentation.
15
+ *
16
+ * Consumers can extend or override any of these via
17
+ * {@link NormalizeOptions.providers}.
18
+ */
19
+ export declare const DEFAULT_PROVIDERS: readonly ProviderRule[];
20
+ //# sourceMappingURL=providers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../../src/providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,iBAAiB,EAAE,SAAS,YAAY,EAmKnD,CAAC"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The set of transformations that can be applied to the local part of an
3
+ * address. Both {@link ProviderRule} and {@link DefaultRule} share these
4
+ * fields so the engine can treat a matched provider and a fallback rule
5
+ * uniformly.
6
+ */
7
+ export interface LocalPartRules {
8
+ /**
9
+ * Lowercase the local part. Most consumer providers treat the local part
10
+ * case-insensitively, so this is enabled for the built-in providers. The
11
+ * email spec (RFC 5321) technically allows case-sensitive local parts, so
12
+ * it is left disabled for unknown domains by default.
13
+ */
14
+ lowercaseLocal?: boolean;
15
+ /**
16
+ * Remove all dots (`.`) from the local part. This is Gmail's behavior:
17
+ * `john.doe@gmail.com` and `johndoe@gmail.com` are the same mailbox.
18
+ */
19
+ removeDots?: boolean;
20
+ /**
21
+ * Characters that begin a "sub-address" (also called plus-addressing or
22
+ * tagging). Everything from the first occurrence of any separator to the
23
+ * end of the local part is stripped. Gmail/Outlook/etc. use `"+"`; Yahoo
24
+ * historically uses `"-"`.
25
+ *
26
+ * A separator at index 0 is ignored, because stripping it would leave an
27
+ * empty local part.
28
+ */
29
+ subaddressSeparators?: string[];
30
+ }
31
+ /**
32
+ * A rule describing how a specific email provider's addresses should be
33
+ * canonicalized.
34
+ */
35
+ export interface ProviderRule extends LocalPartRules {
36
+ /** Stable identifier for the provider, e.g. `"gmail"`. */
37
+ id: string;
38
+ /**
39
+ * The domains this rule applies to. Matched case-insensitively. The first
40
+ * provider registered for a domain wins, and user-supplied providers
41
+ * override the built-ins.
42
+ */
43
+ domains: string[];
44
+ /**
45
+ * Collapse every matched domain to this single canonical domain. Used for
46
+ * provider aliases that deliver to the same mailbox, e.g. `googlemail.com`
47
+ * -> `gmail.com`.
48
+ */
49
+ canonicalDomain?: string;
50
+ }
51
+ /**
52
+ * The fallback rule applied to domains that do not match any provider. It is
53
+ * the same as a {@link ProviderRule} minus the identity fields, and it cannot
54
+ * rewrite the domain.
55
+ */
56
+ export type DefaultRule = LocalPartRules;
57
+ /**
58
+ * Options accepted by the normalization functions.
59
+ */
60
+ export interface NormalizeOptions {
61
+ /**
62
+ * Additional provider rules, or overrides for the built-in providers.
63
+ * Matched by domain; entries here take precedence over the built-ins.
64
+ */
65
+ providers?: ProviderRule[];
66
+ /**
67
+ * Ignore the built-in provider list entirely and use only the providers
68
+ * supplied in {@link NormalizeOptions.providers}. Defaults to `false`.
69
+ */
70
+ replaceDefaultProviders?: boolean;
71
+ /**
72
+ * Rule applied to domains that match no provider. Defaults to a
73
+ * conservative rule that only lowercases the domain and leaves the local
74
+ * part untouched.
75
+ */
76
+ defaultRule?: DefaultRule;
77
+ /**
78
+ * Lowercase the domain. Domains are case-insensitive per RFC 1035, so this
79
+ * defaults to `true`. Disabling it is rarely useful but available.
80
+ */
81
+ lowercaseDomain?: boolean;
82
+ }
83
+ /**
84
+ * The structured result of normalizing an address.
85
+ */
86
+ export interface NormalizedEmail {
87
+ /** The fully normalized address, `${local}@${domain}`. */
88
+ normalized: string;
89
+ /** The normalized local part (before the `@`). */
90
+ local: string;
91
+ /** The normalized domain (after the `@`). */
92
+ domain: string;
93
+ /** The id of the provider that matched, or `null` if none did. */
94
+ providerId: string | null;
95
+ /** The sub-address ("tag") that was stripped, or `null` if there was none. */
96
+ subaddress: string | null;
97
+ /** Whether the input looks like a syntactically valid address. */
98
+ valid: boolean;
99
+ }
100
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC9B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;;OAQG;IACH,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,YAAa,SAAQ,cAAc;IACnD,0DAA0D;IAC1D,EAAE,EAAE,MAAM,CAAC;IAEX;;;;OAIG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;IAElB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,cAAc,CAAC;AAEzC;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC;;;OAGG;IACH,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAE3B;;;OAGG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAElC;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAE1B;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,8EAA8E;IAC9E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,kEAAkE;IAClE,KAAK,EAAE,OAAO,CAAC;CACf"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasmid",
3
- "version": "0.0.1",
3
+ "version": "1.0.0",
4
4
  "description": "TypeScript library for data transformation and normalization.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",