@aglyn/plugins-outreach 1.0.0-beta.218 → 1.0.0-beta.220

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 (112) hide show
  1. package/package.json +15 -15
  2. package/src/lib/components/compliance-section.d.ts +5 -0
  3. package/src/lib/components/compliance-section.js +302 -180
  4. package/src/lib/components/compliance-section.js.map +1 -1
  5. package/src/lib/components/enroll-dialog.d.ts +12 -2
  6. package/src/lib/components/enroll-dialog.js +72 -18
  7. package/src/lib/components/enroll-dialog.js.map +1 -1
  8. package/src/lib/components/enrollment-detail.d.ts +5 -1
  9. package/src/lib/components/enrollment-detail.js +14 -8
  10. package/src/lib/components/enrollment-detail.js.map +1 -1
  11. package/src/lib/components/outreach-console-sections.d.ts +1 -1
  12. package/src/lib/components/outreach-console-sections.js +1 -1
  13. package/src/lib/components/outreach-console-sections.js.map +1 -1
  14. package/src/lib/components/sequence-editor.d.ts +13 -0
  15. package/src/lib/components/sequence-editor.js +586 -335
  16. package/src/lib/components/sequence-editor.js.map +1 -1
  17. package/src/lib/components/sequence-preview.js +25 -10
  18. package/src/lib/components/sequence-preview.js.map +1 -1
  19. package/src/lib/components/sequence-step-card.d.ts +10 -0
  20. package/src/lib/components/sequence-step-card.js +320 -202
  21. package/src/lib/components/sequence-step-card.js.map +1 -1
  22. package/src/lib/components/use-outreach-api.d.ts +2 -1
  23. package/src/lib/components/use-outreach-api.js +5 -3
  24. package/src/lib/components/use-outreach-api.js.map +1 -1
  25. package/src/lib/engine/compose.d.ts +1 -1
  26. package/src/lib/engine/compose.js +10 -6
  27. package/src/lib/engine/compose.js.map +1 -1
  28. package/src/lib/engine/delivery-status.js +1 -1
  29. package/src/lib/engine/delivery-status.js.map +1 -1
  30. package/src/lib/engine/enrollment-activity.d.ts +9 -1
  31. package/src/lib/engine/enrollment-activity.js +15 -2
  32. package/src/lib/engine/enrollment-activity.js.map +1 -1
  33. package/src/lib/engine/enrollment-state.d.ts +33 -2
  34. package/src/lib/engine/enrollment-state.js +68 -9
  35. package/src/lib/engine/enrollment-state.js.map +1 -1
  36. package/src/lib/engine/gates.d.ts +2 -2
  37. package/src/lib/engine/gates.js.map +1 -1
  38. package/src/lib/engine/opt-out-intent.js +1 -1
  39. package/src/lib/engine/opt-out-intent.js.map +1 -1
  40. package/src/lib/engine/sending-capacity.d.ts +1 -1
  41. package/src/lib/engine/sending-capacity.js +1 -1
  42. package/src/lib/engine/sending-capacity.js.map +1 -1
  43. package/src/lib/engine/sequence-validation.d.ts +20 -3
  44. package/src/lib/engine/sequence-validation.js +32 -6
  45. package/src/lib/engine/sequence-validation.js.map +1 -1
  46. package/src/lib/engine/thread-classification.js +3 -3
  47. package/src/lib/engine/thread-classification.js.map +1 -1
  48. package/src/lib/enrollment/enroll-people.js +6 -4
  49. package/src/lib/enrollment/enroll-people.js.map +1 -1
  50. package/src/lib/enrollment/gate-lookups.js +5 -4
  51. package/src/lib/enrollment/gate-lookups.js.map +1 -1
  52. package/src/lib/enrollment/step-overrides.d.ts +4 -2
  53. package/src/lib/enrollment/step-overrides.js +11 -4
  54. package/src/lib/enrollment/step-overrides.js.map +1 -1
  55. package/src/lib/mailboxes/mailbox-api.d.ts +5 -1
  56. package/src/lib/mailboxes/mailbox-api.js.map +1 -1
  57. package/src/lib/mailboxes/mailbox-routes.d.ts +5 -2
  58. package/src/lib/mailboxes/mailbox-routes.js +10 -7
  59. package/src/lib/mailboxes/mailbox-routes.js.map +1 -1
  60. package/src/lib/mailboxes/mailbox-settings.d.ts +2 -2
  61. package/src/lib/mailboxes/mailbox-settings.js +1 -1
  62. package/src/lib/mailboxes/mailbox-settings.js.map +1 -1
  63. package/src/lib/model/enrollment-timeline.d.ts +1 -1
  64. package/src/lib/model/enrollment-timeline.js +22 -3
  65. package/src/lib/model/enrollment-timeline.js.map +1 -1
  66. package/src/lib/model/outreach-api.d.ts +19 -1
  67. package/src/lib/model/outreach-api.js.map +1 -1
  68. package/src/lib/model/outreach.types.d.ts +26 -1
  69. package/src/lib/model/outreach.types.js.map +1 -1
  70. package/src/lib/model/sales-topic.d.ts +27 -0
  71. package/src/lib/model/sales-topic.js +27 -0
  72. package/src/lib/model/sales-topic.js.map +1 -0
  73. package/src/lib/model/sequence-draft.d.ts +1 -1
  74. package/src/lib/model/sequence-draft.js +2 -2
  75. package/src/lib/model/sequence-draft.js.map +1 -1
  76. package/src/lib/model/step-preview.d.ts +2 -0
  77. package/src/lib/model/step-preview.js +11 -7
  78. package/src/lib/model/step-preview.js.map +1 -1
  79. package/src/lib/model/stored-records.js +28 -2
  80. package/src/lib/model/stored-records.js.map +1 -1
  81. package/src/lib/plugin.js +1 -1
  82. package/src/lib/plugin.js.map +1 -1
  83. package/src/lib/routes/curate-routes.js +7 -7
  84. package/src/lib/routes/curate-routes.js.map +1 -1
  85. package/src/lib/routes/enroll-routes.d.ts +6 -0
  86. package/src/lib/routes/enroll-routes.js +31 -19
  87. package/src/lib/routes/enroll-routes.js.map +1 -1
  88. package/src/lib/routes/preview-routes.js +14 -5
  89. package/src/lib/routes/preview-routes.js.map +1 -1
  90. package/src/lib/routes/sequence-routes.js +9 -9
  91. package/src/lib/routes/sequence-routes.js.map +1 -1
  92. package/src/lib/routes/step-test-routes.js +3 -3
  93. package/src/lib/routes/step-test-routes.js.map +1 -1
  94. package/src/lib/runtime/campaign-credit.js +2 -2
  95. package/src/lib/runtime/campaign-credit.js.map +1 -1
  96. package/src/lib/runtime/campaign-detach.js +3 -2
  97. package/src/lib/runtime/campaign-detach.js.map +1 -1
  98. package/src/lib/runtime/lead-records.d.ts +6 -1
  99. package/src/lib/runtime/lead-records.js +25 -6
  100. package/src/lib/runtime/lead-records.js.map +1 -1
  101. package/src/lib/runtime/platform-runtime-deps.js +3 -3
  102. package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
  103. package/src/lib/runtime/send-job.js +20 -4
  104. package/src/lib/runtime/send-job.js.map +1 -1
  105. package/src/lib/runtime/sync-job.js +3 -3
  106. package/src/lib/runtime/sync-job.js.map +1 -1
  107. package/src/lib/testing/stand-in-crm-record-routes.d.ts +25 -0
  108. package/src/lib/testing/stand-in-crm-record-routes.js +46 -0
  109. package/src/lib/testing/stand-in-crm-record-routes.js.map +1 -0
  110. package/src/lib/transport/gmail-errors.d.ts +27 -0
  111. package/src/lib/transport/gmail-errors.js +56 -0
  112. package/src/lib/transport/gmail-errors.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 { 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":"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"}
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 { emailAddressOf, emailExcerpt } from '@aglyn/aglyn/app-utils/email-text'\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: emailExcerpt(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: emailExcerpt(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","emailAddressOf","emailExcerpt","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":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * The CRM's `contact`, `lead` and `company` record routes (AGL-3080), stood in
19
+ * for this plugin's specs, which may not load the CRM. They answer what the
20
+ * CRM publishes — a record's page under the site's hub or the organization's,
21
+ * and the Contacts list opened on one address — and what it publishes is held
22
+ * in its own `crm-record-routes.spec.ts`. Registered under the owner's id,
23
+ * `crm`, so a spec asking what happens where it is not loaded unregisters it.
24
+ */
25
+ export declare function standInCrmRecordRoutes(): void;
@@ -0,0 +1,46 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { registerPluginRecordRoute } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
18
+ /**
19
+ * The CRM's `contact`, `lead` and `company` record routes (AGL-3080), stood in
20
+ * for this plugin's specs, which may not load the CRM. They answer what the
21
+ * CRM publishes — a record's page under the site's hub or the organization's,
22
+ * and the Contacts list opened on one address — and what it publishes is held
23
+ * in its own `crm-record-routes.spec.ts`. Registered under the owner's id,
24
+ * `crm`, so a spec asking what happens where it is not loaded unregisters it.
25
+ */ export function standInCrmRecordRoutes() {
26
+ const hub = ({ orgSlug, host })=>host ? `/${orgSlug}/hosts/${host}/crm` : `/${orgSlug}/crm`;
27
+ const sectionRoute = (section)=>({
28
+ list: (context)=>`${hub(context)}/${section}`,
29
+ record: (context, id)=>`${hub(context)}/${section}/${encodeURIComponent(id)}`
30
+ });
31
+ registerPluginRecordRoute('contact', _extends({}, sectionRoute('contacts'), {
32
+ byEmail: (context, email)=>`${hub(context)}/contacts?${new URLSearchParams({
33
+ email
34
+ }).toString()}`
35
+ }), {
36
+ pluginId: 'crm'
37
+ });
38
+ registerPluginRecordRoute('lead', sectionRoute('leads'), {
39
+ pluginId: 'crm'
40
+ });
41
+ registerPluginRecordRoute('company', sectionRoute('companies'), {
42
+ pluginId: 'crm'
43
+ });
44
+ }
45
+
46
+ //# sourceMappingURL=stand-in-crm-record-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/testing/stand-in-crm-record-routes.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 {\n registerPluginRecordRoute,\n type PluginRecordRoute,\n type PluginRecordRouteContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\n\n/**\n * The CRM's `contact`, `lead` and `company` record routes (AGL-3080), stood in\n * for this plugin's specs, which may not load the CRM. They answer what the\n * CRM publishes — a record's page under the site's hub or the organization's,\n * and the Contacts list opened on one address — and what it publishes is held\n * in its own `crm-record-routes.spec.ts`. Registered under the owner's id,\n * `crm`, so a spec asking what happens where it is not loaded unregisters it.\n */\nexport function standInCrmRecordRoutes(): void {\n const hub = ({ orgSlug, host }: PluginRecordRouteContext) =>\n host ? `/${orgSlug}/hosts/${host}/crm` : `/${orgSlug}/crm`\n const sectionRoute = (section: string): PluginRecordRoute => ({\n list: (context) => `${hub(context)}/${section}`,\n record: (context, id) => `${hub(context)}/${section}/${encodeURIComponent(id)}`,\n })\n registerPluginRecordRoute(\n 'contact',\n {\n ...sectionRoute('contacts'),\n byEmail: (context, email) =>\n `${hub(context)}/contacts?${new URLSearchParams({ email }).toString()}`,\n },\n { pluginId: 'crm' },\n )\n registerPluginRecordRoute('lead', sectionRoute('leads'), { pluginId: 'crm' })\n registerPluginRecordRoute('company', sectionRoute('companies'), { pluginId: 'crm' })\n}\n"],"names":["registerPluginRecordRoute","standInCrmRecordRoutes","hub","orgSlug","host","sectionRoute","section","list","context","record","id","encodeURIComponent","byEmail","email","URLSearchParams","toString","pluginId"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,yBAAyB,QAGpB,mDAAkD;AAEzD;;;;;;;CAOC,GACD,OAAO,SAASC;IACd,MAAMC,MAAM,CAAC,EAAEC,OAAO,EAAEC,IAAI,EAA4B,GACtDA,OAAO,CAAC,CAAC,EAAED,QAAQ,OAAO,EAAEC,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,EAAED,QAAQ,IAAI,CAAC;IAC5D,MAAME,eAAe,CAACC,UAAwC,CAAA;YAC5DC,MAAM,CAACC,UAAY,GAAGN,IAAIM,SAAS,CAAC,EAAEF,SAAS;YAC/CG,QAAQ,CAACD,SAASE,KAAO,GAAGR,IAAIM,SAAS,CAAC,EAAEF,QAAQ,CAAC,EAAEK,mBAAmBD,KAAK;QACjF,CAAA;IACAV,0BACE,WACA,aACKK,aAAa;QAChBO,SAAS,CAACJ,SAASK,QACjB,GAAGX,IAAIM,SAAS,UAAU,EAAE,IAAIM,gBAAgB;gBAAED;YAAM,GAAGE,QAAQ,IAAI;QAE3E;QAAEC,UAAU;IAAM;IAEpBhB,0BAA0B,QAAQK,aAAa,UAAU;QAAEW,UAAU;IAAM;IAC3EhB,0BAA0B,WAAWK,aAAa,cAAc;QAAEW,UAAU;IAAM;AACpF"}
@@ -82,3 +82,30 @@ export declare function isRateLimitBody(body: unknown): boolean;
82
82
  export declare function gmailApiError(status: number, body: unknown, init?: {
83
83
  retryAfterMs?: number | null;
84
84
  }): GmailTransportError;
85
+ /** The refusal reasons a route answers a Google failure with. */
86
+ export type GoogleFailureReason = 'mail-service-unavailable' | 'google-refused' | 'rate-limited' | 'google-unavailable';
87
+ /** How a route answers one Google failure: its status, its reason and the sentence a person reads. */
88
+ export interface GoogleFailureAnswer {
89
+ status: number;
90
+ reason: GoogleFailureReason;
91
+ error: string;
92
+ }
93
+ /** What a person reads when the Google account behind a mailbox has no Gmail. */
94
+ export declare const OUTREACH_MAIL_SERVICE_UNAVAILABLE_MESSAGE: string;
95
+ /**
96
+ * A Google failure a route could not handle itself, answered by whose failure
97
+ * it is. Every route that talks to Google on a person's behalf answers
98
+ * through this one decision.
99
+ *
100
+ * Only an answer Google did not give — an outage, a dropped connection, a
101
+ * body that could not be read — or a request Google called malformed (which
102
+ * is this code's to fix) is a 5xx. A refusal about the ACCOUNT is the
103
+ * person's to act on: an account with no Gmail, one an administrator has
104
+ * barred from the Gmail API, or one Google is rate-limiting. Those are 4xx
105
+ * with a sentence that says what to do, so they do not page as an outage.
106
+ *
107
+ * Every failure is logged with Google's own code, status and reason, which is
108
+ * the one place an operator can read what Google actually said. A value that
109
+ * is not a {@link GmailTransportError} is rethrown: it is not Google's.
110
+ */
111
+ export declare function googleFailureAnswer(error: unknown, route: string): GoogleFailureAnswer;
@@ -134,5 +134,61 @@ export class GmailTransportError extends Error {
134
134
  if (status === 403) return make('forbidden', `Gmail refused the request${detail}`);
135
135
  return make('unexpected', `Gmail answered ${status}${detail}`);
136
136
  }
137
+ /** What a person reads when the Google account behind a mailbox has no Gmail. */ export const OUTREACH_MAIL_SERVICE_UNAVAILABLE_MESSAGE = 'This Google account does not have Gmail turned on yet. A new Google Workspace user can take up to a day ' + 'before Gmail is ready; otherwise ask your Workspace administrator to turn Gmail on for the account. ' + 'Then try again.';
138
+ /**
139
+ * A Google failure a route could not handle itself, answered by whose failure
140
+ * it is. Every route that talks to Google on a person's behalf answers
141
+ * through this one decision.
142
+ *
143
+ * Only an answer Google did not give — an outage, a dropped connection, a
144
+ * body that could not be read — or a request Google called malformed (which
145
+ * is this code's to fix) is a 5xx. A refusal about the ACCOUNT is the
146
+ * person's to act on: an account with no Gmail, one an administrator has
147
+ * barred from the Gmail API, or one Google is rate-limiting. Those are 4xx
148
+ * with a sentence that says what to do, so they do not page as an outage.
149
+ *
150
+ * Every failure is logged with Google's own code, status and reason, which is
151
+ * the one place an operator can read what Google actually said. A value that
152
+ * is not a {@link GmailTransportError} is rethrown: it is not Google's.
153
+ */ export function googleFailureAnswer(error, route) {
154
+ if (!(error instanceof GmailTransportError)) throw error;
155
+ const detail = {
156
+ code: error.code,
157
+ status: error.status,
158
+ providerReason: error.providerReason,
159
+ message: error.message
160
+ };
161
+ switch(error.code){
162
+ case 'mail_service_unavailable':
163
+ console.warn(`[outreach] ${route}: the Google account has no Gmail service`, detail);
164
+ return {
165
+ status: 422,
166
+ reason: 'mail-service-unavailable',
167
+ error: OUTREACH_MAIL_SERVICE_UNAVAILABLE_MESSAGE
168
+ };
169
+ case 'forbidden':
170
+ console.warn(`[outreach] ${route}: Google refused the account`, detail);
171
+ return {
172
+ status: 422,
173
+ reason: 'google-refused',
174
+ error: `Google refused this account's request: ${error.message} ` + 'If it is a Google Workspace account, ask its administrator whether Gmail API access is allowed for it.'
175
+ };
176
+ case 'rate_limited':
177
+ case 'quota_exceeded':
178
+ console.warn(`[outreach] ${route}: Google is limiting the account`, detail);
179
+ return {
180
+ status: 429,
181
+ reason: 'rate-limited',
182
+ error: error.code === 'quota_exceeded' ? 'This Google account has used its Gmail quota for today. Try again tomorrow.' : 'Google is rate-limiting this account. Try again in a few minutes.'
183
+ };
184
+ default:
185
+ console.error(`[outreach] ${route}: Google call failed`, detail);
186
+ return {
187
+ status: 502,
188
+ reason: 'google-unavailable',
189
+ error: error.retryable ? 'Google did not answer. Try again in a moment.' : `Google refused the request: ${error.message}`
190
+ };
191
+ }
192
+ }
137
193
 
138
194
  //# sourceMappingURL=gmail-errors.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transport/gmail-errors.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT A GOOGLE CALL CAN FAIL WITH, AS OUTREACH ACTS ON IT (AGL-2978).\n *\n * Google answers in two dialects — the OAuth token endpoint's\n * `{ error: 'invalid_grant' }` and the Gmail API's\n * `{ error: { code, status, errors: [{ reason }] } }` — and a caller should\n * not have to speak either. Every failure the transport raises is one\n * {@link GmailTransportError} carrying the three decisions a caller makes:\n *\n * - `reconnectRequired` — the grant itself is gone (revoked, expired, a\n * password change, a scope removed). Nothing will work until the rep\n * connects again, so the mailbox goes to `reconnect_required`.\n * - `retryable` — the request may succeed later as it stands (a rate limit,\n * an outage, a dropped connection). `retryAfterMs` says when, if Google\n * said.\n * - neither — the request itself is wrong, or the deployment is (a bad\n * client id or secret), and retrying it changes nothing.\n */\n\nexport type GmailTransportErrorCode =\n /** The refresh token was refused: revoked, expired or otherwise dead. */\n | 'invalid_grant'\n /** The grant is missing a scope the call needs. */\n | 'insufficient_scope'\n /** The OAuth client id or secret was refused — a deployment problem. */\n | 'client_misconfigured'\n /** Rate-limited, still after backing off. */\n | 'rate_limited'\n /** The account's sending or API quota for the day is spent. */\n | 'quota_exceeded'\n /** Google answered 5xx, still after backing off. */\n | 'unavailable'\n /** The request never got an answer: a network failure or a timeout. */\n | 'network'\n /** The access token was refused even after a fresh one was minted. */\n | 'unauthorized'\n /** The account has no Gmail service (a Workspace user without Gmail). */\n | 'mail_service_unavailable'\n | 'not_found'\n /** Google refused the request as malformed. */\n | 'invalid_request'\n | 'forbidden'\n /** An answer the transport could not read. */\n | 'unexpected'\n\nexport interface GmailTransportErrorInit {\n status?: number | null\n retryAfterMs?: number | null\n /** The provider's own reason string, kept for logs and health rows. */\n providerReason?: string | null\n}\n\nconst RECONNECT: ReadonlySet<GmailTransportErrorCode> = new Set([\n 'invalid_grant',\n 'insufficient_scope',\n])\n\nconst RETRYABLE: ReadonlySet<GmailTransportErrorCode> = new Set([\n 'rate_limited',\n 'unavailable',\n 'network',\n])\n\nexport class GmailTransportError extends Error {\n readonly code: GmailTransportErrorCode\n readonly status: number | null\n readonly retryAfterMs: number | null\n readonly providerReason: string | null\n\n constructor(code: GmailTransportErrorCode, message: string, init: GmailTransportErrorInit = {}) {\n super(message)\n this.name = 'GmailTransportError'\n this.code = code\n this.status = init.status ?? null\n this.retryAfterMs = init.retryAfterMs ?? null\n this.providerReason = init.providerReason ?? null\n }\n\n /** The grant is gone; the mailbox needs connecting again. */\n get reconnectRequired(): boolean {\n return RECONNECT.has(this.code)\n }\n\n /** The same request may succeed later. */\n get retryable(): boolean {\n return RETRYABLE.has(this.code)\n }\n}\n\n/** Whether a thrown value says the mailbox's grant is gone. */\nexport function isReconnectRequired(error: unknown): boolean {\n return error instanceof GmailTransportError && error.reconnectRequired\n}\n\n/** The OAuth token endpoint's error body: `{ error, error_description }`. */\nexport function tokenEndpointError(status: number, body: unknown): GmailTransportError {\n const record = (body ?? {}) as { error?: unknown; error_description?: unknown }\n const reason = typeof record.error === 'string' ? record.error : null\n const described = typeof record.error_description === 'string' ? record.error_description : ''\n if (reason === 'invalid_grant') {\n return new GmailTransportError(\n 'invalid_grant',\n 'Google refused the mailbox grant. The mailbox must be connected again.',\n { status, providerReason: reason },\n )\n }\n if (reason === 'invalid_client' || reason === 'unauthorized_client') {\n return new GmailTransportError(\n 'client_misconfigured',\n 'Google refused this deployment’s OAuth client. Check its client id and secret.',\n { status, providerReason: reason },\n )\n }\n if (reason === 'invalid_scope') {\n return new GmailTransportError('insufficient_scope', 'Google refused a requested scope.', {\n status,\n providerReason: reason,\n })\n }\n return new GmailTransportError(\n status >= 500 ? 'unavailable' : 'invalid_request',\n `Google’s token endpoint refused the request${described ? `: ${described.slice(0, 200)}` : '.'}`,\n { status, providerReason: reason },\n )\n}\n\ninterface GoogleApiErrorBody {\n error?: {\n code?: number\n message?: string\n status?: string\n errors?: Array<{ reason?: string; message?: string }>\n details?: Array<{ reason?: string }>\n }\n}\n\n/** Every reason string a Gmail API error body carries. */\nfunction apiReasons(body: unknown): string[] {\n const error = (body as GoogleApiErrorBody | null)?.error\n return [\n ...(error?.errors ?? []).map((entry) => entry?.reason),\n ...(error?.details ?? []).map((entry) => entry?.reason),\n error?.status,\n ].filter((reason): reason is string => typeof reason === 'string' && reason.length > 0)\n}\n\n/** Whether a Gmail API 403 is a rate limit, which backs off like a 429. */\nexport function isRateLimitBody(body: unknown): boolean {\n return apiReasons(body).some((reason) =>\n ['rateLimitExceeded', 'userRateLimitExceeded', 'RESOURCE_EXHAUSTED'].includes(reason),\n )\n}\n\n/** A Gmail API error response, mapped. */\nexport function gmailApiError(\n status: number,\n body: unknown,\n init: { retryAfterMs?: number | null } = {},\n): GmailTransportError {\n const reasons = apiReasons(body)\n const providerReason = reasons[0] ?? null\n const has = (...names: string[]) => reasons.some((reason) => names.includes(reason))\n const message = (body as GoogleApiErrorBody | null)?.error?.message\n const detail = typeof message === 'string' && message ? `: ${message.slice(0, 200)}` : '.'\n const make = (code: GmailTransportErrorCode, text: string) =>\n new GmailTransportError(code, text, { status, providerReason, retryAfterMs: init.retryAfterMs })\n\n if (status === 429 || (status === 403 && isRateLimitBody(body))) {\n return make('rate_limited', 'Gmail is rate-limiting this mailbox. Try again later.')\n }\n if (has('dailyLimitExceeded', 'quotaExceeded')) {\n return make('quota_exceeded', 'This mailbox has spent its Gmail quota for the day.')\n }\n if (has('insufficientPermissions', 'ACCESS_TOKEN_SCOPE_INSUFFICIENT')) {\n return make('insufficient_scope', 'The mailbox grant is missing a permission Sequences needs.')\n }\n if (status >= 500) return make('unavailable', `Gmail is unavailable${detail}`)\n if (status === 401) return make('unauthorized', 'Gmail refused the mailbox’s access token.')\n if (status === 400 && has('failedPrecondition') && /mail service not enabled/i.test(String(message))) {\n return make('mail_service_unavailable', 'This Google account has no Gmail service.')\n }\n if (status === 404) return make('not_found', `Gmail found nothing there${detail}`)\n if (status === 400) return make('invalid_request', `Gmail refused the request${detail}`)\n if (status === 403) return make('forbidden', `Gmail refused the request${detail}`)\n return make('unexpected', `Gmail answered ${status}${detail}`)\n}\n"],"names":["RECONNECT","Set","RETRYABLE","GmailTransportError","Error","reconnectRequired","has","code","retryable","message","init","name","status","retryAfterMs","providerReason","isReconnectRequired","error","tokenEndpointError","body","record","reason","described","error_description","slice","apiReasons","errors","map","entry","details","filter","length","isRateLimitBody","some","includes","gmailApiError","reasons","names","detail","make","text","test","String"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transport/gmail-errors.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT A GOOGLE CALL CAN FAIL WITH, AS OUTREACH ACTS ON IT (AGL-2978).\n *\n * Google answers in two dialects — the OAuth token endpoint's\n * `{ error: 'invalid_grant' }` and the Gmail API's\n * `{ error: { code, status, errors: [{ reason }] } }` — and a caller should\n * not have to speak either. Every failure the transport raises is one\n * {@link GmailTransportError} carrying the three decisions a caller makes:\n *\n * - `reconnectRequired` — the grant itself is gone (revoked, expired, a\n * password change, a scope removed). Nothing will work until the rep\n * connects again, so the mailbox goes to `reconnect_required`.\n * - `retryable` — the request may succeed later as it stands (a rate limit,\n * an outage, a dropped connection). `retryAfterMs` says when, if Google\n * said.\n * - neither — the request itself is wrong, or the deployment is (a bad\n * client id or secret), and retrying it changes nothing.\n */\n\nexport type GmailTransportErrorCode =\n /** The refresh token was refused: revoked, expired or otherwise dead. */\n | 'invalid_grant'\n /** The grant is missing a scope the call needs. */\n | 'insufficient_scope'\n /** The OAuth client id or secret was refused — a deployment problem. */\n | 'client_misconfigured'\n /** Rate-limited, still after backing off. */\n | 'rate_limited'\n /** The account's sending or API quota for the day is spent. */\n | 'quota_exceeded'\n /** Google answered 5xx, still after backing off. */\n | 'unavailable'\n /** The request never got an answer: a network failure or a timeout. */\n | 'network'\n /** The access token was refused even after a fresh one was minted. */\n | 'unauthorized'\n /** The account has no Gmail service (a Workspace user without Gmail). */\n | 'mail_service_unavailable'\n | 'not_found'\n /** Google refused the request as malformed. */\n | 'invalid_request'\n | 'forbidden'\n /** An answer the transport could not read. */\n | 'unexpected'\n\nexport interface GmailTransportErrorInit {\n status?: number | null\n retryAfterMs?: number | null\n /** The provider's own reason string, kept for logs and health rows. */\n providerReason?: string | null\n}\n\nconst RECONNECT: ReadonlySet<GmailTransportErrorCode> = new Set([\n 'invalid_grant',\n 'insufficient_scope',\n])\n\nconst RETRYABLE: ReadonlySet<GmailTransportErrorCode> = new Set([\n 'rate_limited',\n 'unavailable',\n 'network',\n])\n\nexport class GmailTransportError extends Error {\n readonly code: GmailTransportErrorCode\n readonly status: number | null\n readonly retryAfterMs: number | null\n readonly providerReason: string | null\n\n constructor(code: GmailTransportErrorCode, message: string, init: GmailTransportErrorInit = {}) {\n super(message)\n this.name = 'GmailTransportError'\n this.code = code\n this.status = init.status ?? null\n this.retryAfterMs = init.retryAfterMs ?? null\n this.providerReason = init.providerReason ?? null\n }\n\n /** The grant is gone; the mailbox needs connecting again. */\n get reconnectRequired(): boolean {\n return RECONNECT.has(this.code)\n }\n\n /** The same request may succeed later. */\n get retryable(): boolean {\n return RETRYABLE.has(this.code)\n }\n}\n\n/** Whether a thrown value says the mailbox's grant is gone. */\nexport function isReconnectRequired(error: unknown): boolean {\n return error instanceof GmailTransportError && error.reconnectRequired\n}\n\n/** The OAuth token endpoint's error body: `{ error, error_description }`. */\nexport function tokenEndpointError(status: number, body: unknown): GmailTransportError {\n const record = (body ?? {}) as { error?: unknown; error_description?: unknown }\n const reason = typeof record.error === 'string' ? record.error : null\n const described = typeof record.error_description === 'string' ? record.error_description : ''\n if (reason === 'invalid_grant') {\n return new GmailTransportError(\n 'invalid_grant',\n 'Google refused the mailbox grant. The mailbox must be connected again.',\n { status, providerReason: reason },\n )\n }\n if (reason === 'invalid_client' || reason === 'unauthorized_client') {\n return new GmailTransportError(\n 'client_misconfigured',\n 'Google refused this deployment’s OAuth client. Check its client id and secret.',\n { status, providerReason: reason },\n )\n }\n if (reason === 'invalid_scope') {\n return new GmailTransportError('insufficient_scope', 'Google refused a requested scope.', {\n status,\n providerReason: reason,\n })\n }\n return new GmailTransportError(\n status >= 500 ? 'unavailable' : 'invalid_request',\n `Google’s token endpoint refused the request${described ? `: ${described.slice(0, 200)}` : '.'}`,\n { status, providerReason: reason },\n )\n}\n\ninterface GoogleApiErrorBody {\n error?: {\n code?: number\n message?: string\n status?: string\n errors?: Array<{ reason?: string; message?: string }>\n details?: Array<{ reason?: string }>\n }\n}\n\n/** Every reason string a Gmail API error body carries. */\nfunction apiReasons(body: unknown): string[] {\n const error = (body as GoogleApiErrorBody | null)?.error\n return [\n ...(error?.errors ?? []).map((entry) => entry?.reason),\n ...(error?.details ?? []).map((entry) => entry?.reason),\n error?.status,\n ].filter((reason): reason is string => typeof reason === 'string' && reason.length > 0)\n}\n\n/** Whether a Gmail API 403 is a rate limit, which backs off like a 429. */\nexport function isRateLimitBody(body: unknown): boolean {\n return apiReasons(body).some((reason) =>\n ['rateLimitExceeded', 'userRateLimitExceeded', 'RESOURCE_EXHAUSTED'].includes(reason),\n )\n}\n\n/** A Gmail API error response, mapped. */\nexport function gmailApiError(\n status: number,\n body: unknown,\n init: { retryAfterMs?: number | null } = {},\n): GmailTransportError {\n const reasons = apiReasons(body)\n const providerReason = reasons[0] ?? null\n const has = (...names: string[]) => reasons.some((reason) => names.includes(reason))\n const message = (body as GoogleApiErrorBody | null)?.error?.message\n const detail = typeof message === 'string' && message ? `: ${message.slice(0, 200)}` : '.'\n const make = (code: GmailTransportErrorCode, text: string) =>\n new GmailTransportError(code, text, { status, providerReason, retryAfterMs: init.retryAfterMs })\n\n if (status === 429 || (status === 403 && isRateLimitBody(body))) {\n return make('rate_limited', 'Gmail is rate-limiting this mailbox. Try again later.')\n }\n if (has('dailyLimitExceeded', 'quotaExceeded')) {\n return make('quota_exceeded', 'This mailbox has spent its Gmail quota for the day.')\n }\n if (has('insufficientPermissions', 'ACCESS_TOKEN_SCOPE_INSUFFICIENT')) {\n return make('insufficient_scope', 'The mailbox grant is missing a permission Sequences needs.')\n }\n if (status >= 500) return make('unavailable', `Gmail is unavailable${detail}`)\n if (status === 401) return make('unauthorized', 'Gmail refused the mailbox’s access token.')\n if (status === 400 && has('failedPrecondition') && /mail service not enabled/i.test(String(message))) {\n return make('mail_service_unavailable', 'This Google account has no Gmail service.')\n }\n if (status === 404) return make('not_found', `Gmail found nothing there${detail}`)\n if (status === 400) return make('invalid_request', `Gmail refused the request${detail}`)\n if (status === 403) return make('forbidden', `Gmail refused the request${detail}`)\n return make('unexpected', `Gmail answered ${status}${detail}`)\n}\n\n/** The refusal reasons a route answers a Google failure with. */\nexport type GoogleFailureReason =\n | 'mail-service-unavailable'\n | 'google-refused'\n | 'rate-limited'\n | 'google-unavailable'\n\n/** How a route answers one Google failure: its status, its reason and the sentence a person reads. */\nexport interface GoogleFailureAnswer {\n status: number\n reason: GoogleFailureReason\n error: string\n}\n\n/** What a person reads when the Google account behind a mailbox has no Gmail. */\nexport const OUTREACH_MAIL_SERVICE_UNAVAILABLE_MESSAGE =\n 'This Google account does not have Gmail turned on yet. A new Google Workspace user can take up to a day ' +\n 'before Gmail is ready; otherwise ask your Workspace administrator to turn Gmail on for the account. ' +\n 'Then try again.'\n\n/**\n * A Google failure a route could not handle itself, answered by whose failure\n * it is. Every route that talks to Google on a person's behalf answers\n * through this one decision.\n *\n * Only an answer Google did not give — an outage, a dropped connection, a\n * body that could not be read — or a request Google called malformed (which\n * is this code's to fix) is a 5xx. A refusal about the ACCOUNT is the\n * person's to act on: an account with no Gmail, one an administrator has\n * barred from the Gmail API, or one Google is rate-limiting. Those are 4xx\n * with a sentence that says what to do, so they do not page as an outage.\n *\n * Every failure is logged with Google's own code, status and reason, which is\n * the one place an operator can read what Google actually said. A value that\n * is not a {@link GmailTransportError} is rethrown: it is not Google's.\n */\nexport function googleFailureAnswer(error: unknown, route: string): GoogleFailureAnswer {\n if (!(error instanceof GmailTransportError)) throw error\n const detail = {\n code: error.code,\n status: error.status,\n providerReason: error.providerReason,\n message: error.message,\n }\n switch (error.code) {\n case 'mail_service_unavailable':\n console.warn(`[outreach] ${route}: the Google account has no Gmail service`, detail)\n return { status: 422, reason: 'mail-service-unavailable', error: OUTREACH_MAIL_SERVICE_UNAVAILABLE_MESSAGE }\n case 'forbidden':\n console.warn(`[outreach] ${route}: Google refused the account`, detail)\n return {\n status: 422,\n reason: 'google-refused',\n error:\n `Google refused this account's request: ${error.message} ` +\n 'If it is a Google Workspace account, ask its administrator whether Gmail API access is allowed for it.',\n }\n case 'rate_limited':\n case 'quota_exceeded':\n console.warn(`[outreach] ${route}: Google is limiting the account`, detail)\n return {\n status: 429,\n reason: 'rate-limited',\n error:\n error.code === 'quota_exceeded'\n ? 'This Google account has used its Gmail quota for today. Try again tomorrow.'\n : 'Google is rate-limiting this account. Try again in a few minutes.',\n }\n default:\n console.error(`[outreach] ${route}: Google call failed`, detail)\n return {\n status: 502,\n reason: 'google-unavailable',\n error: error.retryable\n ? 'Google did not answer. Try again in a moment.'\n : `Google refused the request: ${error.message}`,\n }\n }\n}\n"],"names":["RECONNECT","Set","RETRYABLE","GmailTransportError","Error","reconnectRequired","has","code","retryable","message","init","name","status","retryAfterMs","providerReason","isReconnectRequired","error","tokenEndpointError","body","record","reason","described","error_description","slice","apiReasons","errors","map","entry","details","filter","length","isRateLimitBody","some","includes","gmailApiError","reasons","names","detail","make","text","test","String","OUTREACH_MAIL_SERVICE_UNAVAILABLE_MESSAGE","googleFailureAnswer","route","console","warn"],"mappings":"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"}