@aglyn/shared-util-email 1.0.0-beta.217 → 1.0.0-beta.219

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.
Files changed (46) hide show
  1. package/README.md +58 -19
  2. package/package.json +3 -3
  3. package/src/index.d.ts +2 -0
  4. package/src/index.js +9 -1
  5. package/src/index.js.map +1 -1
  6. package/src/lib/email-deliverability.d.ts +9 -2
  7. package/src/lib/email-deliverability.js.map +1 -1
  8. package/src/lib/email-delivery-events.d.ts +41 -48
  9. package/src/lib/email-delivery-events.js +18 -247
  10. package/src/lib/email-delivery-events.js.map +1 -1
  11. package/src/lib/email-health.d.ts +30 -43
  12. package/src/lib/email-health.js +45 -141
  13. package/src/lib/email-health.js.map +1 -1
  14. package/src/lib/mail-provider-resend.d.ts +194 -0
  15. package/src/lib/mail-provider-resend.js +649 -0
  16. package/src/lib/mail-provider-resend.js.map +1 -0
  17. package/src/lib/mail-provider-webhook.d.ts +80 -0
  18. package/src/lib/mail-provider-webhook.js +156 -0
  19. package/src/lib/mail-provider-webhook.js.map +1 -0
  20. package/src/lib/mail-provider.d.ts +148 -0
  21. package/src/lib/mail-provider.js +84 -0
  22. package/src/lib/mail-provider.js.map +1 -0
  23. package/src/lib/mail-providers.d.ts +99 -0
  24. package/src/lib/mail-providers.js +182 -0
  25. package/src/lib/mail-providers.js.map +1 -0
  26. package/src/lib/received-email.d.ts +7 -44
  27. package/src/lib/received-email.js +8 -104
  28. package/src/lib/received-email.js.map +1 -1
  29. package/src/lib/risk-notice-catalog.js +7 -1
  30. package/src/lib/risk-notice-catalog.js.map +1 -1
  31. package/src/lib/send-ceilings.d.ts +0 -57
  32. package/src/lib/send-ceilings.js +0 -47
  33. package/src/lib/send-ceilings.js.map +1 -1
  34. package/src/lib/send-email.d.ts +36 -58
  35. package/src/lib/send-email.js +102 -133
  36. package/src/lib/send-email.js.map +1 -1
  37. package/src/lib/send-rate.js +4 -3
  38. package/src/lib/send-rate.js.map +1 -1
  39. package/src/lib/system-email-catalog.js +3 -3
  40. package/src/lib/system-email-catalog.js.map +1 -1
  41. package/src/lib/tenant-email-catalog.d.ts +7 -0
  42. package/src/lib/tenant-email-catalog.js +10 -591
  43. package/src/lib/tenant-email-catalog.js.map +1 -1
  44. package/src/lib/tenant-emails.generated.d.ts +11 -0
  45. package/src/lib/tenant-emails.generated.js +594 -0
  46. package/src/lib/tenant-emails.generated.js.map +1 -0
@@ -0,0 +1,99 @@
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
+ /**
18
+ * WHICH PROVIDER CARRIES THIS DEPLOYMENT'S MAIL.
19
+ *
20
+ * | provider | for |
21
+ * |-----------|--------------------------------------------------------------|
22
+ * | `resend` | Aglyn's own deployment, and anyone with a Resend account |
23
+ * | `webhook` | anything else — the operator's own relay, SMTP, SES, Postmark |
24
+ * | any other | a provider a plugin or a fork registered under that id |
25
+ *
26
+ * `AGLYN_MAIL_PROVIDER` picks one. Unset, the first built-in that has its
27
+ * settings wins, in the order above — so a deployment that already had
28
+ * `RESEND_API_KEY` keeps Resend however else it is configured, and a
29
+ * self-hosted one that set only `AGLYN_MAIL_WEBHOOK_URL` gets its own relay
30
+ * with no other setting. With neither, it is Resend, unconfigured, and every
31
+ * send is skipped with a log line naming the setting — the behavior an
32
+ * install without mail has always had.
33
+ *
34
+ * ## Why the built-ins are compiled in, and not registered by a plugin
35
+ *
36
+ * This is the transactional rail: password resets, invitations, receipts,
37
+ * billing notices and operator alerts, sent from every server process with
38
+ * or without any plugin loaded. A provider a plugin registered at boot is a
39
+ * provider that can be absent — a boot whose declarations threw part-way, a
40
+ * route bundle that evaluates the registry apart from boot's — and an absent
41
+ * provider is indistinguishable from an unconfigured one: every send skipped
42
+ * as if mail were simply off, nothing red. Selecting among providers that
43
+ * are part of this library has no such state. Registration is kept for what
44
+ * cannot be compiled in, and is only ever consulted when the operator named
45
+ * the provider.
46
+ *
47
+ * ## An explicit choice never becomes another vendor
48
+ *
49
+ * A value naming no built-in and no registered provider is REFUSED — every
50
+ * send skipped with an error naming the value — rather than falling back to
51
+ * detection. Falling back would hand an operator's mail, recipients and
52
+ * bodies included, to a vendor they did not choose, because a plugin failed
53
+ * to load or a name was misspelled.
54
+ */
55
+ import { type MailInboundEvent, type MailProvider, type MailProviderReads } from './mail-provider';
56
+ import type { EmailDeliveryEvent } from './email-delivery-events';
57
+ /** The setting that names the provider. */
58
+ export declare const MAIL_PROVIDER_SETTING = "AGLYN_MAIL_PROVIDER";
59
+ type Env = Record<string, string | undefined>;
60
+ /** The ids no registration may take. */
61
+ export declare function builtInMailProviderIds(): string[];
62
+ /**
63
+ * Registers a provider under its `id`, for a deployment that names it in
64
+ * `AGLYN_MAIL_PROVIDER`. The same provider object again is a no-op; a
65
+ * different one under a taken id throws naming it, because two providers
66
+ * answering to one name would split an account's mail between them. A
67
+ * built-in id throws too: the platform's default carrier is not something a
68
+ * plugin can replace by registering.
69
+ */
70
+ export declare function registerMailProvider(provider: MailProvider): void;
71
+ /** Forgets every registration — for specs. */
72
+ export declare function resetMailProvidersForTests(): void;
73
+ /**
74
+ * The provider this deployment's mail is handed to. Never `null`: an
75
+ * unconfigured provider reports what it is missing, and a provider the
76
+ * operator named that this process lacks is a stand-in that refuses (see
77
+ * {@link mailProviderProblem}). Read per call, so a spec — or a runtime
78
+ * whose environment arrives after module load — sees its current settings.
79
+ */
80
+ export declare function mailProvider(env?: Env): MailProvider;
81
+ /**
82
+ * A sentence saying the operator's chosen provider is not available in this
83
+ * process, or `null` when it is (or none was chosen). Printed as an ERROR
84
+ * wherever a skipped send is reported, because unlike an absent key it is
85
+ * not a deployment that simply has no mail: somebody chose a provider and
86
+ * this process cannot find it.
87
+ */
88
+ export declare function mailProviderProblem(provider?: MailProvider): string | null;
89
+ /** One delivery-webhook payload as events, one per recipient; `[]` for anything else. */
90
+ export declare function normalizeDeliveryEvents(payload: unknown, receivedAtMs: number, provider?: MailProvider): EmailDeliveryEvent[];
91
+ /** A received-mail notification, or `null` for any other payload. */
92
+ export declare function readInboundMailEvent(payload: unknown, provider?: MailProvider): MailInboundEvent | null;
93
+ /**
94
+ * The provider's reads of its own account. A provider without any answers
95
+ * with reads whose `unmet` says so, so a route handles "cannot read" the
96
+ * same way whichever the reason.
97
+ */
98
+ export declare function mailProviderReads(provider?: MailProvider): MailProviderReads;
99
+ export {};
@@ -0,0 +1,182 @@
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
+ * WHICH PROVIDER CARRIES THIS DEPLOYMENT'S MAIL.
18
+ *
19
+ * | provider | for |
20
+ * |-----------|--------------------------------------------------------------|
21
+ * | `resend` | Aglyn's own deployment, and anyone with a Resend account |
22
+ * | `webhook` | anything else — the operator's own relay, SMTP, SES, Postmark |
23
+ * | any other | a provider a plugin or a fork registered under that id |
24
+ *
25
+ * `AGLYN_MAIL_PROVIDER` picks one. Unset, the first built-in that has its
26
+ * settings wins, in the order above — so a deployment that already had
27
+ * `RESEND_API_KEY` keeps Resend however else it is configured, and a
28
+ * self-hosted one that set only `AGLYN_MAIL_WEBHOOK_URL` gets its own relay
29
+ * with no other setting. With neither, it is Resend, unconfigured, and every
30
+ * send is skipped with a log line naming the setting — the behavior an
31
+ * install without mail has always had.
32
+ *
33
+ * ## Why the built-ins are compiled in, and not registered by a plugin
34
+ *
35
+ * This is the transactional rail: password resets, invitations, receipts,
36
+ * billing notices and operator alerts, sent from every server process with
37
+ * or without any plugin loaded. A provider a plugin registered at boot is a
38
+ * provider that can be absent — a boot whose declarations threw part-way, a
39
+ * route bundle that evaluates the registry apart from boot's — and an absent
40
+ * provider is indistinguishable from an unconfigured one: every send skipped
41
+ * as if mail were simply off, nothing red. Selecting among providers that
42
+ * are part of this library has no such state. Registration is kept for what
43
+ * cannot be compiled in, and is only ever consulted when the operator named
44
+ * the provider.
45
+ *
46
+ * ## An explicit choice never becomes another vendor
47
+ *
48
+ * A value naming no built-in and no registered provider is REFUSED — every
49
+ * send skipped with an error naming the value — rather than falling back to
50
+ * detection. Falling back would hand an operator's mail, recipients and
51
+ * bodies included, to a vendor they did not choose, because a plugin failed
52
+ * to load or a name was misspelled.
53
+ */ import { resendMailProvider } from "./mail-provider-resend.js";
54
+ import { webhookMailProvider } from "./mail-provider-webhook.js";
55
+ /** The setting that names the provider. */ export const MAIL_PROVIDER_SETTING = 'AGLYN_MAIL_PROVIDER';
56
+ /** The built-in providers, in the order detection tries them. */ function builtInMailProviders(env) {
57
+ return [
58
+ resendMailProvider(env),
59
+ webhookMailProvider(env)
60
+ ];
61
+ }
62
+ /** The ids no registration may take. */ export function builtInMailProviderIds() {
63
+ return builtInMailProviders({}).map((provider)=>provider.id);
64
+ }
65
+ /*==========================================
66
+ * PROVIDERS A PLUGIN OR A FORK SUPPLIES.
67
+ *
68
+ * The slots live on `globalThis` under a registered symbol: a Next server
69
+ * evaluates a module once for boot and again inside a route's bundle, and a
70
+ * module-level map would give the route an empty copy of what boot filled.
71
+ *=========================================*/ const REGISTRY_KEY = Symbol.for('aglyn.mail-providers');
72
+ function registered() {
73
+ const slots = globalThis;
74
+ if (!slots[REGISTRY_KEY]) slots[REGISTRY_KEY] = new Map();
75
+ return slots[REGISTRY_KEY];
76
+ }
77
+ /**
78
+ * Registers a provider under its `id`, for a deployment that names it in
79
+ * `AGLYN_MAIL_PROVIDER`. The same provider object again is a no-op; a
80
+ * different one under a taken id throws naming it, because two providers
81
+ * answering to one name would split an account's mail between them. A
82
+ * built-in id throws too: the platform's default carrier is not something a
83
+ * plugin can replace by registering.
84
+ */ export function registerMailProvider(provider) {
85
+ var _ref;
86
+ const id = String((_ref = provider == null ? void 0 : provider.id) != null ? _ref : '').trim().toLowerCase();
87
+ if (!id) throw new Error('a mail provider needs an id');
88
+ if (builtInMailProviderIds().includes(id)) {
89
+ throw new Error(`"${id}" is a built-in mail provider and cannot be registered`);
90
+ }
91
+ const slots = registered();
92
+ const existing = slots.get(id);
93
+ if (existing && existing !== provider) {
94
+ throw new Error(`a different mail provider is already registered as "${id}"`);
95
+ }
96
+ slots.set(id, provider);
97
+ }
98
+ /** Forgets every registration — for specs. */ export function resetMailProvidersForTests() {
99
+ registered().clear();
100
+ }
101
+ /*==========================================
102
+ * SELECTION.
103
+ *=========================================*/ /** Why each provider that cannot be used cannot, keyed by the stand-in {@link mailProvider} returned. */ const UNAVAILABLE = new WeakMap();
104
+ /**
105
+ * The stand-in for a provider the operator named and this process does not
106
+ * have. It sends nothing and says why in everything that reads it.
107
+ */ function unavailableMailProvider(id) {
108
+ const problem = `${MAIL_PROVIDER_SETTING} is "${id}", which is not a built-in mail ` + `provider (${builtInMailProviderIds().join(', ')}) and was not registered ` + 'in this process, so no mail is sent.';
109
+ const provider = {
110
+ id,
111
+ missingSettings: ()=>[
112
+ MAIL_PROVIDER_SETTING
113
+ ],
114
+ send: async ()=>{
115
+ throw new Error(problem);
116
+ }
117
+ };
118
+ UNAVAILABLE.set(provider, problem);
119
+ return provider;
120
+ }
121
+ /**
122
+ * The provider this deployment's mail is handed to. Never `null`: an
123
+ * unconfigured provider reports what it is missing, and a provider the
124
+ * operator named that this process lacks is a stand-in that refuses (see
125
+ * {@link mailProviderProblem}). Read per call, so a spec — or a runtime
126
+ * whose environment arrives after module load — sees its current settings.
127
+ */ export function mailProvider(env = process.env) {
128
+ var _env_MAIL_PROVIDER_SETTING, _ref, _builtIns_find;
129
+ const requested = String((_env_MAIL_PROVIDER_SETTING = env[MAIL_PROVIDER_SETTING]) != null ? _env_MAIL_PROVIDER_SETTING : '').trim().toLowerCase();
130
+ const builtIns = builtInMailProviders(env);
131
+ if (!requested) {
132
+ var _builtIns_find1;
133
+ return (_builtIns_find1 = builtIns.find((provider)=>!provider.missingSettings().length)) != null ? _builtIns_find1 : builtIns[0];
134
+ }
135
+ return (_ref = (_builtIns_find = builtIns.find((provider)=>provider.id === requested)) != null ? _builtIns_find : registered().get(requested)) != null ? _ref : unavailableMailProvider(requested);
136
+ }
137
+ /**
138
+ * A sentence saying the operator's chosen provider is not available in this
139
+ * process, or `null` when it is (or none was chosen). Printed as an ERROR
140
+ * wherever a skipped send is reported, because unlike an absent key it is
141
+ * not a deployment that simply has no mail: somebody chose a provider and
142
+ * this process cannot find it.
143
+ */ export function mailProviderProblem(provider = mailProvider()) {
144
+ var _UNAVAILABLE_get;
145
+ return (_UNAVAILABLE_get = UNAVAILABLE.get(provider)) != null ? _UNAVAILABLE_get : null;
146
+ }
147
+ /*==========================================
148
+ * WHAT THE REST OF THE PLATFORM ASKS.
149
+ *
150
+ * Each question in the platform's own vocabulary, answered by whichever
151
+ * provider this deployment uses. A provider that cannot answer one gives the
152
+ * empty answer — no events, no inbound message, reads that say they cannot
153
+ * be made — never a guess.
154
+ *=========================================*/ /** One delivery-webhook payload as events, one per recipient; `[]` for anything else. */ export function normalizeDeliveryEvents(payload, receivedAtMs, provider = mailProvider()) {
155
+ var _ref;
156
+ return (_ref = provider.deliveryEvents == null ? void 0 : provider.deliveryEvents.call(provider, payload, receivedAtMs)) != null ? _ref : [];
157
+ }
158
+ /** A received-mail notification, or `null` for any other payload. */ export function readInboundMailEvent(payload, provider = mailProvider()) {
159
+ var _ref;
160
+ return (_ref = provider.inboundEvent == null ? void 0 : provider.inboundEvent.call(provider, payload)) != null ? _ref : null;
161
+ }
162
+ /**
163
+ * The provider's reads of its own account. A provider without any answers
164
+ * with reads whose `unmet` says so, so a route handles "cannot read" the
165
+ * same way whichever the reason.
166
+ */ export function mailProviderReads(provider = mailProvider()) {
167
+ var _mailProviderProblem;
168
+ if (provider.reads) return provider.reads;
169
+ const reason = (_mailProviderProblem = mailProviderProblem(provider)) != null ? _mailProviderProblem : `The "${provider.id}" mail provider cannot read mail it sent or received.`;
170
+ const refuse = async ()=>{
171
+ throw new Error(reason);
172
+ };
173
+ return {
174
+ unmet: ()=>reason,
175
+ history: refuse,
176
+ message: refuse,
177
+ received: refuse,
178
+ sendingDomains: refuse
179
+ };
180
+ }
181
+
182
+ //# sourceMappingURL=mail-providers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/mail-providers.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\n/**\n * WHICH PROVIDER CARRIES THIS DEPLOYMENT'S MAIL.\n *\n * | provider | for |\n * |-----------|--------------------------------------------------------------|\n * | `resend` | Aglyn's own deployment, and anyone with a Resend account |\n * | `webhook` | anything else — the operator's own relay, SMTP, SES, Postmark |\n * | any other | a provider a plugin or a fork registered under that id |\n *\n * `AGLYN_MAIL_PROVIDER` picks one. Unset, the first built-in that has its\n * settings wins, in the order above — so a deployment that already had\n * `RESEND_API_KEY` keeps Resend however else it is configured, and a\n * self-hosted one that set only `AGLYN_MAIL_WEBHOOK_URL` gets its own relay\n * with no other setting. With neither, it is Resend, unconfigured, and every\n * send is skipped with a log line naming the setting — the behavior an\n * install without mail has always had.\n *\n * ## Why the built-ins are compiled in, and not registered by a plugin\n *\n * This is the transactional rail: password resets, invitations, receipts,\n * billing notices and operator alerts, sent from every server process with\n * or without any plugin loaded. A provider a plugin registered at boot is a\n * provider that can be absent — a boot whose declarations threw part-way, a\n * route bundle that evaluates the registry apart from boot's — and an absent\n * provider is indistinguishable from an unconfigured one: every send skipped\n * as if mail were simply off, nothing red. Selecting among providers that\n * are part of this library has no such state. Registration is kept for what\n * cannot be compiled in, and is only ever consulted when the operator named\n * the provider.\n *\n * ## An explicit choice never becomes another vendor\n *\n * A value naming no built-in and no registered provider is REFUSED — every\n * send skipped with an error naming the value — rather than falling back to\n * detection. Falling back would hand an operator's mail, recipients and\n * bodies included, to a vendor they did not choose, because a plugin failed\n * to load or a name was misspelled.\n */\n\nimport {\n type MailInboundEvent,\n type MailProvider,\n type MailProviderReads,\n} from './mail-provider'\nimport { resendMailProvider } from './mail-provider-resend'\nimport { webhookMailProvider } from './mail-provider-webhook'\nimport type { EmailDeliveryEvent } from './email-delivery-events'\n\n/** The setting that names the provider. */\nexport const MAIL_PROVIDER_SETTING = 'AGLYN_MAIL_PROVIDER'\n\ntype Env = Record<string, string | undefined>\n\n/** The built-in providers, in the order detection tries them. */\nfunction builtInMailProviders(env: Env): MailProvider[] {\n return [resendMailProvider(env), webhookMailProvider(env)]\n}\n\n/** The ids no registration may take. */\nexport function builtInMailProviderIds(): string[] {\n return builtInMailProviders({}).map((provider) => provider.id)\n}\n\n/*==========================================\n * PROVIDERS A PLUGIN OR A FORK SUPPLIES.\n *\n * The slots live on `globalThis` under a registered symbol: a Next server\n * evaluates a module once for boot and again inside a route's bundle, and a\n * module-level map would give the route an empty copy of what boot filled.\n *=========================================*/\n\nconst REGISTRY_KEY = Symbol.for('aglyn.mail-providers')\n\nfunction registered(): Map<string, MailProvider> {\n const slots = globalThis as { [REGISTRY_KEY]?: Map<string, MailProvider> }\n if (!slots[REGISTRY_KEY]) slots[REGISTRY_KEY] = new Map()\n return slots[REGISTRY_KEY] as Map<string, MailProvider>\n}\n\n/**\n * Registers a provider under its `id`, for a deployment that names it in\n * `AGLYN_MAIL_PROVIDER`. The same provider object again is a no-op; a\n * different one under a taken id throws naming it, because two providers\n * answering to one name would split an account's mail between them. A\n * built-in id throws too: the platform's default carrier is not something a\n * plugin can replace by registering.\n */\nexport function registerMailProvider(provider: MailProvider): void {\n const id = String(provider?.id ?? '').trim().toLowerCase()\n if (!id) throw new Error('a mail provider needs an id')\n if (builtInMailProviderIds().includes(id)) {\n throw new Error(`\"${id}\" is a built-in mail provider and cannot be registered`)\n }\n const slots = registered()\n const existing = slots.get(id)\n if (existing && existing !== provider) {\n throw new Error(`a different mail provider is already registered as \"${id}\"`)\n }\n slots.set(id, provider)\n}\n\n/** Forgets every registration — for specs. */\nexport function resetMailProvidersForTests(): void {\n registered().clear()\n}\n\n/*==========================================\n * SELECTION.\n *=========================================*/\n\n/** Why each provider that cannot be used cannot, keyed by the stand-in {@link mailProvider} returned. */\nconst UNAVAILABLE = new WeakMap<MailProvider, string>()\n\n/**\n * The stand-in for a provider the operator named and this process does not\n * have. It sends nothing and says why in everything that reads it.\n */\nfunction unavailableMailProvider(id: string): MailProvider {\n const problem =\n `${MAIL_PROVIDER_SETTING} is \"${id}\", which is not a built-in mail ` +\n `provider (${builtInMailProviderIds().join(', ')}) and was not registered ` +\n 'in this process, so no mail is sent.'\n const provider: MailProvider = {\n id,\n missingSettings: () => [MAIL_PROVIDER_SETTING],\n send: async () => {\n throw new Error(problem)\n },\n }\n UNAVAILABLE.set(provider, problem)\n return provider\n}\n\n/**\n * The provider this deployment's mail is handed to. Never `null`: an\n * unconfigured provider reports what it is missing, and a provider the\n * operator named that this process lacks is a stand-in that refuses (see\n * {@link mailProviderProblem}). Read per call, so a spec — or a runtime\n * whose environment arrives after module load — sees its current settings.\n */\nexport function mailProvider(env: Env = process.env as Env): MailProvider {\n const requested = String(env[MAIL_PROVIDER_SETTING] ?? '').trim().toLowerCase()\n const builtIns = builtInMailProviders(env)\n if (!requested) {\n return builtIns.find((provider) => !provider.missingSettings().length) ?? builtIns[0]\n }\n return (\n builtIns.find((provider) => provider.id === requested) ??\n registered().get(requested) ??\n unavailableMailProvider(requested)\n )\n}\n\n/**\n * A sentence saying the operator's chosen provider is not available in this\n * process, or `null` when it is (or none was chosen). Printed as an ERROR\n * wherever a skipped send is reported, because unlike an absent key it is\n * not a deployment that simply has no mail: somebody chose a provider and\n * this process cannot find it.\n */\nexport function mailProviderProblem(provider: MailProvider = mailProvider()): string | null {\n return UNAVAILABLE.get(provider) ?? null\n}\n\n/*==========================================\n * WHAT THE REST OF THE PLATFORM ASKS.\n *\n * Each question in the platform's own vocabulary, answered by whichever\n * provider this deployment uses. A provider that cannot answer one gives the\n * empty answer — no events, no inbound message, reads that say they cannot\n * be made — never a guess.\n *=========================================*/\n\n/** One delivery-webhook payload as events, one per recipient; `[]` for anything else. */\nexport function normalizeDeliveryEvents(\n payload: unknown,\n receivedAtMs: number,\n provider: MailProvider = mailProvider(),\n): EmailDeliveryEvent[] {\n return provider.deliveryEvents?.(payload, receivedAtMs) ?? []\n}\n\n/** A received-mail notification, or `null` for any other payload. */\nexport function readInboundMailEvent(\n payload: unknown,\n provider: MailProvider = mailProvider(),\n): MailInboundEvent | null {\n return provider.inboundEvent?.(payload) ?? null\n}\n\n/**\n * The provider's reads of its own account. A provider without any answers\n * with reads whose `unmet` says so, so a route handles \"cannot read\" the\n * same way whichever the reason.\n */\nexport function mailProviderReads(\n provider: MailProvider = mailProvider(),\n): MailProviderReads {\n if (provider.reads) return provider.reads\n const reason =\n mailProviderProblem(provider) ??\n `The \"${provider.id}\" mail provider cannot read mail it sent or received.`\n const refuse = async (): Promise<never> => {\n throw new Error(reason)\n }\n return {\n unmet: () => reason,\n history: refuse,\n message: refuse,\n received: refuse,\n sendingDomains: refuse,\n }\n}\n"],"names":["resendMailProvider","webhookMailProvider","MAIL_PROVIDER_SETTING","builtInMailProviders","env","builtInMailProviderIds","map","provider","id","REGISTRY_KEY","Symbol","for","registered","slots","globalThis","Map","registerMailProvider","String","trim","toLowerCase","Error","includes","existing","get","set","resetMailProvidersForTests","clear","UNAVAILABLE","WeakMap","unavailableMailProvider","problem","join","missingSettings","send","mailProvider","process","builtIns","requested","find","length","mailProviderProblem","normalizeDeliveryEvents","payload","receivedAtMs","deliveryEvents","readInboundMailEvent","inboundEvent","mailProviderReads","reads","reason","refuse","unmet","history","message","received","sendingDomains"],"mappings":"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"}
@@ -17,23 +17,14 @@
17
17
  /**
18
18
  * RECEIVED MAIL, in the platform's own vocabulary (AGL-2657).
19
19
  *
20
- * Resend announces a received message as an `email.received` webhook that
21
- * carries only its metadata — who it was from and to, its subject, its
22
- * `Message-ID` — and the message itself is read afterwards from the
23
- * receiving API by the id the event named. This module is the one place
24
- * that reads that wire format: the event's shape, the API's shape, and the
25
- * neutral {@link ReceivedEmail} everything downstream works with. A second
26
- * provider is a second normalizer here and nothing else, which is the
27
- * arrangement `email-delivery-events.ts` has for the sending side.
28
- *
29
- * Pure but for {@link resendReceivedEmailSource}, which is a function that
30
- * must be CALLED with a key to do anything, so a client component that
31
- * reached this module through the barrel would execute nothing on load.
20
+ * A provider that receives mail announces each message with a notification
21
+ * that carries its metadata, and the message itself is read afterwards by
22
+ * the id the notification named. Both halves are the provider's to read —
23
+ * its `inboundEvent` and `reads.received` (see `mail-provider.ts`) — and
24
+ * both answer in the neutral {@link ReceivedEmail} everything downstream
25
+ * works with, so a route that files received mail never reads a vendor's
26
+ * payload.
32
27
  */
33
- /** The webhook event type a received message arrives as. */
34
- export declare const RESEND_RECEIVED_EVENT = "email.received";
35
- /** `GET {endpoint}/{id}` — one received message with its text and headers. */
36
- export declare const RESEND_RECEIVING_ENDPOINT = "https://api.resend.com/emails/receiving";
37
28
  /** One received message, provider-neutral. Addresses are as the headers spelled them. */
38
29
  export interface ReceivedEmail {
39
30
  /** The provider's id for the message — what a redelivered event repeats. */
@@ -54,33 +45,5 @@ export interface ReceivedEmail {
54
45
  /** When the provider received it, epoch ms; the read's clock when it said nothing usable. */
55
46
  receivedAtMs: number;
56
47
  }
57
- /**
58
- * The provider's id of the message an `email.received` event announces,
59
- * or `null` for any other event — including a malformed one, which is
60
- * acknowledged and dropped rather than retried forever.
61
- */
62
- export declare function resendReceivedEventId(event: unknown): string | null;
63
- /**
64
- * Every recipient the event names — To, Cc, Bcc and the addresses the
65
- * message was received for — so a route can find the capture token in the
66
- * metadata before it spends a read on the body.
67
- */
68
- export declare function resendReceivedEventRecipients(event: unknown): string[];
69
- /**
70
- * One received message as the receiving API answers it, in the neutral
71
- * shape. `null` for a payload with no id, which is not a message.
72
- */
73
- export declare function normalizeResendReceivedEmail(raw: unknown, nowMs?: number): ReceivedEmail | null;
74
48
  /** Reads one received message by the provider's id; `null` when it has none. */
75
49
  export type ReceivedEmailSource = (id: string) => Promise<ReceivedEmail | null>;
76
- /**
77
- * {@link ReceivedEmailSource} for Resend.
78
- *
79
- * Needs a FULL-ACCESS key: the sending-scoped `RESEND_API_KEY` answers
80
- * every read with `401 restricted_api_key`, which is the right posture for
81
- * the key that sends mail and the reason the console carries a separate
82
- * read key. A `404` is `null` — the message expired or never existed — and
83
- * anything else throws with the status, so the caller decides whether the
84
- * provider should retry.
85
- */
86
- export declare function resendReceivedEmailSource(apiKey: string, fetchImpl?: typeof fetch): ReceivedEmailSource;
@@ -16,109 +16,13 @@
16
16
  */ /**
17
17
  * RECEIVED MAIL, in the platform's own vocabulary (AGL-2657).
18
18
  *
19
- * Resend announces a received message as an `email.received` webhook that
20
- * carries only its metadata — who it was from and to, its subject, its
21
- * `Message-ID` — and the message itself is read afterwards from the
22
- * receiving API by the id the event named. This module is the one place
23
- * that reads that wire format: the event's shape, the API's shape, and the
24
- * neutral {@link ReceivedEmail} everything downstream works with. A second
25
- * provider is a second normalizer here and nothing else, which is the
26
- * arrangement `email-delivery-events.ts` has for the sending side.
27
- *
28
- * Pure but for {@link resendReceivedEmailSource}, which is a function that
29
- * must be CALLED with a key to do anything, so a client component that
30
- * reached this module through the barrel would execute nothing on load.
31
- */ /** The webhook event type a received message arrives as. */ export const RESEND_RECEIVED_EVENT = 'email.received';
32
- /** `GET {endpoint}/{id}` — one received message with its text and headers. */ export const RESEND_RECEIVING_ENDPOINT = 'https://api.resend.com/emails/receiving';
33
- const list = (value)=>Array.isArray(value) ? value.map((entry)=>String(entry != null ? entry : '').trim()).filter((entry)=>entry !== '') : typeof value === 'string' && value.trim() ? [
34
- value.trim()
35
- ] : [];
36
- const text = (value)=>typeof value === 'string' ? value : '';
37
- /** A header by name, case-insensitively, off whatever map the provider sent. */ function header(headers, name) {
38
- if (!headers || typeof headers !== 'object') return '';
39
- const wanted = name.toLowerCase();
40
- for (const [key, value] of Object.entries(headers)){
41
- if (key.toLowerCase() === wanted) {
42
- var _value_;
43
- return Array.isArray(value) ? String((_value_ = value[0]) != null ? _value_ : '').trim() : String(value != null ? value : '').trim();
44
- }
45
- }
46
- return '';
47
- }
48
- /**
49
- * The provider's id of the message an `email.received` event announces,
50
- * or `null` for any other event — including a malformed one, which is
51
- * acknowledged and dropped rather than retried forever.
52
- */ export function resendReceivedEventId(event) {
53
- var _ref;
54
- var _record_data;
55
- const record = event;
56
- if (!record || record.type !== RESEND_RECEIVED_EVENT) return null;
57
- const id = String((_ref = (_record_data = record.data) == null ? void 0 : _record_data.email_id) != null ? _ref : '').trim();
58
- return id || null;
59
- }
60
- /**
61
- * Every recipient the event names — To, Cc, Bcc and the addresses the
62
- * message was received for — so a route can find the capture token in the
63
- * metadata before it spends a read on the body.
64
- */ export function resendReceivedEventRecipients(event) {
65
- var _ref;
66
- const data = (_ref = event == null ? void 0 : event.data) != null ? _ref : {};
67
- return [
68
- ...list(data['to']),
69
- ...list(data['cc']),
70
- ...list(data['bcc']),
71
- ...list(data['received_for'])
72
- ];
73
- }
74
- /**
75
- * One received message as the receiving API answers it, in the neutral
76
- * shape. `null` for a payload with no id, which is not a message.
77
- */ export function normalizeResendReceivedEmail(raw, nowMs = Date.now()) {
78
- var _record_id, _record_created_at, _record_message_id, _record_from, _record_subject;
79
- const record = raw && typeof raw === 'object' ? raw : {};
80
- const id = String((_record_id = record['id']) != null ? _record_id : '').trim();
81
- if (!id) return null;
82
- const headers = record['headers'];
83
- const created = Date.parse(String((_record_created_at = record['created_at']) != null ? _record_created_at : ''));
84
- return {
85
- id,
86
- messageId: String((_record_message_id = record['message_id']) != null ? _record_message_id : '').trim() || header(headers, 'message-id'),
87
- inReplyTo: header(headers, 'in-reply-to'),
88
- from: String((_record_from = record['from']) != null ? _record_from : '').trim(),
89
- to: list(record['to']),
90
- cc: list(record['cc']),
91
- bcc: list(record['bcc']),
92
- receivedFor: list(record['received_for']),
93
- subject: String((_record_subject = record['subject']) != null ? _record_subject : '').trim(),
94
- text: text(record['text']),
95
- html: text(record['html']),
96
- receivedAtMs: Number.isFinite(created) && created > 0 ? created : nowMs
97
- };
98
- }
99
- /**
100
- * {@link ReceivedEmailSource} for Resend.
101
- *
102
- * Needs a FULL-ACCESS key: the sending-scoped `RESEND_API_KEY` answers
103
- * every read with `401 restricted_api_key`, which is the right posture for
104
- * the key that sends mail and the reason the console carries a separate
105
- * read key. A `404` is `null` — the message expired or never existed — and
106
- * anything else throws with the status, so the caller decides whether the
107
- * provider should retry.
108
- */ export function resendReceivedEmailSource(apiKey, fetchImpl = fetch) {
109
- return async (id)=>{
110
- const response = await fetchImpl(`${RESEND_RECEIVING_ENDPOINT}/${encodeURIComponent(id)}`, {
111
- headers: {
112
- Authorization: `Bearer ${apiKey}`
113
- }
114
- });
115
- if (response.status === 404) return null;
116
- if (!response.ok) {
117
- const detail = await response.text().catch(()=>'');
118
- throw new Error(`received email read failed: HTTP ${response.status} ${detail.slice(0, 200)}`);
119
- }
120
- return normalizeResendReceivedEmail(await response.json());
121
- };
122
- }
19
+ * A provider that receives mail announces each message with a notification
20
+ * that carries its metadata, and the message itself is read afterwards by
21
+ * the id the notification named. Both halves are the provider's to read —
22
+ * its `inboundEvent` and `reads.received` (see `mail-provider.ts`) — and
23
+ * both answer in the neutral {@link ReceivedEmail} everything downstream
24
+ * works with, so a route that files received mail never reads a vendor's
25
+ * payload.
26
+ */ /** One received message, provider-neutral. Addresses are as the headers spelled them. */ /** Reads one received message by the provider's id; `null` when it has none. */ export { };
123
27
 
124
28
  //# sourceMappingURL=received-email.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/received-email.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\n/**\n * RECEIVED MAIL, in the platform's own vocabulary (AGL-2657).\n *\n * Resend announces a received message as an `email.received` webhook that\n * carries only its metadata — who it was from and to, its subject, its\n * `Message-ID` — and the message itself is read afterwards from the\n * receiving API by the id the event named. This module is the one place\n * that reads that wire format: the event's shape, the API's shape, and the\n * neutral {@link ReceivedEmail} everything downstream works with. A second\n * provider is a second normalizer here and nothing else, which is the\n * arrangement `email-delivery-events.ts` has for the sending side.\n *\n * Pure but for {@link resendReceivedEmailSource}, which is a function that\n * must be CALLED with a key to do anything, so a client component that\n * reached this module through the barrel would execute nothing on load.\n */\n\n/** The webhook event type a received message arrives as. */\nexport const RESEND_RECEIVED_EVENT = 'email.received'\n\n/** `GET {endpoint}/{id}` — one received message with its text and headers. */\nexport const RESEND_RECEIVING_ENDPOINT = 'https://api.resend.com/emails/receiving'\n\n/** One received message, provider-neutral. Addresses are as the headers spelled them. */\nexport interface ReceivedEmail {\n /** The provider's id for the message — what a redelivered event repeats. */\n id: string\n /** The `Message-ID` header, angle brackets included, or `''`. */\n messageId: string\n /** The `In-Reply-To` header, or `''`. */\n inReplyTo: string\n from: string\n to: string[]\n cc: string[]\n bcc: string[]\n /** The addresses the provider received the message FOR — a forwarded alias's real target. */\n receivedFor: string[]\n subject: string\n text: string\n html: string\n /** When the provider received it, epoch ms; the read's clock when it said nothing usable. */\n receivedAtMs: number\n}\n\nconst list = (value: unknown): string[] =>\n Array.isArray(value)\n ? value.map((entry) => String(entry ?? '').trim()).filter((entry) => entry !== '')\n : typeof value === 'string' && value.trim()\n ? [value.trim()]\n : []\n\nconst text = (value: unknown): string => (typeof value === 'string' ? value : '')\n\n/** A header by name, case-insensitively, off whatever map the provider sent. */\nfunction header(headers: unknown, name: string): string {\n if (!headers || typeof headers !== 'object') return ''\n const wanted = name.toLowerCase()\n for (const [key, value] of Object.entries(headers as Record<string, unknown>)) {\n if (key.toLowerCase() === wanted) {\n return Array.isArray(value) ? String(value[0] ?? '').trim() : String(value ?? '').trim()\n }\n }\n return ''\n}\n\n/**\n * The provider's id of the message an `email.received` event announces,\n * or `null` for any other event — including a malformed one, which is\n * acknowledged and dropped rather than retried forever.\n */\nexport function resendReceivedEventId(event: unknown): string | null {\n const record = event as { type?: unknown; data?: { email_id?: unknown } } | null\n if (!record || record.type !== RESEND_RECEIVED_EVENT) return null\n const id = String(record.data?.email_id ?? '').trim()\n return id || null\n}\n\n/**\n * Every recipient the event names — To, Cc, Bcc and the addresses the\n * message was received for — so a route can find the capture token in the\n * metadata before it spends a read on the body.\n */\nexport function resendReceivedEventRecipients(event: unknown): string[] {\n const data = (event as { data?: Record<string, unknown> } | null)?.data ?? {}\n return [\n ...list(data['to']),\n ...list(data['cc']),\n ...list(data['bcc']),\n ...list(data['received_for']),\n ]\n}\n\n/**\n * One received message as the receiving API answers it, in the neutral\n * shape. `null` for a payload with no id, which is not a message.\n */\nexport function normalizeResendReceivedEmail(\n raw: unknown,\n nowMs: number = Date.now(),\n): ReceivedEmail | null {\n const record = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>\n const id = String(record['id'] ?? '').trim()\n if (!id) return null\n const headers = record['headers']\n const created = Date.parse(String(record['created_at'] ?? ''))\n return {\n id,\n messageId: String(record['message_id'] ?? '').trim() || header(headers, 'message-id'),\n inReplyTo: header(headers, 'in-reply-to'),\n from: String(record['from'] ?? '').trim(),\n to: list(record['to']),\n cc: list(record['cc']),\n bcc: list(record['bcc']),\n receivedFor: list(record['received_for']),\n subject: String(record['subject'] ?? '').trim(),\n text: text(record['text']),\n html: text(record['html']),\n receivedAtMs: Number.isFinite(created) && created > 0 ? created : nowMs,\n }\n}\n\n/** Reads one received message by the provider's id; `null` when it has none. */\nexport type ReceivedEmailSource = (id: string) => Promise<ReceivedEmail | null>\n\n/**\n * {@link ReceivedEmailSource} for Resend.\n *\n * Needs a FULL-ACCESS key: the sending-scoped `RESEND_API_KEY` answers\n * every read with `401 restricted_api_key`, which is the right posture for\n * the key that sends mail and the reason the console carries a separate\n * read key. A `404` is `null` — the message expired or never existed — and\n * anything else throws with the status, so the caller decides whether the\n * provider should retry.\n */\nexport function resendReceivedEmailSource(\n apiKey: string,\n fetchImpl: typeof fetch = fetch,\n): ReceivedEmailSource {\n return async (id) => {\n const response = await fetchImpl(\n `${RESEND_RECEIVING_ENDPOINT}/${encodeURIComponent(id)}`,\n { headers: { Authorization: `Bearer ${apiKey}` } },\n )\n if (response.status === 404) return null\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n throw new Error(\n `received email read failed: HTTP ${response.status} ${detail.slice(0, 200)}`,\n )\n }\n return normalizeResendReceivedEmail(await response.json())\n }\n}\n"],"names":["RESEND_RECEIVED_EVENT","RESEND_RECEIVING_ENDPOINT","list","value","Array","isArray","map","entry","String","trim","filter","text","header","headers","name","wanted","toLowerCase","key","Object","entries","resendReceivedEventId","event","record","type","id","data","email_id","resendReceivedEventRecipients","normalizeResendReceivedEmail","raw","nowMs","Date","now","created","parse","messageId","inReplyTo","from","to","cc","bcc","receivedFor","subject","html","receivedAtMs","Number","isFinite","resendReceivedEmailSource","apiKey","fetchImpl","fetch","response","encodeURIComponent","Authorization","status","ok","detail","catch","Error","slice","json"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/received-email.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\n/**\n * RECEIVED MAIL, in the platform's own vocabulary (AGL-2657).\n *\n * A provider that receives mail announces each message with a notification\n * that carries its metadata, and the message itself is read afterwards by\n * the id the notification named. Both halves are the provider's to read —\n * its `inboundEvent` and `reads.received` (see `mail-provider.ts`) — and\n * both answer in the neutral {@link ReceivedEmail} everything downstream\n * works with, so a route that files received mail never reads a vendor's\n * payload.\n */\n\n/** One received message, provider-neutral. Addresses are as the headers spelled them. */\nexport interface ReceivedEmail {\n /** The provider's id for the message — what a redelivered event repeats. */\n id: string\n /** The `Message-ID` header, angle brackets included, or `''`. */\n messageId: string\n /** The `In-Reply-To` header, or `''`. */\n inReplyTo: string\n from: string\n to: string[]\n cc: string[]\n bcc: string[]\n /** The addresses the provider received the message FOR — a forwarded alias's real target. */\n receivedFor: string[]\n subject: string\n text: string\n html: string\n /** When the provider received it, epoch ms; the read's clock when it said nothing usable. */\n receivedAtMs: number\n}\n\n/** Reads one received message by the provider's id; `null` when it has none. */\nexport type ReceivedEmailSource = (id: string) => Promise<ReceivedEmail | null>\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GAED,uFAAuF,GAqBvF,8EAA8E,GAC9E,WAA+E"}
@@ -278,6 +278,12 @@ export const RISK_STAFF_ACTIONS = {
278
278
  const ASK_FOR_REVIEW = 'If you think this is a mistake, choose Request a review and tell us about it. A person reads every request.';
279
279
  const REVIEW_TIME = 'Reviews are usually answered within one business day. You will get a notice either way.';
280
280
  const CANNOT_SIGN_IN = 'If you cannot sign in, reply to this email or write to {{support.email}} with the reference {{reference}}.';
281
+ /**
282
+ * How a locked ACCOUNT appeals (AGL-3420). It cannot sign in, so the way in
283
+ * is mail: a reply to the notice reaches support (the notice's Reply-To),
284
+ * and support answers from its own mailbox — which a ban does not refuse,
285
+ * because a ban refuses what the platform sends, not what a person does.
286
+ */ const APPEAL_BY_REPLY = 'If you believe this is a mistake, you can appeal: reply to this email, or write to {{support.email}} with the reference {{reference}}. Tell us anything that explains what happened. A person reviews every appeal and answers from that address.';
281
287
  /*
282
288
  * The catalog. Exhaustive by type: a kind added to RISK_EVENT_KINDS is a
283
289
  * compile error here until somebody writes what each audience is told.
@@ -1421,7 +1427,7 @@ const CANNOT_SIGN_IN = 'If you cannot sign in, reply to this email or write to {
1421
1427
  meaning: 'While it is locked: {{lock.affected}}',
1422
1428
  steps: [
1423
1429
  'Read the message above; it says what we need from you.',
1424
- CANNOT_SIGN_IN
1430
+ APPEAL_BY_REPLY
1425
1431
  ],
1426
1432
  actions: [
1427
1433
  'contact-support'