@aglyn/shared-util-email 1.0.0-beta.200 → 1.0.0-beta.201

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/shared-util-email",
3
- "version": "1.0.0-beta.200",
3
+ "version": "1.0.0-beta.201",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,8 +25,8 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/shared-data-enums": "1.0.0-beta.200",
29
- "@aglyn/shared-util-http": "1.0.0-beta.200",
28
+ "@aglyn/shared-data-enums": "1.0.0-beta.201",
29
+ "@aglyn/shared-util-http": "1.0.0-beta.201",
30
30
  "@msgpack/msgpack": "^3.1.3",
31
31
  "@swc/helpers": "0.5.23"
32
32
  },
package/src/index.d.ts CHANGED
@@ -37,3 +37,9 @@ export * from './lib/email-revenue-window';
37
37
  export * from './lib/email-media-src';
38
38
  export * from './lib/stored-email-nodes';
39
39
  export * from './lib/email-merge';
40
+ export * from './lib/mail-gateway';
41
+ export * from './lib/mail-bounce';
42
+ export * from './lib/email-deliverability';
43
+ export * from './lib/outbound-phishing-screen';
44
+ export * from './lib/outbound-screen-gate';
45
+ export * from './lib/hosted-page-screen';
package/src/index.js CHANGED
@@ -45,5 +45,21 @@ export * from "./lib/email-media-src.js";
45
45
  // this scope:shared lib may not import, pinned by `email-nodes-drift.spec.ts`.
46
46
  export * from "./lib/stored-email-nodes.js";
47
47
  export * from "./lib/email-merge.js";
48
+ // The deliverability engine every send path asks (AGL-3328): the gateway an
49
+ // MX names and its ledger, the bounce reader, and the address verdicts. Pure
50
+ // — the DNS lookup and the store are injected by `@aglyn/tenant-data-admin`,
51
+ // so nothing here reaches `node:dns` from the browser.
52
+ export * from "./lib/mail-gateway.js";
53
+ export * from "./lib/mail-bounce.js";
54
+ export * from "./lib/email-deliverability.js";
55
+ // The outbound phishing screen a young workspace's campaigns and automated
56
+ // email pass (AGL-3356). Pure; the hold it leads to is `tenant-data-admin`'s.
57
+ export * from "./lib/outbound-phishing-screen.js";
58
+ // The send seam's gate for that screen — the store a hold writes is injected
59
+ // by `@aglyn/tenant-data-admin`, like the governor's.
60
+ export * from "./lib/outbound-screen-gate.js";
61
+ // The same screen read over a published page (AGL-3356): lookalike links and
62
+ // embeds, author-defined credential fields, a brand's call to action.
63
+ export * from "./lib/hosted-page-screen.js";
48
64
 
49
65
  //# sourceMappingURL=index.js.map
package/src/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/shared/util/email/src/index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport * from './lib/send-email'\nexport * from './lib/send-rate'\nexport * from './lib/sender-reputation'\nexport * from './lib/send-ceilings'\nexport * from './lib/sending-domain'\nexport * from './lib/tracking-host'\nexport * from './lib/sending-mailbox'\nexport * from './lib/host-sender'\nexport * from './lib/platform-sending-domain'\nexport * from './lib/email-health'\nexport * from './lib/system-email-catalog'\nexport * from './lib/tenant-email-catalog'\nexport * from './lib/host-email-render'\nexport * from './lib/email-render'\nexport * from './lib/text-email-html'\nexport * from './lib/marketing-send'\n// The List-Unsubscribe setting sequences and campaigns share (AGL-3307).\nexport * from './lib/list-unsubscribe'\nexport * from './lib/email-delivery-events'\n// The received-mail reader the CRM's capture route files from (AGL-2657).\n// Its neighbor `./lib/svix-signature` is deliberately NOT re-exported here:\n// it holds a `crypto` HMAC, and this barrel is reached from the browser\n// through `@aglyn/shared-ui-email-campaigns`, so a webhook imports it by its\n// own path — `@aglyn/shared-util-email/svix-signature`.\nexport * from './lib/received-email'\nexport * from './lib/email-revenue-window'\n// Exported so the console drift guard can pin it against @aglyn/aglyn.\nexport * from './lib/email-media-src'\n// Same arrangement, same reason (AGL-1223): a copy of `decodeStoredNodes`\n// this scope:shared lib may not import, pinned by `email-nodes-drift.spec.ts`.\nexport * from './lib/stored-email-nodes'\nexport * from './lib/email-merge'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,cAAc,sBAAkB;AAChC,cAAc,qBAAiB;AAC/B,cAAc,6BAAyB;AACvC,cAAc,yBAAqB;AACnC,cAAc,0BAAsB;AACpC,cAAc,yBAAqB;AACnC,cAAc,2BAAuB;AACrC,cAAc,uBAAmB;AACjC,cAAc,mCAA+B;AAC7C,cAAc,wBAAoB;AAClC,cAAc,gCAA4B;AAC1C,cAAc,gCAA4B;AAC1C,cAAc,6BAAyB;AACvC,cAAc,wBAAoB;AAClC,cAAc,2BAAuB;AACrC,cAAc,0BAAsB;AACpC,yEAAyE;AACzE,cAAc,4BAAwB;AACtC,cAAc,iCAA6B;AAC3C,0EAA0E;AAC1E,4EAA4E;AAC5E,wEAAwE;AACxE,6EAA6E;AAC7E,wDAAwD;AACxD,cAAc,0BAAsB;AACpC,cAAc,gCAA4B;AAC1C,uEAAuE;AACvE,cAAc,2BAAuB;AACrC,0EAA0E;AAC1E,+EAA+E;AAC/E,cAAc,8BAA0B;AACxC,cAAc,uBAAmB"}
1
+ {"version":3,"sources":["../../../../../../libs/shared/util/email/src/index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport * from './lib/send-email'\nexport * from './lib/send-rate'\nexport * from './lib/sender-reputation'\nexport * from './lib/send-ceilings'\nexport * from './lib/sending-domain'\nexport * from './lib/tracking-host'\nexport * from './lib/sending-mailbox'\nexport * from './lib/host-sender'\nexport * from './lib/platform-sending-domain'\nexport * from './lib/email-health'\nexport * from './lib/system-email-catalog'\nexport * from './lib/tenant-email-catalog'\nexport * from './lib/host-email-render'\nexport * from './lib/email-render'\nexport * from './lib/text-email-html'\nexport * from './lib/marketing-send'\n// The List-Unsubscribe setting sequences and campaigns share (AGL-3307).\nexport * from './lib/list-unsubscribe'\nexport * from './lib/email-delivery-events'\n// The received-mail reader the CRM's capture route files from (AGL-2657).\n// Its neighbor `./lib/svix-signature` is deliberately NOT re-exported here:\n// it holds a `crypto` HMAC, and this barrel is reached from the browser\n// through `@aglyn/shared-ui-email-campaigns`, so a webhook imports it by its\n// own path — `@aglyn/shared-util-email/svix-signature`.\nexport * from './lib/received-email'\nexport * from './lib/email-revenue-window'\n// Exported so the console drift guard can pin it against @aglyn/aglyn.\nexport * from './lib/email-media-src'\n// Same arrangement, same reason (AGL-1223): a copy of `decodeStoredNodes`\n// this scope:shared lib may not import, pinned by `email-nodes-drift.spec.ts`.\nexport * from './lib/stored-email-nodes'\nexport * from './lib/email-merge'\n// The deliverability engine every send path asks (AGL-3328): the gateway an\n// MX names and its ledger, the bounce reader, and the address verdicts. Pure\n// — the DNS lookup and the store are injected by `@aglyn/tenant-data-admin`,\n// so nothing here reaches `node:dns` from the browser.\nexport * from './lib/mail-gateway'\nexport * from './lib/mail-bounce'\nexport * from './lib/email-deliverability'\n// The outbound phishing screen a young workspace's campaigns and automated\n// email pass (AGL-3356). Pure; the hold it leads to is `tenant-data-admin`'s.\nexport * from './lib/outbound-phishing-screen'\n// The send seam's gate for that screen — the store a hold writes is injected\n// by `@aglyn/tenant-data-admin`, like the governor's.\nexport * from './lib/outbound-screen-gate'\n// The same screen read over a published page (AGL-3356): lookalike links and\n// embeds, author-defined credential fields, a brand's call to action.\nexport * from './lib/hosted-page-screen'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,cAAc,sBAAkB;AAChC,cAAc,qBAAiB;AAC/B,cAAc,6BAAyB;AACvC,cAAc,yBAAqB;AACnC,cAAc,0BAAsB;AACpC,cAAc,yBAAqB;AACnC,cAAc,2BAAuB;AACrC,cAAc,uBAAmB;AACjC,cAAc,mCAA+B;AAC7C,cAAc,wBAAoB;AAClC,cAAc,gCAA4B;AAC1C,cAAc,gCAA4B;AAC1C,cAAc,6BAAyB;AACvC,cAAc,wBAAoB;AAClC,cAAc,2BAAuB;AACrC,cAAc,0BAAsB;AACpC,yEAAyE;AACzE,cAAc,4BAAwB;AACtC,cAAc,iCAA6B;AAC3C,0EAA0E;AAC1E,4EAA4E;AAC5E,wEAAwE;AACxE,6EAA6E;AAC7E,wDAAwD;AACxD,cAAc,0BAAsB;AACpC,cAAc,gCAA4B;AAC1C,uEAAuE;AACvE,cAAc,2BAAuB;AACrC,0EAA0E;AAC1E,+EAA+E;AAC/E,cAAc,8BAA0B;AACxC,cAAc,uBAAmB;AACjC,4EAA4E;AAC5E,6EAA6E;AAC7E,6EAA6E;AAC7E,uDAAuD;AACvD,cAAc,wBAAoB;AAClC,cAAc,uBAAmB;AACjC,cAAc,gCAA4B;AAC1C,2EAA2E;AAC3E,8EAA8E;AAC9E,cAAc,oCAAgC;AAC9C,6EAA6E;AAC7E,sDAAsD;AACtD,cAAc,gCAA4B;AAC1C,6EAA6E;AAC7E,sEAAsE;AACtE,cAAc,8BAA0B"}
@@ -0,0 +1,200 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type MailGateway, type MailGatewayStanding } from './mail-gateway';
18
+ /** An address as the checks read it: trimmed and lowercased, or `null` when it is not one. */
19
+ export declare function normalizeDeliverabilityEmail(value: unknown): string | null;
20
+ /** Whether a value is a domain with at least two labels and a non-numeric top level. */
21
+ export declare function isDeliverabilityDomain(value: unknown): boolean;
22
+ /** The domain of an address, lowercased, or `null` for a value that is not one. */
23
+ export declare function emailAddressDomain(email: unknown): string | null;
24
+ /** Whether an address is a role mailbox (`info@`, `sales@`), read before any `+tag`. */
25
+ export declare function isRoleEmailAddress(email: unknown): boolean;
26
+ /** Whether a domain is a disposable-mailbox provider. */
27
+ export declare function isDisposableEmailDomain(domain: unknown): boolean;
28
+ /**
29
+ * The provider a domain most likely meant, or `null` — a SUGGESTION a
30
+ * surface may offer, never a correction applied: `gmial.com` may be a real
31
+ * domain somebody owns, and changing an address a person typed is sending
32
+ * their mail to someone else.
33
+ */
34
+ export declare function suggestEmailDomain(domain: unknown): string | null;
35
+ /** One MX record as a resolver answers it. */
36
+ export interface MailMxRecord {
37
+ exchange: string;
38
+ priority: number;
39
+ }
40
+ /**
41
+ * The DNS the checks read, handed in by the server layer.
42
+ *
43
+ * `resolveMx` resolves the domain's MX records. A domain that publishes
44
+ * none resolves `[]` or rejects with a `code` of `ENOTFOUND`, `ENODATA` or
45
+ * `NXDOMAIN` — Node's own convention — and any other rejection is a
46
+ * resolver that could not answer, which the checks read as "unknown" and
47
+ * never as "no MX".
48
+ *
49
+ * `resolveAddress`, when given, answers whether the domain has an address
50
+ * record (A or AAAA): the implicit MX a domain with no MX is still
51
+ * delivered to. Omitted, a domain with no MX reads as `no_mx`.
52
+ */
53
+ export interface MailDnsResolver {
54
+ resolveMx(domain: string): Promise<ReadonlyArray<MailMxRecord>>;
55
+ resolveAddress?(domain: string): Promise<boolean>;
56
+ }
57
+ /**
58
+ * What a domain's DNS says about receiving mail.
59
+ *
60
+ * - `mx` — it publishes MX records.
61
+ * - `implicit_mx` — no MX, but an address record, which RFC 5321 §5.1 still
62
+ * delivers to. Deliverable; warned, because it is usually a domain whose
63
+ * mail was never set up.
64
+ * - `null_mx` — RFC 7505's `MX 0 .`: the domain says outright it takes no mail.
65
+ * - `no_mx` — no MX and no address record: nothing to deliver to.
66
+ */
67
+ export type MailDomainStatus = 'mx' | 'implicit_mx' | 'null_mx' | 'no_mx';
68
+ /** One domain's answer, as the platform cache stores it. */
69
+ export interface MailDomainIntel {
70
+ domain: string;
71
+ status: MailDomainStatus;
72
+ /** The MX exchanges, lowest preference first; empty for every status but `mx`. */
73
+ mx: string[];
74
+ gateway: MailGateway;
75
+ /** When the domain was looked up; trusted for {@link mailDomainIntelTtlMs}. */
76
+ resolvedAtMs: number;
77
+ }
78
+ export declare const MAIL_DOMAIN_STATUSES: readonly MailDomainStatus[];
79
+ /** Whether a status means the domain takes no mail. */
80
+ export declare function mailDomainRefusesMail(status: MailDomainStatus | null | undefined): boolean;
81
+ /** How long an answer is trusted: a week for a domain that takes mail, a day for one that does not. */
82
+ export declare function mailDomainIntelTtlMs(status: MailDomainStatus): number;
83
+ /** Whether a cached answer is still trusted at `nowMs`. */
84
+ export declare function isMailDomainIntelFresh(intel: MailDomainIntel | null | undefined, nowMs: number): boolean;
85
+ /** Whether a resolver rejection is an answer ("no such record") rather than a failure to get one. */
86
+ export declare function isNoRecordDnsError(error: unknown): boolean;
87
+ /** A stored answer in its model shape, or `null` for no document or an unreadable one. */
88
+ export declare function readStoredMailDomainIntel(id: string, data: Record<string, unknown> | null | undefined): MailDomainIntel | null;
89
+ /**
90
+ * A domain's mail setup, looked up — see {@link MailDomainStatus}. Rejects
91
+ * when the resolver could not answer, and the caller decides what a missing
92
+ * answer means; every path here reads it as "unknown" and sends.
93
+ */
94
+ export declare function resolveMailDomain(resolver: MailDnsResolver, domain: string, nowMs: number): Promise<MailDomainIntel>;
95
+ /**
96
+ * Who a message is for, which decides what may stop it — see the module
97
+ * note. `transactional` is mail the recipient asked for; `bulk` is a
98
+ * campaign, a workflow's marketing step, a sweep; `cold` is a sequence to
99
+ * somebody who has not written to the sender.
100
+ */
101
+ export type EmailSendPurpose = 'transactional' | 'bulk' | 'cold';
102
+ export type EmailDeliverabilityCode = 'invalid_syntax' | 'no_mx' | 'null_mx' | 'implicit_mx' | 'typo_domain' | 'disposable_domain' | 'role_address' | 'gateway_held';
103
+ export type EmailDeliverabilitySeverity = 'refuse' | 'hold' | 'warn';
104
+ export interface EmailDeliverabilityFinding {
105
+ code: EmailDeliverabilityCode;
106
+ severity: EmailDeliverabilitySeverity;
107
+ /** A sentence a surface shows as is. */
108
+ message: string;
109
+ /** `typo_domain` only: the address with the suggested domain. Never applied for the person. */
110
+ suggestion?: string;
111
+ }
112
+ /**
113
+ * What the checks conclude, worst first: `refused` (never sent), `held`
114
+ * (bulk and cold mail wait for a person), `deliverable` (nothing stops it,
115
+ * warnings or not), `unchecked` (the MX could not be read, so nothing is
116
+ * known — and nothing is stopped).
117
+ */
118
+ export type EmailDeliverabilityOutcome = 'refused' | 'held' | 'deliverable' | 'unchecked';
119
+ export interface EmailDeliverabilityVerdict {
120
+ /** The address, normalized, or `null` when it is not one. */
121
+ email: string | null;
122
+ domain: string | null;
123
+ outcome: EmailDeliverabilityOutcome;
124
+ findings: EmailDeliverabilityFinding[];
125
+ intel: MailDomainIntel | null;
126
+ gateway: MailGatewayStanding | null;
127
+ }
128
+ export interface EmailDeliverabilityInput {
129
+ email: unknown;
130
+ purpose: EmailSendPurpose;
131
+ /** The domain's MX answer; `null` or absent when it could not be read. */
132
+ intel?: MailDomainIntel | null;
133
+ /** The gateway's standing with the sending domain, when the caller read the ledger. */
134
+ gateway?: MailGatewayStanding | null;
135
+ /**
136
+ * The domain is a public mailbox provider (`gmail.com`), which is never
137
+ * refused for its MX. The list lives with the CRM's, so the caller asks.
138
+ */
139
+ publicMailbox?: boolean;
140
+ /** A person already released the gateway hold for this address. */
141
+ holdReleased?: boolean;
142
+ }
143
+ /** The whole verdict for one address — see the module note. */
144
+ export declare function assessEmailDeliverability(input: EmailDeliverabilityInput): EmailDeliverabilityVerdict;
145
+ /** The finding that decided a refusal or a hold, or `null` when nothing stops the address. */
146
+ export declare function decidingDeliverabilityFinding(verdict: EmailDeliverabilityVerdict): EmailDeliverabilityFinding | null;
147
+ /**
148
+ * What a bulk send's review says about the addresses the checks took out,
149
+ * or `null` when they took out nobody: "3 recipients excluded: no mail
150
+ * server · 2 behind a gateway that refused this sender".
151
+ */
152
+ export declare function deliverabilityExclusionSummary(counts: {
153
+ noMailServer?: number | null;
154
+ gatewayHeld?: number | null;
155
+ }): string | null;
156
+ /** What `sendEmail` asks before a message leaves. */
157
+ export interface EmailDeliverabilityPreflightRequest {
158
+ /** The recipients, normalized and lowercased. */
159
+ recipients: string[];
160
+ purpose: EmailSendPurpose;
161
+ /** The domain the message leaves from, or `null` when it could not be read. */
162
+ sendingDomain: string | null;
163
+ /**
164
+ * Whose reputation the sending domain is: a site's own verified domain
165
+ * (`custom`), the pooled tenant identity (`shared`), or Aglyn's own
166
+ * (`platform`). The last two are learned at platform level, where a block
167
+ * hurts every tenant.
168
+ */
169
+ sendingSource: 'custom' | 'shared' | 'platform';
170
+ context?: string;
171
+ hostId?: string | null;
172
+ }
173
+ export interface EmailDeliverabilityPreflightVerdict {
174
+ /** Recipients whose domain takes no mail. Never sent, whatever the purpose. */
175
+ refused: Array<{
176
+ email: string;
177
+ code: EmailDeliverabilityCode;
178
+ reason: string;
179
+ }>;
180
+ /** Recipients behind a gateway that refused this sender twice. Bulk and cold mail only. */
181
+ held: Array<{
182
+ email: string;
183
+ gateway: MailGateway;
184
+ reason: string;
185
+ }>;
186
+ }
187
+ export type EmailDeliverabilityPreflight = (request: EmailDeliverabilityPreflightRequest) => Promise<EmailDeliverabilityPreflightVerdict>;
188
+ /** Installs the durable preflight. Called once, from `@aglyn/tenant-data-admin`. */
189
+ export declare function setEmailDeliverabilityPreflight(preflight: EmailDeliverabilityPreflight | null): void;
190
+ /** The installed preflight, or `null` when nothing has been installed. */
191
+ export declare function getEmailDeliverabilityPreflight(): EmailDeliverabilityPreflight | null;
192
+ /** Test seam: forget any installed preflight. */
193
+ export declare function resetEmailDeliverabilityPreflightForTests(): void;
194
+ /**
195
+ * How long `sendEmail` waits for the preflight before sending anyway. A
196
+ * cold lookup is one DNS round trip and one document read; a preflight
197
+ * slower than this is an outage on the control, and a control outage must
198
+ * not become a mail outage.
199
+ */
200
+ export declare const EMAIL_DELIVERABILITY_PREFLIGHT_TIMEOUT_MS = 2500;