@aglyn/shared-util-email 1.0.0-beta.234 → 1.0.0-beta.235
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/mail-provider-resend.js +3 -1
- package/src/lib/mail-provider-resend.js.map +1 -1
- package/src/lib/mail-provider-webhook.d.ts +3 -0
- package/src/lib/mail-provider-webhook.js +6 -1
- package/src/lib/mail-provider-webhook.js.map +1 -1
- package/src/lib/mail-provider.d.ts +2 -0
- package/src/lib/mail-provider.js.map +1 -1
- package/src/lib/retention-emails.d.ts +104 -0
- package/src/lib/retention-emails.js +375 -0
- package/src/lib/retention-emails.js.map +1 -0
- package/src/lib/send-email.d.ts +8 -0
- package/src/lib/send-email.js +21 -3
- package/src/lib/send-email.js.map +1 -1
- package/src/lib/system-email-catalog.js +11 -1
- package/src/lib/system-email-catalog.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/shared-util-email",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.235",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
28
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.235",
|
|
29
|
+
"@aglyn/shared-util-http": "1.0.0-beta.235",
|
|
30
30
|
"@msgpack/msgpack": "^3.1.3",
|
|
31
31
|
"@swc/helpers": "0.5.23"
|
|
32
32
|
},
|
package/src/index.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export * from './lib/host-sender';
|
|
|
27
27
|
export * from './lib/platform-sending-domain';
|
|
28
28
|
export * from './lib/email-health';
|
|
29
29
|
export * from './lib/system-email-catalog';
|
|
30
|
+
export * from './lib/retention-emails';
|
|
30
31
|
export * from './lib/tenant-email-catalog';
|
|
31
32
|
export * from './lib/host-email-render';
|
|
32
33
|
export * from './lib/email-render';
|
package/src/index.js
CHANGED
|
@@ -31,6 +31,7 @@ export * from "./lib/host-sender.js";
|
|
|
31
31
|
export * from "./lib/platform-sending-domain.js";
|
|
32
32
|
export * from "./lib/email-health.js";
|
|
33
33
|
export * from "./lib/system-email-catalog.js";
|
|
34
|
+
export * from "./lib/retention-emails.js";
|
|
34
35
|
export * from "./lib/tenant-email-catalog.js";
|
|
35
36
|
export * from "./lib/host-email-render.js";
|
|
36
37
|
export * from "./lib/email-render.js";
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/shared/util/email/src/index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport * from './lib/send-email'\n// Who carries the mail (AGL-3080): the provider contract, and the selection\n// of the deployment's provider from the built-ins and any a plugin\n// registered. The providers themselves are imported by their own paths —\n// `@aglyn/shared-util-email/mail-provider-resend`, `…/mail-provider-webhook`\n// — because nothing outside the seam should name one.\nexport * from './lib/mail-provider'\nexport * from './lib/mail-providers'\nexport * from './lib/send-rate'\nexport * from './lib/sender-reputation'\nexport * from './lib/send-ceilings'\nexport * from './lib/sending-domain'\nexport * from './lib/tracking-host'\nexport * from './lib/sending-mailbox'\nexport * from './lib/host-sender'\nexport * from './lib/platform-sending-domain'\nexport * from './lib/email-health'\nexport * from './lib/system-email-catalog'\nexport * from './lib/tenant-email-catalog'\nexport * from './lib/host-email-render'\nexport * from './lib/email-render'\nexport * from './lib/text-email-html'\nexport * from './lib/marketing-send'\n// The List-Unsubscribe setting sequences and campaigns share (AGL-3307).\nexport * from './lib/list-unsubscribe'\nexport * from './lib/email-delivery-events'\n// Received mail in the platform's vocabulary, which the CRM's capture route\n// files from (AGL-2657); the provider reads it.\n// Its neighbor `./lib/svix-signature` is deliberately NOT re-exported here:\n// it holds a `crypto` HMAC, and this barrel is reached from the browser\n// through `@aglyn/shared-ui-email-campaigns`, so a webhook imports it by its\n// own path — `@aglyn/shared-util-email/svix-signature`.\nexport * from './lib/received-email'\nexport * from './lib/email-revenue-window'\n// Exported so the console drift guard can pin it against @aglyn/aglyn.\nexport * from './lib/email-media-src'\n// Same arrangement, same reason (AGL-1223): a copy of `decodeStoredNodes`\n// this scope:shared lib may not import, pinned by `email-nodes-drift.spec.ts`.\nexport * from './lib/stored-email-nodes'\nexport * from './lib/email-merge'\n// The deliverability engine every send path asks (AGL-3328): the gateway an\n// MX names and its ledger, the bounce reader, and the address verdicts. Pure\n// — the DNS lookup and the store are injected by `@aglyn/tenant-data-admin`,\n// so nothing here reaches `node:dns` from the browser.\nexport * from './lib/mail-gateway'\nexport * from './lib/mail-bounce'\nexport * from './lib/email-deliverability'\n// The sender's side of the same engine (AGL-3328): SPF, DKIM and DMARC for\n// the domain mail leaves from, and whether they align. Pure as well; the\n// lookups are `@aglyn/tenant-data-admin`'s.\nexport * from './lib/sender-readiness'\n// The outbound phishing screen a young workspace's campaigns and automated\n// email pass (AGL-3356). Pure; the hold it leads to is `tenant-data-admin`'s.\nexport * from './lib/outbound-phishing-screen'\n// The send seam's gate for that screen — the store a hold writes is injected\n// by `@aglyn/tenant-data-admin`, like the governor's.\nexport * from './lib/outbound-screen-gate'\n// The same screen read over a published page (AGL-3356): lookalike links and\n// embeds, author-defined credential fields, a brand's call to action.\nexport * from './lib/hosted-page-screen'\n// The reputation lookup every foreign link a page, an email or a redirect\n// points at passes (AGL-3451). Pure; the lookup is injected by\n// `@aglyn/tenant-data-admin`, which holds the credential and the cache.\nexport * from './lib/link-reputation'\n// The site's theme palette as plain data (AGL-3370), so a palette token an\n// author picked resolves in mail. Built without MUI; `shared-ui-theme`'s\n// parity spec holds it equal to the theme.\nexport * from './lib/email-palette'\nexport * from './lib/email-design-chrome'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,cAAc,sBAAkB;AAChC,4EAA4E;AAC5E,mEAAmE;AACnE,yEAAyE;AACzE,6EAA6E;AAC7E,sDAAsD;AACtD,cAAc,yBAAqB;AACnC,cAAc,0BAAsB;AACpC,cAAc,qBAAiB;AAC/B,cAAc,6BAAyB;AACvC,cAAc,yBAAqB;AACnC,cAAc,0BAAsB;AACpC,cAAc,yBAAqB;AACnC,cAAc,2BAAuB;AACrC,cAAc,uBAAmB;AACjC,cAAc,mCAA+B;AAC7C,cAAc,wBAAoB;AAClC,cAAc,gCAA4B;AAC1C,cAAc,gCAA4B;AAC1C,cAAc,6BAAyB;AACvC,cAAc,wBAAoB;AAClC,cAAc,2BAAuB;AACrC,cAAc,0BAAsB;AACpC,yEAAyE;AACzE,cAAc,4BAAwB;AACtC,cAAc,iCAA6B;AAC3C,4EAA4E;AAC5E,gDAAgD;AAChD,4EAA4E;AAC5E,wEAAwE;AACxE,6EAA6E;AAC7E,wDAAwD;AACxD,cAAc,0BAAsB;AACpC,cAAc,gCAA4B;AAC1C,uEAAuE;AACvE,cAAc,2BAAuB;AACrC,0EAA0E;AAC1E,+EAA+E;AAC/E,cAAc,8BAA0B;AACxC,cAAc,uBAAmB;AACjC,4EAA4E;AAC5E,6EAA6E;AAC7E,6EAA6E;AAC7E,uDAAuD;AACvD,cAAc,wBAAoB;AAClC,cAAc,uBAAmB;AACjC,cAAc,gCAA4B;AAC1C,2EAA2E;AAC3E,yEAAyE;AACzE,4CAA4C;AAC5C,cAAc,4BAAwB;AACtC,2EAA2E;AAC3E,8EAA8E;AAC9E,cAAc,oCAAgC;AAC9C,6EAA6E;AAC7E,sDAAsD;AACtD,cAAc,gCAA4B;AAC1C,6EAA6E;AAC7E,sEAAsE;AACtE,cAAc,8BAA0B;AACxC,0EAA0E;AAC1E,+DAA+D;AAC/D,wEAAwE;AACxE,cAAc,2BAAuB;AACrC,2EAA2E;AAC3E,yEAAyE;AACzE,2CAA2C;AAC3C,cAAc,yBAAqB;AACnC,cAAc,+BAA2B"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/shared/util/email/src/index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport * from './lib/send-email'\n// Who carries the mail (AGL-3080): the provider contract, and the selection\n// of the deployment's provider from the built-ins and any a plugin\n// registered. The providers themselves are imported by their own paths —\n// `@aglyn/shared-util-email/mail-provider-resend`, `…/mail-provider-webhook`\n// — because nothing outside the seam should name one.\nexport * from './lib/mail-provider'\nexport * from './lib/mail-providers'\nexport * from './lib/send-rate'\nexport * from './lib/sender-reputation'\nexport * from './lib/send-ceilings'\nexport * from './lib/sending-domain'\nexport * from './lib/tracking-host'\nexport * from './lib/sending-mailbox'\nexport * from './lib/host-sender'\nexport * from './lib/platform-sending-domain'\nexport * from './lib/email-health'\nexport * from './lib/system-email-catalog'\nexport * from './lib/retention-emails'\nexport * from './lib/tenant-email-catalog'\nexport * from './lib/host-email-render'\nexport * from './lib/email-render'\nexport * from './lib/text-email-html'\nexport * from './lib/marketing-send'\n// The List-Unsubscribe setting sequences and campaigns share (AGL-3307).\nexport * from './lib/list-unsubscribe'\nexport * from './lib/email-delivery-events'\n// Received mail in the platform's vocabulary, which the CRM's capture route\n// files from (AGL-2657); the provider reads it.\n// Its neighbor `./lib/svix-signature` is deliberately NOT re-exported here:\n// it holds a `crypto` HMAC, and this barrel is reached from the browser\n// through `@aglyn/shared-ui-email-campaigns`, so a webhook imports it by its\n// own path — `@aglyn/shared-util-email/svix-signature`.\nexport * from './lib/received-email'\nexport * from './lib/email-revenue-window'\n// Exported so the console drift guard can pin it against @aglyn/aglyn.\nexport * from './lib/email-media-src'\n// Same arrangement, same reason (AGL-1223): a copy of `decodeStoredNodes`\n// this scope:shared lib may not import, pinned by `email-nodes-drift.spec.ts`.\nexport * from './lib/stored-email-nodes'\nexport * from './lib/email-merge'\n// The deliverability engine every send path asks (AGL-3328): the gateway an\n// MX names and its ledger, the bounce reader, and the address verdicts. Pure\n// — the DNS lookup and the store are injected by `@aglyn/tenant-data-admin`,\n// so nothing here reaches `node:dns` from the browser.\nexport * from './lib/mail-gateway'\nexport * from './lib/mail-bounce'\nexport * from './lib/email-deliverability'\n// The sender's side of the same engine (AGL-3328): SPF, DKIM and DMARC for\n// the domain mail leaves from, and whether they align. Pure as well; the\n// lookups are `@aglyn/tenant-data-admin`'s.\nexport * from './lib/sender-readiness'\n// The outbound phishing screen a young workspace's campaigns and automated\n// email pass (AGL-3356). Pure; the hold it leads to is `tenant-data-admin`'s.\nexport * from './lib/outbound-phishing-screen'\n// The send seam's gate for that screen — the store a hold writes is injected\n// by `@aglyn/tenant-data-admin`, like the governor's.\nexport * from './lib/outbound-screen-gate'\n// The same screen read over a published page (AGL-3356): lookalike links and\n// embeds, author-defined credential fields, a brand's call to action.\nexport * from './lib/hosted-page-screen'\n// The reputation lookup every foreign link a page, an email or a redirect\n// points at passes (AGL-3451). Pure; the lookup is injected by\n// `@aglyn/tenant-data-admin`, which holds the credential and the cache.\nexport * from './lib/link-reputation'\n// The site's theme palette as plain data (AGL-3370), so a palette token an\n// author picked resolves in mail. Built without MUI; `shared-ui-theme`'s\n// parity spec holds it equal to the theme.\nexport * from './lib/email-palette'\nexport * from './lib/email-design-chrome'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,cAAc,sBAAkB;AAChC,4EAA4E;AAC5E,mEAAmE;AACnE,yEAAyE;AACzE,6EAA6E;AAC7E,sDAAsD;AACtD,cAAc,yBAAqB;AACnC,cAAc,0BAAsB;AACpC,cAAc,qBAAiB;AAC/B,cAAc,6BAAyB;AACvC,cAAc,yBAAqB;AACnC,cAAc,0BAAsB;AACpC,cAAc,yBAAqB;AACnC,cAAc,2BAAuB;AACrC,cAAc,uBAAmB;AACjC,cAAc,mCAA+B;AAC7C,cAAc,wBAAoB;AAClC,cAAc,gCAA4B;AAC1C,cAAc,4BAAwB;AACtC,cAAc,gCAA4B;AAC1C,cAAc,6BAAyB;AACvC,cAAc,wBAAoB;AAClC,cAAc,2BAAuB;AACrC,cAAc,0BAAsB;AACpC,yEAAyE;AACzE,cAAc,4BAAwB;AACtC,cAAc,iCAA6B;AAC3C,4EAA4E;AAC5E,gDAAgD;AAChD,4EAA4E;AAC5E,wEAAwE;AACxE,6EAA6E;AAC7E,wDAAwD;AACxD,cAAc,0BAAsB;AACpC,cAAc,gCAA4B;AAC1C,uEAAuE;AACvE,cAAc,2BAAuB;AACrC,0EAA0E;AAC1E,+EAA+E;AAC/E,cAAc,8BAA0B;AACxC,cAAc,uBAAmB;AACjC,4EAA4E;AAC5E,6EAA6E;AAC7E,6EAA6E;AAC7E,uDAAuD;AACvD,cAAc,wBAAoB;AAClC,cAAc,uBAAmB;AACjC,cAAc,gCAA4B;AAC1C,2EAA2E;AAC3E,yEAAyE;AACzE,4CAA4C;AAC5C,cAAc,4BAAwB;AACtC,2EAA2E;AAC3E,8EAA8E;AAC9E,cAAc,oCAAgC;AAC9C,6EAA6E;AAC7E,sDAAsD;AACtD,cAAc,gCAA4B;AAC1C,6EAA6E;AAC7E,sEAAsE;AACtE,cAAc,8BAA0B;AACxC,0EAA0E;AAC1E,+DAA+D;AAC/D,wEAAwE;AACxE,cAAc,2BAAuB;AACrC,2EAA2E;AAC3E,yEAAyE;AACzE,2CAA2C;AAC3C,cAAc,yBAAqB;AACnC,cAAc,+BAA2B"}
|
|
@@ -80,7 +80,7 @@ function setting(env, name) {
|
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
/** One message as Resend's send endpoint takes it. */ export function resendSendPayload(message) {
|
|
83
|
-
var _message_tags;
|
|
83
|
+
var _message_tags, _message_bcc;
|
|
84
84
|
return _extends({
|
|
85
85
|
from: message.from,
|
|
86
86
|
to: message.to,
|
|
@@ -95,6 +95,8 @@ function setting(env, name) {
|
|
|
95
95
|
tags: message.tags
|
|
96
96
|
} : {}, message.replyTo ? {
|
|
97
97
|
reply_to: message.replyTo
|
|
98
|
+
} : {}, ((_message_bcc = message.bcc) == null ? void 0 : _message_bcc.length) ? {
|
|
99
|
+
bcc: message.bcc
|
|
98
100
|
} : {});
|
|
99
101
|
}
|
|
100
102
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/mail-provider-resend.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 * RESEND, as one {@link MailProvider} — the only module in the platform that\n * knows Resend's endpoints, payloads, event names and error names.\n *\n * It is the provider a deployment gets when it names none, which is how\n * every deployment that set `RESEND_API_KEY` before the contract existed\n * keeps sending exactly as it did. Two credentials, on purpose:\n *\n * - `RESEND_API_KEY` sends. It is provisioned sending-scoped, so a leaked\n * copy cannot enumerate everyone the platform ever wrote to.\n * - `RESEND_READ_API_KEY` is full-access and only ever reads: history, one\n * message's body, received mail and the account's sending domains. A\n * sending-scoped key answers every such read `401 restricted_api_key`.\n *\n * Pure but for the `fetch` calls, each inside a function that must be called\n * to do anything, so a client component that reaches this module through the\n * library's barrel executes nothing on load.\n */\n\nimport {\n bareSenderAddress,\n normalizeEventTags,\n type EmailDeliveryEvent,\n type EmailDeliveryEventType,\n type EmailDeliveryHistorySource,\n type EmailDeliveryMessage,\n type EmailDeliveryMessageSource,\n type EmailDeliverySnapshot,\n} from './email-delivery-events'\nimport type { EmailCredentialReport } from './email-health'\nimport { readBounceText } from './mail-bounce'\nimport {\n providerRetryAtMs,\n type MailInboundEvent,\n type MailProvider,\n type MailProviderMessage,\n type MailProviderReads,\n type MailProviderSendResult,\n type MailSendingDomain,\n} from './mail-provider'\nimport type { ReceivedEmail, ReceivedEmailSource } from './received-email'\n\n/** The slug stored on every delivery record this provider produces. */\nexport const RESEND_PROVIDER_ID = 'resend'\n\n/** The sending key's environment variable. */\nexport const RESEND_SEND_KEY_SETTING = 'RESEND_API_KEY'\n\n/** The full-access read key's environment variable. */\nexport const RESEND_READ_KEY_SETTING = 'RESEND_READ_API_KEY'\n\ntype Env = Record<string, string | undefined>\n\nfunction setting(env: Env, name: string): string {\n return String(env[name] ?? '').trim()\n}\n\n/*==========================================\n * SENDING.\n *=========================================*/\n\n/** The send endpoint. */\nexport const RESEND_SEND_ENDPOINT = 'https://api.resend.com/emails'\n\n/** A Resend send payload in the provider's own wire shape. */\nexport interface ResendSendPayload {\n to?: unknown\n from?: unknown\n subject?: unknown\n [field: string]: unknown\n}\n\n/**\n * The one place that POSTs to Resend's send endpoint, and the last thing\n * standing between a payload and the network.\n *\n * A payload carrying no recipient cannot become a message. Resend answers it\n * `422 missing_required_field`, which costs an API call and then shows up in\n * the vendor dashboard as a red line indistinguishable from mail that\n * genuinely failed to deliver — carrying no subject, no recipient and nothing\n * naming the code that produced it. Diagnosing that means reading a log\n * outside the deployment and guessing. So the refusal happens here, before\n * the fetch, and names the caller's `context`.\n *\n * It throws rather than returning a result: this is a programming error,\n * not a delivery outcome. `sendEmail` filters recipients well before it\n * reaches this call, so nothing on the ordinary path can trip it. The guard\n * exists because `RESEND_SEND_ENDPOINT` is exported and any module can\n * therefore reach the send endpoint on its own, bypassing every check\n * `sendEmail` owns.\n */\nexport async function postResendEmail(\n apiKey: string,\n payload: ResendSendPayload,\n context?: string,\n): Promise<Response> {\n const raw = payload?.to\n const recipients = (Array.isArray(raw) ? raw : raw == null ? [] : [raw])\n .map((address) => String(address ?? '').trim())\n .filter(Boolean)\n if (!recipients.length) {\n throw new Error(\n `${context ? `${context} ` : ''}send refused before the network — a ` +\n 'Resend payload with no `to` field cannot become a message, and the ' +\n 'attempt would surface only as a 422 in the Resend dashboard',\n )\n }\n\n return fetch(RESEND_SEND_ENDPOINT, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n })\n}\n\n/** One message as Resend's send endpoint takes it. */\nexport function resendSendPayload(message: MailProviderMessage): ResendSendPayload {\n return {\n from: message.from,\n to: message.to,\n subject: message.subject,\n ...(message.text ? { text: message.text } : {}),\n ...(message.html ? { html: message.html } : {}),\n ...(message.headers && Object.keys(message.headers).length\n ? { headers: message.headers }\n : {}),\n ...(message.tags?.length ? { tags: message.tags } : {}),\n ...(message.replyTo ? { reply_to: message.replyTo } : {}),\n }\n}\n\n/**\n * Sends one message and reads the answer.\n *\n * A 429 is a request to slow down, not a statement about the recipient, so\n * it comes back `rateLimited` with the wait Resend asked for. Its quota\n * errors — `daily_quota_exceeded`, `monthly_quota_exceeded` — are 429s as\n * well, and clear with time the same way.\n */\nasync function sendThroughResend(\n apiKey: string,\n message: MailProviderMessage,\n context?: string,\n): Promise<MailProviderSendResult> {\n const response = await postResendEmail(apiKey, resendSendPayload(message), context)\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n if (response.status === 429) {\n return {\n accepted: false,\n rateLimited: true,\n retryAtMs: providerRetryAtMs(response.headers),\n status: response.status,\n detail,\n }\n }\n return { accepted: false, rateLimited: false, status: response.status, detail }\n }\n const body = (await response.json().catch(() => null)) as { id?: string } | null\n return { accepted: true, id: body?.id ?? null }\n}\n\n/*==========================================\n * THE CREDENTIAL PROBE.\n *=========================================*/\n\n/**\n * Where the credential probe and the domain read ask their questions.\n *\n * Deliberately NOT the send endpoint. A probe aimed at `/emails` is a send\n * attempt however empty its body is: it consumes an API call, and Resend\n * records it in the account's logs as a `422` on `POST /emails` with no\n * recipient, no subject and nothing identifying the caller — a line an\n * operator reading that dashboard has to treat as failed mail. A domain read\n * cannot create a message and cannot be mistaken for one.\n */\nexport const RESEND_DOMAINS_ENDPOINT = 'https://api.resend.com/domains'\n\n/**\n * Error names that mean the key itself was not accepted, as opposed to a key\n * that authenticated and merely lacks read scope. Matched by NAME, not\n * status: `401` and `403` each cover both meanings.\n */\nconst REFUSED_KEY_ERRORS = new Set([\n 'missing_api_key',\n 'validation_error',\n 'suspended_api_key',\n])\n\n/**\n * Error names that mean the key authenticated and was then denied this\n * particular read. A sending-scoped key — the shape Aglyn provisions — always\n * lands here, and reaching this answer at all required Resend to recognize\n * the credential, which is exactly what the probe is asking.\n */\nconst AUTHENTICATED_BUT_UNSCOPED_ERRORS = new Set([\n 'restricted_api_key',\n 'invalid_permission',\n])\n\n/** The `name` Resend puts on an error body, or `''` for anything else. */\nfunction resendErrorName(body: string): string {\n try {\n const parsed = JSON.parse(body) as { name?: unknown }\n return typeof parsed?.name === 'string' ? parsed.name : ''\n } catch {\n return ''\n }\n}\n\n/**\n * Whether the sending key is accepted — without sending anything to anybody,\n * and without leaving anything behind that reads as failed mail.\n *\n * How: a `GET` of the domains collection. The question is only ever \"does\n * Resend recognize this credential\", so the probe reads the ERROR NAME rather\n * than the status, because `401` and `403` each carry both meanings:\n *\n * - `2xx` — the key is accepted and has read scope → `ok`\n * - `restricted_api_key` / `invalid_permission` — Resend authenticated the\n * key and then denied it this read. A sending-scoped key always answers\n * this way, and getting the answer proves the credential works → `ok`\n * - `missing_api_key` / `validation_error` / `suspended_api_key` — the key\n * itself was refused → `invalid-key`\n * - anything else → `unknown`\n *\n * An unrecognized rejection is `unknown`, never `invalid-key`: this feeds a\n * staff diagnostics screen whose whole value is that a red line means\n * something, and a shape we have not seen before is not evidence that a\n * working key is broken.\n */\nexport async function checkResendCredentials(\n apiKey: string,\n): Promise<EmailCredentialReport> {\n if (!apiKey) return { status: 'unconfigured' }\n try {\n const response = await fetch(RESEND_DOMAINS_ENDPOINT, {\n method: 'GET',\n headers: { Authorization: `Bearer ${apiKey}` },\n })\n const detail = (await response.text().catch(() => '')).slice(0, 300)\n\n if (response.status >= 200 && response.status < 300) {\n return { status: 'ok', probeStatus: response.status }\n }\n if (response.status === 401 || response.status === 403) {\n const name = resendErrorName(detail)\n if (AUTHENTICATED_BUT_UNSCOPED_ERRORS.has(name)) {\n return { status: 'ok', probeStatus: response.status }\n }\n if (REFUSED_KEY_ERRORS.has(name)) {\n return { status: 'invalid-key', probeStatus: response.status, detail }\n }\n }\n return { status: 'unknown', probeStatus: response.status, detail }\n } catch (error) {\n return {\n status: 'unknown',\n detail: String((error as Error)?.message ?? error).slice(0, 300),\n }\n }\n}\n\n/**\n * Every sending domain on the account, with its verification and tracking\n * state. Throws with the status when Resend does not answer 2xx, so a caller\n * never reads \"I could not look\" as \"there is nothing there\".\n */\nexport async function readResendSendingDomains(\n apiKey: string,\n endpoint: string = RESEND_DOMAINS_ENDPOINT,\n): Promise<MailSendingDomain[]> {\n const response = await fetch(endpoint, {\n method: 'GET',\n headers: { Authorization: `Bearer ${apiKey}` },\n })\n if (response.status < 200 || response.status >= 300) {\n throw new Error(`The provider answered ${response.status} to the domain read.`)\n }\n const payload = (await response.json().catch(() => null)) as\n | {\n data?: Array<{\n name?: unknown\n status?: unknown\n click_tracking?: unknown\n open_tracking?: unknown\n }>\n }\n | null\n /**\n * A tri-state, and the third state matters: a listing that does not carry\n * the field must not be reported as the field being false. Off is a fault\n * somebody should fix; unknown is a question this read could not ask.\n */\n const flag = (value: unknown): boolean | null =>\n typeof value === 'boolean' ? value : null\n const domains: MailSendingDomain[] = []\n for (const row of payload?.data ?? []) {\n const name = String(row?.name ?? '').toLowerCase()\n if (!name) continue\n domains.push({\n name,\n status: String(row?.status ?? 'unknown'),\n clickTracking: flag(row?.click_tracking),\n openTracking: flag(row?.open_tracking),\n })\n }\n return domains\n}\n\n/*==========================================\n * THE DELIVERY FEED.\n *=========================================*/\n\n/** Epoch ms from an ISO string or a number, or `null` when unreadable. */\nfunction eventTimeMs(value: unknown): number | null {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n const parsed = Date.parse(String(value ?? ''))\n return Number.isFinite(parsed) ? parsed : null\n}\n\nfunction normalizeBounceType(\n value: unknown,\n): EmailDeliveryEvent['bounceType'] {\n const lowered = String(value ?? '')\n .trim()\n .toLowerCase()\n if (lowered === 'permanent') return 'permanent'\n if (lowered === 'transient') return 'transient'\n return lowered ? 'undetermined' : null\n}\n\n/** The Resend event names we understand, mapped onto ours. */\nconst RESEND_EVENT_TYPES: Record<string, EmailDeliveryEventType> = {\n 'email.sent': 'sent',\n 'email.delivered': 'delivered',\n 'email.delivery_delayed': 'delayed',\n 'email.opened': 'opened',\n 'email.clicked': 'clicked',\n 'email.bounced': 'bounced',\n 'email.complained': 'complained',\n 'email.failed': 'failed',\n}\n\n/**\n * Turns one Resend webhook payload into zero or more of our events.\n *\n * One event per recipient, not per message: a send addressed to three people\n * produces one webhook, and a staff view keyed on a person has to be able to\n * find it under each of them.\n *\n * Returns an empty array for anything unrecognised — a contact or domain\n * event, an inbound `email.received`, a type added after this was written.\n * Silence rather than a throw, because a webhook handler that 500s on an\n * unfamiliar event teaches the provider to retry it forever.\n *\n * @param payload The parsed webhook body.\n * @param receivedAtMs Fallback timestamp for a payload that carries none.\n */\nexport function normalizeResendDeliveryEvents(\n payload: unknown,\n receivedAtMs: number,\n): EmailDeliveryEvent[] {\n const event = (payload ?? {}) as Record<string, any>\n const type = RESEND_EVENT_TYPES[String(event.type ?? '')]\n if (!type) return []\n\n const data = (event.data ?? {}) as Record<string, any>\n const providerMessageId = String(data.email_id ?? data.id ?? '').trim()\n if (!providerMessageId) return []\n\n const recipients = (Array.isArray(data.to) ? data.to : [data.to])\n .map((address: unknown) => String(address ?? '').trim().toLowerCase())\n .filter((address: string) => address.includes('@'))\n if (!recipients.length) return []\n\n // The per-state timestamp when the provider gives one, because an open\n // three days after the send is the whole point of recording an open.\n const at =\n eventTimeMs(data.click?.timestamp) ??\n eventTimeMs(data.open?.timestamp) ??\n eventTimeMs(event.created_at) ??\n eventTimeMs(data.created_at) ??\n receivedAtMs\n\n const subject = String(data.subject ?? '').trim() || null\n const tags = normalizeEventTags(data.tags)\n const from = bareSenderAddress(data.from)\n // The provider folds a DSN's fields into one sentence; the structured\n // ones are read first where a payload carries them.\n const bounceText =\n type === 'bounced'\n ? readBounceText(\n [data.bounce?.diagnosticCode, data.bounce?.diagnostic_code, data.bounce?.remoteMta, data.bounce?.remote_mta, data.bounce?.message]\n .filter((part: unknown) => typeof part === 'string' && part.trim())\n .join(' '),\n )\n : null\n\n return recipients.map((to: string) => ({\n type,\n at,\n provider: RESEND_PROVIDER_ID,\n providerMessageId,\n to,\n subject,\n context: tags['context'] || null,\n tags,\n link: type === 'clicked' ? String(data.click?.link ?? '') || null : null,\n bounceType: type === 'bounced' ? normalizeBounceType(data.bounce?.type) : null,\n detail:\n String(data.bounce?.message ?? data.failed?.reason ?? '').trim() || null,\n ...(from ? { from } : {}),\n ...(bounceText?.status ? { bounceStatus: bounceText.status } : {}),\n ...(bounceText?.remoteMta ? { remoteMta: bounceText.remoteMta } : {}),\n }))\n}\n\n/*==========================================\n * THE ACCOUNT'S HISTORY.\n *\n * Resend's list endpoint reports a single `last_event` per message and no\n * engagement detail, so a snapshot can say \"this was opened at least once\"\n * and can never say \"three times\" — see `EmailDeliverySnapshot`.\n *=========================================*/\n\n/**\n * A `last_event` string, mapped onto our lifecycle.\n *\n * Deliberately the bare state names rather than the `email.*` event names:\n * the list endpoint reports `\"delivered\"`, the webhook reports\n * `\"email.delivered\"`, and they are two different vocabularies for one\n * concept.\n */\nconst RESEND_LAST_EVENTS: Record<string, EmailDeliveryEventType> = {\n sent: 'sent',\n delivered: 'delivered',\n delivery_delayed: 'delayed',\n opened: 'opened',\n clicked: 'clicked',\n bounced: 'bounced',\n complained: 'complained',\n failed: 'failed',\n canceled: 'failed',\n queued: 'sent',\n scheduled: 'sent',\n}\n\n/**\n * Turns one entry from Resend's `GET /emails` list into zero or more\n * snapshots — one per recipient, for the same reason the event adapter fans\n * out: the staff view is keyed on a person.\n *\n * An unrecognised `last_event` falls back to `sent` rather than being\n * dropped. The message demonstrably exists and was addressed to somebody, and\n * \"we sent this and cannot characterise what happened next\" is a far more\n * useful row than no row — which is the state that sent a staffer to the\n * vendor dashboard in the first place.\n */\nexport function normalizeResendSentEmails(\n raw: unknown,\n): EmailDeliverySnapshot[] {\n const record = (raw ?? {}) as Record<string, any>\n const providerMessageId = String(record.id ?? '').trim()\n if (!providerMessageId) return []\n\n const recipients = (Array.isArray(record.to) ? record.to : [record.to])\n .map((address: unknown) => String(address ?? '').trim().toLowerCase())\n .filter((address: string) => address.includes('@'))\n if (!recipients.length) return []\n\n const parsed = Date.parse(String(record.created_at ?? ''))\n const sentAt = Number.isFinite(parsed) ? parsed : 0\n // A snapshot with no timestamp cannot be ordered, and the log's read drops\n // any document missing its sort key — so it is refused rather than written\n // somewhere nothing will look for it.\n if (!sentAt) return []\n\n const status =\n RESEND_LAST_EVENTS[\n String(record.last_event ?? '')\n .trim()\n .toLowerCase()\n ] ?? 'sent'\n\n return recipients.map((to: string) => ({\n provider: RESEND_PROVIDER_ID,\n providerMessageId,\n to,\n subject: String(record.subject ?? '').trim() || null,\n sentAt,\n status,\n }))\n}\n\n/** The list endpoint. Paginates with `after=<id>`; caps at 100. */\nexport const RESEND_EMAILS_ENDPOINT = 'https://api.resend.com/emails'\n\n/**\n * {@link EmailDeliveryHistorySource} for Resend. Needs the full-access key.\n *\n * The list endpoint takes `limit`, `after` and `before` and no recipient\n * filter, which is why the source is a cursor-paged sweep.\n */\nexport function resendDeliveryHistorySource(\n apiKey: string,\n): EmailDeliveryHistorySource {\n return async ({ cursor, limit } = {}) => {\n const params = new URLSearchParams({\n limit: String(Math.min(Math.max(1, limit ?? 100), 100)),\n })\n if (cursor) params.set('after', cursor)\n const response = await fetch(`${RESEND_EMAILS_ENDPOINT}?${params}`, {\n headers: { Authorization: `Bearer ${apiKey}` },\n })\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n throw new Error(\n `email history read failed: HTTP ${response.status} ${detail.slice(0, 200)}`,\n )\n }\n const body = (await response.json()) as {\n data?: unknown[]\n has_more?: boolean\n }\n const entries = Array.isArray(body?.data) ? body.data : []\n const snapshots = entries.flatMap((entry) => normalizeResendSentEmails(entry))\n // The cursor is the LAST RAW entry's id, not the last snapshot's: a page\n // whose final entry fanned out to zero snapshots (no recipient, no\n // timestamp) would otherwise rewind the cursor to an earlier message and\n // loop over the same page forever.\n const lastId = String(\n (entries[entries.length - 1] as { id?: unknown })?.id ?? '',\n ).trim()\n return {\n snapshots,\n nextCursor: body?.has_more && lastId ? lastId : null,\n }\n }\n}\n\nfunction addressList(raw: unknown): string[] {\n return (Array.isArray(raw) ? raw : raw == null ? [] : [raw])\n .map((address) => String(address ?? '').trim())\n .filter(Boolean)\n}\n\n/** Resend's `GET /emails/:id` payload, in our vocabulary. */\nexport function normalizeResendMessage(raw: unknown): EmailDeliveryMessage | null {\n const record = (raw ?? {}) as Record<string, any>\n const providerMessageId = String(record.id ?? '').trim()\n if (!providerMessageId) return null\n const parsed = Date.parse(String(record.created_at ?? ''))\n return {\n provider: RESEND_PROVIDER_ID,\n providerMessageId,\n to: addressList(record.to),\n cc: addressList(record.cc),\n bcc: addressList(record.bcc),\n from: String(record.from ?? '').trim() || null,\n replyTo: addressList(record.reply_to).length\n ? addressList(record.reply_to)\n : null,\n subject: String(record.subject ?? '').trim() || null,\n // Empty string is NOT null here, and the difference is the point: a\n // message that went out text-only really does have an empty HTML part,\n // and a reader has to be able to tell that from \"we could not fetch it\".\n html: typeof record.html === 'string' ? record.html : null,\n text: typeof record.text === 'string' ? record.text : null,\n sentAt: Number.isFinite(parsed) ? parsed : null,\n status:\n RESEND_LAST_EVENTS[\n String(record.last_event ?? '')\n .trim()\n .toLowerCase()\n ] ?? null,\n }\n}\n\n/** Resend's single-message endpoint. Needs the full-access key. */\nexport function resendDeliveryMessageSource(\n apiKey: string,\n): EmailDeliveryMessageSource {\n return async (providerMessageId: string) => {\n const response = await fetch(\n `${RESEND_EMAILS_ENDPOINT}/${encodeURIComponent(providerMessageId)}`,\n { headers: { Authorization: `Bearer ${apiKey}` } },\n )\n // A message the provider has aged out is a 404, and that is an ANSWER —\n // \"we know this was sent and the body is gone\" — not a failure to report\n // as an error the staffer must act on.\n if (response.status === 404) return null\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n throw new Error(\n `message read failed: HTTP ${response.status} ${detail.slice(0, 200)}`,\n )\n }\n return normalizeResendMessage(await response.json())\n }\n}\n\n/*==========================================\n * RECEIVED MAIL.\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.\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\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 what it is looking for in\n * the 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/**\n * {@link ReceivedEmailSource} for Resend. Needs the full-access key. A `404`\n * is `null` — the message expired or never existed — and anything else\n * throws with the status, so the caller decides whether the provider should\n * 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\n/** A received-mail notification, or `null` for any other event. */\nexport function resendInboundEvent(event: unknown): MailInboundEvent | null {\n const id = resendReceivedEventId(event)\n return id ? { id, recipients: resendReceivedEventRecipients(event) } : null\n}\n\n/*==========================================\n * THE PROVIDER.\n *=========================================*/\n\n/**\n * Resend as a {@link MailProvider}, reading its two keys from `env` at each\n * call — never at module load, because these run in serverless handlers\n * whose module may be evaluated during a build, long before the runtime\n * environment exists.\n */\nexport function resendMailProvider(\n env: Env = process.env as Env,\n): MailProvider {\n const sendKey = () => setting(env, RESEND_SEND_KEY_SETTING)\n const readKey = () => {\n const key = setting(env, RESEND_READ_KEY_SETTING)\n if (!key) throw new Error(`${RESEND_READ_KEY_SETTING} is not set`)\n return key\n }\n const reads: MailProviderReads = {\n unmet: () =>\n setting(env, RESEND_READ_KEY_SETTING)\n ? null\n : `Set ${RESEND_READ_KEY_SETTING} to a full-access key. The sending ` +\n 'key cannot read mail.',\n // Async, so a missing key REJECTS like every other failed read rather\n // than throwing before the caller has a promise to catch.\n history: async (options) => resendDeliveryHistorySource(readKey())(options),\n message: async (id) => resendDeliveryMessageSource(readKey())(id),\n received: async (id) => resendReceivedEmailSource(readKey())(id),\n sendingDomains: async () => readResendSendingDomains(readKey()),\n }\n return {\n id: RESEND_PROVIDER_ID,\n missingSettings: () => (sendKey() ? [] : [RESEND_SEND_KEY_SETTING]),\n send: (message, context) => sendThroughResend(sendKey(), message, context),\n checkCredentials: () => checkResendCredentials(sendKey()),\n deliveryEvents: normalizeResendDeliveryEvents,\n inboundEvent: resendInboundEvent,\n reads,\n }\n}\n"],"names":["bareSenderAddress","normalizeEventTags","readBounceText","providerRetryAtMs","RESEND_PROVIDER_ID","RESEND_SEND_KEY_SETTING","RESEND_READ_KEY_SETTING","setting","env","name","String","trim","RESEND_SEND_ENDPOINT","postResendEmail","apiKey","payload","context","raw","to","recipients","Array","isArray","map","address","filter","Boolean","length","Error","fetch","method","headers","Authorization","body","JSON","stringify","resendSendPayload","message","from","subject","text","html","Object","keys","tags","replyTo","reply_to","sendThroughResend","response","ok","detail","catch","status","accepted","rateLimited","retryAtMs","json","id","RESEND_DOMAINS_ENDPOINT","REFUSED_KEY_ERRORS","Set","AUTHENTICATED_BUT_UNSCOPED_ERRORS","resendErrorName","parsed","parse","checkResendCredentials","slice","probeStatus","has","error","readResendSendingDomains","endpoint","flag","value","domains","row","data","toLowerCase","push","clickTracking","click_tracking","openTracking","open_tracking","eventTimeMs","Number","isFinite","Date","normalizeBounceType","lowered","RESEND_EVENT_TYPES","normalizeResendDeliveryEvents","receivedAtMs","event","type","providerMessageId","email_id","includes","at","click","timestamp","open","created_at","bounceText","bounce","diagnosticCode","diagnostic_code","remoteMta","remote_mta","part","join","provider","link","bounceType","failed","reason","bounceStatus","RESEND_LAST_EVENTS","sent","delivered","delivery_delayed","opened","clicked","bounced","complained","canceled","queued","scheduled","normalizeResendSentEmails","record","sentAt","last_event","RESEND_EMAILS_ENDPOINT","resendDeliveryHistorySource","cursor","limit","entries","params","URLSearchParams","Math","min","max","set","snapshots","flatMap","entry","lastId","nextCursor","has_more","addressList","normalizeResendMessage","cc","bcc","resendDeliveryMessageSource","encodeURIComponent","RESEND_RECEIVED_EVENT","RESEND_RECEIVING_ENDPOINT","list","header","wanted","key","resendReceivedEventId","resendReceivedEventRecipients","normalizeResendReceivedEmail","nowMs","now","created","messageId","inReplyTo","receivedFor","resendReceivedEmailSource","fetchImpl","resendInboundEvent","resendMailProvider","process","sendKey","readKey","reads","unmet","history","options","received","sendingDomains","missingSettings","send","checkCredentials","deliveryEvents","inboundEvent"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;CAiBC,GAED,SACEA,iBAAiB,EACjBC,kBAAkB,QAOb,6BAAyB;AAEhC,SAASC,cAAc,QAAQ,mBAAe;AAC9C,SACEC,iBAAiB,QAOZ,qBAAiB;AAGxB,qEAAqE,GACrE,OAAO,MAAMC,qBAAqB,SAAQ;AAE1C,4CAA4C,GAC5C,OAAO,MAAMC,0BAA0B,iBAAgB;AAEvD,qDAAqD,GACrD,OAAO,MAAMC,0BAA0B,sBAAqB;AAI5D,SAASC,QAAQC,GAAQ,EAAEC,IAAY;QACvBD;IAAd,OAAOE,QAAOF,YAAAA,GAAG,CAACC,KAAK,YAATD,YAAa,IAAIG,IAAI;AACrC;AAEA;;2CAE2C,GAE3C,uBAAuB,GACvB,OAAO,MAAMC,uBAAuB,gCAA+B;AAUnE;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,eAAeC,gBACpBC,MAAc,EACdC,OAA0B,EAC1BC,OAAgB;IAEhB,MAAMC,MAAMF,2BAAAA,QAASG,EAAE;IACvB,MAAMC,aAAa,AAACC,CAAAA,MAAMC,OAAO,CAACJ,OAAOA,MAAMA,OAAO,OAAO,EAAE,GAAG;QAACA;KAAI,AAAD,EACnEK,GAAG,CAAC,CAACC,UAAYb,OAAOa,kBAAAA,UAAW,IAAIZ,IAAI,IAC3Ca,MAAM,CAACC;IACV,IAAI,CAACN,WAAWO,MAAM,EAAE;QACtB,MAAM,IAAIC,MACR,GAAGX,UAAU,GAAGA,QAAQ,CAAC,CAAC,GAAG,GAAG,oCAAoC,CAAC,GACnE,wEACA;IAEN;IAEA,OAAOY,MAAMhB,sBAAsB;QACjCiB,QAAQ;QACRC,SAAS;YACPC,eAAe,CAAC,OAAO,EAAEjB,QAAQ;YACjC,gBAAgB;QAClB;QACAkB,MAAMC,KAAKC,SAAS,CAACnB;IACvB;AACF;AAEA,oDAAoD,GACpD,OAAO,SAASoB,kBAAkBC,OAA4B;QAUtDA;IATN,OAAO;QACLC,MAAMD,QAAQC,IAAI;QAClBnB,IAAIkB,QAAQlB,EAAE;QACdoB,SAASF,QAAQE,OAAO;OACpBF,QAAQG,IAAI,GAAG;QAAEA,MAAMH,QAAQG,IAAI;IAAC,IAAI,CAAC,GACzCH,QAAQI,IAAI,GAAG;QAAEA,MAAMJ,QAAQI,IAAI;IAAC,IAAI,CAAC,GACzCJ,QAAQN,OAAO,IAAIW,OAAOC,IAAI,CAACN,QAAQN,OAAO,EAAEJ,MAAM,GACtD;QAAEI,SAASM,QAAQN,OAAO;IAAC,IAC3B,CAAC,GACDM,EAAAA,gBAAAA,QAAQO,IAAI,qBAAZP,cAAcV,MAAM,IAAG;QAAEiB,MAAMP,QAAQO,IAAI;IAAC,IAAI,CAAC,GACjDP,QAAQQ,OAAO,GAAG;QAAEC,UAAUT,QAAQQ,OAAO;IAAC,IAAI,CAAC;AAE3D;AAEA;;;;;;;CAOC,GACD,eAAeE,kBACbhC,MAAc,EACdsB,OAA4B,EAC5BpB,OAAgB;;IAEhB,MAAM+B,WAAW,MAAMlC,gBAAgBC,QAAQqB,kBAAkBC,UAAUpB;IAC3E,IAAI,CAAC+B,SAASC,EAAE,EAAE;QAChB,MAAMC,SAAS,MAAMF,SAASR,IAAI,GAAGW,KAAK,CAAC,IAAM;QACjD,IAAIH,SAASI,MAAM,KAAK,KAAK;YAC3B,OAAO;gBACLC,UAAU;gBACVC,aAAa;gBACbC,WAAWnD,kBAAkB4C,SAASjB,OAAO;gBAC7CqB,QAAQJ,SAASI,MAAM;gBACvBF;YACF;QACF;QACA,OAAO;YAAEG,UAAU;YAAOC,aAAa;YAAOF,QAAQJ,SAASI,MAAM;YAAEF;QAAO;IAChF;IACA,MAAMjB,OAAQ,MAAMe,SAASQ,IAAI,GAAGL,KAAK,CAAC,IAAM;IAChD,OAAO;QAAEE,UAAU;QAAMI,EAAE,UAAExB,wBAAAA,KAAMwB,EAAE,mBAAI;IAAK;AAChD;AAEA;;2CAE2C,GAE3C;;;;;;;;;CASC,GACD,OAAO,MAAMC,0BAA0B,iCAAgC;AAEvE;;;;CAIC,GACD,MAAMC,qBAAqB,IAAIC,IAAI;IACjC;IACA;IACA;CACD;AAED;;;;;CAKC,GACD,MAAMC,oCAAoC,IAAID,IAAI;IAChD;IACA;CACD;AAED,wEAAwE,GACxE,SAASE,gBAAgB7B,IAAY;IACnC,IAAI;QACF,MAAM8B,SAAS7B,KAAK8B,KAAK,CAAC/B;QAC1B,OAAO,QAAO8B,0BAAAA,OAAQrD,IAAI,MAAK,WAAWqD,OAAOrD,IAAI,GAAG;IAC1D,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,eAAeuD,uBACpBlD,MAAc;IAEd,IAAI,CAACA,QAAQ,OAAO;QAAEqC,QAAQ;IAAe;IAC7C,IAAI;QACF,MAAMJ,WAAW,MAAMnB,MAAM6B,yBAAyB;YACpD5B,QAAQ;YACRC,SAAS;gBAAEC,eAAe,CAAC,OAAO,EAAEjB,QAAQ;YAAC;QAC/C;QACA,MAAMmC,SAAS,AAAC,CAAA,MAAMF,SAASR,IAAI,GAAGW,KAAK,CAAC,IAAM,GAAE,EAAGe,KAAK,CAAC,GAAG;QAEhE,IAAIlB,SAASI,MAAM,IAAI,OAAOJ,SAASI,MAAM,GAAG,KAAK;YACnD,OAAO;gBAAEA,QAAQ;gBAAMe,aAAanB,SAASI,MAAM;YAAC;QACtD;QACA,IAAIJ,SAASI,MAAM,KAAK,OAAOJ,SAASI,MAAM,KAAK,KAAK;YACtD,MAAM1C,OAAOoD,gBAAgBZ;YAC7B,IAAIW,kCAAkCO,GAAG,CAAC1D,OAAO;gBAC/C,OAAO;oBAAE0C,QAAQ;oBAAMe,aAAanB,SAASI,MAAM;gBAAC;YACtD;YACA,IAAIO,mBAAmBS,GAAG,CAAC1D,OAAO;gBAChC,OAAO;oBAAE0C,QAAQ;oBAAee,aAAanB,SAASI,MAAM;oBAAEF;gBAAO;YACvE;QACF;QACA,OAAO;YAAEE,QAAQ;YAAWe,aAAanB,SAASI,MAAM;YAAEF;QAAO;IACnE,EAAE,OAAOmB,OAAO;;QACd,OAAO;YACLjB,QAAQ;YACRF,QAAQvC,eAAQ0D,yBAAD,AAACA,MAAiBhC,OAAO,mBAAIgC,OAAOH,KAAK,CAAC,GAAG;QAC9D;IACF;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeI,yBACpBvD,MAAc,EACdwD,WAAmBb,uBAAuB;;IAE1C,MAAMV,WAAW,MAAMnB,MAAM0C,UAAU;QACrCzC,QAAQ;QACRC,SAAS;YAAEC,eAAe,CAAC,OAAO,EAAEjB,QAAQ;QAAC;IAC/C;IACA,IAAIiC,SAASI,MAAM,GAAG,OAAOJ,SAASI,MAAM,IAAI,KAAK;QACnD,MAAM,IAAIxB,MAAM,CAAC,sBAAsB,EAAEoB,SAASI,MAAM,CAAC,oBAAoB,CAAC;IAChF;IACA,MAAMpC,UAAW,MAAMgC,SAASQ,IAAI,GAAGL,KAAK,CAAC,IAAM;IAUnD;;;;GAIC,GACD,MAAMqB,OAAO,CAACC,QACZ,OAAOA,UAAU,YAAYA,QAAQ;IACvC,MAAMC,UAA+B,EAAE;IACvC,KAAK,MAAMC,eAAO3D,2BAAAA,QAAS4D,IAAI,mBAAI,EAAE,CAAE;;QACrC,MAAMlE,OAAOC,gBAAOgE,uBAAAA,IAAKjE,IAAI,oBAAI,IAAImE,WAAW;QAChD,IAAI,CAACnE,MAAM;QACXgE,QAAQI,IAAI,CAAC;YACXpE;YACA0C,QAAQzC,gBAAOgE,uBAAAA,IAAKvB,MAAM,oBAAI;YAC9B2B,eAAeP,KAAKG,uBAAAA,IAAKK,cAAc;YACvCC,cAAcT,KAAKG,uBAAAA,IAAKO,aAAa;QACvC;IACF;IACA,OAAOR;AACT;AAEA;;2CAE2C,GAE3C,wEAAwE,GACxE,SAASS,YAAYV,KAAc;IACjC,IAAI,OAAOA,UAAU,YAAYW,OAAOC,QAAQ,CAACZ,QAAQ,OAAOA;IAChE,MAAMV,SAASuB,KAAKtB,KAAK,CAACrD,OAAO8D,gBAAAA,QAAS;IAC1C,OAAOW,OAAOC,QAAQ,CAACtB,UAAUA,SAAS;AAC5C;AAEA,SAASwB,oBACPd,KAAc;IAEd,MAAMe,UAAU7E,OAAO8D,gBAAAA,QAAS,IAC7B7D,IAAI,GACJiE,WAAW;IACd,IAAIW,YAAY,aAAa,OAAO;IACpC,IAAIA,YAAY,aAAa,OAAO;IACpC,OAAOA,UAAU,iBAAiB;AACpC;AAEA,4DAA4D,GAC5D,MAAMC,qBAA6D;IACjE,cAAc;IACd,mBAAmB;IACnB,0BAA0B;IAC1B,gBAAgB;IAChB,iBAAiB;IACjB,iBAAiB;IACjB,oBAAoB;IACpB,gBAAgB;AAClB;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,8BACd1E,OAAgB,EAChB2E,YAAoB;QAGmBC,aAGzBA,aACmBhB,MAAAA,gBAW/BO,OAAAA,OAAAA,OAAAA,cAMqBP;QANTA,aACAA,YAaLA,cAA6BA,eAA8BA,eAAwBA,eAAyBA;IA9BrH,MAAMgB,QAAS5E,kBAAAA,UAAW,CAAC;IAC3B,MAAM6E,OAAOJ,kBAAkB,CAAC9E,QAAOiF,cAAAA,MAAMC,IAAI,YAAVD,cAAc,IAAI;IACzD,IAAI,CAACC,MAAM,OAAO,EAAE;IAEpB,MAAMjB,QAAQgB,cAAAA,MAAMhB,IAAI,YAAVgB,cAAc,CAAC;IAC7B,MAAME,oBAAoBnF,QAAOiE,QAAAA,iBAAAA,KAAKmB,QAAQ,YAAbnB,iBAAiBA,KAAKnB,EAAE,YAAxBmB,OAA4B,IAAIhE,IAAI;IACrE,IAAI,CAACkF,mBAAmB,OAAO,EAAE;IAEjC,MAAM1E,aAAa,AAACC,CAAAA,MAAMC,OAAO,CAACsD,KAAKzD,EAAE,IAAIyD,KAAKzD,EAAE,GAAG;QAACyD,KAAKzD,EAAE;KAAC,AAAD,EAC5DI,GAAG,CAAC,CAACC,UAAqBb,OAAOa,kBAAAA,UAAW,IAAIZ,IAAI,GAAGiE,WAAW,IAClEpD,MAAM,CAAC,CAACD,UAAoBA,QAAQwE,QAAQ,CAAC;IAChD,IAAI,CAAC5E,WAAWO,MAAM,EAAE,OAAO,EAAE;IAEjC,uEAAuE;IACvE,qEAAqE;IACrE,MAAMsE,MACJd,SAAAA,SAAAA,SAAAA,eAAAA,aAAYP,cAAAA,KAAKsB,KAAK,qBAAVtB,YAAYuB,SAAS,aAAjChB,eACAA,aAAYP,aAAAA,KAAKwB,IAAI,qBAATxB,WAAWuB,SAAS,aADhChB,QAEAA,YAAYS,MAAMS,UAAU,aAF5BlB,QAGAA,YAAYP,KAAKyB,UAAU,aAH3BlB,QAIAQ;IAEF,MAAMpD,UAAU5B,QAAOiE,gBAAAA,KAAKrC,OAAO,YAAZqC,gBAAgB,IAAIhE,IAAI,MAAM;IACrD,MAAMgC,OAAO1C,mBAAmB0E,KAAKhC,IAAI;IACzC,MAAMN,OAAOrC,kBAAkB2E,KAAKtC,IAAI;IACxC,sEAAsE;IACtE,oDAAoD;IACpD,MAAMgE,aACJT,SAAS,YACL1F,eACE;SAACyE,eAAAA,KAAK2B,MAAM,qBAAX3B,aAAa4B,cAAc;SAAE5B,gBAAAA,KAAK2B,MAAM,qBAAX3B,cAAa6B,eAAe;SAAE7B,gBAAAA,KAAK2B,MAAM,qBAAX3B,cAAa8B,SAAS;SAAE9B,gBAAAA,KAAK2B,MAAM,qBAAX3B,cAAa+B,UAAU;SAAE/B,gBAAAA,KAAK2B,MAAM,qBAAX3B,cAAavC,OAAO;KAAC,CAC/HZ,MAAM,CAAC,CAACmF,OAAkB,OAAOA,SAAS,YAAYA,KAAKhG,IAAI,IAC/DiG,IAAI,CAAC,QAEV;IAEN,OAAOzF,WAAWG,GAAG,CAAC,CAACJ;kBAYZyD;YAHyBA,aACmBA,cAE5CA,eAAwBA;eAZI;YACrCiB;YACAI;YACAa,UAAUzG;YACVyF;YACA3E;YACAoB;YACAtB,SAAS2B,IAAI,CAAC,UAAU,IAAI;YAC5BA;YACAmE,MAAMlB,SAAS,YAAYlF,gBAAOiE,cAAAA,KAAKsB,KAAK,qBAAVtB,YAAYmC,IAAI,mBAAI,OAAO,OAAO;YACpEC,YAAYnB,SAAS,YAAYN,qBAAoBX,eAAAA,KAAK2B,MAAM,qBAAX3B,aAAaiB,IAAI,IAAI;YAC1E3C,QACEvC,QAAOiE,kBAAAA,gBAAAA,KAAK2B,MAAM,qBAAX3B,cAAavC,OAAO,qBAAIuC,eAAAA,KAAKqC,MAAM,qBAAXrC,aAAasC,MAAM,YAA3CtC,QAA+C,IAAIhE,IAAI,MAAM;WAClE0B,OAAO;YAAEA;QAAK,IAAI,CAAC,GACnBgE,CAAAA,8BAAAA,WAAYlD,MAAM,IAAG;YAAE+D,cAAcb,WAAWlD,MAAM;QAAC,IAAI,CAAC,GAC5DkD,CAAAA,8BAAAA,WAAYI,SAAS,IAAG;YAAEA,WAAWJ,WAAWI,SAAS;QAAC,IAAI,CAAC;;AAEvE;AAEA;;;;;;2CAM2C,GAE3C;;;;;;;CAOC,GACD,MAAMU,qBAA6D;IACjEC,MAAM;IACNC,WAAW;IACXC,kBAAkB;IAClBC,QAAQ;IACRC,SAAS;IACTC,SAAS;IACTC,YAAY;IACZV,QAAQ;IACRW,UAAU;IACVC,QAAQ;IACRC,WAAW;AACb;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASC,0BACd7G,GAAY;QAGqB8G,YAQAA,oBAQ/BZ,6CACSY;IAlBX,MAAMA,SAAU9G,cAAAA,MAAO,CAAC;IACxB,MAAM4E,oBAAoBnF,QAAOqH,aAAAA,OAAOvE,EAAE,YAATuE,aAAa,IAAIpH,IAAI;IACtD,IAAI,CAACkF,mBAAmB,OAAO,EAAE;IAEjC,MAAM1E,aAAa,AAACC,CAAAA,MAAMC,OAAO,CAAC0G,OAAO7G,EAAE,IAAI6G,OAAO7G,EAAE,GAAG;QAAC6G,OAAO7G,EAAE;KAAC,AAAD,EAClEI,GAAG,CAAC,CAACC,UAAqBb,OAAOa,kBAAAA,UAAW,IAAIZ,IAAI,GAAGiE,WAAW,IAClEpD,MAAM,CAAC,CAACD,UAAoBA,QAAQwE,QAAQ,CAAC;IAChD,IAAI,CAAC5E,WAAWO,MAAM,EAAE,OAAO,EAAE;IAEjC,MAAMoC,SAASuB,KAAKtB,KAAK,CAACrD,QAAOqH,qBAAAA,OAAO3B,UAAU,YAAjB2B,qBAAqB;IACtD,MAAMC,SAAS7C,OAAOC,QAAQ,CAACtB,UAAUA,SAAS;IAClD,2EAA2E;IAC3E,2EAA2E;IAC3E,sCAAsC;IACtC,IAAI,CAACkE,QAAQ,OAAO,EAAE;IAEtB,MAAM7E,UACJgE,8CAAAA,kBAAkB,CAChBzG,QAAOqH,qBAAAA,OAAOE,UAAU,YAAjBF,qBAAqB,IACzBpH,IAAI,GACJiE,WAAW,GACf,YAJDuC,8CAIK;IAEP,OAAOhG,WAAWG,GAAG,CAAC,CAACJ;YAIL6G;eAJqB;YACrClB,UAAUzG;YACVyF;YACA3E;YACAoB,SAAS5B,QAAOqH,kBAAAA,OAAOzF,OAAO,YAAdyF,kBAAkB,IAAIpH,IAAI,MAAM;YAChDqH;YACA7E;QACF;;AACF;AAEA,iEAAiE,GACjE,OAAO,MAAM+E,yBAAyB,gCAA+B;AAErE;;;;;CAKC,GACD,OAAO,SAASC,4BACdrH,MAAc;IAEd,OAAO,OAAO,EAAEsH,MAAM,EAAEC,KAAK,EAAE,GAAG,CAAC,CAAC;;YAyB/BC;QAxBH,MAAMC,SAAS,IAAIC,gBAAgB;YACjCH,OAAO3H,OAAO+H,KAAKC,GAAG,CAACD,KAAKE,GAAG,CAAC,GAAGN,gBAAAA,QAAS,MAAM;QACpD;QACA,IAAID,QAAQG,OAAOK,GAAG,CAAC,SAASR;QAChC,MAAMrF,WAAW,MAAMnB,MAAM,GAAGsG,uBAAuB,CAAC,EAAEK,QAAQ,EAAE;YAClEzG,SAAS;gBAAEC,eAAe,CAAC,OAAO,EAAEjB,QAAQ;YAAC;QAC/C;QACA,IAAI,CAACiC,SAASC,EAAE,EAAE;YAChB,MAAMC,SAAS,MAAMF,SAASR,IAAI,GAAGW,KAAK,CAAC,IAAM;YACjD,MAAM,IAAIvB,MACR,CAAC,gCAAgC,EAAEoB,SAASI,MAAM,CAAC,CAAC,EAAEF,OAAOgB,KAAK,CAAC,GAAG,MAAM;QAEhF;QACA,MAAMjC,OAAQ,MAAMe,SAASQ,IAAI;QAIjC,MAAM+E,UAAUlH,MAAMC,OAAO,CAACW,wBAAAA,KAAM2C,IAAI,IAAI3C,KAAK2C,IAAI,GAAG,EAAE;QAC1D,MAAMkE,YAAYP,QAAQQ,OAAO,CAAC,CAACC,QAAUjB,0BAA0BiB;QACvE,yEAAyE;QACzE,mEAAmE;QACnE,yEAAyE;QACzE,mCAAmC;QACnC,MAAMC,SAAStI,gBACZ4H,YAAAA,OAAO,CAACA,QAAQ5G,MAAM,GAAG,EAAE,qBAA5B,AAAC4G,UAAkD9E,EAAE,mBAAI,IACzD7C,IAAI;QACN,OAAO;YACLkI;YACAI,YAAYjH,CAAAA,wBAAAA,KAAMkH,QAAQ,KAAIF,SAASA,SAAS;QAClD;IACF;AACF;AAEA,SAASG,YAAYlI,GAAY;IAC/B,OAAO,AAACG,CAAAA,MAAMC,OAAO,CAACJ,OAAOA,MAAMA,OAAO,OAAO,EAAE,GAAG;QAACA;KAAI,AAAD,EACvDK,GAAG,CAAC,CAACC,UAAYb,OAAOa,kBAAAA,UAAW,IAAIZ,IAAI,IAC3Ca,MAAM,CAACC;AACZ;AAEA,2DAA2D,GAC3D,OAAO,SAAS2H,uBAAuBnI,GAAY;QAEhB8G,YAEAA,oBAOlBA,cAIGA,iBAQdZ,6CACSY;IAvBb,MAAMA,SAAU9G,cAAAA,MAAO,CAAC;IACxB,MAAM4E,oBAAoBnF,QAAOqH,aAAAA,OAAOvE,EAAE,YAATuE,aAAa,IAAIpH,IAAI;IACtD,IAAI,CAACkF,mBAAmB,OAAO;IAC/B,MAAM/B,SAASuB,KAAKtB,KAAK,CAACrD,QAAOqH,qBAAAA,OAAO3B,UAAU,YAAjB2B,qBAAqB;IACtD,OAAO;QACLlB,UAAUzG;QACVyF;QACA3E,IAAIiI,YAAYpB,OAAO7G,EAAE;QACzBmI,IAAIF,YAAYpB,OAAOsB,EAAE;QACzBC,KAAKH,YAAYpB,OAAOuB,GAAG;QAC3BjH,MAAM3B,QAAOqH,eAAAA,OAAO1F,IAAI,YAAX0F,eAAe,IAAIpH,IAAI,MAAM;QAC1CiC,SAASuG,YAAYpB,OAAOlF,QAAQ,EAAEnB,MAAM,GACxCyH,YAAYpB,OAAOlF,QAAQ,IAC3B;QACJP,SAAS5B,QAAOqH,kBAAAA,OAAOzF,OAAO,YAAdyF,kBAAkB,IAAIpH,IAAI,MAAM;QAChD,oEAAoE;QACpE,uEAAuE;QACvE,yEAAyE;QACzE6B,MAAM,OAAOuF,OAAOvF,IAAI,KAAK,WAAWuF,OAAOvF,IAAI,GAAG;QACtDD,MAAM,OAAOwF,OAAOxF,IAAI,KAAK,WAAWwF,OAAOxF,IAAI,GAAG;QACtDyF,QAAQ7C,OAAOC,QAAQ,CAACtB,UAAUA,SAAS;QAC3CX,MAAM,GACJgE,8CAAAA,kBAAkB,CAChBzG,QAAOqH,qBAAAA,OAAOE,UAAU,YAAjBF,qBAAqB,IACzBpH,IAAI,GACJiE,WAAW,GACf,YAJDuC,8CAIK;IACT;AACF;AAEA,iEAAiE,GACjE,OAAO,SAASoC,4BACdzI,MAAc;IAEd,OAAO,OAAO+E;QACZ,MAAM9C,WAAW,MAAMnB,MACrB,GAAGsG,uBAAuB,CAAC,EAAEsB,mBAAmB3D,oBAAoB,EACpE;YAAE/D,SAAS;gBAAEC,eAAe,CAAC,OAAO,EAAEjB,QAAQ;YAAC;QAAE;QAEnD,wEAAwE;QACxE,yEAAyE;QACzE,uCAAuC;QACvC,IAAIiC,SAASI,MAAM,KAAK,KAAK,OAAO;QACpC,IAAI,CAACJ,SAASC,EAAE,EAAE;YAChB,MAAMC,SAAS,MAAMF,SAASR,IAAI,GAAGW,KAAK,CAAC,IAAM;YACjD,MAAM,IAAIvB,MACR,CAAC,0BAA0B,EAAEoB,SAASI,MAAM,CAAC,CAAC,EAAEF,OAAOgB,KAAK,CAAC,GAAG,MAAM;QAE1E;QACA,OAAOmF,uBAAuB,MAAMrG,SAASQ,IAAI;IACnD;AACF;AAEA;;;;;;;2CAO2C,GAE3C,0DAA0D,GAC1D,OAAO,MAAMkG,wBAAwB,iBAAgB;AAErD,4EAA4E,GAC5E,OAAO,MAAMC,4BAA4B,0CAAyC;AAElF,MAAMC,OAAO,CAACnF,QACZpD,MAAMC,OAAO,CAACmD,SACVA,MAAMlD,GAAG,CAAC,CAACyH,QAAUrI,OAAOqI,gBAAAA,QAAS,IAAIpI,IAAI,IAAIa,MAAM,CAAC,CAACuH,QAAUA,UAAU,MAC7E,OAAOvE,UAAU,YAAYA,MAAM7D,IAAI,KACrC;QAAC6D,MAAM7D,IAAI;KAAG,GACd,EAAE;AAEV,MAAM4B,OAAO,CAACiC,QAA4B,OAAOA,UAAU,WAAWA,QAAQ;AAE9E,8EAA8E,GAC9E,SAASoF,OAAO9H,OAAgB,EAAErB,IAAY;IAC5C,IAAI,CAACqB,WAAW,OAAOA,YAAY,UAAU,OAAO;IACpD,MAAM+H,SAASpJ,KAAKmE,WAAW;IAC/B,KAAK,MAAM,CAACkF,KAAKtF,MAAM,IAAI/B,OAAO6F,OAAO,CAACxG,SAAqC;QAC7E,IAAIgI,IAAIlF,WAAW,OAAOiF,QAAQ;gBACKrF;YAArC,OAAOpD,MAAMC,OAAO,CAACmD,SAAS9D,QAAO8D,UAAAA,KAAK,CAAC,EAAE,YAARA,UAAY,IAAI7D,IAAI,KAAKD,OAAO8D,gBAAAA,QAAS,IAAI7D,IAAI;QACxF;IACF;IACA,OAAO;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASoJ,sBAAsBpE,KAAc;;QAGhCoC;IAFlB,MAAMA,SAASpC;IACf,IAAI,CAACoC,UAAUA,OAAOnC,IAAI,KAAK6D,uBAAuB,OAAO;IAC7D,MAAMjG,KAAK9C,gBAAOqH,eAAAA,OAAOpD,IAAI,qBAAXoD,aAAajC,QAAQ,mBAAI,IAAInF,IAAI;IACnD,OAAO6C,MAAM;AACf;AAEA;;;;CAIC,GACD,OAAO,SAASwG,8BAA8BrE,KAAc;;IAC1D,MAAMhB,eAAQgB,yBAAD,AAACA,MAAqDhB,IAAI,mBAAI,CAAC;IAC5E,OAAO;WACFgF,KAAKhF,IAAI,CAAC,KAAK;WACfgF,KAAKhF,IAAI,CAAC,KAAK;WACfgF,KAAKhF,IAAI,CAAC,MAAM;WAChBgF,KAAKhF,IAAI,CAAC,eAAe;KAC7B;AACH;AAEA;;;CAGC,GACD,OAAO,SAASsF,6BACdhJ,GAAY,EACZiJ,QAAgB7E,KAAK8E,GAAG,EAAE;QAGRpC,YAGgBA,oBAGdA,oBAELA,cAKGA;IAdlB,MAAMA,SAAU9G,OAAO,OAAOA,QAAQ,WAAWA,MAAM,CAAC;IACxD,MAAMuC,KAAK9C,QAAOqH,aAAAA,MAAM,CAAC,KAAK,YAAZA,aAAgB,IAAIpH,IAAI;IAC1C,IAAI,CAAC6C,IAAI,OAAO;IAChB,MAAM1B,UAAUiG,MAAM,CAAC,UAAU;IACjC,MAAMqC,UAAU/E,KAAKtB,KAAK,CAACrD,QAAOqH,qBAAAA,MAAM,CAAC,aAAa,YAApBA,qBAAwB;IAC1D,OAAO;QACLvE;QACA6G,WAAW3J,QAAOqH,qBAAAA,MAAM,CAAC,aAAa,YAApBA,qBAAwB,IAAIpH,IAAI,MAAMiJ,OAAO9H,SAAS;QACxEwI,WAAWV,OAAO9H,SAAS;QAC3BO,MAAM3B,QAAOqH,eAAAA,MAAM,CAAC,OAAO,YAAdA,eAAkB,IAAIpH,IAAI;QACvCO,IAAIyI,KAAK5B,MAAM,CAAC,KAAK;QACrBsB,IAAIM,KAAK5B,MAAM,CAAC,KAAK;QACrBuB,KAAKK,KAAK5B,MAAM,CAAC,MAAM;QACvBwC,aAAaZ,KAAK5B,MAAM,CAAC,eAAe;QACxCzF,SAAS5B,QAAOqH,kBAAAA,MAAM,CAAC,UAAU,YAAjBA,kBAAqB,IAAIpH,IAAI;QAC7C4B,MAAMA,KAAKwF,MAAM,CAAC,OAAO;QACzBvF,MAAMD,KAAKwF,MAAM,CAAC,OAAO;QACzBrC,cAAcP,OAAOC,QAAQ,CAACgF,YAAYA,UAAU,IAAIA,UAAUF;IACpE;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASM,0BACd1J,MAAc,EACd2J,YAA0B7I,KAAK;IAE/B,OAAO,OAAO4B;QACZ,MAAMT,WAAW,MAAM0H,UACrB,GAAGf,0BAA0B,CAAC,EAAEF,mBAAmBhG,KAAK,EACxD;YAAE1B,SAAS;gBAAEC,eAAe,CAAC,OAAO,EAAEjB,QAAQ;YAAC;QAAE;QAEnD,IAAIiC,SAASI,MAAM,KAAK,KAAK,OAAO;QACpC,IAAI,CAACJ,SAASC,EAAE,EAAE;YAChB,MAAMC,SAAS,MAAMF,SAASR,IAAI,GAAGW,KAAK,CAAC,IAAM;YACjD,MAAM,IAAIvB,MACR,CAAC,iCAAiC,EAAEoB,SAASI,MAAM,CAAC,CAAC,EAAEF,OAAOgB,KAAK,CAAC,GAAG,MAAM;QAEjF;QACA,OAAOgG,6BAA6B,MAAMlH,SAASQ,IAAI;IACzD;AACF;AAEA,iEAAiE,GACjE,OAAO,SAASmH,mBAAmB/E,KAAc;IAC/C,MAAMnC,KAAKuG,sBAAsBpE;IACjC,OAAOnC,KAAK;QAAEA;QAAIrC,YAAY6I,8BAA8BrE;IAAO,IAAI;AACzE;AAEA;;2CAE2C,GAE3C;;;;;CAKC,GACD,OAAO,SAASgF,mBACdnK,MAAWoK,QAAQpK,GAAG,AAAO;IAE7B,MAAMqK,UAAU,IAAMtK,QAAQC,KAAKH;IACnC,MAAMyK,UAAU;QACd,MAAMhB,MAAMvJ,QAAQC,KAAKF;QACzB,IAAI,CAACwJ,KAAK,MAAM,IAAInI,MAAM,GAAGrB,wBAAwB,WAAW,CAAC;QACjE,OAAOwJ;IACT;IACA,MAAMiB,QAA2B;QAC/BC,OAAO,IACLzK,QAAQC,KAAKF,2BACT,OACA,CAAC,IAAI,EAAEA,wBAAwB,mCAAmC,CAAC,GACnE;QACN,sEAAsE;QACtE,0DAA0D;QAC1D2K,SAAS,OAAOC,UAAY/C,4BAA4B2C,WAAWI;QACnE9I,SAAS,OAAOoB,KAAO+F,4BAA4BuB,WAAWtH;QAC9D2H,UAAU,OAAO3H,KAAOgH,0BAA0BM,WAAWtH;QAC7D4H,gBAAgB,UAAY/G,yBAAyByG;IACvD;IACA,OAAO;QACLtH,IAAIpD;QACJiL,iBAAiB,IAAOR,YAAY,EAAE,GAAG;gBAACxK;aAAwB;QAClEiL,MAAM,CAAClJ,SAASpB,UAAY8B,kBAAkB+H,WAAWzI,SAASpB;QAClEuK,kBAAkB,IAAMvH,uBAAuB6G;QAC/CW,gBAAgB/F;QAChBgG,cAAcf;QACdK;IACF;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/mail-provider-resend.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 * RESEND, as one {@link MailProvider} — the only module in the platform that\n * knows Resend's endpoints, payloads, event names and error names.\n *\n * It is the provider a deployment gets when it names none, which is how\n * every deployment that set `RESEND_API_KEY` before the contract existed\n * keeps sending exactly as it did. Two credentials, on purpose:\n *\n * - `RESEND_API_KEY` sends. It is provisioned sending-scoped, so a leaked\n * copy cannot enumerate everyone the platform ever wrote to.\n * - `RESEND_READ_API_KEY` is full-access and only ever reads: history, one\n * message's body, received mail and the account's sending domains. A\n * sending-scoped key answers every such read `401 restricted_api_key`.\n *\n * Pure but for the `fetch` calls, each inside a function that must be called\n * to do anything, so a client component that reaches this module through the\n * library's barrel executes nothing on load.\n */\n\nimport {\n bareSenderAddress,\n normalizeEventTags,\n type EmailDeliveryEvent,\n type EmailDeliveryEventType,\n type EmailDeliveryHistorySource,\n type EmailDeliveryMessage,\n type EmailDeliveryMessageSource,\n type EmailDeliverySnapshot,\n} from './email-delivery-events'\nimport type { EmailCredentialReport } from './email-health'\nimport { readBounceText } from './mail-bounce'\nimport {\n providerRetryAtMs,\n type MailInboundEvent,\n type MailProvider,\n type MailProviderMessage,\n type MailProviderReads,\n type MailProviderSendResult,\n type MailSendingDomain,\n} from './mail-provider'\nimport type { ReceivedEmail, ReceivedEmailSource } from './received-email'\n\n/** The slug stored on every delivery record this provider produces. */\nexport const RESEND_PROVIDER_ID = 'resend'\n\n/** The sending key's environment variable. */\nexport const RESEND_SEND_KEY_SETTING = 'RESEND_API_KEY'\n\n/** The full-access read key's environment variable. */\nexport const RESEND_READ_KEY_SETTING = 'RESEND_READ_API_KEY'\n\ntype Env = Record<string, string | undefined>\n\nfunction setting(env: Env, name: string): string {\n return String(env[name] ?? '').trim()\n}\n\n/*==========================================\n * SENDING.\n *=========================================*/\n\n/** The send endpoint. */\nexport const RESEND_SEND_ENDPOINT = 'https://api.resend.com/emails'\n\n/** A Resend send payload in the provider's own wire shape. */\nexport interface ResendSendPayload {\n to?: unknown\n from?: unknown\n subject?: unknown\n [field: string]: unknown\n}\n\n/**\n * The one place that POSTs to Resend's send endpoint, and the last thing\n * standing between a payload and the network.\n *\n * A payload carrying no recipient cannot become a message. Resend answers it\n * `422 missing_required_field`, which costs an API call and then shows up in\n * the vendor dashboard as a red line indistinguishable from mail that\n * genuinely failed to deliver — carrying no subject, no recipient and nothing\n * naming the code that produced it. Diagnosing that means reading a log\n * outside the deployment and guessing. So the refusal happens here, before\n * the fetch, and names the caller's `context`.\n *\n * It throws rather than returning a result: this is a programming error,\n * not a delivery outcome. `sendEmail` filters recipients well before it\n * reaches this call, so nothing on the ordinary path can trip it. The guard\n * exists because `RESEND_SEND_ENDPOINT` is exported and any module can\n * therefore reach the send endpoint on its own, bypassing every check\n * `sendEmail` owns.\n */\nexport async function postResendEmail(\n apiKey: string,\n payload: ResendSendPayload,\n context?: string,\n): Promise<Response> {\n const raw = payload?.to\n const recipients = (Array.isArray(raw) ? raw : raw == null ? [] : [raw])\n .map((address) => String(address ?? '').trim())\n .filter(Boolean)\n if (!recipients.length) {\n throw new Error(\n `${context ? `${context} ` : ''}send refused before the network — a ` +\n 'Resend payload with no `to` field cannot become a message, and the ' +\n 'attempt would surface only as a 422 in the Resend dashboard',\n )\n }\n\n return fetch(RESEND_SEND_ENDPOINT, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n })\n}\n\n/** One message as Resend's send endpoint takes it. */\nexport function resendSendPayload(message: MailProviderMessage): ResendSendPayload {\n return {\n from: message.from,\n to: message.to,\n subject: message.subject,\n ...(message.text ? { text: message.text } : {}),\n ...(message.html ? { html: message.html } : {}),\n ...(message.headers && Object.keys(message.headers).length\n ? { headers: message.headers }\n : {}),\n ...(message.tags?.length ? { tags: message.tags } : {}),\n ...(message.replyTo ? { reply_to: message.replyTo } : {}),\n ...(message.bcc?.length ? { bcc: message.bcc } : {}),\n }\n}\n\n/**\n * Sends one message and reads the answer.\n *\n * A 429 is a request to slow down, not a statement about the recipient, so\n * it comes back `rateLimited` with the wait Resend asked for. Its quota\n * errors — `daily_quota_exceeded`, `monthly_quota_exceeded` — are 429s as\n * well, and clear with time the same way.\n */\nasync function sendThroughResend(\n apiKey: string,\n message: MailProviderMessage,\n context?: string,\n): Promise<MailProviderSendResult> {\n const response = await postResendEmail(apiKey, resendSendPayload(message), context)\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n if (response.status === 429) {\n return {\n accepted: false,\n rateLimited: true,\n retryAtMs: providerRetryAtMs(response.headers),\n status: response.status,\n detail,\n }\n }\n return { accepted: false, rateLimited: false, status: response.status, detail }\n }\n const body = (await response.json().catch(() => null)) as { id?: string } | null\n return { accepted: true, id: body?.id ?? null }\n}\n\n/*==========================================\n * THE CREDENTIAL PROBE.\n *=========================================*/\n\n/**\n * Where the credential probe and the domain read ask their questions.\n *\n * Deliberately NOT the send endpoint. A probe aimed at `/emails` is a send\n * attempt however empty its body is: it consumes an API call, and Resend\n * records it in the account's logs as a `422` on `POST /emails` with no\n * recipient, no subject and nothing identifying the caller — a line an\n * operator reading that dashboard has to treat as failed mail. A domain read\n * cannot create a message and cannot be mistaken for one.\n */\nexport const RESEND_DOMAINS_ENDPOINT = 'https://api.resend.com/domains'\n\n/**\n * Error names that mean the key itself was not accepted, as opposed to a key\n * that authenticated and merely lacks read scope. Matched by NAME, not\n * status: `401` and `403` each cover both meanings.\n */\nconst REFUSED_KEY_ERRORS = new Set([\n 'missing_api_key',\n 'validation_error',\n 'suspended_api_key',\n])\n\n/**\n * Error names that mean the key authenticated and was then denied this\n * particular read. A sending-scoped key — the shape Aglyn provisions — always\n * lands here, and reaching this answer at all required Resend to recognize\n * the credential, which is exactly what the probe is asking.\n */\nconst AUTHENTICATED_BUT_UNSCOPED_ERRORS = new Set([\n 'restricted_api_key',\n 'invalid_permission',\n])\n\n/** The `name` Resend puts on an error body, or `''` for anything else. */\nfunction resendErrorName(body: string): string {\n try {\n const parsed = JSON.parse(body) as { name?: unknown }\n return typeof parsed?.name === 'string' ? parsed.name : ''\n } catch {\n return ''\n }\n}\n\n/**\n * Whether the sending key is accepted — without sending anything to anybody,\n * and without leaving anything behind that reads as failed mail.\n *\n * How: a `GET` of the domains collection. The question is only ever \"does\n * Resend recognize this credential\", so the probe reads the ERROR NAME rather\n * than the status, because `401` and `403` each carry both meanings:\n *\n * - `2xx` — the key is accepted and has read scope → `ok`\n * - `restricted_api_key` / `invalid_permission` — Resend authenticated the\n * key and then denied it this read. A sending-scoped key always answers\n * this way, and getting the answer proves the credential works → `ok`\n * - `missing_api_key` / `validation_error` / `suspended_api_key` — the key\n * itself was refused → `invalid-key`\n * - anything else → `unknown`\n *\n * An unrecognized rejection is `unknown`, never `invalid-key`: this feeds a\n * staff diagnostics screen whose whole value is that a red line means\n * something, and a shape we have not seen before is not evidence that a\n * working key is broken.\n */\nexport async function checkResendCredentials(\n apiKey: string,\n): Promise<EmailCredentialReport> {\n if (!apiKey) return { status: 'unconfigured' }\n try {\n const response = await fetch(RESEND_DOMAINS_ENDPOINT, {\n method: 'GET',\n headers: { Authorization: `Bearer ${apiKey}` },\n })\n const detail = (await response.text().catch(() => '')).slice(0, 300)\n\n if (response.status >= 200 && response.status < 300) {\n return { status: 'ok', probeStatus: response.status }\n }\n if (response.status === 401 || response.status === 403) {\n const name = resendErrorName(detail)\n if (AUTHENTICATED_BUT_UNSCOPED_ERRORS.has(name)) {\n return { status: 'ok', probeStatus: response.status }\n }\n if (REFUSED_KEY_ERRORS.has(name)) {\n return { status: 'invalid-key', probeStatus: response.status, detail }\n }\n }\n return { status: 'unknown', probeStatus: response.status, detail }\n } catch (error) {\n return {\n status: 'unknown',\n detail: String((error as Error)?.message ?? error).slice(0, 300),\n }\n }\n}\n\n/**\n * Every sending domain on the account, with its verification and tracking\n * state. Throws with the status when Resend does not answer 2xx, so a caller\n * never reads \"I could not look\" as \"there is nothing there\".\n */\nexport async function readResendSendingDomains(\n apiKey: string,\n endpoint: string = RESEND_DOMAINS_ENDPOINT,\n): Promise<MailSendingDomain[]> {\n const response = await fetch(endpoint, {\n method: 'GET',\n headers: { Authorization: `Bearer ${apiKey}` },\n })\n if (response.status < 200 || response.status >= 300) {\n throw new Error(`The provider answered ${response.status} to the domain read.`)\n }\n const payload = (await response.json().catch(() => null)) as\n | {\n data?: Array<{\n name?: unknown\n status?: unknown\n click_tracking?: unknown\n open_tracking?: unknown\n }>\n }\n | null\n /**\n * A tri-state, and the third state matters: a listing that does not carry\n * the field must not be reported as the field being false. Off is a fault\n * somebody should fix; unknown is a question this read could not ask.\n */\n const flag = (value: unknown): boolean | null =>\n typeof value === 'boolean' ? value : null\n const domains: MailSendingDomain[] = []\n for (const row of payload?.data ?? []) {\n const name = String(row?.name ?? '').toLowerCase()\n if (!name) continue\n domains.push({\n name,\n status: String(row?.status ?? 'unknown'),\n clickTracking: flag(row?.click_tracking),\n openTracking: flag(row?.open_tracking),\n })\n }\n return domains\n}\n\n/*==========================================\n * THE DELIVERY FEED.\n *=========================================*/\n\n/** Epoch ms from an ISO string or a number, or `null` when unreadable. */\nfunction eventTimeMs(value: unknown): number | null {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n const parsed = Date.parse(String(value ?? ''))\n return Number.isFinite(parsed) ? parsed : null\n}\n\nfunction normalizeBounceType(\n value: unknown,\n): EmailDeliveryEvent['bounceType'] {\n const lowered = String(value ?? '')\n .trim()\n .toLowerCase()\n if (lowered === 'permanent') return 'permanent'\n if (lowered === 'transient') return 'transient'\n return lowered ? 'undetermined' : null\n}\n\n/** The Resend event names we understand, mapped onto ours. */\nconst RESEND_EVENT_TYPES: Record<string, EmailDeliveryEventType> = {\n 'email.sent': 'sent',\n 'email.delivered': 'delivered',\n 'email.delivery_delayed': 'delayed',\n 'email.opened': 'opened',\n 'email.clicked': 'clicked',\n 'email.bounced': 'bounced',\n 'email.complained': 'complained',\n 'email.failed': 'failed',\n}\n\n/**\n * Turns one Resend webhook payload into zero or more of our events.\n *\n * One event per recipient, not per message: a send addressed to three people\n * produces one webhook, and a staff view keyed on a person has to be able to\n * find it under each of them.\n *\n * Returns an empty array for anything unrecognised — a contact or domain\n * event, an inbound `email.received`, a type added after this was written.\n * Silence rather than a throw, because a webhook handler that 500s on an\n * unfamiliar event teaches the provider to retry it forever.\n *\n * @param payload The parsed webhook body.\n * @param receivedAtMs Fallback timestamp for a payload that carries none.\n */\nexport function normalizeResendDeliveryEvents(\n payload: unknown,\n receivedAtMs: number,\n): EmailDeliveryEvent[] {\n const event = (payload ?? {}) as Record<string, any>\n const type = RESEND_EVENT_TYPES[String(event.type ?? '')]\n if (!type) return []\n\n const data = (event.data ?? {}) as Record<string, any>\n const providerMessageId = String(data.email_id ?? data.id ?? '').trim()\n if (!providerMessageId) return []\n\n const recipients = (Array.isArray(data.to) ? data.to : [data.to])\n .map((address: unknown) => String(address ?? '').trim().toLowerCase())\n .filter((address: string) => address.includes('@'))\n if (!recipients.length) return []\n\n // The per-state timestamp when the provider gives one, because an open\n // three days after the send is the whole point of recording an open.\n const at =\n eventTimeMs(data.click?.timestamp) ??\n eventTimeMs(data.open?.timestamp) ??\n eventTimeMs(event.created_at) ??\n eventTimeMs(data.created_at) ??\n receivedAtMs\n\n const subject = String(data.subject ?? '').trim() || null\n const tags = normalizeEventTags(data.tags)\n const from = bareSenderAddress(data.from)\n // The provider folds a DSN's fields into one sentence; the structured\n // ones are read first where a payload carries them.\n const bounceText =\n type === 'bounced'\n ? readBounceText(\n [data.bounce?.diagnosticCode, data.bounce?.diagnostic_code, data.bounce?.remoteMta, data.bounce?.remote_mta, data.bounce?.message]\n .filter((part: unknown) => typeof part === 'string' && part.trim())\n .join(' '),\n )\n : null\n\n return recipients.map((to: string) => ({\n type,\n at,\n provider: RESEND_PROVIDER_ID,\n providerMessageId,\n to,\n subject,\n context: tags['context'] || null,\n tags,\n link: type === 'clicked' ? String(data.click?.link ?? '') || null : null,\n bounceType: type === 'bounced' ? normalizeBounceType(data.bounce?.type) : null,\n detail:\n String(data.bounce?.message ?? data.failed?.reason ?? '').trim() || null,\n ...(from ? { from } : {}),\n ...(bounceText?.status ? { bounceStatus: bounceText.status } : {}),\n ...(bounceText?.remoteMta ? { remoteMta: bounceText.remoteMta } : {}),\n }))\n}\n\n/*==========================================\n * THE ACCOUNT'S HISTORY.\n *\n * Resend's list endpoint reports a single `last_event` per message and no\n * engagement detail, so a snapshot can say \"this was opened at least once\"\n * and can never say \"three times\" — see `EmailDeliverySnapshot`.\n *=========================================*/\n\n/**\n * A `last_event` string, mapped onto our lifecycle.\n *\n * Deliberately the bare state names rather than the `email.*` event names:\n * the list endpoint reports `\"delivered\"`, the webhook reports\n * `\"email.delivered\"`, and they are two different vocabularies for one\n * concept.\n */\nconst RESEND_LAST_EVENTS: Record<string, EmailDeliveryEventType> = {\n sent: 'sent',\n delivered: 'delivered',\n delivery_delayed: 'delayed',\n opened: 'opened',\n clicked: 'clicked',\n bounced: 'bounced',\n complained: 'complained',\n failed: 'failed',\n canceled: 'failed',\n queued: 'sent',\n scheduled: 'sent',\n}\n\n/**\n * Turns one entry from Resend's `GET /emails` list into zero or more\n * snapshots — one per recipient, for the same reason the event adapter fans\n * out: the staff view is keyed on a person.\n *\n * An unrecognised `last_event` falls back to `sent` rather than being\n * dropped. The message demonstrably exists and was addressed to somebody, and\n * \"we sent this and cannot characterise what happened next\" is a far more\n * useful row than no row — which is the state that sent a staffer to the\n * vendor dashboard in the first place.\n */\nexport function normalizeResendSentEmails(\n raw: unknown,\n): EmailDeliverySnapshot[] {\n const record = (raw ?? {}) as Record<string, any>\n const providerMessageId = String(record.id ?? '').trim()\n if (!providerMessageId) return []\n\n const recipients = (Array.isArray(record.to) ? record.to : [record.to])\n .map((address: unknown) => String(address ?? '').trim().toLowerCase())\n .filter((address: string) => address.includes('@'))\n if (!recipients.length) return []\n\n const parsed = Date.parse(String(record.created_at ?? ''))\n const sentAt = Number.isFinite(parsed) ? parsed : 0\n // A snapshot with no timestamp cannot be ordered, and the log's read drops\n // any document missing its sort key — so it is refused rather than written\n // somewhere nothing will look for it.\n if (!sentAt) return []\n\n const status =\n RESEND_LAST_EVENTS[\n String(record.last_event ?? '')\n .trim()\n .toLowerCase()\n ] ?? 'sent'\n\n return recipients.map((to: string) => ({\n provider: RESEND_PROVIDER_ID,\n providerMessageId,\n to,\n subject: String(record.subject ?? '').trim() || null,\n sentAt,\n status,\n }))\n}\n\n/** The list endpoint. Paginates with `after=<id>`; caps at 100. */\nexport const RESEND_EMAILS_ENDPOINT = 'https://api.resend.com/emails'\n\n/**\n * {@link EmailDeliveryHistorySource} for Resend. Needs the full-access key.\n *\n * The list endpoint takes `limit`, `after` and `before` and no recipient\n * filter, which is why the source is a cursor-paged sweep.\n */\nexport function resendDeliveryHistorySource(\n apiKey: string,\n): EmailDeliveryHistorySource {\n return async ({ cursor, limit } = {}) => {\n const params = new URLSearchParams({\n limit: String(Math.min(Math.max(1, limit ?? 100), 100)),\n })\n if (cursor) params.set('after', cursor)\n const response = await fetch(`${RESEND_EMAILS_ENDPOINT}?${params}`, {\n headers: { Authorization: `Bearer ${apiKey}` },\n })\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n throw new Error(\n `email history read failed: HTTP ${response.status} ${detail.slice(0, 200)}`,\n )\n }\n const body = (await response.json()) as {\n data?: unknown[]\n has_more?: boolean\n }\n const entries = Array.isArray(body?.data) ? body.data : []\n const snapshots = entries.flatMap((entry) => normalizeResendSentEmails(entry))\n // The cursor is the LAST RAW entry's id, not the last snapshot's: a page\n // whose final entry fanned out to zero snapshots (no recipient, no\n // timestamp) would otherwise rewind the cursor to an earlier message and\n // loop over the same page forever.\n const lastId = String(\n (entries[entries.length - 1] as { id?: unknown })?.id ?? '',\n ).trim()\n return {\n snapshots,\n nextCursor: body?.has_more && lastId ? lastId : null,\n }\n }\n}\n\nfunction addressList(raw: unknown): string[] {\n return (Array.isArray(raw) ? raw : raw == null ? [] : [raw])\n .map((address) => String(address ?? '').trim())\n .filter(Boolean)\n}\n\n/** Resend's `GET /emails/:id` payload, in our vocabulary. */\nexport function normalizeResendMessage(raw: unknown): EmailDeliveryMessage | null {\n const record = (raw ?? {}) as Record<string, any>\n const providerMessageId = String(record.id ?? '').trim()\n if (!providerMessageId) return null\n const parsed = Date.parse(String(record.created_at ?? ''))\n return {\n provider: RESEND_PROVIDER_ID,\n providerMessageId,\n to: addressList(record.to),\n cc: addressList(record.cc),\n bcc: addressList(record.bcc),\n from: String(record.from ?? '').trim() || null,\n replyTo: addressList(record.reply_to).length\n ? addressList(record.reply_to)\n : null,\n subject: String(record.subject ?? '').trim() || null,\n // Empty string is NOT null here, and the difference is the point: a\n // message that went out text-only really does have an empty HTML part,\n // and a reader has to be able to tell that from \"we could not fetch it\".\n html: typeof record.html === 'string' ? record.html : null,\n text: typeof record.text === 'string' ? record.text : null,\n sentAt: Number.isFinite(parsed) ? parsed : null,\n status:\n RESEND_LAST_EVENTS[\n String(record.last_event ?? '')\n .trim()\n .toLowerCase()\n ] ?? null,\n }\n}\n\n/** Resend's single-message endpoint. Needs the full-access key. */\nexport function resendDeliveryMessageSource(\n apiKey: string,\n): EmailDeliveryMessageSource {\n return async (providerMessageId: string) => {\n const response = await fetch(\n `${RESEND_EMAILS_ENDPOINT}/${encodeURIComponent(providerMessageId)}`,\n { headers: { Authorization: `Bearer ${apiKey}` } },\n )\n // A message the provider has aged out is a 404, and that is an ANSWER —\n // \"we know this was sent and the body is gone\" — not a failure to report\n // as an error the staffer must act on.\n if (response.status === 404) return null\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n throw new Error(\n `message read failed: HTTP ${response.status} ${detail.slice(0, 200)}`,\n )\n }\n return normalizeResendMessage(await response.json())\n }\n}\n\n/*==========================================\n * RECEIVED MAIL.\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.\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\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 what it is looking for in\n * the 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/**\n * {@link ReceivedEmailSource} for Resend. Needs the full-access key. A `404`\n * is `null` — the message expired or never existed — and anything else\n * throws with the status, so the caller decides whether the provider should\n * 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\n/** A received-mail notification, or `null` for any other event. */\nexport function resendInboundEvent(event: unknown): MailInboundEvent | null {\n const id = resendReceivedEventId(event)\n return id ? { id, recipients: resendReceivedEventRecipients(event) } : null\n}\n\n/*==========================================\n * THE PROVIDER.\n *=========================================*/\n\n/**\n * Resend as a {@link MailProvider}, reading its two keys from `env` at each\n * call — never at module load, because these run in serverless handlers\n * whose module may be evaluated during a build, long before the runtime\n * environment exists.\n */\nexport function resendMailProvider(\n env: Env = process.env as Env,\n): MailProvider {\n const sendKey = () => setting(env, RESEND_SEND_KEY_SETTING)\n const readKey = () => {\n const key = setting(env, RESEND_READ_KEY_SETTING)\n if (!key) throw new Error(`${RESEND_READ_KEY_SETTING} is not set`)\n return key\n }\n const reads: MailProviderReads = {\n unmet: () =>\n setting(env, RESEND_READ_KEY_SETTING)\n ? null\n : `Set ${RESEND_READ_KEY_SETTING} to a full-access key. The sending ` +\n 'key cannot read mail.',\n // Async, so a missing key REJECTS like every other failed read rather\n // than throwing before the caller has a promise to catch.\n history: async (options) => resendDeliveryHistorySource(readKey())(options),\n message: async (id) => resendDeliveryMessageSource(readKey())(id),\n received: async (id) => resendReceivedEmailSource(readKey())(id),\n sendingDomains: async () => readResendSendingDomains(readKey()),\n }\n return {\n id: RESEND_PROVIDER_ID,\n missingSettings: () => (sendKey() ? [] : [RESEND_SEND_KEY_SETTING]),\n send: (message, context) => sendThroughResend(sendKey(), message, context),\n checkCredentials: () => checkResendCredentials(sendKey()),\n deliveryEvents: normalizeResendDeliveryEvents,\n inboundEvent: resendInboundEvent,\n reads,\n }\n}\n"],"names":["bareSenderAddress","normalizeEventTags","readBounceText","providerRetryAtMs","RESEND_PROVIDER_ID","RESEND_SEND_KEY_SETTING","RESEND_READ_KEY_SETTING","setting","env","name","String","trim","RESEND_SEND_ENDPOINT","postResendEmail","apiKey","payload","context","raw","to","recipients","Array","isArray","map","address","filter","Boolean","length","Error","fetch","method","headers","Authorization","body","JSON","stringify","resendSendPayload","message","from","subject","text","html","Object","keys","tags","replyTo","reply_to","bcc","sendThroughResend","response","ok","detail","catch","status","accepted","rateLimited","retryAtMs","json","id","RESEND_DOMAINS_ENDPOINT","REFUSED_KEY_ERRORS","Set","AUTHENTICATED_BUT_UNSCOPED_ERRORS","resendErrorName","parsed","parse","checkResendCredentials","slice","probeStatus","has","error","readResendSendingDomains","endpoint","flag","value","domains","row","data","toLowerCase","push","clickTracking","click_tracking","openTracking","open_tracking","eventTimeMs","Number","isFinite","Date","normalizeBounceType","lowered","RESEND_EVENT_TYPES","normalizeResendDeliveryEvents","receivedAtMs","event","type","providerMessageId","email_id","includes","at","click","timestamp","open","created_at","bounceText","bounce","diagnosticCode","diagnostic_code","remoteMta","remote_mta","part","join","provider","link","bounceType","failed","reason","bounceStatus","RESEND_LAST_EVENTS","sent","delivered","delivery_delayed","opened","clicked","bounced","complained","canceled","queued","scheduled","normalizeResendSentEmails","record","sentAt","last_event","RESEND_EMAILS_ENDPOINT","resendDeliveryHistorySource","cursor","limit","entries","params","URLSearchParams","Math","min","max","set","snapshots","flatMap","entry","lastId","nextCursor","has_more","addressList","normalizeResendMessage","cc","resendDeliveryMessageSource","encodeURIComponent","RESEND_RECEIVED_EVENT","RESEND_RECEIVING_ENDPOINT","list","header","wanted","key","resendReceivedEventId","resendReceivedEventRecipients","normalizeResendReceivedEmail","nowMs","now","created","messageId","inReplyTo","receivedFor","resendReceivedEmailSource","fetchImpl","resendInboundEvent","resendMailProvider","process","sendKey","readKey","reads","unmet","history","options","received","sendingDomains","missingSettings","send","checkCredentials","deliveryEvents","inboundEvent"],"mappings":"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"}
|
|
@@ -44,6 +44,9 @@
|
|
|
44
44
|
* "replyTo": "support@acme.com" } }
|
|
45
45
|
* ```
|
|
46
46
|
*
|
|
47
|
+
* `bcc` — a list — is present only when the message carries blind copies
|
|
48
|
+
* (AGL-3699), and is delivered without appearing on the message's headers.
|
|
49
|
+
*
|
|
47
50
|
* Answer `2xx` once the message is accepted — with `{ "id": "…" }` if the
|
|
48
51
|
* relay has an id for it, which the delivery log keys on. Answer `429`, with
|
|
49
52
|
* `Retry-After` in seconds, to ask for a slower pace: the message is kept
|
|
@@ -44,6 +44,9 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
44
44
|
* "replyTo": "support@acme.com" } }
|
|
45
45
|
* ```
|
|
46
46
|
*
|
|
47
|
+
* `bcc` — a list — is present only when the message carries blind copies
|
|
48
|
+
* (AGL-3699), and is delivered without appearing on the message's headers.
|
|
49
|
+
*
|
|
47
50
|
* Answer `2xx` once the message is accepted — with `{ "id": "…" }` if the
|
|
48
51
|
* relay has an id for it, which the delivery log keys on. Answer `429`, with
|
|
49
52
|
* `Retry-After` in seconds, to ask for a slower pace: the message is kept
|
|
@@ -69,7 +72,7 @@ export const WEBHOOK_MAIL_PROVIDER_ID = 'webhook';
|
|
|
69
72
|
* caller already handles.
|
|
70
73
|
*/ export const MAIL_WEBHOOK_TIMEOUT_MS = 10000;
|
|
71
74
|
/** The body POSTed for one message. */ export function mailWebhookBody(message, context) {
|
|
72
|
-
var _message_tags;
|
|
75
|
+
var _message_tags, _message_bcc;
|
|
73
76
|
return _extends({
|
|
74
77
|
type: 'mail.send',
|
|
75
78
|
version: 1
|
|
@@ -90,6 +93,8 @@ export const WEBHOOK_MAIL_PROVIDER_ID = 'webhook';
|
|
|
90
93
|
tags: message.tags
|
|
91
94
|
} : {}, message.replyTo ? {
|
|
92
95
|
replyTo: message.replyTo
|
|
96
|
+
} : {}, ((_message_bcc = message.bcc) == null ? void 0 : _message_bcc.length) ? {
|
|
97
|
+
bcc: message.bcc
|
|
93
98
|
} : {})
|
|
94
99
|
});
|
|
95
100
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/mail-provider-webhook.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 * The webhook provider — the sender an operator supplies.\n *\n * Aglyn hands each fully decided message to one endpoint and takes its word\n * for the outcome. What is on the other side is the operator's business: a\n * relay into their own SMTP server, Amazon SES, Postmark, a queue, or twenty\n * lines behind `nc`. Every policy — suppression, the send-rate governor, the\n * phishing screen, the sending identity, the unsubscribe footer — has\n * already been applied by the time the endpoint sees the message.\n *\n * This is the extension point rather than a module path the app imports,\n * for the reason the domain provider's webhook is: both apps are bundled by\n * Next, so a path resolved at runtime is not in the bundle, and an operator\n * running the published image has no build step in which to add one. A\n * webhook needs no rebuild and works from the shipped image unchanged.\n *\n * ## The wire format\n *\n * `POST $AGLYN_MAIL_WEBHOOK_URL`, `Content-Type: application/json`, and\n * `Authorization: Bearer $AGLYN_MAIL_WEBHOOK_TOKEN` when that is set:\n *\n * ```json\n * { \"type\": \"mail.send\", \"version\": 1, \"context\": \"invite\",\n * \"message\": { \"from\": \"\\\"Acme\\\" <hi@acme.com>\", \"to\": [\"ada@example.com\"],\n * \"subject\": \"…\", \"text\": \"…\", \"html\": \"…\",\n * \"headers\": { \"List-Unsubscribe\": \"<…>\" },\n * \"tags\": [{ \"name\": \"context\", \"value\": \"invite\" }],\n * \"replyTo\": \"support@acme.com\" } }\n * ```\n *\n * Answer `2xx` once the message is accepted — with `{ \"id\": \"…\" }` if the\n * relay has an id for it, which the delivery log keys on. Answer `429`, with\n * `Retry-After` in seconds, to ask for a slower pace: the message is kept\n * and retried rather than dropped. Any other status is a refusal of that one\n * message, and the response body is logged as the reason.\n *\n * ## What it does not do\n *\n * It sends and nothing more. It has no credential probe, no delivery feed\n * and no read API, so the staff screens that read those answer \"this\n * provider cannot tell us\" instead of guessing.\n *\n * ⚠️ Send this to a service on your own network. The request carries the\n * bearer token and every message's recipients and body.\n */\n\nimport {\n providerRetryAtMs,\n type MailProvider,\n type MailProviderMessage,\n type MailProviderSendResult,\n} from './mail-provider'\n\nexport const WEBHOOK_MAIL_PROVIDER_ID = 'webhook'\n\n/** Where messages are POSTed. */\nexport const MAIL_WEBHOOK_URL_SETTING = 'AGLYN_MAIL_WEBHOOK_URL'\n\n/** The bearer token sent with each message, when set. */\nexport const MAIL_WEBHOOK_TOKEN_SETTING = 'AGLYN_MAIL_WEBHOOK_TOKEN'\n\n/**\n * Ceiling on one hand-off. A relay that never answers must not hold a\n * request — a sign-up, a checkout — until the platform kills the function;\n * a timeout comes back to `sendEmail` as a network failure, which every\n * caller already handles.\n */\nexport const MAIL_WEBHOOK_TIMEOUT_MS = 10_000\n\ntype Env = Record<string, string | undefined>\n\n/** The body POSTed for one message. */\nexport function mailWebhookBody(\n message: MailProviderMessage,\n context?: string,\n): Record<string, unknown> {\n return {\n type: 'mail.send',\n version: 1,\n ...(context ? { context } : {}),\n message: {\n from: message.from,\n to: message.to,\n subject: message.subject,\n ...(message.text ? { text: message.text } : {}),\n ...(message.html ? { html: message.html } : {}),\n ...(message.headers && Object.keys(message.headers).length\n ? { headers: message.headers }\n : {}),\n ...(message.tags?.length ? { tags: message.tags } : {}),\n ...(message.replyTo ? { replyTo: message.replyTo } : {}),\n },\n }\n}\n\nfunction deadline(): AbortSignal | undefined {\n try {\n return AbortSignal.timeout(MAIL_WEBHOOK_TIMEOUT_MS)\n } catch {\n return undefined\n }\n}\n\n/** The webhook provider, reading its two settings from `env` at each call. */\nexport function webhookMailProvider(env: Env = process.env as Env): MailProvider {\n const url = () => String(env[MAIL_WEBHOOK_URL_SETTING] ?? '').trim()\n const token = () => String(env[MAIL_WEBHOOK_TOKEN_SETTING] ?? '').trim()\n return {\n id: WEBHOOK_MAIL_PROVIDER_ID,\n missingSettings: () => (url() ? [] : [MAIL_WEBHOOK_URL_SETTING]),\n async send(message, context): Promise<MailProviderSendResult> {\n const bearer = token()\n const signal = deadline()\n const response = await fetch(url(), {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),\n },\n body: JSON.stringify(mailWebhookBody(message, context)),\n ...(signal ? { signal } : {}),\n })\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n return response.status === 429\n ? {\n accepted: false,\n rateLimited: true,\n retryAtMs: providerRetryAtMs(response.headers),\n status: response.status,\n detail,\n }\n : { accepted: false, rateLimited: false, status: response.status, detail }\n }\n const body = (await response.json().catch(() => null)) as { id?: unknown } | null\n const id = typeof body?.id === 'string' && body.id.trim() ? body.id.trim() : null\n return { accepted: true, id }\n },\n }\n}\n"],"names":["providerRetryAtMs","WEBHOOK_MAIL_PROVIDER_ID","MAIL_WEBHOOK_URL_SETTING","MAIL_WEBHOOK_TOKEN_SETTING","MAIL_WEBHOOK_TIMEOUT_MS","mailWebhookBody","message","context","type","version","from","to","subject","text","html","headers","Object","keys","length","tags","replyTo","deadline","AbortSignal","timeout","undefined","webhookMailProvider","env","process","url","String","trim","token","id","missingSettings","send","bearer","signal","response","fetch","method","Authorization","body","JSON","stringify","ok","detail","catch","status","accepted","rateLimited","retryAtMs","json"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/mail-provider-webhook.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 * The webhook provider — the sender an operator supplies.\n *\n * Aglyn hands each fully decided message to one endpoint and takes its word\n * for the outcome. What is on the other side is the operator's business: a\n * relay into their own SMTP server, Amazon SES, Postmark, a queue, or twenty\n * lines behind `nc`. Every policy — suppression, the send-rate governor, the\n * phishing screen, the sending identity, the unsubscribe footer — has\n * already been applied by the time the endpoint sees the message.\n *\n * This is the extension point rather than a module path the app imports,\n * for the reason the domain provider's webhook is: both apps are bundled by\n * Next, so a path resolved at runtime is not in the bundle, and an operator\n * running the published image has no build step in which to add one. A\n * webhook needs no rebuild and works from the shipped image unchanged.\n *\n * ## The wire format\n *\n * `POST $AGLYN_MAIL_WEBHOOK_URL`, `Content-Type: application/json`, and\n * `Authorization: Bearer $AGLYN_MAIL_WEBHOOK_TOKEN` when that is set:\n *\n * ```json\n * { \"type\": \"mail.send\", \"version\": 1, \"context\": \"invite\",\n * \"message\": { \"from\": \"\\\"Acme\\\" <hi@acme.com>\", \"to\": [\"ada@example.com\"],\n * \"subject\": \"…\", \"text\": \"…\", \"html\": \"…\",\n * \"headers\": { \"List-Unsubscribe\": \"<…>\" },\n * \"tags\": [{ \"name\": \"context\", \"value\": \"invite\" }],\n * \"replyTo\": \"support@acme.com\" } }\n * ```\n *\n * `bcc` — a list — is present only when the message carries blind copies\n * (AGL-3699), and is delivered without appearing on the message's headers.\n *\n * Answer `2xx` once the message is accepted — with `{ \"id\": \"…\" }` if the\n * relay has an id for it, which the delivery log keys on. Answer `429`, with\n * `Retry-After` in seconds, to ask for a slower pace: the message is kept\n * and retried rather than dropped. Any other status is a refusal of that one\n * message, and the response body is logged as the reason.\n *\n * ## What it does not do\n *\n * It sends and nothing more. It has no credential probe, no delivery feed\n * and no read API, so the staff screens that read those answer \"this\n * provider cannot tell us\" instead of guessing.\n *\n * ⚠️ Send this to a service on your own network. The request carries the\n * bearer token and every message's recipients and body.\n */\n\nimport {\n providerRetryAtMs,\n type MailProvider,\n type MailProviderMessage,\n type MailProviderSendResult,\n} from './mail-provider'\n\nexport const WEBHOOK_MAIL_PROVIDER_ID = 'webhook'\n\n/** Where messages are POSTed. */\nexport const MAIL_WEBHOOK_URL_SETTING = 'AGLYN_MAIL_WEBHOOK_URL'\n\n/** The bearer token sent with each message, when set. */\nexport const MAIL_WEBHOOK_TOKEN_SETTING = 'AGLYN_MAIL_WEBHOOK_TOKEN'\n\n/**\n * Ceiling on one hand-off. A relay that never answers must not hold a\n * request — a sign-up, a checkout — until the platform kills the function;\n * a timeout comes back to `sendEmail` as a network failure, which every\n * caller already handles.\n */\nexport const MAIL_WEBHOOK_TIMEOUT_MS = 10_000\n\ntype Env = Record<string, string | undefined>\n\n/** The body POSTed for one message. */\nexport function mailWebhookBody(\n message: MailProviderMessage,\n context?: string,\n): Record<string, unknown> {\n return {\n type: 'mail.send',\n version: 1,\n ...(context ? { context } : {}),\n message: {\n from: message.from,\n to: message.to,\n subject: message.subject,\n ...(message.text ? { text: message.text } : {}),\n ...(message.html ? { html: message.html } : {}),\n ...(message.headers && Object.keys(message.headers).length\n ? { headers: message.headers }\n : {}),\n ...(message.tags?.length ? { tags: message.tags } : {}),\n ...(message.replyTo ? { replyTo: message.replyTo } : {}),\n ...(message.bcc?.length ? { bcc: message.bcc } : {}),\n },\n }\n}\n\nfunction deadline(): AbortSignal | undefined {\n try {\n return AbortSignal.timeout(MAIL_WEBHOOK_TIMEOUT_MS)\n } catch {\n return undefined\n }\n}\n\n/** The webhook provider, reading its two settings from `env` at each call. */\nexport function webhookMailProvider(env: Env = process.env as Env): MailProvider {\n const url = () => String(env[MAIL_WEBHOOK_URL_SETTING] ?? '').trim()\n const token = () => String(env[MAIL_WEBHOOK_TOKEN_SETTING] ?? '').trim()\n return {\n id: WEBHOOK_MAIL_PROVIDER_ID,\n missingSettings: () => (url() ? [] : [MAIL_WEBHOOK_URL_SETTING]),\n async send(message, context): Promise<MailProviderSendResult> {\n const bearer = token()\n const signal = deadline()\n const response = await fetch(url(), {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),\n },\n body: JSON.stringify(mailWebhookBody(message, context)),\n ...(signal ? { signal } : {}),\n })\n if (!response.ok) {\n const detail = await response.text().catch(() => '')\n return response.status === 429\n ? {\n accepted: false,\n rateLimited: true,\n retryAtMs: providerRetryAtMs(response.headers),\n status: response.status,\n detail,\n }\n : { accepted: false, rateLimited: false, status: response.status, detail }\n }\n const body = (await response.json().catch(() => null)) as { id?: unknown } | null\n const id = typeof body?.id === 'string' && body.id.trim() ? body.id.trim() : null\n return { accepted: true, id }\n },\n }\n}\n"],"names":["providerRetryAtMs","WEBHOOK_MAIL_PROVIDER_ID","MAIL_WEBHOOK_URL_SETTING","MAIL_WEBHOOK_TOKEN_SETTING","MAIL_WEBHOOK_TIMEOUT_MS","mailWebhookBody","message","context","type","version","from","to","subject","text","html","headers","Object","keys","length","tags","replyTo","bcc","deadline","AbortSignal","timeout","undefined","webhookMailProvider","env","process","url","String","trim","token","id","missingSettings","send","bearer","signal","response","fetch","method","Authorization","body","JSON","stringify","ok","detail","catch","status","accepted","rateLimited","retryAtMs","json"],"mappings":"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"}
|
|
@@ -60,6 +60,8 @@ export interface MailProviderMessage {
|
|
|
60
60
|
/** Attribution the delivery feed hands back, e.g. `context`, `campaignId`. */
|
|
61
61
|
tags?: EmailTag[];
|
|
62
62
|
replyTo?: string | string[];
|
|
63
|
+
/** Blind copies (AGL-3699); never on the message's own headers. */
|
|
64
|
+
bcc?: string[];
|
|
63
65
|
}
|
|
64
66
|
/** What the provider said about one message. */
|
|
65
67
|
export type MailProviderSendResult = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/mail-provider.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 * WHO CARRIES THE MAIL — the one seam a sending vendor lives behind.\n *\n * `sendEmail` decides whether a message may leave, as whom and to whom; a\n * provider only carries what it is handed. The same split holds for every\n * other question the platform asks about mail — whether the credential\n * works, what a delivery webhook said, what a received message contained,\n * what the account already sent — so a provider answers them all in the\n * platform's own vocabulary, and nothing outside the provider reads a\n * vendor's wire format.\n *\n * ## The contract every provider owes\n *\n * 1. **`send` never refuses on policy.** Suppression, the send-rate\n * governor, the phishing screen and the sending identity are decided\n * before a provider is asked. A provider answers only \"accepted\", \"not\n * accepted\", or \"not now\" (`rateLimited`), and throws only when it could\n * not ask at all — which `sendEmail` reports as a network failure.\n * 2. **`missingSettings` names what an operator sets.** An empty list means\n * the provider can send. The names are what an operator types into their\n * environment, because they are printed in a skipped send's log line and\n * on the staff email-health screen.\n * 3. **Everything past sending is optional.** A provider with no credential\n * probe, no delivery feed or no read API leaves the member out, and the\n * platform answers \"this provider cannot tell us\" rather than guessing.\n *\n * The providers themselves, and which one a deployment uses, are in\n * `mail-providers.ts`.\n */\n\nimport type { EmailCredentialReport } from './email-health'\nimport type {\n EmailDeliveryEvent,\n EmailDeliveryHistorySource,\n EmailDeliveryMessageSource,\n} from './email-delivery-events'\nimport type { ReceivedEmailSource } from './received-email'\nimport type { EmailTag } from './send-email'\n\n/** One message, fully decided, in the shape every provider is handed. */\nexport interface MailProviderMessage {\n /** The `From:` header value — a verified address, perhaps with a display name. */\n from: string\n /** At least one address; `sendEmail` never hands over an empty list. */\n to: string[]\n subject: string\n text?: string\n html?: string\n headers?: Record<string, string>\n /** Attribution the delivery feed hands back, e.g. `context`, `campaignId`. */\n tags?: EmailTag[]\n replyTo?: string | string[]\n}\n\n/** What the provider said about one message. */\nexport type MailProviderSendResult =\n | { accepted: true; id: string | null }\n | {\n accepted: false\n /**\n * The provider asked for a slower pace, so the message is intact and a\n * later attempt sends it. Distinct from a refusal of the message\n * itself, which a batch settles and never retries.\n */\n rateLimited: boolean\n /** `rateLimited` only: the earliest instant a retry is welcome. */\n retryAtMs?: number\n /** HTTP status, when the provider answered over HTTP. */\n status?: number\n /** The provider's own words, untrimmed; the caller trims for storage. */\n detail: string\n }\n\n/** A sending domain as the provider's account reports it. */\nexport interface MailSendingDomain {\n /** The domain name, lowercased. */\n name: string\n /** `verified` when the provider will accept mail on it. */\n status: string\n /** Whether links on it are rewritten to count clicks; `null` when unreported. */\n clickTracking: boolean | null\n /** The same, for the open pixel. */\n openTracking: boolean | null\n}\n\n/**\n * The provider's reads of its own account: history, one message, received\n * mail and sending domains. Kept apart from sending because a read usually\n * takes a wider credential than a send — a sending key that could enumerate\n * everyone the platform ever wrote to is a worse key to leak.\n */\nexport interface MailProviderReads {\n /**\n * Why reads cannot be made on this deployment — naming the setting to\n * add — or `null` when they can. The functions below throw while it is\n * non-null.\n */\n unmet(): string | null\n history: EmailDeliveryHistorySource\n message: EmailDeliveryMessageSource\n received: ReceivedEmailSource\n /** Every sending domain on the account. Throws when the read fails. */\n sendingDomains(): Promise<MailSendingDomain[]>\n}\n\n/** A received-mail notification, reduced to what a route needs before reading the body. */\nexport interface MailInboundEvent {\n /** The provider's id of the received message — what its reader takes. */\n id: string\n /** Every address the message was for: To, Cc, Bcc and any forwarded target. */\n recipients: string[]\n}\n\nexport interface MailProvider {\n /** Stable slug, stored on delivery records and printed in logs. Never a display name. */\n readonly id: string\n /** Settings sending still needs, as an operator names them; empty when it can send. */\n missingSettings(): string[]\n /**\n * Hands one message to the provider. `context` names the sender for the\n * provider's own error text; it is already on the message as a tag.\n */\n send(\n message: MailProviderMessage,\n context?: string,\n ): Promise<MailProviderSendResult>\n /** Whether the credential is accepted, asked without sending anything. */\n checkCredentials?(): Promise<EmailCredentialReport>\n /** One delivery-webhook payload as zero or more events — one per recipient. */\n deliveryEvents?(payload: unknown, receivedAtMs: number): EmailDeliveryEvent[]\n /** A received-mail notification, or `null` for any other payload. */\n inboundEvent?(payload: unknown): MailInboundEvent | null\n reads?: MailProviderReads\n}\n\n/*==========================================\n * PACING ANSWERS OVER HTTP.\n *=========================================*/\n\n/**\n * How long to wait when the provider names no interval of its own.\n *\n * One second, because a provider's rate limit is counted per second — so a\n * wait of a whole window is the shortest one that is certain to have\n * cleared it.\n */\nconst PROVIDER_RETRY_FALLBACK_MS = 1_000\n\n/**\n * The longest wait a provider header may ask for.\n *\n * A `retry-after` is read off the network and reaches a scheduler, so it is\n * clamped rather than trusted: a header of `86400` would park a campaign for\n * a day on one response nobody saw. An hour is past every documented window\n * and short enough that a wrong one costs a run rather than a day.\n */\nconst PROVIDER_RETRY_MAX_MS = 3_600_000\n\n/** One header as whole seconds, or null when it is absent or unreadable. */\nfunction headerSeconds(\n headers: { get?: (name: string) => string | null } | null | undefined,\n name: string,\n): number | null {\n const raw = headers?.get?.(name)\n // `Number(null)` and `Number('')` are both 0, which would read as \"retry\n // immediately\" for a header that is not there at all.\n if (raw === null || raw === undefined || String(raw).trim() === '') return null\n const seconds = Number(raw)\n return Number.isFinite(seconds) && seconds >= 0 ? seconds : null\n}\n\n/**\n * When an HTTP provider says a refused request may be repeated.\n *\n * Read from the two headers HTTP APIs put beside a 429, both in whole\n * seconds: `retry-after` first because it is the direct answer to this\n * question, then `ratelimit-reset`, which names when the window rolls. A\n * response carrying neither falls back to one window.\n */\nexport function providerRetryAtMs(\n headers: { get?: (name: string) => string | null } | null | undefined,\n nowMs: number = Date.now(),\n): number {\n const seconds =\n headerSeconds(headers, 'retry-after') ??\n headerSeconds(headers, 'ratelimit-reset')\n const waitMs =\n seconds === null\n ? PROVIDER_RETRY_FALLBACK_MS\n : Math.min(seconds * 1_000, PROVIDER_RETRY_MAX_MS)\n return nowMs + waitMs\n}\n"],"names":["PROVIDER_RETRY_FALLBACK_MS","PROVIDER_RETRY_MAX_MS","headerSeconds","headers","name","raw","get","undefined","String","trim","seconds","Number","isFinite","providerRetryAtMs","nowMs","Date","now","waitMs","Math","min"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/mail-provider.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 * WHO CARRIES THE MAIL — the one seam a sending vendor lives behind.\n *\n * `sendEmail` decides whether a message may leave, as whom and to whom; a\n * provider only carries what it is handed. The same split holds for every\n * other question the platform asks about mail — whether the credential\n * works, what a delivery webhook said, what a received message contained,\n * what the account already sent — so a provider answers them all in the\n * platform's own vocabulary, and nothing outside the provider reads a\n * vendor's wire format.\n *\n * ## The contract every provider owes\n *\n * 1. **`send` never refuses on policy.** Suppression, the send-rate\n * governor, the phishing screen and the sending identity are decided\n * before a provider is asked. A provider answers only \"accepted\", \"not\n * accepted\", or \"not now\" (`rateLimited`), and throws only when it could\n * not ask at all — which `sendEmail` reports as a network failure.\n * 2. **`missingSettings` names what an operator sets.** An empty list means\n * the provider can send. The names are what an operator types into their\n * environment, because they are printed in a skipped send's log line and\n * on the staff email-health screen.\n * 3. **Everything past sending is optional.** A provider with no credential\n * probe, no delivery feed or no read API leaves the member out, and the\n * platform answers \"this provider cannot tell us\" rather than guessing.\n *\n * The providers themselves, and which one a deployment uses, are in\n * `mail-providers.ts`.\n */\n\nimport type { EmailCredentialReport } from './email-health'\nimport type {\n EmailDeliveryEvent,\n EmailDeliveryHistorySource,\n EmailDeliveryMessageSource,\n} from './email-delivery-events'\nimport type { ReceivedEmailSource } from './received-email'\nimport type { EmailTag } from './send-email'\n\n/** One message, fully decided, in the shape every provider is handed. */\nexport interface MailProviderMessage {\n /** The `From:` header value — a verified address, perhaps with a display name. */\n from: string\n /** At least one address; `sendEmail` never hands over an empty list. */\n to: string[]\n subject: string\n text?: string\n html?: string\n headers?: Record<string, string>\n /** Attribution the delivery feed hands back, e.g. `context`, `campaignId`. */\n tags?: EmailTag[]\n replyTo?: string | string[]\n /** Blind copies (AGL-3699); never on the message's own headers. */\n bcc?: string[]\n}\n\n/** What the provider said about one message. */\nexport type MailProviderSendResult =\n | { accepted: true; id: string | null }\n | {\n accepted: false\n /**\n * The provider asked for a slower pace, so the message is intact and a\n * later attempt sends it. Distinct from a refusal of the message\n * itself, which a batch settles and never retries.\n */\n rateLimited: boolean\n /** `rateLimited` only: the earliest instant a retry is welcome. */\n retryAtMs?: number\n /** HTTP status, when the provider answered over HTTP. */\n status?: number\n /** The provider's own words, untrimmed; the caller trims for storage. */\n detail: string\n }\n\n/** A sending domain as the provider's account reports it. */\nexport interface MailSendingDomain {\n /** The domain name, lowercased. */\n name: string\n /** `verified` when the provider will accept mail on it. */\n status: string\n /** Whether links on it are rewritten to count clicks; `null` when unreported. */\n clickTracking: boolean | null\n /** The same, for the open pixel. */\n openTracking: boolean | null\n}\n\n/**\n * The provider's reads of its own account: history, one message, received\n * mail and sending domains. Kept apart from sending because a read usually\n * takes a wider credential than a send — a sending key that could enumerate\n * everyone the platform ever wrote to is a worse key to leak.\n */\nexport interface MailProviderReads {\n /**\n * Why reads cannot be made on this deployment — naming the setting to\n * add — or `null` when they can. The functions below throw while it is\n * non-null.\n */\n unmet(): string | null\n history: EmailDeliveryHistorySource\n message: EmailDeliveryMessageSource\n received: ReceivedEmailSource\n /** Every sending domain on the account. Throws when the read fails. */\n sendingDomains(): Promise<MailSendingDomain[]>\n}\n\n/** A received-mail notification, reduced to what a route needs before reading the body. */\nexport interface MailInboundEvent {\n /** The provider's id of the received message — what its reader takes. */\n id: string\n /** Every address the message was for: To, Cc, Bcc and any forwarded target. */\n recipients: string[]\n}\n\nexport interface MailProvider {\n /** Stable slug, stored on delivery records and printed in logs. Never a display name. */\n readonly id: string\n /** Settings sending still needs, as an operator names them; empty when it can send. */\n missingSettings(): string[]\n /**\n * Hands one message to the provider. `context` names the sender for the\n * provider's own error text; it is already on the message as a tag.\n */\n send(\n message: MailProviderMessage,\n context?: string,\n ): Promise<MailProviderSendResult>\n /** Whether the credential is accepted, asked without sending anything. */\n checkCredentials?(): Promise<EmailCredentialReport>\n /** One delivery-webhook payload as zero or more events — one per recipient. */\n deliveryEvents?(payload: unknown, receivedAtMs: number): EmailDeliveryEvent[]\n /** A received-mail notification, or `null` for any other payload. */\n inboundEvent?(payload: unknown): MailInboundEvent | null\n reads?: MailProviderReads\n}\n\n/*==========================================\n * PACING ANSWERS OVER HTTP.\n *=========================================*/\n\n/**\n * How long to wait when the provider names no interval of its own.\n *\n * One second, because a provider's rate limit is counted per second — so a\n * wait of a whole window is the shortest one that is certain to have\n * cleared it.\n */\nconst PROVIDER_RETRY_FALLBACK_MS = 1_000\n\n/**\n * The longest wait a provider header may ask for.\n *\n * A `retry-after` is read off the network and reaches a scheduler, so it is\n * clamped rather than trusted: a header of `86400` would park a campaign for\n * a day on one response nobody saw. An hour is past every documented window\n * and short enough that a wrong one costs a run rather than a day.\n */\nconst PROVIDER_RETRY_MAX_MS = 3_600_000\n\n/** One header as whole seconds, or null when it is absent or unreadable. */\nfunction headerSeconds(\n headers: { get?: (name: string) => string | null } | null | undefined,\n name: string,\n): number | null {\n const raw = headers?.get?.(name)\n // `Number(null)` and `Number('')` are both 0, which would read as \"retry\n // immediately\" for a header that is not there at all.\n if (raw === null || raw === undefined || String(raw).trim() === '') return null\n const seconds = Number(raw)\n return Number.isFinite(seconds) && seconds >= 0 ? seconds : null\n}\n\n/**\n * When an HTTP provider says a refused request may be repeated.\n *\n * Read from the two headers HTTP APIs put beside a 429, both in whole\n * seconds: `retry-after` first because it is the direct answer to this\n * question, then `ratelimit-reset`, which names when the window rolls. A\n * response carrying neither falls back to one window.\n */\nexport function providerRetryAtMs(\n headers: { get?: (name: string) => string | null } | null | undefined,\n nowMs: number = Date.now(),\n): number {\n const seconds =\n headerSeconds(headers, 'retry-after') ??\n headerSeconds(headers, 'ratelimit-reset')\n const waitMs =\n seconds === null\n ? PROVIDER_RETRY_FALLBACK_MS\n : Math.min(seconds * 1_000, PROVIDER_RETRY_MAX_MS)\n return nowMs + waitMs\n}\n"],"names":["PROVIDER_RETRY_FALLBACK_MS","PROVIDER_RETRY_MAX_MS","headerSeconds","headers","name","raw","get","undefined","String","trim","seconds","Number","isFinite","providerRetryAtMs","nowMs","Date","now","waitMs","Math","min"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BC,GA6GD;;2CAE2C,GAE3C;;;;;;CAMC,GACD,MAAMA,6BAA6B;AAEnC;;;;;;;CAOC,GACD,MAAMC,wBAAwB;AAE9B,0EAA0E,GAC1E,SAASC,cACPC,OAAqE,EACrEC,IAAY;QAEAD;IAAZ,MAAME,MAAMF,4BAAAA,eAAAA,QAASG,GAAG,qBAAZH,kBAAAA,SAAeC;IAC3B,yEAAyE;IACzE,sDAAsD;IACtD,IAAIC,QAAQ,QAAQA,QAAQE,aAAaC,OAAOH,KAAKI,IAAI,OAAO,IAAI,OAAO;IAC3E,MAAMC,UAAUC,OAAON;IACvB,OAAOM,OAAOC,QAAQ,CAACF,YAAYA,WAAW,IAAIA,UAAU;AAC9D;AAEA;;;;;;;CAOC,GACD,OAAO,SAASG,kBACdV,OAAqE,EACrEW,QAAgBC,KAAKC,GAAG,EAAE;QAGxBd;IADF,MAAMQ,WACJR,iBAAAA,cAAcC,SAAS,0BAAvBD,iBACAA,cAAcC,SAAS;IACzB,MAAMc,SACJP,YAAY,OACRV,6BACAkB,KAAKC,GAAG,CAACT,UAAU,MAAOT;IAChC,OAAOa,QAAQG;AACjB"}
|
|
@@ -0,0 +1,104 @@
|
|
|
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 GETTING-STARTED EMAILS (AGL-3692).
|
|
19
|
+
*
|
|
20
|
+
* Until these, the platform said one thing to a new account, the welcome
|
|
21
|
+
* email, and then nothing, whether the person built a site, stalled on
|
|
22
|
+
* verification or stopped coming back. Each email here is sent once per
|
|
23
|
+
* crossing of an activation stage by the hourly retention sweep, and is in
|
|
24
|
+
* the system-email catalog so staff can also send it by hand from an
|
|
25
|
+
* account's staff page (AGL-3691).
|
|
26
|
+
*
|
|
27
|
+
* ## Which preference governs them
|
|
28
|
+
*
|
|
29
|
+
* The verification reminder is ACCOUNT mail: it finishes something the
|
|
30
|
+
* person started, so it follows only the suppression list, like the
|
|
31
|
+
* verification email itself.
|
|
32
|
+
*
|
|
33
|
+
* Every other email here is a product tip and belongs to the account's
|
|
34
|
+
* PRODUCT EMAIL answer (`marketingConsent` on `users/{uid}`, the switch under
|
|
35
|
+
* Manage account → Emails). A person who said No there, or left the product
|
|
36
|
+
* updates list from a marketing email (mirrored onto the same answer,
|
|
37
|
+
* AGL-3305), is never sent one. Each carries a link to that switch in its
|
|
38
|
+
* body and in `List-Unsubscribe`.
|
|
39
|
+
*
|
|
40
|
+
* ## Copy rules
|
|
41
|
+
*
|
|
42
|
+
* The heading states the topic, so a reader who skipped the subject still
|
|
43
|
+
* knows what the email is about (AGL-3432). Customer copy says "pages",
|
|
44
|
+
* never "screens". No prices. The product is named by token, so a renamed
|
|
45
|
+
* deployment reads as itself.
|
|
46
|
+
*
|
|
47
|
+
* ## Same design as every other system email
|
|
48
|
+
*
|
|
49
|
+
* Each is a catalog entry like `welcome` or `email-verification`, so it
|
|
50
|
+
* renders through `renderSystemEmail`: the staff design when one is published
|
|
51
|
+
* under Staff → Emails, else these blocks, inside the same header, footer,
|
|
52
|
+
* palette and button. Like the rest, an email carries ONE button; anything
|
|
53
|
+
* else it points at is a caption line with a bare link, the house shape of
|
|
54
|
+
* the opt-out caption `notification` and the digests carry.
|
|
55
|
+
*
|
|
56
|
+
* ## Docs links
|
|
57
|
+
*
|
|
58
|
+
* Each email points at the docs page that walks through its one task. The
|
|
59
|
+
* paths live in {@link RETENTION_DOCS_PATHS}, the merge values are built from
|
|
60
|
+
* the deployment's docs origin by {@link retentionDocsMergeValues} at send
|
|
61
|
+
* time, so the copy names a token and never a host, and a spec holds every
|
|
62
|
+
* path to a real page under `apps/docs`.
|
|
63
|
+
*/
|
|
64
|
+
import type { SystemEmailTemplateDefinition } from './system-email-catalog';
|
|
65
|
+
export declare const RETENTION_VERIFY_REMINDER_EMAIL = "retention-verify-reminder";
|
|
66
|
+
export declare const RETENTION_BUILD_SITE_EMAIL = "retention-build-site";
|
|
67
|
+
export declare const RETENTION_PUBLISH_REMINDER_EMAIL = "retention-publish-reminder";
|
|
68
|
+
export declare const RETENTION_IDLE_EMAIL = "retention-idle";
|
|
69
|
+
export declare const RETENTION_NEXT_STEPS_EMAIL = "retention-next-steps";
|
|
70
|
+
/**
|
|
71
|
+
* The docs pages the getting-started emails link to, by merge token: a path
|
|
72
|
+
* on the docs site, with a heading anchor where the page covers more than
|
|
73
|
+
* the one task. Each is a page under `apps/docs/docs` (the docs site serves
|
|
74
|
+
* it at its root), which `retention-emails.spec.ts` checks.
|
|
75
|
+
*/
|
|
76
|
+
export declare const RETENTION_DOCS_PATHS: {
|
|
77
|
+
readonly 'docs.verifyEmailUrl': "/workspace-and-billing/signing-in-and-sessions#verifying-your-email";
|
|
78
|
+
readonly 'docs.generateSiteUrl': "/ai/generate-a-site";
|
|
79
|
+
readonly 'docs.publishUrl': "/getting-started/publish-your-first-screen";
|
|
80
|
+
readonly 'docs.connectDomainUrl': "/building-sites/custom-domains/connect-a-domain";
|
|
81
|
+
readonly 'docs.formsUrl': "/content-and-data/forms/overview";
|
|
82
|
+
readonly 'docs.generateSectionUrl': "/ai/generate-section";
|
|
83
|
+
readonly 'docs.copyAssistUrl': "/ai/copy-assist";
|
|
84
|
+
};
|
|
85
|
+
export type RetentionDocsToken = keyof typeof RETENTION_DOCS_PATHS;
|
|
86
|
+
/**
|
|
87
|
+
* Every docs link as a merge value, on the given docs origin. The console
|
|
88
|
+
* passes its own docs origin (`DOCS_BASE_URL`), so a self-hosted deployment
|
|
89
|
+
* links its own docs build.
|
|
90
|
+
*/
|
|
91
|
+
export declare function retentionDocsMergeValues(docsOrigin: string): Record<RetentionDocsToken, string>;
|
|
92
|
+
/**
|
|
93
|
+
* The retention emails that are product tips, governed by the account's
|
|
94
|
+
* product email answer. The verification reminder is not one of them.
|
|
95
|
+
*/
|
|
96
|
+
export declare const PRODUCT_TIP_RETENTION_EMAILS: ReadonlySet<string>;
|
|
97
|
+
/**
|
|
98
|
+
* The getting-started entries, built on the catalog's sample console origin.
|
|
99
|
+
*
|
|
100
|
+
* A function rather than a constant so that origin is the catalog's own
|
|
101
|
+
* `SAMPLE_CONSOLE_ORIGIN`, one reading of `NEXT_PUBLIC_CONSOLE_URL` for every
|
|
102
|
+
* preview sample, rather than a second spelled-out default here.
|
|
103
|
+
*/
|
|
104
|
+
export declare function retentionSystemEmailTemplates(SAMPLE_CONSOLE_ORIGIN: string, SAMPLE_DOCS_ORIGIN: string): readonly SystemEmailTemplateDefinition[];
|