@aglyn/tenant-data-admin 1.0.0-beta.217 → 1.0.0-beta.219

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/package.json +9 -9
  2. package/src/lib/server/account-lock-mail.d.ts +74 -0
  3. package/src/lib/server/account-lock-mail.js +139 -0
  4. package/src/lib/server/account-lock-mail.js.map +1 -0
  5. package/src/lib/server/account-mail.d.ts +41 -0
  6. package/src/lib/server/account-mail.js +52 -0
  7. package/src/lib/server/account-mail.js.map +1 -0
  8. package/src/lib/server/api-v1-description.d.ts +112 -0
  9. package/src/lib/server/api-v1-description.js +126 -0
  10. package/src/lib/server/api-v1-description.js.map +1 -0
  11. package/src/lib/server/api-v1-kit.d.ts +156 -0
  12. package/src/lib/server/api-v1-kit.js +191 -0
  13. package/src/lib/server/api-v1-kit.js.map +1 -0
  14. package/src/lib/server/api-v1-resources.d.ts +144 -0
  15. package/src/lib/server/api-v1-resources.js +126 -0
  16. package/src/lib/server/api-v1-resources.js.map +1 -0
  17. package/src/lib/server/consent-group-change.d.ts +2 -1
  18. package/src/lib/server/consent-group-change.js +41 -16
  19. package/src/lib/server/consent-group-change.js.map +1 -1
  20. package/src/lib/server/contact-email-index.d.ts +22 -0
  21. package/src/lib/server/contact-email-index.js +108 -0
  22. package/src/lib/server/contact-email-index.js.map +1 -1
  23. package/src/lib/server/crm-email-activity.d.ts +1 -1
  24. package/src/lib/server/crm-email-activity.js +1 -1
  25. package/src/lib/server/crm-email-activity.js.map +1 -1
  26. package/src/lib/server/crm-inbound-email.d.ts +23 -0
  27. package/src/lib/server/crm-inbound-email.js +1 -16
  28. package/src/lib/server/crm-inbound-email.js.map +1 -1
  29. package/src/lib/server/crm-records.d.ts +9 -1
  30. package/src/lib/server/crm-records.js +19 -2
  31. package/src/lib/server/crm-records.js.map +1 -1
  32. package/src/lib/server/cron-auth.d.ts +44 -0
  33. package/src/lib/server/cron-auth.js +76 -0
  34. package/src/lib/server/cron-auth.js.map +1 -0
  35. package/src/lib/server/dataset-live-pages.js +6 -6
  36. package/src/lib/server/dataset-live-pages.js.map +1 -1
  37. package/src/lib/server/duplicate-resource.d.ts +26 -0
  38. package/src/lib/server/duplicate-resource.js +47 -25
  39. package/src/lib/server/duplicate-resource.js.map +1 -1
  40. package/src/lib/server/dynamic-list-materialize.js +4 -1
  41. package/src/lib/server/dynamic-list-materialize.js.map +1 -1
  42. package/src/lib/server/email-deliverability.js +21 -6
  43. package/src/lib/server/email-deliverability.js.map +1 -1
  44. package/src/lib/server/email-suppression.d.ts +83 -3
  45. package/src/lib/server/email-suppression.js +229 -3
  46. package/src/lib/server/email-suppression.js.map +1 -1
  47. package/src/lib/server/email-unsubscribe-link.d.ts +8 -5
  48. package/src/lib/server/email-unsubscribe-link.js +7 -7
  49. package/src/lib/server/email-unsubscribe-link.js.map +1 -1
  50. package/src/lib/server/erase.d.ts +19 -33
  51. package/src/lib/server/erase.js +37 -162
  52. package/src/lib/server/erase.js.map +1 -1
  53. package/src/lib/server/host-email-tokens.js +1 -1
  54. package/src/lib/server/host-email-tokens.js.map +1 -1
  55. package/src/lib/server/host-memberships.js +3 -3
  56. package/src/lib/server/host-memberships.js.map +1 -1
  57. package/src/lib/server/lockdown.js +2 -0
  58. package/src/lib/server/lockdown.js.map +1 -1
  59. package/src/lib/server/media-download-tokens.js +2 -1
  60. package/src/lib/server/media-download-tokens.js.map +1 -1
  61. package/src/lib/server/member-email-aliases.d.ts +1 -1
  62. package/src/lib/server/member-email-aliases.js +1 -1
  63. package/src/lib/server/member-email-aliases.js.map +1 -1
  64. package/src/lib/server/new-account-announcement.d.ts +62 -0
  65. package/src/lib/server/new-account-announcement.js +77 -0
  66. package/src/lib/server/new-account-announcement.js.map +1 -0
  67. package/src/lib/server/notifications.d.ts +8 -2
  68. package/src/lib/server/notifications.js +18 -4
  69. package/src/lib/server/notifications.js.map +1 -1
  70. package/src/lib/server/org-member-notice.js +5 -1
  71. package/src/lib/server/org-member-notice.js.map +1 -1
  72. package/src/lib/server/organizations.js +4 -0
  73. package/src/lib/server/organizations.js.map +1 -1
  74. package/src/lib/server/{connect-payout-failure.d.ts → payment-provider-stripe-connect-payouts.d.ts} +3 -0
  75. package/src/lib/server/{connect-payout-failure.js → payment-provider-stripe-connect-payouts.js} +1 -1
  76. package/src/lib/server/payment-provider-stripe-connect-payouts.js.map +1 -0
  77. package/src/lib/server/{connect-account-status.d.ts → payment-provider-stripe-connect-status.d.ts} +3 -0
  78. package/src/lib/server/{connect-account-status.js → payment-provider-stripe-connect-status.js} +1 -1
  79. package/src/lib/server/payment-provider-stripe-connect-status.js.map +1 -0
  80. package/src/lib/server/{stripe-account-mode.d.ts → payment-provider-stripe-connect.d.ts} +45 -70
  81. package/src/lib/server/{stripe-account-mode.js → payment-provider-stripe-connect.js} +78 -52
  82. package/src/lib/server/payment-provider-stripe-connect.js.map +1 -0
  83. package/src/lib/server/payment-provider.d.ts +173 -0
  84. package/src/lib/server/payment-provider.js +70 -0
  85. package/src/lib/server/payment-provider.js.map +1 -0
  86. package/src/lib/server/personal-data-export.js +1 -1
  87. package/src/lib/server/personal-data-export.js.map +1 -1
  88. package/src/lib/server/rate-limit-store.js +13 -3
  89. package/src/lib/server/rate-limit-store.js.map +1 -1
  90. package/src/lib/server/realm-plugins.js +8 -3
  91. package/src/lib/server/realm-plugins.js.map +1 -1
  92. package/src/lib/server/retained-refusals.d.ts +142 -0
  93. package/src/lib/server/retained-refusals.js +325 -0
  94. package/src/lib/server/retained-refusals.js.map +1 -0
  95. package/src/lib/server/risk-notice.js +21 -3
  96. package/src/lib/server/risk-notice.js.map +1 -1
  97. package/src/lib/server/serve-media-cdn.js +2 -2
  98. package/src/lib/server/serve-media-cdn.js.map +1 -1
  99. package/src/lib/server/serve-plugin-fetch.d.ts +17 -7
  100. package/src/lib/server/serve-plugin-fetch.js +19 -8
  101. package/src/lib/server/serve-plugin-fetch.js.map +1 -1
  102. package/src/lib/server/test-firestore-queries.d.ts +7 -0
  103. package/src/lib/server/test-firestore-queries.js +70 -27
  104. package/src/lib/server/test-firestore-queries.js.map +1 -1
  105. package/src/lib/server/upsert-contact.d.ts +46 -1
  106. package/src/lib/server/upsert-contact.js +85 -18
  107. package/src/lib/server/upsert-contact.js.map +1 -1
  108. package/src/tenancy.d.ts +4 -5
  109. package/src/tenancy.js +11 -6
  110. package/src/tenancy.js.map +1 -1
  111. package/src/lib/server/connect-account-status.js.map +0 -1
  112. package/src/lib/server/connect-payout-failure.js.map +0 -1
  113. package/src/lib/server/contact-lifecycle-floor.d.ts +0 -89
  114. package/src/lib/server/contact-lifecycle-floor.js +0 -91
  115. package/src/lib/server/contact-lifecycle-floor.js.map +0 -1
  116. package/src/lib/server/contact-merge.d.ts +0 -55
  117. package/src/lib/server/contact-merge.js +0 -288
  118. package/src/lib/server/contact-merge.js.map +0 -1
  119. package/src/lib/server/form-dataset-binding-token.d.ts +0 -35
  120. package/src/lib/server/form-dataset-binding-token.js +0 -107
  121. package/src/lib/server/form-dataset-binding-token.js.map +0 -1
  122. package/src/lib/server/stripe-account-mode.js.map +0 -1
@@ -1 +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 mailGatewayHoldSentence,\n MAIL_GATEWAY_LABELS,\n MAIL_GATEWAY_WINDOW_DAYS,\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 // A named gateway that now holds a SHARED domain stops every site on it\n // from reaching everyone behind that gateway, and no single workspace\n // owns the problem to notice it (AGL-3377). Once a day per pair.\n if (!scope && input.outcome !== 'delivered' && written && isSharedMailSendingDomain(sendingDomain)) {\n const standing = mailGatewayStanding(input.gateway, written, input.atMs)\n if (mailGatewayHolds(standing)) {\n const { raiseOperatorAlert } = await import('./operator-alerts')\n await raiseOperatorAlert('deliverability.gatewayBlocking', {\n dedupeKey: key,\n context: {\n gateway: MAIL_GATEWAY_LABELS[input.gateway] ?? input.gateway,\n domain: sendingDomain,\n detail: mailGatewayHoldSentence(input.gateway, standing.blocked30),\n },\n })\n }\n }\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/** One stored top-level ledger as a staff row; `null` for an unreadable one. */\nexport function mailGatewayLedgerRow(\n id: string,\n data: Record<string, unknown> | null | undefined,\n nowMs: number,\n): MailGatewayLedgerRow | null {\n const ledger = readStoredMailGatewayLedger(id, data)\n if (!ledger) return null\n const standing = mailGatewayStanding(ledger.gateway, ledger, nowMs)\n return {\n ...ledger,\n id,\n shared: data?.['shared'] === true || isSharedMailSendingDomain(ledger.sendingDomain),\n standing,\n holds: mailGatewayHolds(standing),\n }\n}\n\n/** The first instant of the hold's window: midnight UTC of its oldest day. */\nexport function mailGatewayWindowStartMs(nowMs: number): number {\n return Date.parse(`${mailGatewayDay(nowMs - (MAIL_GATEWAY_WINDOW_DAYS - 1) * 86_400_000)}T00:00:00.000Z`)\n}\n\n/** The most rows the refused-lately read returns; past it the answer says it was cut. */\nexport const MAIL_GATEWAY_REFUSED_READ_LIMIT = 500\n\n/**\n * Every top-level ledger with a refusal inside the hold's window, most\n * recent refusal first — the complete set a hold can come from, since a\n * hold needs refusals in that window and `lastBlockedAtMs` is the latest of\n * them. One range on one field, which the automatic index serves; nothing\n * is filtered after the read. `truncated` when there were more than `limit`.\n */\nexport async function listRecentlyRefusedMailGatewayLedgers(\n options: { limit?: number } = {},\n deps: MailDeliverabilityDeps = {},\n): Promise<{ rows: MailGatewayLedgerRow[]; sinceMs: number; truncated: boolean }> {\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) || MAIL_GATEWAY_REFUSED_READ_LIMIT)), MAIL_GATEWAY_REFUSED_READ_LIMIT)\n const sinceMs = mailGatewayWindowStartMs(nowMs)\n const snapshot = await firestore\n .collection(MAIL_GATEWAY_LEDGER_COLLECTION)\n .where('lastBlockedAtMs', '>=', sinceMs)\n .orderBy('lastBlockedAtMs', 'desc')\n .limit(limit + 1)\n .get()\n const rows: MailGatewayLedgerRow[] = []\n for (const doc of snapshot.docs.slice(0, limit)) {\n const row = mailGatewayLedgerRow(doc.id, doc.data() as Record<string, unknown>, nowMs)\n if (row) rows.push(row)\n }\n return { rows, sinceMs, truncated: snapshot.docs.length > limit }\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","mailGatewayHoldSentence","MAIL_GATEWAY_LABELS","MAIL_GATEWAY_WINDOW_DAYS","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","standing","raiseOperatorAlert","dedupeKey","context","blocked30","readMailDeliverability","question","normalized","email","emails","values","Boolean","ledgers","entry","verdicts","given","purpose","publicMailbox","holdReleased","isNoMailServerVerdict","verdict","deciding","isGatewayHeldVerdict","filterDeliverableRecipients","deliverable","noMailServer","gatewayHeld","runEmailDeliverabilityPreflight","request","recipients","refused","held","reason","hostId","catch","installEmailDeliverabilityPreflight","outcomeKey","event","providerMessageId","to","type","recordDeliverabilityFromDeliveryEvents","events","outcomes","first","firstOfType","counted","bounceType","bounceStatus","diagnostic","recorded","sender","known","remoteMta","at","mailGatewayLedgerRow","id","holds","mailGatewayWindowStartMs","parse","MAIL_GATEWAY_REFUSED_READ_LIMIT","listRecentlyRefusedMailGatewayLedgers","options","max","floor","sinceMs","where","orderBy","rows","docs","row","truncated"],"mappings":"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"}
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 mailGatewayHoldSentence,\n MAIL_GATEWAY_LABELS,\n MAIL_GATEWAY_WINDOW_DAYS,\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 { accountBannedRecipients, 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 // A named gateway that now holds a SHARED domain stops every site on it\n // from reaching everyone behind that gateway, and no single workspace\n // owns the problem to notice it (AGL-3377). Once a day per pair.\n if (!scope && input.outcome !== 'delivered' && written && isSharedMailSendingDomain(sendingDomain)) {\n const standing = mailGatewayStanding(input.gateway, written, input.atMs)\n if (mailGatewayHolds(standing)) {\n const { raiseOperatorAlert } = await import('./operator-alerts')\n await raiseOperatorAlert('deliverability.gatewayBlocking', {\n dedupeKey: key,\n context: {\n gateway: MAIL_GATEWAY_LABELS[input.gateway] ?? input.gateway,\n domain: sendingDomain,\n detail: mailGatewayHoldSentence(input.gateway, standing.blocked30),\n },\n })\n }\n }\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 answer: EmailDeliverabilityPreflightVerdict = { refused: [], held: [] }\n /*\n * A BANNED ACCOUNT FIRST (AGL-3420), and for every purpose: after its ban\n * notice it is sent nothing — no receipt, no reset, no site's mail —\n * which is the one refusal on this path that transactional mail does not\n * get past. Only a lock or lift notice says it is exempt.\n */\n const banned = request.accountBanExempt\n ? new Set<string>()\n : await accountBannedRecipients(request.recipients, deps.firestore)\n for (const email of banned) {\n answer.refused.push({\n email,\n code: 'account_banned',\n reason: 'This address belongs to a closed account and is not sent mail.',\n })\n }\n const recipients = request.recipients.filter((email) => !banned.has(email))\n if (!recipients.length) return answer\n const verdicts = await readMailDeliverability(\n {\n emails: recipients,\n purpose: request.purpose,\n sendingDomain: request.sendingDomain,\n scope: null,\n },\n deps,\n )\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/** One stored top-level ledger as a staff row; `null` for an unreadable one. */\nexport function mailGatewayLedgerRow(\n id: string,\n data: Record<string, unknown> | null | undefined,\n nowMs: number,\n): MailGatewayLedgerRow | null {\n const ledger = readStoredMailGatewayLedger(id, data)\n if (!ledger) return null\n const standing = mailGatewayStanding(ledger.gateway, ledger, nowMs)\n return {\n ...ledger,\n id,\n shared: data?.['shared'] === true || isSharedMailSendingDomain(ledger.sendingDomain),\n standing,\n holds: mailGatewayHolds(standing),\n }\n}\n\n/** The first instant of the hold's window: midnight UTC of its oldest day. */\nexport function mailGatewayWindowStartMs(nowMs: number): number {\n return Date.parse(`${mailGatewayDay(nowMs - (MAIL_GATEWAY_WINDOW_DAYS - 1) * 86_400_000)}T00:00:00.000Z`)\n}\n\n/** The most rows the refused-lately read returns; past it the answer says it was cut. */\nexport const MAIL_GATEWAY_REFUSED_READ_LIMIT = 500\n\n/**\n * Every top-level ledger with a refusal inside the hold's window, most\n * recent refusal first — the complete set a hold can come from, since a\n * hold needs refusals in that window and `lastBlockedAtMs` is the latest of\n * them. One range on one field, which the automatic index serves; nothing\n * is filtered after the read. `truncated` when there were more than `limit`.\n */\nexport async function listRecentlyRefusedMailGatewayLedgers(\n options: { limit?: number } = {},\n deps: MailDeliverabilityDeps = {},\n): Promise<{ rows: MailGatewayLedgerRow[]; sinceMs: number; truncated: boolean }> {\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) || MAIL_GATEWAY_REFUSED_READ_LIMIT)), MAIL_GATEWAY_REFUSED_READ_LIMIT)\n const sinceMs = mailGatewayWindowStartMs(nowMs)\n const snapshot = await firestore\n .collection(MAIL_GATEWAY_LEDGER_COLLECTION)\n .where('lastBlockedAtMs', '>=', sinceMs)\n .orderBy('lastBlockedAtMs', 'desc')\n .limit(limit + 1)\n .get()\n const rows: MailGatewayLedgerRow[] = []\n for (const doc of snapshot.docs.slice(0, limit)) {\n const row = mailGatewayLedgerRow(doc.id, doc.data() as Record<string, unknown>, nowMs)\n if (row) rows.push(row)\n }\n return { rows, sinceMs, truncated: snapshot.docs.length > limit }\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","mailGatewayHoldSentence","MAIL_GATEWAY_LABELS","MAIL_GATEWAY_WINDOW_DAYS","normalizeDeliverabilityEmail","normalizeLedgerSendingDomain","readBounceText","readStoredMailDomainIntel","readStoredMailGatewayLedger","resolveMailDomain","scrubBounceDiagnostic","setEmailDeliverabilityPreflight","bareSenderAddress","promises","dns","isConclusiveDnsCode","lookupAddress","lookupMx","accountBannedRecipients","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","standing","raiseOperatorAlert","dedupeKey","context","blocked30","readMailDeliverability","question","normalized","email","emails","values","Boolean","ledgers","entry","verdicts","given","purpose","publicMailbox","holdReleased","isNoMailServerVerdict","verdict","deciding","isGatewayHeldVerdict","filterDeliverableRecipients","deliverable","noMailServer","gatewayHeld","runEmailDeliverabilityPreflight","request","refused","held","banned","accountBanExempt","recipients","reason","hostId","catch","installEmailDeliverabilityPreflight","outcomeKey","event","providerMessageId","to","type","recordDeliverabilityFromDeliveryEvents","events","outcomes","first","firstOfType","counted","bounceType","bounceStatus","diagnostic","recorded","sender","known","remoteMta","at","mailGatewayLedgerRow","id","holds","mailGatewayWindowStartMs","parse","MAIL_GATEWAY_REFUSED_READ_LIMIT","listRecentlyRefusedMailGatewayLedgers","options","max","floor","sinceMs","where","orderBy","rows","docs","row","truncated"],"mappings":"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"}
@@ -125,7 +125,17 @@ export type EmailSuppressionReason =
125
125
  * like a bounce; a staff release lifts it, and the preflight files it
126
126
  * again only if the domain still takes no mail.
127
127
  */
128
- | 'no_mail_server';
128
+ | 'no_mail_server'
129
+ /**
130
+ * The account holding this address is BANNED — locked for phishing, fraud
131
+ * or malicious content (AGL-3420). Not a mailing preference and not a
132
+ * deliverability fact: the platform's own decision that this address is
133
+ * sent nothing, by any sender, for any purpose, once the ban notice has
134
+ * gone. Unlike every other reason it is also read on TRANSACTIONAL mail,
135
+ * by the deliverability preflight on `sendEmail`. Only lifting the ban
136
+ * releases it — see {@link ACCOUNT_BAN_SUPPRESSION_REASON}.
137
+ */
138
+ | 'account_ban';
129
139
  /**
130
140
  * The reasons that may be filed PLATFORM-WIDE, as a runtime set.
131
141
  *
@@ -155,6 +165,14 @@ export type EmailSuppressionReason =
155
165
  * suppression list does not mention it.
156
166
  */
157
167
  export declare const PLATFORM_SUPPRESSION_REASONS: readonly EmailSuppressionReason[];
168
+ /**
169
+ * The ban reason, named because four rules hang on it: a later failure never
170
+ * rewrites it ({@link suppressEmail}), no staff release or opt-in lifts it
171
+ * ({@link releaseEmail}, {@link releaseEmailForConfirmedOptIn}), only
172
+ * {@link releaseAccountBanSuppressions} does, and the preflight refuses it on
173
+ * every send ({@link accountBannedRecipients}).
174
+ */
175
+ export declare const ACCOUNT_BAN_SUPPRESSION_REASON = "account_ban";
158
176
  /**
159
177
  * The reason a self-service opt-out is filed under, on the PER-SITE list.
160
178
  *
@@ -231,6 +249,11 @@ export interface SuppressEmailInput {
231
249
  * A sender that stamps a richer verdict of its own passes `false`.
232
250
  */
233
251
  stampRecord?: boolean;
252
+ /**
253
+ * The account the address belongs to, on a ban row, so lifting that
254
+ * account's ban releases exactly its rows.
255
+ */
256
+ subjectUid?: string | null;
234
257
  /** Injectable for tests; defaults to the admin app's Firestore. */
235
258
  firestore?: any;
236
259
  }
@@ -264,7 +287,9 @@ export type EmailReleaseChannel =
264
287
  /** A staff correction through the admin suppressions surface. */
265
288
  'staff'
266
289
  /** A recipient completed a double opt-in from this address. */
267
- | 'double-opt-in';
290
+ | 'double-opt-in'
291
+ /** Staff lifted the ban on the account holding it (AGL-3420). */
292
+ | 'ban-lifted';
268
293
  /**
269
294
  * Put an address back in circulation (a staff correction, or the person asking
270
295
  * to be re-added). The record is kept and marked released, never deleted —
@@ -277,7 +302,13 @@ export type EmailReleaseChannel =
277
302
  * a report filed against the wrong message, an address entered by mistake.
278
303
  * Nothing automatic gets that latitude.
279
304
  *
280
- * @returns false when there was no live record to release.
305
+ * Except a ban (AGL-3420): an `account_ban` row belongs to the account's
306
+ * lock, and is lifted by lifting the ban, never from this list — releasing
307
+ * one here would put a banned account back on every sender while the
308
+ * account itself stays locked.
309
+ *
310
+ * @returns false when there was no live record to release, or the record is
311
+ * a ban.
281
312
  */
282
313
  export declare function releaseEmail(input: {
283
314
  email: string;
@@ -285,6 +316,22 @@ export declare function releaseEmail(input: {
285
316
  note?: string | null;
286
317
  firestore?: any;
287
318
  }): Promise<boolean>;
319
+ /** Whether a live ban row holds this address. Fails open, logging. */
320
+ export declare function isAccountBanSuppression(email: string | null | undefined, injectedFirestore?: any): Promise<boolean>;
321
+ /**
322
+ * Lift a ban's rows: every live `account_ban` row filed for `uid`, and only
323
+ * those. Kept and marked released, like every other release. Answers how
324
+ * many it released.
325
+ */
326
+ export declare function releaseAccountBanSuppressions(input: {
327
+ uid: string;
328
+ releasedByUid?: string | null;
329
+ firestore?: any;
330
+ }): Promise<number>;
331
+ /** Drop the held set, so the next send reads the list again. */
332
+ export declare function forgetAccountBans(): void;
333
+ /** The addresses among `emails` whose account is banned, lowercased. */
334
+ export declare function accountBannedRecipients(emails: readonly string[], injectedFirestore?: any): Promise<Set<string>>;
288
335
  /**
289
336
  * The ONLY reason a completed round trip may lift, as a runtime set.
290
337
  *
@@ -584,3 +631,36 @@ export declare function suppressEmailForHostErasure(input: {
584
631
  * recreated record by the same address.
585
632
  */
586
633
  export declare function hostRefusesCaptureForErasure(hostId: string, email: string | null | undefined, injectedFirestore?: any): Promise<boolean>;
634
+ /** The `reason` a lock's per-site row carries. */
635
+ /**
636
+ * {@link hostRefusesCaptureForErasure} for many addresses in one read.
637
+ *
638
+ * The addresses a site has erased, among the ones given — one `getAll` of
639
+ * their suppression rows rather than one read per address, for a door that
640
+ * captures a whole file at once. Answers `null` when the read throws, which
641
+ * a caller takes as "not looked up" and asks per address instead, so the
642
+ * fail-open rule above is the single-address function's and not a second
643
+ * copy of it.
644
+ */
645
+ export declare function hostErasedEmails(hostId: string, emails: readonly (string | null | undefined)[], injectedFirestore?: any): Promise<Set<string> | null>;
646
+ export declare const HOST_ACCOUNT_LOCK_SUPPRESSION_REASON = "account_lock";
647
+ /**
648
+ * Suppress an address on one site because the account holding it is
649
+ * locked. Answers whether it wrote a row: false when one already stood.
650
+ */
651
+ export declare function suppressEmailForAccountLock(input: {
652
+ hostId: string;
653
+ email: string;
654
+ uid: string;
655
+ firestore?: any;
656
+ }): Promise<boolean>;
657
+ /**
658
+ * Lift a lock's row on one site: deleted only while it is still this
659
+ * account's lock row. Answers whether it deleted one.
660
+ */
661
+ export declare function releaseAccountLockSuppression(input: {
662
+ hostId: string;
663
+ email: string;
664
+ uid: string;
665
+ firestore?: any;
666
+ }): Promise<boolean>;