@aglyn/tenant-data-admin 1.0.0-beta.200 → 1.0.0-beta.202
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 +8 -7
- package/src/lib/server/card-payment-velocity.d.ts +85 -0
- package/src/lib/server/card-payment-velocity.js +202 -0
- package/src/lib/server/card-payment-velocity.js.map +1 -0
- package/src/lib/server/configured-url-fetch.d.ts +42 -0
- package/src/lib/server/configured-url-fetch.js +118 -0
- package/src/lib/server/configured-url-fetch.js.map +1 -0
- package/src/lib/server/dns-probe.d.ts +7 -0
- package/src/lib/server/dns-probe.js +52 -0
- package/src/lib/server/dns-probe.js.map +1 -1
- package/src/lib/server/email-deliverability.d.ts +150 -0
- package/src/lib/server/email-deliverability.js +538 -0
- package/src/lib/server/email-deliverability.js.map +1 -0
- package/src/lib/server/email-sender-reputation.d.ts +2 -11
- package/src/lib/server/email-sender-reputation.js +8 -25
- package/src/lib/server/email-sender-reputation.js.map +1 -1
- package/src/lib/server/email-suppression.d.ts +10 -1
- package/src/lib/server/email-suppression.js +4 -3
- package/src/lib/server/email-suppression.js.map +1 -1
- package/src/lib/server/erase-person.js +31 -3
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/host-visitor-records.d.ts +16 -2
- package/src/lib/server/host-visitor-records.js +31 -6
- package/src/lib/server/host-visitor-records.js.map +1 -1
- package/src/lib/server/hosted-page-review.d.ts +112 -0
- package/src/lib/server/hosted-page-review.js +378 -0
- package/src/lib/server/hosted-page-review.js.map +1 -0
- package/src/lib/server/member-email-aliases.js +18 -7
- package/src/lib/server/member-email-aliases.js.map +1 -1
- package/src/lib/server/notifications.d.ts +8 -0
- package/src/lib/server/notifications.js +92 -17
- package/src/lib/server/notifications.js.map +1 -1
- package/src/lib/server/org-age.d.ts +27 -0
- package/src/lib/server/org-age.js +44 -0
- package/src/lib/server/org-age.js.map +1 -0
- package/src/lib/server/org-member-notice.js +20 -5
- package/src/lib/server/org-member-notice.js.map +1 -1
- package/src/lib/server/organizations.d.ts +10 -1
- package/src/lib/server/organizations.js +12 -4
- package/src/lib/server/organizations.js.map +1 -1
- package/src/lib/server/outbound-send-review.d.ts +224 -0
- package/src/lib/server/outbound-send-review.js +479 -0
- package/src/lib/server/outbound-send-review.js.map +1 -0
- package/src/lib/server/payment-fraud-signal.d.ts +156 -0
- package/src/lib/server/payment-fraud-signal.js +368 -0
- package/src/lib/server/payment-fraud-signal.js.map +1 -0
- package/src/lib/server/payment-risk-record.d.ts +59 -0
- package/src/lib/server/payment-risk-record.js +76 -0
- package/src/lib/server/payment-risk-record.js.map +1 -0
- package/src/lib/server/render-system-email.d.ts +176 -0
- package/src/lib/server/render-system-email.js +315 -0
- package/src/lib/server/render-system-email.js.map +1 -0
- package/src/lib/server/sending-domains.d.ts +6 -0
- package/src/lib/server/sending-domains.js +98 -6
- package/src/lib/server/sending-domains.js.map +1 -1
- package/src/lib/server/staff-alert-email.d.ts +2 -1
- package/src/lib/server/staff-alert-email.js +16 -5
- package/src/lib/server/staff-alert-email.js.map +1 -1
- package/src/lib/server/stripe-card-authentication.d.ts +35 -0
- package/src/lib/server/stripe-card-authentication.js +63 -0
- package/src/lib/server/stripe-card-authentication.js.map +1 -0
- package/src/lib/server/tenant-write-lockdown.d.ts +32 -2
- package/src/lib/server/tenant-write-lockdown.js +33 -3
- package/src/lib/server/tenant-write-lockdown.js.map +1 -1
- package/src/tenancy.d.ts +5 -0
- package/src/tenancy.js +10 -0
- package/src/tenancy.js.map +1 -1
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /*==========================================
|
|
18
|
+
* THE PLATFORM'S DELIVERABILITY STORE (AGL-3328).
|
|
19
|
+
*
|
|
20
|
+
* The durable half of `@aglyn/shared-util-email`'s deliverability engine:
|
|
21
|
+
* the DNS lookups, the cache and the ledger, and the preflight this module
|
|
22
|
+
* installs on `sendEmail` when it loads — the way `email-send-rate` installs
|
|
23
|
+
* the governor, so every server surface that imports this library's barrel
|
|
24
|
+
* gets it without a call at each entrypoint.
|
|
25
|
+
*
|
|
26
|
+
* ## Two collections at the top level, one under an organization
|
|
27
|
+
*
|
|
28
|
+
* - `mailDomains/{domain}` — a recipient domain's MX answer. A fact about
|
|
29
|
+
* the world, not about an organization, so it is cached ONCE for the whole
|
|
30
|
+
* platform: the first organization to write to a domain warms it for every
|
|
31
|
+
* other. A week for a domain that takes mail, a day for one that does not.
|
|
32
|
+
* - `mailGatewayLedger/{sendingDomain}~{gateway}` — what a gateway did with
|
|
33
|
+
* one sending domain's mail: the Resend path's ledger. A Resend sending
|
|
34
|
+
* domain is one name on one provider account — the platform's own, a
|
|
35
|
+
* shared pool member, or one site's verified domain — so the domain alone
|
|
36
|
+
* is the key, and a pooled or platform domain is flagged `shared`: a block
|
|
37
|
+
* there hurts every tenant, and the staff console shows it.
|
|
38
|
+
* - `orgs/{orgId}/mailGatewayLedger/{sendingDomain}~{gateway}` — the same
|
|
39
|
+
* ledger for a connected mailbox (Sequences). A mailbox's domain is not
|
|
40
|
+
* one organization's — two workspaces can each connect a `gmail.com`
|
|
41
|
+
* mailbox — so its record stays with the organization that sends from it.
|
|
42
|
+
*
|
|
43
|
+
* All three are server-written; clients read them only as the rules allow,
|
|
44
|
+
* staff for the top level and members for their organization's.
|
|
45
|
+
*
|
|
46
|
+
* ## Failing open, and asking twice before saying no
|
|
47
|
+
*
|
|
48
|
+
* Every read here fails OPEN: a resolver that cannot answer, a store that
|
|
49
|
+
* cannot be read, is "unknown", and unknown sends. The one answer that stops
|
|
50
|
+
* mail — "this domain has no MX" — is asked of two resolvers, the pinned
|
|
51
|
+
* public ones and the runtime's own, and stands only when both agree; and a
|
|
52
|
+
* stale "no MX" is never reused when a fresh lookup cannot be made.
|
|
53
|
+
*==========================================*/ import { isPublicMailboxDomain } from "@aglyn/aglyn/app-utils/crm";
|
|
54
|
+
import { applyMailGatewayOutcome, assessEmailDeliverability, decidingDeliverabilityFinding, getEmailConfig, isDeliverabilityDomain, isMailDomainIntelFresh, isMailGatewayBlock, isSharedSendingDomain, mailGatewayLedgerKey, mailGatewayOfHost, mailGatewayStanding, mailDomainRefusesMail, MAIL_GATEWAY_DAILY_DELIVERY_CAP, mailGatewayDay, mailGatewayHolds, normalizeDeliverabilityEmail, normalizeLedgerSendingDomain, readBounceText, readStoredMailDomainIntel, readStoredMailGatewayLedger, resolveMailDomain, scrubBounceDiagnostic, setEmailDeliverabilityPreflight, bareSenderAddress } from "@aglyn/shared-util-email";
|
|
55
|
+
import { promises as dns } from "dns";
|
|
56
|
+
import { isConclusiveDnsCode, lookupAddress, lookupMx } from "./dns-probe.js";
|
|
57
|
+
import { suppressEmail } from "./email-suppression.js";
|
|
58
|
+
import { firebaseAdmin } from "./firebase-admin.js";
|
|
59
|
+
/** The platform MX cache, keyed by recipient domain. */ export const MAIL_DOMAINS_COLLECTION = 'mailDomains';
|
|
60
|
+
/** The gateway ledger, at the top level and under an organization — see the module note. */ export const MAIL_GATEWAY_LEDGER_COLLECTION = 'mailGatewayLedger';
|
|
61
|
+
const defaultFirestore = ()=>firebaseAdmin.app().firestore();
|
|
62
|
+
/** `getAll` takes this many references at most per call. */ const GET_ALL_CHUNK = 300;
|
|
63
|
+
/** Domains looked up at once when a batch is cold. */ const LOOKUP_CONCURRENCY = 16;
|
|
64
|
+
/*==========================================
|
|
65
|
+
* DNS
|
|
66
|
+
*==========================================*/ /** A resolver rejection that means nobody answered, which every caller reads as "unknown". */ function unanswered(domain, what) {
|
|
67
|
+
return Object.assign(new Error(`the ${what} lookup for ${domain} went unanswered`), {
|
|
68
|
+
code: 'EUNANSWERED'
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The platform's resolver: the pinned public resolvers first, and — for the
|
|
73
|
+
* one answer that stops mail, "no MX" — the runtime's own as a second
|
|
74
|
+
* opinion that must agree. See the module note.
|
|
75
|
+
*/ export function platformMailDnsResolver() {
|
|
76
|
+
return {
|
|
77
|
+
async resolveMx (domain) {
|
|
78
|
+
const pinned = await lookupMx(domain);
|
|
79
|
+
if (!pinned.answered) throw unanswered(domain, 'MX');
|
|
80
|
+
if (pinned.records.length) return pinned.records;
|
|
81
|
+
try {
|
|
82
|
+
return (await dns.resolveMx(domain)).map((record)=>{
|
|
83
|
+
var _ref;
|
|
84
|
+
return {
|
|
85
|
+
exchange: String((_ref = record == null ? void 0 : record.exchange) != null ? _ref : '').trim().toLowerCase().replace(/\.$/, ''),
|
|
86
|
+
priority: Number(record == null ? void 0 : record.priority) || 0
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (isConclusiveDnsCode(error == null ? void 0 : error.code)) return [];
|
|
91
|
+
throw unanswered(domain, 'MX');
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
async resolveAddress (domain) {
|
|
95
|
+
const result = await lookupAddress(domain);
|
|
96
|
+
if (!result.answered) throw unanswered(domain, 'address');
|
|
97
|
+
return result.records.length > 0;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/*==========================================
|
|
102
|
+
* THE MX CACHE
|
|
103
|
+
*==========================================*/ /**
|
|
104
|
+
* A process-lifetime copy of the answers already read, so a campaign's
|
|
105
|
+
* five hundred messages to one domain ask the store once. Bounded, oldest
|
|
106
|
+
* out first; trusted exactly as long as the stored answer is.
|
|
107
|
+
*/ const domainMemory = new Map();
|
|
108
|
+
const DOMAIN_MEMORY_MAX = 5000;
|
|
109
|
+
function rememberDomain(intel) {
|
|
110
|
+
if (domainMemory.has(intel.domain)) domainMemory.delete(intel.domain);
|
|
111
|
+
else if (domainMemory.size >= DOMAIN_MEMORY_MAX) {
|
|
112
|
+
const oldest = domainMemory.keys().next().value;
|
|
113
|
+
if (oldest !== undefined) domainMemory.delete(oldest);
|
|
114
|
+
}
|
|
115
|
+
domainMemory.set(intel.domain, intel);
|
|
116
|
+
}
|
|
117
|
+
/** A ledger read, remembered for a minute so a batch does not re-read it per message. */ const ledgerMemory = new Map();
|
|
118
|
+
const LEDGER_MEMORY_MS = 60000;
|
|
119
|
+
const LEDGER_MEMORY_MAX = 2000;
|
|
120
|
+
function rememberLedger(path, ledger, nowMs) {
|
|
121
|
+
if (ledgerMemory.has(path)) ledgerMemory.delete(path);
|
|
122
|
+
else if (ledgerMemory.size >= LEDGER_MEMORY_MAX) {
|
|
123
|
+
const oldest = ledgerMemory.keys().next().value;
|
|
124
|
+
if (oldest !== undefined) ledgerMemory.delete(oldest);
|
|
125
|
+
}
|
|
126
|
+
ledgerMemory.set(path, {
|
|
127
|
+
ledger,
|
|
128
|
+
readAtMs: nowMs
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Test seam: forget everything remembered in this process. */ export function resetMailDeliverabilityMemoryForTests() {
|
|
132
|
+
domainMemory.clear();
|
|
133
|
+
ledgerMemory.clear();
|
|
134
|
+
}
|
|
135
|
+
/** Runs `work` over `items`, at most `limit` at a time. */ async function eachLimited(items, limit, work) {
|
|
136
|
+
let next = 0;
|
|
137
|
+
const runners = Array.from({
|
|
138
|
+
length: Math.min(limit, items.length)
|
|
139
|
+
}, async ()=>{
|
|
140
|
+
while(next < items.length){
|
|
141
|
+
const item = items[next];
|
|
142
|
+
next += 1;
|
|
143
|
+
await work(item);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
await Promise.all(runners);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Each domain's MX answer, keyed by domain: remembered, stored and fresh, or
|
|
150
|
+
* looked up and stored. `null` for a domain that could not be looked up and
|
|
151
|
+
* has no answer that may stand — see the module note.
|
|
152
|
+
*/ export async function readMailDomains(domains, deps = {}) {
|
|
153
|
+
var _deps_nowMs, _deps_firestore, _deps_resolver;
|
|
154
|
+
const nowMs = (_deps_nowMs = deps.nowMs) != null ? _deps_nowMs : Date.now();
|
|
155
|
+
const answers = new Map();
|
|
156
|
+
const wanted = [
|
|
157
|
+
...new Set(domains.map((domain)=>String(domain != null ? domain : '').trim().toLowerCase()))
|
|
158
|
+
].filter(isDeliverabilityDomain);
|
|
159
|
+
const missing = [];
|
|
160
|
+
for (const domain of wanted){
|
|
161
|
+
const remembered = domainMemory.get(domain);
|
|
162
|
+
if (remembered && isMailDomainIntelFresh(remembered, nowMs)) answers.set(domain, remembered);
|
|
163
|
+
else missing.push(domain);
|
|
164
|
+
}
|
|
165
|
+
if (!missing.length) return answers;
|
|
166
|
+
const firestore = (_deps_firestore = deps.firestore) != null ? _deps_firestore : defaultFirestore();
|
|
167
|
+
const collection = firestore.collection(MAIL_DOMAINS_COLLECTION);
|
|
168
|
+
const stored = new Map();
|
|
169
|
+
try {
|
|
170
|
+
for(let start = 0; start < missing.length; start += GET_ALL_CHUNK){
|
|
171
|
+
const chunk = missing.slice(start, start + GET_ALL_CHUNK);
|
|
172
|
+
const snapshots = await firestore.getAll(...chunk.map((domain)=>collection.doc(domain)));
|
|
173
|
+
chunk.forEach((domain, index)=>{
|
|
174
|
+
const snapshot = snapshots[index];
|
|
175
|
+
stored.set(domain, readStoredMailDomainIntel(domain, (snapshot == null ? void 0 : snapshot.exists) ? snapshot.data() : undefined));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('[deliverability] the MX cache could not be read; looking the domains up', error);
|
|
180
|
+
}
|
|
181
|
+
const stale = [];
|
|
182
|
+
for (const domain of missing){
|
|
183
|
+
var _stored_get;
|
|
184
|
+
const answer = (_stored_get = stored.get(domain)) != null ? _stored_get : null;
|
|
185
|
+
if (answer && isMailDomainIntelFresh(answer, nowMs)) {
|
|
186
|
+
answers.set(domain, answer);
|
|
187
|
+
rememberDomain(answer);
|
|
188
|
+
} else {
|
|
189
|
+
stale.push(domain);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const resolver = (_deps_resolver = deps.resolver) != null ? _deps_resolver : platformMailDnsResolver();
|
|
193
|
+
await eachLimited(stale, LOOKUP_CONCURRENCY, async (domain)=>{
|
|
194
|
+
var _stored_get;
|
|
195
|
+
const previous = (_stored_get = stored.get(domain)) != null ? _stored_get : null;
|
|
196
|
+
let fresh;
|
|
197
|
+
try {
|
|
198
|
+
fresh = await resolveMailDomain(resolver, domain, nowMs);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
var _ref;
|
|
201
|
+
// Nobody answered. A stale "takes mail" still says the domain takes
|
|
202
|
+
// mail; a stale "takes none" is not reused to stop a send.
|
|
203
|
+
console.warn(`[deliverability] the MX of ${domain} could not be looked up`, (_ref = error == null ? void 0 : error.message) != null ? _ref : error);
|
|
204
|
+
answers.set(domain, previous && !mailDomainRefusesMail(previous.status) ? previous : null);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
answers.set(domain, fresh);
|
|
208
|
+
rememberDomain(fresh);
|
|
209
|
+
try {
|
|
210
|
+
await collection.doc(domain).set({
|
|
211
|
+
domain,
|
|
212
|
+
status: fresh.status,
|
|
213
|
+
mx: fresh.mx,
|
|
214
|
+
gateway: fresh.gateway,
|
|
215
|
+
resolvedAtMs: fresh.resolvedAtMs
|
|
216
|
+
});
|
|
217
|
+
} catch (error) {
|
|
218
|
+
// The cache is a convenience; the answer is already in hand.
|
|
219
|
+
console.error(`[deliverability] the MX of ${domain} could not be cached`, error);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
return answers;
|
|
223
|
+
}
|
|
224
|
+
/*==========================================
|
|
225
|
+
* THE LEDGER
|
|
226
|
+
*==========================================*/ function ledgerCollection(firestore, scope) {
|
|
227
|
+
return scope ? firestore.collection('orgs').doc(scope.orgId).collection(MAIL_GATEWAY_LEDGER_COLLECTION) : firestore.collection(MAIL_GATEWAY_LEDGER_COLLECTION);
|
|
228
|
+
}
|
|
229
|
+
function ledgerPath(scope, key) {
|
|
230
|
+
return scope ? `orgs/${scope.orgId}/${MAIL_GATEWAY_LEDGER_COLLECTION}/${key}` : `${MAIL_GATEWAY_LEDGER_COLLECTION}/${key}`;
|
|
231
|
+
}
|
|
232
|
+
/** The platform's own sending domain: `USAGE_EMAIL_FROM`'s. */ function platformFromDomain() {
|
|
233
|
+
const address = bareSenderAddress(getEmailConfig().from);
|
|
234
|
+
return address ? address.slice(address.lastIndexOf('@') + 1) : null;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Whether a block on this sending domain hurts every tenant — Aglyn's own
|
|
238
|
+
* domain or a shared pool member — which the staff console surfaces first.
|
|
239
|
+
*/ export function isSharedMailSendingDomain(domain) {
|
|
240
|
+
const name = normalizeLedgerSendingDomain(domain);
|
|
241
|
+
if (!name) return false;
|
|
242
|
+
return isSharedSendingDomain(name) || name === platformFromDomain();
|
|
243
|
+
}
|
|
244
|
+
/** The ledger of each gateway named for one sending domain, keyed by gateway; `null` where none exists yet. */ export async function readMailGatewayLedgers(scope, sendingDomain, gateways, deps = {}) {
|
|
245
|
+
var _deps_nowMs, _deps_firestore;
|
|
246
|
+
const nowMs = (_deps_nowMs = deps.nowMs) != null ? _deps_nowMs : Date.now();
|
|
247
|
+
const domain = normalizeLedgerSendingDomain(sendingDomain);
|
|
248
|
+
const answers = new Map();
|
|
249
|
+
const wanted = [
|
|
250
|
+
...new Set(gateways)
|
|
251
|
+
].filter((gateway)=>gateway !== 'none');
|
|
252
|
+
if (!domain || !wanted.length) return answers;
|
|
253
|
+
const unread = [];
|
|
254
|
+
for (const gateway of wanted){
|
|
255
|
+
const remembered = ledgerMemory.get(ledgerPath(scope, mailGatewayLedgerKey(domain, gateway)));
|
|
256
|
+
if (remembered && remembered.readAtMs + LEDGER_MEMORY_MS > nowMs) answers.set(gateway, remembered.ledger);
|
|
257
|
+
else unread.push(gateway);
|
|
258
|
+
}
|
|
259
|
+
if (!unread.length) return answers;
|
|
260
|
+
const firestore = (_deps_firestore = deps.firestore) != null ? _deps_firestore : defaultFirestore();
|
|
261
|
+
const collection = ledgerCollection(firestore, scope);
|
|
262
|
+
const keys = unread.map((gateway)=>mailGatewayLedgerKey(domain, gateway));
|
|
263
|
+
const snapshots = await firestore.getAll(...keys.map((key)=>collection.doc(key)));
|
|
264
|
+
unread.forEach((gateway, index)=>{
|
|
265
|
+
const snapshot = snapshots[index];
|
|
266
|
+
const ledger = readStoredMailGatewayLedger(keys[index], (snapshot == null ? void 0 : snapshot.exists) ? snapshot.data() : undefined);
|
|
267
|
+
answers.set(gateway, ledger);
|
|
268
|
+
rememberLedger(ledgerPath(scope, keys[index]), ledger, nowMs);
|
|
269
|
+
});
|
|
270
|
+
return answers;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Counts one outcome on a sending domain's ledger with a gateway. Never
|
|
274
|
+
* throws: the send or the bounce it records has already happened, and a
|
|
275
|
+
* lost count understates a gateway while a thrown one loses the step that
|
|
276
|
+
* called. A delivery past the day's cap costs no write at all.
|
|
277
|
+
*/ export async function recordMailGatewayOutcome(scope, input, deps = {}) {
|
|
278
|
+
const sendingDomain = normalizeLedgerSendingDomain(input.sendingDomain);
|
|
279
|
+
if (!sendingDomain || input.gateway === 'none') return;
|
|
280
|
+
const key = mailGatewayLedgerKey(sendingDomain, input.gateway);
|
|
281
|
+
const path = ledgerPath(scope, key);
|
|
282
|
+
if (input.outcome === 'delivered') {
|
|
283
|
+
var _ref;
|
|
284
|
+
var _ledgerMemory_get, _remembered_days;
|
|
285
|
+
const remembered = (_ledgerMemory_get = ledgerMemory.get(path)) == null ? void 0 : _ledgerMemory_get.ledger;
|
|
286
|
+
const today = remembered == null ? void 0 : (_remembered_days = remembered.days) == null ? void 0 : _remembered_days[mailGatewayDay(input.atMs)];
|
|
287
|
+
if (((_ref = today == null ? void 0 : today.delivered) != null ? _ref : 0) >= MAIL_GATEWAY_DAILY_DELIVERY_CAP) return;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
var _deps_firestore;
|
|
291
|
+
const firestore = (_deps_firestore = deps.firestore) != null ? _deps_firestore : defaultFirestore();
|
|
292
|
+
const ref = ledgerCollection(firestore, scope).doc(key);
|
|
293
|
+
const written = await firestore.runTransaction(async (transaction)=>{
|
|
294
|
+
const snapshot = await transaction.get(ref);
|
|
295
|
+
const current = readStoredMailGatewayLedger(key, snapshot.exists ? snapshot.data() : undefined);
|
|
296
|
+
const next = applyMailGatewayOutcome(current, {
|
|
297
|
+
sendingDomain,
|
|
298
|
+
gateway: input.gateway,
|
|
299
|
+
outcome: input.outcome,
|
|
300
|
+
count: input.count,
|
|
301
|
+
atMs: input.atMs,
|
|
302
|
+
detail: scrubBounceDiagnostic(input.detail)
|
|
303
|
+
});
|
|
304
|
+
if (!next) return current;
|
|
305
|
+
transaction.set(ref, _extends({}, next, scope ? {
|
|
306
|
+
orgId: scope.orgId
|
|
307
|
+
} : {
|
|
308
|
+
shared: isSharedMailSendingDomain(sendingDomain)
|
|
309
|
+
}));
|
|
310
|
+
return next;
|
|
311
|
+
});
|
|
312
|
+
rememberLedger(path, written != null ? written : null, input.atMs);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error(`[deliverability] the ledger could not count a ${input.outcome} for ${sendingDomain} at ${input.gateway}`, error);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* The deliverability verdict for every address, keyed by the address as
|
|
319
|
+
* given: the MX answers read (and refreshed) above, joined to the sending
|
|
320
|
+
* domain's ledger with each gateway they name. A ledger that cannot be read
|
|
321
|
+
* reads as no history, which holds nothing.
|
|
322
|
+
*/ export async function readMailDeliverability(question, deps = {}) {
|
|
323
|
+
var _deps_nowMs;
|
|
324
|
+
const nowMs = (_deps_nowMs = deps.nowMs) != null ? _deps_nowMs : Date.now();
|
|
325
|
+
const normalized = new Map();
|
|
326
|
+
for (const email of question.emails)normalized.set(email, normalizeDeliverabilityEmail(email));
|
|
327
|
+
const domains = [
|
|
328
|
+
...normalized.values()
|
|
329
|
+
].filter((email)=>Boolean(email)).map((email)=>email.slice(email.lastIndexOf('@') + 1));
|
|
330
|
+
const intel = await readMailDomains(domains, _extends({}, deps, {
|
|
331
|
+
nowMs
|
|
332
|
+
}));
|
|
333
|
+
const sendingDomain = normalizeLedgerSendingDomain(question.sendingDomain);
|
|
334
|
+
let ledgers = new Map();
|
|
335
|
+
if (sendingDomain) {
|
|
336
|
+
const gateways = [
|
|
337
|
+
...intel.values()
|
|
338
|
+
].filter((entry)=>Boolean(entry)).map((entry)=>entry.gateway);
|
|
339
|
+
try {
|
|
340
|
+
ledgers = await readMailGatewayLedgers(question.scope, sendingDomain, gateways, _extends({}, deps, {
|
|
341
|
+
nowMs
|
|
342
|
+
}));
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error('[deliverability] the gateway ledger could not be read; reading as no history', error);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const verdicts = new Map();
|
|
348
|
+
for (const [given, email] of normalized){
|
|
349
|
+
var _intel_get, _ledgers_get;
|
|
350
|
+
const domain = email ? email.slice(email.lastIndexOf('@') + 1) : null;
|
|
351
|
+
const answer = domain ? (_intel_get = intel.get(domain)) != null ? _intel_get : null : null;
|
|
352
|
+
const standing = answer && sendingDomain ? mailGatewayStanding(answer.gateway, (_ledgers_get = ledgers.get(answer.gateway)) != null ? _ledgers_get : null, nowMs) : null;
|
|
353
|
+
verdicts.set(given, assessEmailDeliverability({
|
|
354
|
+
email: given,
|
|
355
|
+
purpose: question.purpose,
|
|
356
|
+
intel: answer,
|
|
357
|
+
gateway: standing,
|
|
358
|
+
publicMailbox: domain ? isPublicMailboxDomain(domain) : false,
|
|
359
|
+
holdReleased: email ? (question.holdReleased == null ? void 0 : question.holdReleased.call(question, email)) === true : false
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
return verdicts;
|
|
363
|
+
}
|
|
364
|
+
/** Whether a verdict refuses because the domain takes no mail — the refusal that suppresses. */ export function isNoMailServerVerdict(verdict) {
|
|
365
|
+
const deciding = verdict ? decidingDeliverabilityFinding(verdict) : null;
|
|
366
|
+
return (deciding == null ? void 0 : deciding.code) === 'no_mx' || (deciding == null ? void 0 : deciding.code) === 'null_mx';
|
|
367
|
+
}
|
|
368
|
+
/** Whether a verdict holds bulk mail for its gateway. */ export function isGatewayHeldVerdict(verdict) {
|
|
369
|
+
const deciding = verdict ? decidingDeliverabilityFinding(verdict) : null;
|
|
370
|
+
return (deciding == null ? void 0 : deciding.code) === 'gateway_held';
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* A bulk audience with the addresses that would bounce, or that sit behind
|
|
374
|
+
* a gateway holding this sending domain, taken out — the count a campaign's
|
|
375
|
+
* review shows before Send, and the filter its batches run. Fails open: an
|
|
376
|
+
* error keeps everyone.
|
|
377
|
+
*/ export async function filterDeliverableRecipients(input, deps = {}) {
|
|
378
|
+
const answer = {
|
|
379
|
+
deliverable: [],
|
|
380
|
+
noMailServer: [],
|
|
381
|
+
gatewayHeld: []
|
|
382
|
+
};
|
|
383
|
+
let verdicts;
|
|
384
|
+
try {
|
|
385
|
+
var _input_purpose;
|
|
386
|
+
verdicts = await readMailDeliverability({
|
|
387
|
+
emails: input.emails,
|
|
388
|
+
purpose: (_input_purpose = input.purpose) != null ? _input_purpose : 'bulk',
|
|
389
|
+
sendingDomain: input.sendingDomain,
|
|
390
|
+
scope: null
|
|
391
|
+
}, deps);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.error('[deliverability] the audience could not be checked; keeping everyone', error);
|
|
394
|
+
return _extends({}, answer, {
|
|
395
|
+
deliverable: [
|
|
396
|
+
...input.emails
|
|
397
|
+
]
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
for (const email of input.emails){
|
|
401
|
+
const verdict = verdicts.get(email);
|
|
402
|
+
if (isNoMailServerVerdict(verdict)) answer.noMailServer.push(email);
|
|
403
|
+
else if (isGatewayHeldVerdict(verdict)) answer.gatewayHeld.push(email);
|
|
404
|
+
else answer.deliverable.push(email);
|
|
405
|
+
}
|
|
406
|
+
return answer;
|
|
407
|
+
}
|
|
408
|
+
/*==========================================
|
|
409
|
+
* THE PREFLIGHT ON `sendEmail`
|
|
410
|
+
*==========================================*/ /**
|
|
411
|
+
* What `sendEmail` asks before a message leaves — see the seam in
|
|
412
|
+
* `@aglyn/shared-util-email`. Refuses every recipient whose domain takes no
|
|
413
|
+
* mail, and files the address on the platform suppression list with its own
|
|
414
|
+
* reason; holds bulk mail behind a gateway that refused this sending domain
|
|
415
|
+
* twice. The Resend path's ledger is keyed by the sending domain alone.
|
|
416
|
+
*/ export async function runEmailDeliverabilityPreflight(request, deps = {}) {
|
|
417
|
+
const verdicts = await readMailDeliverability({
|
|
418
|
+
emails: request.recipients,
|
|
419
|
+
purpose: request.purpose,
|
|
420
|
+
sendingDomain: request.sendingDomain,
|
|
421
|
+
scope: null
|
|
422
|
+
}, deps);
|
|
423
|
+
const answer = {
|
|
424
|
+
refused: [],
|
|
425
|
+
held: []
|
|
426
|
+
};
|
|
427
|
+
for (const [email, verdict] of verdicts){
|
|
428
|
+
const deciding = decidingDeliverabilityFinding(verdict);
|
|
429
|
+
if (!deciding) continue;
|
|
430
|
+
if (isNoMailServerVerdict(verdict)) {
|
|
431
|
+
var _request_context, _request_hostId;
|
|
432
|
+
answer.refused.push({
|
|
433
|
+
email,
|
|
434
|
+
code: deciding.code,
|
|
435
|
+
reason: deciding.message
|
|
436
|
+
});
|
|
437
|
+
await suppressEmail(_extends({
|
|
438
|
+
email,
|
|
439
|
+
reason: 'no_mail_server',
|
|
440
|
+
context: (_request_context = request.context) != null ? _request_context : null,
|
|
441
|
+
hostId: (_request_hostId = request.hostId) != null ? _request_hostId : null
|
|
442
|
+
}, deps.firestore ? {
|
|
443
|
+
firestore: deps.firestore
|
|
444
|
+
} : {})).catch((error)=>{
|
|
445
|
+
console.error('[deliverability] a refused address could not be suppressed', error);
|
|
446
|
+
});
|
|
447
|
+
} else if (isGatewayHeldVerdict(verdict) && verdict.gateway) {
|
|
448
|
+
answer.held.push({
|
|
449
|
+
email,
|
|
450
|
+
gateway: verdict.gateway.gateway,
|
|
451
|
+
reason: deciding.message
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return answer;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Puts the preflight on `sendEmail`'s path. **Called at module load**, from
|
|
459
|
+
* the bottom of this file, for the reason `installEmailSendGovernor` is.
|
|
460
|
+
* Idempotent; the closure holds no state of its own.
|
|
461
|
+
*/ export function installEmailDeliverabilityPreflight() {
|
|
462
|
+
// A harness that stands a partial email library in for the real one
|
|
463
|
+
// still loads this module; the seam is simply not there to install on.
|
|
464
|
+
if (typeof setEmailDeliverabilityPreflight !== 'function') return;
|
|
465
|
+
setEmailDeliverabilityPreflight((request)=>runEmailDeliverabilityPreflight(request));
|
|
466
|
+
}
|
|
467
|
+
/*==========================================
|
|
468
|
+
* WHAT THE PROVIDER'S EVENTS TEACH THE LEDGER
|
|
469
|
+
*==========================================*/ /** The events the ledger learns from, keyed the way the delivery log decides "first". */ const outcomeKey = (event)=>`${event.providerMessageId}|${event.to}|${event.type}`;
|
|
470
|
+
/**
|
|
471
|
+
* Counts what a Resend webhook reported on the sending domain's ledger: a
|
|
472
|
+
* delivery credits the gateway in front of the recipient, and a permanent
|
|
473
|
+
* bounce that reads as the gateway refusing the SENDER — not an unknown
|
|
474
|
+
* address — counts as a block, filed under the gateway the bounce names
|
|
475
|
+
* (its remote server, or its own words) or else the domain's MX. Only an
|
|
476
|
+
* event the delivery log saw for the first time counts, so a redelivered
|
|
477
|
+
* webhook counts nothing twice. Answers how many outcomes it recorded.
|
|
478
|
+
*/ export async function recordDeliverabilityFromDeliveryEvents(events, outcomes, deps = {}) {
|
|
479
|
+
const first = new Set((outcomes != null ? outcomes : []).filter((outcome)=>outcome.firstOfType).map(outcomeKey));
|
|
480
|
+
const counted = events.filter((event)=>{
|
|
481
|
+
var _event_bounceStatus;
|
|
482
|
+
if (!first.has(outcomeKey(event))) return false;
|
|
483
|
+
if (!normalizeLedgerSendingDomain(bareSenderAddress(event.from))) return false;
|
|
484
|
+
if (event.type === 'delivered') return true;
|
|
485
|
+
return event.type === 'bounced' && event.bounceType === 'permanent' && isMailGatewayBlock({
|
|
486
|
+
status: (_event_bounceStatus = event.bounceStatus) != null ? _event_bounceStatus : null,
|
|
487
|
+
diagnostic: event.detail
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
if (!counted.length) return 0;
|
|
491
|
+
const intel = await readMailDomains(counted.map((event)=>event.to.slice(event.to.lastIndexOf('@') + 1)), deps).catch(()=>new Map());
|
|
492
|
+
let recorded = 0;
|
|
493
|
+
for (const event of counted){
|
|
494
|
+
var _ref, _ref1, _mailGatewayOfHost;
|
|
495
|
+
var _intel_get;
|
|
496
|
+
const sender = bareSenderAddress(event.from);
|
|
497
|
+
const sendingDomain = sender.slice(sender.lastIndexOf('@') + 1);
|
|
498
|
+
const known = (_ref = (_intel_get = intel.get(event.to.slice(event.to.lastIndexOf('@') + 1))) == null ? void 0 : _intel_get.gateway) != null ? _ref : null;
|
|
499
|
+
const gateway = event.type === 'bounced' ? (_ref1 = (_mailGatewayOfHost = mailGatewayOfHost(event.remoteMta)) != null ? _mailGatewayOfHost : readBounceText(event.detail).gateway) != null ? _ref1 : known : known;
|
|
500
|
+
if (!gateway || gateway === 'none') continue;
|
|
501
|
+
// `other` is many unrelated exchanges and never holds, so a delivery
|
|
502
|
+
// there would be a write that decides nothing; a refusal is still kept.
|
|
503
|
+
if (event.type === 'delivered' && gateway === 'other') continue;
|
|
504
|
+
await recordMailGatewayOutcome(null, {
|
|
505
|
+
sendingDomain,
|
|
506
|
+
gateway,
|
|
507
|
+
outcome: event.type === 'bounced' ? 'blocked' : 'delivered',
|
|
508
|
+
atMs: event.at,
|
|
509
|
+
detail: event.type === 'bounced' ? event.detail : null
|
|
510
|
+
}, deps);
|
|
511
|
+
recorded += 1;
|
|
512
|
+
}
|
|
513
|
+
return recorded;
|
|
514
|
+
}
|
|
515
|
+
/** The platform ledger, most recently moved first. */ export async function listMailGatewayLedgers(options = {}, deps = {}) {
|
|
516
|
+
var _deps_nowMs, _deps_firestore;
|
|
517
|
+
const nowMs = (_deps_nowMs = deps.nowMs) != null ? _deps_nowMs : Date.now();
|
|
518
|
+
const firestore = (_deps_firestore = deps.firestore) != null ? _deps_firestore : defaultFirestore();
|
|
519
|
+
const limit = Math.min(Math.max(1, Math.floor(Number(options.limit) || 100)), 500);
|
|
520
|
+
const snapshot = await firestore.collection(MAIL_GATEWAY_LEDGER_COLLECTION).orderBy('updatedAtMs', 'desc').limit(limit).get();
|
|
521
|
+
const rows = [];
|
|
522
|
+
for (const doc of snapshot.docs){
|
|
523
|
+
const data = doc.data();
|
|
524
|
+
const ledger = readStoredMailGatewayLedger(doc.id, data);
|
|
525
|
+
if (!ledger) continue;
|
|
526
|
+
const standing = mailGatewayStanding(ledger.gateway, ledger, nowMs);
|
|
527
|
+
rows.push(_extends({}, ledger, {
|
|
528
|
+
id: doc.id,
|
|
529
|
+
shared: data['shared'] === true || isSharedMailSendingDomain(ledger.sendingDomain),
|
|
530
|
+
standing,
|
|
531
|
+
holds: mailGatewayHolds(standing)
|
|
532
|
+
}));
|
|
533
|
+
}
|
|
534
|
+
return rows;
|
|
535
|
+
}
|
|
536
|
+
installEmailDeliverabilityPreflight();
|
|
537
|
+
|
|
538
|
+
//# sourceMappingURL=email-deliverability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/email-deliverability.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 PLATFORM'S DELIVERABILITY STORE (AGL-3328).\n *\n * The durable half of `@aglyn/shared-util-email`'s deliverability engine:\n * the DNS lookups, the cache and the ledger, and the preflight this module\n * installs on `sendEmail` when it loads — the way `email-send-rate` installs\n * the governor, so every server surface that imports this library's barrel\n * gets it without a call at each entrypoint.\n *\n * ## Two collections at the top level, one under an organization\n *\n * - `mailDomains/{domain}` — a recipient domain's MX answer. A fact about\n * the world, not about an organization, so it is cached ONCE for the whole\n * platform: the first organization to write to a domain warms it for every\n * other. A week for a domain that takes mail, a day for one that does not.\n * - `mailGatewayLedger/{sendingDomain}~{gateway}` — what a gateway did with\n * one sending domain's mail: the Resend path's ledger. A Resend sending\n * domain is one name on one provider account — the platform's own, a\n * shared pool member, or one site's verified domain — so the domain alone\n * is the key, and a pooled or platform domain is flagged `shared`: a block\n * there hurts every tenant, and the staff console shows it.\n * - `orgs/{orgId}/mailGatewayLedger/{sendingDomain}~{gateway}` — the same\n * ledger for a connected mailbox (Sequences). A mailbox's domain is not\n * one organization's — two workspaces can each connect a `gmail.com`\n * mailbox — so its record stays with the organization that sends from it.\n *\n * All three are server-written; clients read them only as the rules allow,\n * staff for the top level and members for their organization's.\n *\n * ## Failing open, and asking twice before saying no\n *\n * Every read here fails OPEN: a resolver that cannot answer, a store that\n * cannot be read, is \"unknown\", and unknown sends. The one answer that stops\n * mail — \"this domain has no MX\" — is asked of two resolvers, the pinned\n * public ones and the runtime's own, and stands only when both agree; and a\n * stale \"no MX\" is never reused when a fresh lookup cannot be made.\n *==========================================*/\n\nimport { isPublicMailboxDomain } from '@aglyn/aglyn/app-utils/crm'\nimport {\n applyMailGatewayOutcome,\n assessEmailDeliverability,\n decidingDeliverabilityFinding,\n type EmailDeliverabilityPreflightRequest,\n type EmailDeliverabilityPreflightVerdict,\n type EmailDeliverabilityVerdict,\n type EmailDeliveryEvent,\n type EmailSendPurpose,\n getEmailConfig,\n isDeliverabilityDomain,\n isMailDomainIntelFresh,\n isMailGatewayBlock,\n isSharedSendingDomain,\n type MailDnsResolver,\n type MailDomainIntel,\n type MailGateway,\n type MailGatewayLedger,\n mailGatewayLedgerKey,\n mailGatewayOfHost,\n type MailGatewayOutcome,\n mailGatewayStanding,\n type MailGatewayStanding,\n mailDomainRefusesMail,\n MAIL_GATEWAY_DAILY_DELIVERY_CAP,\n mailGatewayDay,\n mailGatewayHolds,\n normalizeDeliverabilityEmail,\n normalizeLedgerSendingDomain,\n readBounceText,\n readStoredMailDomainIntel,\n readStoredMailGatewayLedger,\n resolveMailDomain,\n scrubBounceDiagnostic,\n setEmailDeliverabilityPreflight,\n bareSenderAddress,\n} from '@aglyn/shared-util-email'\nimport { promises as dns } from 'dns'\nimport { isConclusiveDnsCode, lookupAddress, lookupMx } from './dns-probe'\nimport type { EmailDeliveryEventOutcome } from './email-delivery-log'\nimport { suppressEmail } from './email-suppression'\nimport { firebaseAdmin } from './firebase-admin'\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The platform MX cache, keyed by recipient domain. */\nexport const MAIL_DOMAINS_COLLECTION = 'mailDomains'\n\n/** The gateway ledger, at the top level and under an organization — see the module note. */\nexport const MAIL_GATEWAY_LEDGER_COLLECTION = 'mailGatewayLedger'\n\n/**\n * Where a ledger lives: `null` for the top level (a Resend sending domain),\n * or the organization a connected mailbox belongs to.\n */\nexport type MailGatewayLedgerScope = { orgId: string } | null\n\n/** What the reads and writes here need beside their input; every field defaults to the platform's. */\nexport interface MailDeliverabilityDeps {\n firestore?: Firestore\n resolver?: MailDnsResolver\n nowMs?: number\n}\n\nconst defaultFirestore = (): Firestore => firebaseAdmin.app().firestore() as Firestore\n\n/** `getAll` takes this many references at most per call. */\nconst GET_ALL_CHUNK = 300\n\n/** Domains looked up at once when a batch is cold. */\nconst LOOKUP_CONCURRENCY = 16\n\n/*==========================================\n * DNS\n *==========================================*/\n\n/** A resolver rejection that means nobody answered, which every caller reads as \"unknown\". */\nfunction unanswered(domain: string, what: string): Error {\n return Object.assign(new Error(`the ${what} lookup for ${domain} went unanswered`), { code: 'EUNANSWERED' })\n}\n\n/**\n * The platform's resolver: the pinned public resolvers first, and — for the\n * one answer that stops mail, \"no MX\" — the runtime's own as a second\n * opinion that must agree. See the module note.\n */\nexport function platformMailDnsResolver(): MailDnsResolver {\n return {\n async resolveMx(domain) {\n const pinned = await lookupMx(domain)\n if (!pinned.answered) throw unanswered(domain, 'MX')\n if (pinned.records.length) return pinned.records\n try {\n return (await dns.resolveMx(domain)).map((record) => ({\n exchange: String(record?.exchange ?? '')\n .trim()\n .toLowerCase()\n .replace(/\\.$/, ''),\n priority: Number(record?.priority) || 0,\n }))\n } catch (error) {\n if (isConclusiveDnsCode((error as NodeJS.ErrnoException)?.code)) return []\n throw unanswered(domain, 'MX')\n }\n },\n async resolveAddress(domain) {\n const result = await lookupAddress(domain)\n if (!result.answered) throw unanswered(domain, 'address')\n return result.records.length > 0\n },\n }\n}\n\n/*==========================================\n * THE MX CACHE\n *==========================================*/\n\n/**\n * A process-lifetime copy of the answers already read, so a campaign's\n * five hundred messages to one domain ask the store once. Bounded, oldest\n * out first; trusted exactly as long as the stored answer is.\n */\nconst domainMemory = new Map<string, MailDomainIntel>()\nconst DOMAIN_MEMORY_MAX = 5_000\n\nfunction rememberDomain(intel: MailDomainIntel): void {\n if (domainMemory.has(intel.domain)) domainMemory.delete(intel.domain)\n else if (domainMemory.size >= DOMAIN_MEMORY_MAX) {\n const oldest = domainMemory.keys().next().value\n if (oldest !== undefined) domainMemory.delete(oldest)\n }\n domainMemory.set(intel.domain, intel)\n}\n\n/** A ledger read, remembered for a minute so a batch does not re-read it per message. */\nconst ledgerMemory = new Map<string, { ledger: MailGatewayLedger | null; readAtMs: number }>()\nconst LEDGER_MEMORY_MS = 60_000\nconst LEDGER_MEMORY_MAX = 2_000\n\nfunction rememberLedger(path: string, ledger: MailGatewayLedger | null, nowMs: number): void {\n if (ledgerMemory.has(path)) ledgerMemory.delete(path)\n else if (ledgerMemory.size >= LEDGER_MEMORY_MAX) {\n const oldest = ledgerMemory.keys().next().value\n if (oldest !== undefined) ledgerMemory.delete(oldest)\n }\n ledgerMemory.set(path, { ledger, readAtMs: nowMs })\n}\n\n/** Test seam: forget everything remembered in this process. */\nexport function resetMailDeliverabilityMemoryForTests(): void {\n domainMemory.clear()\n ledgerMemory.clear()\n}\n\n/** Runs `work` over `items`, at most `limit` at a time. */\nasync function eachLimited<T>(items: readonly T[], limit: number, work: (item: T) => Promise<void>): Promise<void> {\n let next = 0\n const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {\n while (next < items.length) {\n const item = items[next]\n next += 1\n await work(item)\n }\n })\n await Promise.all(runners)\n}\n\n/**\n * Each domain's MX answer, keyed by domain: remembered, stored and fresh, or\n * looked up and stored. `null` for a domain that could not be looked up and\n * has no answer that may stand — see the module note.\n */\nexport async function readMailDomains(\n domains: readonly string[],\n deps: MailDeliverabilityDeps = {},\n): Promise<Map<string, MailDomainIntel | null>> {\n const nowMs = deps.nowMs ?? Date.now()\n const answers = new Map<string, MailDomainIntel | null>()\n const wanted = [...new Set(domains.map((domain) => String(domain ?? '').trim().toLowerCase()))].filter(\n isDeliverabilityDomain,\n )\n const missing: string[] = []\n for (const domain of wanted) {\n const remembered = domainMemory.get(domain)\n if (remembered && isMailDomainIntelFresh(remembered, nowMs)) answers.set(domain, remembered)\n else missing.push(domain)\n }\n if (!missing.length) return answers\n\n const firestore = deps.firestore ?? defaultFirestore()\n const collection = firestore.collection(MAIL_DOMAINS_COLLECTION)\n const stored = new Map<string, MailDomainIntel | null>()\n try {\n for (let start = 0; start < missing.length; start += GET_ALL_CHUNK) {\n const chunk = missing.slice(start, start + GET_ALL_CHUNK)\n const snapshots = await firestore.getAll(...chunk.map((domain) => collection.doc(domain)))\n chunk.forEach((domain, index) => {\n const snapshot = snapshots[index]\n stored.set(domain, readStoredMailDomainIntel(domain, snapshot?.exists ? snapshot.data() : undefined))\n })\n }\n } catch (error) {\n console.error('[deliverability] the MX cache could not be read; looking the domains up', error)\n }\n\n const stale: string[] = []\n for (const domain of missing) {\n const answer = stored.get(domain) ?? null\n if (answer && isMailDomainIntelFresh(answer, nowMs)) {\n answers.set(domain, answer)\n rememberDomain(answer)\n } else {\n stale.push(domain)\n }\n }\n\n const resolver = deps.resolver ?? platformMailDnsResolver()\n await eachLimited(stale, LOOKUP_CONCURRENCY, async (domain) => {\n const previous = stored.get(domain) ?? null\n let fresh: MailDomainIntel\n try {\n fresh = await resolveMailDomain(resolver, domain, nowMs)\n } catch (error) {\n // Nobody answered. A stale \"takes mail\" still says the domain takes\n // mail; a stale \"takes none\" is not reused to stop a send.\n console.warn(`[deliverability] the MX of ${domain} could not be looked up`, (error as Error)?.message ?? error)\n answers.set(domain, previous && !mailDomainRefusesMail(previous.status) ? previous : null)\n return\n }\n answers.set(domain, fresh)\n rememberDomain(fresh)\n try {\n await collection.doc(domain).set({\n domain,\n status: fresh.status,\n mx: fresh.mx,\n gateway: fresh.gateway,\n resolvedAtMs: fresh.resolvedAtMs,\n })\n } catch (error) {\n // The cache is a convenience; the answer is already in hand.\n console.error(`[deliverability] the MX of ${domain} could not be cached`, error)\n }\n })\n return answers\n}\n\n/*==========================================\n * THE LEDGER\n *==========================================*/\n\nfunction ledgerCollection(firestore: Firestore, scope: MailGatewayLedgerScope) {\n return scope\n ? firestore.collection('orgs').doc(scope.orgId).collection(MAIL_GATEWAY_LEDGER_COLLECTION)\n : firestore.collection(MAIL_GATEWAY_LEDGER_COLLECTION)\n}\n\nfunction ledgerPath(scope: MailGatewayLedgerScope, key: string): string {\n return scope ? `orgs/${scope.orgId}/${MAIL_GATEWAY_LEDGER_COLLECTION}/${key}` : `${MAIL_GATEWAY_LEDGER_COLLECTION}/${key}`\n}\n\n/** The platform's own sending domain: `USAGE_EMAIL_FROM`'s. */\nfunction platformFromDomain(): string | null {\n const address = bareSenderAddress(getEmailConfig().from)\n return address ? address.slice(address.lastIndexOf('@') + 1) : null\n}\n\n/**\n * Whether a block on this sending domain hurts every tenant — Aglyn's own\n * domain or a shared pool member — which the staff console surfaces first.\n */\nexport function isSharedMailSendingDomain(domain: string | null | undefined): boolean {\n const name = normalizeLedgerSendingDomain(domain)\n if (!name) return false\n return isSharedSendingDomain(name) || name === platformFromDomain()\n}\n\n/** The ledger of each gateway named for one sending domain, keyed by gateway; `null` where none exists yet. */\nexport async function readMailGatewayLedgers(\n scope: MailGatewayLedgerScope,\n sendingDomain: string,\n gateways: readonly MailGateway[],\n deps: MailDeliverabilityDeps = {},\n): Promise<Map<MailGateway, MailGatewayLedger | null>> {\n const nowMs = deps.nowMs ?? Date.now()\n const domain = normalizeLedgerSendingDomain(sendingDomain)\n const answers = new Map<MailGateway, MailGatewayLedger | null>()\n const wanted = [...new Set(gateways)].filter((gateway) => gateway !== 'none')\n if (!domain || !wanted.length) return answers\n const unread: MailGateway[] = []\n for (const gateway of wanted) {\n const remembered = ledgerMemory.get(ledgerPath(scope, mailGatewayLedgerKey(domain, gateway)))\n if (remembered && remembered.readAtMs + LEDGER_MEMORY_MS > nowMs) answers.set(gateway, remembered.ledger)\n else unread.push(gateway)\n }\n if (!unread.length) return answers\n const firestore = deps.firestore ?? defaultFirestore()\n const collection = ledgerCollection(firestore, scope)\n const keys = unread.map((gateway) => mailGatewayLedgerKey(domain, gateway))\n const snapshots = await firestore.getAll(...keys.map((key) => collection.doc(key)))\n unread.forEach((gateway, index) => {\n const snapshot = snapshots[index]\n const ledger = readStoredMailGatewayLedger(keys[index], snapshot?.exists ? snapshot.data() : undefined)\n answers.set(gateway, ledger)\n rememberLedger(ledgerPath(scope, keys[index]), ledger, nowMs)\n })\n return answers\n}\n\n/**\n * Counts one outcome on a sending domain's ledger with a gateway. Never\n * throws: the send or the bounce it records has already happened, and a\n * lost count understates a gateway while a thrown one loses the step that\n * called. A delivery past the day's cap costs no write at all.\n */\nexport async function recordMailGatewayOutcome(\n scope: MailGatewayLedgerScope,\n input: {\n sendingDomain: string\n gateway: MailGateway\n outcome: MailGatewayOutcome\n count?: number\n atMs: number\n /** A refusal's diagnostic, scrubbed here of addresses before it is kept. */\n detail?: string | null\n },\n deps: MailDeliverabilityDeps = {},\n): Promise<void> {\n const sendingDomain = normalizeLedgerSendingDomain(input.sendingDomain)\n if (!sendingDomain || input.gateway === 'none') return\n const key = mailGatewayLedgerKey(sendingDomain, input.gateway)\n const path = ledgerPath(scope, key)\n if (input.outcome === 'delivered') {\n const remembered = ledgerMemory.get(path)?.ledger\n const today = remembered?.days?.[mailGatewayDay(input.atMs)]\n if ((today?.delivered ?? 0) >= MAIL_GATEWAY_DAILY_DELIVERY_CAP) return\n }\n try {\n const firestore = deps.firestore ?? defaultFirestore()\n const ref = ledgerCollection(firestore, scope).doc(key)\n const written = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const current = readStoredMailGatewayLedger(key, snapshot.exists ? snapshot.data() : undefined)\n const next = applyMailGatewayOutcome(current, {\n sendingDomain,\n gateway: input.gateway,\n outcome: input.outcome,\n count: input.count,\n atMs: input.atMs,\n detail: scrubBounceDiagnostic(input.detail),\n })\n if (!next) return current\n transaction.set(ref, {\n ...next,\n ...(scope ? { orgId: scope.orgId } : { shared: isSharedMailSendingDomain(sendingDomain) }),\n })\n return next\n })\n rememberLedger(path, written ?? null, input.atMs)\n } catch (error) {\n console.error(`[deliverability] the ledger could not count a ${input.outcome} for ${sendingDomain} at ${input.gateway}`, error)\n }\n}\n\n/*==========================================\n * THE VERDICTS\n *==========================================*/\n\n/** What a batch read needs to know about the mail being sent. */\nexport interface MailDeliverabilityQuestion {\n emails: readonly string[]\n purpose: EmailSendPurpose\n /** The domain the mail leaves from; without one, no ledger is read and nothing is held. */\n sendingDomain: string | null\n scope: MailGatewayLedgerScope\n /** Addresses a person already released the gateway hold for. */\n holdReleased?: (email: string) => boolean\n}\n\n/**\n * The deliverability verdict for every address, keyed by the address as\n * given: the MX answers read (and refreshed) above, joined to the sending\n * domain's ledger with each gateway they name. A ledger that cannot be read\n * reads as no history, which holds nothing.\n */\nexport async function readMailDeliverability(\n question: MailDeliverabilityQuestion,\n deps: MailDeliverabilityDeps = {},\n): Promise<Map<string, EmailDeliverabilityVerdict>> {\n const nowMs = deps.nowMs ?? Date.now()\n const normalized = new Map<string, string | null>()\n for (const email of question.emails) normalized.set(email, normalizeDeliverabilityEmail(email))\n const domains = [...normalized.values()]\n .filter((email): email is string => Boolean(email))\n .map((email) => email.slice(email.lastIndexOf('@') + 1))\n const intel = await readMailDomains(domains, { ...deps, nowMs })\n\n const sendingDomain = normalizeLedgerSendingDomain(question.sendingDomain)\n let ledgers = new Map<MailGateway, MailGatewayLedger | null>()\n if (sendingDomain) {\n const gateways = [...intel.values()]\n .filter((entry): entry is MailDomainIntel => Boolean(entry))\n .map((entry) => entry.gateway)\n try {\n ledgers = await readMailGatewayLedgers(question.scope, sendingDomain, gateways, { ...deps, nowMs })\n } catch (error) {\n console.error('[deliverability] the gateway ledger could not be read; reading as no history', error)\n }\n }\n\n const verdicts = new Map<string, EmailDeliverabilityVerdict>()\n for (const [given, email] of normalized) {\n const domain = email ? email.slice(email.lastIndexOf('@') + 1) : null\n const answer = domain ? (intel.get(domain) ?? null) : null\n const standing: MailGatewayStanding | null =\n answer && sendingDomain ? mailGatewayStanding(answer.gateway, ledgers.get(answer.gateway) ?? null, nowMs) : null\n verdicts.set(\n given,\n assessEmailDeliverability({\n email: given,\n purpose: question.purpose,\n intel: answer,\n gateway: standing,\n publicMailbox: domain ? isPublicMailboxDomain(domain) : false,\n holdReleased: email ? question.holdReleased?.(email) === true : false,\n }),\n )\n }\n return verdicts\n}\n\n/** Whether a verdict refuses because the domain takes no mail — the refusal that suppresses. */\nexport function isNoMailServerVerdict(verdict: EmailDeliverabilityVerdict | null | undefined): boolean {\n const deciding = verdict ? decidingDeliverabilityFinding(verdict) : null\n return deciding?.code === 'no_mx' || deciding?.code === 'null_mx'\n}\n\n/** Whether a verdict holds bulk mail for its gateway. */\nexport function isGatewayHeldVerdict(verdict: EmailDeliverabilityVerdict | null | undefined): boolean {\n const deciding = verdict ? decidingDeliverabilityFinding(verdict) : null\n return deciding?.code === 'gateway_held'\n}\n\n/** A campaign's audience after the checks, with who was excluded and why. */\nexport interface DeliverableRecipients {\n deliverable: string[]\n /** Addresses whose domain takes no mail. */\n noMailServer: string[]\n /** Addresses behind a gateway that refused this sending domain twice lately. */\n gatewayHeld: string[]\n}\n\n/**\n * A bulk audience with the addresses that would bounce, or that sit behind\n * a gateway holding this sending domain, taken out — the count a campaign's\n * review shows before Send, and the filter its batches run. Fails open: an\n * error keeps everyone.\n */\nexport async function filterDeliverableRecipients(\n input: { emails: readonly string[]; sendingDomain: string | null; purpose?: EmailSendPurpose },\n deps: MailDeliverabilityDeps = {},\n): Promise<DeliverableRecipients> {\n const answer: DeliverableRecipients = { deliverable: [], noMailServer: [], gatewayHeld: [] }\n let verdicts: Map<string, EmailDeliverabilityVerdict>\n try {\n verdicts = await readMailDeliverability(\n { emails: input.emails, purpose: input.purpose ?? 'bulk', sendingDomain: input.sendingDomain, scope: null },\n deps,\n )\n } catch (error) {\n console.error('[deliverability] the audience could not be checked; keeping everyone', error)\n return { ...answer, deliverable: [...input.emails] }\n }\n for (const email of input.emails) {\n const verdict = verdicts.get(email)\n if (isNoMailServerVerdict(verdict)) answer.noMailServer.push(email)\n else if (isGatewayHeldVerdict(verdict)) answer.gatewayHeld.push(email)\n else answer.deliverable.push(email)\n }\n return answer\n}\n\n/*==========================================\n * THE PREFLIGHT ON `sendEmail`\n *==========================================*/\n\n/**\n * What `sendEmail` asks before a message leaves — see the seam in\n * `@aglyn/shared-util-email`. Refuses every recipient whose domain takes no\n * mail, and files the address on the platform suppression list with its own\n * reason; holds bulk mail behind a gateway that refused this sending domain\n * twice. The Resend path's ledger is keyed by the sending domain alone.\n */\nexport async function runEmailDeliverabilityPreflight(\n request: EmailDeliverabilityPreflightRequest,\n deps: MailDeliverabilityDeps = {},\n): Promise<EmailDeliverabilityPreflightVerdict> {\n const verdicts = await readMailDeliverability(\n {\n emails: request.recipients,\n purpose: request.purpose,\n sendingDomain: request.sendingDomain,\n scope: null,\n },\n deps,\n )\n const answer: EmailDeliverabilityPreflightVerdict = { refused: [], held: [] }\n for (const [email, verdict] of verdicts) {\n const deciding = decidingDeliverabilityFinding(verdict)\n if (!deciding) continue\n if (isNoMailServerVerdict(verdict)) {\n answer.refused.push({ email, code: deciding.code, reason: deciding.message })\n await suppressEmail({\n email,\n reason: 'no_mail_server',\n context: request.context ?? null,\n hostId: request.hostId ?? null,\n ...(deps.firestore ? { firestore: deps.firestore } : {}),\n }).catch((error) => {\n console.error('[deliverability] a refused address could not be suppressed', error)\n })\n } else if (isGatewayHeldVerdict(verdict) && verdict.gateway) {\n answer.held.push({ email, gateway: verdict.gateway.gateway, reason: deciding.message })\n }\n }\n return answer\n}\n\n/**\n * Puts the preflight on `sendEmail`'s path. **Called at module load**, from\n * the bottom of this file, for the reason `installEmailSendGovernor` is.\n * Idempotent; the closure holds no state of its own.\n */\nexport function installEmailDeliverabilityPreflight(): void {\n // A harness that stands a partial email library in for the real one\n // still loads this module; the seam is simply not there to install on.\n if (typeof setEmailDeliverabilityPreflight !== 'function') return\n setEmailDeliverabilityPreflight((request) => runEmailDeliverabilityPreflight(request))\n}\n\n/*==========================================\n * WHAT THE PROVIDER'S EVENTS TEACH THE LEDGER\n *==========================================*/\n\n/** The events the ledger learns from, keyed the way the delivery log decides \"first\". */\nconst outcomeKey = (event: { providerMessageId: string; to: string; type: string }) =>\n `${event.providerMessageId}|${event.to}|${event.type}`\n\n/**\n * Counts what a Resend webhook reported on the sending domain's ledger: a\n * delivery credits the gateway in front of the recipient, and a permanent\n * bounce that reads as the gateway refusing the SENDER — not an unknown\n * address — counts as a block, filed under the gateway the bounce names\n * (its remote server, or its own words) or else the domain's MX. Only an\n * event the delivery log saw for the first time counts, so a redelivered\n * webhook counts nothing twice. Answers how many outcomes it recorded.\n */\nexport async function recordDeliverabilityFromDeliveryEvents(\n events: readonly EmailDeliveryEvent[],\n outcomes: readonly EmailDeliveryEventOutcome[] | null,\n deps: MailDeliverabilityDeps = {},\n): Promise<number> {\n const first = new Set((outcomes ?? []).filter((outcome) => outcome.firstOfType).map(outcomeKey))\n const counted = events.filter((event) => {\n if (!first.has(outcomeKey(event))) return false\n if (!normalizeLedgerSendingDomain(bareSenderAddress(event.from))) return false\n if (event.type === 'delivered') return true\n return (\n event.type === 'bounced' &&\n event.bounceType === 'permanent' &&\n isMailGatewayBlock({ status: event.bounceStatus ?? null, diagnostic: event.detail })\n )\n })\n if (!counted.length) return 0\n const intel = await readMailDomains(\n counted.map((event) => event.to.slice(event.to.lastIndexOf('@') + 1)),\n deps,\n ).catch(() => new Map<string, MailDomainIntel | null>())\n let recorded = 0\n for (const event of counted) {\n const sender = bareSenderAddress(event.from) as string\n const sendingDomain = sender.slice(sender.lastIndexOf('@') + 1)\n const known = intel.get(event.to.slice(event.to.lastIndexOf('@') + 1))?.gateway ?? null\n const gateway =\n event.type === 'bounced'\n ? (mailGatewayOfHost(event.remoteMta) ?? readBounceText(event.detail).gateway ?? known)\n : known\n if (!gateway || gateway === 'none') continue\n // `other` is many unrelated exchanges and never holds, so a delivery\n // there would be a write that decides nothing; a refusal is still kept.\n if (event.type === 'delivered' && gateway === 'other') continue\n await recordMailGatewayOutcome(\n null,\n {\n sendingDomain,\n gateway,\n outcome: event.type === 'bounced' ? 'blocked' : 'delivered',\n atMs: event.at,\n detail: event.type === 'bounced' ? event.detail : null,\n },\n deps,\n )\n recorded += 1\n }\n return recorded\n}\n\n/*==========================================\n * THE STAFF READ\n *==========================================*/\n\n/** One row of the platform ledger as the staff console shows it. */\nexport interface MailGatewayLedgerRow extends MailGatewayLedger {\n id: string\n /** A pooled or platform sending domain: a block there hurts every tenant. */\n shared: boolean\n standing: MailGatewayStanding\n /** Whether bulk mail from this domain into this gateway is held today. */\n holds: boolean\n}\n\n/** The platform ledger, most recently moved first. */\nexport async function listMailGatewayLedgers(\n options: { limit?: number } = {},\n deps: MailDeliverabilityDeps = {},\n): Promise<MailGatewayLedgerRow[]> {\n const nowMs = deps.nowMs ?? Date.now()\n const firestore = deps.firestore ?? defaultFirestore()\n const limit = Math.min(Math.max(1, Math.floor(Number(options.limit) || 100)), 500)\n const snapshot = await firestore\n .collection(MAIL_GATEWAY_LEDGER_COLLECTION)\n .orderBy('updatedAtMs', 'desc')\n .limit(limit)\n .get()\n const rows: MailGatewayLedgerRow[] = []\n for (const doc of snapshot.docs) {\n const data = doc.data() as Record<string, unknown>\n const ledger = readStoredMailGatewayLedger(doc.id, data)\n if (!ledger) continue\n const standing = mailGatewayStanding(ledger.gateway, ledger, nowMs)\n rows.push({\n ...ledger,\n id: doc.id,\n shared: data['shared'] === true || isSharedMailSendingDomain(ledger.sendingDomain),\n standing,\n holds: mailGatewayHolds(standing),\n })\n }\n return rows\n}\n\ninstallEmailDeliverabilityPreflight()\n"],"names":["isPublicMailboxDomain","applyMailGatewayOutcome","assessEmailDeliverability","decidingDeliverabilityFinding","getEmailConfig","isDeliverabilityDomain","isMailDomainIntelFresh","isMailGatewayBlock","isSharedSendingDomain","mailGatewayLedgerKey","mailGatewayOfHost","mailGatewayStanding","mailDomainRefusesMail","MAIL_GATEWAY_DAILY_DELIVERY_CAP","mailGatewayDay","mailGatewayHolds","normalizeDeliverabilityEmail","normalizeLedgerSendingDomain","readBounceText","readStoredMailDomainIntel","readStoredMailGatewayLedger","resolveMailDomain","scrubBounceDiagnostic","setEmailDeliverabilityPreflight","bareSenderAddress","promises","dns","isConclusiveDnsCode","lookupAddress","lookupMx","suppressEmail","firebaseAdmin","MAIL_DOMAINS_COLLECTION","MAIL_GATEWAY_LEDGER_COLLECTION","defaultFirestore","app","firestore","GET_ALL_CHUNK","LOOKUP_CONCURRENCY","unanswered","domain","what","Object","assign","Error","code","platformMailDnsResolver","resolveMx","pinned","answered","records","length","map","record","exchange","String","trim","toLowerCase","replace","priority","Number","error","resolveAddress","result","domainMemory","Map","DOMAIN_MEMORY_MAX","rememberDomain","intel","has","delete","size","oldest","keys","next","value","undefined","set","ledgerMemory","LEDGER_MEMORY_MS","LEDGER_MEMORY_MAX","rememberLedger","path","ledger","nowMs","readAtMs","resetMailDeliverabilityMemoryForTests","clear","eachLimited","items","limit","work","runners","Array","from","Math","min","item","Promise","all","readMailDomains","domains","deps","Date","now","answers","wanted","Set","filter","missing","remembered","get","push","collection","stored","start","chunk","slice","snapshots","getAll","doc","forEach","index","snapshot","exists","data","console","stale","answer","resolver","previous","fresh","warn","message","status","mx","gateway","resolvedAtMs","ledgerCollection","scope","orgId","ledgerPath","key","platformFromDomain","address","lastIndexOf","isSharedMailSendingDomain","name","readMailGatewayLedgers","sendingDomain","gateways","unread","recordMailGatewayOutcome","input","outcome","today","days","atMs","delivered","ref","written","runTransaction","transaction","current","count","detail","shared","readMailDeliverability","question","normalized","email","emails","values","Boolean","ledgers","entry","verdicts","given","standing","purpose","publicMailbox","holdReleased","isNoMailServerVerdict","verdict","deciding","isGatewayHeldVerdict","filterDeliverableRecipients","deliverable","noMailServer","gatewayHeld","runEmailDeliverabilityPreflight","request","recipients","refused","held","reason","context","hostId","catch","installEmailDeliverabilityPreflight","outcomeKey","event","providerMessageId","to","type","recordDeliverabilityFromDeliveryEvents","events","outcomes","first","firstOfType","counted","bounceType","bounceStatus","diagnostic","recorded","sender","known","remoteMta","at","listMailGatewayLedgers","options","max","floor","orderBy","rows","docs","id","holds"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4CAoC4C,GAE5C,SAASA,qBAAqB,QAAQ,6BAA4B;AAClE,SACEC,uBAAuB,EACvBC,yBAAyB,EACzBC,6BAA6B,EAM7BC,cAAc,EACdC,sBAAsB,EACtBC,sBAAsB,EACtBC,kBAAkB,EAClBC,qBAAqB,EAKrBC,oBAAoB,EACpBC,iBAAiB,EAEjBC,mBAAmB,EAEnBC,qBAAqB,EACrBC,+BAA+B,EAC/BC,cAAc,EACdC,gBAAgB,EAChBC,4BAA4B,EAC5BC,4BAA4B,EAC5BC,cAAc,EACdC,yBAAyB,EACzBC,2BAA2B,EAC3BC,iBAAiB,EACjBC,qBAAqB,EACrBC,+BAA+B,EAC/BC,iBAAiB,QACZ,2BAA0B;AACjC,SAASC,YAAYC,GAAG,QAAQ,MAAK;AACrC,SAASC,mBAAmB,EAAEC,aAAa,EAAEC,QAAQ,QAAQ,iBAAa;AAE1E,SAASC,aAAa,QAAQ,yBAAqB;AACnD,SAASC,aAAa,QAAQ,sBAAkB;AAIhD,sDAAsD,GACtD,OAAO,MAAMC,0BAA0B,cAAa;AAEpD,0FAA0F,GAC1F,OAAO,MAAMC,iCAAiC,oBAAmB;AAejE,MAAMC,mBAAmB,IAAiBH,cAAcI,GAAG,GAAGC,SAAS;AAEvE,0DAA0D,GAC1D,MAAMC,gBAAgB;AAEtB,oDAAoD,GACpD,MAAMC,qBAAqB;AAE3B;;4CAE4C,GAE5C,4FAA4F,GAC5F,SAASC,WAAWC,MAAc,EAAEC,IAAY;IAC9C,OAAOC,OAAOC,MAAM,CAAC,IAAIC,MAAM,CAAC,IAAI,EAAEH,KAAK,YAAY,EAAED,OAAO,gBAAgB,CAAC,GAAG;QAAEK,MAAM;IAAc;AAC5G;AAEA;;;;CAIC,GACD,OAAO,SAASC;IACd,OAAO;QACL,MAAMC,WAAUP,MAAM;YACpB,MAAMQ,SAAS,MAAMnB,SAASW;YAC9B,IAAI,CAACQ,OAAOC,QAAQ,EAAE,MAAMV,WAAWC,QAAQ;YAC/C,IAAIQ,OAAOE,OAAO,CAACC,MAAM,EAAE,OAAOH,OAAOE,OAAO;YAChD,IAAI;gBACF,OAAO,AAAC,CAAA,MAAMxB,IAAIqB,SAAS,CAACP,OAAM,EAAGY,GAAG,CAAC,CAACC;;2BAAY;wBACpDC,UAAUC,eAAOF,0BAAAA,OAAQC,QAAQ,mBAAI,IAClCE,IAAI,GACJC,WAAW,GACXC,OAAO,CAAC,OAAO;wBAClBC,UAAUC,OAAOP,0BAAAA,OAAQM,QAAQ,KAAK;oBACxC;;YACF,EAAE,OAAOE,OAAO;gBACd,IAAIlC,oBAAqBkC,yBAAD,AAACA,MAAiChB,IAAI,GAAG,OAAO,EAAE;gBAC1E,MAAMN,WAAWC,QAAQ;YAC3B;QACF;QACA,MAAMsB,gBAAetB,MAAM;YACzB,MAAMuB,SAAS,MAAMnC,cAAcY;YACnC,IAAI,CAACuB,OAAOd,QAAQ,EAAE,MAAMV,WAAWC,QAAQ;YAC/C,OAAOuB,OAAOb,OAAO,CAACC,MAAM,GAAG;QACjC;IACF;AACF;AAEA;;4CAE4C,GAE5C;;;;CAIC,GACD,MAAMa,eAAe,IAAIC;AACzB,MAAMC,oBAAoB;AAE1B,SAASC,eAAeC,KAAsB;IAC5C,IAAIJ,aAAaK,GAAG,CAACD,MAAM5B,MAAM,GAAGwB,aAAaM,MAAM,CAACF,MAAM5B,MAAM;SAC/D,IAAIwB,aAAaO,IAAI,IAAIL,mBAAmB;QAC/C,MAAMM,SAASR,aAAaS,IAAI,GAAGC,IAAI,GAAGC,KAAK;QAC/C,IAAIH,WAAWI,WAAWZ,aAAaM,MAAM,CAACE;IAChD;IACAR,aAAaa,GAAG,CAACT,MAAM5B,MAAM,EAAE4B;AACjC;AAEA,uFAAuF,GACvF,MAAMU,eAAe,IAAIb;AACzB,MAAMc,mBAAmB;AACzB,MAAMC,oBAAoB;AAE1B,SAASC,eAAeC,IAAY,EAAEC,MAAgC,EAAEC,KAAa;IACnF,IAAIN,aAAaT,GAAG,CAACa,OAAOJ,aAAaR,MAAM,CAACY;SAC3C,IAAIJ,aAAaP,IAAI,IAAIS,mBAAmB;QAC/C,MAAMR,SAASM,aAAaL,IAAI,GAAGC,IAAI,GAAGC,KAAK;QAC/C,IAAIH,WAAWI,WAAWE,aAAaR,MAAM,CAACE;IAChD;IACAM,aAAaD,GAAG,CAACK,MAAM;QAAEC;QAAQE,UAAUD;IAAM;AACnD;AAEA,6DAA6D,GAC7D,OAAO,SAASE;IACdtB,aAAauB,KAAK;IAClBT,aAAaS,KAAK;AACpB;AAEA,yDAAyD,GACzD,eAAeC,YAAeC,KAAmB,EAAEC,KAAa,EAAEC,IAAgC;IAChG,IAAIjB,OAAO;IACX,MAAMkB,UAAUC,MAAMC,IAAI,CAAC;QAAE3C,QAAQ4C,KAAKC,GAAG,CAACN,OAAOD,MAAMtC,MAAM;IAAE,GAAG;QACpE,MAAOuB,OAAOe,MAAMtC,MAAM,CAAE;YAC1B,MAAM8C,OAAOR,KAAK,CAACf,KAAK;YACxBA,QAAQ;YACR,MAAMiB,KAAKM;QACb;IACF;IACA,MAAMC,QAAQC,GAAG,CAACP;AACpB;AAEA;;;;CAIC,GACD,OAAO,eAAeQ,gBACpBC,OAA0B,EAC1BC,OAA+B,CAAC,CAAC;QAEnBA,aAaIA,iBA2BDA;IAxCjB,MAAMlB,SAAQkB,cAAAA,KAAKlB,KAAK,YAAVkB,cAAcC,KAAKC,GAAG;IACpC,MAAMC,UAAU,IAAIxC;IACpB,MAAMyC,SAAS;WAAI,IAAIC,IAAIN,QAAQjD,GAAG,CAAC,CAACZ,SAAWe,OAAOf,iBAAAA,SAAU,IAAIgB,IAAI,GAAGC,WAAW;KAAK,CAACmD,MAAM,CACpGvG;IAEF,MAAMwG,UAAoB,EAAE;IAC5B,KAAK,MAAMrE,UAAUkE,OAAQ;QAC3B,MAAMI,aAAa9C,aAAa+C,GAAG,CAACvE;QACpC,IAAIsE,cAAcxG,uBAAuBwG,YAAY1B,QAAQqB,QAAQ5B,GAAG,CAACrC,QAAQsE;aAC5ED,QAAQG,IAAI,CAACxE;IACpB;IACA,IAAI,CAACqE,QAAQ1D,MAAM,EAAE,OAAOsD;IAE5B,MAAMrE,aAAYkE,kBAAAA,KAAKlE,SAAS,YAAdkE,kBAAkBpE;IACpC,MAAM+E,aAAa7E,UAAU6E,UAAU,CAACjF;IACxC,MAAMkF,SAAS,IAAIjD;IACnB,IAAI;QACF,IAAK,IAAIkD,QAAQ,GAAGA,QAAQN,QAAQ1D,MAAM,EAAEgE,SAAS9E,cAAe;YAClE,MAAM+E,QAAQP,QAAQQ,KAAK,CAACF,OAAOA,QAAQ9E;YAC3C,MAAMiF,YAAY,MAAMlF,UAAUmF,MAAM,IAAIH,MAAMhE,GAAG,CAAC,CAACZ,SAAWyE,WAAWO,GAAG,CAAChF;YACjF4E,MAAMK,OAAO,CAAC,CAACjF,QAAQkF;gBACrB,MAAMC,WAAWL,SAAS,CAACI,MAAM;gBACjCR,OAAOrC,GAAG,CAACrC,QAAQrB,0BAA0BqB,QAAQmF,CAAAA,4BAAAA,SAAUC,MAAM,IAAGD,SAASE,IAAI,KAAKjD;YAC5F;QACF;IACF,EAAE,OAAOf,OAAO;QACdiE,QAAQjE,KAAK,CAAC,2EAA2EA;IAC3F;IAEA,MAAMkE,QAAkB,EAAE;IAC1B,KAAK,MAAMvF,UAAUqE,QAAS;YACbK;QAAf,MAAMc,UAASd,cAAAA,OAAOH,GAAG,CAACvE,mBAAX0E,cAAsB;QACrC,IAAIc,UAAU1H,uBAAuB0H,QAAQ5C,QAAQ;YACnDqB,QAAQ5B,GAAG,CAACrC,QAAQwF;YACpB7D,eAAe6D;QACjB,OAAO;YACLD,MAAMf,IAAI,CAACxE;QACb;IACF;IAEA,MAAMyF,YAAW3B,iBAAAA,KAAK2B,QAAQ,YAAb3B,iBAAiBxD;IAClC,MAAM0C,YAAYuC,OAAOzF,oBAAoB,OAAOE;YACjC0E;QAAjB,MAAMgB,YAAWhB,cAAAA,OAAOH,GAAG,CAACvE,mBAAX0E,cAAsB;QACvC,IAAIiB;QACJ,IAAI;YACFA,QAAQ,MAAM9G,kBAAkB4G,UAAUzF,QAAQ4C;QACpD,EAAE,OAAOvB,OAAO;;YACd,oEAAoE;YACpE,2DAA2D;YAC3DiE,QAAQM,IAAI,CAAC,CAAC,2BAA2B,EAAE5F,OAAO,uBAAuB,CAAC,UAAGqB,yBAAD,AAACA,MAAiBwE,OAAO,mBAAIxE;YACzG4C,QAAQ5B,GAAG,CAACrC,QAAQ0F,YAAY,CAACtH,sBAAsBsH,SAASI,MAAM,IAAIJ,WAAW;YACrF;QACF;QACAzB,QAAQ5B,GAAG,CAACrC,QAAQ2F;QACpBhE,eAAegE;QACf,IAAI;YACF,MAAMlB,WAAWO,GAAG,CAAChF,QAAQqC,GAAG,CAAC;gBAC/BrC;gBACA8F,QAAQH,MAAMG,MAAM;gBACpBC,IAAIJ,MAAMI,EAAE;gBACZC,SAASL,MAAMK,OAAO;gBACtBC,cAAcN,MAAMM,YAAY;YAClC;QACF,EAAE,OAAO5E,OAAO;YACd,6DAA6D;YAC7DiE,QAAQjE,KAAK,CAAC,CAAC,2BAA2B,EAAErB,OAAO,oBAAoB,CAAC,EAAEqB;QAC5E;IACF;IACA,OAAO4C;AACT;AAEA;;4CAE4C,GAE5C,SAASiC,iBAAiBtG,SAAoB,EAAEuG,KAA6B;IAC3E,OAAOA,QACHvG,UAAU6E,UAAU,CAAC,QAAQO,GAAG,CAACmB,MAAMC,KAAK,EAAE3B,UAAU,CAAChF,kCACzDG,UAAU6E,UAAU,CAAChF;AAC3B;AAEA,SAAS4G,WAAWF,KAA6B,EAAEG,GAAW;IAC5D,OAAOH,QAAQ,CAAC,KAAK,EAAEA,MAAMC,KAAK,CAAC,CAAC,EAAE3G,+BAA+B,CAAC,EAAE6G,KAAK,GAAG,GAAG7G,+BAA+B,CAAC,EAAE6G,KAAK;AAC5H;AAEA,6DAA6D,GAC7D,SAASC;IACP,MAAMC,UAAUxH,kBAAkBpB,iBAAiB0F,IAAI;IACvD,OAAOkD,UAAUA,QAAQ3B,KAAK,CAAC2B,QAAQC,WAAW,CAAC,OAAO,KAAK;AACjE;AAEA;;;CAGC,GACD,OAAO,SAASC,0BAA0B1G,MAAiC;IACzE,MAAM2G,OAAOlI,6BAA6BuB;IAC1C,IAAI,CAAC2G,MAAM,OAAO;IAClB,OAAO3I,sBAAsB2I,SAASA,SAASJ;AACjD;AAEA,6GAA6G,GAC7G,OAAO,eAAeK,uBACpBT,KAA6B,EAC7BU,aAAqB,EACrBC,QAAgC,EAChChD,OAA+B,CAAC,CAAC;QAEnBA,aAYIA;IAZlB,MAAMlB,SAAQkB,cAAAA,KAAKlB,KAAK,YAAVkB,cAAcC,KAAKC,GAAG;IACpC,MAAMhE,SAASvB,6BAA6BoI;IAC5C,MAAM5C,UAAU,IAAIxC;IACpB,MAAMyC,SAAS;WAAI,IAAIC,IAAI2C;KAAU,CAAC1C,MAAM,CAAC,CAAC4B,UAAYA,YAAY;IACtE,IAAI,CAAChG,UAAU,CAACkE,OAAOvD,MAAM,EAAE,OAAOsD;IACtC,MAAM8C,SAAwB,EAAE;IAChC,KAAK,MAAMf,WAAW9B,OAAQ;QAC5B,MAAMI,aAAahC,aAAaiC,GAAG,CAAC8B,WAAWF,OAAOlI,qBAAqB+B,QAAQgG;QACnF,IAAI1B,cAAcA,WAAWzB,QAAQ,GAAGN,mBAAmBK,OAAOqB,QAAQ5B,GAAG,CAAC2D,SAAS1B,WAAW3B,MAAM;aACnGoE,OAAOvC,IAAI,CAACwB;IACnB;IACA,IAAI,CAACe,OAAOpG,MAAM,EAAE,OAAOsD;IAC3B,MAAMrE,aAAYkE,kBAAAA,KAAKlE,SAAS,YAAdkE,kBAAkBpE;IACpC,MAAM+E,aAAayB,iBAAiBtG,WAAWuG;IAC/C,MAAMlE,OAAO8E,OAAOnG,GAAG,CAAC,CAACoF,UAAY/H,qBAAqB+B,QAAQgG;IAClE,MAAMlB,YAAY,MAAMlF,UAAUmF,MAAM,IAAI9C,KAAKrB,GAAG,CAAC,CAAC0F,MAAQ7B,WAAWO,GAAG,CAACsB;IAC7ES,OAAO9B,OAAO,CAAC,CAACe,SAASd;QACvB,MAAMC,WAAWL,SAAS,CAACI,MAAM;QACjC,MAAMvC,SAAS/D,4BAA4BqD,IAAI,CAACiD,MAAM,EAAEC,CAAAA,4BAAAA,SAAUC,MAAM,IAAGD,SAASE,IAAI,KAAKjD;QAC7F6B,QAAQ5B,GAAG,CAAC2D,SAASrD;QACrBF,eAAe4D,WAAWF,OAAOlE,IAAI,CAACiD,MAAM,GAAGvC,QAAQC;IACzD;IACA,OAAOqB;AACT;AAEA;;;;;CAKC,GACD,OAAO,eAAe+C,yBACpBb,KAA6B,EAC7Bc,KAQC,EACDnD,OAA+B,CAAC,CAAC;IAEjC,MAAM+C,gBAAgBpI,6BAA6BwI,MAAMJ,aAAa;IACtE,IAAI,CAACA,iBAAiBI,MAAMjB,OAAO,KAAK,QAAQ;IAChD,MAAMM,MAAMrI,qBAAqB4I,eAAeI,MAAMjB,OAAO;IAC7D,MAAMtD,OAAO2D,WAAWF,OAAOG;IAC/B,IAAIW,MAAMC,OAAO,KAAK,aAAa;;YACd5E,mBACLgC;QADd,MAAMA,cAAahC,oBAAAA,aAAaiC,GAAG,CAAC7B,0BAAjBJ,kBAAwBK,MAAM;QACjD,MAAMwE,QAAQ7C,+BAAAA,mBAAAA,WAAY8C,IAAI,qBAAhB9C,gBAAkB,CAAChG,eAAe2I,MAAMI,IAAI,EAAE;QAC5D,IAAI,SAACF,yBAAAA,MAAOG,SAAS,mBAAI,MAAMjJ,iCAAiC;IAClE;IACA,IAAI;YACgByF;QAAlB,MAAMlE,aAAYkE,kBAAAA,KAAKlE,SAAS,YAAdkE,kBAAkBpE;QACpC,MAAM6H,MAAMrB,iBAAiBtG,WAAWuG,OAAOnB,GAAG,CAACsB;QACnD,MAAMkB,UAAU,MAAM5H,UAAU6H,cAAc,CAAC,OAAOC;YACpD,MAAMvC,WAAW,MAAMuC,YAAYnD,GAAG,CAACgD;YACvC,MAAMI,UAAU/I,4BAA4B0H,KAAKnB,SAASC,MAAM,GAAGD,SAASE,IAAI,KAAKjD;YACrF,MAAMF,OAAOzE,wBAAwBkK,SAAS;gBAC5Cd;gBACAb,SAASiB,MAAMjB,OAAO;gBACtBkB,SAASD,MAAMC,OAAO;gBACtBU,OAAOX,MAAMW,KAAK;gBAClBP,MAAMJ,MAAMI,IAAI;gBAChBQ,QAAQ/I,sBAAsBmI,MAAMY,MAAM;YAC5C;YACA,IAAI,CAAC3F,MAAM,OAAOyF;YAClBD,YAAYrF,GAAG,CAACkF,KAAK,aAChBrF,MACCiE,QAAQ;gBAAEC,OAAOD,MAAMC,KAAK;YAAC,IAAI;gBAAE0B,QAAQpB,0BAA0BG;YAAe;YAE1F,OAAO3E;QACT;QACAO,eAAeC,MAAM8E,kBAAAA,UAAW,MAAMP,MAAMI,IAAI;IAClD,EAAE,OAAOhG,OAAO;QACdiE,QAAQjE,KAAK,CAAC,CAAC,8CAA8C,EAAE4F,MAAMC,OAAO,CAAC,KAAK,EAAEL,cAAc,IAAI,EAAEI,MAAMjB,OAAO,EAAE,EAAE3E;IAC3H;AACF;AAiBA;;;;;CAKC,GACD,OAAO,eAAe0G,uBACpBC,QAAoC,EACpClE,OAA+B,CAAC,CAAC;QAEnBA;IAAd,MAAMlB,SAAQkB,cAAAA,KAAKlB,KAAK,YAAVkB,cAAcC,KAAKC,GAAG;IACpC,MAAMiE,aAAa,IAAIxG;IACvB,KAAK,MAAMyG,SAASF,SAASG,MAAM,CAAEF,WAAW5F,GAAG,CAAC6F,OAAO1J,6BAA6B0J;IACxF,MAAMrE,UAAU;WAAIoE,WAAWG,MAAM;KAAG,CACrChE,MAAM,CAAC,CAAC8D,QAA2BG,QAAQH,QAC3CtH,GAAG,CAAC,CAACsH,QAAUA,MAAMrD,KAAK,CAACqD,MAAMzB,WAAW,CAAC,OAAO;IACvD,MAAM7E,QAAQ,MAAMgC,gBAAgBC,SAAS,aAAKC;QAAMlB;;IAExD,MAAMiE,gBAAgBpI,6BAA6BuJ,SAASnB,aAAa;IACzE,IAAIyB,UAAU,IAAI7G;IAClB,IAAIoF,eAAe;QACjB,MAAMC,WAAW;eAAIlF,MAAMwG,MAAM;SAAG,CACjChE,MAAM,CAAC,CAACmE,QAAoCF,QAAQE,QACpD3H,GAAG,CAAC,CAAC2H,QAAUA,MAAMvC,OAAO;QAC/B,IAAI;YACFsC,UAAU,MAAM1B,uBAAuBoB,SAAS7B,KAAK,EAAEU,eAAeC,UAAU,aAAKhD;gBAAMlB;;QAC7F,EAAE,OAAOvB,OAAO;YACdiE,QAAQjE,KAAK,CAAC,gFAAgFA;QAChG;IACF;IAEA,MAAMmH,WAAW,IAAI/G;IACrB,KAAK,MAAM,CAACgH,OAAOP,MAAM,IAAID,WAAY;YAEdrG,YAEuC0G;QAHhE,MAAMtI,SAASkI,QAAQA,MAAMrD,KAAK,CAACqD,MAAMzB,WAAW,CAAC,OAAO,KAAK;QACjE,MAAMjB,SAASxF,UAAU4B,aAAAA,MAAM2C,GAAG,CAACvE,mBAAV4B,aAAqB,OAAQ;QACtD,MAAM8G,WACJlD,UAAUqB,gBAAgB1I,oBAAoBqH,OAAOQ,OAAO,GAAEsC,eAAAA,QAAQ/D,GAAG,CAACiB,OAAOQ,OAAO,aAA1BsC,eAA+B,MAAM1F,SAAS;QAC9G4F,SAASnG,GAAG,CACVoG,OACA/K,0BAA0B;YACxBwK,OAAOO;YACPE,SAASX,SAASW,OAAO;YACzB/G,OAAO4D;YACPQ,SAAS0C;YACTE,eAAe5I,SAASxC,sBAAsBwC,UAAU;YACxD6I,cAAcX,QAAQF,CAAAA,SAASa,YAAY,oBAArBb,SAASa,YAAY,MAArBb,UAAwBE,YAAW,OAAO;QAClE;IAEJ;IACA,OAAOM;AACT;AAEA,8FAA8F,GAC9F,OAAO,SAASM,sBAAsBC,OAAsD;IAC1F,MAAMC,WAAWD,UAAUpL,8BAA8BoL,WAAW;IACpE,OAAOC,CAAAA,4BAAAA,SAAU3I,IAAI,MAAK,WAAW2I,CAAAA,4BAAAA,SAAU3I,IAAI,MAAK;AAC1D;AAEA,uDAAuD,GACvD,OAAO,SAAS4I,qBAAqBF,OAAsD;IACzF,MAAMC,WAAWD,UAAUpL,8BAA8BoL,WAAW;IACpE,OAAOC,CAAAA,4BAAAA,SAAU3I,IAAI,MAAK;AAC5B;AAWA;;;;;CAKC,GACD,OAAO,eAAe6I,4BACpBjC,KAA8F,EAC9FnD,OAA+B,CAAC,CAAC;IAEjC,MAAM0B,SAAgC;QAAE2D,aAAa,EAAE;QAAEC,cAAc,EAAE;QAAEC,aAAa,EAAE;IAAC;IAC3F,IAAIb;IACJ,IAAI;YAEiCvB;QADnCuB,WAAW,MAAMT,uBACf;YAAEI,QAAQlB,MAAMkB,MAAM;YAAEQ,OAAO,GAAE1B,iBAAAA,MAAM0B,OAAO,YAAb1B,iBAAiB;YAAQJ,eAAeI,MAAMJ,aAAa;YAAEV,OAAO;QAAK,GAC1GrC;IAEJ,EAAE,OAAOzC,OAAO;QACdiE,QAAQjE,KAAK,CAAC,wEAAwEA;QACtF,OAAO,aAAKmE;YAAQ2D,aAAa;mBAAIlC,MAAMkB,MAAM;aAAC;;IACpD;IACA,KAAK,MAAMD,SAASjB,MAAMkB,MAAM,CAAE;QAChC,MAAMY,UAAUP,SAASjE,GAAG,CAAC2D;QAC7B,IAAIY,sBAAsBC,UAAUvD,OAAO4D,YAAY,CAAC5E,IAAI,CAAC0D;aACxD,IAAIe,qBAAqBF,UAAUvD,OAAO6D,WAAW,CAAC7E,IAAI,CAAC0D;aAC3D1C,OAAO2D,WAAW,CAAC3E,IAAI,CAAC0D;IAC/B;IACA,OAAO1C;AACT;AAEA;;4CAE4C,GAE5C;;;;;;CAMC,GACD,OAAO,eAAe8D,gCACpBC,OAA4C,EAC5CzF,OAA+B,CAAC,CAAC;IAEjC,MAAM0E,WAAW,MAAMT,uBACrB;QACEI,QAAQoB,QAAQC,UAAU;QAC1Bb,SAASY,QAAQZ,OAAO;QACxB9B,eAAe0C,QAAQ1C,aAAa;QACpCV,OAAO;IACT,GACArC;IAEF,MAAM0B,SAA8C;QAAEiE,SAAS,EAAE;QAAEC,MAAM,EAAE;IAAC;IAC5E,KAAK,MAAM,CAACxB,OAAOa,QAAQ,IAAIP,SAAU;QACvC,MAAMQ,WAAWrL,8BAA8BoL;QAC/C,IAAI,CAACC,UAAU;QACf,IAAIF,sBAAsBC,UAAU;gBAKvBQ,kBACDA;YALV/D,OAAOiE,OAAO,CAACjF,IAAI,CAAC;gBAAE0D;gBAAO7H,MAAM2I,SAAS3I,IAAI;gBAAEsJ,QAAQX,SAASnD,OAAO;YAAC;YAC3E,MAAMvG,cAAc;gBAClB4I;gBACAyB,QAAQ;gBACRC,OAAO,GAAEL,mBAAAA,QAAQK,OAAO,YAAfL,mBAAmB;gBAC5BM,MAAM,GAAEN,kBAAAA,QAAQM,MAAM,YAAdN,kBAAkB;eACtBzF,KAAKlE,SAAS,GAAG;gBAAEA,WAAWkE,KAAKlE,SAAS;YAAC,IAAI,CAAC,IACrDkK,KAAK,CAAC,CAACzI;gBACRiE,QAAQjE,KAAK,CAAC,8DAA8DA;YAC9E;QACF,OAAO,IAAI4H,qBAAqBF,YAAYA,QAAQ/C,OAAO,EAAE;YAC3DR,OAAOkE,IAAI,CAAClF,IAAI,CAAC;gBAAE0D;gBAAOlC,SAAS+C,QAAQ/C,OAAO,CAACA,OAAO;gBAAE2D,QAAQX,SAASnD,OAAO;YAAC;QACvF;IACF;IACA,OAAOL;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASuE;IACd,oEAAoE;IACpE,uEAAuE;IACvE,IAAI,OAAOhL,oCAAoC,YAAY;IAC3DA,gCAAgC,CAACwK,UAAYD,gCAAgCC;AAC/E;AAEA;;4CAE4C,GAE5C,uFAAuF,GACvF,MAAMS,aAAa,CAACC,QAClB,GAAGA,MAAMC,iBAAiB,CAAC,CAAC,EAAED,MAAME,EAAE,CAAC,CAAC,EAAEF,MAAMG,IAAI,EAAE;AAExD;;;;;;;;CAQC,GACD,OAAO,eAAeC,uCACpBC,MAAqC,EACrCC,QAAqD,EACrDzG,OAA+B,CAAC,CAAC;IAEjC,MAAM0G,QAAQ,IAAIrG,IAAI,CAACoG,mBAAAA,WAAY,EAAE,EAAEnG,MAAM,CAAC,CAAC8C,UAAYA,QAAQuD,WAAW,EAAE7J,GAAG,CAACoJ;IACpF,MAAMU,UAAUJ,OAAOlG,MAAM,CAAC,CAAC6F;YAOEA;QAN/B,IAAI,CAACO,MAAM3I,GAAG,CAACmI,WAAWC,SAAS,OAAO;QAC1C,IAAI,CAACxL,6BAA6BO,kBAAkBiL,MAAM3G,IAAI,IAAI,OAAO;QACzE,IAAI2G,MAAMG,IAAI,KAAK,aAAa,OAAO;QACvC,OACEH,MAAMG,IAAI,KAAK,aACfH,MAAMU,UAAU,KAAK,eACrB5M,mBAAmB;YAAE+H,MAAM,GAAEmE,sBAAAA,MAAMW,YAAY,YAAlBX,sBAAsB;YAAMY,YAAYZ,MAAMpC,MAAM;QAAC;IAEtF;IACA,IAAI,CAAC6C,QAAQ/J,MAAM,EAAE,OAAO;IAC5B,MAAMiB,QAAQ,MAAMgC,gBAClB8G,QAAQ9J,GAAG,CAAC,CAACqJ,QAAUA,MAAME,EAAE,CAACtF,KAAK,CAACoF,MAAME,EAAE,CAAC1D,WAAW,CAAC,OAAO,KAClE3C,MACAgG,KAAK,CAAC,IAAM,IAAIrI;IAClB,IAAIqJ,WAAW;IACf,KAAK,MAAMb,SAASS,QAAS;kBAMpBxM,OAAAA;YAHO0D;QAFd,MAAMmJ,SAAS/L,kBAAkBiL,MAAM3G,IAAI;QAC3C,MAAMuD,gBAAgBkE,OAAOlG,KAAK,CAACkG,OAAOtE,WAAW,CAAC,OAAO;QAC7D,MAAMuE,iBAAQpJ,aAAAA,MAAM2C,GAAG,CAAC0F,MAAME,EAAE,CAACtF,KAAK,CAACoF,MAAME,EAAE,CAAC1D,WAAW,CAAC,OAAO,wBAArD7E,WAA0DoE,OAAO,mBAAI;QACnF,MAAMA,UACJiE,MAAMG,IAAI,KAAK,aACVlM,SAAAA,qBAAAA,kBAAkB+L,MAAMgB,SAAS,aAAjC/M,qBAAsCQ,eAAeuL,MAAMpC,MAAM,EAAE7B,OAAO,YAA1E9H,QAA8E8M,QAC/EA;QACN,IAAI,CAAChF,WAAWA,YAAY,QAAQ;QACpC,qEAAqE;QACrE,wEAAwE;QACxE,IAAIiE,MAAMG,IAAI,KAAK,eAAepE,YAAY,SAAS;QACvD,MAAMgB,yBACJ,MACA;YACEH;YACAb;YACAkB,SAAS+C,MAAMG,IAAI,KAAK,YAAY,YAAY;YAChD/C,MAAM4C,MAAMiB,EAAE;YACdrD,QAAQoC,MAAMG,IAAI,KAAK,YAAYH,MAAMpC,MAAM,GAAG;QACpD,GACA/D;QAEFgH,YAAY;IACd;IACA,OAAOA;AACT;AAgBA,oDAAoD,GACpD,OAAO,eAAeK,uBACpBC,UAA8B,CAAC,CAAC,EAChCtH,OAA+B,CAAC,CAAC;QAEnBA,aACIA;IADlB,MAAMlB,SAAQkB,cAAAA,KAAKlB,KAAK,YAAVkB,cAAcC,KAAKC,GAAG;IACpC,MAAMpE,aAAYkE,kBAAAA,KAAKlE,SAAS,YAAdkE,kBAAkBpE;IACpC,MAAMwD,QAAQK,KAAKC,GAAG,CAACD,KAAK8H,GAAG,CAAC,GAAG9H,KAAK+H,KAAK,CAAClK,OAAOgK,QAAQlI,KAAK,KAAK,OAAO;IAC9E,MAAMiC,WAAW,MAAMvF,UACpB6E,UAAU,CAAChF,gCACX8L,OAAO,CAAC,eAAe,QACvBrI,KAAK,CAACA,OACNqB,GAAG;IACN,MAAMiH,OAA+B,EAAE;IACvC,KAAK,MAAMxG,OAAOG,SAASsG,IAAI,CAAE;QAC/B,MAAMpG,OAAOL,IAAIK,IAAI;QACrB,MAAM1C,SAAS/D,4BAA4BoG,IAAI0G,EAAE,EAAErG;QACnD,IAAI,CAAC1C,QAAQ;QACb,MAAM+F,WAAWvK,oBAAoBwE,OAAOqD,OAAO,EAAErD,QAAQC;QAC7D4I,KAAKhH,IAAI,CAAC,aACL7B;YACH+I,IAAI1G,IAAI0G,EAAE;YACV5D,QAAQzC,IAAI,CAAC,SAAS,KAAK,QAAQqB,0BAA0B/D,OAAOkE,aAAa;YACjF6B;YACAiD,OAAOpN,iBAAiBmK;;IAE5B;IACA,OAAO8C;AACT;AAEAzB"}
|