@aglyn/shared-util-email 1.0.0-beta.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +104 -0
- package/package.json +37 -0
- package/src/index.d.ts +37 -0
- package/src/index.js +46 -0
- package/src/index.js.map +1 -0
- package/src/lib/email-delivery-events.d.ts +198 -0
- package/src/lib/email-delivery-events.js +310 -0
- package/src/lib/email-delivery-events.js.map +1 -0
- package/src/lib/email-health.d.ts +154 -0
- package/src/lib/email-health.js +264 -0
- package/src/lib/email-health.js.map +1 -0
- package/src/lib/email-media-src.d.ts +33 -0
- package/src/lib/email-media-src.js +113 -0
- package/src/lib/email-media-src.js.map +1 -0
- package/src/lib/email-merge.d.ts +31 -0
- package/src/lib/email-merge.js +36 -0
- package/src/lib/email-merge.js.map +1 -0
- package/src/lib/email-render.d.ts +114 -0
- package/src/lib/email-render.js +293 -0
- package/src/lib/email-render.js.map +1 -0
- package/src/lib/email-revenue-window.d.ts +55 -0
- package/src/lib/email-revenue-window.js +58 -0
- package/src/lib/email-revenue-window.js.map +1 -0
- package/src/lib/host-email-render.d.ts +99 -0
- package/src/lib/host-email-render.js +126 -0
- package/src/lib/host-email-render.js.map +1 -0
- package/src/lib/host-sender.d.ts +86 -0
- package/src/lib/host-sender.js +131 -0
- package/src/lib/host-sender.js.map +1 -0
- package/src/lib/marketing-send.d.ts +466 -0
- package/src/lib/marketing-send.js +459 -0
- package/src/lib/marketing-send.js.map +1 -0
- package/src/lib/platform-sending-domain.d.ts +362 -0
- package/src/lib/platform-sending-domain.js +697 -0
- package/src/lib/platform-sending-domain.js.map +1 -0
- package/src/lib/received-email.d.ts +86 -0
- package/src/lib/received-email.js +124 -0
- package/src/lib/received-email.js.map +1 -0
- package/src/lib/send-ceilings.d.ts +394 -0
- package/src/lib/send-ceilings.js +341 -0
- package/src/lib/send-ceilings.js.map +1 -0
- package/src/lib/send-email.d.ts +385 -0
- package/src/lib/send-email.js +586 -0
- package/src/lib/send-email.js.map +1 -0
- package/src/lib/send-rate.d.ts +298 -0
- package/src/lib/send-rate.js +310 -0
- package/src/lib/send-rate.js.map +1 -0
- package/src/lib/sender-reputation.d.ts +357 -0
- package/src/lib/sender-reputation.js +392 -0
- package/src/lib/sender-reputation.js.map +1 -0
- package/src/lib/sending-domain.d.ts +744 -0
- package/src/lib/sending-domain.js +798 -0
- package/src/lib/sending-domain.js.map +1 -0
- package/src/lib/sending-mailbox.d.ts +132 -0
- package/src/lib/sending-mailbox.js +192 -0
- package/src/lib/sending-mailbox.js.map +1 -0
- package/src/lib/stored-email-nodes.d.ts +38 -0
- package/src/lib/stored-email-nodes.js +78 -0
- package/src/lib/stored-email-nodes.js.map +1 -0
- package/src/lib/svix-signature.d.ts +38 -0
- package/src/lib/svix-signature.js +55 -0
- package/src/lib/svix-signature.js.map +1 -0
- package/src/lib/system-email-catalog.d.ts +170 -0
- package/src/lib/system-email-catalog.js +828 -0
- package/src/lib/system-email-catalog.js.map +1 -0
- package/src/lib/tenant-email-catalog.d.ts +78 -0
- package/src/lib/tenant-email-catalog.js +452 -0
- package/src/lib/tenant-email-catalog.js.map +1 -0
- package/src/lib/text-email-html.d.ts +32 -0
- package/src/lib/text-email-html.js +119 -0
- package/src/lib/text-email-html.js.map +1 -0
|
@@ -0,0 +1,385 @@
|
|
|
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
|
+
* The Resend send endpoint. Every outbound application email in Aglyn goes
|
|
19
|
+
* through here — invites, receipts, usage summaries, campaigns, staff alerts.
|
|
20
|
+
*
|
|
21
|
+
* Auth email (verification, password reset) is Firebase's job and does NOT
|
|
22
|
+
* come through this module.
|
|
23
|
+
*/
|
|
24
|
+
import { type EmailSendPriority } from './send-rate';
|
|
25
|
+
import { type SendingIdentityAudience, type SendingIdentityVerdict } from './sending-domain';
|
|
26
|
+
import { type MarketingSendContext } from './marketing-send';
|
|
27
|
+
export declare const RESEND_SEND_ENDPOINT = "https://api.resend.com/emails";
|
|
28
|
+
/** A Resend delivery tag, used for webhook attribution (AGL-268). */
|
|
29
|
+
export interface EmailTag {
|
|
30
|
+
name: string;
|
|
31
|
+
value: string;
|
|
32
|
+
}
|
|
33
|
+
export interface SendEmailOptions {
|
|
34
|
+
/** One or more recipient addresses. */
|
|
35
|
+
to: string | string[];
|
|
36
|
+
subject: string;
|
|
37
|
+
/** Plain-text body. Supply at least one of `text` or `html`. */
|
|
38
|
+
text?: string;
|
|
39
|
+
/**
|
|
40
|
+
* HTML body. Supply at least one of `text` or `html`.
|
|
41
|
+
*
|
|
42
|
+
* Omitted, one is synthesized from `text` so the message always carries an
|
|
43
|
+
* HTML part — a text-only message has no anchors, so its links are inert in
|
|
44
|
+
* the inbox and Resend's click tracking has nothing to rewrite. See
|
|
45
|
+
* `text-email-html.ts`.
|
|
46
|
+
*/
|
|
47
|
+
html?: string;
|
|
48
|
+
/** Extra MIME headers, e.g. `List-Unsubscribe`. */
|
|
49
|
+
headers?: Record<string, string>;
|
|
50
|
+
/** Delivery tags for the opens/clicks webhook. */
|
|
51
|
+
tags?: EmailTag[];
|
|
52
|
+
replyTo?: string | string[];
|
|
53
|
+
/**
|
|
54
|
+
* White-label display name for the sender (White-Label Phase 1). Replaces
|
|
55
|
+
* only the display name in front of the verified address — the address
|
|
56
|
+
* itself is never taken from the caller, so this cannot forge a different
|
|
57
|
+
* sender. Callers pass `resolveBrandingProfile(org).fromName` here so an
|
|
58
|
+
* agency's mail reads as their brand instead of "Aglyn".
|
|
59
|
+
*/
|
|
60
|
+
fromName?: string;
|
|
61
|
+
/**
|
|
62
|
+
* The server-resolved sending identity for this message, from
|
|
63
|
+
* `resolveSendingIdentity`.
|
|
64
|
+
*
|
|
65
|
+
* Supplied, it decides the address and it may refuse the send outright —
|
|
66
|
+
* `fromName` is subordinate to it, because a verdict is the answer to "may
|
|
67
|
+
* this leave, and as whom" and a display name is not. Omitted, every
|
|
68
|
+
* existing caller keeps the behavior it had: the configured platform
|
|
69
|
+
* identity with an optional display name.
|
|
70
|
+
*
|
|
71
|
+
* Callers resolve it from the ORG DOCUMENT, never from request input. An
|
|
72
|
+
* address assembled from a request body is a `From:` override wearing a new
|
|
73
|
+
* name, and the invariant `applyFromName` exists to hold is that the
|
|
74
|
+
* address cannot move off a verified identity.
|
|
75
|
+
*/
|
|
76
|
+
sendingIdentity?: SendingIdentityVerdict | null;
|
|
77
|
+
/**
|
|
78
|
+
* Whose mail this is — see {@link SendingIdentityAudience}.
|
|
79
|
+
*
|
|
80
|
+
* `tenant` says the message belongs to a SITE, and it makes the platform
|
|
81
|
+
* sender unreachable: a tenant message with no resolved identity is refused
|
|
82
|
+
* rather than sent from `aglyn.com`. Pair it with `sendingIdentity` from
|
|
83
|
+
* `hostSendingIdentity(hostId)` and the ordinary path is unchanged; the flag
|
|
84
|
+
* is what decides the behavior when that resolution is missing or refuses.
|
|
85
|
+
*
|
|
86
|
+
* Omitted, a send is platform mail and keeps the configured sender, because
|
|
87
|
+
* that is what the console's own senders are. `email-audience-coverage.spec`
|
|
88
|
+
* sweeps the tenant-owned trees so the omission cannot be an accident there.
|
|
89
|
+
*/
|
|
90
|
+
audience?: SendingIdentityAudience;
|
|
91
|
+
/**
|
|
92
|
+
* Short label for logs, e.g. `'invite'` or `'usage-summary'`. Makes a
|
|
93
|
+
* failure in the runtime logs traceable to the feature that caused it.
|
|
94
|
+
*
|
|
95
|
+
* Since AGL-2407 it is also stamped as a Resend `context` TAG on every
|
|
96
|
+
* send — see `contextTag` below.
|
|
97
|
+
*/
|
|
98
|
+
context?: string;
|
|
99
|
+
/**
|
|
100
|
+
* What the platform send-rate governor is allowed to do to this message
|
|
101
|
+
* (AGL-2409). Omitted, it is derived from `context`: `'campaign'` is a
|
|
102
|
+
* campaign and everything else is transactional, so no existing caller
|
|
103
|
+
* changes and the default is the one that can never be refused.
|
|
104
|
+
*
|
|
105
|
+
* Set it to `'bulk'` ONLY from a resumable sweep — a cron that leaves its
|
|
106
|
+
* subject unstamped and picks it up on the next run. A refusal for a bulk
|
|
107
|
+
* send means "not this hour", and a caller that cannot come back would turn
|
|
108
|
+
* that into a message nobody ever gets.
|
|
109
|
+
*/
|
|
110
|
+
priority?: EmailSendPriority;
|
|
111
|
+
/**
|
|
112
|
+
* Declares this message as MARKETING mail for one site's audience — see
|
|
113
|
+
* `marketing-send.ts` for what that means and why it is one seam.
|
|
114
|
+
*
|
|
115
|
+
* Set it and the message gains, in one place, the three things marketing
|
|
116
|
+
* mail owes: the RFC 8058 unsubscribe header pair plus a visible opt-out
|
|
117
|
+
* link, a check against both suppression lists, and a ceiling on how much
|
|
118
|
+
* one person receives from one site.
|
|
119
|
+
*
|
|
120
|
+
* ONE RECIPIENT. An unsubscribe link is an HMAC over the address it belongs
|
|
121
|
+
* to, and a suppression verdict is about one person — so a marketing send
|
|
122
|
+
* addressed to a list would carry the wrong link for everybody after the
|
|
123
|
+
* first, and would ask the gate about one of them. Callers fan out.
|
|
124
|
+
*/
|
|
125
|
+
marketing?: MarketingSendContext;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The `context` tag, attached to every send (AGL-2407).
|
|
129
|
+
*
|
|
130
|
+
* ## Why this is here and not at 37 call sites
|
|
131
|
+
*
|
|
132
|
+
* Until now `tags` were set by exactly one sender, `campaign-send.ts`, which
|
|
133
|
+
* stamps `hostId` and `campaignId` for the opens/clicks webhook. Everything
|
|
134
|
+
* else went out with NO tags at all, so a bounce on an invite, a password
|
|
135
|
+
* reset, a receipt or a usage summary reached the webhook carrying nothing to
|
|
136
|
+
* identify it, and was dropped.
|
|
137
|
+
*
|
|
138
|
+
* The obvious fix — thread an identifier through every call site — asks 37
|
|
139
|
+
* places to remember, which is the shape that produces the 38th that does
|
|
140
|
+
* not. But `context` is ALREADY threaded through 35 of the 37 for logging,
|
|
141
|
+
* and it is exactly the right value: it names the sender. So the tag is
|
|
142
|
+
* derived here, once, and no caller changes.
|
|
143
|
+
*
|
|
144
|
+
* Resend tag values are restricted to ASCII letters, digits, `_` and `-`;
|
|
145
|
+
* anything else is rejected and would fail the whole send. Every `context` in
|
|
146
|
+
* the tree is already a plain slug, but this is mail delivery — a value that
|
|
147
|
+
* makes the send fail is far worse than a value that is sanitised — so the
|
|
148
|
+
* label is normalised rather than trusted, and a context that sanitises to
|
|
149
|
+
* nothing yields no tag rather than an invalid one.
|
|
150
|
+
*/
|
|
151
|
+
export declare function contextTag(context: string | undefined): EmailTag[];
|
|
152
|
+
/**
|
|
153
|
+
* Why a send did not happen. `unconfigured` and `no-recipient` mean nothing
|
|
154
|
+
* was attempted; `rejected` and `network` mean Resend was called and failed;
|
|
155
|
+
* `rate-limited` is either, and says so in `status`.
|
|
156
|
+
*
|
|
157
|
+
* `rate-limited` (AGL-2409) and `frequency-capped` are the two a caller may
|
|
158
|
+
* reasonably retry unchanged — see {@link isDeferrableSendResult}, which is
|
|
159
|
+
* where that distinction is made once rather than at each sweep.
|
|
160
|
+
*/
|
|
161
|
+
export type SendEmailFailureReason = 'unconfigured' | 'no-recipient'
|
|
162
|
+
/**
|
|
163
|
+
* Resend answered and would not take this message.
|
|
164
|
+
*
|
|
165
|
+
* Per-message and terminal: a malformed payload, an address the provider
|
|
166
|
+
* will not accept, a tag it rejected. A caller that retries it unchanged
|
|
167
|
+
* gets the same answer, so a batch settles the recipient rather than
|
|
168
|
+
* holding a slot open for them.
|
|
169
|
+
*
|
|
170
|
+
* A 429 is deliberately NOT this. See `rate-limited`.
|
|
171
|
+
*/
|
|
172
|
+
| 'rejected' | 'network'
|
|
173
|
+
/**
|
|
174
|
+
* A refusal that is about the RATE, not about this message — so the message
|
|
175
|
+
* is intact and a later attempt sends it.
|
|
176
|
+
*
|
|
177
|
+
* Two sources, which is why it is one value. The platform hourly governor
|
|
178
|
+
* refuses before the network and nothing is attempted (AGL-2409). Resend
|
|
179
|
+
* answers `429` on the wire when requests arrive faster than it accepts
|
|
180
|
+
* them, and `status` is 429 in that case.
|
|
181
|
+
*
|
|
182
|
+
* The provider arm reports here rather than as `rejected` because of what
|
|
183
|
+
* the two mean to a batch. A `rejected` recipient is settled and never
|
|
184
|
+
* addressed again, which is right for an address the provider will not take
|
|
185
|
+
* and wrong for every recipient a 429 touches: nothing about them was
|
|
186
|
+
* refused, the request was simply too soon. Classified as `rejected` a
|
|
187
|
+
* single rate-limited burst silently deletes the rest of a campaign's
|
|
188
|
+
* audience while the campaign reports itself complete — and the `sent`
|
|
189
|
+
* figure stays honest throughout, so no rate on the report can show it.
|
|
190
|
+
*
|
|
191
|
+
* The provider's quota errors — `daily_quota_exceeded`,
|
|
192
|
+
* `monthly_quota_exceeded` — are also 429 and also land here. That is the
|
|
193
|
+
* right answer for the same reason: none of them is a statement about the
|
|
194
|
+
* recipient, and all of them clear with time.
|
|
195
|
+
*/
|
|
196
|
+
| 'rate-limited'
|
|
197
|
+
/**
|
|
198
|
+
* The org selected a custom sending domain and that domain is not verified.
|
|
199
|
+
*
|
|
200
|
+
* Distinct from `unconfigured` because the two need opposite responses: an
|
|
201
|
+
* unconfigured deployment is the operator's to fix, while this is a
|
|
202
|
+
* customer's DNS that is not finished, and the customer is the only person
|
|
203
|
+
* who can finish it. `detail` carries the sentence naming the domain.
|
|
204
|
+
*
|
|
205
|
+
* This is the LAST line of defence, not the visible one. A caller that
|
|
206
|
+
* reaches it has already skipped the check its route should have made, and
|
|
207
|
+
* a refusal seen only here is a log line — which is the shape of the
|
|
208
|
+
* `USAGE_EMAIL_FROM` outage. `performCampaignSend` refuses first, with a
|
|
209
|
+
* `409`, so a person finds out.
|
|
210
|
+
*/
|
|
211
|
+
| 'unverified-domain'
|
|
212
|
+
/**
|
|
213
|
+
* A MARKETING send whose recipient is on a suppression list — they
|
|
214
|
+
* unsubscribed from this site, hard-bounced, or pressed "report spam".
|
|
215
|
+
*
|
|
216
|
+
* Terminal, and the only outcome here a caller must not retry: retrying is
|
|
217
|
+
* the behavior the suppression exists to stop. Distinct from `rejected`
|
|
218
|
+
* because nothing was attempted and nothing failed — this is the control
|
|
219
|
+
* working.
|
|
220
|
+
*/
|
|
221
|
+
| 'suppressed'
|
|
222
|
+
/**
|
|
223
|
+
* A MARKETING send refused because this person has already received their
|
|
224
|
+
* ceiling from this site inside the window.
|
|
225
|
+
*
|
|
226
|
+
* Retryable, unlike `suppressed`: the window rolls. A resumable sweep does
|
|
227
|
+
* not need to do anything about it — its next run asks again.
|
|
228
|
+
*/
|
|
229
|
+
| 'frequency-capped'
|
|
230
|
+
/**
|
|
231
|
+
* A MARKETING send refused because this site has been mailing this person
|
|
232
|
+
* for longer than the sunset window with nothing to show for it.
|
|
233
|
+
*
|
|
234
|
+
* TERMINAL for a sweep, and it sits with `suppressed` rather than with
|
|
235
|
+
* `frequency-capped` for a reason worth stating: the frequency window
|
|
236
|
+
* clears by the passage of time, so waiting works. A sunset clears when the
|
|
237
|
+
* PERSON engages, which more mail from us cannot cause — so a sweep that
|
|
238
|
+
* treated it as deferrable would re-read the same doomed row on every beat
|
|
239
|
+
* forever. Nothing about the recipient has been reduced; the next message
|
|
240
|
+
* after they open anything goes.
|
|
241
|
+
*/
|
|
242
|
+
| 'unengaged';
|
|
243
|
+
export type SendEmailResult = {
|
|
244
|
+
sent: true;
|
|
245
|
+
id: string | null;
|
|
246
|
+
} | {
|
|
247
|
+
sent: false;
|
|
248
|
+
reason: SendEmailFailureReason;
|
|
249
|
+
/** HTTP status, when Resend answered. */
|
|
250
|
+
status?: number;
|
|
251
|
+
/** Resend's error body or the thrown message, trimmed for logs. */
|
|
252
|
+
detail?: string;
|
|
253
|
+
/**
|
|
254
|
+
* `rate-limited` only: the earliest instant a caller may try again —
|
|
255
|
+
* when the platform hourly window rolls, or what the provider's own
|
|
256
|
+
* `retry-after` asked for. A resumable sweep does not need to wait on
|
|
257
|
+
* it; its next scheduled run is the retry.
|
|
258
|
+
*/
|
|
259
|
+
retryAtMs?: number;
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* The retry instant when the platform send-rate governor deferred this
|
|
263
|
+
* message, or `null` for every other outcome (AGL-2409).
|
|
264
|
+
*
|
|
265
|
+
* A FUNCTION rather than `result.reason === 'rate-limited'` at each call site,
|
|
266
|
+
* because `strictNullChecks` is OFF repo-wide and TypeScript will not narrow a
|
|
267
|
+
* boolean-literal discriminant without it: `if (result.sent) … else
|
|
268
|
+
* result.reason` does not compile, in every consumer, for a reason that has
|
|
269
|
+
* nothing to do with this union. One helper is also one place to change if the
|
|
270
|
+
* shape of a deferral ever moves.
|
|
271
|
+
*/
|
|
272
|
+
export declare function rateLimitedRetryAtMs(result: SendEmailResult | null | undefined): number | null;
|
|
273
|
+
/**
|
|
274
|
+
* When the provider says a refused request may be repeated.
|
|
275
|
+
*
|
|
276
|
+
* Read from the two headers Resend documents beside a 429, both in whole
|
|
277
|
+
* seconds: `retry-after` first because it is the direct answer to this
|
|
278
|
+
* question, then `ratelimit-reset`, which names when the window rolls. A
|
|
279
|
+
* response carrying neither falls back to one window.
|
|
280
|
+
*/
|
|
281
|
+
export declare function providerRetryAtMs(headers: {
|
|
282
|
+
get?: (name: string) => string | null;
|
|
283
|
+
} | null | undefined, nowMs?: number): number;
|
|
284
|
+
/**
|
|
285
|
+
* Why a send did not happen, or `null` when it did.
|
|
286
|
+
*
|
|
287
|
+
* The same accessor `rateLimitedRetryAtMs` is, generalized: `strictNullChecks`
|
|
288
|
+
* is OFF repo-wide, so TypeScript will not narrow the union on `result.sent`
|
|
289
|
+
* and reading `result.reason` at a call site does not compile.
|
|
290
|
+
*/
|
|
291
|
+
export declare function sendFailureReason(result: SendEmailResult | null | undefined): SendEmailFailureReason | null;
|
|
292
|
+
/**
|
|
293
|
+
* Whether this outcome is worth coming back for.
|
|
294
|
+
*
|
|
295
|
+
* TRUE only for the two refusals a later attempt can pass: the platform hour
|
|
296
|
+
* rolls, and so does the marketing frequency window. Everything else is
|
|
297
|
+
* either a delivery that happened or a failure a retry repeats — a
|
|
298
|
+
* suppression most of all, since retrying is the exact behavior a suppression
|
|
299
|
+
* exists to stop, and a sunset for the same reason at one remove: it clears
|
|
300
|
+
* when the recipient engages, which no amount of further mail from us brings
|
|
301
|
+
* about.
|
|
302
|
+
*
|
|
303
|
+
* A resumable sweep uses this to decide whether to leave its subject
|
|
304
|
+
* unstamped. Stamping on a deferrable refusal discards a message; NOT
|
|
305
|
+
* stamping on a terminal one re-reads the same doomed row on every beat until
|
|
306
|
+
* it crowds out the work that could succeed. Both are silent, so the
|
|
307
|
+
* distinction lives here instead of at each sweep.
|
|
308
|
+
*
|
|
309
|
+
* A FUNCTION rather than `result.reason === …` at each call site, because
|
|
310
|
+
* `strictNullChecks` is OFF repo-wide and TypeScript will not narrow a
|
|
311
|
+
* boolean-literal discriminant without it — the same reason
|
|
312
|
+
* {@link rateLimitedRetryAtMs} beside it is one.
|
|
313
|
+
*/
|
|
314
|
+
export declare function isDeferrableSendResult(result: SendEmailResult | null | undefined): boolean;
|
|
315
|
+
export interface EmailConfig {
|
|
316
|
+
apiKey: string | undefined;
|
|
317
|
+
from: string | undefined;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Reads the email environment.
|
|
321
|
+
*
|
|
322
|
+
* Deliberately read per call rather than captured at module load: these run
|
|
323
|
+
* in serverless handlers where the module may be evaluated during a build,
|
|
324
|
+
* long before the runtime env exists.
|
|
325
|
+
*/
|
|
326
|
+
export declare function getEmailConfig(): EmailConfig;
|
|
327
|
+
/**
|
|
328
|
+
* True when both `RESEND_API_KEY` and `USAGE_EMAIL_FROM` are present.
|
|
329
|
+
*
|
|
330
|
+
* Callers that answer an HTTP request (rather than firing best-effort mail)
|
|
331
|
+
* use this to return a 501 with an actionable message instead of pretending
|
|
332
|
+
* to have sent something.
|
|
333
|
+
*/
|
|
334
|
+
export declare function isEmailConfigured(): boolean;
|
|
335
|
+
/**
|
|
336
|
+
* Applies a white-label display name to a configured sender while keeping
|
|
337
|
+
* its verified address (White-Label Phase 1). Accepts either a bare address
|
|
338
|
+
* (`noreply@aglyn.com`) or an RFC-5322 `Name <addr>` header and returns
|
|
339
|
+
* `"<fromName>" <addr>`. A blank name, or a value with no extractable
|
|
340
|
+
* address, yields the original `from` untouched — the sender identity is
|
|
341
|
+
* never dropped on the floor.
|
|
342
|
+
*/
|
|
343
|
+
export declare function applyFromName(from: string | undefined, fromName: string | undefined): string | undefined;
|
|
344
|
+
/** A Resend send payload in the provider's own wire shape. */
|
|
345
|
+
export interface ResendSendPayload {
|
|
346
|
+
to?: unknown;
|
|
347
|
+
from?: unknown;
|
|
348
|
+
subject?: unknown;
|
|
349
|
+
[field: string]: unknown;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* The one place that POSTs to Resend's send endpoint, and the last thing
|
|
353
|
+
* standing between a payload and the network.
|
|
354
|
+
*
|
|
355
|
+
* A payload carrying no recipient cannot become a message. Resend answers it
|
|
356
|
+
* `422 missing_required_field`, which costs an API call and then shows up in
|
|
357
|
+
* the vendor dashboard as a red line indistinguishable from mail that
|
|
358
|
+
* genuinely failed to deliver — carrying no subject, no recipient and nothing
|
|
359
|
+
* naming the code that produced it. Diagnosing that means reading a log
|
|
360
|
+
* outside the deployment and guessing. So the refusal happens here, before
|
|
361
|
+
* the fetch, and names the caller's `context`.
|
|
362
|
+
*
|
|
363
|
+
* It throws rather than returning a `SendEmailResult`: this is a programming
|
|
364
|
+
* error, not a delivery outcome. `sendEmail` filters recipients well before
|
|
365
|
+
* it reaches this call, so nothing on the ordinary path can trip it. The
|
|
366
|
+
* guard exists because `RESEND_SEND_ENDPOINT` is exported and any module can
|
|
367
|
+
* therefore reach the send endpoint on its own, bypassing every check
|
|
368
|
+
* `sendEmail` owns.
|
|
369
|
+
*/
|
|
370
|
+
export declare function postResendEmail(apiKey: string, payload: ResendSendPayload, context?: string): Promise<Response>;
|
|
371
|
+
/**
|
|
372
|
+
* Sends one email through Resend.
|
|
373
|
+
*
|
|
374
|
+
* **Never throws and never rejects.** Outbound mail is best-effort across
|
|
375
|
+
* every caller in this codebase — a checkout must not fail because a receipt
|
|
376
|
+
* bounced — so every outcome comes back as a `SendEmailResult` instead. The
|
|
377
|
+
* one thing callers must not do is ignore the result: `sent` is what tells
|
|
378
|
+
* the user whether a message actually went out (AGL-708).
|
|
379
|
+
*
|
|
380
|
+
* When the env vars are missing this warns once per call and returns
|
|
381
|
+
* `{ sent: false, reason: 'unconfigured' }` rather than failing, so local and
|
|
382
|
+
* preview environments keep working without a Resend account.
|
|
383
|
+
*/
|
|
384
|
+
export declare function sendEmail(options: SendEmailOptions): Promise<SendEmailResult>;
|
|
385
|
+
export default sendEmail;
|