@aglyn/plugins-outreach 1.0.0-beta.159 → 1.0.0-beta.161

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 (72) hide show
  1. package/README.md +4 -0
  2. package/package.json +13 -13
  3. package/src/lib/components/compliance-section.d.ts +3 -2
  4. package/src/lib/components/compliance-section.js +7 -2
  5. package/src/lib/components/compliance-section.js.map +1 -1
  6. package/src/lib/components/do-not-contact-domains.d.ts +29 -0
  7. package/src/lib/components/do-not-contact-domains.js +208 -0
  8. package/src/lib/components/do-not-contact-domains.js.map +1 -0
  9. package/src/lib/components/use-outreach-api.d.ts +5 -1
  10. package/src/lib/components/use-outreach-api.js +12 -0
  11. package/src/lib/components/use-outreach-api.js.map +1 -1
  12. package/src/lib/components/use-outreach-data.d.ts +8 -1
  13. package/src/lib/components/use-outreach-data.js +32 -0
  14. package/src/lib/components/use-outreach-data.js.map +1 -1
  15. package/src/lib/constants/api-routes.d.ts +5 -0
  16. package/src/lib/constants/api-routes.js +4 -0
  17. package/src/lib/constants/api-routes.js.map +1 -1
  18. package/src/lib/engine/do-not-contact-domain.d.ts +39 -0
  19. package/src/lib/engine/do-not-contact-domain.js +51 -0
  20. package/src/lib/engine/do-not-contact-domain.js.map +1 -0
  21. package/src/lib/engine/gates.d.ts +6 -1
  22. package/src/lib/engine/gates.js +6 -0
  23. package/src/lib/engine/gates.js.map +1 -1
  24. package/src/lib/engine/gateway-block.d.ts +31 -0
  25. package/src/lib/engine/gateway-block.js +54 -0
  26. package/src/lib/engine/gateway-block.js.map +1 -0
  27. package/src/lib/engine/index.d.ts +3 -0
  28. package/src/lib/engine/index.js +3 -0
  29. package/src/lib/engine/index.js.map +1 -1
  30. package/src/lib/engine/mailbox-health.d.ts +5 -0
  31. package/src/lib/engine/mailbox-health.js +12 -5
  32. package/src/lib/engine/mailbox-health.js.map +1 -1
  33. package/src/lib/engine/mailbox-notice.d.ts +46 -0
  34. package/src/lib/engine/mailbox-notice.js +64 -0
  35. package/src/lib/engine/mailbox-notice.js.map +1 -0
  36. package/src/lib/enrollment/gate-lookups.js +7 -3
  37. package/src/lib/enrollment/gate-lookups.js.map +1 -1
  38. package/src/lib/model/outreach-api.d.ts +23 -2
  39. package/src/lib/model/outreach-api.js.map +1 -1
  40. package/src/lib/model/outreach.types.d.ts +33 -1
  41. package/src/lib/model/outreach.types.js +11 -1
  42. package/src/lib/model/outreach.types.js.map +1 -1
  43. package/src/lib/routes/do-not-contact-routes.d.ts +35 -0
  44. package/src/lib/routes/do-not-contact-routes.js +92 -0
  45. package/src/lib/routes/do-not-contact-routes.js.map +1 -0
  46. package/src/lib/routes/enroll-routes.js +1 -0
  47. package/src/lib/routes/enroll-routes.js.map +1 -1
  48. package/src/lib/routes/register-routes.js +4 -0
  49. package/src/lib/routes/register-routes.js.map +1 -1
  50. package/src/lib/runtime/mailbox-health-store.d.ts +1 -1
  51. package/src/lib/runtime/mailbox-health-store.js +17 -4
  52. package/src/lib/runtime/mailbox-health-store.js.map +1 -1
  53. package/src/lib/runtime/mailbox-notices.d.ts +39 -0
  54. package/src/lib/runtime/mailbox-notices.js +117 -0
  55. package/src/lib/runtime/mailbox-notices.js.map +1 -0
  56. package/src/lib/runtime/person-erasure.d.ts +4 -0
  57. package/src/lib/runtime/person-erasure.js.map +1 -1
  58. package/src/lib/runtime/platform-runtime-deps.js +25 -0
  59. package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
  60. package/src/lib/runtime/runtime-deps.d.ts +21 -0
  61. package/src/lib/runtime/runtime-deps.js.map +1 -1
  62. package/src/lib/runtime/send-job.js +6 -6
  63. package/src/lib/runtime/send-job.js.map +1 -1
  64. package/src/lib/runtime/sync-job.d.ts +2 -0
  65. package/src/lib/runtime/sync-job.js +34 -7
  66. package/src/lib/runtime/sync-job.js.map +1 -1
  67. package/src/lib/server.d.ts +1 -1
  68. package/src/lib/server.js +1 -1
  69. package/src/lib/server.js.map +1 -1
  70. package/src/lib/storage/do-not-contact-store.d.ts +28 -1
  71. package/src/lib/storage/do-not-contact-store.js +117 -9
  72. package/src/lib/storage/do-not-contact-store.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/sync-job.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { crmInboundExcerpt, emailAddressOf } from '@aglyn/aglyn/app-utils/crm-inbound'\nimport { outreachMessageIdHeader } from '../engine/compose'\nimport { readOutreachDeliveryReport } from '../engine/delivery-status'\nimport type { OutreachEnrollmentEvent } from '../engine/enrollment-state'\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 { markOutreachMailboxReconnectRequired } from '../mailboxes/mailbox-transport'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachEnrollment,\n type OutreachMailbox,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport { addOutreachDoNotContact } 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 { applyOutreachEvent, recordOutreachOptOut } from './enrollment-events'\nimport {\n applyOutreachMailboxHealth,\n emptyOutreachHealthDelta,\n type OutreachMailboxHealthDelta,\n} from './mailbox-health-store'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { markOutreachLeadWorking } from './lead-records'\nimport { fileOutreachEmail, fileOutreachTask } 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.\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 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 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 markOutreachMailboxReconnectRequired(firestore, {\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 markOutreachMailboxReconnectRequired(firestore, {\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 for (const doc of watched.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment) continue\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 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 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 })\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, { hostId: enrollment.hostId, 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 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 event = { type: 'bounce', atMs: decidedBy?.atMs || nowMs, detail: diagnostic }\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 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","crmInboundExcerpt","emailAddressOf","outreachMessageIdHeader","readOutreachDeliveryReport","effectiveOutreachWindow","postponeOutreachForAutoReply","decideOutreachThread","outreachHeader","outreachThreadMessageFromGmail","mailboxRef","markOutreachMailboxReconnectRequired","OUTREACH_COLLECTIONS","addOutreachDoNotContact","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","applyOutreachEvent","recordOutreachOptOut","applyOutreachMailboxHealth","emptyOutreachHealthDelta","markOutreachLeadWorking","fileOutreachEmail","fileOutreachTask","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","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","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","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","dedupeKey","title","contactName","notes","dueAtMs","assigneeUid","connectedByUid","target","leadId","type","suppressBouncedEmail","enrollmentId","sequence","nextDueAtMs","receivedAtMs","timeZone","timezone","window","settings","random","extra","current","waiting","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 { 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 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 { applyOutreachEvent, recordOutreachOptOut } from './enrollment-events'\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, fileOutreachTask } 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 *\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 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 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 for (const doc of watched.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment) continue\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 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 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 })\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, { hostId: enrollment.hostId, 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 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 if (domain && !isPublicMailboxDomain(domain) && isOutreachGatewayBlock(decidedBy?.bounce ?? null)) {\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 event = { type: 'bounce', atMs: decidedBy?.atMs || nowMs, 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 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","isOutreachGatewayBlock","outreachGatewayBlockDetail","effectiveOutreachWindow","postponeOutreachForAutoReply","decideOutreachThread","outreachHeader","outreachThreadMessageFromGmail","mailboxRef","OUTREACH_COLLECTIONS","addOutreachDoNotContact","addOutreachDoNotContactDomain","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","applyOutreachEvent","recordOutreachOptOut","applyOutreachMailboxHealth","emptyOutreachHealthDelta","noteOutreachMailboxReconnectRequired","markOutreachLeadWorking","fileOutreachEmail","fileOutreachTask","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","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","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","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","dedupeKey","title","contactName","notes","dueAtMs","assigneeUid","connectedByUid","target","leadId","type","suppressBouncedEmail","enrollmentId","domain","sequence","nextDueAtMs","receivedAtMs","timeZone","timezone","window","settings","random","extra","current","waiting","Object","assign"],"mappings":"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"}
@@ -50,4 +50,4 @@ export type { GmailClient, GmailFullThread, GmailMessageMetadata, GmailThread, }
50
50
  export { buildRfc5322Message, encodeGmailRawMessage, LIST_UNSUBSCRIBE_ONE_CLICK, Rfc5322MessageError, type OutreachComposedMessage, type OutreachMailAddress, } from './transport/rfc5322';
51
51
  export { sendComposedOutreachEmail, sendOutreachMessage, type OutreachSentMessage, type SendOutreachMessageOptions, } from './transport/send-message';
52
52
  export { outreachComplianceSettingsRef, readOutreachComplianceSettingsDoc, writeOutreachComplianceSettings, type WriteOutreachComplianceSettingsInput, type WriteOutreachComplianceSettingsResult, } from './storage/compliance-settings-store';
53
- export { addOutreachDoNotContact, getOutreachDoNotContactEntry, isOutreachDoNotContact, lookupOutreachDoNotContact, outreachDoNotContactCollection, readOutreachDoNotContactEntry, type AddOutreachDoNotContactInput, type AddOutreachDoNotContactResult, } from './storage/do-not-contact-store';
53
+ export { addOutreachDoNotContact, addOutreachDoNotContactDomain, getOutreachDoNotContactEntry, isOutreachDoNotContact, listOutreachDoNotContactDomains, lookupOutreachDoNotContact, lookupOutreachDoNotContactDomains, outreachDoNotContactCollection, outreachDoNotContactDomainCollection, readOutreachDoNotContactDomainEntry, readOutreachDoNotContactEntry, removeOutreachDoNotContactDomain, type AddOutreachDoNotContactDomainInput, type AddOutreachDoNotContactDomainResult, type AddOutreachDoNotContactInput, type AddOutreachDoNotContactResult, } from './storage/do-not-contact-store';
package/src/lib/server.js CHANGED
@@ -97,6 +97,6 @@ export { sendComposedOutreachEmail, sendOutreachMessage } from "./transport/send
97
97
  // the list is checked before every send, and a reply that opts out, an
98
98
  // unsubscribe and a hard bounce each put the address on it.
99
99
  export { outreachComplianceSettingsRef, readOutreachComplianceSettingsDoc, writeOutreachComplianceSettings } from "./storage/compliance-settings-store.js";
100
- export { addOutreachDoNotContact, getOutreachDoNotContactEntry, isOutreachDoNotContact, lookupOutreachDoNotContact, outreachDoNotContactCollection, readOutreachDoNotContactEntry } from "./storage/do-not-contact-store.js";
100
+ export { addOutreachDoNotContact, addOutreachDoNotContactDomain, getOutreachDoNotContactEntry, isOutreachDoNotContact, listOutreachDoNotContactDomains, lookupOutreachDoNotContact, lookupOutreachDoNotContactDomains, outreachDoNotContactCollection, outreachDoNotContactDomainCollection, readOutreachDoNotContactDomainEntry, readOutreachDoNotContactEntry, removeOutreachDoNotContactDomain } from "./storage/do-not-contact-store.js";
101
101
 
102
102
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/outreach/src/lib/server.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 * Outreach's SERVER half (AGL-2974), `@aglyn/plugins-outreach/server`.\n *\n * Every write to an `outreach*` collection is made here: the Firestore rules\n * refuse all of them to clients, so a mailbox, a sequence, an enrollment and a\n * credential only ever change through a handler registered below, behind the\n * session, entitlement and permission checks that handler makes.\n *\n * `outreach/ping` exists so the wiring is proven rather than assumed, as the\n * CRM's does: `plugins.config.json` names `registerOutreachConsoleApi` and the\n * `outreach` prefix, the generated server manifest loads this module, and the\n * console's `/api/[...pluginApi]` dispatcher reaches the handler — after its\n * own gates, which already refuse a request while `release_outreach` is off\n * for the caller. A first real route that also had to be the first wiring\n * test would be two things to debug at once.\n *\n * A route that reads or writes org data resolves the caller the way the CRM's\n * org routes do (`resolveOrgMembership` / `memberHasOrgPermission` from\n * `@aglyn/tenant-data-admin`) and asks `checkEntitlement(org, 'outreach')`\n * and `outreach.use` before it touches a document.\n */\n\nimport {\n registerPluginApiRoute,\n registerPluginPermissions,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { OUTREACH_API_ROUTES } from './constants/api-routes'\nimport {\n OUTREACH_PERMISSIONS,\n OUTREACH_PLUGIN_ID,\n} from './constants/bundle-common'\nimport { registerOutreachMailboxRoutes } from './mailboxes/register-mailbox-routes'\nimport { registerOutreachRoutes } from './routes/register-routes'\n\n/**\n * No auth, no org, no data: it answers whether this server bundle was loaded\n * and its routes registered, which is a fact about the process rather than\n * about any workspace.\n */\nexport const outreachPingHandler: PluginApiHandler = (req, res) => {\n if (req.method !== 'GET') {\n res.setHeader('Allow', 'GET')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n res.status(200).json({ ok: true, plugin: OUTREACH_PLUGIN_ID })\n}\n\n/** Console API registration, named in `plugins.config.json` as `consoleApi`. */\nexport function registerOutreachConsoleApi(): void {\n // The server resolves plugin permission keys from this registry, so a\n // route's `outreach.use` check and the console's gate read the same tier\n // defaults.\n registerPluginPermissions(OUTREACH_PERMISSIONS)\n registerPluginApiRoute(OUTREACH_API_ROUTES.ping, outreachPingHandler)\n // Connect a Google mailbox, its settings, pause, test and disconnect\n // (AGL-2978).\n registerOutreachMailboxRoutes()\n // Compliance settings, sequences, enrollment and enrollment actions\n // (AGL-2980).\n registerOutreachRoutes()\n // The one-click unsubscribe every email carries (AGL-2981): a recipient\n // link, so it answers whether or not Outreach is released to the\n // organization now. Its module loads on the first request.\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.unsubscribe,\n {\n web: async (request, context) =>\n (await import('./runtime/platform-runtime-deps')).platformOutreachUnsubscribeRoute()(request, context),\n },\n { recipientLink: true },\n )\n // A link in a tracked sequence email (AGL-3239): a recipient link for the\n // same reason, and more pressingly — a link already sitting in somebody's\n // inbox has to keep forwarding them whether or not the rollout is paused.\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.click,\n {\n web: async (request, context) =>\n (await import('./runtime/platform-runtime-deps')).platformOutreachClickRoute()(request, context),\n },\n { recipientLink: true },\n )\n}\n\n// The transport the sending runtime reaches a connected mailbox through\n// (AGL-2978): open a mailbox's Gmail client, mark one reconnect-required,\n// send the engine's composed email through the one RFC 5322 writer, and read\n// a thread whole for the engine's reply and bounce classifier.\nexport {\n markOutreachMailboxReconnectRequired,\n openOutreachMailboxClient,\n type OpenedOutreachMailbox,\n} from './mailboxes/mailbox-transport'\nexport {\n GmailTransportError,\n isReconnectRequired,\n type GmailTransportErrorCode,\n} from './transport/gmail-errors'\nexport type {\n GmailClient,\n GmailFullThread,\n GmailMessageMetadata,\n GmailThread,\n} from './transport/gmail-client'\nexport {\n buildRfc5322Message,\n encodeGmailRawMessage,\n LIST_UNSUBSCRIBE_ONE_CLICK,\n Rfc5322MessageError,\n type OutreachComposedMessage,\n type OutreachMailAddress,\n} from './transport/rfc5322'\nexport {\n sendComposedOutreachEmail,\n sendOutreachMessage,\n type OutreachSentMessage,\n type SendOutreachMessageOptions,\n} from './transport/send-message'\n\n// The organization's compliance settings and do-not-contact list\n// (AGL-2980), for the sending runtime: the footer is read before every send,\n// the list is checked before every send, and a reply that opts out, an\n// unsubscribe and a hard bounce each put the address on it.\nexport {\n outreachComplianceSettingsRef,\n readOutreachComplianceSettingsDoc,\n writeOutreachComplianceSettings,\n type WriteOutreachComplianceSettingsInput,\n type WriteOutreachComplianceSettingsResult,\n} from './storage/compliance-settings-store'\nexport {\n addOutreachDoNotContact,\n getOutreachDoNotContactEntry,\n isOutreachDoNotContact,\n lookupOutreachDoNotContact,\n outreachDoNotContactCollection,\n readOutreachDoNotContactEntry,\n type AddOutreachDoNotContactInput,\n type AddOutreachDoNotContactResult,\n} from './storage/do-not-contact-store'\n"],"names":["registerPluginApiRoute","registerPluginPermissions","OUTREACH_API_ROUTES","OUTREACH_PERMISSIONS","OUTREACH_PLUGIN_ID","registerOutreachMailboxRoutes","registerOutreachRoutes","outreachPingHandler","req","res","method","setHeader","status","json","error","ok","plugin","registerOutreachConsoleApi","ping","unsubscribe","web","request","context","platformOutreachUnsubscribeRoute","recipientLink","click","platformOutreachClickRoute","markOutreachMailboxReconnectRequired","openOutreachMailboxClient","GmailTransportError","isReconnectRequired","buildRfc5322Message","encodeGmailRawMessage","LIST_UNSUBSCRIBE_ONE_CLICK","Rfc5322MessageError","sendComposedOutreachEmail","sendOutreachMessage","outreachComplianceSettingsRef","readOutreachComplianceSettingsDoc","writeOutreachComplianceSettings","addOutreachDoNotContact","getOutreachDoNotContactEntry","isOutreachDoNotContact","lookupOutreachDoNotContact","outreachDoNotContactCollection","readOutreachDoNotContactEntry"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;CAoBC,GAED,SACEA,sBAAsB,EACtBC,yBAAyB,QAEpB,sBAAqB;AAC5B,SAASC,mBAAmB,QAAQ,4BAAwB;AAC5D,SACEC,oBAAoB,EACpBC,kBAAkB,QACb,+BAA2B;AAClC,SAASC,6BAA6B,QAAQ,yCAAqC;AACnF,SAASC,sBAAsB,QAAQ,8BAA0B;AAEjE;;;;CAIC,GACD,OAAO,MAAMC,sBAAwC,CAACC,KAAKC;IACzD,IAAID,IAAIE,MAAM,KAAK,OAAO;QACxBD,IAAIE,SAAS,CAAC,SAAS;QACvBF,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;QACnD;IACF;IACAL,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEE,IAAI;QAAMC,QAAQZ;IAAmB;AAC9D,EAAC;AAED,8EAA8E,GAC9E,OAAO,SAASa;IACd,sEAAsE;IACtE,yEAAyE;IACzE,YAAY;IACZhB,0BAA0BE;IAC1BH,uBAAuBE,oBAAoBgB,IAAI,EAAEX;IACjD,qEAAqE;IACrE,cAAc;IACdF;IACA,oEAAoE;IACpE,cAAc;IACdC;IACA,wEAAwE;IACxE,iEAAiE;IACjE,2DAA2D;IAC3DN,uBACEE,oBAAoBiB,WAAW,EAC/B;QACEC,KAAK,OAAOC,SAASC,UACnB,AAAC,CAAA,MAAM,MAAM,CAAC,qCAAiC,EAAGC,gCAAgC,GAAGF,SAASC;IAClG,GACA;QAAEE,eAAe;IAAK;IAExB,0EAA0E;IAC1E,0EAA0E;IAC1E,0EAA0E;IAC1ExB,uBACEE,oBAAoBuB,KAAK,EACzB;QACEL,KAAK,OAAOC,SAASC,UACnB,AAAC,CAAA,MAAM,MAAM,CAAC,qCAAiC,EAAGI,0BAA0B,GAAGL,SAASC;IAC5F,GACA;QAAEE,eAAe;IAAK;AAE1B;AAEA,wEAAwE;AACxE,0EAA0E;AAC1E,6EAA6E;AAC7E,+DAA+D;AAC/D,SACEG,oCAAoC,EACpCC,yBAAyB,QAEpB,mCAA+B;AACtC,SACEC,mBAAmB,EACnBC,mBAAmB,QAEd,8BAA0B;AAOjC,SACEC,mBAAmB,EACnBC,qBAAqB,EACrBC,0BAA0B,EAC1BC,mBAAmB,QAGd,yBAAqB;AAC5B,SACEC,yBAAyB,EACzBC,mBAAmB,QAGd,8BAA0B;AAEjC,iEAAiE;AACjE,6EAA6E;AAC7E,uEAAuE;AACvE,4DAA4D;AAC5D,SACEC,6BAA6B,EAC7BC,iCAAiC,EACjCC,+BAA+B,QAG1B,yCAAqC;AAC5C,SACEC,uBAAuB,EACvBC,4BAA4B,EAC5BC,sBAAsB,EACtBC,0BAA0B,EAC1BC,8BAA8B,EAC9BC,6BAA6B,QAGxB,oCAAgC"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/outreach/src/lib/server.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 * Outreach's SERVER half (AGL-2974), `@aglyn/plugins-outreach/server`.\n *\n * Every write to an `outreach*` collection is made here: the Firestore rules\n * refuse all of them to clients, so a mailbox, a sequence, an enrollment and a\n * credential only ever change through a handler registered below, behind the\n * session, entitlement and permission checks that handler makes.\n *\n * `outreach/ping` exists so the wiring is proven rather than assumed, as the\n * CRM's does: `plugins.config.json` names `registerOutreachConsoleApi` and the\n * `outreach` prefix, the generated server manifest loads this module, and the\n * console's `/api/[...pluginApi]` dispatcher reaches the handler — after its\n * own gates, which already refuse a request while `release_outreach` is off\n * for the caller. A first real route that also had to be the first wiring\n * test would be two things to debug at once.\n *\n * A route that reads or writes org data resolves the caller the way the CRM's\n * org routes do (`resolveOrgMembership` / `memberHasOrgPermission` from\n * `@aglyn/tenant-data-admin`) and asks `checkEntitlement(org, 'outreach')`\n * and `outreach.use` before it touches a document.\n */\n\nimport {\n registerPluginApiRoute,\n registerPluginPermissions,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { OUTREACH_API_ROUTES } from './constants/api-routes'\nimport {\n OUTREACH_PERMISSIONS,\n OUTREACH_PLUGIN_ID,\n} from './constants/bundle-common'\nimport { registerOutreachMailboxRoutes } from './mailboxes/register-mailbox-routes'\nimport { registerOutreachRoutes } from './routes/register-routes'\n\n/**\n * No auth, no org, no data: it answers whether this server bundle was loaded\n * and its routes registered, which is a fact about the process rather than\n * about any workspace.\n */\nexport const outreachPingHandler: PluginApiHandler = (req, res) => {\n if (req.method !== 'GET') {\n res.setHeader('Allow', 'GET')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n res.status(200).json({ ok: true, plugin: OUTREACH_PLUGIN_ID })\n}\n\n/** Console API registration, named in `plugins.config.json` as `consoleApi`. */\nexport function registerOutreachConsoleApi(): void {\n // The server resolves plugin permission keys from this registry, so a\n // route's `outreach.use` check and the console's gate read the same tier\n // defaults.\n registerPluginPermissions(OUTREACH_PERMISSIONS)\n registerPluginApiRoute(OUTREACH_API_ROUTES.ping, outreachPingHandler)\n // Connect a Google mailbox, its settings, pause, test and disconnect\n // (AGL-2978).\n registerOutreachMailboxRoutes()\n // Compliance settings, sequences, enrollment and enrollment actions\n // (AGL-2980).\n registerOutreachRoutes()\n // The one-click unsubscribe every email carries (AGL-2981): a recipient\n // link, so it answers whether or not Outreach is released to the\n // organization now. Its module loads on the first request.\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.unsubscribe,\n {\n web: async (request, context) =>\n (await import('./runtime/platform-runtime-deps')).platformOutreachUnsubscribeRoute()(request, context),\n },\n { recipientLink: true },\n )\n // A link in a tracked sequence email (AGL-3239): a recipient link for the\n // same reason, and more pressingly — a link already sitting in somebody's\n // inbox has to keep forwarding them whether or not the rollout is paused.\n registerPluginApiRoute(\n OUTREACH_API_ROUTES.click,\n {\n web: async (request, context) =>\n (await import('./runtime/platform-runtime-deps')).platformOutreachClickRoute()(request, context),\n },\n { recipientLink: true },\n )\n}\n\n// The transport the sending runtime reaches a connected mailbox through\n// (AGL-2978): open a mailbox's Gmail client, mark one reconnect-required,\n// send the engine's composed email through the one RFC 5322 writer, and read\n// a thread whole for the engine's reply and bounce classifier.\nexport {\n markOutreachMailboxReconnectRequired,\n openOutreachMailboxClient,\n type OpenedOutreachMailbox,\n} from './mailboxes/mailbox-transport'\nexport {\n GmailTransportError,\n isReconnectRequired,\n type GmailTransportErrorCode,\n} from './transport/gmail-errors'\nexport type {\n GmailClient,\n GmailFullThread,\n GmailMessageMetadata,\n GmailThread,\n} from './transport/gmail-client'\nexport {\n buildRfc5322Message,\n encodeGmailRawMessage,\n LIST_UNSUBSCRIBE_ONE_CLICK,\n Rfc5322MessageError,\n type OutreachComposedMessage,\n type OutreachMailAddress,\n} from './transport/rfc5322'\nexport {\n sendComposedOutreachEmail,\n sendOutreachMessage,\n type OutreachSentMessage,\n type SendOutreachMessageOptions,\n} from './transport/send-message'\n\n// The organization's compliance settings and do-not-contact list\n// (AGL-2980), for the sending runtime: the footer is read before every send,\n// the list is checked before every send, and a reply that opts out, an\n// unsubscribe and a hard bounce each put the address on it.\nexport {\n outreachComplianceSettingsRef,\n readOutreachComplianceSettingsDoc,\n writeOutreachComplianceSettings,\n type WriteOutreachComplianceSettingsInput,\n type WriteOutreachComplianceSettingsResult,\n} from './storage/compliance-settings-store'\nexport {\n addOutreachDoNotContact,\n addOutreachDoNotContactDomain,\n getOutreachDoNotContactEntry,\n isOutreachDoNotContact,\n listOutreachDoNotContactDomains,\n lookupOutreachDoNotContact,\n lookupOutreachDoNotContactDomains,\n outreachDoNotContactCollection,\n outreachDoNotContactDomainCollection,\n readOutreachDoNotContactDomainEntry,\n readOutreachDoNotContactEntry,\n removeOutreachDoNotContactDomain,\n type AddOutreachDoNotContactDomainInput,\n type AddOutreachDoNotContactDomainResult,\n type AddOutreachDoNotContactInput,\n type AddOutreachDoNotContactResult,\n} from './storage/do-not-contact-store'\n"],"names":["registerPluginApiRoute","registerPluginPermissions","OUTREACH_API_ROUTES","OUTREACH_PERMISSIONS","OUTREACH_PLUGIN_ID","registerOutreachMailboxRoutes","registerOutreachRoutes","outreachPingHandler","req","res","method","setHeader","status","json","error","ok","plugin","registerOutreachConsoleApi","ping","unsubscribe","web","request","context","platformOutreachUnsubscribeRoute","recipientLink","click","platformOutreachClickRoute","markOutreachMailboxReconnectRequired","openOutreachMailboxClient","GmailTransportError","isReconnectRequired","buildRfc5322Message","encodeGmailRawMessage","LIST_UNSUBSCRIBE_ONE_CLICK","Rfc5322MessageError","sendComposedOutreachEmail","sendOutreachMessage","outreachComplianceSettingsRef","readOutreachComplianceSettingsDoc","writeOutreachComplianceSettings","addOutreachDoNotContact","addOutreachDoNotContactDomain","getOutreachDoNotContactEntry","isOutreachDoNotContact","listOutreachDoNotContactDomains","lookupOutreachDoNotContact","lookupOutreachDoNotContactDomains","outreachDoNotContactCollection","outreachDoNotContactDomainCollection","readOutreachDoNotContactDomainEntry","readOutreachDoNotContactEntry","removeOutreachDoNotContactDomain"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;CAoBC,GAED,SACEA,sBAAsB,EACtBC,yBAAyB,QAEpB,sBAAqB;AAC5B,SAASC,mBAAmB,QAAQ,4BAAwB;AAC5D,SACEC,oBAAoB,EACpBC,kBAAkB,QACb,+BAA2B;AAClC,SAASC,6BAA6B,QAAQ,yCAAqC;AACnF,SAASC,sBAAsB,QAAQ,8BAA0B;AAEjE;;;;CAIC,GACD,OAAO,MAAMC,sBAAwC,CAACC,KAAKC;IACzD,IAAID,IAAIE,MAAM,KAAK,OAAO;QACxBD,IAAIE,SAAS,CAAC,SAAS;QACvBF,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;QACnD;IACF;IACAL,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEE,IAAI;QAAMC,QAAQZ;IAAmB;AAC9D,EAAC;AAED,8EAA8E,GAC9E,OAAO,SAASa;IACd,sEAAsE;IACtE,yEAAyE;IACzE,YAAY;IACZhB,0BAA0BE;IAC1BH,uBAAuBE,oBAAoBgB,IAAI,EAAEX;IACjD,qEAAqE;IACrE,cAAc;IACdF;IACA,oEAAoE;IACpE,cAAc;IACdC;IACA,wEAAwE;IACxE,iEAAiE;IACjE,2DAA2D;IAC3DN,uBACEE,oBAAoBiB,WAAW,EAC/B;QACEC,KAAK,OAAOC,SAASC,UACnB,AAAC,CAAA,MAAM,MAAM,CAAC,qCAAiC,EAAGC,gCAAgC,GAAGF,SAASC;IAClG,GACA;QAAEE,eAAe;IAAK;IAExB,0EAA0E;IAC1E,0EAA0E;IAC1E,0EAA0E;IAC1ExB,uBACEE,oBAAoBuB,KAAK,EACzB;QACEL,KAAK,OAAOC,SAASC,UACnB,AAAC,CAAA,MAAM,MAAM,CAAC,qCAAiC,EAAGI,0BAA0B,GAAGL,SAASC;IAC5F,GACA;QAAEE,eAAe;IAAK;AAE1B;AAEA,wEAAwE;AACxE,0EAA0E;AAC1E,6EAA6E;AAC7E,+DAA+D;AAC/D,SACEG,oCAAoC,EACpCC,yBAAyB,QAEpB,mCAA+B;AACtC,SACEC,mBAAmB,EACnBC,mBAAmB,QAEd,8BAA0B;AAOjC,SACEC,mBAAmB,EACnBC,qBAAqB,EACrBC,0BAA0B,EAC1BC,mBAAmB,QAGd,yBAAqB;AAC5B,SACEC,yBAAyB,EACzBC,mBAAmB,QAGd,8BAA0B;AAEjC,iEAAiE;AACjE,6EAA6E;AAC7E,uEAAuE;AACvE,4DAA4D;AAC5D,SACEC,6BAA6B,EAC7BC,iCAAiC,EACjCC,+BAA+B,QAG1B,yCAAqC;AAC5C,SACEC,uBAAuB,EACvBC,6BAA6B,EAC7BC,4BAA4B,EAC5BC,sBAAsB,EACtBC,+BAA+B,EAC/BC,0BAA0B,EAC1BC,iCAAiC,EACjCC,8BAA8B,EAC9BC,oCAAoC,EACpCC,mCAAmC,EACnCC,6BAA6B,EAC7BC,gCAAgC,QAK3B,oCAAgC"}
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { type OutreachDoNotContactEntry, type OutreachDoNotContactReason, type OutreachDoNotContactSource } from '../model/outreach.types';
17
+ import { type OutreachDoNotContactDomainEntry, type OutreachDoNotContactEntry, type OutreachDoNotContactReason, type OutreachDoNotContactSource } from '../model/outreach.types';
18
18
  /** The longest detail an entry keeps. */
19
19
  export declare const OUTREACH_DO_NOT_CONTACT_DETAIL_MAX = 500;
20
20
  /** `orgs/{orgId}/outreachDoNotContact`. */
@@ -58,3 +58,30 @@ export interface AddOutreachDoNotContactResult {
58
58
  * anything.
59
59
  */
60
60
  export declare function addOutreachDoNotContact(firestore: FirebaseFirestore.Firestore, input: AddOutreachDoNotContactInput): Promise<AddOutreachDoNotContactResult>;
61
+ /** `orgs/{orgId}/outreachDoNotContactDomains`. */
62
+ export declare function outreachDoNotContactDomainCollection(firestore: FirebaseFirestore.Firestore, orgId: string): FirebaseFirestore.CollectionReference;
63
+ /** A stored domain entry held to its shape, or `null` for a document that is not one. */
64
+ export declare function readOutreachDoNotContactDomainEntry(domain: string, data: Record<string, unknown> | undefined): OutreachDoNotContactDomainEntry | null;
65
+ /**
66
+ * Whether each address's DOMAIN is on the list, in one round trip, keyed by
67
+ * the address exactly as given: `true` or `false` per address, `null` for
68
+ * an address whose domain cannot be read off it and for every address when
69
+ * the read failed.
70
+ */
71
+ export declare function lookupOutreachDoNotContactDomains(firestore: FirebaseFirestore.Firestore, orgId: string, emails: readonly string[]): Promise<Map<string, boolean | null>>;
72
+ /** Every domain on the list, alphabetically. */
73
+ export declare function listOutreachDoNotContactDomains(firestore: FirebaseFirestore.Firestore, orgId: string): Promise<OutreachDoNotContactDomainEntry[]>;
74
+ export interface AddOutreachDoNotContactDomainInput extends Omit<AddOutreachDoNotContactInput, 'email'> {
75
+ /** The domain, or an address at it; spelled by `normalizeOutreachDomain`. */
76
+ domain: string;
77
+ }
78
+ export interface AddOutreachDoNotContactDomainResult {
79
+ domain: string;
80
+ /** False when the domain was on the list already; its entry is unchanged. */
81
+ created: boolean;
82
+ entry: OutreachDoNotContactDomainEntry;
83
+ }
84
+ /** Puts a domain on the list; a second add changes nothing. Throws for a value that is not a domain. */
85
+ export declare function addOutreachDoNotContactDomain(firestore: FirebaseFirestore.Firestore, input: AddOutreachDoNotContactDomainInput): Promise<AddOutreachDoNotContactDomainResult>;
86
+ /** Takes a domain off the list; answers whether it was on it. */
87
+ export declare function removeOutreachDoNotContactDomain(firestore: FirebaseFirestore.Firestore, orgId: string, value: string): Promise<boolean>;
@@ -1,3 +1,5 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
1
3
  /**
2
4
  * @license
3
5
  * Copyright 2026 Aglyn LLC
@@ -15,7 +17,10 @@
15
17
  * limitations under the License.
16
18
  */ /**
17
19
  * The organization's Outreach do-not-contact list (AGL-2980):
18
- * `orgs/{orgId}/outreachDoNotContact/{key}`, one document per address.
20
+ * `orgs/{orgId}/outreachDoNotContact/{key}`, one document per address —
21
+ * and, beside it, `orgs/{orgId}/outreachDoNotContactDomains/{domain}`, one
22
+ * document per DOMAIN (AGL-3244), which the same callers read: an address
23
+ * is refused when it, or the domain it is at, is listed.
19
24
  *
20
25
  * SERVER ONLY — the key hashes with `node:crypto`. Two kinds of caller, and
21
26
  * the API is shaped for both:
@@ -44,6 +49,7 @@
44
49
  * that atomic without a transaction: two adds racing for one address cannot
45
50
  * both write.
46
51
  */ import { outreachDoNotContactKey } from "../engine/do-not-contact.js";
52
+ import { normalizeOutreachDomain, outreachEmailDomain } from "../engine/do-not-contact-domain.js";
47
53
  import { OUTREACH_COLLECTIONS, OUTREACH_DO_NOT_CONTACT_REASONS, OUTREACH_DO_NOT_CONTACT_SOURCES } from "../model/outreach.types.js";
48
54
  /** The longest detail an entry keeps. */ export const OUTREACH_DO_NOT_CONTACT_DETAIL_MAX = 500;
49
55
  /** `getAll` takes this many references at most per call. */ const GET_ALL_CHUNK = 300;
@@ -121,9 +127,36 @@ import { OUTREACH_COLLECTIONS, OUTREACH_DO_NOT_CONTACT_REASONS, OUTREACH_DO_NOT_
121
127
  * a list that could hold an entry nobody can explain is not evidence of
122
128
  * anything.
123
129
  */ export async function addOutreachDoNotContact(firestore, input) {
124
- var _input_detail;
125
130
  const key = outreachDoNotContactKey(input.email);
126
131
  if (!key) throw new Error('[outreach] cannot key a do-not-contact entry for that value');
132
+ const entry = _extends({
133
+ key
134
+ }, entryFields(input));
135
+ const ref = outreachDoNotContactCollection(firestore, input.orgId).doc(key);
136
+ try {
137
+ await ref.create(entry);
138
+ return {
139
+ key,
140
+ created: true,
141
+ entry
142
+ };
143
+ } catch (error) {
144
+ var _readOutreachDoNotContactEntry;
145
+ if ((error == null ? void 0 : error.code) !== ALREADY_EXISTS) throw error;
146
+ const existing = await ref.get();
147
+ return {
148
+ key,
149
+ created: false,
150
+ entry: (_readOutreachDoNotContactEntry = readOutreachDoNotContactEntry(key, existing.data())) != null ? _readOutreachDoNotContactEntry : entry
151
+ };
152
+ }
153
+ }
154
+ /**
155
+ * The fields an address entry and a domain entry share, held to the rules
156
+ * the module note gives: a reason and a source the list knows, and a member
157
+ * behind every member-sourced add.
158
+ */ function entryFields(input) {
159
+ var _input_detail;
127
160
  if (!OUTREACH_DO_NOT_CONTACT_REASONS.includes(input.reason)) {
128
161
  throw new Error(`[outreach] "${String(input.reason)}" is not a do-not-contact reason`);
129
162
  }
@@ -135,8 +168,7 @@ import { OUTREACH_COLLECTIONS, OUTREACH_DO_NOT_CONTACT_REASONS, OUTREACH_DO_NOT_
135
168
  throw new Error('[outreach] a member-added do-not-contact entry names the member');
136
169
  }
137
170
  const detail = String((_input_detail = input.detail) != null ? _input_detail : '').replace(/\s+/g, ' ').trim();
138
- const entry = {
139
- key,
171
+ return {
140
172
  reason: input.reason,
141
173
  source: input.source,
142
174
  addedByUid,
@@ -145,24 +177,100 @@ import { OUTREACH_COLLECTIONS, OUTREACH_DO_NOT_CONTACT_REASONS, OUTREACH_DO_NOT_
145
177
  sequenceId: input.sequenceId || null,
146
178
  detail: detail ? detail.slice(0, OUTREACH_DO_NOT_CONTACT_DETAIL_MAX) : null
147
179
  };
148
- const ref = outreachDoNotContactCollection(firestore, input.orgId).doc(key);
180
+ }
181
+ /*==========================================
182
+ * DOMAINS (AGL-3244).
183
+ *
184
+ * `orgs/{orgId}/outreachDoNotContactDomains/{domain}`, the domain itself
185
+ * the id — it names a company, not a person, so it is stored in clear and
186
+ * the Compliance page lists it. The same three rules hold: a read that
187
+ * failed is `null`, an entry is written once, and every add is explained.
188
+ *==========================================*/ /** `orgs/{orgId}/outreachDoNotContactDomains`. */ export function outreachDoNotContactDomainCollection(firestore, orgId) {
189
+ return firestore.collection('orgs').doc(orgId).collection(OUTREACH_COLLECTIONS.doNotContactDomains);
190
+ }
191
+ /** A stored domain entry held to its shape, or `null` for a document that is not one. */ export function readOutreachDoNotContactDomainEntry(domain, data) {
192
+ var _normalizeOutreachDomain;
193
+ const entry = readOutreachDoNotContactEntry(domain, data);
194
+ if (!entry) return null;
195
+ const { key } = entry, rest = _object_without_properties_loose(entry, [
196
+ "key"
197
+ ]);
198
+ return _extends({
199
+ domain: (_normalizeOutreachDomain = normalizeOutreachDomain(data == null ? void 0 : data['domain'])) != null ? _normalizeOutreachDomain : key
200
+ }, rest);
201
+ }
202
+ /**
203
+ * Whether each address's DOMAIN is on the list, in one round trip, keyed by
204
+ * the address exactly as given: `true` or `false` per address, `null` for
205
+ * an address whose domain cannot be read off it and for every address when
206
+ * the read failed.
207
+ */ export async function lookupOutreachDoNotContactDomains(firestore, orgId, emails) {
208
+ const answers = new Map();
209
+ const domainOf = new Map();
210
+ for (const email of emails){
211
+ const domain = outreachEmailDomain(email);
212
+ if (domain) domainOf.set(email, domain);
213
+ else answers.set(email, null);
214
+ }
215
+ const domains = [
216
+ ...new Set(domainOf.values())
217
+ ];
218
+ if (!domains.length) return answers;
219
+ const list = outreachDoNotContactDomainCollection(firestore, orgId);
220
+ const listed = new Map();
221
+ try {
222
+ var _listed_get;
223
+ for(let start = 0; start < domains.length; start += GET_ALL_CHUNK){
224
+ const chunk = domains.slice(start, start + GET_ALL_CHUNK);
225
+ const snapshots = await firestore.getAll(...chunk.map((domain)=>list.doc(domain)));
226
+ chunk.forEach((domain, index)=>{
227
+ var _snapshots_index;
228
+ return listed.set(domain, ((_snapshots_index = snapshots[index]) == null ? void 0 : _snapshots_index.exists) === true);
229
+ });
230
+ }
231
+ for (const [email, domain] of domainOf)answers.set(email, (_listed_get = listed.get(domain)) != null ? _listed_get : null);
232
+ } catch (error) {
233
+ console.error('[outreach] do-not-contact domain lookup failed; reading as unchecked', error);
234
+ for (const email of domainOf.keys())answers.set(email, null);
235
+ }
236
+ return answers;
237
+ }
238
+ /** Every domain on the list, alphabetically. */ export async function listOutreachDoNotContactDomains(firestore, orgId) {
239
+ const snapshot = await outreachDoNotContactDomainCollection(firestore, orgId).get();
240
+ return snapshot.docs.map((doc)=>readOutreachDoNotContactDomainEntry(doc.id, doc.data())).filter((entry)=>entry !== null).sort((a, b)=>a.domain.localeCompare(b.domain));
241
+ }
242
+ /** Puts a domain on the list; a second add changes nothing. Throws for a value that is not a domain. */ export async function addOutreachDoNotContactDomain(firestore, input) {
243
+ const domain = normalizeOutreachDomain(input.domain);
244
+ if (!domain) throw new Error('[outreach] cannot key a do-not-contact entry for a value that is not a domain');
245
+ const entry = _extends({
246
+ domain
247
+ }, entryFields(input));
248
+ const ref = outreachDoNotContactDomainCollection(firestore, input.orgId).doc(domain);
149
249
  try {
150
250
  await ref.create(entry);
151
251
  return {
152
- key,
252
+ domain,
153
253
  created: true,
154
254
  entry
155
255
  };
156
256
  } catch (error) {
157
- var _readOutreachDoNotContactEntry;
257
+ var _readOutreachDoNotContactDomainEntry;
158
258
  if ((error == null ? void 0 : error.code) !== ALREADY_EXISTS) throw error;
159
259
  const existing = await ref.get();
160
260
  return {
161
- key,
261
+ domain,
162
262
  created: false,
163
- entry: (_readOutreachDoNotContactEntry = readOutreachDoNotContactEntry(key, existing.data())) != null ? _readOutreachDoNotContactEntry : entry
263
+ entry: (_readOutreachDoNotContactDomainEntry = readOutreachDoNotContactDomainEntry(domain, existing.data())) != null ? _readOutreachDoNotContactDomainEntry : entry
164
264
  };
165
265
  }
166
266
  }
267
+ /** Takes a domain off the list; answers whether it was on it. */ export async function removeOutreachDoNotContactDomain(firestore, orgId, value) {
268
+ const domain = normalizeOutreachDomain(value);
269
+ if (!domain) return false;
270
+ const ref = outreachDoNotContactDomainCollection(firestore, orgId).doc(domain);
271
+ if (!(await ref.get()).exists) return false;
272
+ await ref.delete();
273
+ return true;
274
+ }
167
275
 
168
276
  //# sourceMappingURL=do-not-contact-store.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/storage/do-not-contact-store.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The organization's Outreach do-not-contact list (AGL-2980):\n * `orgs/{orgId}/outreachDoNotContact/{key}`, one document per address.\n *\n * SERVER ONLY — the key hashes with `node:crypto`. Two kinds of caller, and\n * the API is shaped for both:\n *\n * - The console's routes: the enrollment gates read the list for every\n * person being enrolled at once ({@link lookupOutreachDoNotContact}, one\n * `getAll`), and \"Mark do-not-contact\" on an enrollment adds an entry with\n * the member as its author.\n * - The sending runtime: it re-reads the list for each person before each\n * send ({@link isOutreachDoNotContact}), and adds an entry when a reply\n * opts out, when the unsubscribe link is used and when an address bounces\n * for good, with `source: 'runtime'` and no member.\n *\n * ## Reads answer `null` when they could not be made\n *\n * `null` is not `false`. The gates read a `null` lookup as \"we couldn't\n * check\" and refuse the person, which is the only safe reading of a list\n * whose whole job is to stop mail: a person who asked not to be emailed must\n * not be emailed because a read timed out.\n *\n * ## An entry is written once\n *\n * The first reason an address was added is the record of why it is there;\n * a later add for the same address — an unsubscribe after a member already\n * marked it — finds it on the list and changes nothing. `create()` makes\n * that atomic without a transaction: two adds racing for one address cannot\n * both write.\n */\n\nimport { outreachDoNotContactKey } from '../engine/do-not-contact'\nimport {\n OUTREACH_COLLECTIONS,\n OUTREACH_DO_NOT_CONTACT_REASONS,\n OUTREACH_DO_NOT_CONTACT_SOURCES,\n type OutreachDoNotContactEntry,\n type OutreachDoNotContactReason,\n type OutreachDoNotContactSource,\n} from '../model/outreach.types'\n\n/** The longest detail an entry keeps. */\nexport const OUTREACH_DO_NOT_CONTACT_DETAIL_MAX = 500\n\n/** `getAll` takes this many references at most per call. */\nconst GET_ALL_CHUNK = 300\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\n/** `orgs/{orgId}/outreachDoNotContact`. */\nexport function outreachDoNotContactCollection(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): FirebaseFirestore.CollectionReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(OUTREACH_COLLECTIONS.doNotContact)\n}\n\n/** A stored entry held to its shape, or `null` for a document that is not one. */\nexport function readOutreachDoNotContactEntry(\n key: string,\n data: Record<string, unknown> | undefined,\n): OutreachDoNotContactEntry | null {\n if (!data) return null\n const reason = data['reason']\n const source = data['source']\n const text = (value: unknown): string | null =>\n typeof value === 'string' && value ? value : null\n const addedAtMs = Number(data['addedAtMs'])\n return {\n key,\n // An entry some other writer produced still keeps its address off the\n // list's sends; only how it reads is defaulted.\n reason: (OUTREACH_DO_NOT_CONTACT_REASONS as readonly unknown[]).includes(reason)\n ? (reason as OutreachDoNotContactReason)\n : 'manual',\n source: (OUTREACH_DO_NOT_CONTACT_SOURCES as readonly unknown[]).includes(source)\n ? (source as OutreachDoNotContactSource)\n : 'member',\n addedByUid: text(data['addedByUid']),\n addedAtMs: Number.isFinite(addedAtMs) && addedAtMs > 0 ? addedAtMs : 0,\n enrollmentId: text(data['enrollmentId']),\n sequenceId: text(data['sequenceId']),\n detail: text(data['detail']),\n }\n}\n\n/**\n * Whether each address is on the list, in one round trip: `true` or `false`\n * per address, or `null` for an address that cannot be keyed and for every\n * address when the read failed. Keyed by the address exactly as given.\n */\nexport async function lookupOutreachDoNotContact(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n emails: readonly string[],\n): Promise<Map<string, boolean | null>> {\n const answers = new Map<string, boolean | null>()\n const keyed: Array<{ email: string; key: string }> = []\n for (const email of emails) {\n const key = outreachDoNotContactKey(email)\n if (key) keyed.push({ email, key })\n else answers.set(email, null)\n }\n if (!keyed.length) return answers\n const list = outreachDoNotContactCollection(firestore, orgId)\n try {\n for (let start = 0; start < keyed.length; start += GET_ALL_CHUNK) {\n const chunk = keyed.slice(start, start + GET_ALL_CHUNK)\n const snapshots = await firestore.getAll(...chunk.map((entry) => list.doc(entry.key)))\n chunk.forEach((entry, index) => {\n answers.set(entry.email, snapshots[index]?.exists === true)\n })\n }\n } catch (error) {\n console.error('[outreach] do-not-contact lookup failed; reading as unchecked', error)\n for (const entry of keyed) answers.set(entry.email, null)\n }\n return answers\n}\n\n/** Whether one address is on the list; `null` when that could not be told. */\nexport async function isOutreachDoNotContact(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n email: string,\n): Promise<boolean | null> {\n return (await lookupOutreachDoNotContact(firestore, orgId, [email])).get(email) ?? null\n}\n\n/** The entry an address has, or `null` when it has none. */\nexport async function getOutreachDoNotContactEntry(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n email: string,\n): Promise<OutreachDoNotContactEntry | null> {\n const key = outreachDoNotContactKey(email)\n if (!key) return null\n const snapshot = await outreachDoNotContactCollection(firestore, orgId).doc(key).get()\n return snapshot.exists ? readOutreachDoNotContactEntry(key, snapshot.data()) : null\n}\n\nexport interface AddOutreachDoNotContactInput {\n orgId: string\n email: string\n reason: OutreachDoNotContactReason\n source: OutreachDoNotContactSource\n /** The member adding it; omit or `null` for the runtime. */\n addedByUid?: string | null\n nowMs: number\n enrollmentId?: string | null\n sequenceId?: string | null\n detail?: string | null\n}\n\nexport interface AddOutreachDoNotContactResult {\n key: string\n /** False when the address was on the list already; its entry is unchanged. */\n created: boolean\n /** The entry as it stands: the new one, or the one that was already there. */\n entry: OutreachDoNotContactEntry\n}\n\n/**\n * Puts an address on the list — see the module note for why a second add\n * changes nothing. Throws for a value that is not an address, a reason or a\n * source the list does not know, and a member-sourced add with no member:\n * a list that could hold an entry nobody can explain is not evidence of\n * anything.\n */\nexport async function addOutreachDoNotContact(\n firestore: FirebaseFirestore.Firestore,\n input: AddOutreachDoNotContactInput,\n): Promise<AddOutreachDoNotContactResult> {\n const key = outreachDoNotContactKey(input.email)\n if (!key) throw new Error('[outreach] cannot key a do-not-contact entry for that value')\n if (!(OUTREACH_DO_NOT_CONTACT_REASONS as readonly string[]).includes(input.reason)) {\n throw new Error(`[outreach] \"${String(input.reason)}\" is not a do-not-contact reason`)\n }\n if (!(OUTREACH_DO_NOT_CONTACT_SOURCES as readonly string[]).includes(input.source)) {\n throw new Error(`[outreach] \"${String(input.source)}\" is not a do-not-contact source`)\n }\n const addedByUid = typeof input.addedByUid === 'string' && input.addedByUid ? input.addedByUid : null\n if (input.source === 'member' && !addedByUid) {\n throw new Error('[outreach] a member-added do-not-contact entry names the member')\n }\n const detail = String(input.detail ?? '').replace(/\\s+/g, ' ').trim()\n const entry: OutreachDoNotContactEntry = {\n key,\n reason: input.reason,\n source: input.source,\n addedByUid,\n addedAtMs: input.nowMs,\n enrollmentId: input.enrollmentId || null,\n sequenceId: input.sequenceId || null,\n detail: detail ? detail.slice(0, OUTREACH_DO_NOT_CONTACT_DETAIL_MAX) : null,\n }\n const ref = outreachDoNotContactCollection(firestore, input.orgId).doc(key)\n try {\n await ref.create(entry)\n return { key, created: true, entry }\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n const existing = await ref.get()\n return {\n key,\n created: false,\n entry: readOutreachDoNotContactEntry(key, existing.data()) ?? entry,\n }\n }\n}\n"],"names":["outreachDoNotContactKey","OUTREACH_COLLECTIONS","OUTREACH_DO_NOT_CONTACT_REASONS","OUTREACH_DO_NOT_CONTACT_SOURCES","OUTREACH_DO_NOT_CONTACT_DETAIL_MAX","GET_ALL_CHUNK","ALREADY_EXISTS","outreachDoNotContactCollection","firestore","orgId","collection","doc","doNotContact","readOutreachDoNotContactEntry","key","data","reason","source","text","value","addedAtMs","Number","includes","addedByUid","isFinite","enrollmentId","sequenceId","detail","lookupOutreachDoNotContact","emails","answers","Map","keyed","email","push","set","length","list","start","chunk","slice","snapshots","getAll","map","entry","forEach","index","exists","error","console","isOutreachDoNotContact","get","getOutreachDoNotContactEntry","snapshot","addOutreachDoNotContact","input","Error","String","replace","trim","nowMs","ref","create","created","code","existing"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/storage/do-not-contact-store.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The organization's Outreach do-not-contact list (AGL-2980):\n * `orgs/{orgId}/outreachDoNotContact/{key}`, one document per address —\n * and, beside it, `orgs/{orgId}/outreachDoNotContactDomains/{domain}`, one\n * document per DOMAIN (AGL-3244), which the same callers read: an address\n * is refused when it, or the domain it is at, is listed.\n *\n * SERVER ONLY — the key hashes with `node:crypto`. Two kinds of caller, and\n * the API is shaped for both:\n *\n * - The console's routes: the enrollment gates read the list for every\n * person being enrolled at once ({@link lookupOutreachDoNotContact}, one\n * `getAll`), and \"Mark do-not-contact\" on an enrollment adds an entry with\n * the member as its author.\n * - The sending runtime: it re-reads the list for each person before each\n * send ({@link isOutreachDoNotContact}), and adds an entry when a reply\n * opts out, when the unsubscribe link is used and when an address bounces\n * for good, with `source: 'runtime'` and no member.\n *\n * ## Reads answer `null` when they could not be made\n *\n * `null` is not `false`. The gates read a `null` lookup as \"we couldn't\n * check\" and refuse the person, which is the only safe reading of a list\n * whose whole job is to stop mail: a person who asked not to be emailed must\n * not be emailed because a read timed out.\n *\n * ## An entry is written once\n *\n * The first reason an address was added is the record of why it is there;\n * a later add for the same address — an unsubscribe after a member already\n * marked it — finds it on the list and changes nothing. `create()` makes\n * that atomic without a transaction: two adds racing for one address cannot\n * both write.\n */\n\nimport { outreachDoNotContactKey } from '../engine/do-not-contact'\nimport { normalizeOutreachDomain, outreachEmailDomain } from '../engine/do-not-contact-domain'\nimport {\n OUTREACH_COLLECTIONS,\n OUTREACH_DO_NOT_CONTACT_REASONS,\n OUTREACH_DO_NOT_CONTACT_SOURCES,\n type OutreachDoNotContactDomainEntry,\n type OutreachDoNotContactEntry,\n type OutreachDoNotContactReason,\n type OutreachDoNotContactSource,\n} from '../model/outreach.types'\n\n/** The longest detail an entry keeps. */\nexport const OUTREACH_DO_NOT_CONTACT_DETAIL_MAX = 500\n\n/** `getAll` takes this many references at most per call. */\nconst GET_ALL_CHUNK = 300\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\n/** `orgs/{orgId}/outreachDoNotContact`. */\nexport function outreachDoNotContactCollection(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): FirebaseFirestore.CollectionReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(OUTREACH_COLLECTIONS.doNotContact)\n}\n\n/** A stored entry held to its shape, or `null` for a document that is not one. */\nexport function readOutreachDoNotContactEntry(\n key: string,\n data: Record<string, unknown> | undefined,\n): OutreachDoNotContactEntry | null {\n if (!data) return null\n const reason = data['reason']\n const source = data['source']\n const text = (value: unknown): string | null =>\n typeof value === 'string' && value ? value : null\n const addedAtMs = Number(data['addedAtMs'])\n return {\n key,\n // An entry some other writer produced still keeps its address off the\n // list's sends; only how it reads is defaulted.\n reason: (OUTREACH_DO_NOT_CONTACT_REASONS as readonly unknown[]).includes(reason)\n ? (reason as OutreachDoNotContactReason)\n : 'manual',\n source: (OUTREACH_DO_NOT_CONTACT_SOURCES as readonly unknown[]).includes(source)\n ? (source as OutreachDoNotContactSource)\n : 'member',\n addedByUid: text(data['addedByUid']),\n addedAtMs: Number.isFinite(addedAtMs) && addedAtMs > 0 ? addedAtMs : 0,\n enrollmentId: text(data['enrollmentId']),\n sequenceId: text(data['sequenceId']),\n detail: text(data['detail']),\n }\n}\n\n/**\n * Whether each address is on the list, in one round trip: `true` or `false`\n * per address, or `null` for an address that cannot be keyed and for every\n * address when the read failed. Keyed by the address exactly as given.\n */\nexport async function lookupOutreachDoNotContact(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n emails: readonly string[],\n): Promise<Map<string, boolean | null>> {\n const answers = new Map<string, boolean | null>()\n const keyed: Array<{ email: string; key: string }> = []\n for (const email of emails) {\n const key = outreachDoNotContactKey(email)\n if (key) keyed.push({ email, key })\n else answers.set(email, null)\n }\n if (!keyed.length) return answers\n const list = outreachDoNotContactCollection(firestore, orgId)\n try {\n for (let start = 0; start < keyed.length; start += GET_ALL_CHUNK) {\n const chunk = keyed.slice(start, start + GET_ALL_CHUNK)\n const snapshots = await firestore.getAll(...chunk.map((entry) => list.doc(entry.key)))\n chunk.forEach((entry, index) => {\n answers.set(entry.email, snapshots[index]?.exists === true)\n })\n }\n } catch (error) {\n console.error('[outreach] do-not-contact lookup failed; reading as unchecked', error)\n for (const entry of keyed) answers.set(entry.email, null)\n }\n return answers\n}\n\n/** Whether one address is on the list; `null` when that could not be told. */\nexport async function isOutreachDoNotContact(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n email: string,\n): Promise<boolean | null> {\n return (await lookupOutreachDoNotContact(firestore, orgId, [email])).get(email) ?? null\n}\n\n/** The entry an address has, or `null` when it has none. */\nexport async function getOutreachDoNotContactEntry(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n email: string,\n): Promise<OutreachDoNotContactEntry | null> {\n const key = outreachDoNotContactKey(email)\n if (!key) return null\n const snapshot = await outreachDoNotContactCollection(firestore, orgId).doc(key).get()\n return snapshot.exists ? readOutreachDoNotContactEntry(key, snapshot.data()) : null\n}\n\nexport interface AddOutreachDoNotContactInput {\n orgId: string\n email: string\n reason: OutreachDoNotContactReason\n source: OutreachDoNotContactSource\n /** The member adding it; omit or `null` for the runtime. */\n addedByUid?: string | null\n nowMs: number\n enrollmentId?: string | null\n sequenceId?: string | null\n detail?: string | null\n}\n\nexport interface AddOutreachDoNotContactResult {\n key: string\n /** False when the address was on the list already; its entry is unchanged. */\n created: boolean\n /** The entry as it stands: the new one, or the one that was already there. */\n entry: OutreachDoNotContactEntry\n}\n\n/**\n * Puts an address on the list — see the module note for why a second add\n * changes nothing. Throws for a value that is not an address, a reason or a\n * source the list does not know, and a member-sourced add with no member:\n * a list that could hold an entry nobody can explain is not evidence of\n * anything.\n */\nexport async function addOutreachDoNotContact(\n firestore: FirebaseFirestore.Firestore,\n input: AddOutreachDoNotContactInput,\n): Promise<AddOutreachDoNotContactResult> {\n const key = outreachDoNotContactKey(input.email)\n if (!key) throw new Error('[outreach] cannot key a do-not-contact entry for that value')\n const entry: OutreachDoNotContactEntry = { key, ...entryFields(input) }\n const ref = outreachDoNotContactCollection(firestore, input.orgId).doc(key)\n try {\n await ref.create(entry)\n return { key, created: true, entry }\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n const existing = await ref.get()\n return {\n key,\n created: false,\n entry: readOutreachDoNotContactEntry(key, existing.data()) ?? entry,\n }\n }\n}\n\n/**\n * The fields an address entry and a domain entry share, held to the rules\n * the module note gives: a reason and a source the list knows, and a member\n * behind every member-sourced add.\n */\nfunction entryFields(\n input: Omit<AddOutreachDoNotContactInput, 'orgId' | 'email'>,\n): Omit<OutreachDoNotContactEntry, 'key'> {\n if (!(OUTREACH_DO_NOT_CONTACT_REASONS as readonly string[]).includes(input.reason)) {\n throw new Error(`[outreach] \"${String(input.reason)}\" is not a do-not-contact reason`)\n }\n if (!(OUTREACH_DO_NOT_CONTACT_SOURCES as readonly string[]).includes(input.source)) {\n throw new Error(`[outreach] \"${String(input.source)}\" is not a do-not-contact source`)\n }\n const addedByUid = typeof input.addedByUid === 'string' && input.addedByUid ? input.addedByUid : null\n if (input.source === 'member' && !addedByUid) {\n throw new Error('[outreach] a member-added do-not-contact entry names the member')\n }\n const detail = String(input.detail ?? '').replace(/\\s+/g, ' ').trim()\n return {\n reason: input.reason,\n source: input.source,\n addedByUid,\n addedAtMs: input.nowMs,\n enrollmentId: input.enrollmentId || null,\n sequenceId: input.sequenceId || null,\n detail: detail ? detail.slice(0, OUTREACH_DO_NOT_CONTACT_DETAIL_MAX) : null,\n }\n}\n\n/*==========================================\n * DOMAINS (AGL-3244).\n *\n * `orgs/{orgId}/outreachDoNotContactDomains/{domain}`, the domain itself\n * the id — it names a company, not a person, so it is stored in clear and\n * the Compliance page lists it. The same three rules hold: a read that\n * failed is `null`, an entry is written once, and every add is explained.\n *==========================================*/\n\n/** `orgs/{orgId}/outreachDoNotContactDomains`. */\nexport function outreachDoNotContactDomainCollection(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): FirebaseFirestore.CollectionReference {\n return firestore\n .collection('orgs')\n .doc(orgId)\n .collection(OUTREACH_COLLECTIONS.doNotContactDomains)\n}\n\n/** A stored domain entry held to its shape, or `null` for a document that is not one. */\nexport function readOutreachDoNotContactDomainEntry(\n domain: string,\n data: Record<string, unknown> | undefined,\n): OutreachDoNotContactDomainEntry | null {\n const entry = readOutreachDoNotContactEntry(domain, data)\n if (!entry) return null\n const { key, ...rest } = entry\n return { domain: normalizeOutreachDomain(data?.['domain']) ?? key, ...rest }\n}\n\n/**\n * Whether each address's DOMAIN is on the list, in one round trip, keyed by\n * the address exactly as given: `true` or `false` per address, `null` for\n * an address whose domain cannot be read off it and for every address when\n * the read failed.\n */\nexport async function lookupOutreachDoNotContactDomains(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n emails: readonly string[],\n): Promise<Map<string, boolean | null>> {\n const answers = new Map<string, boolean | null>()\n const domainOf = new Map<string, string>()\n for (const email of emails) {\n const domain = outreachEmailDomain(email)\n if (domain) domainOf.set(email, domain)\n else answers.set(email, null)\n }\n const domains = [...new Set(domainOf.values())]\n if (!domains.length) return answers\n const list = outreachDoNotContactDomainCollection(firestore, orgId)\n const listed = new Map<string, boolean>()\n try {\n for (let start = 0; start < domains.length; start += GET_ALL_CHUNK) {\n const chunk = domains.slice(start, start + GET_ALL_CHUNK)\n const snapshots = await firestore.getAll(...chunk.map((domain) => list.doc(domain)))\n chunk.forEach((domain, index) => listed.set(domain, snapshots[index]?.exists === true))\n }\n for (const [email, domain] of domainOf) answers.set(email, listed.get(domain) ?? null)\n } catch (error) {\n console.error('[outreach] do-not-contact domain lookup failed; reading as unchecked', error)\n for (const email of domainOf.keys()) answers.set(email, null)\n }\n return answers\n}\n\n/** Every domain on the list, alphabetically. */\nexport async function listOutreachDoNotContactDomains(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): Promise<OutreachDoNotContactDomainEntry[]> {\n const snapshot = await outreachDoNotContactDomainCollection(firestore, orgId).get()\n return snapshot.docs\n .map((doc) => readOutreachDoNotContactDomainEntry(doc.id, doc.data()))\n .filter((entry): entry is OutreachDoNotContactDomainEntry => entry !== null)\n .sort((a, b) => a.domain.localeCompare(b.domain))\n}\n\nexport interface AddOutreachDoNotContactDomainInput extends Omit<AddOutreachDoNotContactInput, 'email'> {\n /** The domain, or an address at it; spelled by `normalizeOutreachDomain`. */\n domain: string\n}\n\nexport interface AddOutreachDoNotContactDomainResult {\n domain: string\n /** False when the domain was on the list already; its entry is unchanged. */\n created: boolean\n entry: OutreachDoNotContactDomainEntry\n}\n\n/** Puts a domain on the list; a second add changes nothing. Throws for a value that is not a domain. */\nexport async function addOutreachDoNotContactDomain(\n firestore: FirebaseFirestore.Firestore,\n input: AddOutreachDoNotContactDomainInput,\n): Promise<AddOutreachDoNotContactDomainResult> {\n const domain = normalizeOutreachDomain(input.domain)\n if (!domain) throw new Error('[outreach] cannot key a do-not-contact entry for a value that is not a domain')\n const entry: OutreachDoNotContactDomainEntry = { domain, ...entryFields(input) }\n const ref = outreachDoNotContactDomainCollection(firestore, input.orgId).doc(domain)\n try {\n await ref.create(entry)\n return { domain, created: true, entry }\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n const existing = await ref.get()\n return {\n domain,\n created: false,\n entry: readOutreachDoNotContactDomainEntry(domain, existing.data()) ?? entry,\n }\n }\n}\n\n/** Takes a domain off the list; answers whether it was on it. */\nexport async function removeOutreachDoNotContactDomain(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n value: string,\n): Promise<boolean> {\n const domain = normalizeOutreachDomain(value)\n if (!domain) return false\n const ref = outreachDoNotContactDomainCollection(firestore, orgId).doc(domain)\n if (!(await ref.get()).exists) return false\n await ref.delete()\n return true\n}\n"],"names":["outreachDoNotContactKey","normalizeOutreachDomain","outreachEmailDomain","OUTREACH_COLLECTIONS","OUTREACH_DO_NOT_CONTACT_REASONS","OUTREACH_DO_NOT_CONTACT_SOURCES","OUTREACH_DO_NOT_CONTACT_DETAIL_MAX","GET_ALL_CHUNK","ALREADY_EXISTS","outreachDoNotContactCollection","firestore","orgId","collection","doc","doNotContact","readOutreachDoNotContactEntry","key","data","reason","source","text","value","addedAtMs","Number","includes","addedByUid","isFinite","enrollmentId","sequenceId","detail","lookupOutreachDoNotContact","emails","answers","Map","keyed","email","push","set","length","list","start","chunk","slice","snapshots","getAll","map","entry","forEach","index","exists","error","console","isOutreachDoNotContact","get","getOutreachDoNotContactEntry","snapshot","addOutreachDoNotContact","input","Error","entryFields","ref","create","created","code","existing","String","replace","trim","nowMs","outreachDoNotContactDomainCollection","doNotContactDomains","readOutreachDoNotContactDomainEntry","domain","rest","lookupOutreachDoNotContactDomains","domainOf","domains","Set","values","listed","keys","listOutreachDoNotContactDomains","docs","id","filter","sort","a","b","localeCompare","addOutreachDoNotContactDomain","removeOutreachDoNotContactDomain","delete"],"mappings":"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"}