@aglyn/plugins-outreach 1.0.0-beta.200 → 1.0.0-beta.201

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 (90) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/enrollment-actions.d.ts +33 -0
  3. package/src/lib/components/enrollment-actions.js +174 -0
  4. package/src/lib/components/enrollment-actions.js.map +1 -0
  5. package/src/lib/components/enrollment-detail.d.ts +21 -0
  6. package/src/lib/components/enrollment-detail.js +870 -0
  7. package/src/lib/components/enrollment-detail.js.map +1 -0
  8. package/src/lib/components/enrollment-filter-params.d.ts +58 -0
  9. package/src/lib/components/enrollment-filter-params.js +98 -0
  10. package/src/lib/components/enrollment-filter-params.js.map +1 -0
  11. package/src/lib/components/enrollments-table.d.ts +21 -0
  12. package/src/lib/components/enrollments-table.js +249 -210
  13. package/src/lib/components/enrollments-table.js.map +1 -1
  14. package/src/lib/components/outreach-console-page.js +10 -0
  15. package/src/lib/components/outreach-console-page.js.map +1 -1
  16. package/src/lib/components/sequence-detail.d.ts +2 -0
  17. package/src/lib/components/sequence-detail.js +70 -8
  18. package/src/lib/components/sequence-detail.js.map +1 -1
  19. package/src/lib/components/sequence-report-card.d.ts +17 -0
  20. package/src/lib/components/sequence-report-card.js +63 -15
  21. package/src/lib/components/sequence-report-card.js.map +1 -1
  22. package/src/lib/components/sequences-section.d.ts +4 -1
  23. package/src/lib/components/sequences-section.js +12 -1
  24. package/src/lib/components/sequences-section.js.map +1 -1
  25. package/src/lib/components/use-outreach-api.d.ts +3 -1
  26. package/src/lib/components/use-outreach-api.js +6 -0
  27. package/src/lib/components/use-outreach-api.js.map +1 -1
  28. package/src/lib/components/use-outreach-enrollment.d.ts +18 -0
  29. package/src/lib/components/use-outreach-enrollment.js +133 -0
  30. package/src/lib/components/use-outreach-enrollment.js.map +1 -0
  31. package/src/lib/constants/api-routes.d.ts +2 -0
  32. package/src/lib/constants/api-routes.js +1 -0
  33. package/src/lib/constants/api-routes.js.map +1 -1
  34. package/src/lib/engine/gates.d.ts +4 -3
  35. package/src/lib/engine/gates.js.map +1 -1
  36. package/src/lib/engine/gateway-block.d.ts +4 -8
  37. package/src/lib/engine/gateway-block.js +6 -26
  38. package/src/lib/engine/gateway-block.js.map +1 -1
  39. package/src/lib/engine/mail-gateway.d.ts +18 -73
  40. package/src/lib/engine/mail-gateway.js +32 -206
  41. package/src/lib/engine/mail-gateway.js.map +1 -1
  42. package/src/lib/model/enrollment-engagement.d.ts +26 -2
  43. package/src/lib/model/enrollment-engagement.js +40 -3
  44. package/src/lib/model/enrollment-engagement.js.map +1 -1
  45. package/src/lib/model/enrollment-history.d.ts +40 -0
  46. package/src/lib/model/enrollment-history.js +95 -0
  47. package/src/lib/model/enrollment-history.js.map +1 -0
  48. package/src/lib/model/enrollment-list-query.d.ts +6 -0
  49. package/src/lib/model/enrollment-list-query.js +35 -1
  50. package/src/lib/model/enrollment-list-query.js.map +1 -1
  51. package/src/lib/model/enrollment-timeline.d.ts +78 -0
  52. package/src/lib/model/enrollment-timeline.js +430 -0
  53. package/src/lib/model/enrollment-timeline.js.map +1 -0
  54. package/src/lib/model/outreach-api.d.ts +24 -0
  55. package/src/lib/model/outreach-api.js.map +1 -1
  56. package/src/lib/model/outreach.types.d.ts +78 -29
  57. package/src/lib/model/outreach.types.js +33 -8
  58. package/src/lib/model/outreach.types.js.map +1 -1
  59. package/src/lib/routes/enroll-routes.js +26 -3
  60. package/src/lib/routes/enroll-routes.js.map +1 -1
  61. package/src/lib/routes/enrollment-routes.d.ts +10 -0
  62. package/src/lib/routes/enrollment-routes.js +82 -1
  63. package/src/lib/routes/enrollment-routes.js.map +1 -1
  64. package/src/lib/routes/register-routes.js +14 -4
  65. package/src/lib/routes/register-routes.js.map +1 -1
  66. package/src/lib/routes/route-deps.d.ts +3 -1
  67. package/src/lib/routes/route-deps.js.map +1 -1
  68. package/src/lib/runtime/click-events.d.ts +10 -5
  69. package/src/lib/runtime/click-events.js +45 -6
  70. package/src/lib/runtime/click-events.js.map +1 -1
  71. package/src/lib/runtime/gateway-ledger.d.ts +6 -0
  72. package/src/lib/runtime/gateway-ledger.js +9 -3
  73. package/src/lib/runtime/gateway-ledger.js.map +1 -1
  74. package/src/lib/runtime/host-suspension.d.ts +30 -0
  75. package/src/lib/runtime/host-suspension.js +35 -0
  76. package/src/lib/runtime/host-suspension.js.map +1 -0
  77. package/src/lib/runtime/person-erasure.d.ts +4 -1
  78. package/src/lib/runtime/person-erasure.js +14 -3
  79. package/src/lib/runtime/person-erasure.js.map +1 -1
  80. package/src/lib/runtime/platform-runtime-deps.js +34 -3
  81. package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
  82. package/src/lib/runtime/runtime-deps.d.ts +27 -1
  83. package/src/lib/runtime/runtime-deps.js.map +1 -1
  84. package/src/lib/runtime/send-job.js +46 -6
  85. package/src/lib/runtime/send-job.js.map +1 -1
  86. package/src/lib/runtime/sync-job.js +4 -0
  87. package/src/lib/runtime/sync-job.js.map +1 -1
  88. package/src/lib/storage/domain-intel-store.d.ts +61 -43
  89. package/src/lib/storage/domain-intel-store.js +138 -226
  90. package/src/lib/storage/domain-intel-store.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/sync-job.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\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { isPublicMailboxDomain } from '@aglyn/aglyn/app-utils/crm'\nimport { crmInboundExcerpt, emailAddressOf } from '@aglyn/aglyn/app-utils/crm-inbound'\nimport { outreachMessageIdHeader } from '../engine/compose'\nimport { readOutreachDeliveryReport } from '../engine/delivery-status'\nimport { outreachEmailDomain } from '../engine/do-not-contact-domain'\nimport { outreachStoppedEntry } from '../engine/enrollment-activity'\nimport type { OutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport { isOutreachGatewayBlock, outreachGatewayBlockDetail } from '../engine/gateway-block'\nimport { effectiveOutreachWindow, postponeOutreachForAutoReply } from '../engine/schedule'\nimport {\n decideOutreachThread,\n type OutreachMessageClassification,\n type OutreachThreadDecision,\n} from '../engine/thread-classification'\nimport { outreachHeader, outreachThreadMessageFromGmail, type OutreachThreadMessage } from '../engine/thread-message'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachEnrollment,\n type OutreachMailbox,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport { addOutreachDoNotContact, addOutreachDoNotContactDomain } from '../storage/do-not-contact-store'\nimport {\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport type { GmailClient } from '../transport/gmail-client'\nimport { GmailTransportError, isReconnectRequired } from '../transport/gmail-errors'\nimport { creditOutreachReply } from './campaign-credit'\nimport { applyOutreachEvent, recordOutreachOptOut } from './enrollment-events'\nimport { creditOutreachGatewayDeliveries, recordOutreachGatewayBlock } from './gateway-ledger'\nimport {\n applyOutreachMailboxHealth,\n emptyOutreachHealthDelta,\n type OutreachMailboxHealthDelta,\n} from './mailbox-health-store'\nimport { noteOutreachMailboxReconnectRequired } from './mailbox-notices'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { markOutreachLeadWorking } from './lead-records'\nimport { fileOutreachEmail, fileOutreachNote, fileOutreachTask, markOutreachEmailDelivery } from './timeline'\nimport { outreachUnsubscribeMailbox } from './unsubscribe-link'\n\n/*==========================================\n * THE SYNC JOB (AGL-2981): every fifteen minutes, on the console.\n *\n * For each connected or paused mailbox, what came back since the last run —\n * read with `format=full`, turned into the engine's message shape and\n * classified by `decideOutreachThread`:\n *\n * - a REPLY stops the enrollment, files the message on the contact's\n * timeline (once per `Message-ID`, under the capture address's own id for\n * it) and gives the rep the task \"Reply from <name>\";\n * - an OUT-OF-OFFICE postpones the next step a working week from the reply;\n * - an OPT-OUT — a reply asking to be left alone, or a message to the\n * `+unsubscribe` address the `mailto:` names — puts the address on the\n * organization's do-not-contact list and off the site's `sales` topic,\n * and stops every open enrollment of it;\n * - a HARD BOUNCE stops the enrollment, files the address on the platform's\n * suppression list and the do-not-contact list — and, when its diagnostic\n * reads as the domain's mail gateway refusing the sender rather than one\n * address being unknown (`engine/gateway-block`, AGL-3244), files the\n * DOMAIN on the do-not-contact list too, and says so on the enrollment —\n * and counts the block on the gateway ledger (AGL-3326), under the\n * gateway the bounce's `Remote-MTA` names.\n *\n * And once the mail is read, every email step of a watched enrollment that\n * is a day old with no bounce against it is credited to the ledger as\n * delivered (`./gateway-ledger`): the silence that says a gateway let the\n * sender through.\n *\n * Every verdict a list takes is said on the record the person is as well\n * (AGL-3245): the lead and the contact carry `emailState` through the\n * platform's stamp, and a bounce or a complaint lands on the send's own\n * timeline entry, so a member reading the record sees what the runtime saw.\n *\n * Four reads find those, each narrowed by Gmail rather than read in full:\n * the threads of this mailbox's enrollments that have new mail; delivery\n * reports outside those threads (a receiving server's own bounce), by\n * searching for the mail systems that send them; messages to the\n * unsubscribe address; and messages from the people this mailbox is\n * writing to, in a thread of their own.\n *\n * Every write it makes is idempotent — the do-not-contact list keeps its\n * first entry, a topic opt-out keeps its first moment, a timeline entry is\n * filed once per message — and the messages it handled are marked AFTER\n * their effects, on the enrollment and on the mailbox, so a run that dies\n * part-way repeats harmless work rather than skipping some. The mailbox's\n * health takes the run's bounces and complaints in one write at the end,\n * which is where it pauses itself.\n *==========================================*/\n\nexport { OUTREACH_SYNC_JOB_ID } from '../constants/runtime-jobs'\n\n/** The furthest back a run reads: a mailbox's first run, or one that fell behind. */\nexport const OUTREACH_SYNC_LOOKBACK_MS = 7 * 24 * 60 * 60_000\n\n/** How far a run reads back past the previous one, for mail Gmail indexed late. */\nexport const OUTREACH_SYNC_OVERLAP_MS = 2 * 60 * 60_000\n\n/** How long after its last send an enrollment's mail is still watched. */\nexport const OUTREACH_SYNC_WATCH_MS = 45 * 24 * 60 * 60_000\n\n/** The most pages of one search a run reads. */\nconst SEARCH_PAGES = 5\n/** Handled message ids a mailbox keeps, and an enrollment keeps. */\nconst MAILBOX_HANDLED_MAX = 500\nconst ENROLLMENT_HANDLED_MAX = 100\n/** Addresses in one `from:` search. */\nconst FROM_CHUNK = 15\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface OutreachSyncReport {\n [key: string]: number\n mailboxes: number\n threads: number\n replies: number\n optOuts: number\n bounces: number\n /** Hard bounces that read as a gateway block, each filing its domain (AGL-3244). */\n gatewayBlocks: number\n /** Email steps a day old with no bounce, credited to the gateway ledger (AGL-3326). */\n deliveries: number\n postponed: number\n paused: number\n}\n\ninterface SyncContext {\n deps: OutreachRuntimeDeps\n firestore: Firestore\n orgId: string\n mailbox: OutreachMailbox\n client: GmailClient\n selfAddresses: string[]\n unsubscribeAddress: string | null\n sequences: Map<string, OutreachSequence | null>\n delta: OutreachMailboxHealthDelta\n report: OutreachSyncReport\n nowMs: number\n}\n\n/** Runs the sync job once — see the module note. */\nexport async function runOutreachSyncJob(\n deps: OutreachRuntimeDeps,\n context: { nowMs: number; deadlineMs: number },\n): Promise<OutreachSyncReport> {\n const firestore = deps.firestore()\n const report: OutreachSyncReport = {\n mailboxes: 0,\n threads: 0,\n replies: 0,\n optOuts: 0,\n bounces: 0,\n gatewayBlocks: 0,\n deliveries: 0,\n postponed: 0,\n paused: 0,\n }\n const all = await firestore.collectionGroup(OUTREACH_COLLECTIONS.mailboxes).get()\n const byOrg = new Map<string, OutreachMailbox[]>()\n for (const doc of all.docs) {\n const orgId = doc.ref.parent.parent?.id\n const mailbox = readStoredOutreachMailbox(doc.id, doc.data())\n if (!orgId || !mailbox || (mailbox.status !== 'connected' && mailbox.status !== 'paused')) continue\n byOrg.set(orgId, [...(byOrg.get(orgId) ?? []), mailbox])\n }\n for (const [orgId, mailboxes] of byOrg) {\n if (deps.now() >= context.deadlineMs) break\n const orgSnapshot = await firestore.collection('orgs').doc(orgId).get()\n const org = (orgSnapshot.exists ? orgSnapshot.data() : null) as Record<string, unknown> | null\n if (!org || (await deps.orgRefusal(orgId, org))) continue\n for (const mailbox of mailboxes) {\n if (deps.now() >= context.deadlineMs) break\n try {\n await syncMailbox(deps, firestore, { orgId, mailbox, report, nowMs: context.nowMs })\n report.mailboxes += 1\n } catch (error) {\n // One mailbox's failure is its own; the next mailbox is read. A grant\n // Google refused is the mailbox's to reconnect.\n if (isReconnectRequired(error)) {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: error instanceof GmailTransportError ? error.code : 'invalid_grant',\n nowMs: context.nowMs,\n })\n }\n console.error('[outreach] syncing a mailbox failed', error)\n }\n }\n }\n return report\n}\n\n/** Every stub a search finds, a few pages deep. */\nasync function search(\n client: GmailClient,\n q: string,\n): Promise<Array<{ id: string; threadId: string }>> {\n const found: Array<{ id: string; threadId: string }> = []\n let pageToken: string | null = null\n for (let page = 0; page < SEARCH_PAGES; page += 1) {\n const list = await client.listMessages({ q, maxResults: 100, pageToken, includeSpamTrash: true })\n found.push(...list.messages.filter((message) => message.id))\n pageToken = list.nextPageToken\n if (!pageToken) break\n }\n return found\n}\n\nasync function syncMailbox(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n input: { orgId: string; mailbox: OutreachMailbox; report: OutreachSyncReport; nowMs: number },\n): Promise<void> {\n const { orgId, mailbox, report, nowMs } = input\n const opened = await deps.openMailbox(mailbox.id)\n if (opened.ok === false) {\n if (opened.reason !== 'not-configured') {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n }\n return\n }\n const unsubscribe = outreachUnsubscribeMailbox(mailbox.email)\n const context: SyncContext = {\n deps,\n firestore,\n orgId,\n mailbox,\n client: opened.client,\n selfAddresses: [\n mailbox.email,\n mailbox.sendAs,\n ...(mailbox.sendAsOptions ?? []).map((option) => option.email),\n ].filter(Boolean),\n unsubscribeAddress: unsubscribe?.address ?? null,\n sequences: new Map(),\n delta: emptyOutreachHealthDelta(),\n report,\n nowMs,\n }\n\n // The enrollments this mailbox is writing to, by thread and by address.\n const watched = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('mailboxId', '==', mailbox.id)\n .where('lastSentAtMs', '>=', nowMs - OUTREACH_SYNC_WATCH_MS)\n .get()\n const byThread = new Map<string, OutreachEnrollment>()\n const byAddress = new Map<string, OutreachEnrollment>()\n const watchedEnrollments: OutreachEnrollment[] = []\n for (const doc of watched.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment) continue\n watchedEnrollments.push(enrollment)\n for (const threadId of enrollment.gmailThreadIds) byThread.set(threadId, enrollment)\n const known = byAddress.get(enrollment.email)\n if (!known || (enrollment.lastSentAtMs ?? 0) > (known.lastSentAtMs ?? 0)) byAddress.set(enrollment.email, enrollment)\n }\n\n const sinceMs = Math.max(Number(mailbox.sync?.throughMs) || 0, nowMs - OUTREACH_SYNC_LOOKBACK_MS) - OUTREACH_SYNC_OVERLAP_MS\n const after = `after:${Math.floor(sinceMs / 1000)}`\n // Every message this run or an earlier one handled outside an enrollment's\n // thread; one message is handled once however many searches find it.\n const handled = new Set(mailbox.sync?.handledMessageIds ?? [])\n const newlyHandled: string[] = []\n const markHandled = (id: string) => {\n if (handled.has(id)) return\n handled.add(id)\n newlyHandled.push(id)\n }\n const fresh = (stub: { id: string; threadId: string }) => !handled.has(stub.id) && !byThread.has(stub.threadId)\n\n // 1. The enrollments' own threads that have new mail.\n const inbound = await search(context.client, `${after} -from:me`)\n for (const threadId of new Set(inbound.map((stub) => stub.threadId))) {\n const enrollment = byThread.get(threadId)\n if (!enrollment) continue\n const thread = await context.client.getFullThread(threadId)\n report.threads += 1\n await applyMessages(context, enrollment, thread.messages.map(outreachThreadMessageFromGmail))\n }\n\n // 2. Delivery reports outside those threads: a receiving server's bounce.\n for (const stub of await search(context.client, `${after} from:(mailer-daemon OR postmaster)`)) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const delivery = readOutreachDeliveryReport(message)\n if (delivery?.kind === 'hard') {\n for (const address of delivery.failedAddresses) {\n const enrollment = byAddress.get(address)\n if (enrollment) await applyMessages(context, enrollment, [message])\n }\n }\n markHandled(stub.id)\n }\n\n // 3. Messages to the unsubscribe address: whoever wrote asked to leave.\n if (unsubscribe) {\n for (const stub of await search(context.client, `${after} to:${unsubscribe.address}`)) {\n if (handled.has(stub.id)) continue\n const metadata = await context.client.getMessage(stub.id, { metadataHeaders: ['From'] })\n const from = emailAddressOf(metadata.headers.find((header) => header.name.toLowerCase() === 'from')?.value ?? '')\n const address = normalizeContactEmail(from)\n if (address && !context.selfAddresses.includes(address)) {\n const enrollment = byAddress.get(address) ?? null\n await recordOutreachOptOut(deps, {\n orgId,\n email: address,\n source: 'unsubscribe',\n enrollment,\n hostIds: enrollment ? [enrollment.hostId] : [],\n detail: 'Wrote to the unsubscribe address.',\n nowMs,\n })\n report.optOuts += 1\n }\n markHandled(stub.id)\n }\n }\n\n // 4. The people this mailbox writes to, answering in a thread of their own.\n const addresses = [...byAddress.keys()]\n for (let start = 0; start < addresses.length; start += FROM_CHUNK) {\n const chunk = addresses.slice(start, start + FROM_CHUNK)\n for (const stub of await search(context.client, `${after} from:(${chunk.join(' OR ')})`)) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const enrollment = byAddress.get(normalizeContactEmail(emailAddressOf(message.from)) ?? '')\n if (enrollment) await applyMessages(context, enrollment, [message])\n markHandled(stub.id)\n }\n }\n\n // 5. What went through: a day of silence after a send is a delivery\n // (AGL-3326), judged on the enrollments as this run left them.\n try {\n report.deliveries += await creditOutreachGatewayDeliveries(firestore, {\n orgId,\n enrollments: watchedEnrollments,\n resolveMx: deps.resolveMx,\n nowMs,\n })\n } catch (error) {\n console.error('[outreach] the gateway ledger could not credit deliveries', error)\n }\n\n const paused = await applyOutreachMailboxHealth(deps, { orgId, mailboxId: mailbox.id, delta: context.delta })\n if (paused) report.paused += 1\n await mailboxRef(firestore, orgId, mailbox.id).update({\n sync: {\n throughMs: nowMs,\n handledMessageIds: [...(mailbox.sync?.handledMessageIds ?? []), ...newlyHandled].slice(-MAILBOX_HANDLED_MAX),\n },\n })\n}\n\n/** The sequence an enrollment runs through, read once per run. */\nasync function sequenceOf(context: SyncContext, sequenceId: string): Promise<OutreachSequence | null> {\n if (!context.sequences.has(sequenceId)) {\n const snapshot = await outreachOrgCollection(context.firestore, context.orgId, 'sequences').doc(sequenceId).get()\n context.sequences.set(sequenceId, readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined))\n }\n return context.sequences.get(sequenceId) ?? null\n}\n\n/** A message's `Message-ID` in header form, or `null` when it carries none. */\nconst messageIdOf = (message: OutreachThreadMessage) => outreachMessageIdHeader(outreachHeader(message, 'Message-ID'))\n\n/** A bounce's diagnostic with the recipient's address taken out of it. */\nexport function scrubbedOutreachDiagnostic(diagnostic: string | null, email: string): string | null {\n if (!diagnostic) return null\n const pattern = new RegExp(email.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'gi')\n return diagnostic.replace(pattern, 'the address')\n}\n\n/** Files a message the person wrote on their timeline. */\nasync function fileInbound(context: SyncContext, enrollment: OutreachEnrollment, message: OutreachThreadMessage) {\n const messageId = messageIdOf(message)\n if (!messageId) return\n await fileOutreachEmail(context.deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n direction: 'inbound',\n subject: message.subject,\n from: emailAddressOf(message.from) ?? message.from,\n to: context.mailbox.sendAs || context.mailbox.email,\n messageId,\n inReplyTo: outreachMessageIdHeader(outreachHeader(message, 'In-Reply-To')),\n body: crmInboundExcerpt(message.textBody ?? message.snippet ?? '', message.htmlBody ?? ''),\n atMs: message.internalDateMs,\n byUid: '',\n })\n}\n\n/**\n * What the new messages of one enrollment mean, done: the effects first,\n * each idempotent, then the event and the handled ids in one transaction.\n */\nasync function applyMessages(\n context: SyncContext,\n enrollment: OutreachEnrollment,\n messages: readonly OutreachThreadMessage[],\n): Promise<void> {\n const { deps, firestore, report, nowMs } = context\n const decision: OutreachThreadDecision = decideOutreachThread({\n messages,\n recipient: enrollment.email,\n handledMessageIds: enrollment.syncedMessageIds ?? [],\n selfAddresses: context.selfAddresses,\n unsubscribeAddress: context.unsubscribeAddress,\n threadSubject: enrollment.threadSubject,\n })\n const freshIds = decision.classifications.map((classification) => classification.messageId).filter(Boolean)\n if (!freshIds.length) return\n const decidedBy: OutreachMessageClassification | null = decision.decidedBy\n const decidingMessage = decidedBy ? messages.find((message) => message.id === decidedBy.messageId) ?? null : null\n const lastSentMessageId = enrollment.messageIds[enrollment.messageIds.length - 1] ?? null\n\n for (const classification of decision.classifications) {\n const aboutRecipient =\n !classification.bounce?.recipients.length || classification.bounce.recipients.includes(enrollment.email)\n if (classification.kind === 'hard_bounce' && aboutRecipient) {\n context.delta.bounces.push({ atMs: classification.atMs || nowMs, messageId: lastSentMessageId })\n }\n if (classification.complaint) {\n context.delta.complaintAtMs = Math.max(context.delta.complaintAtMs ?? 0, classification.atMs || nowMs)\n }\n }\n\n let event: OutreachEnrollmentEvent | null = null\n let postponedTo: number | null = null\n switch (decision.outcome) {\n case 'opted_out': {\n if (decidingMessage) await fileInbound(context, enrollment, decidingMessage)\n const complaint = decidedBy?.complaint === true\n await recordOutreachOptOut(deps, {\n orgId: context.orgId,\n email: enrollment.email,\n source: decidedBy?.evidence?.startsWith('To:') ? 'unsubscribe' : 'opt_out_reply',\n enrollment,\n hostIds: [enrollment.hostId],\n detail: decidedBy?.evidence ?? null,\n nowMs,\n complaint,\n })\n // A reply that called the email spam marks the send it answered\n // (AGL-3245), as the campaign webhook would mark a reported one.\n if (complaint) {\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'complained',\n atMs: decidedBy?.atMs || nowMs,\n detail: decidedBy?.evidence ?? null,\n })\n }\n report.optOuts += 1\n break\n }\n case 'replied': {\n if (decidingMessage) {\n await fileInbound(context, enrollment, decidingMessage)\n try {\n await fileOutreachTask(deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: `reply:${messageIdOf(decidingMessage) ?? decidingMessage.id}`,\n title: `Reply from ${enrollment.contactName || enrollment.email}`,\n notes: crmInboundExcerpt(decidingMessage.textBody ?? decidingMessage.snippet ?? '', decidingMessage.htmlBody ?? '').slice(0, 500),\n kind: 'email',\n dueAtMs: nowMs,\n assigneeUid: context.mailbox.connectedByUid,\n })\n } catch (error) {\n console.error('[outreach] the reply task could not be filed', error)\n }\n }\n // A lead somebody wrote back to is being worked (AGL-3234).\n if (enrollment.target === 'lead' && enrollment.leadId) {\n await markOutreachLeadWorking(firestore, { orgId: context.orgId, leadId: enrollment.leadId })\n }\n context.delta.replies += 1\n event = { type: 'reply', atMs: decidedBy?.atMs || nowMs, detail: decidedBy?.evidence ?? null }\n report.replies += 1\n // Once per enrollment (AGL-3254): a reply moves the status to\n // `replied`, and nothing is synced for it again.\n await creditOutreachReply(deps, { enrollment, atMs: event.atMs })\n break\n }\n case 'bounced': {\n const diagnostic = scrubbedOutreachDiagnostic(decidedBy?.bounce?.diagnostic ?? decidedBy?.bounce?.status ?? null, enrollment.email)\n await deps.suppressBouncedEmail({ email: enrollment.email, hostId: enrollment.hostId })\n await addOutreachDoNotContact(firestore, {\n orgId: context.orgId,\n email: enrollment.email,\n reason: 'hard_bounce',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n let detail = diagnostic\n // A gateway block is the domain's verdict on the sender (AGL-3244):\n // the domain is filed beside the address, and the enrollment says so.\n // Never a public mailbox provider's domain — Gmail refusing one\n // message on policy is not a reason to stop writing to Gmail.\n const domain = outreachEmailDomain(enrollment.email)\n const gateway = isOutreachGatewayBlock(decidedBy?.bounce ?? null)\n if (domain && !isPublicMailboxDomain(domain) && gateway) {\n await addOutreachDoNotContactDomain(firestore, {\n orgId: context.orgId,\n domain,\n reason: 'gateway_block',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n detail = outreachGatewayBlockDetail(domain, diagnostic)\n report.gatewayBlocks += 1\n }\n // The ledger learns the refusal (AGL-3326), whichever domain it was\n // at: a public provider refusing on policy is still that provider's\n // verdict on the sender, even though its domain is never listed.\n if (gateway) {\n await recordOutreachGatewayBlock(firestore, {\n orgId: context.orgId,\n email: enrollment.email,\n remoteMta: decidedBy?.bounce?.remoteMta ?? null,\n resolveMx: deps.resolveMx,\n nowMs,\n })\n }\n const bouncedAtMs = decidedBy?.atMs || nowMs\n // The record the person is says so too (AGL-3245): the verdict on the\n // lead and the contact, and the bounce on the send's own timeline\n // entry, so the record reads Sent, then Bounced.\n await deps.stampRecordEmailState({\n orgId: context.orgId,\n email: enrollment.email,\n state: {\n status: gateway ? 'blocked' : 'bounced',\n atMs: bouncedAtMs,\n source: 'sequence',\n detail: diagnostic,\n enrollmentId: enrollment.id,\n },\n })\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'bounced',\n atMs: bouncedAtMs,\n detail: diagnostic,\n })\n event = { type: 'bounce', atMs: bouncedAtMs, detail }\n report.bounces += 1\n break\n }\n case 'postpone': {\n const sequence = await sequenceOf(context, enrollment.sequenceId)\n postponedTo = postponeOutreachForAutoReply({\n nextDueAtMs: enrollment.nextDueAtMs,\n receivedAtMs: decidedBy?.atMs || nowMs,\n timeZone: context.mailbox.timezone,\n window: effectiveOutreachWindow(sequence?.settings.window, context.mailbox.window),\n random: deps.random,\n })\n break\n }\n default:\n break\n }\n\n const outcome = await applyOutreachEvent(firestore, {\n orgId: context.orgId,\n enrollmentId: enrollment.id,\n event,\n extra: (current) => {\n const extra: Record<string, unknown> = {\n syncedMessageIds: [...(current.syncedMessageIds ?? []), ...freshIds].slice(-ENROLLMENT_HANDLED_MAX),\n }\n const waiting = current.status === 'active' || current.status === 'paused'\n if (postponedTo !== null && waiting && current.nextDueAtMs !== null && postponedTo > current.nextDueAtMs) {\n extra['nextDueAtMs'] = postponedTo\n }\n return extra\n },\n nowMs,\n })\n if (postponedTo !== null && outcome.enrollment?.nextDueAtMs === postponedTo) report.postponed += 1\n // The person's record says the sequence ended (AGL-3274), once, under the\n // reply that ended it — after the transaction moved the status, so a run\n // that reads the same reply again finds nothing to file. An opt-out's\n // line is `recordOutreachOptOut`'s, beside the lists it writes.\n if (event?.type === 'reply' && outcome.changed) {\n const sequence = await sequenceOf(context, enrollment.sequenceId)\n const entry = outreachStoppedEntry({\n enrollmentId: enrollment.id,\n sequenceName: sequence?.name ?? '',\n reason: 'replied',\n })\n await fileOutreachNote(deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: event.atMs,\n })\n }\n // The run's own view of the enrollment moves with it, so a second message\n // of it in this run is judged against what this one did.\n if (outcome.enrollment) Object.assign(enrollment, outcome.enrollment)\n}\n"],"names":["normalizeContactEmail","isPublicMailboxDomain","crmInboundExcerpt","emailAddressOf","outreachMessageIdHeader","readOutreachDeliveryReport","outreachEmailDomain","outreachStoppedEntry","isOutreachGatewayBlock","outreachGatewayBlockDetail","effectiveOutreachWindow","postponeOutreachForAutoReply","decideOutreachThread","outreachHeader","outreachThreadMessageFromGmail","mailboxRef","OUTREACH_COLLECTIONS","addOutreachDoNotContact","addOutreachDoNotContactDomain","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","creditOutreachReply","applyOutreachEvent","recordOutreachOptOut","creditOutreachGatewayDeliveries","recordOutreachGatewayBlock","applyOutreachMailboxHealth","emptyOutreachHealthDelta","noteOutreachMailboxReconnectRequired","markOutreachLeadWorking","fileOutreachEmail","fileOutreachNote","fileOutreachTask","markOutreachEmailDelivery","outreachUnsubscribeMailbox","OUTREACH_SYNC_JOB_ID","OUTREACH_SYNC_LOOKBACK_MS","OUTREACH_SYNC_OVERLAP_MS","OUTREACH_SYNC_WATCH_MS","SEARCH_PAGES","MAILBOX_HANDLED_MAX","ENROLLMENT_HANDLED_MAX","FROM_CHUNK","runOutreachSyncJob","deps","context","firestore","report","mailboxes","threads","replies","optOuts","bounces","gatewayBlocks","deliveries","postponed","paused","all","collectionGroup","get","byOrg","Map","doc","docs","orgId","ref","parent","id","mailbox","data","status","set","now","deadlineMs","orgSnapshot","collection","org","exists","orgRefusal","syncMailbox","nowMs","error","mailboxId","errorCode","code","console","search","client","q","found","pageToken","page","list","listMessages","maxResults","includeSpamTrash","push","messages","filter","message","nextPageToken","input","opened","openMailbox","ok","reason","unsubscribe","email","selfAddresses","sendAs","sendAsOptions","map","option","Boolean","unsubscribeAddress","address","sequences","delta","watched","where","byThread","byAddress","watchedEnrollments","enrollment","known","threadId","gmailThreadIds","lastSentAtMs","sinceMs","Math","max","Number","sync","throughMs","after","floor","handled","Set","handledMessageIds","newlyHandled","markHandled","has","add","fresh","stub","inbound","thread","getFullThread","applyMessages","getFullMessage","delivery","kind","failedAddresses","metadata","getMessage","metadataHeaders","from","headers","find","header","name","toLowerCase","value","includes","source","hostIds","hostId","detail","addresses","keys","start","length","chunk","slice","join","enrollments","resolveMx","update","sequenceOf","sequenceId","snapshot","undefined","messageIdOf","scrubbedOutreachDiagnostic","diagnostic","pattern","RegExp","replace","fileInbound","messageId","link","direction","subject","to","inReplyTo","body","textBody","snippet","htmlBody","atMs","internalDateMs","byUid","outcome","decision","recipient","syncedMessageIds","threadSubject","freshIds","classifications","classification","decidedBy","decidingMessage","lastSentMessageId","messageIds","aboutRecipient","bounce","recipients","complaint","complaintAtMs","event","postponedTo","evidence","startsWith","state","dedupeKey","title","contactName","notes","dueAtMs","assigneeUid","connectedByUid","target","leadId","type","suppressBouncedEmail","enrollmentId","domain","gateway","remoteMta","bouncedAtMs","stampRecordEmailState","sequence","nextDueAtMs","receivedAtMs","timeZone","timezone","window","settings","random","extra","current","waiting","changed","entry","sequenceName","Object","assign"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,qBAAqB,QAAQ,kCAAiC;AACvE,SAASC,qBAAqB,QAAQ,6BAA4B;AAClE,SAASC,iBAAiB,EAAEC,cAAc,QAAQ,qCAAoC;AACtF,SAASC,uBAAuB,QAAQ,uBAAmB;AAC3D,SAASC,0BAA0B,QAAQ,+BAA2B;AACtE,SAASC,mBAAmB,QAAQ,qCAAiC;AACrE,SAASC,oBAAoB,QAAQ,mCAA+B;AAEpE,SAASC,sBAAsB,EAAEC,0BAA0B,QAAQ,6BAAyB;AAC5F,SAASC,uBAAuB,EAAEC,4BAA4B,QAAQ,wBAAoB;AAC1F,SACEC,oBAAoB,QAGf,qCAAiC;AACxC,SAASC,cAAc,EAAEC,8BAA8B,QAAoC,8BAA0B;AACrH,SAASC,UAAU,QAAQ,sCAAkC;AAC7D,SACEC,oBAAoB,QAIf,6BAAyB;AAChC,SAASC,uBAAuB,EAAEC,6BAA6B,QAAQ,qCAAiC;AACxG,SACEC,sBAAsB,EACtBC,qBAAqB,EACrBC,4BAA4B,EAC5BC,yBAAyB,EACzBC,0BAA0B,QACrB,iCAA6B;AAEpC,SAASC,mBAAmB,EAAEC,mBAAmB,QAAQ,+BAA2B;AACpF,SAASC,mBAAmB,QAAQ,uBAAmB;AACvD,SAASC,kBAAkB,EAAEC,oBAAoB,QAAQ,yBAAqB;AAC9E,SAASC,+BAA+B,EAAEC,0BAA0B,QAAQ,sBAAkB;AAC9F,SACEC,0BAA0B,EAC1BC,wBAAwB,QAEnB,4BAAwB;AAC/B,SAASC,oCAAoC,QAAQ,uBAAmB;AAExE,SAASC,uBAAuB,QAAQ,oBAAgB;AACxD,SAASC,iBAAiB,EAAEC,gBAAgB,EAAEC,gBAAgB,EAAEC,yBAAyB,QAAQ,gBAAY;AAC7G,SAASC,0BAA0B,QAAQ,wBAAoB;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4CA+C4C,GAE5C,SAASC,oBAAoB,QAAQ,+BAA2B;AAEhE,mFAAmF,GACnF,OAAO,MAAMC,4BAA4B,IAAI,KAAK,KAAK,MAAM;AAE7D,iFAAiF,GACjF,OAAO,MAAMC,2BAA2B,IAAI,KAAK,MAAM;AAEvD,wEAAwE,GACxE,OAAO,MAAMC,yBAAyB,KAAK,KAAK,KAAK,MAAM;AAE3D,8CAA8C,GAC9C,MAAMC,eAAe;AACrB,kEAAkE,GAClE,MAAMC,sBAAsB;AAC5B,MAAMC,yBAAyB;AAC/B,qCAAqC,GACrC,MAAMC,aAAa;AAiCnB,kDAAkD,GAClD,OAAO,eAAeC,mBACpBC,IAAyB,EACzBC,OAA8C;IAE9C,MAAMC,YAAYF,KAAKE,SAAS;IAChC,MAAMC,SAA6B;QACjCC,WAAW;QACXC,SAAS;QACTC,SAAS;QACTC,SAAS;QACTC,SAAS;QACTC,eAAe;QACfC,YAAY;QACZC,WAAW;QACXC,QAAQ;IACV;IACA,MAAMC,MAAM,MAAMX,UAAUY,eAAe,CAAC/C,qBAAqBqC,SAAS,EAAEW,GAAG;IAC/E,MAAMC,QAAQ,IAAIC;IAClB,KAAK,MAAMC,OAAOL,IAAIM,IAAI,CAAE;YAIJH;YAHRE;QAAd,MAAME,SAAQF,yBAAAA,IAAIG,GAAG,CAACC,MAAM,CAACA,MAAM,qBAArBJ,uBAAuBK,EAAE;QACvC,MAAMC,UAAUnD,0BAA0B6C,IAAIK,EAAE,EAAEL,IAAIO,IAAI;QAC1D,IAAI,CAACL,SAAS,CAACI,WAAYA,QAAQE,MAAM,KAAK,eAAeF,QAAQE,MAAM,KAAK,UAAW;QAC3FV,MAAMW,GAAG,CAACP,OAAO;gBAAKJ,aAAAA,MAAMD,GAAG,CAACK,kBAAVJ,aAAoB,EAAE;YAAGQ;SAAQ;IACzD;IACA,KAAK,MAAM,CAACJ,OAAOhB,UAAU,IAAIY,MAAO;QACtC,IAAIhB,KAAK4B,GAAG,MAAM3B,QAAQ4B,UAAU,EAAE;QACtC,MAAMC,cAAc,MAAM5B,UAAU6B,UAAU,CAAC,QAAQb,GAAG,CAACE,OAAOL,GAAG;QACrE,MAAMiB,MAAOF,YAAYG,MAAM,GAAGH,YAAYL,IAAI,KAAK;QACvD,IAAI,CAACO,OAAQ,MAAMhC,KAAKkC,UAAU,CAACd,OAAOY,MAAO;QACjD,KAAK,MAAMR,WAAWpB,UAAW;YAC/B,IAAIJ,KAAK4B,GAAG,MAAM3B,QAAQ4B,UAAU,EAAE;YACtC,IAAI;gBACF,MAAMM,YAAYnC,MAAME,WAAW;oBAAEkB;oBAAOI;oBAASrB;oBAAQiC,OAAOnC,QAAQmC,KAAK;gBAAC;gBAClFjC,OAAOC,SAAS,IAAI;YACtB,EAAE,OAAOiC,OAAO;gBACd,sEAAsE;gBACtE,gDAAgD;gBAChD,IAAI7D,oBAAoB6D,QAAQ;oBAC9B,MAAMrD,qCAAqCgB,MAAM;wBAC/CoB;wBACAkB,WAAWd,QAAQD,EAAE;wBACrBgB,WAAWF,iBAAiB9D,sBAAsB8D,MAAMG,IAAI,GAAG;wBAC/DJ,OAAOnC,QAAQmC,KAAK;oBACtB;gBACF;gBACAK,QAAQJ,KAAK,CAAC,uCAAuCA;YACvD;QACF;IACF;IACA,OAAOlC;AACT;AAEA,iDAAiD,GACjD,eAAeuC,OACbC,MAAmB,EACnBC,CAAS;IAET,MAAMC,QAAiD,EAAE;IACzD,IAAIC,YAA2B;IAC/B,IAAK,IAAIC,OAAO,GAAGA,OAAOpD,cAAcoD,QAAQ,EAAG;QACjD,MAAMC,OAAO,MAAML,OAAOM,YAAY,CAAC;YAAEL;YAAGM,YAAY;YAAKJ;YAAWK,kBAAkB;QAAK;QAC/FN,MAAMO,IAAI,IAAIJ,KAAKK,QAAQ,CAACC,MAAM,CAAC,CAACC,UAAYA,QAAQhC,EAAE;QAC1DuB,YAAYE,KAAKQ,aAAa;QAC9B,IAAI,CAACV,WAAW;IAClB;IACA,OAAOD;AACT;AAEA,eAAeV,YACbnC,IAAyB,EACzBE,SAAoB,EACpBuD,KAA6F;QAyBrFjC;QA0BwBA,eAIRA,gBAwFIA;IA7I5B,MAAM,EAAEJ,KAAK,EAAEI,OAAO,EAAErB,MAAM,EAAEiC,KAAK,EAAE,GAAGqB;IAC1C,MAAMC,SAAS,MAAM1D,KAAK2D,WAAW,CAACnC,QAAQD,EAAE;IAChD,IAAImC,OAAOE,EAAE,KAAK,OAAO;QACvB,IAAIF,OAAOG,MAAM,KAAK,kBAAkB;YACtC,MAAM7E,qCAAqCgB,MAAM;gBAC/CoB;gBACAkB,WAAWd,QAAQD,EAAE;gBACrBgB,WAAWmB,OAAOG,MAAM;gBACxBzB;YACF;QACF;QACA;IACF;IACA,MAAM0B,cAAcxE,2BAA2BkC,QAAQuC,KAAK;IAC5D,MAAM9D,UAAuB;QAC3BD;QACAE;QACAkB;QACAI;QACAmB,QAAQe,OAAOf,MAAM;QACrBqB,eAAe;YACbxC,QAAQuC,KAAK;YACbvC,QAAQyC,MAAM;eACX,EAACzC,yBAAAA,QAAQ0C,aAAa,YAArB1C,yBAAyB,EAAE,EAAE2C,GAAG,CAAC,CAACC,SAAWA,OAAOL,KAAK;SAC9D,CAACT,MAAM,CAACe;QACTC,kBAAkB,UAAER,+BAAAA,YAAaS,OAAO,mBAAI;QAC5CC,WAAW,IAAIvD;QACfwD,OAAO1F;QACPoB;QACAiC;IACF;IAEA,wEAAwE;IACxE,MAAMsC,UAAU,MAAMvG,sBAAsB+B,WAAWkB,OAAO,eAC3DuD,KAAK,CAAC,aAAa,MAAMnD,QAAQD,EAAE,EACnCoD,KAAK,CAAC,gBAAgB,MAAMvC,QAAQ1C,wBACpCqB,GAAG;IACN,MAAM6D,WAAW,IAAI3D;IACrB,MAAM4D,YAAY,IAAI5D;IACtB,MAAM6D,qBAA2C,EAAE;IACnD,KAAK,MAAM5D,OAAOwD,QAAQvD,IAAI,CAAE;YAMf4D,0BAAiCC;QALhD,MAAMD,aAAa3G,6BAA6B8C,IAAIK,EAAE,EAAEL,IAAIO,IAAI;QAChE,IAAI,CAACsD,YAAY;QACjBD,mBAAmB1B,IAAI,CAAC2B;QACxB,KAAK,MAAME,YAAYF,WAAWG,cAAc,CAAEN,SAASjD,GAAG,CAACsD,UAAUF;QACzE,MAAMC,QAAQH,UAAU9D,GAAG,CAACgE,WAAWhB,KAAK;QAC5C,IAAI,CAACiB,SAAS,EAACD,2BAAAA,WAAWI,YAAY,YAAvBJ,2BAA2B,OAAMC,sBAAAA,MAAMG,YAAY,YAAlBH,sBAAsB,IAAIH,UAAUlD,GAAG,CAACoD,WAAWhB,KAAK,EAAEgB;IAC5G;IAEA,MAAMK,UAAUC,KAAKC,GAAG,CAACC,QAAO/D,gBAAAA,QAAQgE,IAAI,qBAAZhE,cAAciE,SAAS,KAAK,GAAGrD,QAAQ5C,6BAA6BC;IACpG,MAAMiG,QAAQ,CAAC,MAAM,EAAEL,KAAKM,KAAK,CAACP,UAAU,OAAO;IACnD,2EAA2E;IAC3E,qEAAqE;IACrE,MAAMQ,UAAU,IAAIC,cAAIrE,iBAAAA,QAAQgE,IAAI,qBAAZhE,eAAcsE,iBAAiB,oBAAI,EAAE;IAC7D,MAAMC,eAAyB,EAAE;IACjC,MAAMC,cAAc,CAACzE;QACnB,IAAIqE,QAAQK,GAAG,CAAC1E,KAAK;QACrBqE,QAAQM,GAAG,CAAC3E;QACZwE,aAAa3C,IAAI,CAAC7B;IACpB;IACA,MAAM4E,QAAQ,CAACC,OAA2C,CAACR,QAAQK,GAAG,CAACG,KAAK7E,EAAE,KAAK,CAACqD,SAASqB,GAAG,CAACG,KAAKnB,QAAQ;IAE9G,sDAAsD;IACtD,MAAMoB,UAAU,MAAM3D,OAAOzC,QAAQ0C,MAAM,EAAE,GAAG+C,MAAM,SAAS,CAAC;IAChE,KAAK,MAAMT,YAAY,IAAIY,IAAIQ,QAAQlC,GAAG,CAAC,CAACiC,OAASA,KAAKnB,QAAQ,GAAI;QACpE,MAAMF,aAAaH,SAAS7D,GAAG,CAACkE;QAChC,IAAI,CAACF,YAAY;QACjB,MAAMuB,SAAS,MAAMrG,QAAQ0C,MAAM,CAAC4D,aAAa,CAACtB;QAClD9E,OAAOE,OAAO,IAAI;QAClB,MAAMmG,cAAcvG,SAAS8E,YAAYuB,OAAOjD,QAAQ,CAACc,GAAG,CAACtG;IAC/D;IAEA,0EAA0E;IAC1E,KAAK,MAAMuI,QAAQ,CAAA,MAAM1D,OAAOzC,QAAQ0C,MAAM,EAAE,GAAG+C,MAAM,mCAAmC,CAAC,CAAA,EAAG;QAC9F,IAAI,CAACS,MAAMC,OAAO;QAClB,MAAM7C,UAAU1F,+BAA+B,MAAMoC,QAAQ0C,MAAM,CAAC8D,cAAc,CAACL,KAAK7E,EAAE;QAC1F,MAAMmF,WAAWtJ,2BAA2BmG;QAC5C,IAAImD,CAAAA,4BAAAA,SAAUC,IAAI,MAAK,QAAQ;YAC7B,KAAK,MAAMpC,WAAWmC,SAASE,eAAe,CAAE;gBAC9C,MAAM7B,aAAaF,UAAU9D,GAAG,CAACwD;gBACjC,IAAIQ,YAAY,MAAMyB,cAAcvG,SAAS8E,YAAY;oBAACxB;iBAAQ;YACpE;QACF;QACAyC,YAAYI,KAAK7E,EAAE;IACrB;IAEA,wEAAwE;IACxE,IAAIuC,aAAa;QACf,KAAK,MAAMsC,QAAQ,CAAA,MAAM1D,OAAOzC,QAAQ0C,MAAM,EAAE,GAAG+C,MAAM,IAAI,EAAE5B,YAAYS,OAAO,EAAE,CAAA,EAAG;;gBAGzDsC;YAF5B,IAAIjB,QAAQK,GAAG,CAACG,KAAK7E,EAAE,GAAG;YAC1B,MAAMsF,WAAW,MAAM5G,QAAQ0C,MAAM,CAACmE,UAAU,CAACV,KAAK7E,EAAE,EAAE;gBAAEwF,iBAAiB;oBAAC;iBAAO;YAAC;YACtF,MAAMC,OAAO9J,yBAAe2J,yBAAAA,SAASI,OAAO,CAACC,IAAI,CAAC,CAACC,SAAWA,OAAOC,IAAI,CAACC,WAAW,OAAO,4BAAhER,uBAAyES,KAAK,oBAAI;YAC9G,MAAM/C,UAAUxH,sBAAsBiK;YACtC,IAAIzC,WAAW,CAACtE,QAAQ+D,aAAa,CAACuD,QAAQ,CAAChD,UAAU;oBACpCM;gBAAnB,MAAME,cAAaF,iBAAAA,UAAU9D,GAAG,CAACwD,oBAAdM,iBAA0B;gBAC7C,MAAMlG,qBAAqBqB,MAAM;oBAC/BoB;oBACA2C,OAAOQ;oBACPiD,QAAQ;oBACRzC;oBACA0C,SAAS1C,aAAa;wBAACA,WAAW2C,MAAM;qBAAC,GAAG,EAAE;oBAC9CC,QAAQ;oBACRvF;gBACF;gBACAjC,OAAOI,OAAO,IAAI;YACpB;YACAyF,YAAYI,KAAK7E,EAAE;QACrB;IACF;IAEA,4EAA4E;IAC5E,MAAMqG,YAAY;WAAI/C,UAAUgD,IAAI;KAAG;IACvC,IAAK,IAAIC,QAAQ,GAAGA,QAAQF,UAAUG,MAAM,EAAED,SAAShI,WAAY;QACjE,MAAMkI,QAAQJ,UAAUK,KAAK,CAACH,OAAOA,QAAQhI;QAC7C,KAAK,MAAMsG,QAAQ,CAAA,MAAM1D,OAAOzC,QAAQ0C,MAAM,EAAE,GAAG+C,MAAM,OAAO,EAAEsC,MAAME,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA,EAAG;gBAGvDnL;YAFjC,IAAI,CAACoJ,MAAMC,OAAO;YAClB,MAAM7C,UAAU1F,+BAA+B,MAAMoC,QAAQ0C,MAAM,CAAC8D,cAAc,CAACL,KAAK7E,EAAE;YAC1F,MAAMwD,aAAaF,UAAU9D,GAAG,EAAChE,yBAAAA,sBAAsBG,eAAeqG,QAAQyD,IAAI,cAAjDjK,yBAAuD;YACxF,IAAIgI,YAAY,MAAMyB,cAAcvG,SAAS8E,YAAY;gBAACxB;aAAQ;YAClEyC,YAAYI,KAAK7E,EAAE;QACrB;IACF;IAEA,oEAAoE;IACpE,kEAAkE;IAClE,IAAI;QACFpB,OAAOO,UAAU,IAAI,MAAM9B,gCAAgCsB,WAAW;YACpEkB;YACA+G,aAAarD;YACbsD,WAAWpI,KAAKoI,SAAS;YACzBhG;QACF;IACF,EAAE,OAAOC,OAAO;QACdI,QAAQJ,KAAK,CAAC,6DAA6DA;IAC7E;IAEA,MAAMzB,SAAS,MAAM9B,2BAA2BkB,MAAM;QAAEoB;QAAOkB,WAAWd,QAAQD,EAAE;QAAEkD,OAAOxE,QAAQwE,KAAK;IAAC;IAC3G,IAAI7D,QAAQT,OAAOS,MAAM,IAAI;IAC7B,MAAM9C,WAAWoC,WAAWkB,OAAOI,QAAQD,EAAE,EAAE8G,MAAM,CAAC;QACpD7C,MAAM;YACJC,WAAWrD;YACX0D,mBAAmB;6BAAKtE,iBAAAA,QAAQgE,IAAI,qBAAZhE,eAAcsE,iBAAiB,oBAAI,EAAE;mBAAMC;aAAa,CAACkC,KAAK,CAAC,CAACrI;QAC1F;IACF;AACF;AAEA,gEAAgE,GAChE,eAAe0I,WAAWrI,OAAoB,EAAEsI,UAAkB;QAKzDtI;IAJP,IAAI,CAACA,QAAQuE,SAAS,CAACyB,GAAG,CAACsC,aAAa;QACtC,MAAMC,WAAW,MAAMrK,sBAAsB8B,QAAQC,SAAS,EAAED,QAAQmB,KAAK,EAAE,aAAaF,GAAG,CAACqH,YAAYxH,GAAG;QAC/Gd,QAAQuE,SAAS,CAAC7C,GAAG,CAAC4G,YAAYjK,2BAA2BiK,YAAYC,SAASvG,MAAM,GAAGuG,SAAS/G,IAAI,KAAKgH;IAC/G;IACA,QAAOxI,yBAAAA,QAAQuE,SAAS,CAACzD,GAAG,CAACwH,uBAAtBtI,yBAAqC;AAC9C;AAEA,6EAA6E,GAC7E,MAAMyI,cAAc,CAACnF,UAAmCpG,wBAAwBS,eAAe2F,SAAS;AAExG,wEAAwE,GACxE,OAAO,SAASoF,2BAA2BC,UAAyB,EAAE7E,KAAa;IACjF,IAAI,CAAC6E,YAAY,OAAO;IACxB,MAAMC,UAAU,IAAIC,OAAO/E,MAAMgF,OAAO,CAAC,uBAAuB,SAAS;IACzE,OAAOH,WAAWG,OAAO,CAACF,SAAS;AACrC;AAEA,wDAAwD,GACxD,eAAeG,YAAY/I,OAAoB,EAAE8E,UAA8B,EAAExB,OAA8B;QASrGrG,iBAIkBqG,MAAAA,mBAA2CA;IAZrE,MAAM0F,YAAYP,YAAYnF;IAC9B,IAAI,CAAC0F,WAAW;IAChB,MAAM/J,kBAAkBe,QAAQD,IAAI,EAAE;QACpCoB,OAAOnB,QAAQmB,KAAK;QACpBsG,QAAQ3C,WAAW2C,MAAM;QACzBwB,MAAMhL,uBAAuB6G;QAC7BoE,WAAW;QACXC,SAAS7F,QAAQ6F,OAAO;QACxBpC,IAAI,GAAE9J,kBAAAA,eAAeqG,QAAQyD,IAAI,aAA3B9J,kBAAgCqG,QAAQyD,IAAI;QAClDqC,IAAIpJ,QAAQuB,OAAO,CAACyC,MAAM,IAAIhE,QAAQuB,OAAO,CAACuC,KAAK;QACnDkF;QACAK,WAAWnM,wBAAwBS,eAAe2F,SAAS;QAC3DgG,MAAMtM,mBAAkBsG,QAAAA,oBAAAA,QAAQiG,QAAQ,YAAhBjG,oBAAoBA,QAAQkG,OAAO,YAAnClG,OAAuC,KAAIA,oBAAAA,QAAQmG,QAAQ,YAAhBnG,oBAAoB;QACvFoG,MAAMpG,QAAQqG,cAAc;QAC5BC,OAAO;IACT;AACF;AAEA;;;CAGC,GACD,eAAerD,cACbvG,OAAoB,EACpB8E,UAA8B,EAC9B1B,QAA0C;QAMrB0B,8BAQe1B,gBACV0B;QAiLE+E;IA9L5B,MAAM,EAAE9J,IAAI,EAAEE,SAAS,EAAEC,MAAM,EAAEiC,KAAK,EAAE,GAAGnC;IAC3C,MAAM8J,WAAmCpM,qBAAqB;QAC5D0F;QACA2G,WAAWjF,WAAWhB,KAAK;QAC3B+B,iBAAiB,GAAEf,+BAAAA,WAAWkF,gBAAgB,YAA3BlF,+BAA+B,EAAE;QACpDf,eAAe/D,QAAQ+D,aAAa;QACpCM,oBAAoBrE,QAAQqE,kBAAkB;QAC9C4F,eAAenF,WAAWmF,aAAa;IACzC;IACA,MAAMC,WAAWJ,SAASK,eAAe,CAACjG,GAAG,CAAC,CAACkG,iBAAmBA,eAAepB,SAAS,EAAE3F,MAAM,CAACe;IACnG,IAAI,CAAC8F,SAASpC,MAAM,EAAE;IACtB,MAAMuC,YAAkDP,SAASO,SAAS;IAC1E,MAAMC,kBAAkBD,aAAYjH,iBAAAA,SAAS6D,IAAI,CAAC,CAAC3D,UAAYA,QAAQhC,EAAE,KAAK+I,UAAUrB,SAAS,aAA7D5F,iBAAkE,OAAO;IAC7G,MAAMmH,qBAAoBzF,0BAAAA,WAAW0F,UAAU,CAAC1F,WAAW0F,UAAU,CAAC1C,MAAM,GAAG,EAAE,YAAvDhD,0BAA2D;IAErF,KAAK,MAAMsF,kBAAkBN,SAASK,eAAe,CAAE;YAElDC;QADH,MAAMK,iBACJ,GAACL,yBAAAA,eAAeM,MAAM,qBAArBN,uBAAuBO,UAAU,CAAC7C,MAAM,KAAIsC,eAAeM,MAAM,CAACC,UAAU,CAACrD,QAAQ,CAACxC,WAAWhB,KAAK;QACzG,IAAIsG,eAAe1D,IAAI,KAAK,iBAAiB+D,gBAAgB;YAC3DzK,QAAQwE,KAAK,CAACjE,OAAO,CAAC4C,IAAI,CAAC;gBAAEuG,MAAMU,eAAeV,IAAI,IAAIvH;gBAAO6G,WAAWuB;YAAkB;QAChG;QACA,IAAIH,eAAeQ,SAAS,EAAE;gBACW5K;YAAvCA,QAAQwE,KAAK,CAACqG,aAAa,GAAGzF,KAAKC,GAAG,EAACrF,+BAAAA,QAAQwE,KAAK,CAACqG,aAAa,YAA3B7K,+BAA+B,GAAGoK,eAAeV,IAAI,IAAIvH;QAClG;IACF;IAEA,IAAI2I,QAAwC;IAC5C,IAAIC,cAA6B;IACjC,OAAQjB,SAASD,OAAO;QACtB,KAAK;YAAa;;oBAMNQ;gBALV,IAAIC,iBAAiB,MAAMvB,YAAY/I,SAAS8E,YAAYwF;gBAC5D,MAAMM,YAAYP,CAAAA,6BAAAA,UAAWO,SAAS,MAAK;gBAC3C,MAAMlM,qBAAqBqB,MAAM;oBAC/BoB,OAAOnB,QAAQmB,KAAK;oBACpB2C,OAAOgB,WAAWhB,KAAK;oBACvByD,QAAQ8C,CAAAA,8BAAAA,sBAAAA,UAAWW,QAAQ,qBAAnBX,oBAAqBY,UAAU,CAAC,UAAS,gBAAgB;oBACjEnG;oBACA0C,SAAS;wBAAC1C,WAAW2C,MAAM;qBAAC;oBAC5BC,MAAM,UAAE2C,6BAAAA,UAAWW,QAAQ,mBAAI;oBAC/B7I;oBACAyI;gBACF;gBACA,gEAAgE;gBAChE,iEAAiE;gBACjE,IAAIA,WAAW;;oBACb,MAAMxL,0BAA0BW,MAAM;wBACpCoB,OAAOnB,QAAQmB,KAAK;wBACpB6H,WAAWuB;wBACXW,OAAO;wBACPxB,MAAMW,CAAAA,6BAAAA,UAAWX,IAAI,KAAIvH;wBACzBuF,MAAM,WAAE2C,6BAAAA,UAAWW,QAAQ,oBAAI;oBACjC;gBACF;gBACA9K,OAAOI,OAAO,IAAI;gBAClB;YACF;QACA,KAAK;YAAW;;gBACd,IAAIgK,iBAAiB;oBACnB,MAAMvB,YAAY/I,SAAS8E,YAAYwF;oBACvC,IAAI;4BAKoB7B,cAEK6B,OAAAA,2BAA2DA;wBANtF,MAAMnL,iBAAiBY,MAAM;4BAC3BoB,OAAOnB,QAAQmB,KAAK;4BACpBsG,QAAQ3C,WAAW2C,MAAM;4BACzBwB,MAAMhL,uBAAuB6G;4BAC7BqG,WAAW,CAAC,MAAM,GAAE1C,eAAAA,YAAY6B,4BAAZ7B,eAAgC6B,gBAAgBhJ,EAAE,EAAE;4BACxE8J,OAAO,CAAC,WAAW,EAAEtG,WAAWuG,WAAW,IAAIvG,WAAWhB,KAAK,EAAE;4BACjEwH,OAAOtO,mBAAkBsN,SAAAA,4BAAAA,gBAAgBf,QAAQ,YAAxBe,4BAA4BA,gBAAgBd,OAAO,YAAnDc,QAAuD,KAAIA,4BAAAA,gBAAgBb,QAAQ,YAAxBa,4BAA4B,IAAItC,KAAK,CAAC,GAAG;4BAC7HtB,MAAM;4BACN6E,SAASpJ;4BACTqJ,aAAaxL,QAAQuB,OAAO,CAACkK,cAAc;wBAC7C;oBACF,EAAE,OAAOrJ,OAAO;wBACdI,QAAQJ,KAAK,CAAC,gDAAgDA;oBAChE;gBACF;gBACA,4DAA4D;gBAC5D,IAAI0C,WAAW4G,MAAM,KAAK,UAAU5G,WAAW6G,MAAM,EAAE;oBACrD,MAAM3M,wBAAwBiB,WAAW;wBAAEkB,OAAOnB,QAAQmB,KAAK;wBAAEwK,QAAQ7G,WAAW6G,MAAM;oBAAC;gBAC7F;gBACA3L,QAAQwE,KAAK,CAACnE,OAAO,IAAI;gBACzByK,QAAQ;oBAAEc,MAAM;oBAASlC,MAAMW,CAAAA,6BAAAA,UAAWX,IAAI,KAAIvH;oBAAOuF,MAAM,WAAE2C,6BAAAA,UAAWW,QAAQ,oBAAI;gBAAK;gBAC7F9K,OAAOG,OAAO,IAAI;gBAClB,8DAA8D;gBAC9D,iDAAiD;gBACjD,MAAM7B,oBAAoBuB,MAAM;oBAAE+E;oBAAY4E,MAAMoB,MAAMpB,IAAI;gBAAC;gBAC/D;YACF;QACA,KAAK;YAAW;oBACgCW;oBAAAA,mBAAiCA;gBAA/E,MAAM1B,aAAaD,4BAA2B2B,iBAAAA,8BAAAA,oBAAAA,UAAWK,MAAM,qBAAjBL,kBAAmB1B,UAAU,oBAAI0B,8BAAAA,qBAAAA,UAAWK,MAAM,qBAAjBL,mBAAmB5I,MAAM,YAA1D4I,QAA8D,MAAMvF,WAAWhB,KAAK;gBAClI,MAAM/D,KAAK8L,oBAAoB,CAAC;oBAAE/H,OAAOgB,WAAWhB,KAAK;oBAAE2D,QAAQ3C,WAAW2C,MAAM;gBAAC;gBACrF,MAAM1J,wBAAwBkC,WAAW;oBACvCkB,OAAOnB,QAAQmB,KAAK;oBACpB2C,OAAOgB,WAAWhB,KAAK;oBACvBF,QAAQ;oBACR2D,QAAQ;oBACRpF;oBACA2J,cAAchH,WAAWxD,EAAE;oBAC3BgH,YAAYxD,WAAWwD,UAAU;oBACjCZ,QAAQiB;gBACV;gBACA,IAAIjB,SAASiB;gBACb,oEAAoE;gBACpE,sEAAsE;gBACtE,gEAAgE;gBAChE,8DAA8D;gBAC9D,MAAMoD,SAAS3O,oBAAoB0H,WAAWhB,KAAK;gBACnD,MAAMkI,UAAU1O,gCAAuB+M,6BAAAA,UAAWK,MAAM,oBAAI;gBAC5D,IAAIqB,UAAU,CAAChP,sBAAsBgP,WAAWC,SAAS;oBACvD,MAAMhO,8BAA8BiC,WAAW;wBAC7CkB,OAAOnB,QAAQmB,KAAK;wBACpB4K;wBACAnI,QAAQ;wBACR2D,QAAQ;wBACRpF;wBACA2J,cAAchH,WAAWxD,EAAE;wBAC3BgH,YAAYxD,WAAWwD,UAAU;wBACjCZ,QAAQiB;oBACV;oBACAjB,SAASnK,2BAA2BwO,QAAQpD;oBAC5CzI,OAAOM,aAAa,IAAI;gBAC1B;gBACA,oEAAoE;gBACpE,oEAAoE;gBACpE,iEAAiE;gBACjE,IAAIwL,SAAS;;wBAIE3B;oBAHb,MAAMzL,2BAA2BqB,WAAW;wBAC1CkB,OAAOnB,QAAQmB,KAAK;wBACpB2C,OAAOgB,WAAWhB,KAAK;wBACvBmI,SAAS,WAAE5B,8BAAAA,qBAAAA,UAAWK,MAAM,qBAAjBL,mBAAmB4B,SAAS,oBAAI;wBAC3C9D,WAAWpI,KAAKoI,SAAS;wBACzBhG;oBACF;gBACF;gBACA,MAAM+J,cAAc7B,CAAAA,6BAAAA,UAAWX,IAAI,KAAIvH;gBACvC,sEAAsE;gBACtE,kEAAkE;gBAClE,iDAAiD;gBACjD,MAAMpC,KAAKoM,qBAAqB,CAAC;oBAC/BhL,OAAOnB,QAAQmB,KAAK;oBACpB2C,OAAOgB,WAAWhB,KAAK;oBACvBoH,OAAO;wBACLzJ,QAAQuK,UAAU,YAAY;wBAC9BtC,MAAMwC;wBACN3E,QAAQ;wBACRG,QAAQiB;wBACRmD,cAAchH,WAAWxD,EAAE;oBAC7B;gBACF;gBACA,MAAMlC,0BAA0BW,MAAM;oBACpCoB,OAAOnB,QAAQmB,KAAK;oBACpB6H,WAAWuB;oBACXW,OAAO;oBACPxB,MAAMwC;oBACNxE,QAAQiB;gBACV;gBACAmC,QAAQ;oBAAEc,MAAM;oBAAUlC,MAAMwC;oBAAaxE;gBAAO;gBACpDxH,OAAOK,OAAO,IAAI;gBAClB;YACF;QACA,KAAK;YAAY;gBACf,MAAM6L,WAAW,MAAM/D,WAAWrI,SAAS8E,WAAWwD,UAAU;gBAChEyC,cAActN,6BAA6B;oBACzC4O,aAAavH,WAAWuH,WAAW;oBACnCC,cAAcjC,CAAAA,6BAAAA,UAAWX,IAAI,KAAIvH;oBACjCoK,UAAUvM,QAAQuB,OAAO,CAACiL,QAAQ;oBAClCC,QAAQjP,wBAAwB4O,4BAAAA,SAAUM,QAAQ,CAACD,MAAM,EAAEzM,QAAQuB,OAAO,CAACkL,MAAM;oBACjFE,QAAQ5M,KAAK4M,MAAM;gBACrB;gBACA;YACF;QACA;YACE;IACJ;IAEA,MAAM9C,UAAU,MAAMpL,mBAAmBwB,WAAW;QAClDkB,OAAOnB,QAAQmB,KAAK;QACpB2K,cAAchH,WAAWxD,EAAE;QAC3BwJ;QACA8B,OAAO,CAACC;gBAEmBA;YADzB,MAAMD,QAAiC;gBACrC5C,kBAAkB;wBAAK6C,4BAAAA,QAAQ7C,gBAAgB,YAAxB6C,4BAA4B,EAAE;uBAAM3C;iBAAS,CAAClC,KAAK,CAAC,CAACpI;YAC9E;YACA,MAAMkN,UAAUD,QAAQpL,MAAM,KAAK,YAAYoL,QAAQpL,MAAM,KAAK;YAClE,IAAIsJ,gBAAgB,QAAQ+B,WAAWD,QAAQR,WAAW,KAAK,QAAQtB,cAAc8B,QAAQR,WAAW,EAAE;gBACxGO,KAAK,CAAC,cAAc,GAAG7B;YACzB;YACA,OAAO6B;QACT;QACAzK;IACF;IACA,IAAI4I,gBAAgB,QAAQlB,EAAAA,sBAAAA,QAAQ/E,UAAU,qBAAlB+E,oBAAoBwC,WAAW,MAAKtB,aAAa7K,OAAOQ,SAAS,IAAI;IACjG,0EAA0E;IAC1E,yEAAyE;IACzE,sEAAsE;IACtE,gEAAgE;IAChE,IAAIoK,CAAAA,yBAAAA,MAAOc,IAAI,MAAK,WAAW/B,QAAQkD,OAAO,EAAE;;QAC9C,MAAMX,WAAW,MAAM/D,WAAWrI,SAAS8E,WAAWwD,UAAU;QAChE,MAAM0E,QAAQ3P,qBAAqB;YACjCyO,cAAchH,WAAWxD,EAAE;YAC3B2L,YAAY,WAAEb,4BAAAA,SAAUjF,IAAI,oBAAI;YAChCvD,QAAQ;QACV;QACA,MAAM1E,iBAAiBa,MAAM;YAC3BoB,OAAOnB,QAAQmB,KAAK;YACpBsG,QAAQ3C,WAAW2C,MAAM;YACzBwB,MAAMhL,uBAAuB6G;YAC7BqG,WAAW6B,MAAM7B,SAAS;YAC1B7B,MAAM0D,MAAM1D,IAAI;YAChBI,MAAMoB,MAAMpB,IAAI;QAClB;IACF;IACA,0EAA0E;IAC1E,yDAAyD;IACzD,IAAIG,QAAQ/E,UAAU,EAAEoI,OAAOC,MAAM,CAACrI,YAAY+E,QAAQ/E,UAAU;AACtE"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/sync-job.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\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { isPublicMailboxDomain } from '@aglyn/aglyn/app-utils/crm'\nimport { crmInboundExcerpt, emailAddressOf } from '@aglyn/aglyn/app-utils/crm-inbound'\nimport { outreachMessageIdHeader } from '../engine/compose'\nimport { readOutreachDeliveryReport } from '../engine/delivery-status'\nimport { outreachEmailDomain } from '../engine/do-not-contact-domain'\nimport { outreachStoppedEntry } from '../engine/enrollment-activity'\nimport type { OutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport { isOutreachGatewayBlock, outreachGatewayBlockDetail } from '../engine/gateway-block'\nimport { effectiveOutreachWindow, postponeOutreachForAutoReply } from '../engine/schedule'\nimport {\n decideOutreachThread,\n type OutreachMessageClassification,\n type OutreachThreadDecision,\n} from '../engine/thread-classification'\nimport { outreachHeader, outreachThreadMessageFromGmail, type OutreachThreadMessage } from '../engine/thread-message'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachEnrollment,\n type OutreachMailbox,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport { addOutreachDoNotContact, addOutreachDoNotContactDomain } from '../storage/do-not-contact-store'\nimport {\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport type { GmailClient } from '../transport/gmail-client'\nimport { GmailTransportError, isReconnectRequired } from '../transport/gmail-errors'\nimport { creditOutreachReply } from './campaign-credit'\nimport { applyOutreachEvent, recordOutreachOptOut } from './enrollment-events'\nimport { creditOutreachGatewayDeliveries, recordOutreachGatewayBlock } from './gateway-ledger'\nimport { outreachMailboxSendingDomain } from '../storage/domain-intel-store'\nimport {\n applyOutreachMailboxHealth,\n emptyOutreachHealthDelta,\n type OutreachMailboxHealthDelta,\n} from './mailbox-health-store'\nimport { noteOutreachMailboxReconnectRequired } from './mailbox-notices'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { markOutreachLeadWorking } from './lead-records'\nimport { fileOutreachEmail, fileOutreachNote, fileOutreachTask, markOutreachEmailDelivery } from './timeline'\nimport { outreachUnsubscribeMailbox } from './unsubscribe-link'\n\n/*==========================================\n * THE SYNC JOB (AGL-2981): every fifteen minutes, on the console.\n *\n * For each connected or paused mailbox, what came back since the last run —\n * read with `format=full`, turned into the engine's message shape and\n * classified by `decideOutreachThread`:\n *\n * - a REPLY stops the enrollment, files the message on the contact's\n * timeline (once per `Message-ID`, under the capture address's own id for\n * it) and gives the rep the task \"Reply from <name>\";\n * - an OUT-OF-OFFICE postpones the next step a working week from the reply;\n * - an OPT-OUT — a reply asking to be left alone, or a message to the\n * `+unsubscribe` address the `mailto:` names — puts the address on the\n * organization's do-not-contact list and off the site's `sales` topic,\n * and stops every open enrollment of it;\n * - a HARD BOUNCE stops the enrollment, files the address on the platform's\n * suppression list and the do-not-contact list — and, when its diagnostic\n * reads as the domain's mail gateway refusing the sender rather than one\n * address being unknown (`engine/gateway-block`, AGL-3244), files the\n * DOMAIN on the do-not-contact list too, and says so on the enrollment —\n * and counts the block on the gateway ledger (AGL-3326), under the\n * gateway the bounce's `Remote-MTA` names.\n *\n * And once the mail is read, every email step of a watched enrollment that\n * is a day old with no bounce against it is credited to the ledger as\n * delivered (`./gateway-ledger`): the silence that says a gateway let the\n * sender through.\n *\n * Every verdict a list takes is said on the record the person is as well\n * (AGL-3245): the lead and the contact carry `emailState` through the\n * platform's stamp, and a bounce or a complaint lands on the send's own\n * timeline entry, so a member reading the record sees what the runtime saw.\n *\n * Four reads find those, each narrowed by Gmail rather than read in full:\n * the threads of this mailbox's enrollments that have new mail; delivery\n * reports outside those threads (a receiving server's own bounce), by\n * searching for the mail systems that send them; messages to the\n * unsubscribe address; and messages from the people this mailbox is\n * writing to, in a thread of their own.\n *\n * Every write it makes is idempotent — the do-not-contact list keeps its\n * first entry, a topic opt-out keeps its first moment, a timeline entry is\n * filed once per message — and the messages it handled are marked AFTER\n * their effects, on the enrollment and on the mailbox, so a run that dies\n * part-way repeats harmless work rather than skipping some. The mailbox's\n * health takes the run's bounces and complaints in one write at the end,\n * which is where it pauses itself.\n *==========================================*/\n\nexport { OUTREACH_SYNC_JOB_ID } from '../constants/runtime-jobs'\n\n/** The furthest back a run reads: a mailbox's first run, or one that fell behind. */\nexport const OUTREACH_SYNC_LOOKBACK_MS = 7 * 24 * 60 * 60_000\n\n/** How far a run reads back past the previous one, for mail Gmail indexed late. */\nexport const OUTREACH_SYNC_OVERLAP_MS = 2 * 60 * 60_000\n\n/** How long after its last send an enrollment's mail is still watched. */\nexport const OUTREACH_SYNC_WATCH_MS = 45 * 24 * 60 * 60_000\n\n/** The most pages of one search a run reads. */\nconst SEARCH_PAGES = 5\n/** Handled message ids a mailbox keeps, and an enrollment keeps. */\nconst MAILBOX_HANDLED_MAX = 500\nconst ENROLLMENT_HANDLED_MAX = 100\n/** Addresses in one `from:` search. */\nconst FROM_CHUNK = 15\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface OutreachSyncReport {\n [key: string]: number\n mailboxes: number\n threads: number\n replies: number\n optOuts: number\n bounces: number\n /** Hard bounces that read as a gateway block, each filing its domain (AGL-3244). */\n gatewayBlocks: number\n /** Email steps a day old with no bounce, credited to the gateway ledger (AGL-3326). */\n deliveries: number\n postponed: number\n paused: number\n}\n\ninterface SyncContext {\n deps: OutreachRuntimeDeps\n firestore: Firestore\n orgId: string\n mailbox: OutreachMailbox\n client: GmailClient\n selfAddresses: string[]\n unsubscribeAddress: string | null\n sequences: Map<string, OutreachSequence | null>\n delta: OutreachMailboxHealthDelta\n report: OutreachSyncReport\n nowMs: number\n}\n\n/** Runs the sync job once — see the module note. */\nexport async function runOutreachSyncJob(\n deps: OutreachRuntimeDeps,\n context: { nowMs: number; deadlineMs: number },\n): Promise<OutreachSyncReport> {\n const firestore = deps.firestore()\n const report: OutreachSyncReport = {\n mailboxes: 0,\n threads: 0,\n replies: 0,\n optOuts: 0,\n bounces: 0,\n gatewayBlocks: 0,\n deliveries: 0,\n postponed: 0,\n paused: 0,\n }\n const all = await firestore.collectionGroup(OUTREACH_COLLECTIONS.mailboxes).get()\n const byOrg = new Map<string, OutreachMailbox[]>()\n for (const doc of all.docs) {\n const orgId = doc.ref.parent.parent?.id\n const mailbox = readStoredOutreachMailbox(doc.id, doc.data())\n if (!orgId || !mailbox || (mailbox.status !== 'connected' && mailbox.status !== 'paused')) continue\n byOrg.set(orgId, [...(byOrg.get(orgId) ?? []), mailbox])\n }\n for (const [orgId, mailboxes] of byOrg) {\n if (deps.now() >= context.deadlineMs) break\n const orgSnapshot = await firestore.collection('orgs').doc(orgId).get()\n const org = (orgSnapshot.exists ? orgSnapshot.data() : null) as Record<string, unknown> | null\n if (!org || (await deps.orgRefusal(orgId, org))) continue\n for (const mailbox of mailboxes) {\n if (deps.now() >= context.deadlineMs) break\n try {\n await syncMailbox(deps, firestore, { orgId, mailbox, report, nowMs: context.nowMs })\n report.mailboxes += 1\n } catch (error) {\n // One mailbox's failure is its own; the next mailbox is read. A grant\n // Google refused is the mailbox's to reconnect.\n if (isReconnectRequired(error)) {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: error instanceof GmailTransportError ? error.code : 'invalid_grant',\n nowMs: context.nowMs,\n })\n }\n console.error('[outreach] syncing a mailbox failed', error)\n }\n }\n }\n return report\n}\n\n/** Every stub a search finds, a few pages deep. */\nasync function search(\n client: GmailClient,\n q: string,\n): Promise<Array<{ id: string; threadId: string }>> {\n const found: Array<{ id: string; threadId: string }> = []\n let pageToken: string | null = null\n for (let page = 0; page < SEARCH_PAGES; page += 1) {\n const list = await client.listMessages({ q, maxResults: 100, pageToken, includeSpamTrash: true })\n found.push(...list.messages.filter((message) => message.id))\n pageToken = list.nextPageToken\n if (!pageToken) break\n }\n return found\n}\n\nasync function syncMailbox(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n input: { orgId: string; mailbox: OutreachMailbox; report: OutreachSyncReport; nowMs: number },\n): Promise<void> {\n const { orgId, mailbox, report, nowMs } = input\n const opened = await deps.openMailbox(mailbox.id)\n if (opened.ok === false) {\n if (opened.reason !== 'not-configured') {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n }\n return\n }\n const unsubscribe = outreachUnsubscribeMailbox(mailbox.email)\n const context: SyncContext = {\n deps,\n firestore,\n orgId,\n mailbox,\n client: opened.client,\n selfAddresses: [\n mailbox.email,\n mailbox.sendAs,\n ...(mailbox.sendAsOptions ?? []).map((option) => option.email),\n ].filter(Boolean),\n unsubscribeAddress: unsubscribe?.address ?? null,\n sequences: new Map(),\n delta: emptyOutreachHealthDelta(),\n report,\n nowMs,\n }\n\n // The enrollments this mailbox is writing to, by thread and by address.\n const watched = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('mailboxId', '==', mailbox.id)\n .where('lastSentAtMs', '>=', nowMs - OUTREACH_SYNC_WATCH_MS)\n .get()\n const byThread = new Map<string, OutreachEnrollment>()\n const byAddress = new Map<string, OutreachEnrollment>()\n const watchedEnrollments: OutreachEnrollment[] = []\n for (const doc of watched.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment) continue\n watchedEnrollments.push(enrollment)\n for (const threadId of enrollment.gmailThreadIds) byThread.set(threadId, enrollment)\n const known = byAddress.get(enrollment.email)\n if (!known || (enrollment.lastSentAtMs ?? 0) > (known.lastSentAtMs ?? 0)) byAddress.set(enrollment.email, enrollment)\n }\n\n const sinceMs = Math.max(Number(mailbox.sync?.throughMs) || 0, nowMs - OUTREACH_SYNC_LOOKBACK_MS) - OUTREACH_SYNC_OVERLAP_MS\n const after = `after:${Math.floor(sinceMs / 1000)}`\n // Every message this run or an earlier one handled outside an enrollment's\n // thread; one message is handled once however many searches find it.\n const handled = new Set(mailbox.sync?.handledMessageIds ?? [])\n const newlyHandled: string[] = []\n const markHandled = (id: string) => {\n if (handled.has(id)) return\n handled.add(id)\n newlyHandled.push(id)\n }\n const fresh = (stub: { id: string; threadId: string }) => !handled.has(stub.id) && !byThread.has(stub.threadId)\n\n // 1. The enrollments' own threads that have new mail.\n const inbound = await search(context.client, `${after} -from:me`)\n for (const threadId of new Set(inbound.map((stub) => stub.threadId))) {\n const enrollment = byThread.get(threadId)\n if (!enrollment) continue\n const thread = await context.client.getFullThread(threadId)\n report.threads += 1\n await applyMessages(context, enrollment, thread.messages.map(outreachThreadMessageFromGmail))\n }\n\n // 2. Delivery reports outside those threads: a receiving server's bounce.\n for (const stub of await search(context.client, `${after} from:(mailer-daemon OR postmaster)`)) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const delivery = readOutreachDeliveryReport(message)\n if (delivery?.kind === 'hard') {\n for (const address of delivery.failedAddresses) {\n const enrollment = byAddress.get(address)\n if (enrollment) await applyMessages(context, enrollment, [message])\n }\n }\n markHandled(stub.id)\n }\n\n // 3. Messages to the unsubscribe address: whoever wrote asked to leave.\n if (unsubscribe) {\n for (const stub of await search(context.client, `${after} to:${unsubscribe.address}`)) {\n if (handled.has(stub.id)) continue\n const metadata = await context.client.getMessage(stub.id, { metadataHeaders: ['From'] })\n const from = emailAddressOf(metadata.headers.find((header) => header.name.toLowerCase() === 'from')?.value ?? '')\n const address = normalizeContactEmail(from)\n if (address && !context.selfAddresses.includes(address)) {\n const enrollment = byAddress.get(address) ?? null\n await recordOutreachOptOut(deps, {\n orgId,\n email: address,\n source: 'unsubscribe',\n enrollment,\n hostIds: enrollment ? [enrollment.hostId] : [],\n detail: 'Wrote to the unsubscribe address.',\n nowMs,\n })\n report.optOuts += 1\n }\n markHandled(stub.id)\n }\n }\n\n // 4. The people this mailbox writes to, answering in a thread of their own.\n const addresses = [...byAddress.keys()]\n for (let start = 0; start < addresses.length; start += FROM_CHUNK) {\n const chunk = addresses.slice(start, start + FROM_CHUNK)\n for (const stub of await search(context.client, `${after} from:(${chunk.join(' OR ')})`)) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const enrollment = byAddress.get(normalizeContactEmail(emailAddressOf(message.from)) ?? '')\n if (enrollment) await applyMessages(context, enrollment, [message])\n markHandled(stub.id)\n }\n }\n\n // 5. What went through: a day of silence after a send is a delivery\n // (AGL-3326), judged on the enrollments as this run left them.\n try {\n report.deliveries += await creditOutreachGatewayDeliveries(firestore, {\n orgId,\n enrollments: watchedEnrollments,\n sendingDomain: outreachMailboxSendingDomain(mailbox),\n resolveMx: deps.resolveMx,\n nowMs,\n })\n } catch (error) {\n console.error('[outreach] the gateway ledger could not credit deliveries', error)\n }\n\n const paused = await applyOutreachMailboxHealth(deps, { orgId, mailboxId: mailbox.id, delta: context.delta })\n if (paused) report.paused += 1\n await mailboxRef(firestore, orgId, mailbox.id).update({\n sync: {\n throughMs: nowMs,\n handledMessageIds: [...(mailbox.sync?.handledMessageIds ?? []), ...newlyHandled].slice(-MAILBOX_HANDLED_MAX),\n },\n })\n}\n\n/** The sequence an enrollment runs through, read once per run. */\nasync function sequenceOf(context: SyncContext, sequenceId: string): Promise<OutreachSequence | null> {\n if (!context.sequences.has(sequenceId)) {\n const snapshot = await outreachOrgCollection(context.firestore, context.orgId, 'sequences').doc(sequenceId).get()\n context.sequences.set(sequenceId, readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined))\n }\n return context.sequences.get(sequenceId) ?? null\n}\n\n/** A message's `Message-ID` in header form, or `null` when it carries none. */\nconst messageIdOf = (message: OutreachThreadMessage) => outreachMessageIdHeader(outreachHeader(message, 'Message-ID'))\n\n/** A bounce's diagnostic with the recipient's address taken out of it. */\nexport function scrubbedOutreachDiagnostic(diagnostic: string | null, email: string): string | null {\n if (!diagnostic) return null\n const pattern = new RegExp(email.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'gi')\n return diagnostic.replace(pattern, 'the address')\n}\n\n/** Files a message the person wrote on their timeline. */\nasync function fileInbound(context: SyncContext, enrollment: OutreachEnrollment, message: OutreachThreadMessage) {\n const messageId = messageIdOf(message)\n if (!messageId) return\n await fileOutreachEmail(context.deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n direction: 'inbound',\n subject: message.subject,\n from: emailAddressOf(message.from) ?? message.from,\n to: context.mailbox.sendAs || context.mailbox.email,\n messageId,\n inReplyTo: outreachMessageIdHeader(outreachHeader(message, 'In-Reply-To')),\n body: crmInboundExcerpt(message.textBody ?? message.snippet ?? '', message.htmlBody ?? ''),\n atMs: message.internalDateMs,\n byUid: '',\n })\n}\n\n/**\n * What the new messages of one enrollment mean, done: the effects first,\n * each idempotent, then the event and the handled ids in one transaction.\n */\nasync function applyMessages(\n context: SyncContext,\n enrollment: OutreachEnrollment,\n messages: readonly OutreachThreadMessage[],\n): Promise<void> {\n const { deps, firestore, report, nowMs } = context\n const decision: OutreachThreadDecision = decideOutreachThread({\n messages,\n recipient: enrollment.email,\n handledMessageIds: enrollment.syncedMessageIds ?? [],\n selfAddresses: context.selfAddresses,\n unsubscribeAddress: context.unsubscribeAddress,\n threadSubject: enrollment.threadSubject,\n })\n const freshIds = decision.classifications.map((classification) => classification.messageId).filter(Boolean)\n if (!freshIds.length) return\n const decidedBy: OutreachMessageClassification | null = decision.decidedBy\n const decidingMessage = decidedBy ? messages.find((message) => message.id === decidedBy.messageId) ?? null : null\n const lastSentMessageId = enrollment.messageIds[enrollment.messageIds.length - 1] ?? null\n\n for (const classification of decision.classifications) {\n const aboutRecipient =\n !classification.bounce?.recipients.length || classification.bounce.recipients.includes(enrollment.email)\n if (classification.kind === 'hard_bounce' && aboutRecipient) {\n context.delta.bounces.push({ atMs: classification.atMs || nowMs, messageId: lastSentMessageId })\n }\n if (classification.complaint) {\n context.delta.complaintAtMs = Math.max(context.delta.complaintAtMs ?? 0, classification.atMs || nowMs)\n }\n }\n\n let event: OutreachEnrollmentEvent | null = null\n let postponedTo: number | null = null\n switch (decision.outcome) {\n case 'opted_out': {\n if (decidingMessage) await fileInbound(context, enrollment, decidingMessage)\n const complaint = decidedBy?.complaint === true\n await recordOutreachOptOut(deps, {\n orgId: context.orgId,\n email: enrollment.email,\n source: decidedBy?.evidence?.startsWith('To:') ? 'unsubscribe' : 'opt_out_reply',\n enrollment,\n hostIds: [enrollment.hostId],\n detail: decidedBy?.evidence ?? null,\n nowMs,\n complaint,\n })\n // A reply that called the email spam marks the send it answered\n // (AGL-3245), as the campaign webhook would mark a reported one.\n if (complaint) {\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'complained',\n atMs: decidedBy?.atMs || nowMs,\n detail: decidedBy?.evidence ?? null,\n })\n }\n report.optOuts += 1\n break\n }\n case 'replied': {\n if (decidingMessage) {\n await fileInbound(context, enrollment, decidingMessage)\n try {\n await fileOutreachTask(deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: `reply:${messageIdOf(decidingMessage) ?? decidingMessage.id}`,\n title: `Reply from ${enrollment.contactName || enrollment.email}`,\n notes: crmInboundExcerpt(decidingMessage.textBody ?? decidingMessage.snippet ?? '', decidingMessage.htmlBody ?? '').slice(0, 500),\n kind: 'email',\n dueAtMs: nowMs,\n assigneeUid: context.mailbox.connectedByUid,\n })\n } catch (error) {\n console.error('[outreach] the reply task could not be filed', error)\n }\n }\n // A lead somebody wrote back to is being worked (AGL-3234).\n if (enrollment.target === 'lead' && enrollment.leadId) {\n await markOutreachLeadWorking(firestore, { orgId: context.orgId, leadId: enrollment.leadId })\n }\n context.delta.replies += 1\n event = { type: 'reply', atMs: decidedBy?.atMs || nowMs, detail: decidedBy?.evidence ?? null }\n report.replies += 1\n // Once per enrollment (AGL-3254): a reply moves the status to\n // `replied`, and nothing is synced for it again.\n await creditOutreachReply(deps, { enrollment, atMs: event.atMs })\n break\n }\n case 'bounced': {\n const diagnostic = scrubbedOutreachDiagnostic(decidedBy?.bounce?.diagnostic ?? decidedBy?.bounce?.status ?? null, enrollment.email)\n await deps.suppressBouncedEmail({ email: enrollment.email, hostId: enrollment.hostId })\n await addOutreachDoNotContact(firestore, {\n orgId: context.orgId,\n email: enrollment.email,\n reason: 'hard_bounce',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n let detail = diagnostic\n // A gateway block is the domain's verdict on the sender (AGL-3244):\n // the domain is filed beside the address, and the enrollment says so.\n // Never a public mailbox provider's domain — Gmail refusing one\n // message on policy is not a reason to stop writing to Gmail.\n const domain = outreachEmailDomain(enrollment.email)\n const gateway = isOutreachGatewayBlock(decidedBy?.bounce ?? null)\n if (domain && !isPublicMailboxDomain(domain) && gateway) {\n await addOutreachDoNotContactDomain(firestore, {\n orgId: context.orgId,\n domain,\n reason: 'gateway_block',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n detail = outreachGatewayBlockDetail(domain, diagnostic)\n report.gatewayBlocks += 1\n }\n // The ledger learns the refusal (AGL-3326), whichever domain it was\n // at: a public provider refusing on policy is still that provider's\n // verdict on the sender, even though its domain is never listed.\n if (gateway) {\n await recordOutreachGatewayBlock(firestore, {\n orgId: context.orgId,\n email: enrollment.email,\n remoteMta: decidedBy?.bounce?.remoteMta ?? null,\n sendingDomain: outreachMailboxSendingDomain(context.mailbox),\n diagnostic,\n resolveMx: deps.resolveMx,\n nowMs,\n })\n }\n const bouncedAtMs = decidedBy?.atMs || nowMs\n // The record the person is says so too (AGL-3245): the verdict on the\n // lead and the contact, and the bounce on the send's own timeline\n // entry, so the record reads Sent, then Bounced.\n await deps.stampRecordEmailState({\n orgId: context.orgId,\n email: enrollment.email,\n state: {\n status: gateway ? 'blocked' : 'bounced',\n atMs: bouncedAtMs,\n source: 'sequence',\n detail: diagnostic,\n enrollmentId: enrollment.id,\n },\n })\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'bounced',\n atMs: bouncedAtMs,\n detail: diagnostic,\n })\n event = { type: 'bounce', atMs: bouncedAtMs, detail }\n report.bounces += 1\n break\n }\n case 'postpone': {\n const sequence = await sequenceOf(context, enrollment.sequenceId)\n postponedTo = postponeOutreachForAutoReply({\n nextDueAtMs: enrollment.nextDueAtMs,\n receivedAtMs: decidedBy?.atMs || nowMs,\n timeZone: context.mailbox.timezone,\n window: effectiveOutreachWindow(sequence?.settings.window, context.mailbox.window),\n random: deps.random,\n })\n break\n }\n default:\n break\n }\n\n const outcome = await applyOutreachEvent(firestore, {\n orgId: context.orgId,\n enrollmentId: enrollment.id,\n event,\n extra: (current) => {\n const extra: Record<string, unknown> = {\n syncedMessageIds: [...(current.syncedMessageIds ?? []), ...freshIds].slice(-ENROLLMENT_HANDLED_MAX),\n }\n const waiting = current.status === 'active' || current.status === 'paused'\n if (postponedTo !== null && waiting && current.nextDueAtMs !== null && postponedTo > current.nextDueAtMs) {\n extra['nextDueAtMs'] = postponedTo\n }\n return extra\n },\n nowMs,\n })\n if (postponedTo !== null && outcome.enrollment?.nextDueAtMs === postponedTo) report.postponed += 1\n // The person's record says the sequence ended (AGL-3274), once, under the\n // reply that ended it — after the transaction moved the status, so a run\n // that reads the same reply again finds nothing to file. An opt-out's\n // line is `recordOutreachOptOut`'s, beside the lists it writes.\n if (event?.type === 'reply' && outcome.changed) {\n const sequence = await sequenceOf(context, enrollment.sequenceId)\n const entry = outreachStoppedEntry({\n enrollmentId: enrollment.id,\n sequenceName: sequence?.name ?? '',\n reason: 'replied',\n })\n await fileOutreachNote(deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: event.atMs,\n })\n }\n // The run's own view of the enrollment moves with it, so a second message\n // of it in this run is judged against what this one did.\n if (outcome.enrollment) Object.assign(enrollment, outcome.enrollment)\n}\n"],"names":["normalizeContactEmail","isPublicMailboxDomain","crmInboundExcerpt","emailAddressOf","outreachMessageIdHeader","readOutreachDeliveryReport","outreachEmailDomain","outreachStoppedEntry","isOutreachGatewayBlock","outreachGatewayBlockDetail","effectiveOutreachWindow","postponeOutreachForAutoReply","decideOutreachThread","outreachHeader","outreachThreadMessageFromGmail","mailboxRef","OUTREACH_COLLECTIONS","addOutreachDoNotContact","addOutreachDoNotContactDomain","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","creditOutreachReply","applyOutreachEvent","recordOutreachOptOut","creditOutreachGatewayDeliveries","recordOutreachGatewayBlock","outreachMailboxSendingDomain","applyOutreachMailboxHealth","emptyOutreachHealthDelta","noteOutreachMailboxReconnectRequired","markOutreachLeadWorking","fileOutreachEmail","fileOutreachNote","fileOutreachTask","markOutreachEmailDelivery","outreachUnsubscribeMailbox","OUTREACH_SYNC_JOB_ID","OUTREACH_SYNC_LOOKBACK_MS","OUTREACH_SYNC_OVERLAP_MS","OUTREACH_SYNC_WATCH_MS","SEARCH_PAGES","MAILBOX_HANDLED_MAX","ENROLLMENT_HANDLED_MAX","FROM_CHUNK","runOutreachSyncJob","deps","context","firestore","report","mailboxes","threads","replies","optOuts","bounces","gatewayBlocks","deliveries","postponed","paused","all","collectionGroup","get","byOrg","Map","doc","docs","orgId","ref","parent","id","mailbox","data","status","set","now","deadlineMs","orgSnapshot","collection","org","exists","orgRefusal","syncMailbox","nowMs","error","mailboxId","errorCode","code","console","search","client","q","found","pageToken","page","list","listMessages","maxResults","includeSpamTrash","push","messages","filter","message","nextPageToken","input","opened","openMailbox","ok","reason","unsubscribe","email","selfAddresses","sendAs","sendAsOptions","map","option","Boolean","unsubscribeAddress","address","sequences","delta","watched","where","byThread","byAddress","watchedEnrollments","enrollment","known","threadId","gmailThreadIds","lastSentAtMs","sinceMs","Math","max","Number","sync","throughMs","after","floor","handled","Set","handledMessageIds","newlyHandled","markHandled","has","add","fresh","stub","inbound","thread","getFullThread","applyMessages","getFullMessage","delivery","kind","failedAddresses","metadata","getMessage","metadataHeaders","from","headers","find","header","name","toLowerCase","value","includes","source","hostIds","hostId","detail","addresses","keys","start","length","chunk","slice","join","enrollments","sendingDomain","resolveMx","update","sequenceOf","sequenceId","snapshot","undefined","messageIdOf","scrubbedOutreachDiagnostic","diagnostic","pattern","RegExp","replace","fileInbound","messageId","link","direction","subject","to","inReplyTo","body","textBody","snippet","htmlBody","atMs","internalDateMs","byUid","outcome","decision","recipient","syncedMessageIds","threadSubject","freshIds","classifications","classification","decidedBy","decidingMessage","lastSentMessageId","messageIds","aboutRecipient","bounce","recipients","complaint","complaintAtMs","event","postponedTo","evidence","startsWith","state","dedupeKey","title","contactName","notes","dueAtMs","assigneeUid","connectedByUid","target","leadId","type","suppressBouncedEmail","enrollmentId","domain","gateway","remoteMta","bouncedAtMs","stampRecordEmailState","sequence","nextDueAtMs","receivedAtMs","timeZone","timezone","window","settings","random","extra","current","waiting","changed","entry","sequenceName","Object","assign"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,qBAAqB,QAAQ,kCAAiC;AACvE,SAASC,qBAAqB,QAAQ,6BAA4B;AAClE,SAASC,iBAAiB,EAAEC,cAAc,QAAQ,qCAAoC;AACtF,SAASC,uBAAuB,QAAQ,uBAAmB;AAC3D,SAASC,0BAA0B,QAAQ,+BAA2B;AACtE,SAASC,mBAAmB,QAAQ,qCAAiC;AACrE,SAASC,oBAAoB,QAAQ,mCAA+B;AAEpE,SAASC,sBAAsB,EAAEC,0BAA0B,QAAQ,6BAAyB;AAC5F,SAASC,uBAAuB,EAAEC,4BAA4B,QAAQ,wBAAoB;AAC1F,SACEC,oBAAoB,QAGf,qCAAiC;AACxC,SAASC,cAAc,EAAEC,8BAA8B,QAAoC,8BAA0B;AACrH,SAASC,UAAU,QAAQ,sCAAkC;AAC7D,SACEC,oBAAoB,QAIf,6BAAyB;AAChC,SAASC,uBAAuB,EAAEC,6BAA6B,QAAQ,qCAAiC;AACxG,SACEC,sBAAsB,EACtBC,qBAAqB,EACrBC,4BAA4B,EAC5BC,yBAAyB,EACzBC,0BAA0B,QACrB,iCAA6B;AAEpC,SAASC,mBAAmB,EAAEC,mBAAmB,QAAQ,+BAA2B;AACpF,SAASC,mBAAmB,QAAQ,uBAAmB;AACvD,SAASC,kBAAkB,EAAEC,oBAAoB,QAAQ,yBAAqB;AAC9E,SAASC,+BAA+B,EAAEC,0BAA0B,QAAQ,sBAAkB;AAC9F,SAASC,4BAA4B,QAAQ,mCAA+B;AAC5E,SACEC,0BAA0B,EAC1BC,wBAAwB,QAEnB,4BAAwB;AAC/B,SAASC,oCAAoC,QAAQ,uBAAmB;AAExE,SAASC,uBAAuB,QAAQ,oBAAgB;AACxD,SAASC,iBAAiB,EAAEC,gBAAgB,EAAEC,gBAAgB,EAAEC,yBAAyB,QAAQ,gBAAY;AAC7G,SAASC,0BAA0B,QAAQ,wBAAoB;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4CA+C4C,GAE5C,SAASC,oBAAoB,QAAQ,+BAA2B;AAEhE,mFAAmF,GACnF,OAAO,MAAMC,4BAA4B,IAAI,KAAK,KAAK,MAAM;AAE7D,iFAAiF,GACjF,OAAO,MAAMC,2BAA2B,IAAI,KAAK,MAAM;AAEvD,wEAAwE,GACxE,OAAO,MAAMC,yBAAyB,KAAK,KAAK,KAAK,MAAM;AAE3D,8CAA8C,GAC9C,MAAMC,eAAe;AACrB,kEAAkE,GAClE,MAAMC,sBAAsB;AAC5B,MAAMC,yBAAyB;AAC/B,qCAAqC,GACrC,MAAMC,aAAa;AAiCnB,kDAAkD,GAClD,OAAO,eAAeC,mBACpBC,IAAyB,EACzBC,OAA8C;IAE9C,MAAMC,YAAYF,KAAKE,SAAS;IAChC,MAAMC,SAA6B;QACjCC,WAAW;QACXC,SAAS;QACTC,SAAS;QACTC,SAAS;QACTC,SAAS;QACTC,eAAe;QACfC,YAAY;QACZC,WAAW;QACXC,QAAQ;IACV;IACA,MAAMC,MAAM,MAAMX,UAAUY,eAAe,CAAChD,qBAAqBsC,SAAS,EAAEW,GAAG;IAC/E,MAAMC,QAAQ,IAAIC;IAClB,KAAK,MAAMC,OAAOL,IAAIM,IAAI,CAAE;YAIJH;YAHRE;QAAd,MAAME,SAAQF,yBAAAA,IAAIG,GAAG,CAACC,MAAM,CAACA,MAAM,qBAArBJ,uBAAuBK,EAAE;QACvC,MAAMC,UAAUpD,0BAA0B8C,IAAIK,EAAE,EAAEL,IAAIO,IAAI;QAC1D,IAAI,CAACL,SAAS,CAACI,WAAYA,QAAQE,MAAM,KAAK,eAAeF,QAAQE,MAAM,KAAK,UAAW;QAC3FV,MAAMW,GAAG,CAACP,OAAO;gBAAKJ,aAAAA,MAAMD,GAAG,CAACK,kBAAVJ,aAAoB,EAAE;YAAGQ;SAAQ;IACzD;IACA,KAAK,MAAM,CAACJ,OAAOhB,UAAU,IAAIY,MAAO;QACtC,IAAIhB,KAAK4B,GAAG,MAAM3B,QAAQ4B,UAAU,EAAE;QACtC,MAAMC,cAAc,MAAM5B,UAAU6B,UAAU,CAAC,QAAQb,GAAG,CAACE,OAAOL,GAAG;QACrE,MAAMiB,MAAOF,YAAYG,MAAM,GAAGH,YAAYL,IAAI,KAAK;QACvD,IAAI,CAACO,OAAQ,MAAMhC,KAAKkC,UAAU,CAACd,OAAOY,MAAO;QACjD,KAAK,MAAMR,WAAWpB,UAAW;YAC/B,IAAIJ,KAAK4B,GAAG,MAAM3B,QAAQ4B,UAAU,EAAE;YACtC,IAAI;gBACF,MAAMM,YAAYnC,MAAME,WAAW;oBAAEkB;oBAAOI;oBAASrB;oBAAQiC,OAAOnC,QAAQmC,KAAK;gBAAC;gBAClFjC,OAAOC,SAAS,IAAI;YACtB,EAAE,OAAOiC,OAAO;gBACd,sEAAsE;gBACtE,gDAAgD;gBAChD,IAAI9D,oBAAoB8D,QAAQ;oBAC9B,MAAMrD,qCAAqCgB,MAAM;wBAC/CoB;wBACAkB,WAAWd,QAAQD,EAAE;wBACrBgB,WAAWF,iBAAiB/D,sBAAsB+D,MAAMG,IAAI,GAAG;wBAC/DJ,OAAOnC,QAAQmC,KAAK;oBACtB;gBACF;gBACAK,QAAQJ,KAAK,CAAC,uCAAuCA;YACvD;QACF;IACF;IACA,OAAOlC;AACT;AAEA,iDAAiD,GACjD,eAAeuC,OACbC,MAAmB,EACnBC,CAAS;IAET,MAAMC,QAAiD,EAAE;IACzD,IAAIC,YAA2B;IAC/B,IAAK,IAAIC,OAAO,GAAGA,OAAOpD,cAAcoD,QAAQ,EAAG;QACjD,MAAMC,OAAO,MAAML,OAAOM,YAAY,CAAC;YAAEL;YAAGM,YAAY;YAAKJ;YAAWK,kBAAkB;QAAK;QAC/FN,MAAMO,IAAI,IAAIJ,KAAKK,QAAQ,CAACC,MAAM,CAAC,CAACC,UAAYA,QAAQhC,EAAE;QAC1DuB,YAAYE,KAAKQ,aAAa;QAC9B,IAAI,CAACV,WAAW;IAClB;IACA,OAAOD;AACT;AAEA,eAAeV,YACbnC,IAAyB,EACzBE,SAAoB,EACpBuD,KAA6F;QAyBrFjC;QA0BwBA,eAIRA,gBAyFIA;IA9I5B,MAAM,EAAEJ,KAAK,EAAEI,OAAO,EAAErB,MAAM,EAAEiC,KAAK,EAAE,GAAGqB;IAC1C,MAAMC,SAAS,MAAM1D,KAAK2D,WAAW,CAACnC,QAAQD,EAAE;IAChD,IAAImC,OAAOE,EAAE,KAAK,OAAO;QACvB,IAAIF,OAAOG,MAAM,KAAK,kBAAkB;YACtC,MAAM7E,qCAAqCgB,MAAM;gBAC/CoB;gBACAkB,WAAWd,QAAQD,EAAE;gBACrBgB,WAAWmB,OAAOG,MAAM;gBACxBzB;YACF;QACF;QACA;IACF;IACA,MAAM0B,cAAcxE,2BAA2BkC,QAAQuC,KAAK;IAC5D,MAAM9D,UAAuB;QAC3BD;QACAE;QACAkB;QACAI;QACAmB,QAAQe,OAAOf,MAAM;QACrBqB,eAAe;YACbxC,QAAQuC,KAAK;YACbvC,QAAQyC,MAAM;eACX,EAACzC,yBAAAA,QAAQ0C,aAAa,YAArB1C,yBAAyB,EAAE,EAAE2C,GAAG,CAAC,CAACC,SAAWA,OAAOL,KAAK;SAC9D,CAACT,MAAM,CAACe;QACTC,kBAAkB,UAAER,+BAAAA,YAAaS,OAAO,mBAAI;QAC5CC,WAAW,IAAIvD;QACfwD,OAAO1F;QACPoB;QACAiC;IACF;IAEA,wEAAwE;IACxE,MAAMsC,UAAU,MAAMxG,sBAAsBgC,WAAWkB,OAAO,eAC3DuD,KAAK,CAAC,aAAa,MAAMnD,QAAQD,EAAE,EACnCoD,KAAK,CAAC,gBAAgB,MAAMvC,QAAQ1C,wBACpCqB,GAAG;IACN,MAAM6D,WAAW,IAAI3D;IACrB,MAAM4D,YAAY,IAAI5D;IACtB,MAAM6D,qBAA2C,EAAE;IACnD,KAAK,MAAM5D,OAAOwD,QAAQvD,IAAI,CAAE;YAMf4D,0BAAiCC;QALhD,MAAMD,aAAa5G,6BAA6B+C,IAAIK,EAAE,EAAEL,IAAIO,IAAI;QAChE,IAAI,CAACsD,YAAY;QACjBD,mBAAmB1B,IAAI,CAAC2B;QACxB,KAAK,MAAME,YAAYF,WAAWG,cAAc,CAAEN,SAASjD,GAAG,CAACsD,UAAUF;QACzE,MAAMC,QAAQH,UAAU9D,GAAG,CAACgE,WAAWhB,KAAK;QAC5C,IAAI,CAACiB,SAAS,EAACD,2BAAAA,WAAWI,YAAY,YAAvBJ,2BAA2B,OAAMC,sBAAAA,MAAMG,YAAY,YAAlBH,sBAAsB,IAAIH,UAAUlD,GAAG,CAACoD,WAAWhB,KAAK,EAAEgB;IAC5G;IAEA,MAAMK,UAAUC,KAAKC,GAAG,CAACC,QAAO/D,gBAAAA,QAAQgE,IAAI,qBAAZhE,cAAciE,SAAS,KAAK,GAAGrD,QAAQ5C,6BAA6BC;IACpG,MAAMiG,QAAQ,CAAC,MAAM,EAAEL,KAAKM,KAAK,CAACP,UAAU,OAAO;IACnD,2EAA2E;IAC3E,qEAAqE;IACrE,MAAMQ,UAAU,IAAIC,cAAIrE,iBAAAA,QAAQgE,IAAI,qBAAZhE,eAAcsE,iBAAiB,oBAAI,EAAE;IAC7D,MAAMC,eAAyB,EAAE;IACjC,MAAMC,cAAc,CAACzE;QACnB,IAAIqE,QAAQK,GAAG,CAAC1E,KAAK;QACrBqE,QAAQM,GAAG,CAAC3E;QACZwE,aAAa3C,IAAI,CAAC7B;IACpB;IACA,MAAM4E,QAAQ,CAACC,OAA2C,CAACR,QAAQK,GAAG,CAACG,KAAK7E,EAAE,KAAK,CAACqD,SAASqB,GAAG,CAACG,KAAKnB,QAAQ;IAE9G,sDAAsD;IACtD,MAAMoB,UAAU,MAAM3D,OAAOzC,QAAQ0C,MAAM,EAAE,GAAG+C,MAAM,SAAS,CAAC;IAChE,KAAK,MAAMT,YAAY,IAAIY,IAAIQ,QAAQlC,GAAG,CAAC,CAACiC,OAASA,KAAKnB,QAAQ,GAAI;QACpE,MAAMF,aAAaH,SAAS7D,GAAG,CAACkE;QAChC,IAAI,CAACF,YAAY;QACjB,MAAMuB,SAAS,MAAMrG,QAAQ0C,MAAM,CAAC4D,aAAa,CAACtB;QAClD9E,OAAOE,OAAO,IAAI;QAClB,MAAMmG,cAAcvG,SAAS8E,YAAYuB,OAAOjD,QAAQ,CAACc,GAAG,CAACvG;IAC/D;IAEA,0EAA0E;IAC1E,KAAK,MAAMwI,QAAQ,CAAA,MAAM1D,OAAOzC,QAAQ0C,MAAM,EAAE,GAAG+C,MAAM,mCAAmC,CAAC,CAAA,EAAG;QAC9F,IAAI,CAACS,MAAMC,OAAO;QAClB,MAAM7C,UAAU3F,+BAA+B,MAAMqC,QAAQ0C,MAAM,CAAC8D,cAAc,CAACL,KAAK7E,EAAE;QAC1F,MAAMmF,WAAWvJ,2BAA2BoG;QAC5C,IAAImD,CAAAA,4BAAAA,SAAUC,IAAI,MAAK,QAAQ;YAC7B,KAAK,MAAMpC,WAAWmC,SAASE,eAAe,CAAE;gBAC9C,MAAM7B,aAAaF,UAAU9D,GAAG,CAACwD;gBACjC,IAAIQ,YAAY,MAAMyB,cAAcvG,SAAS8E,YAAY;oBAACxB;iBAAQ;YACpE;QACF;QACAyC,YAAYI,KAAK7E,EAAE;IACrB;IAEA,wEAAwE;IACxE,IAAIuC,aAAa;QACf,KAAK,MAAMsC,QAAQ,CAAA,MAAM1D,OAAOzC,QAAQ0C,MAAM,EAAE,GAAG+C,MAAM,IAAI,EAAE5B,YAAYS,OAAO,EAAE,CAAA,EAAG;;gBAGzDsC;YAF5B,IAAIjB,QAAQK,GAAG,CAACG,KAAK7E,EAAE,GAAG;YAC1B,MAAMsF,WAAW,MAAM5G,QAAQ0C,MAAM,CAACmE,UAAU,CAACV,KAAK7E,EAAE,EAAE;gBAAEwF,iBAAiB;oBAAC;iBAAO;YAAC;YACtF,MAAMC,OAAO/J,yBAAe4J,yBAAAA,SAASI,OAAO,CAACC,IAAI,CAAC,CAACC,SAAWA,OAAOC,IAAI,CAACC,WAAW,OAAO,4BAAhER,uBAAyES,KAAK,oBAAI;YAC9G,MAAM/C,UAAUzH,sBAAsBkK;YACtC,IAAIzC,WAAW,CAACtE,QAAQ+D,aAAa,CAACuD,QAAQ,CAAChD,UAAU;oBACpCM;gBAAnB,MAAME,cAAaF,iBAAAA,UAAU9D,GAAG,CAACwD,oBAAdM,iBAA0B;gBAC7C,MAAMnG,qBAAqBsB,MAAM;oBAC/BoB;oBACA2C,OAAOQ;oBACPiD,QAAQ;oBACRzC;oBACA0C,SAAS1C,aAAa;wBAACA,WAAW2C,MAAM;qBAAC,GAAG,EAAE;oBAC9CC,QAAQ;oBACRvF;gBACF;gBACAjC,OAAOI,OAAO,IAAI;YACpB;YACAyF,YAAYI,KAAK7E,EAAE;QACrB;IACF;IAEA,4EAA4E;IAC5E,MAAMqG,YAAY;WAAI/C,UAAUgD,IAAI;KAAG;IACvC,IAAK,IAAIC,QAAQ,GAAGA,QAAQF,UAAUG,MAAM,EAAED,SAAShI,WAAY;QACjE,MAAMkI,QAAQJ,UAAUK,KAAK,CAACH,OAAOA,QAAQhI;QAC7C,KAAK,MAAMsG,QAAQ,CAAA,MAAM1D,OAAOzC,QAAQ0C,MAAM,EAAE,GAAG+C,MAAM,OAAO,EAAEsC,MAAME,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA,EAAG;gBAGvDpL;YAFjC,IAAI,CAACqJ,MAAMC,OAAO;YAClB,MAAM7C,UAAU3F,+BAA+B,MAAMqC,QAAQ0C,MAAM,CAAC8D,cAAc,CAACL,KAAK7E,EAAE;YAC1F,MAAMwD,aAAaF,UAAU9D,GAAG,EAACjE,yBAAAA,sBAAsBG,eAAesG,QAAQyD,IAAI,cAAjDlK,yBAAuD;YACxF,IAAIiI,YAAY,MAAMyB,cAAcvG,SAAS8E,YAAY;gBAACxB;aAAQ;YAClEyC,YAAYI,KAAK7E,EAAE;QACrB;IACF;IAEA,oEAAoE;IACpE,kEAAkE;IAClE,IAAI;QACFpB,OAAOO,UAAU,IAAI,MAAM/B,gCAAgCuB,WAAW;YACpEkB;YACA+G,aAAarD;YACbsD,eAAevJ,6BAA6B2C;YAC5C6G,WAAWrI,KAAKqI,SAAS;YACzBjG;QACF;IACF,EAAE,OAAOC,OAAO;QACdI,QAAQJ,KAAK,CAAC,6DAA6DA;IAC7E;IAEA,MAAMzB,SAAS,MAAM9B,2BAA2BkB,MAAM;QAAEoB;QAAOkB,WAAWd,QAAQD,EAAE;QAAEkD,OAAOxE,QAAQwE,KAAK;IAAC;IAC3G,IAAI7D,QAAQT,OAAOS,MAAM,IAAI;IAC7B,MAAM/C,WAAWqC,WAAWkB,OAAOI,QAAQD,EAAE,EAAE+G,MAAM,CAAC;QACpD9C,MAAM;YACJC,WAAWrD;YACX0D,mBAAmB;6BAAKtE,iBAAAA,QAAQgE,IAAI,qBAAZhE,eAAcsE,iBAAiB,oBAAI,EAAE;mBAAMC;aAAa,CAACkC,KAAK,CAAC,CAACrI;QAC1F;IACF;AACF;AAEA,gEAAgE,GAChE,eAAe2I,WAAWtI,OAAoB,EAAEuI,UAAkB;QAKzDvI;IAJP,IAAI,CAACA,QAAQuE,SAAS,CAACyB,GAAG,CAACuC,aAAa;QACtC,MAAMC,WAAW,MAAMvK,sBAAsB+B,QAAQC,SAAS,EAAED,QAAQmB,KAAK,EAAE,aAAaF,GAAG,CAACsH,YAAYzH,GAAG;QAC/Gd,QAAQuE,SAAS,CAAC7C,GAAG,CAAC6G,YAAYnK,2BAA2BmK,YAAYC,SAASxG,MAAM,GAAGwG,SAAShH,IAAI,KAAKiH;IAC/G;IACA,QAAOzI,yBAAAA,QAAQuE,SAAS,CAACzD,GAAG,CAACyH,uBAAtBvI,yBAAqC;AAC9C;AAEA,6EAA6E,GAC7E,MAAM0I,cAAc,CAACpF,UAAmCrG,wBAAwBS,eAAe4F,SAAS;AAExG,wEAAwE,GACxE,OAAO,SAASqF,2BAA2BC,UAAyB,EAAE9E,KAAa;IACjF,IAAI,CAAC8E,YAAY,OAAO;IACxB,MAAMC,UAAU,IAAIC,OAAOhF,MAAMiF,OAAO,CAAC,uBAAuB,SAAS;IACzE,OAAOH,WAAWG,OAAO,CAACF,SAAS;AACrC;AAEA,wDAAwD,GACxD,eAAeG,YAAYhJ,OAAoB,EAAE8E,UAA8B,EAAExB,OAA8B;QASrGtG,iBAIkBsG,MAAAA,mBAA2CA;IAZrE,MAAM2F,YAAYP,YAAYpF;IAC9B,IAAI,CAAC2F,WAAW;IAChB,MAAMhK,kBAAkBe,QAAQD,IAAI,EAAE;QACpCoB,OAAOnB,QAAQmB,KAAK;QACpBsG,QAAQ3C,WAAW2C,MAAM;QACzByB,MAAMlL,uBAAuB8G;QAC7BqE,WAAW;QACXC,SAAS9F,QAAQ8F,OAAO;QACxBrC,IAAI,GAAE/J,kBAAAA,eAAesG,QAAQyD,IAAI,aAA3B/J,kBAAgCsG,QAAQyD,IAAI;QAClDsC,IAAIrJ,QAAQuB,OAAO,CAACyC,MAAM,IAAIhE,QAAQuB,OAAO,CAACuC,KAAK;QACnDmF;QACAK,WAAWrM,wBAAwBS,eAAe4F,SAAS;QAC3DiG,MAAMxM,mBAAkBuG,QAAAA,oBAAAA,QAAQkG,QAAQ,YAAhBlG,oBAAoBA,QAAQmG,OAAO,YAAnCnG,OAAuC,KAAIA,oBAAAA,QAAQoG,QAAQ,YAAhBpG,oBAAoB;QACvFqG,MAAMrG,QAAQsG,cAAc;QAC5BC,OAAO;IACT;AACF;AAEA;;;CAGC,GACD,eAAetD,cACbvG,OAAoB,EACpB8E,UAA8B,EAC9B1B,QAA0C;QAMrB0B,8BAQe1B,gBACV0B;QAmLEgF;IAhM5B,MAAM,EAAE/J,IAAI,EAAEE,SAAS,EAAEC,MAAM,EAAEiC,KAAK,EAAE,GAAGnC;IAC3C,MAAM+J,WAAmCtM,qBAAqB;QAC5D2F;QACA4G,WAAWlF,WAAWhB,KAAK;QAC3B+B,iBAAiB,GAAEf,+BAAAA,WAAWmF,gBAAgB,YAA3BnF,+BAA+B,EAAE;QACpDf,eAAe/D,QAAQ+D,aAAa;QACpCM,oBAAoBrE,QAAQqE,kBAAkB;QAC9C6F,eAAepF,WAAWoF,aAAa;IACzC;IACA,MAAMC,WAAWJ,SAASK,eAAe,CAAClG,GAAG,CAAC,CAACmG,iBAAmBA,eAAepB,SAAS,EAAE5F,MAAM,CAACe;IACnG,IAAI,CAAC+F,SAASrC,MAAM,EAAE;IACtB,MAAMwC,YAAkDP,SAASO,SAAS;IAC1E,MAAMC,kBAAkBD,aAAYlH,iBAAAA,SAAS6D,IAAI,CAAC,CAAC3D,UAAYA,QAAQhC,EAAE,KAAKgJ,UAAUrB,SAAS,aAA7D7F,iBAAkE,OAAO;IAC7G,MAAMoH,qBAAoB1F,0BAAAA,WAAW2F,UAAU,CAAC3F,WAAW2F,UAAU,CAAC3C,MAAM,GAAG,EAAE,YAAvDhD,0BAA2D;IAErF,KAAK,MAAMuF,kBAAkBN,SAASK,eAAe,CAAE;YAElDC;QADH,MAAMK,iBACJ,GAACL,yBAAAA,eAAeM,MAAM,qBAArBN,uBAAuBO,UAAU,CAAC9C,MAAM,KAAIuC,eAAeM,MAAM,CAACC,UAAU,CAACtD,QAAQ,CAACxC,WAAWhB,KAAK;QACzG,IAAIuG,eAAe3D,IAAI,KAAK,iBAAiBgE,gBAAgB;YAC3D1K,QAAQwE,KAAK,CAACjE,OAAO,CAAC4C,IAAI,CAAC;gBAAEwG,MAAMU,eAAeV,IAAI,IAAIxH;gBAAO8G,WAAWuB;YAAkB;QAChG;QACA,IAAIH,eAAeQ,SAAS,EAAE;gBACW7K;YAAvCA,QAAQwE,KAAK,CAACsG,aAAa,GAAG1F,KAAKC,GAAG,EAACrF,+BAAAA,QAAQwE,KAAK,CAACsG,aAAa,YAA3B9K,+BAA+B,GAAGqK,eAAeV,IAAI,IAAIxH;QAClG;IACF;IAEA,IAAI4I,QAAwC;IAC5C,IAAIC,cAA6B;IACjC,OAAQjB,SAASD,OAAO;QACtB,KAAK;YAAa;;oBAMNQ;gBALV,IAAIC,iBAAiB,MAAMvB,YAAYhJ,SAAS8E,YAAYyF;gBAC5D,MAAMM,YAAYP,CAAAA,6BAAAA,UAAWO,SAAS,MAAK;gBAC3C,MAAMpM,qBAAqBsB,MAAM;oBAC/BoB,OAAOnB,QAAQmB,KAAK;oBACpB2C,OAAOgB,WAAWhB,KAAK;oBACvByD,QAAQ+C,CAAAA,8BAAAA,sBAAAA,UAAWW,QAAQ,qBAAnBX,oBAAqBY,UAAU,CAAC,UAAS,gBAAgB;oBACjEpG;oBACA0C,SAAS;wBAAC1C,WAAW2C,MAAM;qBAAC;oBAC5BC,MAAM,UAAE4C,6BAAAA,UAAWW,QAAQ,mBAAI;oBAC/B9I;oBACA0I;gBACF;gBACA,gEAAgE;gBAChE,iEAAiE;gBACjE,IAAIA,WAAW;;oBACb,MAAMzL,0BAA0BW,MAAM;wBACpCoB,OAAOnB,QAAQmB,KAAK;wBACpB8H,WAAWuB;wBACXW,OAAO;wBACPxB,MAAMW,CAAAA,6BAAAA,UAAWX,IAAI,KAAIxH;wBACzBuF,MAAM,WAAE4C,6BAAAA,UAAWW,QAAQ,oBAAI;oBACjC;gBACF;gBACA/K,OAAOI,OAAO,IAAI;gBAClB;YACF;QACA,KAAK;YAAW;;gBACd,IAAIiK,iBAAiB;oBACnB,MAAMvB,YAAYhJ,SAAS8E,YAAYyF;oBACvC,IAAI;4BAKoB7B,cAEK6B,OAAAA,2BAA2DA;wBANtF,MAAMpL,iBAAiBY,MAAM;4BAC3BoB,OAAOnB,QAAQmB,KAAK;4BACpBsG,QAAQ3C,WAAW2C,MAAM;4BACzByB,MAAMlL,uBAAuB8G;4BAC7BsG,WAAW,CAAC,MAAM,GAAE1C,eAAAA,YAAY6B,4BAAZ7B,eAAgC6B,gBAAgBjJ,EAAE,EAAE;4BACxE+J,OAAO,CAAC,WAAW,EAAEvG,WAAWwG,WAAW,IAAIxG,WAAWhB,KAAK,EAAE;4BACjEyH,OAAOxO,mBAAkBwN,SAAAA,4BAAAA,gBAAgBf,QAAQ,YAAxBe,4BAA4BA,gBAAgBd,OAAO,YAAnDc,QAAuD,KAAIA,4BAAAA,gBAAgBb,QAAQ,YAAxBa,4BAA4B,IAAIvC,KAAK,CAAC,GAAG;4BAC7HtB,MAAM;4BACN8E,SAASrJ;4BACTsJ,aAAazL,QAAQuB,OAAO,CAACmK,cAAc;wBAC7C;oBACF,EAAE,OAAOtJ,OAAO;wBACdI,QAAQJ,KAAK,CAAC,gDAAgDA;oBAChE;gBACF;gBACA,4DAA4D;gBAC5D,IAAI0C,WAAW6G,MAAM,KAAK,UAAU7G,WAAW8G,MAAM,EAAE;oBACrD,MAAM5M,wBAAwBiB,WAAW;wBAAEkB,OAAOnB,QAAQmB,KAAK;wBAAEyK,QAAQ9G,WAAW8G,MAAM;oBAAC;gBAC7F;gBACA5L,QAAQwE,KAAK,CAACnE,OAAO,IAAI;gBACzB0K,QAAQ;oBAAEc,MAAM;oBAASlC,MAAMW,CAAAA,6BAAAA,UAAWX,IAAI,KAAIxH;oBAAOuF,MAAM,WAAE4C,6BAAAA,UAAWW,QAAQ,oBAAI;gBAAK;gBAC7F/K,OAAOG,OAAO,IAAI;gBAClB,8DAA8D;gBAC9D,iDAAiD;gBACjD,MAAM9B,oBAAoBwB,MAAM;oBAAE+E;oBAAY6E,MAAMoB,MAAMpB,IAAI;gBAAC;gBAC/D;YACF;QACA,KAAK;YAAW;oBACgCW;oBAAAA,mBAAiCA;gBAA/E,MAAM1B,aAAaD,4BAA2B2B,iBAAAA,8BAAAA,oBAAAA,UAAWK,MAAM,qBAAjBL,kBAAmB1B,UAAU,oBAAI0B,8BAAAA,qBAAAA,UAAWK,MAAM,qBAAjBL,mBAAmB7I,MAAM,YAA1D6I,QAA8D,MAAMxF,WAAWhB,KAAK;gBAClI,MAAM/D,KAAK+L,oBAAoB,CAAC;oBAAEhI,OAAOgB,WAAWhB,KAAK;oBAAE2D,QAAQ3C,WAAW2C,MAAM;gBAAC;gBACrF,MAAM3J,wBAAwBmC,WAAW;oBACvCkB,OAAOnB,QAAQmB,KAAK;oBACpB2C,OAAOgB,WAAWhB,KAAK;oBACvBF,QAAQ;oBACR2D,QAAQ;oBACRpF;oBACA4J,cAAcjH,WAAWxD,EAAE;oBAC3BiH,YAAYzD,WAAWyD,UAAU;oBACjCb,QAAQkB;gBACV;gBACA,IAAIlB,SAASkB;gBACb,oEAAoE;gBACpE,sEAAsE;gBACtE,gEAAgE;gBAChE,8DAA8D;gBAC9D,MAAMoD,SAAS7O,oBAAoB2H,WAAWhB,KAAK;gBACnD,MAAMmI,UAAU5O,gCAAuBiN,6BAAAA,UAAWK,MAAM,oBAAI;gBAC5D,IAAIqB,UAAU,CAAClP,sBAAsBkP,WAAWC,SAAS;oBACvD,MAAMlO,8BAA8BkC,WAAW;wBAC7CkB,OAAOnB,QAAQmB,KAAK;wBACpB6K;wBACApI,QAAQ;wBACR2D,QAAQ;wBACRpF;wBACA4J,cAAcjH,WAAWxD,EAAE;wBAC3BiH,YAAYzD,WAAWyD,UAAU;wBACjCb,QAAQkB;oBACV;oBACAlB,SAASpK,2BAA2B0O,QAAQpD;oBAC5C1I,OAAOM,aAAa,IAAI;gBAC1B;gBACA,oEAAoE;gBACpE,oEAAoE;gBACpE,iEAAiE;gBACjE,IAAIyL,SAAS;;wBAIE3B;oBAHb,MAAM3L,2BAA2BsB,WAAW;wBAC1CkB,OAAOnB,QAAQmB,KAAK;wBACpB2C,OAAOgB,WAAWhB,KAAK;wBACvBoI,SAAS,WAAE5B,8BAAAA,qBAAAA,UAAWK,MAAM,qBAAjBL,mBAAmB4B,SAAS,oBAAI;wBAC3C/D,eAAevJ,6BAA6BoB,QAAQuB,OAAO;wBAC3DqH;wBACAR,WAAWrI,KAAKqI,SAAS;wBACzBjG;oBACF;gBACF;gBACA,MAAMgK,cAAc7B,CAAAA,6BAAAA,UAAWX,IAAI,KAAIxH;gBACvC,sEAAsE;gBACtE,kEAAkE;gBAClE,iDAAiD;gBACjD,MAAMpC,KAAKqM,qBAAqB,CAAC;oBAC/BjL,OAAOnB,QAAQmB,KAAK;oBACpB2C,OAAOgB,WAAWhB,KAAK;oBACvBqH,OAAO;wBACL1J,QAAQwK,UAAU,YAAY;wBAC9BtC,MAAMwC;wBACN5E,QAAQ;wBACRG,QAAQkB;wBACRmD,cAAcjH,WAAWxD,EAAE;oBAC7B;gBACF;gBACA,MAAMlC,0BAA0BW,MAAM;oBACpCoB,OAAOnB,QAAQmB,KAAK;oBACpB8H,WAAWuB;oBACXW,OAAO;oBACPxB,MAAMwC;oBACNzE,QAAQkB;gBACV;gBACAmC,QAAQ;oBAAEc,MAAM;oBAAUlC,MAAMwC;oBAAazE;gBAAO;gBACpDxH,OAAOK,OAAO,IAAI;gBAClB;YACF;QACA,KAAK;YAAY;gBACf,MAAM8L,WAAW,MAAM/D,WAAWtI,SAAS8E,WAAWyD,UAAU;gBAChEyC,cAAcxN,6BAA6B;oBACzC8O,aAAaxH,WAAWwH,WAAW;oBACnCC,cAAcjC,CAAAA,6BAAAA,UAAWX,IAAI,KAAIxH;oBACjCqK,UAAUxM,QAAQuB,OAAO,CAACkL,QAAQ;oBAClCC,QAAQnP,wBAAwB8O,4BAAAA,SAAUM,QAAQ,CAACD,MAAM,EAAE1M,QAAQuB,OAAO,CAACmL,MAAM;oBACjFE,QAAQ7M,KAAK6M,MAAM;gBACrB;gBACA;YACF;QACA;YACE;IACJ;IAEA,MAAM9C,UAAU,MAAMtL,mBAAmByB,WAAW;QAClDkB,OAAOnB,QAAQmB,KAAK;QACpB4K,cAAcjH,WAAWxD,EAAE;QAC3ByJ;QACA8B,OAAO,CAACC;gBAEmBA;YADzB,MAAMD,QAAiC;gBACrC5C,kBAAkB;wBAAK6C,4BAAAA,QAAQ7C,gBAAgB,YAAxB6C,4BAA4B,EAAE;uBAAM3C;iBAAS,CAACnC,KAAK,CAAC,CAACpI;YAC9E;YACA,MAAMmN,UAAUD,QAAQrL,MAAM,KAAK,YAAYqL,QAAQrL,MAAM,KAAK;YAClE,IAAIuJ,gBAAgB,QAAQ+B,WAAWD,QAAQR,WAAW,KAAK,QAAQtB,cAAc8B,QAAQR,WAAW,EAAE;gBACxGO,KAAK,CAAC,cAAc,GAAG7B;YACzB;YACA,OAAO6B;QACT;QACA1K;IACF;IACA,IAAI6I,gBAAgB,QAAQlB,EAAAA,sBAAAA,QAAQhF,UAAU,qBAAlBgF,oBAAoBwC,WAAW,MAAKtB,aAAa9K,OAAOQ,SAAS,IAAI;IACjG,0EAA0E;IAC1E,yEAAyE;IACzE,sEAAsE;IACtE,gEAAgE;IAChE,IAAIqK,CAAAA,yBAAAA,MAAOc,IAAI,MAAK,WAAW/B,QAAQkD,OAAO,EAAE;;QAC9C,MAAMX,WAAW,MAAM/D,WAAWtI,SAAS8E,WAAWyD,UAAU;QAChE,MAAM0E,QAAQ7P,qBAAqB;YACjC2O,cAAcjH,WAAWxD,EAAE;YAC3B4L,YAAY,WAAEb,4BAAAA,SAAUlF,IAAI,oBAAI;YAChCvD,QAAQ;QACV;QACA,MAAM1E,iBAAiBa,MAAM;YAC3BoB,OAAOnB,QAAQmB,KAAK;YACpBsG,QAAQ3C,WAAW2C,MAAM;YACzByB,MAAMlL,uBAAuB8G;YAC7BsG,WAAW6B,MAAM7B,SAAS;YAC1B7B,MAAM0D,MAAM1D,IAAI;YAChBI,MAAMoB,MAAMpB,IAAI;QAClB;IACF;IACA,0EAA0E;IAC1E,yDAAyD;IACzD,IAAIG,QAAQhF,UAAU,EAAEqI,OAAOC,MAAM,CAACtI,YAAYgF,QAAQhF,UAAU;AACtE"}
@@ -15,32 +15,41 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  /**
18
- * WHAT THE ORGANIZATION KNOWS ABOUT THE DOMAINS IT WRITES TO (AGL-3326).
18
+ * WHAT SEQUENCES KNOWS ABOUT THE DOMAINS IT WRITES TO (AGL-3326).
19
19
  *
20
- * Two documents, both server-written:
20
+ * Both halves live on the platform (AGL-3328), in `@aglyn/tenant-data-admin`'s
21
+ * `email-deliverability`, which the Resend path reads and writes too:
21
22
  *
22
- * - `outreachDomainIntel/{domain}` — the domain's MX records and the mail
23
- * gateway they name, looked up with the resolver the caller hands in and
24
- * trusted for {@link OUTREACH_DOMAIN_INTEL_TTL_MS}; beside them, what this
25
- * organization's mail met at the domain;
26
- * - `outreachGatewayStats/{gateway}` — the same three counts per gateway,
27
- * in total and by UTC day, which is the ledger the hold and the
28
- * Check-people chip read.
23
+ * - a recipient domain's MX — the platform-wide `mailDomains/{domain}`
24
+ * cache, looked up with the resolver the caller hands in and trusted for
25
+ * a week (a day for a domain that takes no mail). The first workspace to
26
+ * write to a domain warms it for every other;
27
+ * - the ledger — `orgs/{orgId}/mailGatewayLedger/{sendingDomain}~{gateway}`,
28
+ * keyed by the MAILBOX's sending domain, because a gateway refuses a
29
+ * sender: the Barracuda that refused `aglyn.io` refuses `aglyn.io`, not
30
+ * the organization's other mailbox on another domain. It stays under the
31
+ * organization because a mailbox's domain is not one organization's — two
32
+ * workspaces can each connect a `gmail.com` mailbox.
29
33
  *
30
34
  * A read that could not be made answers `null` for the addresses it
31
35
  * covered, which the gates refuse as "we couldn't check" — except that a
32
- * stale answer outranks no answer: a domain whose MX was known last week
33
- * and cannot be looked up today is read as it was, because a resolver
34
- * timing out says nothing about the domain. A domain that resolves to no
35
- * MX at all is `none`, and the gates block it.
36
+ * stale answer that the domain takes mail outranks no answer. A domain that
37
+ * resolves to no MX is `none`, and the gates block it.
36
38
  *
37
- * The ledger is written in a transaction over both documents so a run that
38
- * records a block and a run that credits a delivery cannot lose each
39
- * other's count, and the days map is pruned on every write so it never
40
- * grows past the window it serves.
39
+ * ## The two collections this module wrote first
40
+ *
41
+ * `outreachDomainIntel` and `outreachGatewayStats` are READ THROUGH, not
42
+ * migrated. The first held MX answers, which a lookup re-derives, so it is
43
+ * no longer read. The second held a per-gateway ledger with no sending
44
+ * domain; its days are summed into every sending domain's standing for the
45
+ * thirty days they can still hold a send — the conservative reading, since
46
+ * an organization with one mailbox domain loses nothing and one with two
47
+ * holds a little more. Nothing writes either any more; both are retired,
48
+ * with their rules, once the window has passed.
41
49
  */
50
+ import { type MailDomainIntel, type MailGatewayOutcome } from '@aglyn/shared-util-email';
42
51
  import type { OutreachGatewayStanding, OutreachMailGateway } from '../engine/mail-gateway';
43
- import type { OutreachDomainIntel, OutreachGatewayStats } from '../model/outreach.types';
52
+ import type { OutreachGatewayStats, OutreachMailbox } from '../model/outreach.types';
44
53
  type Firestore = FirebaseFirestore.Firestore;
45
54
  /** One MX record as Node's resolver answers it. */
46
55
  export interface OutreachMxRecord {
@@ -48,8 +57,8 @@ export interface OutreachMxRecord {
48
57
  priority: number;
49
58
  }
50
59
  /**
51
- * `dns.promises.resolveMx`, or a stand-in: the platform hands in Node's,
52
- * the specs hand in a table. Rejects the way Node does — `code`
60
+ * `dns.promises.resolveMx`, or a stand-in: the platform hands in its own
61
+ * resolver, the specs hand in a table. Rejects the way Node does — `code`
53
62
  * `ENOTFOUND` or `ENODATA` for a domain with no MX — and any other
54
63
  * rejection is a resolver that could not answer.
55
64
  */
@@ -57,51 +66,60 @@ export type OutreachResolveMx = (domain: string) => Promise<ReadonlyArray<Outrea
57
66
  /** What a domain-intel read needs beside the store. */
58
67
  export interface OutreachDomainIntelReadInput {
59
68
  resolveMx: OutreachResolveMx;
69
+ /**
70
+ * Whether a domain with no MX has an address record — the implicit MX
71
+ * RFC 5321 still delivers to. Omitted, a domain with no MX reads as none.
72
+ */
73
+ resolveAddress?: (domain: string) => Promise<boolean>;
60
74
  nowMs: number;
75
+ /**
76
+ * The domain the mailbox sends from (AGL-3328): the ledger is keyed by
77
+ * it. Omitted or `null`, no ledger is read and nothing is held.
78
+ */
79
+ sendingDomain?: string | null;
61
80
  }
62
- /** A stored domain-intel document in its model shape, or `null` for no document. */
63
- export declare function readStoredOutreachDomainIntel(id: string, data: Record<string, unknown> | undefined): OutreachDomainIntel | null;
64
- /** A stored gateway ledger in its model shape, or `null` for no document. */
65
- export declare function readStoredOutreachGatewayStats(id: string, data: Record<string, unknown> | undefined): OutreachGatewayStats | null;
81
+ /** The domain a mailbox's mail leaves from: its send-as address's, else the account's. */
82
+ export declare function outreachMailboxSendingDomain(mailbox: Pick<OutreachMailbox, 'sendAs' | 'email'> | null | undefined): string | null;
66
83
  /**
67
- * A domain's MX, resolved and classified — see the module note. A domain
68
- * the resolver says has no MX answers `none`; a resolver that could not
69
- * answer rejects, and the caller decides what a missing answer means.
84
+ * A domain's MX, resolved and classified. A domain the resolver says has no
85
+ * MX answers `none`; a resolver that could not answer rejects, and the
86
+ * caller decides what a missing answer means.
70
87
  */
71
88
  export declare function resolveOutreachDomainMx(resolveMx: OutreachResolveMx, domain: string): Promise<{
72
89
  mx: string[];
73
90
  gateway: OutreachMailGateway;
74
91
  }>;
75
92
  /**
76
- * The domain intel for every address, keyed by address — fresh from the
77
- * store, or looked up and stored when missing or older than the TTL. An
93
+ * The MX answer for every address, keyed by address — read through the
94
+ * platform cache, and looked up and stored when missing or stale. An
78
95
  * address with no domain, and one whose domain could not be looked up and
79
96
  * was never known, answers `null`.
80
97
  */
81
- export declare function readOutreachDomainIntel(firestore: Firestore, orgId: string, emails: readonly string[], input: OutreachDomainIntelReadInput): Promise<Map<string, OutreachDomainIntel | null>>;
82
- /** The ledger of each gateway named, keyed by gateway; a gateway with no ledger yet answers `null`. */
83
- export declare function readOutreachGatewayStats(firestore: Firestore, orgId: string, gateways: readonly OutreachMailGateway[]): Promise<Map<OutreachMailGateway, OutreachGatewayStats | null>>;
84
- /** What the gates and the chip read for one domain: its gateway and that gateway's recent record here. */
85
- export declare function outreachGatewayStandingOf(intel: OutreachDomainIntel, stats: OutreachGatewayStats | null, nowMs: number): OutreachGatewayStanding;
98
+ export declare function readOutreachDomainIntel(firestore: Firestore, _orgId: string, emails: readonly string[], input: OutreachDomainIntelReadInput): Promise<Map<string, MailDomainIntel | null>>;
99
+ /** A stored per-gateway ledger from before the sending domain keyed it, or `null` for no document. */
100
+ export declare function readStoredOutreachGatewayStats(id: string, data: Record<string, unknown> | undefined): OutreachGatewayStats | null;
86
101
  /**
87
- * The gateway standing of every address, keyed by address: the domain
88
- * intel read (and refreshed) above, joined to the ledger of each gateway it
89
- * names. `null` for an address whose domain could not be checked.
102
+ * The gateway standing of every address, keyed by address: the MX answer
103
+ * read (and refreshed) above, joined to the mailbox's sending-domain ledger
104
+ * with each gateway it names, plus what the legacy per-gateway ledger still
105
+ * holds. `null` for an address whose domain could not be checked.
90
106
  */
91
107
  export declare function readOutreachGatewayStandings(firestore: Firestore, orgId: string, emails: readonly string[], input: OutreachDomainIntelReadInput): Promise<Map<string, OutreachGatewayStanding | null>>;
92
108
  /** What a send met at a gateway, as the ledger counts it. */
93
- export type OutreachGatewayOutcome = 'sent' | 'delivered' | 'blocked';
109
+ export type OutreachGatewayOutcome = MailGatewayOutcome;
94
110
  /**
95
- * Counts an outcome on the domain's intel and on the gateway's ledger — see
96
- * the module note. Never throws: the send or the bounce it records has
97
- * already happened, and a lost count understates a gateway while a thrown
98
- * one loses the step that called.
111
+ * Counts an outcome on the mailbox's sending-domain ledger with a gateway.
112
+ * Never throws: the send or the bounce it records has already happened,
113
+ * and a lost count understates a gateway while a thrown one loses the step
114
+ * that called. A mailbox whose sending domain cannot be read counts nothing.
99
115
  */
100
116
  export declare function recordOutreachGatewayOutcome(firestore: Firestore, orgId: string, input: {
101
- domain: string;
117
+ sendingDomain: string | null;
102
118
  gateway: OutreachMailGateway;
103
119
  outcome: OutreachGatewayOutcome;
104
120
  count?: number;
105
121
  atMs: number;
122
+ /** A refusal's diagnostic, kept scrubbed of addresses. */
123
+ detail?: string | null;
106
124
  }): Promise<void>;
107
125
  export {};