@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.
- package/README.md +58 -19
- package/package.json +3 -3
- package/src/index.d.ts +2 -0
- package/src/index.js +9 -1
- package/src/index.js.map +1 -1
- package/src/lib/email-deliverability.d.ts +9 -2
- package/src/lib/email-deliverability.js.map +1 -1
- package/src/lib/email-delivery-events.d.ts +41 -48
- package/src/lib/email-delivery-events.js +18 -247
- package/src/lib/email-delivery-events.js.map +1 -1
- package/src/lib/email-health.d.ts +30 -43
- package/src/lib/email-health.js +45 -141
- package/src/lib/email-health.js.map +1 -1
- package/src/lib/mail-provider-resend.d.ts +194 -0
- package/src/lib/mail-provider-resend.js +649 -0
- package/src/lib/mail-provider-resend.js.map +1 -0
- package/src/lib/mail-provider-webhook.d.ts +80 -0
- package/src/lib/mail-provider-webhook.js +156 -0
- package/src/lib/mail-provider-webhook.js.map +1 -0
- package/src/lib/mail-provider.d.ts +148 -0
- package/src/lib/mail-provider.js +84 -0
- package/src/lib/mail-provider.js.map +1 -0
- package/src/lib/mail-providers.d.ts +99 -0
- package/src/lib/mail-providers.js +182 -0
- package/src/lib/mail-providers.js.map +1 -0
- package/src/lib/received-email.d.ts +7 -44
- package/src/lib/received-email.js +8 -104
- package/src/lib/received-email.js.map +1 -1
- package/src/lib/risk-notice-catalog.js +7 -1
- package/src/lib/risk-notice-catalog.js.map +1 -1
- package/src/lib/send-ceilings.d.ts +0 -57
- package/src/lib/send-ceilings.js +0 -47
- package/src/lib/send-ceilings.js.map +1 -1
- package/src/lib/send-email.d.ts +36 -58
- package/src/lib/send-email.js +102 -133
- package/src/lib/send-email.js.map +1 -1
- package/src/lib/send-rate.js +4 -3
- package/src/lib/send-rate.js.map +1 -1
- package/src/lib/system-email-catalog.js +3 -3
- package/src/lib/system-email-catalog.js.map +1 -1
- package/src/lib/tenant-email-catalog.d.ts +7 -0
- package/src/lib/tenant-email-catalog.js +10 -591
- package/src/lib/tenant-email-catalog.js.map +1 -1
- package/src/lib/tenant-emails.generated.d.ts +11 -0
- package/src/lib/tenant-emails.generated.js +594 -0
- 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
|
-
*
|
|
21
|
-
* carries
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
*
|
|
20
|
-
* carries
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
|
|
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
|
-
|
|
1430
|
+
APPEAL_BY_REPLY
|
|
1425
1431
|
],
|
|
1426
1432
|
actions: [
|
|
1427
1433
|
'contact-support'
|