@aglyn/plugins-outreach 1.0.0-beta.189 → 1.0.0-beta.191

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-outreach",
3
- "version": "1.0.0-beta.189",
3
+ "version": "1.0.0-beta.191",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,19 +25,19 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.189",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.189",
30
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.189",
31
- "@aglyn/shared-ui-jsx": "1.0.0-beta.189",
32
- "@aglyn/shared-ui-next": "1.0.0-beta.189",
33
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.189",
34
- "@aglyn/shared-util-email": "1.0.0-beta.189",
35
- "@aglyn/shared-util-http": "1.0.0-beta.189",
36
- "@aglyn/shared-util-timestamp": "1.0.0-beta.189",
37
- "@aglyn/shared-util-tools": "1.0.0-beta.189",
38
- "@aglyn/tenant-data-admin": "1.0.0-beta.189",
39
- "@aglyn/tenant-feature-instance": "1.0.0-beta.189",
40
- "@aglyn/tenant-runtime": "1.0.0-beta.189",
28
+ "@aglyn/aglyn": "1.0.0-beta.191",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.191",
30
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.191",
31
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.191",
32
+ "@aglyn/shared-ui-next": "1.0.0-beta.191",
33
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.191",
34
+ "@aglyn/shared-util-email": "1.0.0-beta.191",
35
+ "@aglyn/shared-util-http": "1.0.0-beta.191",
36
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.191",
37
+ "@aglyn/shared-util-tools": "1.0.0-beta.191",
38
+ "@aglyn/tenant-data-admin": "1.0.0-beta.191",
39
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.191",
40
+ "@aglyn/tenant-runtime": "1.0.0-beta.191",
41
41
  "@swc/helpers": "0.5.23"
42
42
  },
43
43
  "peerDependencies": {
@@ -43,9 +43,10 @@ export interface OutreachGateLookups {
43
43
  /**
44
44
  * The address's standing on the site's `sales` topic —
45
45
  * `readTopicSubscriptionState` of the `sales` entry in
46
- * `hosts/{hostId}/topicOptOuts/{key}`. Only `opted-out` refuses: a pending
47
- * confirmation is about a subscription, and a sales email does not rest on
48
- * one.
46
+ * `hosts/{hostId}/topicOptOuts/{key}`, folded across the site's consent
47
+ * group. Only `opted-out` refuses: a pending confirmation — the site's own,
48
+ * or a sibling's where the org said its group waits — is about a
49
+ * subscription, and a sales email does not rest on one.
49
50
  */
50
51
  salesTopicState: TopicSubscriptionState | null;
51
52
  /** The address is on the organization's Outreach do-not-contact list. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/gates.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 * WHO MAY BE EMAILED (AGL-2979).\n *\n * One pure decision, run twice: when a rep enrolls a person, so the dialog\n * can say everything that stands in the way at once, and again before every\n * send, because a person enrolled on Monday can unsubscribe, become a\n * customer or join the team by Thursday. It reads a contact as stored and\n * the RESULTS of the lookups the runtime makes; it makes none itself.\n *\n * ## Every block, not the first\n *\n * The result lists each gate that refuses, in the order below, with a\n * sentence a rep can act on. A dialog that revealed one problem per attempt\n * would be a dialog people learn to fight.\n *\n * ## Cold\n *\n * A contact is COLD when no inbound door ever brought them to the sending\n * site's business — no form, sign-up, order or booking recorded on that\n * site's facet of the contact, and no email they wrote to the workspace.\n * Cold contacts carry the heavier rules: a business address rather than a\n * personal mailbox, a United States address, a personal line from the rep,\n * and the three attestations only the rep can make. The facet is read, never\n * the whole document, because a form filled in on another client's site is\n * that client's relationship and not this one's.\n *\n * ## Lookups fail closed\n *\n * A lookup the runtime could not complete is passed as `null`, and `null`\n * refuses — with a sentence that says the check could not be made rather\n * than one that accuses the address of something. A person who asked not to\n * be emailed must not be emailed because a read timed out.\n *==========================================*/\n\nimport {\n type ContactSource,\n contactEmails,\n normalizeContactEmail,\n readContactFacet,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport { isPublicMailboxDomain } from '@aglyn/aglyn/app-utils/crm'\nimport { emailStateRefusal, readEmailState } from '@aglyn/aglyn/app-utils/email-state'\nimport type { TopicSubscriptionState } from '@aglyn/aglyn/app-utils/email-topics'\nimport {\n findMemberByEmailAddress,\n type MemberAddresses,\n} from '@aglyn/aglyn/app-utils/member-email-aliases'\nimport {\n OUTREACH_ATTESTATION_KINDS,\n OUTREACH_PERSONAL_LINE_MAX,\n type OutreachAttestationKind,\n type OutreachAttestations,\n type OutreachEnrollmentStatus,\n type OutreachSequenceSettings,\n} from '../model/outreach.types'\nimport {\n type OutreachRecipientCountry,\n outreachCountryName,\n resolveOutreachRecipientCountry,\n} from './recipient-country'\nimport { readOutreachSequenceSettings } from './sequence-validation'\n\n/**\n * The capture doors that mean a person came to the business: a form, a\n * sign-up to the site or its newsletter, an order, a booking. `api`,\n * `import` and `manual` are how a business adds people it found, which is\n * exactly what cold means.\n */\nexport const OUTREACH_INBOUND_CONTACT_SOURCES: readonly ContactSource[] = [\n 'form',\n 'member',\n 'newsletter',\n 'order',\n 'booking',\n]\n\n/** The enrollment statuses that hold a person: one sequence at a time. */\nexport const OUTREACH_OPEN_ENROLLMENT_STATUSES: readonly OutreachEnrollmentStatus[] = [\n 'active',\n 'paused',\n]\n\n/**\n * What the runtime looked up about the address before asking. Every field is\n * `null` when its read failed, and a `null` refuses.\n */\nexport interface OutreachGateLookups {\n /**\n * `emailSuppressions/{key}` holds a record in force — a hard bounce or a\n * spam complaint anywhere on the platform (`isEmailSuppressed`).\n */\n platformSuppressed: boolean | null\n /** `hosts/{hostId}/suppressions/{key}` exists for the sequence's site. */\n hostSuppressed: boolean | null\n /**\n * The address's standing on the site's `sales` topic —\n * `readTopicSubscriptionState` of the `sales` entry in\n * `hosts/{hostId}/topicOptOuts/{key}`. Only `opted-out` refuses: a pending\n * confirmation is about a subscription, and a sales email does not rest on\n * one.\n */\n salesTopicState: TopicSubscriptionState | null\n /** The address is on the organization's Outreach do-not-contact list. */\n doNotContact: boolean | null\n /**\n * The address's DOMAIN is on that list (AGL-3244): a member put it there,\n * or the domain's mail gateway blocked the sender outright.\n */\n doNotContactDomain: boolean | null\n /**\n * The organization's roster: each member's sign-in address and confirmed\n * aliases, as `crmInboundRoster` builds it.\n */\n workspaceMembers: readonly MemberAddresses[] | null\n /**\n * The person's enrollments that are `active` or `paused`, found by contact\n * id or by address. Other statuses are ignored, so passing every\n * enrollment is safe too.\n */\n openEnrollments: ReadonlyArray<{\n id: string\n sequenceId: string\n status: OutreachEnrollmentStatus\n }> | null\n /**\n * An email the person wrote (`direction: 'inbound'`) is filed on the\n * contact. `null` reads as `false`, which only makes the contact cold and\n * the rules stricter.\n */\n hasInboundEmail: boolean | null\n}\n\nexport interface OutreachGateInput {\n /** The address the steps go to. */\n email: string\n /** The contact document as stored (`orgs/{orgId}/contacts/{id}`), or `null` when it is gone. */\n contact: Record<string, unknown> | null | undefined\n /** The consent group of the sequence's site: whose facet holds stage, sources and address. */\n contactGroupId: string\n /** The company document the contact is filed under, when there is one. */\n company?: Record<string, unknown> | null\n /** The sequence's settings as stored; a missing field reads as its default. */\n settings: Partial<OutreachSequenceSettings> | null | undefined\n /** The rep's signal sentence. */\n personalLine: string | null | undefined\n attestations: OutreachAttestations | null | undefined\n /** The enrollment being re-checked before a send, so it does not count as another. */\n enrollmentId?: string | null\n lookups: OutreachGateLookups\n}\n\nexport type OutreachGateCode =\n | 'contact_missing'\n | 'invalid_email'\n | 'email_not_on_contact'\n | 'free_mail'\n | 'country_not_allowed'\n | 'country_unknown'\n | 'platform_suppressed'\n | 'host_suppressed'\n | 'email_state'\n | 'sales_opted_out'\n | 'do_not_contact'\n | 'do_not_contact_domain'\n | 'workspace_member'\n | 'customer'\n | 'already_enrolled'\n | 'attestations_missing'\n | 'personal_line_missing'\n | 'personal_line_too_long'\n\nexport interface OutreachGateBlock {\n code: OutreachGateCode\n /** A sentence a rep can act on. */\n reason: string\n}\n\nexport interface OutreachGateResult {\n allowed: boolean\n /** The address, normalized, or `null` when it is not one. */\n email: string | null\n cold: boolean\n country: OutreachRecipientCountry\n /** Every gate that refuses, in gate order. */\n blocks: OutreachGateBlock[]\n /** The attestations a cold contact still needs, in the order they are asked. */\n missingAttestations: OutreachAttestationKind[]\n}\n\n/** Local parts that answer no person: nobody reads a reply sent there. */\nconst MACHINE_LOCAL_PART = /^(no-?reply|do-?not-?reply|mailer-daemon|postmaster|bounces?)(\\+.*)?$/\n\n/**\n * Whether a contact came to the sending site's business through no inbound\n * door — see the module note.\n */\nexport function isColdOutreachContact(\n contact: Record<string, unknown> | null | undefined,\n contactGroupId: string,\n hasInboundEmail: boolean,\n): boolean {\n if (hasInboundEmail) return false\n const { sources } = readContactFacet(contact, contactGroupId)\n return !OUTREACH_INBOUND_CONTACT_SOURCES.some((source) => sources?.[source] === true)\n}\n\n/**\n * Whether the sending site's business counts the contact as a customer: a\n * `customer` stage, the `evangelist` stage past it, or an order on record.\n */\nexport function isOutreachCustomer(\n contact: Record<string, unknown> | null | undefined,\n contactGroupId: string,\n): boolean {\n const facet = readContactFacet(contact, contactGroupId)\n return (\n facet.lifecycleStage === 'customer' ||\n facet.lifecycleStage === 'evangelist' ||\n facet.sources?.order === true ||\n Number(facet.ordersCount) > 0\n )\n}\n\n/** A confirmation counts only when it says who confirmed it and when. */\nexport function outreachAttestationHolds(\n attestations: OutreachAttestations | null | undefined,\n kind: OutreachAttestationKind,\n): boolean {\n const entry = attestations?.[kind]\n return (\n typeof entry?.uid === 'string' &&\n entry.uid.trim() !== '' &&\n typeof entry.atMs === 'number' &&\n Number.isFinite(entry.atMs) &&\n entry.atMs > 0\n )\n}\n\n/** The personal line as stored: one line, whitespace settled. */\nexport function normalizeOutreachPersonalLine(value: unknown): string {\n return typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n}\n\n/** Each attestation as the sentence that asks for it reads it. */\nconst ATTESTATION_PHRASES: Record<OutreachAttestationKind, string> = {\n us_business_address: \"it's a US business address\",\n published_or_given: 'they or their company published it, or they gave it to you',\n verified_deliverable: 'it was verified as deliverable',\n}\n\nfunction listInWords(items: readonly string[]): string {\n if (items.length <= 1) return items.join('')\n return `${items.slice(0, -1).join('; ')}; and ${items[items.length - 1]}`\n}\n\nconst COULD_NOT_CHECK = 'Try again in a moment.'\n\n/** Every gate an address and a contact must pass — see the module note. */\nexport function evaluateOutreachGates(input: OutreachGateInput): OutreachGateResult {\n const lookups = input.lookups\n const settings = readOutreachSequenceSettings(input.settings)\n const email = normalizeContactEmail(input.email)\n const contact = input.contact ?? null\n const cold = isColdOutreachContact(\n contact,\n input.contactGroupId,\n lookups?.hasInboundEmail === true,\n )\n const blocks: OutreachGateBlock[] = []\n const block = (code: OutreachGateCode, reason: string) => blocks.push({ code, reason })\n // The gates below need both a record and an address to read.\n const refusedOutright = (): OutreachGateResult => ({\n allowed: false,\n email,\n cold,\n country: { country: null, source: null },\n blocks,\n missingAttestations: [],\n })\n\n if (!contact) {\n block('contact_missing', 'This contact no longer exists in the CRM.')\n return refusedOutright()\n }\n\n // 1. A valid address, one that belongs to this contact and to a person.\n if (!email) {\n block('invalid_email', `\"${String(input.email ?? '').trim()}\" isn't a valid email address.`)\n return refusedOutright()\n }\n if (MACHINE_LOCAL_PART.test(email.slice(0, email.lastIndexOf('@')))) {\n block('invalid_email', `${email} is an automated address that no person reads.`)\n }\n if (!contactEmails(contact).includes(email)) {\n block('email_not_on_contact', `${email} isn't an address on this contact's record.`)\n }\n\n // 2. A business address for a cold contact.\n if (cold && isPublicMailboxDomain(email)) {\n block(\n 'free_mail',\n `${email} is a personal mailbox. Cold outreach goes only to business addresses.`,\n )\n }\n\n // 3. The country.\n const facet = readContactFacet(contact, input.contactGroupId)\n const companyAddress = input.company ? input.company['address'] : null\n const country = resolveOutreachRecipientCountry({\n email,\n contactAddress: facet.address,\n companyAddress,\n })\n const attestedUs = outreachAttestationHolds(input.attestations, 'us_business_address')\n if (country.country) {\n const name = outreachCountryName(country.country)\n const evidence =\n country.source === 'domain' ? `its domain places it in ${name}` : `it's in ${name}`\n if (!settings.allowedCountries.includes(country.country)) {\n block(\n 'country_not_allowed',\n `${email} can't be emailed from this sequence: ${evidence}, which isn't one of its countries.`,\n )\n } else if (cold && country.country !== 'US') {\n block(\n 'country_not_allowed',\n `Cold outreach goes only to US business addresses, and ${evidence}.`,\n )\n }\n } else if (!settings.allowedCountries.includes('US')) {\n block(\n 'country_unknown',\n `We don't know which country ${email} is in. Add the country to the contact's address.`,\n )\n } else if (!attestedUs) {\n block(\n 'country_unknown',\n `We don't know which country ${email} is in. Confirm it's a US business address, or add the country to the contact.`,\n )\n }\n\n // 4. Every list that says not to — the record's own verdict first\n // (AGL-3245), in the words the record page shows: what the lists below\n // hold, said by the reason it is held.\n const recordRefusal = emailStateRefusal(readEmailState(contact))\n if (recordRefusal) block('email_state', recordRefusal)\n if (lookups?.platformSuppressed === true) {\n block(\n 'platform_suppressed',\n `${email} is on the platform's suppression list after a hard bounce or a spam complaint.`,\n )\n } else if (lookups?.platformSuppressed !== false) {\n block(\n 'platform_suppressed',\n `We couldn't check the platform's suppression list for ${email}. ${COULD_NOT_CHECK}`,\n )\n }\n if (lookups?.hostSuppressed === true) {\n block('host_suppressed', `${email} unsubscribed from this site's email, or mail to it bounced.`)\n } else if (lookups?.hostSuppressed !== false) {\n block(\n 'host_suppressed',\n `We couldn't check this site's suppression list for ${email}. ${COULD_NOT_CHECK}`,\n )\n }\n if (lookups?.salesTopicState === 'opted-out') {\n block('sales_opted_out', `${email} opted out of sales outreach from this site.`)\n } else if (lookups?.salesTopicState !== 'subscribed' && lookups?.salesTopicState !== 'pending') {\n block(\n 'sales_opted_out',\n `We couldn't check whether ${email} opted out of sales outreach. ${COULD_NOT_CHECK}`,\n )\n }\n if (lookups?.doNotContact === true) {\n block('do_not_contact', `${email} is on your organization's do-not-contact list.`)\n } else if (lookups?.doNotContact !== false) {\n block(\n 'do_not_contact',\n `We couldn't check your do-not-contact list for ${email}. ${COULD_NOT_CHECK}`,\n )\n }\n const domain = email.slice(email.lastIndexOf('@') + 1)\n if (lookups?.doNotContactDomain === true) {\n block(\n 'do_not_contact_domain',\n `${email} is at ${domain}, which is on your organization's do-not-contact list: no address there is emailed.`,\n )\n } else if (lookups?.doNotContactDomain !== false) {\n block(\n 'do_not_contact_domain',\n `We couldn't check your do-not-contact list for ${domain}. ${COULD_NOT_CHECK}`,\n )\n }\n\n // 5. Not one of the workspace's own people.\n const members = lookups?.workspaceMembers\n if (!Array.isArray(members)) {\n block(\n 'workspace_member',\n `We couldn't check whether ${email} belongs to a member of this workspace. ${COULD_NOT_CHECK}`,\n )\n } else if (findMemberByEmailAddress(members, email)) {\n block('workspace_member', `${email} belongs to a member of this workspace.`)\n }\n\n // 6. Not a customer, unless the sequence is written for them.\n if (!settings.allowCustomers && isOutreachCustomer(contact, input.contactGroupId)) {\n block('customer', \"This contact is a customer, and this sequence doesn't include customers.\")\n }\n\n // 7. One sequence at a time.\n const enrollments = lookups?.openEnrollments\n if (!Array.isArray(enrollments)) {\n block(\n 'already_enrolled',\n `We couldn't check this contact's other sequences. ${COULD_NOT_CHECK}`,\n )\n } else if (\n enrollments.some(\n (enrollment) =>\n enrollment?.id !== input.enrollmentId &&\n OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment?.status),\n )\n ) {\n block(\n 'already_enrolled',\n 'This contact is already in another sequence. Finish or stop that one first.',\n )\n }\n\n // 8. What only the rep can vouch for, on a cold contact.\n const personalLine = normalizeOutreachPersonalLine(input.personalLine)\n const missingAttestations = cold\n ? OUTREACH_ATTESTATION_KINDS.filter(\n (kind) => !outreachAttestationHolds(input.attestations, kind),\n )\n : []\n if (missingAttestations.length) {\n const phrases = missingAttestations.map((kind) => `that ${ATTESTATION_PHRASES[kind]}`)\n block(\n 'attestations_missing',\n `Before emailing a cold contact, confirm ${listInWords(phrases)}.`,\n )\n }\n if (cold && !personalLine) {\n block(\n 'personal_line_missing',\n 'Write a personal line: one sentence on why you are writing to this person now.',\n )\n }\n if (personalLine.length > OUTREACH_PERSONAL_LINE_MAX) {\n block(\n 'personal_line_too_long',\n `Keep the personal line to one sentence, under ${OUTREACH_PERSONAL_LINE_MAX} characters.`,\n )\n }\n\n return {\n allowed: blocks.length === 0,\n email,\n cold,\n country,\n blocks,\n missingAttestations,\n }\n}\n"],"names":["contactEmails","normalizeContactEmail","readContactFacet","isPublicMailboxDomain","emailStateRefusal","readEmailState","findMemberByEmailAddress","OUTREACH_ATTESTATION_KINDS","OUTREACH_PERSONAL_LINE_MAX","outreachCountryName","resolveOutreachRecipientCountry","readOutreachSequenceSettings","OUTREACH_INBOUND_CONTACT_SOURCES","OUTREACH_OPEN_ENROLLMENT_STATUSES","MACHINE_LOCAL_PART","isColdOutreachContact","contact","contactGroupId","hasInboundEmail","sources","some","source","isOutreachCustomer","facet","lifecycleStage","order","Number","ordersCount","outreachAttestationHolds","attestations","kind","entry","uid","trim","atMs","isFinite","normalizeOutreachPersonalLine","value","replace","ATTESTATION_PHRASES","us_business_address","published_or_given","verified_deliverable","listInWords","items","length","join","slice","COULD_NOT_CHECK","evaluateOutreachGates","input","lookups","settings","email","cold","blocks","block","code","reason","push","refusedOutright","allowed","country","missingAttestations","String","test","lastIndexOf","includes","companyAddress","company","contactAddress","address","attestedUs","name","evidence","allowedCountries","recordRefusal","platformSuppressed","hostSuppressed","salesTopicState","doNotContact","domain","doNotContactDomain","members","workspaceMembers","Array","isArray","allowCustomers","enrollments","openEnrollments","enrollment","id","enrollmentId","status","personalLine","filter","phrases","map"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/gates.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 * WHO MAY BE EMAILED (AGL-2979).\n *\n * One pure decision, run twice: when a rep enrolls a person, so the dialog\n * can say everything that stands in the way at once, and again before every\n * send, because a person enrolled on Monday can unsubscribe, become a\n * customer or join the team by Thursday. It reads a contact as stored and\n * the RESULTS of the lookups the runtime makes; it makes none itself.\n *\n * ## Every block, not the first\n *\n * The result lists each gate that refuses, in the order below, with a\n * sentence a rep can act on. A dialog that revealed one problem per attempt\n * would be a dialog people learn to fight.\n *\n * ## Cold\n *\n * A contact is COLD when no inbound door ever brought them to the sending\n * site's business — no form, sign-up, order or booking recorded on that\n * site's facet of the contact, and no email they wrote to the workspace.\n * Cold contacts carry the heavier rules: a business address rather than a\n * personal mailbox, a United States address, a personal line from the rep,\n * and the three attestations only the rep can make. The facet is read, never\n * the whole document, because a form filled in on another client's site is\n * that client's relationship and not this one's.\n *\n * ## Lookups fail closed\n *\n * A lookup the runtime could not complete is passed as `null`, and `null`\n * refuses — with a sentence that says the check could not be made rather\n * than one that accuses the address of something. A person who asked not to\n * be emailed must not be emailed because a read timed out.\n *==========================================*/\n\nimport {\n type ContactSource,\n contactEmails,\n normalizeContactEmail,\n readContactFacet,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport { isPublicMailboxDomain } from '@aglyn/aglyn/app-utils/crm'\nimport { emailStateRefusal, readEmailState } from '@aglyn/aglyn/app-utils/email-state'\nimport type { TopicSubscriptionState } from '@aglyn/aglyn/app-utils/email-topics'\nimport {\n findMemberByEmailAddress,\n type MemberAddresses,\n} from '@aglyn/aglyn/app-utils/member-email-aliases'\nimport {\n OUTREACH_ATTESTATION_KINDS,\n OUTREACH_PERSONAL_LINE_MAX,\n type OutreachAttestationKind,\n type OutreachAttestations,\n type OutreachEnrollmentStatus,\n type OutreachSequenceSettings,\n} from '../model/outreach.types'\nimport {\n type OutreachRecipientCountry,\n outreachCountryName,\n resolveOutreachRecipientCountry,\n} from './recipient-country'\nimport { readOutreachSequenceSettings } from './sequence-validation'\n\n/**\n * The capture doors that mean a person came to the business: a form, a\n * sign-up to the site or its newsletter, an order, a booking. `api`,\n * `import` and `manual` are how a business adds people it found, which is\n * exactly what cold means.\n */\nexport const OUTREACH_INBOUND_CONTACT_SOURCES: readonly ContactSource[] = [\n 'form',\n 'member',\n 'newsletter',\n 'order',\n 'booking',\n]\n\n/** The enrollment statuses that hold a person: one sequence at a time. */\nexport const OUTREACH_OPEN_ENROLLMENT_STATUSES: readonly OutreachEnrollmentStatus[] = [\n 'active',\n 'paused',\n]\n\n/**\n * What the runtime looked up about the address before asking. Every field is\n * `null` when its read failed, and a `null` refuses.\n */\nexport interface OutreachGateLookups {\n /**\n * `emailSuppressions/{key}` holds a record in force — a hard bounce or a\n * spam complaint anywhere on the platform (`isEmailSuppressed`).\n */\n platformSuppressed: boolean | null\n /** `hosts/{hostId}/suppressions/{key}` exists for the sequence's site. */\n hostSuppressed: boolean | null\n /**\n * The address's standing on the site's `sales` topic —\n * `readTopicSubscriptionState` of the `sales` entry in\n * `hosts/{hostId}/topicOptOuts/{key}`, folded across the site's consent\n * group. Only `opted-out` refuses: a pending confirmation — the site's own,\n * or a sibling's where the org said its group waits — is about a\n * subscription, and a sales email does not rest on one.\n */\n salesTopicState: TopicSubscriptionState | null\n /** The address is on the organization's Outreach do-not-contact list. */\n doNotContact: boolean | null\n /**\n * The address's DOMAIN is on that list (AGL-3244): a member put it there,\n * or the domain's mail gateway blocked the sender outright.\n */\n doNotContactDomain: boolean | null\n /**\n * The organization's roster: each member's sign-in address and confirmed\n * aliases, as `crmInboundRoster` builds it.\n */\n workspaceMembers: readonly MemberAddresses[] | null\n /**\n * The person's enrollments that are `active` or `paused`, found by contact\n * id or by address. Other statuses are ignored, so passing every\n * enrollment is safe too.\n */\n openEnrollments: ReadonlyArray<{\n id: string\n sequenceId: string\n status: OutreachEnrollmentStatus\n }> | null\n /**\n * An email the person wrote (`direction: 'inbound'`) is filed on the\n * contact. `null` reads as `false`, which only makes the contact cold and\n * the rules stricter.\n */\n hasInboundEmail: boolean | null\n}\n\nexport interface OutreachGateInput {\n /** The address the steps go to. */\n email: string\n /** The contact document as stored (`orgs/{orgId}/contacts/{id}`), or `null` when it is gone. */\n contact: Record<string, unknown> | null | undefined\n /** The consent group of the sequence's site: whose facet holds stage, sources and address. */\n contactGroupId: string\n /** The company document the contact is filed under, when there is one. */\n company?: Record<string, unknown> | null\n /** The sequence's settings as stored; a missing field reads as its default. */\n settings: Partial<OutreachSequenceSettings> | null | undefined\n /** The rep's signal sentence. */\n personalLine: string | null | undefined\n attestations: OutreachAttestations | null | undefined\n /** The enrollment being re-checked before a send, so it does not count as another. */\n enrollmentId?: string | null\n lookups: OutreachGateLookups\n}\n\nexport type OutreachGateCode =\n | 'contact_missing'\n | 'invalid_email'\n | 'email_not_on_contact'\n | 'free_mail'\n | 'country_not_allowed'\n | 'country_unknown'\n | 'platform_suppressed'\n | 'host_suppressed'\n | 'email_state'\n | 'sales_opted_out'\n | 'do_not_contact'\n | 'do_not_contact_domain'\n | 'workspace_member'\n | 'customer'\n | 'already_enrolled'\n | 'attestations_missing'\n | 'personal_line_missing'\n | 'personal_line_too_long'\n\nexport interface OutreachGateBlock {\n code: OutreachGateCode\n /** A sentence a rep can act on. */\n reason: string\n}\n\nexport interface OutreachGateResult {\n allowed: boolean\n /** The address, normalized, or `null` when it is not one. */\n email: string | null\n cold: boolean\n country: OutreachRecipientCountry\n /** Every gate that refuses, in gate order. */\n blocks: OutreachGateBlock[]\n /** The attestations a cold contact still needs, in the order they are asked. */\n missingAttestations: OutreachAttestationKind[]\n}\n\n/** Local parts that answer no person: nobody reads a reply sent there. */\nconst MACHINE_LOCAL_PART = /^(no-?reply|do-?not-?reply|mailer-daemon|postmaster|bounces?)(\\+.*)?$/\n\n/**\n * Whether a contact came to the sending site's business through no inbound\n * door — see the module note.\n */\nexport function isColdOutreachContact(\n contact: Record<string, unknown> | null | undefined,\n contactGroupId: string,\n hasInboundEmail: boolean,\n): boolean {\n if (hasInboundEmail) return false\n const { sources } = readContactFacet(contact, contactGroupId)\n return !OUTREACH_INBOUND_CONTACT_SOURCES.some((source) => sources?.[source] === true)\n}\n\n/**\n * Whether the sending site's business counts the contact as a customer: a\n * `customer` stage, the `evangelist` stage past it, or an order on record.\n */\nexport function isOutreachCustomer(\n contact: Record<string, unknown> | null | undefined,\n contactGroupId: string,\n): boolean {\n const facet = readContactFacet(contact, contactGroupId)\n return (\n facet.lifecycleStage === 'customer' ||\n facet.lifecycleStage === 'evangelist' ||\n facet.sources?.order === true ||\n Number(facet.ordersCount) > 0\n )\n}\n\n/** A confirmation counts only when it says who confirmed it and when. */\nexport function outreachAttestationHolds(\n attestations: OutreachAttestations | null | undefined,\n kind: OutreachAttestationKind,\n): boolean {\n const entry = attestations?.[kind]\n return (\n typeof entry?.uid === 'string' &&\n entry.uid.trim() !== '' &&\n typeof entry.atMs === 'number' &&\n Number.isFinite(entry.atMs) &&\n entry.atMs > 0\n )\n}\n\n/** The personal line as stored: one line, whitespace settled. */\nexport function normalizeOutreachPersonalLine(value: unknown): string {\n return typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n}\n\n/** Each attestation as the sentence that asks for it reads it. */\nconst ATTESTATION_PHRASES: Record<OutreachAttestationKind, string> = {\n us_business_address: \"it's a US business address\",\n published_or_given: 'they or their company published it, or they gave it to you',\n verified_deliverable: 'it was verified as deliverable',\n}\n\nfunction listInWords(items: readonly string[]): string {\n if (items.length <= 1) return items.join('')\n return `${items.slice(0, -1).join('; ')}; and ${items[items.length - 1]}`\n}\n\nconst COULD_NOT_CHECK = 'Try again in a moment.'\n\n/** Every gate an address and a contact must pass — see the module note. */\nexport function evaluateOutreachGates(input: OutreachGateInput): OutreachGateResult {\n const lookups = input.lookups\n const settings = readOutreachSequenceSettings(input.settings)\n const email = normalizeContactEmail(input.email)\n const contact = input.contact ?? null\n const cold = isColdOutreachContact(\n contact,\n input.contactGroupId,\n lookups?.hasInboundEmail === true,\n )\n const blocks: OutreachGateBlock[] = []\n const block = (code: OutreachGateCode, reason: string) => blocks.push({ code, reason })\n // The gates below need both a record and an address to read.\n const refusedOutright = (): OutreachGateResult => ({\n allowed: false,\n email,\n cold,\n country: { country: null, source: null },\n blocks,\n missingAttestations: [],\n })\n\n if (!contact) {\n block('contact_missing', 'This contact no longer exists in the CRM.')\n return refusedOutright()\n }\n\n // 1. A valid address, one that belongs to this contact and to a person.\n if (!email) {\n block('invalid_email', `\"${String(input.email ?? '').trim()}\" isn't a valid email address.`)\n return refusedOutright()\n }\n if (MACHINE_LOCAL_PART.test(email.slice(0, email.lastIndexOf('@')))) {\n block('invalid_email', `${email} is an automated address that no person reads.`)\n }\n if (!contactEmails(contact).includes(email)) {\n block('email_not_on_contact', `${email} isn't an address on this contact's record.`)\n }\n\n // 2. A business address for a cold contact.\n if (cold && isPublicMailboxDomain(email)) {\n block(\n 'free_mail',\n `${email} is a personal mailbox. Cold outreach goes only to business addresses.`,\n )\n }\n\n // 3. The country.\n const facet = readContactFacet(contact, input.contactGroupId)\n const companyAddress = input.company ? input.company['address'] : null\n const country = resolveOutreachRecipientCountry({\n email,\n contactAddress: facet.address,\n companyAddress,\n })\n const attestedUs = outreachAttestationHolds(input.attestations, 'us_business_address')\n if (country.country) {\n const name = outreachCountryName(country.country)\n const evidence =\n country.source === 'domain' ? `its domain places it in ${name}` : `it's in ${name}`\n if (!settings.allowedCountries.includes(country.country)) {\n block(\n 'country_not_allowed',\n `${email} can't be emailed from this sequence: ${evidence}, which isn't one of its countries.`,\n )\n } else if (cold && country.country !== 'US') {\n block(\n 'country_not_allowed',\n `Cold outreach goes only to US business addresses, and ${evidence}.`,\n )\n }\n } else if (!settings.allowedCountries.includes('US')) {\n block(\n 'country_unknown',\n `We don't know which country ${email} is in. Add the country to the contact's address.`,\n )\n } else if (!attestedUs) {\n block(\n 'country_unknown',\n `We don't know which country ${email} is in. Confirm it's a US business address, or add the country to the contact.`,\n )\n }\n\n // 4. Every list that says not to — the record's own verdict first\n // (AGL-3245), in the words the record page shows: what the lists below\n // hold, said by the reason it is held.\n const recordRefusal = emailStateRefusal(readEmailState(contact))\n if (recordRefusal) block('email_state', recordRefusal)\n if (lookups?.platformSuppressed === true) {\n block(\n 'platform_suppressed',\n `${email} is on the platform's suppression list after a hard bounce or a spam complaint.`,\n )\n } else if (lookups?.platformSuppressed !== false) {\n block(\n 'platform_suppressed',\n `We couldn't check the platform's suppression list for ${email}. ${COULD_NOT_CHECK}`,\n )\n }\n if (lookups?.hostSuppressed === true) {\n block('host_suppressed', `${email} unsubscribed from this site's email, or mail to it bounced.`)\n } else if (lookups?.hostSuppressed !== false) {\n block(\n 'host_suppressed',\n `We couldn't check this site's suppression list for ${email}. ${COULD_NOT_CHECK}`,\n )\n }\n if (lookups?.salesTopicState === 'opted-out') {\n block('sales_opted_out', `${email} opted out of sales outreach from this site.`)\n } else if (lookups?.salesTopicState !== 'subscribed' && lookups?.salesTopicState !== 'pending') {\n block(\n 'sales_opted_out',\n `We couldn't check whether ${email} opted out of sales outreach. ${COULD_NOT_CHECK}`,\n )\n }\n if (lookups?.doNotContact === true) {\n block('do_not_contact', `${email} is on your organization's do-not-contact list.`)\n } else if (lookups?.doNotContact !== false) {\n block(\n 'do_not_contact',\n `We couldn't check your do-not-contact list for ${email}. ${COULD_NOT_CHECK}`,\n )\n }\n const domain = email.slice(email.lastIndexOf('@') + 1)\n if (lookups?.doNotContactDomain === true) {\n block(\n 'do_not_contact_domain',\n `${email} is at ${domain}, which is on your organization's do-not-contact list: no address there is emailed.`,\n )\n } else if (lookups?.doNotContactDomain !== false) {\n block(\n 'do_not_contact_domain',\n `We couldn't check your do-not-contact list for ${domain}. ${COULD_NOT_CHECK}`,\n )\n }\n\n // 5. Not one of the workspace's own people.\n const members = lookups?.workspaceMembers\n if (!Array.isArray(members)) {\n block(\n 'workspace_member',\n `We couldn't check whether ${email} belongs to a member of this workspace. ${COULD_NOT_CHECK}`,\n )\n } else if (findMemberByEmailAddress(members, email)) {\n block('workspace_member', `${email} belongs to a member of this workspace.`)\n }\n\n // 6. Not a customer, unless the sequence is written for them.\n if (!settings.allowCustomers && isOutreachCustomer(contact, input.contactGroupId)) {\n block('customer', \"This contact is a customer, and this sequence doesn't include customers.\")\n }\n\n // 7. One sequence at a time.\n const enrollments = lookups?.openEnrollments\n if (!Array.isArray(enrollments)) {\n block(\n 'already_enrolled',\n `We couldn't check this contact's other sequences. ${COULD_NOT_CHECK}`,\n )\n } else if (\n enrollments.some(\n (enrollment) =>\n enrollment?.id !== input.enrollmentId &&\n OUTREACH_OPEN_ENROLLMENT_STATUSES.includes(enrollment?.status),\n )\n ) {\n block(\n 'already_enrolled',\n 'This contact is already in another sequence. Finish or stop that one first.',\n )\n }\n\n // 8. What only the rep can vouch for, on a cold contact.\n const personalLine = normalizeOutreachPersonalLine(input.personalLine)\n const missingAttestations = cold\n ? OUTREACH_ATTESTATION_KINDS.filter(\n (kind) => !outreachAttestationHolds(input.attestations, kind),\n )\n : []\n if (missingAttestations.length) {\n const phrases = missingAttestations.map((kind) => `that ${ATTESTATION_PHRASES[kind]}`)\n block(\n 'attestations_missing',\n `Before emailing a cold contact, confirm ${listInWords(phrases)}.`,\n )\n }\n if (cold && !personalLine) {\n block(\n 'personal_line_missing',\n 'Write a personal line: one sentence on why you are writing to this person now.',\n )\n }\n if (personalLine.length > OUTREACH_PERSONAL_LINE_MAX) {\n block(\n 'personal_line_too_long',\n `Keep the personal line to one sentence, under ${OUTREACH_PERSONAL_LINE_MAX} characters.`,\n )\n }\n\n return {\n allowed: blocks.length === 0,\n email,\n cold,\n country,\n blocks,\n missingAttestations,\n }\n}\n"],"names":["contactEmails","normalizeContactEmail","readContactFacet","isPublicMailboxDomain","emailStateRefusal","readEmailState","findMemberByEmailAddress","OUTREACH_ATTESTATION_KINDS","OUTREACH_PERSONAL_LINE_MAX","outreachCountryName","resolveOutreachRecipientCountry","readOutreachSequenceSettings","OUTREACH_INBOUND_CONTACT_SOURCES","OUTREACH_OPEN_ENROLLMENT_STATUSES","MACHINE_LOCAL_PART","isColdOutreachContact","contact","contactGroupId","hasInboundEmail","sources","some","source","isOutreachCustomer","facet","lifecycleStage","order","Number","ordersCount","outreachAttestationHolds","attestations","kind","entry","uid","trim","atMs","isFinite","normalizeOutreachPersonalLine","value","replace","ATTESTATION_PHRASES","us_business_address","published_or_given","verified_deliverable","listInWords","items","length","join","slice","COULD_NOT_CHECK","evaluateOutreachGates","input","lookups","settings","email","cold","blocks","block","code","reason","push","refusedOutright","allowed","country","missingAttestations","String","test","lastIndexOf","includes","companyAddress","company","contactAddress","address","attestedUs","name","evidence","allowedCountries","recordRefusal","platformSuppressed","hostSuppressed","salesTopicState","doNotContact","domain","doNotContactDomain","members","workspaceMembers","Array","isArray","allowCustomers","enrollments","openEnrollments","enrollment","id","enrollmentId","status","personalLine","filter","phrases","map"],"mappings":"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"}
@@ -41,6 +41,12 @@ export interface ReadOutreachGateLookupsInput {
41
41
  * each. Omitted, the site alone.
42
42
  */
43
43
  consentHostIds?: readonly string[];
44
+ /**
45
+ * The same group's `awaitsConfirmation`: whether a sibling's pending
46
+ * confirmation counts in the sales stream's standing as the sequence
47
+ * site's own does. Omitted, it does not.
48
+ */
49
+ consentAwaitsConfirmation?: boolean;
44
50
  people: readonly OutreachGateLookupPerson[];
45
51
  }
46
52
  type Firestore = FirebaseFirestore.Firestore;
@@ -36,7 +36,8 @@
36
36
  * covered, which the gates read as "we couldn't check" and refuse: a
37
37
  * person who asked not to be emailed must not be emailed because a read
38
38
  * timed out. One failed list does not blank the others.
39
- */ import { CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
39
+ */ import { consentGroupTopicState } from "@aglyn/aglyn/app-utils/consent-groups";
40
+ import { CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
40
41
  import { EMAIL_TOPIC_SALES, readTopicSubscriptionState, TOPIC_OPT_OUTS_SUBCOLLECTION } from "@aglyn/aglyn/app-utils/email-topics";
41
42
  import { personKey } from "@aglyn/aglyn/app-utils/person-key";
42
43
  import { loadCrmInboundRoster } from "@aglyn/tenant-data-admin/server/crm-inbound-email";
@@ -116,9 +117,14 @@ async function getAllChunked(firestore, refs) {
116
117
  keyedGroupLookup(firestore, people, hostIds, (host, key)=>host.collection(TOPIC_OPT_OUTS_SUBCOLLECTION).doc(key), (snapshot)=>{
117
118
  var _ref;
118
119
  return readTopicSubscriptionState(((_ref = snapshot.exists ? snapshot.get('topics') : null) != null ? _ref : {})[EMAIL_TOPIC_SALES]);
119
- }, // The sequence site's own standing, unless a sibling holds a refusal:
120
- // leaving the sales stream anywhere in the group is leaving the sender.
121
- ([own, ...siblings])=>siblings.includes('opted-out') ? 'opted-out' : own, 'sales topic'),
120
+ }, // The group's standing, the way the topic filter folds it: leaving the
121
+ // sales stream anywhere in the group is leaving the sender, and a
122
+ // sibling's pending confirmation counts only where the org said the
123
+ // group waits. What a pending standing means for a sales email is the
124
+ // gates' question, not this one.
125
+ (states)=>consentGroupTopicState({
126
+ awaitsConfirmation: input.consentAwaitsConfirmation === true
127
+ }, states), 'sales topic'),
122
128
  lookupOutreachDoNotContact(firestore, orgId, people.map((person)=>person.email)),
123
129
  // The domain half of the same list (AGL-3244), one `getAll` over the
124
130
  // distinct domains the people are at.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/enrollment/gate-lookups.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 LOOKUPS THE ENROLLMENT GATES DECIDE ON (AGL-2980).\n *\n * `evaluateOutreachGates` is pure: it reads a contact as stored and the\n * RESULTS of the reads the runtime makes. This is those reads, for many\n * people at once — the enroll preview asks about everyone a view names in\n * one pass, and the sending runtime asks about one person before each send\n * with the same function, so the two can never consult different lists.\n *\n * Server only. Every list keyed by address is read with one `getAll` over\n * `personKey` ids — the suppression lists, the sales topic opt-outs, the\n * do-not-contact list — so a lookup costs one round trip however many\n * people it covers, and needs no index that could go missing. The roster\n * is read once. The two per-person facts that are queries — the person's\n * other enrollments and whether they ever wrote in — are equality filters\n * on single fields.\n *\n * ## A read that failed is `null`, and `null` refuses\n *\n * Each lookup is caught on its own and answers `null` for everyone it\n * covered, which the gates read as \"we couldn't check\" and refuse: a\n * person who asked not to be emailed must not be emailed because a read\n * timed out. One failed list does not blank the others.\n */\n\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport {\n EMAIL_TOPIC_SALES,\n readTopicSubscriptionState,\n TOPIC_OPT_OUTS_SUBCOLLECTION,\n type TopicSubscriptionState,\n} from '@aglyn/aglyn/app-utils/email-topics'\nimport type { MemberAddresses } from '@aglyn/aglyn/app-utils/member-email-aliases'\nimport { personKey } from '@aglyn/aglyn/app-utils/person-key'\nimport { loadCrmInboundRoster } from '@aglyn/tenant-data-admin/server/crm-inbound-email'\nimport {\n EMAIL_SUPPRESSIONS_COLLECTION,\n HOST_SUPPRESSIONS_SUBCOLLECTION,\n} from '@aglyn/tenant-data-admin/server/email-suppression'\nimport type { OutreachGateLookups } from '../engine/gates'\nimport type { OutreachEnrollmentStatus } from '../model/outreach.types'\nimport {\n lookupOutreachDoNotContact,\n lookupOutreachDoNotContactDomains,\n} from '../storage/do-not-contact-store'\nimport { outreachOrgCollection } from '../storage/outreach-records'\n\n/**\n * One person to look up: the record they are, and the address the steps go\n * to. `personId` keys the answer — the contact's id, or the lead's person\n * key (AGL-3234); a lead has no contact to look up enrollments or inbound\n * email by, so those reads go by the lead instead.\n */\nexport interface OutreachGateLookupPerson {\n personId: string\n /** The contact's id, or `null` for a lead. */\n contactId: string | null\n /** The lead's person key, or `null` for a contact. */\n leadId: string | null\n email: string\n}\n\nexport interface ReadOutreachGateLookupsInput {\n orgId: string\n /** The sequence's site: whose suppression list and topic opt-outs apply. */\n hostId: string\n /**\n * Every site in the sequence site's consent group, the site included —\n * `consentGroupForHost(org, hostId).hostIds`. An org that declared several\n * sites one sender is one sender to the person who unsubscribed from any of\n * them, so the site suppression list and the sales opt-outs are read on\n * each. Omitted, the site alone.\n */\n consentHostIds?: readonly string[]\n people: readonly OutreachGateLookupPerson[]\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** `getAll` takes this many references at most per call. */\nconst GET_ALL_CHUNK = 300\n\nasync function getAllChunked(\n firestore: Firestore,\n refs: readonly FirebaseFirestore.DocumentReference[],\n): Promise<FirebaseFirestore.DocumentSnapshot[]> {\n const snapshots: FirebaseFirestore.DocumentSnapshot[] = []\n for (let start = 0; start < refs.length; start += GET_ALL_CHUNK) {\n snapshots.push(...(await firestore.getAll(...refs.slice(start, start + GET_ALL_CHUNK))))\n }\n return snapshots\n}\n\n/**\n * One keyed list, read for every person at once: `answer` turns each\n * person's document into their answer. An address that cannot be keyed has\n * no document anywhere and is answered as unchecked, and so is everyone\n * when the read fails.\n */\nasync function keyedLookup<T>(\n firestore: Firestore,\n people: readonly OutreachGateLookupPerson[],\n collection: (key: string) => FirebaseFirestore.DocumentReference,\n answer: (snapshot: FirebaseFirestore.DocumentSnapshot) => T,\n label: string,\n): Promise<Map<string, T | null>> {\n const answers = new Map<string, T | null>()\n const keyed: Array<{ personId: string; key: string }> = []\n for (const person of people) {\n const key = personKey(person.email)\n if (key) keyed.push({ personId: person.personId, key })\n else answers.set(person.personId, null)\n }\n if (!keyed.length) return answers\n try {\n const snapshots = await getAllChunked(\n firestore,\n keyed.map((entry) => collection(entry.key)),\n )\n keyed.forEach((entry, index) => answers.set(entry.personId, answer(snapshots[index])))\n } catch (error) {\n console.error(`[outreach] ${label} lookup failed; reading as unchecked`, error)\n for (const entry of keyed) answers.set(entry.personId, null)\n }\n return answers\n}\n\n/**\n * The same keyed lookup on every site of a consent group, folded into one\n * answer per person by `fold`. A read that failed anywhere is `null` for the\n * people it covered, so a sibling's unreadable list refuses as the site's own\n * would. A group of one is one lookup, exactly as the site alone.\n */\nasync function keyedGroupLookup<T>(\n firestore: Firestore,\n people: readonly OutreachGateLookupPerson[],\n hostIds: readonly string[],\n collection: (host: FirebaseFirestore.DocumentReference, key: string) => FirebaseFirestore.DocumentReference,\n answer: (snapshot: FirebaseFirestore.DocumentSnapshot) => T,\n fold: (answers: readonly T[]) => T,\n label: string,\n): Promise<Map<string, T | null>> {\n const bySite = await Promise.all(\n hostIds.map((id) => {\n const host = firestore.collection('hosts').doc(id)\n return keyedLookup(firestore, people, (key) => collection(host, key), answer, label)\n }),\n )\n if (bySite.length === 1) return bySite[0]\n const folded = new Map<string, T | null>()\n for (const person of people) {\n const answers = bySite.map((site) => site.get(person.personId) ?? null)\n folded.set(\n person.personId,\n answers.some((answer) => answer === null) ? null : fold(answers as T[]),\n )\n }\n return folded\n}\n\n/** Every lookup the gates take, for each person, keyed by person id. */\nexport async function readOutreachGateLookups(\n firestore: Firestore,\n input: ReadOutreachGateLookupsInput,\n): Promise<Map<string, OutreachGateLookups>> {\n const { orgId, hostId, people } = input\n // The sequence's site first, then the rest of its consent group.\n const hostIds = [\n hostId,\n ...new Set((input.consentHostIds ?? []).filter((id) => id && id !== hostId)),\n ]\n const enrollments = outreachOrgCollection(firestore, orgId, 'enrollments')\n const activities = firestore.collection('orgs').doc(orgId).collection(CRM_COLLECTIONS.activities)\n\n const [platform, site, sales, doNotContact, doNotContactDomain, roster, perPerson] = await Promise.all([\n keyedLookup(\n firestore,\n people,\n (key) => firestore.collection(EMAIL_SUPPRESSIONS_COLLECTION).doc(key),\n // A released record no longer suppresses.\n (snapshot) => snapshot.exists && !snapshot.get('releasedAt'),\n 'platform suppression',\n ),\n keyedGroupLookup(\n firestore,\n people,\n hostIds,\n (host, key) => host.collection(HOST_SUPPRESSIONS_SUBCOLLECTION).doc(key),\n (snapshot) => snapshot.exists,\n // An entry on any site of the group suppresses.\n (answers) => answers.some(Boolean),\n 'site suppression',\n ),\n keyedGroupLookup<TopicSubscriptionState>(\n firestore,\n people,\n hostIds,\n (host, key) => host.collection(TOPIC_OPT_OUTS_SUBCOLLECTION).doc(key),\n (snapshot) =>\n readTopicSubscriptionState(\n ((snapshot.exists ? snapshot.get('topics') : null) ?? {})[EMAIL_TOPIC_SALES],\n ),\n // The sequence site's own standing, unless a sibling holds a refusal:\n // leaving the sales stream anywhere in the group is leaving the sender.\n ([own, ...siblings]) => (siblings.includes('opted-out') ? 'opted-out' : own),\n 'sales topic',\n ),\n lookupOutreachDoNotContact(\n firestore,\n orgId,\n people.map((person) => person.email),\n ),\n // The domain half of the same list (AGL-3244), one `getAll` over the\n // distinct domains the people are at.\n lookupOutreachDoNotContactDomains(\n firestore,\n orgId,\n people.map((person) => person.email),\n ),\n loadCrmInboundRoster(firestore, orgId).catch((error: unknown): MemberAddresses[] | null => {\n console.error('[outreach] roster lookup failed; reading as unchecked', error)\n return null\n }),\n Promise.all(\n people.map(async (person) => {\n const open = await Promise.all([\n person.contactId\n ? enrollments.where('contactId', '==', person.contactId).get()\n : enrollments.where('leadId', '==', person.leadId ?? '').get(),\n enrollments.where('email', '==', person.email).get(),\n ])\n .then(([byContact, byEmail]) => {\n const found = new Map<string, { id: string; sequenceId: string; status: OutreachEnrollmentStatus }>()\n for (const doc of [...byContact.docs, ...byEmail.docs]) {\n found.set(doc.id, {\n id: doc.id,\n sequenceId: String(doc.get('sequenceId') ?? ''),\n status: doc.get('status') as OutreachEnrollmentStatus,\n })\n }\n return [...found.values()]\n })\n .catch((error: unknown) => {\n console.error('[outreach] enrollment lookup failed; reading as unchecked', error)\n return null\n })\n // An email the person wrote: filed on the contact, or on the lead\n // while they were one (AGL-3234).\n const inbound = await activities\n .where(person.contactId ? 'contactId' : 'leadId', '==', person.contactId ?? person.leadId ?? '')\n .where('direction', '==', 'inbound')\n .limit(1)\n .get()\n .then((snapshot) => !snapshot.empty)\n .catch((error: unknown) => {\n console.error('[outreach] inbound email lookup failed; reading as cold', error)\n return null\n })\n return { personId: person.personId, open, inbound }\n }),\n ),\n ])\n\n const byPerson = new Map(perPerson.map((entry) => [entry.personId, entry]))\n const lookups = new Map<string, OutreachGateLookups>()\n for (const person of people) {\n const own = byPerson.get(person.personId)\n lookups.set(person.personId, {\n platformSuppressed: platform.get(person.personId) ?? null,\n hostSuppressed: site.get(person.personId) ?? null,\n salesTopicState: sales.get(person.personId) ?? null,\n doNotContact: doNotContact.get(person.email) ?? null,\n doNotContactDomain: doNotContactDomain.get(person.email) ?? null,\n workspaceMembers: roster,\n openEnrollments: own?.open ?? null,\n hasInboundEmail: own?.inbound ?? null,\n })\n }\n return lookups\n}\n"],"names":["CRM_COLLECTIONS","EMAIL_TOPIC_SALES","readTopicSubscriptionState","TOPIC_OPT_OUTS_SUBCOLLECTION","personKey","loadCrmInboundRoster","EMAIL_SUPPRESSIONS_COLLECTION","HOST_SUPPRESSIONS_SUBCOLLECTION","lookupOutreachDoNotContact","lookupOutreachDoNotContactDomains","outreachOrgCollection","GET_ALL_CHUNK","getAllChunked","firestore","refs","snapshots","start","length","push","getAll","slice","keyedLookup","people","collection","answer","label","answers","Map","keyed","person","key","email","personId","set","map","entry","forEach","index","error","console","keyedGroupLookup","hostIds","fold","bySite","Promise","all","id","host","doc","folded","site","get","some","readOutreachGateLookups","input","orgId","hostId","Set","consentHostIds","filter","enrollments","activities","platform","sales","doNotContact","doNotContactDomain","roster","perPerson","snapshot","exists","Boolean","own","siblings","includes","catch","open","contactId","where","leadId","then","byContact","byEmail","found","docs","sequenceId","String","status","values","inbound","limit","empty","byPerson","lookups","platformSuppressed","hostSuppressed","salesTopicState","workspaceMembers","openEnrollments","hasInboundEmail"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/enrollment/gate-lookups.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 LOOKUPS THE ENROLLMENT GATES DECIDE ON (AGL-2980).\n *\n * `evaluateOutreachGates` is pure: it reads a contact as stored and the\n * RESULTS of the reads the runtime makes. This is those reads, for many\n * people at once — the enroll preview asks about everyone a view names in\n * one pass, and the sending runtime asks about one person before each send\n * with the same function, so the two can never consult different lists.\n *\n * Server only. Every list keyed by address is read with one `getAll` over\n * `personKey` ids — the suppression lists, the sales topic opt-outs, the\n * do-not-contact list — so a lookup costs one round trip however many\n * people it covers, and needs no index that could go missing. The roster\n * is read once. The two per-person facts that are queries — the person's\n * other enrollments and whether they ever wrote in — are equality filters\n * on single fields.\n *\n * ## A read that failed is `null`, and `null` refuses\n *\n * Each lookup is caught on its own and answers `null` for everyone it\n * covered, which the gates read as \"we couldn't check\" and refuse: a\n * person who asked not to be emailed must not be emailed because a read\n * timed out. One failed list does not blank the others.\n */\n\nimport { consentGroupTopicState } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport {\n EMAIL_TOPIC_SALES,\n readTopicSubscriptionState,\n TOPIC_OPT_OUTS_SUBCOLLECTION,\n type TopicSubscriptionState,\n} from '@aglyn/aglyn/app-utils/email-topics'\nimport type { MemberAddresses } from '@aglyn/aglyn/app-utils/member-email-aliases'\nimport { personKey } from '@aglyn/aglyn/app-utils/person-key'\nimport { loadCrmInboundRoster } from '@aglyn/tenant-data-admin/server/crm-inbound-email'\nimport {\n EMAIL_SUPPRESSIONS_COLLECTION,\n HOST_SUPPRESSIONS_SUBCOLLECTION,\n} from '@aglyn/tenant-data-admin/server/email-suppression'\nimport type { OutreachGateLookups } from '../engine/gates'\nimport type { OutreachEnrollmentStatus } from '../model/outreach.types'\nimport {\n lookupOutreachDoNotContact,\n lookupOutreachDoNotContactDomains,\n} from '../storage/do-not-contact-store'\nimport { outreachOrgCollection } from '../storage/outreach-records'\n\n/**\n * One person to look up: the record they are, and the address the steps go\n * to. `personId` keys the answer — the contact's id, or the lead's person\n * key (AGL-3234); a lead has no contact to look up enrollments or inbound\n * email by, so those reads go by the lead instead.\n */\nexport interface OutreachGateLookupPerson {\n personId: string\n /** The contact's id, or `null` for a lead. */\n contactId: string | null\n /** The lead's person key, or `null` for a contact. */\n leadId: string | null\n email: string\n}\n\nexport interface ReadOutreachGateLookupsInput {\n orgId: string\n /** The sequence's site: whose suppression list and topic opt-outs apply. */\n hostId: string\n /**\n * Every site in the sequence site's consent group, the site included —\n * `consentGroupForHost(org, hostId).hostIds`. An org that declared several\n * sites one sender is one sender to the person who unsubscribed from any of\n * them, so the site suppression list and the sales opt-outs are read on\n * each. Omitted, the site alone.\n */\n consentHostIds?: readonly string[]\n /**\n * The same group's `awaitsConfirmation`: whether a sibling's pending\n * confirmation counts in the sales stream's standing as the sequence\n * site's own does. Omitted, it does not.\n */\n consentAwaitsConfirmation?: boolean\n people: readonly OutreachGateLookupPerson[]\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** `getAll` takes this many references at most per call. */\nconst GET_ALL_CHUNK = 300\n\nasync function getAllChunked(\n firestore: Firestore,\n refs: readonly FirebaseFirestore.DocumentReference[],\n): Promise<FirebaseFirestore.DocumentSnapshot[]> {\n const snapshots: FirebaseFirestore.DocumentSnapshot[] = []\n for (let start = 0; start < refs.length; start += GET_ALL_CHUNK) {\n snapshots.push(...(await firestore.getAll(...refs.slice(start, start + GET_ALL_CHUNK))))\n }\n return snapshots\n}\n\n/**\n * One keyed list, read for every person at once: `answer` turns each\n * person's document into their answer. An address that cannot be keyed has\n * no document anywhere and is answered as unchecked, and so is everyone\n * when the read fails.\n */\nasync function keyedLookup<T>(\n firestore: Firestore,\n people: readonly OutreachGateLookupPerson[],\n collection: (key: string) => FirebaseFirestore.DocumentReference,\n answer: (snapshot: FirebaseFirestore.DocumentSnapshot) => T,\n label: string,\n): Promise<Map<string, T | null>> {\n const answers = new Map<string, T | null>()\n const keyed: Array<{ personId: string; key: string }> = []\n for (const person of people) {\n const key = personKey(person.email)\n if (key) keyed.push({ personId: person.personId, key })\n else answers.set(person.personId, null)\n }\n if (!keyed.length) return answers\n try {\n const snapshots = await getAllChunked(\n firestore,\n keyed.map((entry) => collection(entry.key)),\n )\n keyed.forEach((entry, index) => answers.set(entry.personId, answer(snapshots[index])))\n } catch (error) {\n console.error(`[outreach] ${label} lookup failed; reading as unchecked`, error)\n for (const entry of keyed) answers.set(entry.personId, null)\n }\n return answers\n}\n\n/**\n * The same keyed lookup on every site of a consent group, folded into one\n * answer per person by `fold`. A read that failed anywhere is `null` for the\n * people it covered, so a sibling's unreadable list refuses as the site's own\n * would. A group of one is one lookup, exactly as the site alone.\n */\nasync function keyedGroupLookup<T>(\n firestore: Firestore,\n people: readonly OutreachGateLookupPerson[],\n hostIds: readonly string[],\n collection: (host: FirebaseFirestore.DocumentReference, key: string) => FirebaseFirestore.DocumentReference,\n answer: (snapshot: FirebaseFirestore.DocumentSnapshot) => T,\n fold: (answers: readonly T[]) => T,\n label: string,\n): Promise<Map<string, T | null>> {\n const bySite = await Promise.all(\n hostIds.map((id) => {\n const host = firestore.collection('hosts').doc(id)\n return keyedLookup(firestore, people, (key) => collection(host, key), answer, label)\n }),\n )\n if (bySite.length === 1) return bySite[0]\n const folded = new Map<string, T | null>()\n for (const person of people) {\n const answers = bySite.map((site) => site.get(person.personId) ?? null)\n folded.set(\n person.personId,\n answers.some((answer) => answer === null) ? null : fold(answers as T[]),\n )\n }\n return folded\n}\n\n/** Every lookup the gates take, for each person, keyed by person id. */\nexport async function readOutreachGateLookups(\n firestore: Firestore,\n input: ReadOutreachGateLookupsInput,\n): Promise<Map<string, OutreachGateLookups>> {\n const { orgId, hostId, people } = input\n // The sequence's site first, then the rest of its consent group.\n const hostIds = [\n hostId,\n ...new Set((input.consentHostIds ?? []).filter((id) => id && id !== hostId)),\n ]\n const enrollments = outreachOrgCollection(firestore, orgId, 'enrollments')\n const activities = firestore.collection('orgs').doc(orgId).collection(CRM_COLLECTIONS.activities)\n\n const [platform, site, sales, doNotContact, doNotContactDomain, roster, perPerson] = await Promise.all([\n keyedLookup(\n firestore,\n people,\n (key) => firestore.collection(EMAIL_SUPPRESSIONS_COLLECTION).doc(key),\n // A released record no longer suppresses.\n (snapshot) => snapshot.exists && !snapshot.get('releasedAt'),\n 'platform suppression',\n ),\n keyedGroupLookup(\n firestore,\n people,\n hostIds,\n (host, key) => host.collection(HOST_SUPPRESSIONS_SUBCOLLECTION).doc(key),\n (snapshot) => snapshot.exists,\n // An entry on any site of the group suppresses.\n (answers) => answers.some(Boolean),\n 'site suppression',\n ),\n keyedGroupLookup<TopicSubscriptionState>(\n firestore,\n people,\n hostIds,\n (host, key) => host.collection(TOPIC_OPT_OUTS_SUBCOLLECTION).doc(key),\n (snapshot) =>\n readTopicSubscriptionState(\n ((snapshot.exists ? snapshot.get('topics') : null) ?? {})[EMAIL_TOPIC_SALES],\n ),\n // The group's standing, the way the topic filter folds it: leaving the\n // sales stream anywhere in the group is leaving the sender, and a\n // sibling's pending confirmation counts only where the org said the\n // group waits. What a pending standing means for a sales email is the\n // gates' question, not this one.\n (states) =>\n consentGroupTopicState(\n { awaitsConfirmation: input.consentAwaitsConfirmation === true },\n states,\n ),\n 'sales topic',\n ),\n lookupOutreachDoNotContact(\n firestore,\n orgId,\n people.map((person) => person.email),\n ),\n // The domain half of the same list (AGL-3244), one `getAll` over the\n // distinct domains the people are at.\n lookupOutreachDoNotContactDomains(\n firestore,\n orgId,\n people.map((person) => person.email),\n ),\n loadCrmInboundRoster(firestore, orgId).catch((error: unknown): MemberAddresses[] | null => {\n console.error('[outreach] roster lookup failed; reading as unchecked', error)\n return null\n }),\n Promise.all(\n people.map(async (person) => {\n const open = await Promise.all([\n person.contactId\n ? enrollments.where('contactId', '==', person.contactId).get()\n : enrollments.where('leadId', '==', person.leadId ?? '').get(),\n enrollments.where('email', '==', person.email).get(),\n ])\n .then(([byContact, byEmail]) => {\n const found = new Map<string, { id: string; sequenceId: string; status: OutreachEnrollmentStatus }>()\n for (const doc of [...byContact.docs, ...byEmail.docs]) {\n found.set(doc.id, {\n id: doc.id,\n sequenceId: String(doc.get('sequenceId') ?? ''),\n status: doc.get('status') as OutreachEnrollmentStatus,\n })\n }\n return [...found.values()]\n })\n .catch((error: unknown) => {\n console.error('[outreach] enrollment lookup failed; reading as unchecked', error)\n return null\n })\n // An email the person wrote: filed on the contact, or on the lead\n // while they were one (AGL-3234).\n const inbound = await activities\n .where(person.contactId ? 'contactId' : 'leadId', '==', person.contactId ?? person.leadId ?? '')\n .where('direction', '==', 'inbound')\n .limit(1)\n .get()\n .then((snapshot) => !snapshot.empty)\n .catch((error: unknown) => {\n console.error('[outreach] inbound email lookup failed; reading as cold', error)\n return null\n })\n return { personId: person.personId, open, inbound }\n }),\n ),\n ])\n\n const byPerson = new Map(perPerson.map((entry) => [entry.personId, entry]))\n const lookups = new Map<string, OutreachGateLookups>()\n for (const person of people) {\n const own = byPerson.get(person.personId)\n lookups.set(person.personId, {\n platformSuppressed: platform.get(person.personId) ?? null,\n hostSuppressed: site.get(person.personId) ?? null,\n salesTopicState: sales.get(person.personId) ?? null,\n doNotContact: doNotContact.get(person.email) ?? null,\n doNotContactDomain: doNotContactDomain.get(person.email) ?? null,\n workspaceMembers: roster,\n openEnrollments: own?.open ?? null,\n hasInboundEmail: own?.inbound ?? null,\n })\n }\n return lookups\n}\n"],"names":["consentGroupTopicState","CRM_COLLECTIONS","EMAIL_TOPIC_SALES","readTopicSubscriptionState","TOPIC_OPT_OUTS_SUBCOLLECTION","personKey","loadCrmInboundRoster","EMAIL_SUPPRESSIONS_COLLECTION","HOST_SUPPRESSIONS_SUBCOLLECTION","lookupOutreachDoNotContact","lookupOutreachDoNotContactDomains","outreachOrgCollection","GET_ALL_CHUNK","getAllChunked","firestore","refs","snapshots","start","length","push","getAll","slice","keyedLookup","people","collection","answer","label","answers","Map","keyed","person","key","email","personId","set","map","entry","forEach","index","error","console","keyedGroupLookup","hostIds","fold","bySite","Promise","all","id","host","doc","folded","site","get","some","readOutreachGateLookups","input","orgId","hostId","Set","consentHostIds","filter","enrollments","activities","platform","sales","doNotContact","doNotContactDomain","roster","perPerson","snapshot","exists","Boolean","states","awaitsConfirmation","consentAwaitsConfirmation","catch","open","contactId","where","leadId","then","byContact","byEmail","found","docs","sequenceId","String","status","values","inbound","limit","empty","byPerson","lookups","own","platformSuppressed","hostSuppressed","salesTopicState","workspaceMembers","openEnrollments","hasInboundEmail"],"mappings":"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"}
@@ -99,6 +99,7 @@ async function enrollContext(firestore, caller, sequence) {
99
99
  }),
100
100
  contactGroupId: consentGroup.groupId,
101
101
  consentHostIds: consentGroup.hostIds,
102
+ consentAwaitsConfirmation: consentGroup.awaitsConfirmation,
102
103
  siteName: host.exists ? String((_host_get = host.get('name')) != null ? _host_get : '') : ''
103
104
  };
104
105
  }
@@ -129,6 +130,7 @@ async function enrollContext(firestore, caller, sequence) {
129
130
  orgId: caller.orgId,
130
131
  hostId: sequence.hostId,
131
132
  consentHostIds: context.consentHostIds,
133
+ consentAwaitsConfirmation: context.consentAwaitsConfirmation,
132
134
  people: askable.map((candidate)=>({
133
135
  personId: candidate.personId,
134
136
  contactId: candidate.contactId || null,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/enroll-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CAMPAIGN_MEMBERSHIP_FIELD,\n contactCampaignFieldPath,\n normalizeCampaignIds,\n} from '@aglyn/aglyn/app-utils/campaign-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n CRM_COLLECTIONS,\n crmLeadStatus,\n crmViewIsListed,\n isCrmLeadOpen,\n normalizeCrmViewFilters,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { dynamicListDimensionsForCrmView } from '@aglyn/aglyn/app-utils/dynamic-list-rule'\nimport type { PluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { outreachEnrolledEntry } from '../engine/enrollment-activity'\nimport { buildOutreachEnrollment, planOutreachFirstDue } from '../engine/enrollment-state'\nimport type { OutreachGateLookups } from '../engine/gates'\nimport {\n decideOutreachEnrollment,\n previewOutreachPerson,\n readOutreachEnrollCandidates,\n type OutreachEnrollCandidate,\n type OutreachGateQuestion,\n} from '../enrollment/enroll-people'\nimport { readOutreachGateLookups } from '../enrollment/gate-lookups'\nimport { effectiveOutreachAllowedCountries } from '../model/compliance-settings'\nimport {\n OUTREACH_ENROLL_BATCH_MAX,\n type OutreachEnrollOutcome,\n type OutreachEnrollPreviewResponse,\n type OutreachEnrollResponse,\n type OutreachPersonRef,\n} from '../model/outreach-api'\nimport type { OutreachSequence } from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentId,\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller, type OutreachRouteExtraPermission } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ENROLLING PEOPLE IN A SEQUENCE (AGL-2980): `outreach/enroll/preview` and\n * `outreach/enroll`.\n *\n * The preview reads the people a saved Contacts view or a search named —\n * at most {@link OUTREACH_ENROLL_BATCH_MAX} — and says where each stands:\n * eligible, blocked with the gate's own sentence, or waiting on the rep for\n * a personal line and the attestations only they can make. Confirm reads\n * everything again and asks the gates again with what the rep supplied,\n * because a preview is a picture of a moment: someone can unsubscribe\n * between the two, and a request can claim anything. Nothing the preview\n * answered is trusted by the enroll.\n *\n * Both read the CRM's people, so both ask for `data.manage` beside\n * `outreach.use` — the permission the contacts' own rules read with, so\n * Outreach is no side door into the address book.\n *\n * Only an ACTIVE sequence takes people: enrolling schedules the first step\n * in the mailbox's hours, and a draft's steps and a paused sequence's timing\n * are not yet what will be sent.\n */\n\n/** The permission enrolling reads the CRM with. */\nexport const OUTREACH_READS_PEOPLE: OutreachRouteExtraPermission = {\n key: 'data.manage',\n refusal: \"Enrolling reads your CRM's contacts, and your role does not include Manage data.\",\n}\n\n/** Everything enrolling reaches beyond the routes' own dependencies. */\nexport interface OutreachEnrollRouteDeps extends OutreachRouteDeps {\n /**\n * The addresses a saved Contacts view selects among one site's contacts,\n * read the way the dynamic-list sweep reads one, and whether the read\n * reached the whole view before its budget.\n */\n crmViewEmails(input: { hostId: string; viewId: string }): Promise<{ emails: string[]; complete: boolean }>\n /**\n * The workspace's record system on the timeline seam (AGL-3274), when a\n * plugin keeps one: the enroll files \"Enrolled in\" on the person's record\n * — the runtime's `timeline`, reached the same way.\n */\n timeline(): PluginRecordTimelineWriter | null\n}\n\nexport interface OutreachEnrollRoutes {\n preview: PluginWebApiHandler\n confirm: PluginWebApiHandler\n}\n\n/** The activity line an enroll writes. */\nexport function outreachEnrolledActivity(count: number): string {\n return count === 1\n ? 'Enrolled 1 person in a sequence'\n : `Enrolled ${count} people in a sequence`\n}\n\n/** Lookups for a person the gates will never be asked about: every one unchecked. */\nconst UNCHECKED: OutreachGateLookups = {\n platformSuppressed: null,\n hostSuppressed: null,\n salesTopicState: null,\n doNotContact: null,\n doNotContactDomain: null,\n workspaceMembers: null,\n openEnrollments: null,\n hasInboundEmail: null,\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\nasync function loadActiveSequence(\n firestore: Firestore,\n orgId: string,\n rawSequenceId: unknown,\n): Promise<Response | OutreachSequence> {\n const sequenceId = readOutreachDocumentId(rawSequenceId)\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to enroll people in.')\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.status === 'archived') {\n return outreachRefusal(409, 'sequence-archived', 'This sequence is archived and takes no one new.')\n }\n if (sequence.status !== 'active') {\n return outreachRefusal(409, 'sequence-not-active', 'Activate this sequence before enrolling people.')\n }\n return sequence\n}\n\n/** What every person in one request is judged against. */\ninterface EnrollContext {\n settings: OutreachGateQuestion['settings']\n contactGroupId: string\n /** The sequence site's consent group, whose opt-outs all apply. */\n consentHostIds: readonly string[]\n siteName: string\n}\n\nasync function enrollContext(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n): Promise<EnrollContext> {\n const [orgSettings, host] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n firestore.collection('hosts').doc(sequence.hostId).get(),\n ])\n const consentGroup = consentGroupForHost(caller.org, sequence.hostId)\n return {\n // The organization's countries are a ceiling over the sequence's.\n settings: {\n ...sequence.settings,\n allowedCountries: effectiveOutreachAllowedCountries(\n sequence.settings.allowedCountries,\n orgSettings.allowedCountries,\n ),\n },\n contactGroupId: consentGroup.groupId,\n consentHostIds: consentGroup.hostIds,\n siteName: host.exists ? String(host.get('name') ?? '') : '',\n }\n}\n\n/**\n * The candidates, the enrollments they already have here, and the gates'\n * lookups — every map keyed by the person's own id.\n *\n * \"Already in this sequence\" is answered two ways (AGL-3234): by the id the\n * person's enrollment would have, and by any enrollment in this sequence\n * that carries the person's ADDRESS. The second is what keeps a contact\n * from being enrolled beside the lead they were converted from: the lead's\n * enrollment followed them, under the lead's key, and still names them.\n */\nasync function readPeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n context: EnrollContext,\n people: readonly OutreachPersonRef[],\n): Promise<{\n candidates: OutreachEnrollCandidate[]\n enrolledHere: Set<string>\n lookups: Map<string, OutreachGateLookups>\n}> {\n const candidates = await readOutreachEnrollCandidates(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n contactGroupId: context.contactGroupId,\n people,\n })\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const existing = candidates.length\n ? await firestore.getAll(\n ...candidates.map((candidate) => enrollments.doc(outreachEnrollmentId(sequence.id, candidate.personId))),\n )\n : []\n const enrolledHere = new Set(\n candidates.filter((_candidate, index) => existing[index]?.exists).map((candidate) => candidate.personId),\n )\n const askable = candidates.filter((candidate) => candidate.contact && candidate.visible && candidate.email)\n const lookups = askable.length\n ? await readOutreachGateLookups(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n consentHostIds: context.consentHostIds,\n people: askable.map((candidate) => ({\n personId: candidate.personId,\n contactId: candidate.contactId || null,\n leadId: candidate.leadId,\n email: String(candidate.email),\n })),\n })\n : new Map<string, OutreachGateLookups>()\n for (const candidate of askable) {\n const open = lookups.get(candidate.personId)?.openEnrollments ?? []\n if (open.some((entry) => entry.sequenceId === sequence.id)) enrolledHere.add(candidate.personId)\n }\n return { candidates, enrolledHere, lookups }\n}\n\nconst uniqueIds = (values: unknown): string[] =>\n Array.isArray(values)\n ? [...new Set(values.map(readOutreachDocumentId).filter((id): id is string => id !== null))]\n : []\n\n/** The most leads a saved Leads view reads — the Leads list's own window. */\nconst LEADS_VIEW_WINDOW = 200\n\n/**\n * The people a saved LEADS view selects (AGL-3234): the sequence's site's\n * most recently seen leads, narrowed by the view's status clause the way\n * the Leads list narrows its window — open leads when the view names no\n * status, one status when it does, everything for `all`. The person key IS\n * the id, so there is no address to resolve — and since AGL-3275 the window is\n * narrowed by `visibleTo` rather than by the collection's parent.\n */\nasync function leadsViewPeople(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n filters: ReturnType<typeof normalizeCrmViewFilters>,\n): Promise<{ people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n const statusClause = filters.find((clause) => clause.field === 'status' && clause.op === 'equals')\n const wanted = String(statusClause?.value ?? 'open')\n /*\n * SCOPED (AGL-3275). The collection is org-wide, so the window is narrowed\n * to what the sequence's site may see before it is ordered — otherwise an\n * agency's sequence would offer another client's people to enroll. The\n * composite index for `visibleTo array-contains-any` + `lastSeenAtMs desc`\n * is in `cloud/firestore.indexes.json`.\n */\n const window = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('leads')\n .where('visibleTo', 'array-contains-any', scopeTokensForHost(sequence.hostId))\n .orderBy('lastSeenAtMs', 'desc')\n .limit(LEADS_VIEW_WINDOW + 1)\n .get()\n const matching = window.docs.slice(0, LEADS_VIEW_WINDOW).filter((doc) => {\n const lead = doc.data() as Record<string, unknown>\n if (wanted === 'all') return true\n if (wanted === 'open') return isCrmLeadOpen(lead as never) && !lead['convertedContactId']\n return crmLeadStatus(lead as never) === wanted\n })\n return {\n people: matching.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((doc) => ({ kind: 'lead', id: doc.id })),\n total: matching.length,\n truncated: matching.length > OUTREACH_ENROLL_BATCH_MAX || window.docs.length > LEADS_VIEW_WINDOW,\n }\n}\n\nexport function createOutreachEnrollRoutes(deps: OutreachEnrollRouteDeps): OutreachEnrollRoutes {\n /**\n * The person joins the sequence's campaigns (AGL-3254), the moment they\n * are enrolled: `arrayUnion` on the lead's own `campaignIds`, or on the\n * contact's facet for the sequence's site — the same field, the same\n * shape, every other campaign member carries — and `enrolled` credited\n * to each campaign. After the enrollment is created and never before:\n * a person the create refused joins nothing. Neither write may fail the\n * enrollment, which already exists; a membership that could not be\n * stamped is one the list's bulk bar can add by hand.\n */\n async function joinSequenceCampaigns(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n contactGroupId: string,\n candidate: OutreachEnrollCandidate,\n nowMs: number,\n ): Promise<void> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length) return\n try {\n const ref =\n candidate.target === 'lead' && candidate.leadId\n ? firestore.collection('orgs').doc(orgId).collection('leads').doc(candidate.leadId)\n : candidate.contactId\n ? firestore.collection('orgs').doc(orgId).collection('contacts').doc(candidate.contactId)\n : null\n if (ref) {\n const field =\n candidate.target === 'lead' ? CAMPAIGN_MEMBERSHIP_FIELD : contactCampaignFieldPath(contactGroupId)\n await ref.update({ [field]: FieldValue.arrayUnion(...campaignIds), updatedAt: FieldValue.serverTimestamp() })\n }\n } catch (error) {\n console.error('[outreach] the enrolled person could not join the sequence’s campaigns', error)\n }\n try {\n await deps.creditCampaign({ hostId: sequence.hostId, orgId, campaignIds, outcome: 'enrolled', atMs: nowMs })\n } catch (error) {\n console.error('[outreach] the enrollment could not be credited to the sequence’s campaigns', error)\n }\n }\n\n /**\n * The names of the sequence's campaigns as they stand, for the entry the\n * enroll files on the person's record (AGL-3274) — the org's containers,\n * `orgs/{orgId}/emailCampaigns`. Read once per request, not per person; a\n * container that is gone answers nothing and the entry names the rest.\n */\n async function sequenceCampaignNames(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n ): Promise<string[]> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length || !orgId) return []\n try {\n const containers = firestore.collection('orgs').doc(orgId).collection('emailCampaigns')\n const found = await firestore.getAll(...campaignIds.map((id) => containers.doc(id)))\n return found.map((snapshot) => String(snapshot.get('name') ?? '').trim()).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the sequence’s campaigns could not be named for the record', error)\n return []\n }\n }\n\n /**\n * The people a source names, the batch limit applied: every contact a\n * search picked, every lead picked from the sequence's site, or the\n * people a saved Contacts or Leads view selects there — contacts found by\n * address, leads by their own key.\n */\n async function sourcePeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n source: Record<string, unknown>,\n ): Promise<Response | { people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n if (source['kind'] === 'contacts' || source['kind'] === 'leads') {\n const kind = source['kind'] === 'leads' ? 'lead' : 'contact'\n const ids = uniqueIds(kind === 'lead' ? source['leadIds'] : source['contactIds'])\n if (!ids.length) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n return {\n people: ids.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((id) => ({ kind, id })),\n total: ids.length,\n truncated: ids.length > OUTREACH_ENROLL_BATCH_MAX,\n }\n }\n if (source['kind'] !== 'view') {\n return outreachRefusal(400, 'invalid-request', 'Enroll people from a saved view or a search.')\n }\n const viewId = readOutreachDocumentId(source['viewId'])\n const view = viewId\n ? await firestore.collection('orgs').doc(caller.orgId).collection(CRM_COLLECTIONS.views).doc(viewId).get()\n : null\n const data = view?.exists ? (view.data() as Record<string, unknown>) : null\n // A colleague's private view is theirs: it is not listed for this\n // reader, so it does not exist for them here either.\n if (\n !viewId ||\n !data ||\n !crmViewIsListed({ shared: data['shared'] === true, ownerUid: String(data['ownerUid'] ?? '') }, caller.uid)\n ) {\n return outreachRefusal(404, 'view-not-found', 'That saved view no longer exists.')\n }\n if (data['section'] === 'leads') {\n return leadsViewPeople(firestore, caller.orgId, sequence, normalizeCrmViewFilters(data['filters']))\n }\n if (data['section'] !== 'contacts') {\n return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts or Leads.')\n }\n const { unsupported } = dynamicListDimensionsForCrmView(normalizeCrmViewFilters(data['filters']))\n if (unsupported.length) {\n // Dropping the filter enrolling cannot read would enroll more people\n // than the view shows, so the view is refused whole.\n return outreachRefusal(\n 400,\n 'view-unsupported',\n `This view filters on ${unsupported.map((clause) => clause.label || clause.field).join(', ')}, which enrolling can't apply. Pick another view, or search.`,\n )\n }\n const { emails, complete } = await deps.crmViewEmails({ hostId: sequence.hostId, viewId })\n const ordered = [...new Set(emails)].sort()\n const contacts = firestore.collection('orgs').doc(caller.orgId).collection('contacts')\n const found = await Promise.all(\n ordered\n .slice(0, OUTREACH_ENROLL_BATCH_MAX)\n .map((email) => findContactByEmail(contacts, email, { hostId: sequence.hostId })),\n )\n return {\n people: [...new Set(found.filter((snapshot) => snapshot).map((snapshot) => String(snapshot?.id)))].map(\n (id) => ({ kind: 'contact', id }),\n ),\n total: ordered.length,\n truncated: ordered.length > OUTREACH_ENROLL_BATCH_MAX || !complete,\n }\n }\n\n const preview: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n const source = body['source'] && typeof body['source'] === 'object' ? (body['source'] as Record<string, unknown>) : {}\n const named = await sourcePeople(firestore, caller, sequence, source)\n if (named instanceof Response) return named\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n named.people,\n )\n const people = candidates.map((candidate) =>\n previewOutreachPerson(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n ),\n )\n return outreachOk({\n ok: true,\n people,\n total: named.total,\n truncated: named.truncated,\n } satisfies OutreachEnrollPreviewResponse)\n }\n\n const confirm: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n\n // Each person once, by whichever record they are: a contact by id, or a\n // lead by its key (AGL-3234). A person named both ways is a contact.\n const requested = new Map<\n string,\n { ref: OutreachPersonRef; personalLine: string; attestations: unknown[] }\n >()\n for (const entry of Array.isArray(body['people']) ? body['people'] : []) {\n const person = (entry && typeof entry === 'object' ? entry : {}) as Record<string, unknown>\n const contactId = readOutreachDocumentId(person['contactId'])\n const leadId = readOutreachDocumentId(person['leadId'])\n const ref: OutreachPersonRef | null = contactId\n ? { kind: 'contact', id: contactId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : null\n if (!ref || requested.has(ref.id)) continue\n requested.set(ref.id, {\n ref,\n personalLine: typeof person['personalLine'] === 'string' ? person['personalLine'] : '',\n attestations: Array.isArray(person['attestations']) ? person['attestations'] : [],\n })\n }\n if (!requested.size) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n if (requested.size > OUTREACH_ENROLL_BATCH_MAX) {\n return outreachRefusal(\n 400,\n 'invalid-request',\n `Enroll at most ${OUTREACH_ENROLL_BATCH_MAX} people at a time.`,\n )\n }\n\n const mailboxSnapshot = sequence.mailboxId\n ? await outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get()\n : null\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n if (!mailbox || mailbox.status === 'disconnected') {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's mailbox is no longer connected. Choose another mailbox for it before enrolling people.\",\n )\n }\n const nowMs = deps.now()\n if (planOutreachFirstDue({ sequence, mailbox, enrolledAtMs: nowMs, random: deps.random }) === null) {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's sending hours never open in its mailbox's timezone. Fix the hours before enrolling people.\",\n )\n }\n\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n [...requested.values()].map((entry) => entry.ref),\n )\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const campaignNames = await sequenceCampaignNames(firestore, caller.orgId, sequence)\n const results: OutreachEnrollOutcome[] = await Promise.all(\n candidates.map(async (candidate): Promise<OutreachEnrollOutcome> => {\n const named = {\n personId: candidate.personId,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n }\n const supplied = requested.get(candidate.personId) ?? { personalLine: '', attestations: [] }\n const decision = decideOutreachEnrollment(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n { personalLine: supplied.personalLine, attestations: supplied.attestations, uid: caller.uid, nowMs },\n )\n if (!decision.allowed || !decision.email) {\n return { ...named, email: candidate.email, outcome: 'blocked', blocks: decision.blocks }\n }\n const id = outreachEnrollmentId(sequence.id, candidate.personId)\n const enrollment = buildOutreachEnrollment({\n id,\n sequence,\n mailbox,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n contactName: candidate.name,\n email: decision.email,\n cold: decision.cold,\n personalLine: supplied.personalLine,\n attestations: decision.attestations,\n enrolledByUid: caller.uid,\n nowMs,\n random: deps.random,\n })\n try {\n await enrollments.doc(id).create(enrollment)\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n return {\n ...named,\n email: candidate.email,\n outcome: 'blocked',\n blocks: [\n {\n code: 'already_in_sequence',\n reason: \"This person has already been in this sequence, so they won't be sent its emails again.\",\n },\n ],\n }\n }\n await joinSequenceCampaigns(firestore, caller.orgId, sequence, context.contactGroupId, candidate, nowMs)\n // The person's record says so (AGL-3274): \"Enrolled in <sequence>\",\n // with the campaigns it carried them into, once per enrollment.\n const entry = outreachEnrolledEntry({ enrollmentId: id, sequenceName: sequence.name, campaignNames })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n return { ...named, email: decision.email, outcome: 'enrolled', enrollmentId: id }\n }),\n )\n const enrolled = results.filter((result) => result.outcome === 'enrolled').length\n if (enrolled) {\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachEnrolledActivity(enrolled),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n }\n return outreachOk({ ok: true, results, enrolled } satisfies OutreachEnrollResponse)\n }\n\n return { preview, confirm }\n}\n"],"names":["CAMPAIGN_MEMBERSHIP_FIELD","contactCampaignFieldPath","normalizeCampaignIds","consentGroupForHost","scopeTokensForHost","CRM_COLLECTIONS","crmLeadStatus","crmViewIsListed","isCrmLeadOpen","normalizeCrmViewFilters","dynamicListDimensionsForCrmView","findContactByEmail","FieldValue","outreachEnrolledEntry","buildOutreachEnrollment","planOutreachFirstDue","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","fileOutreachNote","outreachEnrollmentId","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_READS_PEOPLE","key","refusal","outreachEnrolledActivity","count","UNCHECKED","platformSuppressed","hostSuppressed","salesTopicState","doNotContact","doNotContactDomain","workspaceMembers","openEnrollments","hasInboundEmail","ALREADY_EXISTS","loadActiveSequence","firestore","orgId","rawSequenceId","sequenceId","snapshot","doc","get","sequence","exists","data","undefined","status","enrollContext","caller","host","orgSettings","Promise","all","collection","hostId","consentGroup","org","settings","allowedCountries","contactGroupId","groupId","consentHostIds","hostIds","siteName","String","readPeople","context","people","candidates","enrollments","existing","length","getAll","map","candidate","id","personId","enrolledHere","Set","filter","_candidate","index","askable","contact","visible","email","lookups","contactId","leadId","Map","open","some","entry","add","uniqueIds","values","Array","isArray","LEADS_VIEW_WINDOW","leadsViewPeople","filters","statusClause","find","clause","field","op","wanted","value","window","where","orderBy","limit","matching","docs","slice","lead","kind","total","truncated","createOutreachEnrollRoutes","deps","joinSequenceCampaigns","nowMs","campaignIds","ref","target","update","arrayUnion","updatedAt","serverTimestamp","error","console","creditCampaign","outcome","atMs","sequenceCampaignNames","containers","found","trim","Boolean","sourcePeople","source","ids","viewId","view","views","shared","ownerUid","uid","unsupported","label","join","emails","complete","crmViewEmails","ordered","sort","contacts","preview","request","method","body","gate","Response","named","alreadyInSequence","has","ok","confirm","requested","person","set","personalLine","attestations","size","mailboxSnapshot","mailboxId","mailbox","now","enrolledAtMs","random","campaignNames","results","supplied","decision","allowed","blocks","enrollment","contactName","name","cold","enrolledByUid","create","code","reason","enrollmentId","sequenceName","link","dedupeKey","enrolled","result","logOrgActivity","type"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/enroll-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n CAMPAIGN_MEMBERSHIP_FIELD,\n contactCampaignFieldPath,\n normalizeCampaignIds,\n} from '@aglyn/aglyn/app-utils/campaign-membership'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n CRM_COLLECTIONS,\n crmLeadStatus,\n crmViewIsListed,\n isCrmLeadOpen,\n normalizeCrmViewFilters,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { dynamicListDimensionsForCrmView } from '@aglyn/aglyn/app-utils/dynamic-list-rule'\nimport type { PluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { outreachEnrolledEntry } from '../engine/enrollment-activity'\nimport { buildOutreachEnrollment, planOutreachFirstDue } from '../engine/enrollment-state'\nimport type { OutreachGateLookups } from '../engine/gates'\nimport {\n decideOutreachEnrollment,\n previewOutreachPerson,\n readOutreachEnrollCandidates,\n type OutreachEnrollCandidate,\n type OutreachGateQuestion,\n} from '../enrollment/enroll-people'\nimport { readOutreachGateLookups } from '../enrollment/gate-lookups'\nimport { effectiveOutreachAllowedCountries } from '../model/compliance-settings'\nimport {\n OUTREACH_ENROLL_BATCH_MAX,\n type OutreachEnrollOutcome,\n type OutreachEnrollPreviewResponse,\n type OutreachEnrollResponse,\n type OutreachPersonRef,\n} from '../model/outreach-api'\nimport type { OutreachSequence } from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport { fileOutreachNote } from '../runtime/timeline'\nimport {\n outreachEnrollmentId,\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { OUTREACH_SEQUENCE_ACTIVITY_TARGET, type OutreachRouteDeps } from './route-deps'\nimport { outreachRouteGate, type OutreachRouteCaller, type OutreachRouteExtraPermission } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ENROLLING PEOPLE IN A SEQUENCE (AGL-2980): `outreach/enroll/preview` and\n * `outreach/enroll`.\n *\n * The preview reads the people a saved Contacts view or a search named —\n * at most {@link OUTREACH_ENROLL_BATCH_MAX} — and says where each stands:\n * eligible, blocked with the gate's own sentence, or waiting on the rep for\n * a personal line and the attestations only they can make. Confirm reads\n * everything again and asks the gates again with what the rep supplied,\n * because a preview is a picture of a moment: someone can unsubscribe\n * between the two, and a request can claim anything. Nothing the preview\n * answered is trusted by the enroll.\n *\n * Both read the CRM's people, so both ask for `data.manage` beside\n * `outreach.use` — the permission the contacts' own rules read with, so\n * Outreach is no side door into the address book.\n *\n * Only an ACTIVE sequence takes people: enrolling schedules the first step\n * in the mailbox's hours, and a draft's steps and a paused sequence's timing\n * are not yet what will be sent.\n */\n\n/** The permission enrolling reads the CRM with. */\nexport const OUTREACH_READS_PEOPLE: OutreachRouteExtraPermission = {\n key: 'data.manage',\n refusal: \"Enrolling reads your CRM's contacts, and your role does not include Manage data.\",\n}\n\n/** Everything enrolling reaches beyond the routes' own dependencies. */\nexport interface OutreachEnrollRouteDeps extends OutreachRouteDeps {\n /**\n * The addresses a saved Contacts view selects among one site's contacts,\n * read the way the dynamic-list sweep reads one, and whether the read\n * reached the whole view before its budget.\n */\n crmViewEmails(input: { hostId: string; viewId: string }): Promise<{ emails: string[]; complete: boolean }>\n /**\n * The workspace's record system on the timeline seam (AGL-3274), when a\n * plugin keeps one: the enroll files \"Enrolled in\" on the person's record\n * — the runtime's `timeline`, reached the same way.\n */\n timeline(): PluginRecordTimelineWriter | null\n}\n\nexport interface OutreachEnrollRoutes {\n preview: PluginWebApiHandler\n confirm: PluginWebApiHandler\n}\n\n/** The activity line an enroll writes. */\nexport function outreachEnrolledActivity(count: number): string {\n return count === 1\n ? 'Enrolled 1 person in a sequence'\n : `Enrolled ${count} people in a sequence`\n}\n\n/** Lookups for a person the gates will never be asked about: every one unchecked. */\nconst UNCHECKED: OutreachGateLookups = {\n platformSuppressed: null,\n hostSuppressed: null,\n salesTopicState: null,\n doNotContact: null,\n doNotContactDomain: null,\n workspaceMembers: null,\n openEnrollments: null,\n hasInboundEmail: null,\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\nasync function loadActiveSequence(\n firestore: Firestore,\n orgId: string,\n rawSequenceId: unknown,\n): Promise<Response | OutreachSequence> {\n const sequenceId = readOutreachDocumentId(rawSequenceId)\n if (!sequenceId) return outreachRefusal(400, 'invalid-request', 'Name the sequence to enroll people in.')\n const snapshot = await outreachOrgCollection(firestore, orgId, 'sequences').doc(sequenceId).get()\n const sequence = readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined)\n if (!sequence) return outreachRefusal(404, 'sequence-not-found', 'That sequence no longer exists.')\n if (sequence.status === 'archived') {\n return outreachRefusal(409, 'sequence-archived', 'This sequence is archived and takes no one new.')\n }\n if (sequence.status !== 'active') {\n return outreachRefusal(409, 'sequence-not-active', 'Activate this sequence before enrolling people.')\n }\n return sequence\n}\n\n/** What every person in one request is judged against. */\ninterface EnrollContext {\n settings: OutreachGateQuestion['settings']\n contactGroupId: string\n /** The sequence site's consent group, whose opt-outs all apply. */\n consentHostIds: readonly string[]\n /** Whether that group's sites wait for each other's confirmation click. */\n consentAwaitsConfirmation: boolean\n siteName: string\n}\n\nasync function enrollContext(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n): Promise<EnrollContext> {\n const [orgSettings, host] = await Promise.all([\n readOutreachComplianceSettingsDoc(firestore, caller.orgId),\n firestore.collection('hosts').doc(sequence.hostId).get(),\n ])\n const consentGroup = consentGroupForHost(caller.org, sequence.hostId)\n return {\n // The organization's countries are a ceiling over the sequence's.\n settings: {\n ...sequence.settings,\n allowedCountries: effectiveOutreachAllowedCountries(\n sequence.settings.allowedCountries,\n orgSettings.allowedCountries,\n ),\n },\n contactGroupId: consentGroup.groupId,\n consentHostIds: consentGroup.hostIds,\n consentAwaitsConfirmation: consentGroup.awaitsConfirmation,\n siteName: host.exists ? String(host.get('name') ?? '') : '',\n }\n}\n\n/**\n * The candidates, the enrollments they already have here, and the gates'\n * lookups — every map keyed by the person's own id.\n *\n * \"Already in this sequence\" is answered two ways (AGL-3234): by the id the\n * person's enrollment would have, and by any enrollment in this sequence\n * that carries the person's ADDRESS. The second is what keeps a contact\n * from being enrolled beside the lead they were converted from: the lead's\n * enrollment followed them, under the lead's key, and still names them.\n */\nasync function readPeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n context: EnrollContext,\n people: readonly OutreachPersonRef[],\n): Promise<{\n candidates: OutreachEnrollCandidate[]\n enrolledHere: Set<string>\n lookups: Map<string, OutreachGateLookups>\n}> {\n const candidates = await readOutreachEnrollCandidates(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n contactGroupId: context.contactGroupId,\n people,\n })\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const existing = candidates.length\n ? await firestore.getAll(\n ...candidates.map((candidate) => enrollments.doc(outreachEnrollmentId(sequence.id, candidate.personId))),\n )\n : []\n const enrolledHere = new Set(\n candidates.filter((_candidate, index) => existing[index]?.exists).map((candidate) => candidate.personId),\n )\n const askable = candidates.filter((candidate) => candidate.contact && candidate.visible && candidate.email)\n const lookups = askable.length\n ? await readOutreachGateLookups(firestore, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n consentHostIds: context.consentHostIds,\n consentAwaitsConfirmation: context.consentAwaitsConfirmation,\n people: askable.map((candidate) => ({\n personId: candidate.personId,\n contactId: candidate.contactId || null,\n leadId: candidate.leadId,\n email: String(candidate.email),\n })),\n })\n : new Map<string, OutreachGateLookups>()\n for (const candidate of askable) {\n const open = lookups.get(candidate.personId)?.openEnrollments ?? []\n if (open.some((entry) => entry.sequenceId === sequence.id)) enrolledHere.add(candidate.personId)\n }\n return { candidates, enrolledHere, lookups }\n}\n\nconst uniqueIds = (values: unknown): string[] =>\n Array.isArray(values)\n ? [...new Set(values.map(readOutreachDocumentId).filter((id): id is string => id !== null))]\n : []\n\n/** The most leads a saved Leads view reads — the Leads list's own window. */\nconst LEADS_VIEW_WINDOW = 200\n\n/**\n * The people a saved LEADS view selects (AGL-3234): the sequence's site's\n * most recently seen leads, narrowed by the view's status clause the way\n * the Leads list narrows its window — open leads when the view names no\n * status, one status when it does, everything for `all`. The person key IS\n * the id, so there is no address to resolve — and since AGL-3275 the window is\n * narrowed by `visibleTo` rather than by the collection's parent.\n */\nasync function leadsViewPeople(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n filters: ReturnType<typeof normalizeCrmViewFilters>,\n): Promise<{ people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n const statusClause = filters.find((clause) => clause.field === 'status' && clause.op === 'equals')\n const wanted = String(statusClause?.value ?? 'open')\n /*\n * SCOPED (AGL-3275). The collection is org-wide, so the window is narrowed\n * to what the sequence's site may see before it is ordered — otherwise an\n * agency's sequence would offer another client's people to enroll. The\n * composite index for `visibleTo array-contains-any` + `lastSeenAtMs desc`\n * is in `cloud/firestore.indexes.json`.\n */\n const window = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('leads')\n .where('visibleTo', 'array-contains-any', scopeTokensForHost(sequence.hostId))\n .orderBy('lastSeenAtMs', 'desc')\n .limit(LEADS_VIEW_WINDOW + 1)\n .get()\n const matching = window.docs.slice(0, LEADS_VIEW_WINDOW).filter((doc) => {\n const lead = doc.data() as Record<string, unknown>\n if (wanted === 'all') return true\n if (wanted === 'open') return isCrmLeadOpen(lead as never) && !lead['convertedContactId']\n return crmLeadStatus(lead as never) === wanted\n })\n return {\n people: matching.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((doc) => ({ kind: 'lead', id: doc.id })),\n total: matching.length,\n truncated: matching.length > OUTREACH_ENROLL_BATCH_MAX || window.docs.length > LEADS_VIEW_WINDOW,\n }\n}\n\nexport function createOutreachEnrollRoutes(deps: OutreachEnrollRouteDeps): OutreachEnrollRoutes {\n /**\n * The person joins the sequence's campaigns (AGL-3254), the moment they\n * are enrolled: `arrayUnion` on the lead's own `campaignIds`, or on the\n * contact's facet for the sequence's site — the same field, the same\n * shape, every other campaign member carries — and `enrolled` credited\n * to each campaign. After the enrollment is created and never before:\n * a person the create refused joins nothing. Neither write may fail the\n * enrollment, which already exists; a membership that could not be\n * stamped is one the list's bulk bar can add by hand.\n */\n async function joinSequenceCampaigns(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n contactGroupId: string,\n candidate: OutreachEnrollCandidate,\n nowMs: number,\n ): Promise<void> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length) return\n try {\n const ref =\n candidate.target === 'lead' && candidate.leadId\n ? firestore.collection('orgs').doc(orgId).collection('leads').doc(candidate.leadId)\n : candidate.contactId\n ? firestore.collection('orgs').doc(orgId).collection('contacts').doc(candidate.contactId)\n : null\n if (ref) {\n const field =\n candidate.target === 'lead' ? CAMPAIGN_MEMBERSHIP_FIELD : contactCampaignFieldPath(contactGroupId)\n await ref.update({ [field]: FieldValue.arrayUnion(...campaignIds), updatedAt: FieldValue.serverTimestamp() })\n }\n } catch (error) {\n console.error('[outreach] the enrolled person could not join the sequence’s campaigns', error)\n }\n try {\n await deps.creditCampaign({ hostId: sequence.hostId, orgId, campaignIds, outcome: 'enrolled', atMs: nowMs })\n } catch (error) {\n console.error('[outreach] the enrollment could not be credited to the sequence’s campaigns', error)\n }\n }\n\n /**\n * The names of the sequence's campaigns as they stand, for the entry the\n * enroll files on the person's record (AGL-3274) — the org's containers,\n * `orgs/{orgId}/emailCampaigns`. Read once per request, not per person; a\n * container that is gone answers nothing and the entry names the rest.\n */\n async function sequenceCampaignNames(\n firestore: Firestore,\n orgId: string,\n sequence: OutreachSequence,\n ): Promise<string[]> {\n const campaignIds = normalizeCampaignIds(sequence.campaignIds)\n if (!campaignIds.length || !orgId) return []\n try {\n const containers = firestore.collection('orgs').doc(orgId).collection('emailCampaigns')\n const found = await firestore.getAll(...campaignIds.map((id) => containers.doc(id)))\n return found.map((snapshot) => String(snapshot.get('name') ?? '').trim()).filter(Boolean)\n } catch (error) {\n console.error('[outreach] the sequence’s campaigns could not be named for the record', error)\n return []\n }\n }\n\n /**\n * The people a source names, the batch limit applied: every contact a\n * search picked, every lead picked from the sequence's site, or the\n * people a saved Contacts or Leads view selects there — contacts found by\n * address, leads by their own key.\n */\n async function sourcePeople(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n sequence: OutreachSequence,\n source: Record<string, unknown>,\n ): Promise<Response | { people: OutreachPersonRef[]; total: number; truncated: boolean }> {\n if (source['kind'] === 'contacts' || source['kind'] === 'leads') {\n const kind = source['kind'] === 'leads' ? 'lead' : 'contact'\n const ids = uniqueIds(kind === 'lead' ? source['leadIds'] : source['contactIds'])\n if (!ids.length) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n return {\n people: ids.slice(0, OUTREACH_ENROLL_BATCH_MAX).map((id) => ({ kind, id })),\n total: ids.length,\n truncated: ids.length > OUTREACH_ENROLL_BATCH_MAX,\n }\n }\n if (source['kind'] !== 'view') {\n return outreachRefusal(400, 'invalid-request', 'Enroll people from a saved view or a search.')\n }\n const viewId = readOutreachDocumentId(source['viewId'])\n const view = viewId\n ? await firestore.collection('orgs').doc(caller.orgId).collection(CRM_COLLECTIONS.views).doc(viewId).get()\n : null\n const data = view?.exists ? (view.data() as Record<string, unknown>) : null\n // A colleague's private view is theirs: it is not listed for this\n // reader, so it does not exist for them here either.\n if (\n !viewId ||\n !data ||\n !crmViewIsListed({ shared: data['shared'] === true, ownerUid: String(data['ownerUid'] ?? '') }, caller.uid)\n ) {\n return outreachRefusal(404, 'view-not-found', 'That saved view no longer exists.')\n }\n if (data['section'] === 'leads') {\n return leadsViewPeople(firestore, caller.orgId, sequence, normalizeCrmViewFilters(data['filters']))\n }\n if (data['section'] !== 'contacts') {\n return outreachRefusal(400, 'view-unsupported', 'Enroll from a saved view of Contacts or Leads.')\n }\n const { unsupported } = dynamicListDimensionsForCrmView(normalizeCrmViewFilters(data['filters']))\n if (unsupported.length) {\n // Dropping the filter enrolling cannot read would enroll more people\n // than the view shows, so the view is refused whole.\n return outreachRefusal(\n 400,\n 'view-unsupported',\n `This view filters on ${unsupported.map((clause) => clause.label || clause.field).join(', ')}, which enrolling can't apply. Pick another view, or search.`,\n )\n }\n const { emails, complete } = await deps.crmViewEmails({ hostId: sequence.hostId, viewId })\n const ordered = [...new Set(emails)].sort()\n const contacts = firestore.collection('orgs').doc(caller.orgId).collection('contacts')\n const found = await Promise.all(\n ordered\n .slice(0, OUTREACH_ENROLL_BATCH_MAX)\n .map((email) => findContactByEmail(contacts, email, { hostId: sequence.hostId })),\n )\n return {\n people: [...new Set(found.filter((snapshot) => snapshot).map((snapshot) => String(snapshot?.id)))].map(\n (id) => ({ kind: 'contact', id }),\n ),\n total: ordered.length,\n truncated: ordered.length > OUTREACH_ENROLL_BATCH_MAX || !complete,\n }\n }\n\n const preview: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n const source = body['source'] && typeof body['source'] === 'object' ? (body['source'] as Record<string, unknown>) : {}\n const named = await sourcePeople(firestore, caller, sequence, source)\n if (named instanceof Response) return named\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n named.people,\n )\n const people = candidates.map((candidate) =>\n previewOutreachPerson(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n ),\n )\n return outreachOk({\n ok: true,\n people,\n total: named.total,\n truncated: named.truncated,\n } satisfies OutreachEnrollPreviewResponse)\n }\n\n const confirm: PluginWebApiHandler = async (request) => {\n if (request.method !== 'POST') return outreachMethodNotAllowed('POST')\n const body = await readOutreachJsonBody(request)\n const caller = await outreachRouteGate(request, body['orgId'], deps.gate, [OUTREACH_READS_PEOPLE])\n if (caller instanceof Response) return caller\n const firestore = deps.firestore()\n const sequence = await loadActiveSequence(firestore, caller.orgId, body['sequenceId'])\n if (sequence instanceof Response) return sequence\n\n // Each person once, by whichever record they are: a contact by id, or a\n // lead by its key (AGL-3234). A person named both ways is a contact.\n const requested = new Map<\n string,\n { ref: OutreachPersonRef; personalLine: string; attestations: unknown[] }\n >()\n for (const entry of Array.isArray(body['people']) ? body['people'] : []) {\n const person = (entry && typeof entry === 'object' ? entry : {}) as Record<string, unknown>\n const contactId = readOutreachDocumentId(person['contactId'])\n const leadId = readOutreachDocumentId(person['leadId'])\n const ref: OutreachPersonRef | null = contactId\n ? { kind: 'contact', id: contactId }\n : leadId\n ? { kind: 'lead', id: leadId }\n : null\n if (!ref || requested.has(ref.id)) continue\n requested.set(ref.id, {\n ref,\n personalLine: typeof person['personalLine'] === 'string' ? person['personalLine'] : '',\n attestations: Array.isArray(person['attestations']) ? person['attestations'] : [],\n })\n }\n if (!requested.size) return outreachRefusal(400, 'invalid-request', 'Pick the people to enroll.')\n if (requested.size > OUTREACH_ENROLL_BATCH_MAX) {\n return outreachRefusal(\n 400,\n 'invalid-request',\n `Enroll at most ${OUTREACH_ENROLL_BATCH_MAX} people at a time.`,\n )\n }\n\n const mailboxSnapshot = sequence.mailboxId\n ? await outreachOrgCollection(firestore, caller.orgId, 'mailboxes').doc(sequence.mailboxId).get()\n : null\n const mailbox = readStoredOutreachMailbox(\n sequence.mailboxId,\n mailboxSnapshot?.exists ? mailboxSnapshot.data() : undefined,\n )\n if (!mailbox || mailbox.status === 'disconnected') {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's mailbox is no longer connected. Choose another mailbox for it before enrolling people.\",\n )\n }\n const nowMs = deps.now()\n if (planOutreachFirstDue({ sequence, mailbox, enrolledAtMs: nowMs, random: deps.random }) === null) {\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n \"This sequence's sending hours never open in its mailbox's timezone. Fix the hours before enrolling people.\",\n )\n }\n\n const context = await enrollContext(firestore, caller, sequence)\n const { candidates, enrolledHere, lookups } = await readPeople(\n firestore,\n caller,\n sequence,\n context,\n [...requested.values()].map((entry) => entry.ref),\n )\n const enrollments = outreachOrgCollection(firestore, caller.orgId, 'enrollments')\n const campaignNames = await sequenceCampaignNames(firestore, caller.orgId, sequence)\n const results: OutreachEnrollOutcome[] = await Promise.all(\n candidates.map(async (candidate): Promise<OutreachEnrollOutcome> => {\n const named = {\n personId: candidate.personId,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n }\n const supplied = requested.get(candidate.personId) ?? { personalLine: '', attestations: [] }\n const decision = decideOutreachEnrollment(\n {\n candidate,\n lookups: lookups.get(candidate.personId) ?? UNCHECKED,\n settings: context.settings,\n contactGroupId: context.contactGroupId,\n },\n { siteName: context.siteName, alreadyInSequence: enrolledHere.has(candidate.personId) },\n { personalLine: supplied.personalLine, attestations: supplied.attestations, uid: caller.uid, nowMs },\n )\n if (!decision.allowed || !decision.email) {\n return { ...named, email: candidate.email, outcome: 'blocked', blocks: decision.blocks }\n }\n const id = outreachEnrollmentId(sequence.id, candidate.personId)\n const enrollment = buildOutreachEnrollment({\n id,\n sequence,\n mailbox,\n target: candidate.target,\n contactId: candidate.contactId,\n leadId: candidate.leadId,\n contactName: candidate.name,\n email: decision.email,\n cold: decision.cold,\n personalLine: supplied.personalLine,\n attestations: decision.attestations,\n enrolledByUid: caller.uid,\n nowMs,\n random: deps.random,\n })\n try {\n await enrollments.doc(id).create(enrollment)\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n return {\n ...named,\n email: candidate.email,\n outcome: 'blocked',\n blocks: [\n {\n code: 'already_in_sequence',\n reason: \"This person has already been in this sequence, so they won't be sent its emails again.\",\n },\n ],\n }\n }\n await joinSequenceCampaigns(firestore, caller.orgId, sequence, context.contactGroupId, candidate, nowMs)\n // The person's record says so (AGL-3274): \"Enrolled in <sequence>\",\n // with the campaigns it carried them into, once per enrollment.\n const entry = outreachEnrolledEntry({ enrollmentId: id, sequenceName: sequence.name, campaignNames })\n await fileOutreachNote(deps, {\n orgId: caller.orgId,\n hostId: sequence.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: nowMs,\n })\n return { ...named, email: decision.email, outcome: 'enrolled', enrollmentId: id }\n }),\n )\n const enrolled = results.filter((result) => result.outcome === 'enrolled').length\n if (enrolled) {\n await deps.logOrgActivity(\n caller.orgId,\n { uid: caller.uid, email: caller.email },\n outreachEnrolledActivity(enrolled),\n { type: OUTREACH_SEQUENCE_ACTIVITY_TARGET, id: sequence.id, name: sequence.name },\n )\n }\n return outreachOk({ ok: true, results, enrolled } satisfies OutreachEnrollResponse)\n }\n\n return { preview, confirm }\n}\n"],"names":["CAMPAIGN_MEMBERSHIP_FIELD","contactCampaignFieldPath","normalizeCampaignIds","consentGroupForHost","scopeTokensForHost","CRM_COLLECTIONS","crmLeadStatus","crmViewIsListed","isCrmLeadOpen","normalizeCrmViewFilters","dynamicListDimensionsForCrmView","findContactByEmail","FieldValue","outreachEnrolledEntry","buildOutreachEnrollment","planOutreachFirstDue","decideOutreachEnrollment","previewOutreachPerson","readOutreachEnrollCandidates","readOutreachGateLookups","effectiveOutreachAllowedCountries","OUTREACH_ENROLL_BATCH_MAX","readOutreachComplianceSettingsDoc","fileOutreachNote","outreachEnrollmentId","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachMailbox","readStoredOutreachSequence","OUTREACH_SEQUENCE_ACTIVITY_TARGET","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_READS_PEOPLE","key","refusal","outreachEnrolledActivity","count","UNCHECKED","platformSuppressed","hostSuppressed","salesTopicState","doNotContact","doNotContactDomain","workspaceMembers","openEnrollments","hasInboundEmail","ALREADY_EXISTS","loadActiveSequence","firestore","orgId","rawSequenceId","sequenceId","snapshot","doc","get","sequence","exists","data","undefined","status","enrollContext","caller","host","orgSettings","Promise","all","collection","hostId","consentGroup","org","settings","allowedCountries","contactGroupId","groupId","consentHostIds","hostIds","consentAwaitsConfirmation","awaitsConfirmation","siteName","String","readPeople","context","people","candidates","enrollments","existing","length","getAll","map","candidate","id","personId","enrolledHere","Set","filter","_candidate","index","askable","contact","visible","email","lookups","contactId","leadId","Map","open","some","entry","add","uniqueIds","values","Array","isArray","LEADS_VIEW_WINDOW","leadsViewPeople","filters","statusClause","find","clause","field","op","wanted","value","window","where","orderBy","limit","matching","docs","slice","lead","kind","total","truncated","createOutreachEnrollRoutes","deps","joinSequenceCampaigns","nowMs","campaignIds","ref","target","update","arrayUnion","updatedAt","serverTimestamp","error","console","creditCampaign","outcome","atMs","sequenceCampaignNames","containers","found","trim","Boolean","sourcePeople","source","ids","viewId","view","views","shared","ownerUid","uid","unsupported","label","join","emails","complete","crmViewEmails","ordered","sort","contacts","preview","request","method","body","gate","Response","named","alreadyInSequence","has","ok","confirm","requested","person","set","personalLine","attestations","size","mailboxSnapshot","mailboxId","mailbox","now","enrolledAtMs","random","campaignNames","results","supplied","decision","allowed","blocks","enrollment","contactName","name","cold","enrolledByUid","create","code","reason","enrollmentId","sequenceName","link","dedupeKey","enrolled","result","logOrgActivity","type"],"mappings":"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"}
@@ -263,6 +263,7 @@ async function runMailbox(deps, firestore, input) {
263
263
  orgId,
264
264
  hostId,
265
265
  consentHostIds: consentGroup.hostIds,
266
+ consentAwaitsConfirmation: consentGroup.awaitsConfirmation,
266
267
  people: enrollments.map((enrollment)=>{
267
268
  const person = outreachEnrollmentPerson(enrollment);
268
269
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/send-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 { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { normalizeCrmEmailTemplate } from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { randomUUID } from 'node:crypto'\nimport { composeOutreachEmail } from '../engine/compose'\nimport { planOutreachStepCompletion, type OutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport { evaluateOutreachGates, type OutreachGateBlock, type OutreachGateLookups } from '../engine/gates'\nimport {\n outreachSentToday,\n outreachTickAllowance,\n selectDueOutreachEnrollments,\n type OutreachDueCandidate,\n} from '../engine/sending-capacity'\nimport { readOutreachEnrollCandidates, type OutreachEnrollCandidate } from '../enrollment/enroll-people'\nimport { readOutreachGateLookups } from '../enrollment/gate-lookups'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport { outreachEffectiveDailyCap, outreachLocalDay } from '../mailboxes/mailbox-settings'\nimport type { OpenedOutreachMailbox } from '../mailboxes/mailbox-transport'\nimport { effectiveOutreachAllowedCountries } from '../model/compliance-settings'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachComplianceSettingsDocument,\n type OutreachEnrollment,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachStepRecord,\n type OutreachTaskStep,\n} from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachEnrollmentLink,\n outreachEnrollmentPerson,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { GmailTransportError, isReconnectRequired } from '../transport/gmail-errors'\nimport { Rfc5322MessageError } from '../transport/rfc5322'\nimport { sendComposedOutreachEmail } from '../transport/send-message'\nimport { creditOutreachFirstSend } from './campaign-credit'\nimport { newOutreachLinkId, outreachStoredLink, type OutreachStoredLink } from './click-link'\nimport { applyOutreachEvent } from './enrollment-events'\nimport { outreachSendDigest, withRecentSend } from './mailbox-health-store'\nimport { noteOutreachMailboxReconnectRequired } from './mailbox-notices'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { fileOutreachEmail, fileOutreachTask, OUTREACH_TASK_KIND_TO_RECORD } from './timeline'\nimport { outreachListUnsubscribe } from './unsubscribe-link'\n\n/*==========================================\n * THE SEND JOB (AGL-2981): every fifteen minutes, on the console.\n *\n * ## What comes due\n *\n * One collection-group read finds every active enrollment whose next step\n * is due, oldest first, bounded per run. They are grouped by workspace and\n * mailbox, and each group is judged before anything is sent:\n *\n * - the workspace: Outreach switched on, entitled and released to it;\n * - the mailbox: an enrollment whose mailbox is gone, disconnected, or whose\n * member left the organization is STOPPED — nothing will ever send it, and\n * leaving it active would show a sequence that is not running. A paused\n * mailbox, or one waiting to be reconnected, holds its enrollments;\n * - the member it sends as: a lockdown of the platform, the workspace or the\n * member holds everything.\n *\n * ## How much goes\n *\n * The engine's per-run allowance — the day's cap and ramp, spread across\n * the window, at most five a run — picks the enrollments (follow-ups first);\n * a task step is due whenever its time comes and spends no allowance.\n *\n * ## One send, exactly once\n *\n * Each email is CLAIMED before it is sent: a transaction over the enrollment\n * and its mailbox checks the enrollment is still active on the same step\n * with no live claim, and that the mailbox's day has room under its cap,\n * then writes the claim and reserves the day's count. Two runs racing for\n * one enrollment write one claim; two racing for one mailbox's last slot\n * reserve it once. The claim carries the `Message-ID` minted for the send,\n * so a claim a run died holding is settled by finding the message in the\n * mailbox rather than by sending it a second time. A send that fails hands\n * its reservation back.\n *\n * Before it is claimed, every gate is asked again — the do-not-contact\n * list, the suppression lists, the sales topic, the roster, the other\n * sequences — because a person enrolled on Monday can unsubscribe by\n * Thursday. A lookup that could not be made holds the send; a gate that\n * refuses stops the enrollment, saying why.\n *==========================================*/\n\nexport { OUTREACH_SEND_JOB_ID } from '../constants/runtime-jobs'\n\n/** The most due enrollments one run reads. */\nexport const OUTREACH_SEND_SCAN_LIMIT = 300\n\n/** How long a claim holds before a later run settles it. */\nexport const OUTREACH_CLAIM_STALE_MS = 10 * 60_000\n\n/** What one run did, for the tick's answer. */\nexport interface OutreachSendReport {\n [key: string]: number\n due: number\n sent: number\n tasks: number\n /** Stopped by a gate or an unusable mailbox. */\n stopped: number\n /** Paused for the rep to fix, or failed for good. */\n failed: number\n /** Due, and left for a later run: the allowance, a lookup, a hold. */\n held: number\n /** Claims a dead run left, settled. */\n recovered: number\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\ninterface MailboxRun {\n orgId: string\n org: Record<string, unknown>\n mailbox: OutreachMailbox\n settings: OutreachComplianceSettingsDocument\n report: OutreachSendReport\n deadlineMs: number\n opened: OpenedOutreachMailbox | null\n /** Set once the mailbox itself cannot send this run. */\n stopped: boolean\n templates: Map<string, string | null>\n /**\n * The click-tracking origin for this mailbox's sending domain (AGL-3306):\n * `undefined` until a tracked send first asks, then the verified\n * `https://links.<domain>` or `null` for the console's own address.\n */\n linkOrigin?: string | null\n}\n\nconst blankReport = (): OutreachSendReport => ({\n due: 0,\n sent: 0,\n tasks: 0,\n stopped: 0,\n failed: 0,\n held: 0,\n recovered: 0,\n})\n\n/** Runs the send job once — see the module note. */\nexport async function runOutreachSendJob(\n deps: OutreachRuntimeDeps,\n context: { nowMs: number; deadlineMs: number },\n): Promise<OutreachSendReport> {\n const firestore = deps.firestore()\n const report = blankReport()\n const due = await firestore\n .collectionGroup(OUTREACH_COLLECTIONS.enrollments)\n .where('status', '==', 'active')\n .where('nextDueAtMs', '<=', context.nowMs)\n .orderBy('nextDueAtMs')\n .limit(OUTREACH_SEND_SCAN_LIMIT)\n .get()\n report.due = due.size\n const byOrg = new Map<string, Map<string, OutreachEnrollment[]>>()\n for (const doc of due.docs) {\n const orgId = doc.ref.parent.parent?.id\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!orgId || !enrollment) continue\n const mailboxes = byOrg.get(orgId) ?? new Map<string, OutreachEnrollment[]>()\n mailboxes.set(enrollment.mailboxId, [...(mailboxes.get(enrollment.mailboxId) ?? []), enrollment])\n byOrg.set(orgId, mailboxes)\n }\n\n for (const [orgId, mailboxes] of byOrg) {\n const waiting = [...mailboxes.values()].reduce((sum, list) => sum + list.length, 0)\n if (deps.now() >= context.deadlineMs) {\n report.held += waiting\n continue\n }\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))) {\n report.held += waiting\n continue\n }\n const settings = await readOutreachComplianceSettingsDoc(firestore, orgId)\n for (const [mailboxId, enrollments] of mailboxes) {\n await runMailbox(deps, firestore, { orgId, org, mailboxId, enrollments, settings, report, context })\n }\n }\n return report\n}\n\n/** The reason an unusable mailbox stops an enrollment, or `null` for a usable one. */\nasync function unusableMailbox(\n firestore: Firestore,\n orgId: string,\n mailbox: OutreachMailbox | null,\n): Promise<string | null> {\n if (!mailbox) return 'The mailbox this sequence sends from was removed.'\n if (mailbox.status === 'disconnected') return 'The mailbox this sequence sends from was disconnected.'\n const member = await firestore.collection('orgs').doc(orgId).collection('members').doc(mailbox.connectedByUid).get()\n return member.exists\n ? null\n : 'The member who connected the mailbox this sequence sends from is no longer in the organization.'\n}\n\nasync function runMailbox(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n input: {\n orgId: string\n org: Record<string, unknown>\n mailboxId: string\n enrollments: OutreachEnrollment[]\n settings: OutreachComplianceSettingsDocument\n report: OutreachSendReport\n context: { nowMs: number; deadlineMs: number }\n },\n): Promise<void> {\n const { orgId, org, report, context } = input\n const nowMs = context.nowMs\n const snapshot = await mailboxRef(firestore, orgId, input.mailboxId).get()\n const mailbox = readStoredOutreachMailbox(input.mailboxId, snapshot.exists ? snapshot.data() : undefined)\n const unusable = await unusableMailbox(firestore, orgId, mailbox)\n if (unusable || !mailbox) {\n for (const enrollment of input.enrollments) {\n const outcome = await applyOutreachEvent(firestore, {\n orgId,\n enrollmentId: enrollment.id,\n event: { type: 'stop', atMs: nowMs, byUid: null, reason: 'gate', detail: unusable },\n nowMs,\n })\n if (outcome.changed) report.stopped += 1\n }\n return\n }\n if (mailbox.status !== 'connected' || (await deps.sendRefusal({ orgId, org, uid: mailbox.connectedByUid }))) {\n report.held += input.enrollments.length\n return\n }\n\n const run: MailboxRun = {\n orgId,\n org,\n mailbox,\n settings: input.settings,\n report,\n deadlineMs: context.deadlineMs,\n opened: null,\n stopped: false,\n templates: new Map(),\n }\n const sequenceIds = [...new Set(input.enrollments.map((enrollment) => enrollment.sequenceId))]\n const sequenceSnapshots = await firestore.getAll(\n ...sequenceIds.map((id) => outreachOrgCollection(firestore, orgId, 'sequences').doc(id)),\n )\n const sequences = new Map<string, OutreachSequence>()\n sequenceSnapshots.forEach((sequenceSnapshot, index) => {\n const sequence = readStoredOutreachSequence(\n sequenceIds[index],\n sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined,\n )\n if (sequence) sequences.set(sequence.id, sequence)\n })\n\n const candidates: OutreachDueCandidate[] = []\n for (const enrollment of input.enrollments) {\n const sequence = sequences.get(enrollment.sequenceId)\n const claim = enrollment.sendClaim\n if (claim && nowMs - claim.atMs < OUTREACH_CLAIM_STALE_MS) continue\n if (claim && sequence) {\n if (await recoverClaim(deps, firestore, run, enrollment, sequence)) report.recovered += 1\n continue\n }\n if (sequence) candidates.push({ enrollment, sequence })\n }\n\n const allowance = outreachTickAllowance({ mailbox, nowMs })\n const selection = selectDueOutreachEnrollments({ candidates, mailbox, nowMs, allowance: allowance.allowance })\n report.held += selection.deferred.length\n\n for (const candidate of selection.tasks) {\n if (deps.now() >= context.deadlineMs) {\n report.held += 1\n continue\n }\n if (await runTaskStep(deps, firestore, run, candidate)) report.tasks += 1\n }\n if (!selection.emails.length) return\n\n // The people behind the emails, read once per site: the contact and its\n // company, and every lookup the gates decide on.\n const people = new Map<string, OutreachEnrollCandidate>()\n const lookups = new Map<string, OutreachGateLookups>()\n const byHost = new Map<string, OutreachEnrollment[]>()\n for (const candidate of selection.emails) {\n const enrollment = candidate.enrollment as OutreachEnrollment\n byHost.set(enrollment.hostId, [...(byHost.get(enrollment.hostId) ?? []), enrollment])\n }\n const siteNames = new Map<string, string>()\n for (const [hostId, enrollments] of byHost) {\n const consentGroup = consentGroupForHost(org, hostId)\n const contactGroupId = consentGroup.groupId\n // Each by the record it names — the contact, or the lead while the\n // person is one (AGL-3234) — keyed by that record's id.\n const [candidatesRead, lookupsRead, host] = await Promise.all([\n readOutreachEnrollCandidates(firestore, {\n orgId,\n hostId,\n contactGroupId,\n people: enrollments.map(outreachEnrollmentPerson),\n }),\n readOutreachGateLookups(firestore, {\n orgId,\n hostId,\n consentHostIds: consentGroup.hostIds,\n people: enrollments.map((enrollment) => {\n const person = outreachEnrollmentPerson(enrollment)\n return {\n personId: person.id,\n contactId: person.kind === 'contact' ? person.id : null,\n leadId: person.kind === 'lead' ? person.id : null,\n email: enrollment.email,\n }\n }),\n }),\n firestore.collection('hosts').doc(hostId).get(),\n ])\n for (const person of candidatesRead) people.set(person.personId, person)\n for (const [personId, answer] of lookupsRead) lookups.set(personId, answer)\n siteNames.set(hostId, host.exists ? String(host.get('name') ?? '') : '')\n }\n\n for (const candidate of selection.emails) {\n if (run.stopped || deps.now() >= context.deadlineMs) {\n report.held += 1\n continue\n }\n const enrollment = candidate.enrollment as OutreachEnrollment\n const personId = outreachEnrollmentPerson(enrollment).id\n await runEmailStep(deps, firestore, run, {\n enrollment,\n sequence: candidate.sequence as OutreachSequence,\n person: people.get(personId) ?? null,\n lookups: lookups.get(personId) ?? null,\n siteName: siteNames.get(enrollment.hostId) ?? '',\n })\n }\n}\n\n/** Any lookup the runtime could not make: the send waits for a run that can. */\nfunction uncheckable(lookups: OutreachGateLookups | null): boolean {\n return !lookups || Object.values(lookups).some((value) => value === null)\n}\n\n/** The event a gate's refusal ends an enrollment with. */\nfunction gateEvent(blocks: readonly OutreachGateBlock[], atMs: number): OutreachEnrollmentEvent {\n const optedOut = blocks.find((block) => block.code === 'do_not_contact' || block.code === 'sales_opted_out')\n if (optedOut) return { type: 'opt_out', atMs, reason: 'do_not_contact', detail: optedOut.reason }\n return { type: 'stop', atMs, byUid: null, reason: 'gate', detail: blocks.map((block) => block.reason).join(' ') }\n}\n\n/**\n * The click-tracking origin for mail sent as `senderAddress`, or `null` for\n * the console's own address — which is also what a failed lookup reads as: a\n * link on the app address still counts the click, so no send waits on it.\n */\nasync function trackingLinkOrigin(\n deps: OutreachRuntimeDeps,\n orgId: string,\n senderAddress: string,\n): Promise<string | null> {\n if (!deps.clickLinkOrigin) return null\n try {\n return await deps.clickLinkOrigin({ orgId, senderAddress })\n } catch (error) {\n console.warn('[outreach] the click-tracking host could not be read; links use the app address', error)\n return null\n }\n}\n\n/** The sender line a mailbox's mail goes out with. */\nconst senderOf = (mailbox: OutreachMailbox) => ({\n address: mailbox.sendAs || mailbox.email,\n name: mailbox.displayName || null,\n})\n\nasync function openClient(deps: OutreachRuntimeDeps, run: MailboxRun) {\n const opened = (run.opened ??= await deps.openMailbox(run.mailbox.id))\n if (opened.ok === true) return opened.client\n run.stopped = true\n if (opened.reason !== 'not-configured') {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId: run.orgId,\n mailboxId: run.mailbox.id,\n errorCode: opened.reason,\n nowMs: deps.now(),\n })\n }\n return null\n}\n\n/**\n * Claims one step: the enrollment still active on the same step with no\n * live claim, and — for an email — a slot under the mailbox's cap today,\n * reserved in the same transaction.\n */\nasync function claimStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n enrollment: OutreachEnrollment,\n messageId: string | null,\n): Promise<'claimed' | 'taken' | 'cap'> {\n const enrollmentRef = outreachOrgCollection(firestore, run.orgId, 'enrollments').doc(enrollment.id)\n const boxRef = mailboxRef(firestore, run.orgId, run.mailbox.id)\n const nowMs = deps.now()\n return firestore.runTransaction(async (transaction) => {\n const [current, box] = await Promise.all([transaction.get(enrollmentRef), transaction.get(boxRef)])\n const stored = readStoredOutreachEnrollment(enrollment.id, current.exists ? current.data() : undefined)\n const claim = stored?.sendClaim\n if (\n !stored ||\n stored.status !== 'active' ||\n stored.stepIndex !== enrollment.stepIndex ||\n stored.nextDueAtMs === null ||\n stored.nextDueAtMs > nowMs ||\n (claim && nowMs - claim.atMs < OUTREACH_CLAIM_STALE_MS)\n ) {\n return 'taken'\n }\n if (messageId) {\n const mailbox = readStoredOutreachMailbox(run.mailbox.id, box.exists ? box.data() : undefined)\n if (!mailbox || mailbox.status !== 'connected') return 'taken'\n const sent = outreachSentToday(mailbox.health, nowMs, mailbox.timezone)\n if (sent >= outreachEffectiveDailyCap(mailbox, nowMs)) return 'cap'\n transaction.update(boxRef, {\n 'health.sentToday': sent + 1,\n 'health.sentOnDay': outreachLocalDay(nowMs, mailbox.timezone),\n })\n }\n transaction.update(enrollmentRef, {\n sendClaim: { token: randomUUID(), atMs: nowMs, stepIndex: enrollment.stepIndex, messageId },\n updatedAtMs: nowMs,\n })\n return 'claimed'\n })\n}\n\n/** Drops a claim, and hands an email's reservation back to the mailbox's day. */\nasync function releaseClaim(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n enrollment: Pick<OutreachEnrollment, 'id'>,\n reserved: boolean,\n event: OutreachEnrollmentEvent | null = null,\n): Promise<void> {\n const nowMs = deps.now()\n await applyOutreachEvent(firestore, {\n orgId: run.orgId,\n enrollmentId: enrollment.id,\n event,\n extra: () => ({ sendClaim: null }),\n nowMs,\n })\n if (!reserved) return\n const boxRef = mailboxRef(firestore, run.orgId, run.mailbox.id)\n await firestore.runTransaction(async (transaction) => {\n const box = await transaction.get(boxRef)\n const mailbox = readStoredOutreachMailbox(run.mailbox.id, box.exists ? box.data() : undefined)\n if (!mailbox || mailbox.health?.sentOnDay !== outreachLocalDay(nowMs, mailbox.timezone)) return\n transaction.update(boxRef, { 'health.sentToday': Math.max(0, (Number(mailbox.health.sentToday) || 0) - 1) })\n })\n}\n\n/**\n * Records a step that ran: the engine's completion patch, the claim\n * cleared, the step's record kept — and for an email the mailbox's send on\n * its day, its last-sent time and its window of recent sends. Refuses\n * (answers false) when the claim is no longer this run's.\n */\nasync function completeStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n input: {\n enrollment: OutreachEnrollment\n sequence: OutreachSequence\n record: OutreachStepRecord\n sent: { messageId: string; threadId: string; subject: string } | null\n claimMessageId: string | null\n },\n): Promise<boolean> {\n const enrollmentRef = outreachOrgCollection(firestore, run.orgId, 'enrollments').doc(input.enrollment.id)\n const boxRef = mailboxRef(firestore, run.orgId, run.mailbox.id)\n const nowMs = deps.now()\n /*\n * Whether this is the FIRST email this person has had from the sequence\n * (AGL-3239), decided from the same read the step is recorded against and\n * so re-decided on every transaction retry. It is what `stats.people`\n * counts: the denominator of every engagement rate, and the thing\n * `stats.sent` is not, since one person takes several steps.\n */\n let firstEmail = false\n const completed = await firestore.runTransaction(async (transaction) => {\n const [current, box] = await Promise.all([\n transaction.get(enrollmentRef),\n input.sent ? transaction.get(boxRef) : Promise.resolve(null),\n ])\n const stored = readStoredOutreachEnrollment(input.enrollment.id, current.exists ? current.data() : undefined)\n if (!stored?.sendClaim || stored.sendClaim.messageId !== input.claimMessageId) return false\n if (stored.stepIndex !== input.enrollment.stepIndex) return false\n // The step RAN, whatever happened to the enrollment while it did — a\n // member paused it, a reply stopped it — so it is recorded as run, and a\n // resume never sends it twice.\n firstEmail =\n input.sent !== null &&\n !(stored.stepRecords ?? []).some((record) => record.kind === 'email')\n const plan = planOutreachStepCompletion({\n enrollment: { ...stored, status: 'active' },\n sequence: input.sequence,\n mailbox: run.mailbox,\n completedAtMs: nowMs,\n sent: input.sent,\n random: deps.random,\n })\n const patch: Record<string, unknown> = {\n sendClaim: null,\n stepRecords: [...(stored.stepRecords ?? []), input.record].slice(-20),\n updatedAtMs: nowMs,\n ...(plan.patch ?? {}),\n }\n if (stored.status !== 'active') {\n patch['status'] = stored.status\n if (stored.status !== 'paused') patch['nextDueAtMs'] = null\n }\n transaction.update(enrollmentRef, patch)\n if (input.sent && box?.exists) {\n const mailbox = readStoredOutreachMailbox(run.mailbox.id, box.data())\n if (mailbox) {\n const day = outreachLocalDay(nowMs, mailbox.timezone)\n const counts = mailbox.health?.daily?.[day] ?? { sent: 0, bounces: 0, replies: 0 }\n transaction.update(boxRef, {\n 'health.lastSentAtMs': nowMs,\n 'health.lastErrorAtMs': null,\n 'health.lastErrorCode': null,\n [`health.daily.${day}`]: { ...counts, sent: (Number(counts.sent) || 0) + 1 },\n 'health.recentSends': withRecentSend(mailbox.health?.recentSends, {\n id: outreachSendDigest(input.sent.messageId),\n atMs: nowMs,\n bounced: false,\n }),\n updatedAtMs: nowMs,\n })\n }\n }\n return true\n })\n if (completed && input.sent) {\n await bumpOutreachSequenceStats(firestore, {\n orgId: run.orgId,\n sequenceId: input.sequence.id,\n firstEmail,\n trackedLinks: input.record.links?.length ?? 0,\n })\n }\n return completed\n}\n\n/**\n * The sequence's send counters (AGL-3239), written outside the enrollment's\n * transaction.\n *\n * One sequence holds every enrollment, so a transaction over it would\n * serialise every recipient against every other; `FieldValue.increment` is a\n * blind write with nothing to contend on. It is bookkeeping beside the act,\n * so a failure is logged and the send stands — the email has already left.\n *\n * `clickTracked` is stamped by the first send that actually carried a\n * rewritten link, never by the SETTING: a sequence with the setting on whose\n * emails have no links in them measured nothing, and the report must say\n * \"not measured\" for it rather than publish a 0% click rate.\n */\nasync function bumpOutreachSequenceStats(\n firestore: Firestore,\n input: { orgId: string; sequenceId: string; firstEmail: boolean; trackedLinks: number },\n): Promise<void> {\n if (!input.sequenceId) return\n await outreachOrgCollection(firestore, input.orgId, 'sequences')\n .doc(input.sequenceId)\n .update({\n 'stats.sent': FieldValue.increment(1),\n ...(input.firstEmail ? { 'stats.people': FieldValue.increment(1) } : {}),\n ...(input.trackedLinks > 0 ? { 'stats.clickTracked': true } : {}),\n })\n .catch((error: unknown) => {\n console.warn('[outreach] a send could not be counted on its sequence', error)\n })\n}\n\n/** One task step: the rep's task on the contact, then the step recorded. */\nasync function runTaskStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n candidate: OutreachDueCandidate,\n): Promise<boolean> {\n const enrollment = candidate.enrollment as OutreachEnrollment\n const sequence = candidate.sequence as OutreachSequence\n const step = sequence.steps[enrollment.stepIndex] as OutreachTaskStep\n if ((await claimStep(deps, firestore, run, enrollment, null)) !== 'claimed') return false\n const nowMs = deps.now()\n let taskId: string | null\n try {\n const written = await fileOutreachTask(deps, {\n orgId: run.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: `step:${enrollment.id}:${step.id}`,\n title: step.title,\n notes: `From sequence step ${enrollment.stepIndex + 1}.`,\n kind: OUTREACH_TASK_KIND_TO_RECORD[step.taskKind] ?? 'todo',\n dueAtMs: nowMs,\n assigneeUid: run.mailbox.connectedByUid,\n })\n taskId = written?.ok ? written.id : null\n if (written?.ok === false) console.warn(`[outreach] a task step's task was not filed: ${written.error}`)\n } catch (error) {\n // The record system failed rather than refused: the step waits for a\n // run that can file its task.\n console.error('[outreach] filing a task step failed; the step waits', error)\n await releaseClaim(deps, firestore, run, enrollment, false)\n return false\n }\n return completeStep(deps, firestore, run, {\n enrollment,\n sequence,\n record: { stepIndex: enrollment.stepIndex, stepId: step.id, kind: 'task', atMs: nowMs, taskId },\n sent: null,\n claimMessageId: null,\n })\n}\n\n/** A CRM template's body, read once per run. */\nasync function templateBody(firestore: Firestore, run: MailboxRun, templateId: string): Promise<string | null> {\n if (!run.templates.has(templateId)) {\n const snapshot = await firestore\n .collection('orgs')\n .doc(run.orgId)\n .collection(CRM_COLLECTIONS.emailTemplates)\n .doc(templateId)\n .get()\n run.templates.set(\n templateId,\n snapshot.exists ? normalizeCrmEmailTemplate(snapshot.data() as Record<string, unknown>).body : null,\n )\n }\n return run.templates.get(templateId) ?? null\n}\n\nasync function runEmailStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n input: {\n enrollment: OutreachEnrollment\n sequence: OutreachSequence\n person: OutreachEnrollCandidate | null\n lookups: OutreachGateLookups | null\n siteName: string\n },\n): Promise<void> {\n const { enrollment, sequence, person } = input\n const { report, mailbox } = run\n const nowMs = deps.now()\n const stop = async (event: OutreachEnrollmentEvent, counter: 'stopped' | 'failed') => {\n const outcome = await applyOutreachEvent(firestore, { orgId: run.orgId, enrollmentId: enrollment.id, event, nowMs })\n if (outcome.changed) report[counter] += 1\n }\n\n // Every gate again, on the person as they stand now.\n if (!person?.contact) {\n return stop(\n {\n type: 'stop',\n atMs: nowMs,\n byUid: null,\n reason: 'gate',\n detail:\n enrollment.target === 'lead'\n ? \"This lead no longer exists in the sequence's site's CRM.\"\n : 'This contact no longer exists in the CRM.',\n },\n 'stopped',\n )\n }\n if (person.convertedContactId && enrollment.target === 'lead') {\n // The lead converted and the enrollment has not followed it yet: the\n // conversion's listener re-points it, and the next run sends as the contact.\n report.held += 1\n return\n }\n if (!person.visible) {\n return stop(\n { type: 'stop', atMs: nowMs, byUid: null, reason: 'gate', detail: \"This contact is no longer in the sequence's site's CRM.\" },\n 'stopped',\n )\n }\n if (uncheckable(input.lookups)) {\n report.held += 1\n return\n }\n const contactGroupId = consentGroupForHost(run.org, enrollment.hostId).groupId\n const gate = evaluateOutreachGates({\n email: enrollment.email,\n contact: person.contact,\n contactGroupId,\n company: person.company,\n settings: {\n ...sequence.settings,\n allowedCountries: effectiveOutreachAllowedCountries(\n sequence.settings.allowedCountries,\n run.settings.allowedCountries,\n ),\n },\n personalLine: enrollment.personalLine,\n attestations: enrollment.attestations,\n enrollmentId: enrollment.id,\n lookups: input.lookups as OutreachGateLookups,\n })\n if (!gate.allowed) return stop(gateEvent(gate.blocks, nowMs), 'stopped')\n\n /*\n * The email, with its way out. The footer's \"reply 'no'\" line and the\n * postal address are always there (compose refuses without them). The\n * `List-Unsubscribe` header is the sequence's own setting (AGL-3296), read\n * here per send like click tracking, and absent reads as off.\n */\n const unsubscribe = outreachListUnsubscribe({\n enabled: sequence.settings.listUnsubscribe,\n mintUrl: () => deps.unsubscribeUrl({ orgId: run.orgId, enrollmentId: enrollment.id }),\n mailboxEmail: mailbox.email,\n })\n if (unsubscribe.status === 'unavailable') {\n // On, but no link can be minted — no secret, or no HTTPS console origin\n // — and an email without the way out its sequence promised does not leave.\n console.error('[outreach] no unsubscribe link could be minted; nothing sends until one can')\n run.stopped = true\n report.held += 1\n return\n }\n const step = sequence.steps[enrollment.stepIndex]\n const sender = senderOf(mailbox)\n /*\n * Click tracking is the sequence's own setting (AGL-3239) and is read\n * here, per send, rather than once per run: a member turning it off\n * applies to the next email, not to the next hour. Each link becomes a\n * short link (AGL-3297) whose document is written below, before the email\n * leaves; a link that cannot be made is left exactly as the step wrote it.\n */\n if (sequence.settings.trackClicks && run.linkOrigin === undefined) {\n run.linkOrigin = await trackingLinkOrigin(deps, run.orgId, sender.address)\n }\n const shortLinks: Array<{ id: string; doc: OutreachStoredLink }> = []\n const rewriteLink = sequence.settings.trackClicks\n ? (link: { url: string; index: number }) => {\n const doc = outreachStoredLink(\n {\n orgId: run.orgId,\n enrollmentId: enrollment.id,\n stepIndex: enrollment.stepIndex,\n linkIndex: link.index,\n url: link.url,\n },\n nowMs,\n )\n const id = newOutreachLinkId()\n const url = doc ? deps.clickLinkUrl(id, run.linkOrigin ?? null) : null\n if (!doc || !url) return null\n shortLinks.push({ id, doc })\n return url\n }\n : null\n const composed = composeOutreachEmail({\n sequence,\n enrollment,\n orgSettings: run.settings,\n rewriteLink,\n merge: {\n // For a lead, the contact-shaped view of it (`leadAsContact`), so a\n // step written with `{{contact.*}}` reads the lead; `{{lead.*}}` reads\n // the lead's own document beside it.\n contact: person.contact,\n contactGroupId,\n lead: person.lead,\n sender: { name: mailbox.displayName, email: sender.address },\n site: { name: input.siteName },\n },\n templateBody: step?.kind === 'email' && step.templateId ? await templateBody(firestore, run, step.templateId) : null,\n listUnsubscribeUrl: unsubscribe.status === 'ready' ? unsubscribe.url : null,\n listUnsubscribeMailto: unsubscribe.status === 'ready' ? unsubscribe.mailto : null,\n })\n if (composed.error || !composed.email) {\n const code = composed.error?.code\n if (code === 'missing_postal_address' || code === 'missing_legal_name') {\n // The organization's footer: nothing of theirs sends until it is set.\n run.stopped = true\n report.held += 1\n return\n }\n if (code === 'invalid_recipient' || code === 'missing_thread' || code === 'not_an_email_step') {\n return stop({ type: 'send_failed', atMs: nowMs, detail: composed.error?.message }, 'failed')\n }\n // The sequence's own words: the rep fixes the step and resumes.\n return stop({ type: 'pause', atMs: nowMs, byUid: null, detail: composed.error?.message }, 'failed')\n }\n if (composed.unresolvedFields.length) {\n const fields = composed.unresolvedFields.map((field) => `{{${field}}}`).join(', ')\n return stop(\n {\n type: 'pause',\n atMs: nowMs,\n byUid: null,\n detail: `Paused before sending: this contact has nothing for ${fields}. Fill it in and resume.`,\n },\n 'failed',\n )\n }\n\n /*\n * The short links' documents, before anything is claimed or sent: a link\n * in an email that already left must resolve on the first click. A write\n * that fails holds the send for the next run, which mints new ones — an\n * orphaned document is inert, an email whose links go nowhere is not.\n */\n if (shortLinks.length) {\n try {\n const batch = firestore.batch()\n for (const link of shortLinks) {\n batch.create(firestore.collection(OUTREACH_COLLECTIONS.links).doc(link.id), link.doc)\n }\n await batch.commit()\n } catch (error) {\n console.error('[outreach] the short links could not be stored; the send waits for the next run', error)\n report.held += 1\n return\n }\n }\n\n // Claimed, then sent, then recorded.\n const domain = sender.address.slice(sender.address.lastIndexOf('@') + 1)\n const local = randomUUID()\n const messageId = `<${local}@${domain}>`\n const claimed = await claimStep(deps, firestore, run, enrollment, messageId)\n if (claimed !== 'claimed') {\n if (claimed === 'cap') run.stopped = true\n report.held += 1\n return\n }\n const client = await openClient(deps, run)\n if (!client) {\n await releaseClaim(deps, firestore, run, enrollment, true)\n report.held += 1\n return\n }\n let sent: Awaited<ReturnType<typeof sendComposedOutreachEmail>>\n try {\n sent = await sendComposedOutreachEmail(client, composed.email, sender, {\n messageIdLocalPart: local,\n messageIdDomain: domain,\n })\n } catch (error) {\n if (error instanceof Rfc5322MessageError || (error instanceof GmailTransportError && error.code === 'invalid_request')) {\n await releaseClaim(deps, firestore, run, enrollment, true, {\n type: 'send_failed',\n atMs: deps.now(),\n detail: `Gmail refused the email: ${error.message}`,\n })\n report.failed += 1\n return\n }\n await releaseClaim(deps, firestore, run, enrollment, true)\n report.held += 1\n run.stopped = true\n const code = error instanceof GmailTransportError ? error.code : 'unexpected'\n if (isReconnectRequired(error)) {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId: run.orgId,\n mailboxId: mailbox.id,\n errorCode: code,\n nowMs: deps.now(),\n })\n } else {\n await mailboxRef(firestore, run.orgId, mailbox.id)\n .update({ 'health.lastErrorAtMs': deps.now(), 'health.lastErrorCode': code })\n .catch(() => undefined)\n }\n console.error(`[outreach] a send from a mailbox failed (${code}); its sends wait for the next run`)\n return\n }\n\n const record: OutreachStepRecord = {\n stepIndex: enrollment.stepIndex,\n stepId: step?.id ?? '',\n kind: 'email',\n atMs: deps.now(),\n gmailMessageId: sent.gmailMessageId,\n gmailThreadId: sent.threadId,\n messageId: sent.messageId,\n subject: sent.subject,\n ...(composed.email.trackedLinks.length ? { links: composed.email.trackedLinks } : {}),\n }\n await completeStep(deps, firestore, run, {\n enrollment,\n sequence,\n record,\n sent: { messageId: sent.messageId, threadId: sent.threadId, subject: sent.subject },\n claimMessageId: messageId,\n })\n report.sent += 1\n await fileOutreachEmail(deps, {\n orgId: run.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n direction: 'outbound',\n subject: sent.subject,\n from: sender.address,\n to: composed.email.to,\n messageId: sent.messageId,\n inReplyTo: composed.email.inReplyTo ?? null,\n body: composed.email.text,\n atMs: record.atMs,\n byUid: mailbox.connectedByUid,\n byName: mailbox.displayName || null,\n })\n // The first email is the touch (AGL-3254): `sent` on the sequence's\n // campaigns, and the person's record credited to the first of them.\n // Judged on the enrollment as it was read, before this step's record.\n await creditOutreachFirstSend(deps, { enrollment, atMs: record.atMs })\n}\n\n/**\n * Settles a claim a run died holding. A task step's claim is dropped and\n * the step runs again (its task is filed once however often it is asked).\n * An email's claim looks for the message it minted in the mailbox: found,\n * the send happened and is recorded; not found, it did not, and the claim\n * and its reservation are handed back for the next run.\n */\nasync function recoverClaim(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n enrollment: OutreachEnrollment,\n sequence: OutreachSequence,\n): Promise<boolean> {\n const claim = enrollment.sendClaim\n if (!claim) return false\n if (!claim.messageId) {\n await releaseClaim(deps, firestore, run, enrollment, false)\n return true\n }\n const client = await openClient(deps, run)\n if (!client) return false\n const bare = claim.messageId.replace(/^<|>$/g, '')\n const found = await client.listMessages({ q: `rfc822msgid:${bare}`, maxResults: 1, includeSpamTrash: true })\n const hit = found.messages[0]\n if (!hit) {\n await releaseClaim(deps, firestore, run, enrollment, true)\n return true\n }\n const message = await client.getMessage(hit.id, { metadataHeaders: ['Subject'] })\n const subject = message.headers.find((header) => header.name.toLowerCase() === 'subject')?.value ?? ''\n const step = sequence.steps[enrollment.stepIndex]\n const atMs = deps.now()\n const completed = await completeStep(deps, firestore, run, {\n enrollment,\n sequence,\n record: {\n stepIndex: enrollment.stepIndex,\n stepId: step?.id ?? '',\n kind: 'email',\n atMs,\n gmailMessageId: hit.id,\n gmailThreadId: hit.threadId,\n messageId: claim.messageId,\n subject,\n },\n sent: { messageId: claim.messageId, threadId: hit.threadId, subject },\n claimMessageId: claim.messageId,\n })\n // A recovered first email is still the first email (AGL-3254).\n if (completed) await creditOutreachFirstSend(deps, { enrollment, atMs })\n return completed\n}\n"],"names":["consentGroupForHost","CRM_COLLECTIONS","normalizeCrmEmailTemplate","FieldValue","randomUUID","composeOutreachEmail","planOutreachStepCompletion","evaluateOutreachGates","outreachSentToday","outreachTickAllowance","selectDueOutreachEnrollments","readOutreachEnrollCandidates","readOutreachGateLookups","mailboxRef","outreachEffectiveDailyCap","outreachLocalDay","effectiveOutreachAllowedCountries","OUTREACH_COLLECTIONS","readOutreachComplianceSettingsDoc","outreachEnrollmentLink","outreachEnrollmentPerson","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","Rfc5322MessageError","sendComposedOutreachEmail","creditOutreachFirstSend","newOutreachLinkId","outreachStoredLink","applyOutreachEvent","outreachSendDigest","withRecentSend","noteOutreachMailboxReconnectRequired","fileOutreachEmail","fileOutreachTask","OUTREACH_TASK_KIND_TO_RECORD","outreachListUnsubscribe","OUTREACH_SEND_JOB_ID","OUTREACH_SEND_SCAN_LIMIT","OUTREACH_CLAIM_STALE_MS","blankReport","due","sent","tasks","stopped","failed","held","recovered","runOutreachSendJob","deps","context","firestore","report","collectionGroup","enrollments","where","nowMs","orderBy","limit","get","size","byOrg","Map","doc","docs","mailboxes","orgId","ref","parent","id","enrollment","data","set","mailboxId","waiting","values","reduce","sum","list","length","now","deadlineMs","orgSnapshot","collection","org","exists","orgRefusal","settings","runMailbox","unusableMailbox","mailbox","status","member","connectedByUid","input","snapshot","undefined","unusable","outcome","enrollmentId","event","type","atMs","byUid","reason","detail","changed","sendRefusal","uid","run","opened","templates","sequenceIds","Set","map","sequenceId","sequenceSnapshots","getAll","sequences","forEach","sequenceSnapshot","index","sequence","candidates","claim","sendClaim","recoverClaim","push","allowance","selection","deferred","candidate","runTaskStep","emails","people","lookups","byHost","hostId","siteNames","host","consentGroup","contactGroupId","groupId","candidatesRead","lookupsRead","Promise","all","consentHostIds","hostIds","person","personId","contactId","kind","leadId","email","answer","String","runEmailStep","siteName","uncheckable","Object","some","value","gateEvent","blocks","optedOut","find","block","code","join","trackingLinkOrigin","senderAddress","clickLinkOrigin","error","console","warn","senderOf","address","sendAs","name","displayName","openClient","openMailbox","ok","client","errorCode","claimStep","messageId","enrollmentRef","boxRef","runTransaction","transaction","current","box","stored","stepIndex","nextDueAtMs","health","timezone","update","token","updatedAtMs","releaseClaim","reserved","extra","sentOnDay","Math","max","Number","sentToday","completeStep","firstEmail","completed","plan","resolve","claimMessageId","stepRecords","record","completedAtMs","random","patch","slice","day","counts","daily","bounces","replies","recentSends","bounced","bumpOutreachSequenceStats","trackedLinks","links","increment","catch","step","steps","taskId","written","link","dedupeKey","title","notes","taskKind","dueAtMs","assigneeUid","stepId","templateBody","templateId","has","emailTemplates","body","composed","stop","counter","contact","target","convertedContactId","visible","gate","company","allowedCountries","personalLine","attestations","allowed","unsubscribe","enabled","listUnsubscribe","mintUrl","unsubscribeUrl","mailboxEmail","sender","trackClicks","linkOrigin","shortLinks","rewriteLink","linkIndex","url","clickLinkUrl","orgSettings","merge","lead","site","listUnsubscribeUrl","listUnsubscribeMailto","mailto","message","unresolvedFields","fields","field","batch","create","commit","domain","lastIndexOf","local","claimed","messageIdLocalPart","messageIdDomain","gmailMessageId","gmailThreadId","threadId","subject","direction","from","to","inReplyTo","text","byName","bare","replace","found","listMessages","q","maxResults","includeSpamTrash","hit","messages","getMessage","metadataHeaders","headers","header","toLowerCase"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/send-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 { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { normalizeCrmEmailTemplate } from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { randomUUID } from 'node:crypto'\nimport { composeOutreachEmail } from '../engine/compose'\nimport { planOutreachStepCompletion, type OutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport { evaluateOutreachGates, type OutreachGateBlock, type OutreachGateLookups } from '../engine/gates'\nimport {\n outreachSentToday,\n outreachTickAllowance,\n selectDueOutreachEnrollments,\n type OutreachDueCandidate,\n} from '../engine/sending-capacity'\nimport { readOutreachEnrollCandidates, type OutreachEnrollCandidate } from '../enrollment/enroll-people'\nimport { readOutreachGateLookups } from '../enrollment/gate-lookups'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport { outreachEffectiveDailyCap, outreachLocalDay } from '../mailboxes/mailbox-settings'\nimport type { OpenedOutreachMailbox } from '../mailboxes/mailbox-transport'\nimport { effectiveOutreachAllowedCountries } from '../model/compliance-settings'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachComplianceSettingsDocument,\n type OutreachEnrollment,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachStepRecord,\n type OutreachTaskStep,\n} from '../model/outreach.types'\nimport { readOutreachComplianceSettingsDoc } from '../storage/compliance-settings-store'\nimport {\n outreachEnrollmentLink,\n outreachEnrollmentPerson,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport { GmailTransportError, isReconnectRequired } from '../transport/gmail-errors'\nimport { Rfc5322MessageError } from '../transport/rfc5322'\nimport { sendComposedOutreachEmail } from '../transport/send-message'\nimport { creditOutreachFirstSend } from './campaign-credit'\nimport { newOutreachLinkId, outreachStoredLink, type OutreachStoredLink } from './click-link'\nimport { applyOutreachEvent } from './enrollment-events'\nimport { outreachSendDigest, withRecentSend } from './mailbox-health-store'\nimport { noteOutreachMailboxReconnectRequired } from './mailbox-notices'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { fileOutreachEmail, fileOutreachTask, OUTREACH_TASK_KIND_TO_RECORD } from './timeline'\nimport { outreachListUnsubscribe } from './unsubscribe-link'\n\n/*==========================================\n * THE SEND JOB (AGL-2981): every fifteen minutes, on the console.\n *\n * ## What comes due\n *\n * One collection-group read finds every active enrollment whose next step\n * is due, oldest first, bounded per run. They are grouped by workspace and\n * mailbox, and each group is judged before anything is sent:\n *\n * - the workspace: Outreach switched on, entitled and released to it;\n * - the mailbox: an enrollment whose mailbox is gone, disconnected, or whose\n * member left the organization is STOPPED — nothing will ever send it, and\n * leaving it active would show a sequence that is not running. A paused\n * mailbox, or one waiting to be reconnected, holds its enrollments;\n * - the member it sends as: a lockdown of the platform, the workspace or the\n * member holds everything.\n *\n * ## How much goes\n *\n * The engine's per-run allowance — the day's cap and ramp, spread across\n * the window, at most five a run — picks the enrollments (follow-ups first);\n * a task step is due whenever its time comes and spends no allowance.\n *\n * ## One send, exactly once\n *\n * Each email is CLAIMED before it is sent: a transaction over the enrollment\n * and its mailbox checks the enrollment is still active on the same step\n * with no live claim, and that the mailbox's day has room under its cap,\n * then writes the claim and reserves the day's count. Two runs racing for\n * one enrollment write one claim; two racing for one mailbox's last slot\n * reserve it once. The claim carries the `Message-ID` minted for the send,\n * so a claim a run died holding is settled by finding the message in the\n * mailbox rather than by sending it a second time. A send that fails hands\n * its reservation back.\n *\n * Before it is claimed, every gate is asked again — the do-not-contact\n * list, the suppression lists, the sales topic, the roster, the other\n * sequences — because a person enrolled on Monday can unsubscribe by\n * Thursday. A lookup that could not be made holds the send; a gate that\n * refuses stops the enrollment, saying why.\n *==========================================*/\n\nexport { OUTREACH_SEND_JOB_ID } from '../constants/runtime-jobs'\n\n/** The most due enrollments one run reads. */\nexport const OUTREACH_SEND_SCAN_LIMIT = 300\n\n/** How long a claim holds before a later run settles it. */\nexport const OUTREACH_CLAIM_STALE_MS = 10 * 60_000\n\n/** What one run did, for the tick's answer. */\nexport interface OutreachSendReport {\n [key: string]: number\n due: number\n sent: number\n tasks: number\n /** Stopped by a gate or an unusable mailbox. */\n stopped: number\n /** Paused for the rep to fix, or failed for good. */\n failed: number\n /** Due, and left for a later run: the allowance, a lookup, a hold. */\n held: number\n /** Claims a dead run left, settled. */\n recovered: number\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\ninterface MailboxRun {\n orgId: string\n org: Record<string, unknown>\n mailbox: OutreachMailbox\n settings: OutreachComplianceSettingsDocument\n report: OutreachSendReport\n deadlineMs: number\n opened: OpenedOutreachMailbox | null\n /** Set once the mailbox itself cannot send this run. */\n stopped: boolean\n templates: Map<string, string | null>\n /**\n * The click-tracking origin for this mailbox's sending domain (AGL-3306):\n * `undefined` until a tracked send first asks, then the verified\n * `https://links.<domain>` or `null` for the console's own address.\n */\n linkOrigin?: string | null\n}\n\nconst blankReport = (): OutreachSendReport => ({\n due: 0,\n sent: 0,\n tasks: 0,\n stopped: 0,\n failed: 0,\n held: 0,\n recovered: 0,\n})\n\n/** Runs the send job once — see the module note. */\nexport async function runOutreachSendJob(\n deps: OutreachRuntimeDeps,\n context: { nowMs: number; deadlineMs: number },\n): Promise<OutreachSendReport> {\n const firestore = deps.firestore()\n const report = blankReport()\n const due = await firestore\n .collectionGroup(OUTREACH_COLLECTIONS.enrollments)\n .where('status', '==', 'active')\n .where('nextDueAtMs', '<=', context.nowMs)\n .orderBy('nextDueAtMs')\n .limit(OUTREACH_SEND_SCAN_LIMIT)\n .get()\n report.due = due.size\n const byOrg = new Map<string, Map<string, OutreachEnrollment[]>>()\n for (const doc of due.docs) {\n const orgId = doc.ref.parent.parent?.id\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!orgId || !enrollment) continue\n const mailboxes = byOrg.get(orgId) ?? new Map<string, OutreachEnrollment[]>()\n mailboxes.set(enrollment.mailboxId, [...(mailboxes.get(enrollment.mailboxId) ?? []), enrollment])\n byOrg.set(orgId, mailboxes)\n }\n\n for (const [orgId, mailboxes] of byOrg) {\n const waiting = [...mailboxes.values()].reduce((sum, list) => sum + list.length, 0)\n if (deps.now() >= context.deadlineMs) {\n report.held += waiting\n continue\n }\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))) {\n report.held += waiting\n continue\n }\n const settings = await readOutreachComplianceSettingsDoc(firestore, orgId)\n for (const [mailboxId, enrollments] of mailboxes) {\n await runMailbox(deps, firestore, { orgId, org, mailboxId, enrollments, settings, report, context })\n }\n }\n return report\n}\n\n/** The reason an unusable mailbox stops an enrollment, or `null` for a usable one. */\nasync function unusableMailbox(\n firestore: Firestore,\n orgId: string,\n mailbox: OutreachMailbox | null,\n): Promise<string | null> {\n if (!mailbox) return 'The mailbox this sequence sends from was removed.'\n if (mailbox.status === 'disconnected') return 'The mailbox this sequence sends from was disconnected.'\n const member = await firestore.collection('orgs').doc(orgId).collection('members').doc(mailbox.connectedByUid).get()\n return member.exists\n ? null\n : 'The member who connected the mailbox this sequence sends from is no longer in the organization.'\n}\n\nasync function runMailbox(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n input: {\n orgId: string\n org: Record<string, unknown>\n mailboxId: string\n enrollments: OutreachEnrollment[]\n settings: OutreachComplianceSettingsDocument\n report: OutreachSendReport\n context: { nowMs: number; deadlineMs: number }\n },\n): Promise<void> {\n const { orgId, org, report, context } = input\n const nowMs = context.nowMs\n const snapshot = await mailboxRef(firestore, orgId, input.mailboxId).get()\n const mailbox = readStoredOutreachMailbox(input.mailboxId, snapshot.exists ? snapshot.data() : undefined)\n const unusable = await unusableMailbox(firestore, orgId, mailbox)\n if (unusable || !mailbox) {\n for (const enrollment of input.enrollments) {\n const outcome = await applyOutreachEvent(firestore, {\n orgId,\n enrollmentId: enrollment.id,\n event: { type: 'stop', atMs: nowMs, byUid: null, reason: 'gate', detail: unusable },\n nowMs,\n })\n if (outcome.changed) report.stopped += 1\n }\n return\n }\n if (mailbox.status !== 'connected' || (await deps.sendRefusal({ orgId, org, uid: mailbox.connectedByUid }))) {\n report.held += input.enrollments.length\n return\n }\n\n const run: MailboxRun = {\n orgId,\n org,\n mailbox,\n settings: input.settings,\n report,\n deadlineMs: context.deadlineMs,\n opened: null,\n stopped: false,\n templates: new Map(),\n }\n const sequenceIds = [...new Set(input.enrollments.map((enrollment) => enrollment.sequenceId))]\n const sequenceSnapshots = await firestore.getAll(\n ...sequenceIds.map((id) => outreachOrgCollection(firestore, orgId, 'sequences').doc(id)),\n )\n const sequences = new Map<string, OutreachSequence>()\n sequenceSnapshots.forEach((sequenceSnapshot, index) => {\n const sequence = readStoredOutreachSequence(\n sequenceIds[index],\n sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined,\n )\n if (sequence) sequences.set(sequence.id, sequence)\n })\n\n const candidates: OutreachDueCandidate[] = []\n for (const enrollment of input.enrollments) {\n const sequence = sequences.get(enrollment.sequenceId)\n const claim = enrollment.sendClaim\n if (claim && nowMs - claim.atMs < OUTREACH_CLAIM_STALE_MS) continue\n if (claim && sequence) {\n if (await recoverClaim(deps, firestore, run, enrollment, sequence)) report.recovered += 1\n continue\n }\n if (sequence) candidates.push({ enrollment, sequence })\n }\n\n const allowance = outreachTickAllowance({ mailbox, nowMs })\n const selection = selectDueOutreachEnrollments({ candidates, mailbox, nowMs, allowance: allowance.allowance })\n report.held += selection.deferred.length\n\n for (const candidate of selection.tasks) {\n if (deps.now() >= context.deadlineMs) {\n report.held += 1\n continue\n }\n if (await runTaskStep(deps, firestore, run, candidate)) report.tasks += 1\n }\n if (!selection.emails.length) return\n\n // The people behind the emails, read once per site: the contact and its\n // company, and every lookup the gates decide on.\n const people = new Map<string, OutreachEnrollCandidate>()\n const lookups = new Map<string, OutreachGateLookups>()\n const byHost = new Map<string, OutreachEnrollment[]>()\n for (const candidate of selection.emails) {\n const enrollment = candidate.enrollment as OutreachEnrollment\n byHost.set(enrollment.hostId, [...(byHost.get(enrollment.hostId) ?? []), enrollment])\n }\n const siteNames = new Map<string, string>()\n for (const [hostId, enrollments] of byHost) {\n const consentGroup = consentGroupForHost(org, hostId)\n const contactGroupId = consentGroup.groupId\n // Each by the record it names — the contact, or the lead while the\n // person is one (AGL-3234) — keyed by that record's id.\n const [candidatesRead, lookupsRead, host] = await Promise.all([\n readOutreachEnrollCandidates(firestore, {\n orgId,\n hostId,\n contactGroupId,\n people: enrollments.map(outreachEnrollmentPerson),\n }),\n readOutreachGateLookups(firestore, {\n orgId,\n hostId,\n consentHostIds: consentGroup.hostIds,\n consentAwaitsConfirmation: consentGroup.awaitsConfirmation,\n people: enrollments.map((enrollment) => {\n const person = outreachEnrollmentPerson(enrollment)\n return {\n personId: person.id,\n contactId: person.kind === 'contact' ? person.id : null,\n leadId: person.kind === 'lead' ? person.id : null,\n email: enrollment.email,\n }\n }),\n }),\n firestore.collection('hosts').doc(hostId).get(),\n ])\n for (const person of candidatesRead) people.set(person.personId, person)\n for (const [personId, answer] of lookupsRead) lookups.set(personId, answer)\n siteNames.set(hostId, host.exists ? String(host.get('name') ?? '') : '')\n }\n\n for (const candidate of selection.emails) {\n if (run.stopped || deps.now() >= context.deadlineMs) {\n report.held += 1\n continue\n }\n const enrollment = candidate.enrollment as OutreachEnrollment\n const personId = outreachEnrollmentPerson(enrollment).id\n await runEmailStep(deps, firestore, run, {\n enrollment,\n sequence: candidate.sequence as OutreachSequence,\n person: people.get(personId) ?? null,\n lookups: lookups.get(personId) ?? null,\n siteName: siteNames.get(enrollment.hostId) ?? '',\n })\n }\n}\n\n/** Any lookup the runtime could not make: the send waits for a run that can. */\nfunction uncheckable(lookups: OutreachGateLookups | null): boolean {\n return !lookups || Object.values(lookups).some((value) => value === null)\n}\n\n/** The event a gate's refusal ends an enrollment with. */\nfunction gateEvent(blocks: readonly OutreachGateBlock[], atMs: number): OutreachEnrollmentEvent {\n const optedOut = blocks.find((block) => block.code === 'do_not_contact' || block.code === 'sales_opted_out')\n if (optedOut) return { type: 'opt_out', atMs, reason: 'do_not_contact', detail: optedOut.reason }\n return { type: 'stop', atMs, byUid: null, reason: 'gate', detail: blocks.map((block) => block.reason).join(' ') }\n}\n\n/**\n * The click-tracking origin for mail sent as `senderAddress`, or `null` for\n * the console's own address — which is also what a failed lookup reads as: a\n * link on the app address still counts the click, so no send waits on it.\n */\nasync function trackingLinkOrigin(\n deps: OutreachRuntimeDeps,\n orgId: string,\n senderAddress: string,\n): Promise<string | null> {\n if (!deps.clickLinkOrigin) return null\n try {\n return await deps.clickLinkOrigin({ orgId, senderAddress })\n } catch (error) {\n console.warn('[outreach] the click-tracking host could not be read; links use the app address', error)\n return null\n }\n}\n\n/** The sender line a mailbox's mail goes out with. */\nconst senderOf = (mailbox: OutreachMailbox) => ({\n address: mailbox.sendAs || mailbox.email,\n name: mailbox.displayName || null,\n})\n\nasync function openClient(deps: OutreachRuntimeDeps, run: MailboxRun) {\n const opened = (run.opened ??= await deps.openMailbox(run.mailbox.id))\n if (opened.ok === true) return opened.client\n run.stopped = true\n if (opened.reason !== 'not-configured') {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId: run.orgId,\n mailboxId: run.mailbox.id,\n errorCode: opened.reason,\n nowMs: deps.now(),\n })\n }\n return null\n}\n\n/**\n * Claims one step: the enrollment still active on the same step with no\n * live claim, and — for an email — a slot under the mailbox's cap today,\n * reserved in the same transaction.\n */\nasync function claimStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n enrollment: OutreachEnrollment,\n messageId: string | null,\n): Promise<'claimed' | 'taken' | 'cap'> {\n const enrollmentRef = outreachOrgCollection(firestore, run.orgId, 'enrollments').doc(enrollment.id)\n const boxRef = mailboxRef(firestore, run.orgId, run.mailbox.id)\n const nowMs = deps.now()\n return firestore.runTransaction(async (transaction) => {\n const [current, box] = await Promise.all([transaction.get(enrollmentRef), transaction.get(boxRef)])\n const stored = readStoredOutreachEnrollment(enrollment.id, current.exists ? current.data() : undefined)\n const claim = stored?.sendClaim\n if (\n !stored ||\n stored.status !== 'active' ||\n stored.stepIndex !== enrollment.stepIndex ||\n stored.nextDueAtMs === null ||\n stored.nextDueAtMs > nowMs ||\n (claim && nowMs - claim.atMs < OUTREACH_CLAIM_STALE_MS)\n ) {\n return 'taken'\n }\n if (messageId) {\n const mailbox = readStoredOutreachMailbox(run.mailbox.id, box.exists ? box.data() : undefined)\n if (!mailbox || mailbox.status !== 'connected') return 'taken'\n const sent = outreachSentToday(mailbox.health, nowMs, mailbox.timezone)\n if (sent >= outreachEffectiveDailyCap(mailbox, nowMs)) return 'cap'\n transaction.update(boxRef, {\n 'health.sentToday': sent + 1,\n 'health.sentOnDay': outreachLocalDay(nowMs, mailbox.timezone),\n })\n }\n transaction.update(enrollmentRef, {\n sendClaim: { token: randomUUID(), atMs: nowMs, stepIndex: enrollment.stepIndex, messageId },\n updatedAtMs: nowMs,\n })\n return 'claimed'\n })\n}\n\n/** Drops a claim, and hands an email's reservation back to the mailbox's day. */\nasync function releaseClaim(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n enrollment: Pick<OutreachEnrollment, 'id'>,\n reserved: boolean,\n event: OutreachEnrollmentEvent | null = null,\n): Promise<void> {\n const nowMs = deps.now()\n await applyOutreachEvent(firestore, {\n orgId: run.orgId,\n enrollmentId: enrollment.id,\n event,\n extra: () => ({ sendClaim: null }),\n nowMs,\n })\n if (!reserved) return\n const boxRef = mailboxRef(firestore, run.orgId, run.mailbox.id)\n await firestore.runTransaction(async (transaction) => {\n const box = await transaction.get(boxRef)\n const mailbox = readStoredOutreachMailbox(run.mailbox.id, box.exists ? box.data() : undefined)\n if (!mailbox || mailbox.health?.sentOnDay !== outreachLocalDay(nowMs, mailbox.timezone)) return\n transaction.update(boxRef, { 'health.sentToday': Math.max(0, (Number(mailbox.health.sentToday) || 0) - 1) })\n })\n}\n\n/**\n * Records a step that ran: the engine's completion patch, the claim\n * cleared, the step's record kept — and for an email the mailbox's send on\n * its day, its last-sent time and its window of recent sends. Refuses\n * (answers false) when the claim is no longer this run's.\n */\nasync function completeStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n input: {\n enrollment: OutreachEnrollment\n sequence: OutreachSequence\n record: OutreachStepRecord\n sent: { messageId: string; threadId: string; subject: string } | null\n claimMessageId: string | null\n },\n): Promise<boolean> {\n const enrollmentRef = outreachOrgCollection(firestore, run.orgId, 'enrollments').doc(input.enrollment.id)\n const boxRef = mailboxRef(firestore, run.orgId, run.mailbox.id)\n const nowMs = deps.now()\n /*\n * Whether this is the FIRST email this person has had from the sequence\n * (AGL-3239), decided from the same read the step is recorded against and\n * so re-decided on every transaction retry. It is what `stats.people`\n * counts: the denominator of every engagement rate, and the thing\n * `stats.sent` is not, since one person takes several steps.\n */\n let firstEmail = false\n const completed = await firestore.runTransaction(async (transaction) => {\n const [current, box] = await Promise.all([\n transaction.get(enrollmentRef),\n input.sent ? transaction.get(boxRef) : Promise.resolve(null),\n ])\n const stored = readStoredOutreachEnrollment(input.enrollment.id, current.exists ? current.data() : undefined)\n if (!stored?.sendClaim || stored.sendClaim.messageId !== input.claimMessageId) return false\n if (stored.stepIndex !== input.enrollment.stepIndex) return false\n // The step RAN, whatever happened to the enrollment while it did — a\n // member paused it, a reply stopped it — so it is recorded as run, and a\n // resume never sends it twice.\n firstEmail =\n input.sent !== null &&\n !(stored.stepRecords ?? []).some((record) => record.kind === 'email')\n const plan = planOutreachStepCompletion({\n enrollment: { ...stored, status: 'active' },\n sequence: input.sequence,\n mailbox: run.mailbox,\n completedAtMs: nowMs,\n sent: input.sent,\n random: deps.random,\n })\n const patch: Record<string, unknown> = {\n sendClaim: null,\n stepRecords: [...(stored.stepRecords ?? []), input.record].slice(-20),\n updatedAtMs: nowMs,\n ...(plan.patch ?? {}),\n }\n if (stored.status !== 'active') {\n patch['status'] = stored.status\n if (stored.status !== 'paused') patch['nextDueAtMs'] = null\n }\n transaction.update(enrollmentRef, patch)\n if (input.sent && box?.exists) {\n const mailbox = readStoredOutreachMailbox(run.mailbox.id, box.data())\n if (mailbox) {\n const day = outreachLocalDay(nowMs, mailbox.timezone)\n const counts = mailbox.health?.daily?.[day] ?? { sent: 0, bounces: 0, replies: 0 }\n transaction.update(boxRef, {\n 'health.lastSentAtMs': nowMs,\n 'health.lastErrorAtMs': null,\n 'health.lastErrorCode': null,\n [`health.daily.${day}`]: { ...counts, sent: (Number(counts.sent) || 0) + 1 },\n 'health.recentSends': withRecentSend(mailbox.health?.recentSends, {\n id: outreachSendDigest(input.sent.messageId),\n atMs: nowMs,\n bounced: false,\n }),\n updatedAtMs: nowMs,\n })\n }\n }\n return true\n })\n if (completed && input.sent) {\n await bumpOutreachSequenceStats(firestore, {\n orgId: run.orgId,\n sequenceId: input.sequence.id,\n firstEmail,\n trackedLinks: input.record.links?.length ?? 0,\n })\n }\n return completed\n}\n\n/**\n * The sequence's send counters (AGL-3239), written outside the enrollment's\n * transaction.\n *\n * One sequence holds every enrollment, so a transaction over it would\n * serialise every recipient against every other; `FieldValue.increment` is a\n * blind write with nothing to contend on. It is bookkeeping beside the act,\n * so a failure is logged and the send stands — the email has already left.\n *\n * `clickTracked` is stamped by the first send that actually carried a\n * rewritten link, never by the SETTING: a sequence with the setting on whose\n * emails have no links in them measured nothing, and the report must say\n * \"not measured\" for it rather than publish a 0% click rate.\n */\nasync function bumpOutreachSequenceStats(\n firestore: Firestore,\n input: { orgId: string; sequenceId: string; firstEmail: boolean; trackedLinks: number },\n): Promise<void> {\n if (!input.sequenceId) return\n await outreachOrgCollection(firestore, input.orgId, 'sequences')\n .doc(input.sequenceId)\n .update({\n 'stats.sent': FieldValue.increment(1),\n ...(input.firstEmail ? { 'stats.people': FieldValue.increment(1) } : {}),\n ...(input.trackedLinks > 0 ? { 'stats.clickTracked': true } : {}),\n })\n .catch((error: unknown) => {\n console.warn('[outreach] a send could not be counted on its sequence', error)\n })\n}\n\n/** One task step: the rep's task on the contact, then the step recorded. */\nasync function runTaskStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n candidate: OutreachDueCandidate,\n): Promise<boolean> {\n const enrollment = candidate.enrollment as OutreachEnrollment\n const sequence = candidate.sequence as OutreachSequence\n const step = sequence.steps[enrollment.stepIndex] as OutreachTaskStep\n if ((await claimStep(deps, firestore, run, enrollment, null)) !== 'claimed') return false\n const nowMs = deps.now()\n let taskId: string | null\n try {\n const written = await fileOutreachTask(deps, {\n orgId: run.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: `step:${enrollment.id}:${step.id}`,\n title: step.title,\n notes: `From sequence step ${enrollment.stepIndex + 1}.`,\n kind: OUTREACH_TASK_KIND_TO_RECORD[step.taskKind] ?? 'todo',\n dueAtMs: nowMs,\n assigneeUid: run.mailbox.connectedByUid,\n })\n taskId = written?.ok ? written.id : null\n if (written?.ok === false) console.warn(`[outreach] a task step's task was not filed: ${written.error}`)\n } catch (error) {\n // The record system failed rather than refused: the step waits for a\n // run that can file its task.\n console.error('[outreach] filing a task step failed; the step waits', error)\n await releaseClaim(deps, firestore, run, enrollment, false)\n return false\n }\n return completeStep(deps, firestore, run, {\n enrollment,\n sequence,\n record: { stepIndex: enrollment.stepIndex, stepId: step.id, kind: 'task', atMs: nowMs, taskId },\n sent: null,\n claimMessageId: null,\n })\n}\n\n/** A CRM template's body, read once per run. */\nasync function templateBody(firestore: Firestore, run: MailboxRun, templateId: string): Promise<string | null> {\n if (!run.templates.has(templateId)) {\n const snapshot = await firestore\n .collection('orgs')\n .doc(run.orgId)\n .collection(CRM_COLLECTIONS.emailTemplates)\n .doc(templateId)\n .get()\n run.templates.set(\n templateId,\n snapshot.exists ? normalizeCrmEmailTemplate(snapshot.data() as Record<string, unknown>).body : null,\n )\n }\n return run.templates.get(templateId) ?? null\n}\n\nasync function runEmailStep(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n input: {\n enrollment: OutreachEnrollment\n sequence: OutreachSequence\n person: OutreachEnrollCandidate | null\n lookups: OutreachGateLookups | null\n siteName: string\n },\n): Promise<void> {\n const { enrollment, sequence, person } = input\n const { report, mailbox } = run\n const nowMs = deps.now()\n const stop = async (event: OutreachEnrollmentEvent, counter: 'stopped' | 'failed') => {\n const outcome = await applyOutreachEvent(firestore, { orgId: run.orgId, enrollmentId: enrollment.id, event, nowMs })\n if (outcome.changed) report[counter] += 1\n }\n\n // Every gate again, on the person as they stand now.\n if (!person?.contact) {\n return stop(\n {\n type: 'stop',\n atMs: nowMs,\n byUid: null,\n reason: 'gate',\n detail:\n enrollment.target === 'lead'\n ? \"This lead no longer exists in the sequence's site's CRM.\"\n : 'This contact no longer exists in the CRM.',\n },\n 'stopped',\n )\n }\n if (person.convertedContactId && enrollment.target === 'lead') {\n // The lead converted and the enrollment has not followed it yet: the\n // conversion's listener re-points it, and the next run sends as the contact.\n report.held += 1\n return\n }\n if (!person.visible) {\n return stop(\n { type: 'stop', atMs: nowMs, byUid: null, reason: 'gate', detail: \"This contact is no longer in the sequence's site's CRM.\" },\n 'stopped',\n )\n }\n if (uncheckable(input.lookups)) {\n report.held += 1\n return\n }\n const contactGroupId = consentGroupForHost(run.org, enrollment.hostId).groupId\n const gate = evaluateOutreachGates({\n email: enrollment.email,\n contact: person.contact,\n contactGroupId,\n company: person.company,\n settings: {\n ...sequence.settings,\n allowedCountries: effectiveOutreachAllowedCountries(\n sequence.settings.allowedCountries,\n run.settings.allowedCountries,\n ),\n },\n personalLine: enrollment.personalLine,\n attestations: enrollment.attestations,\n enrollmentId: enrollment.id,\n lookups: input.lookups as OutreachGateLookups,\n })\n if (!gate.allowed) return stop(gateEvent(gate.blocks, nowMs), 'stopped')\n\n /*\n * The email, with its way out. The footer's \"reply 'no'\" line and the\n * postal address are always there (compose refuses without them). The\n * `List-Unsubscribe` header is the sequence's own setting (AGL-3296), read\n * here per send like click tracking, and absent reads as off.\n */\n const unsubscribe = outreachListUnsubscribe({\n enabled: sequence.settings.listUnsubscribe,\n mintUrl: () => deps.unsubscribeUrl({ orgId: run.orgId, enrollmentId: enrollment.id }),\n mailboxEmail: mailbox.email,\n })\n if (unsubscribe.status === 'unavailable') {\n // On, but no link can be minted — no secret, or no HTTPS console origin\n // — and an email without the way out its sequence promised does not leave.\n console.error('[outreach] no unsubscribe link could be minted; nothing sends until one can')\n run.stopped = true\n report.held += 1\n return\n }\n const step = sequence.steps[enrollment.stepIndex]\n const sender = senderOf(mailbox)\n /*\n * Click tracking is the sequence's own setting (AGL-3239) and is read\n * here, per send, rather than once per run: a member turning it off\n * applies to the next email, not to the next hour. Each link becomes a\n * short link (AGL-3297) whose document is written below, before the email\n * leaves; a link that cannot be made is left exactly as the step wrote it.\n */\n if (sequence.settings.trackClicks && run.linkOrigin === undefined) {\n run.linkOrigin = await trackingLinkOrigin(deps, run.orgId, sender.address)\n }\n const shortLinks: Array<{ id: string; doc: OutreachStoredLink }> = []\n const rewriteLink = sequence.settings.trackClicks\n ? (link: { url: string; index: number }) => {\n const doc = outreachStoredLink(\n {\n orgId: run.orgId,\n enrollmentId: enrollment.id,\n stepIndex: enrollment.stepIndex,\n linkIndex: link.index,\n url: link.url,\n },\n nowMs,\n )\n const id = newOutreachLinkId()\n const url = doc ? deps.clickLinkUrl(id, run.linkOrigin ?? null) : null\n if (!doc || !url) return null\n shortLinks.push({ id, doc })\n return url\n }\n : null\n const composed = composeOutreachEmail({\n sequence,\n enrollment,\n orgSettings: run.settings,\n rewriteLink,\n merge: {\n // For a lead, the contact-shaped view of it (`leadAsContact`), so a\n // step written with `{{contact.*}}` reads the lead; `{{lead.*}}` reads\n // the lead's own document beside it.\n contact: person.contact,\n contactGroupId,\n lead: person.lead,\n sender: { name: mailbox.displayName, email: sender.address },\n site: { name: input.siteName },\n },\n templateBody: step?.kind === 'email' && step.templateId ? await templateBody(firestore, run, step.templateId) : null,\n listUnsubscribeUrl: unsubscribe.status === 'ready' ? unsubscribe.url : null,\n listUnsubscribeMailto: unsubscribe.status === 'ready' ? unsubscribe.mailto : null,\n })\n if (composed.error || !composed.email) {\n const code = composed.error?.code\n if (code === 'missing_postal_address' || code === 'missing_legal_name') {\n // The organization's footer: nothing of theirs sends until it is set.\n run.stopped = true\n report.held += 1\n return\n }\n if (code === 'invalid_recipient' || code === 'missing_thread' || code === 'not_an_email_step') {\n return stop({ type: 'send_failed', atMs: nowMs, detail: composed.error?.message }, 'failed')\n }\n // The sequence's own words: the rep fixes the step and resumes.\n return stop({ type: 'pause', atMs: nowMs, byUid: null, detail: composed.error?.message }, 'failed')\n }\n if (composed.unresolvedFields.length) {\n const fields = composed.unresolvedFields.map((field) => `{{${field}}}`).join(', ')\n return stop(\n {\n type: 'pause',\n atMs: nowMs,\n byUid: null,\n detail: `Paused before sending: this contact has nothing for ${fields}. Fill it in and resume.`,\n },\n 'failed',\n )\n }\n\n /*\n * The short links' documents, before anything is claimed or sent: a link\n * in an email that already left must resolve on the first click. A write\n * that fails holds the send for the next run, which mints new ones — an\n * orphaned document is inert, an email whose links go nowhere is not.\n */\n if (shortLinks.length) {\n try {\n const batch = firestore.batch()\n for (const link of shortLinks) {\n batch.create(firestore.collection(OUTREACH_COLLECTIONS.links).doc(link.id), link.doc)\n }\n await batch.commit()\n } catch (error) {\n console.error('[outreach] the short links could not be stored; the send waits for the next run', error)\n report.held += 1\n return\n }\n }\n\n // Claimed, then sent, then recorded.\n const domain = sender.address.slice(sender.address.lastIndexOf('@') + 1)\n const local = randomUUID()\n const messageId = `<${local}@${domain}>`\n const claimed = await claimStep(deps, firestore, run, enrollment, messageId)\n if (claimed !== 'claimed') {\n if (claimed === 'cap') run.stopped = true\n report.held += 1\n return\n }\n const client = await openClient(deps, run)\n if (!client) {\n await releaseClaim(deps, firestore, run, enrollment, true)\n report.held += 1\n return\n }\n let sent: Awaited<ReturnType<typeof sendComposedOutreachEmail>>\n try {\n sent = await sendComposedOutreachEmail(client, composed.email, sender, {\n messageIdLocalPart: local,\n messageIdDomain: domain,\n })\n } catch (error) {\n if (error instanceof Rfc5322MessageError || (error instanceof GmailTransportError && error.code === 'invalid_request')) {\n await releaseClaim(deps, firestore, run, enrollment, true, {\n type: 'send_failed',\n atMs: deps.now(),\n detail: `Gmail refused the email: ${error.message}`,\n })\n report.failed += 1\n return\n }\n await releaseClaim(deps, firestore, run, enrollment, true)\n report.held += 1\n run.stopped = true\n const code = error instanceof GmailTransportError ? error.code : 'unexpected'\n if (isReconnectRequired(error)) {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId: run.orgId,\n mailboxId: mailbox.id,\n errorCode: code,\n nowMs: deps.now(),\n })\n } else {\n await mailboxRef(firestore, run.orgId, mailbox.id)\n .update({ 'health.lastErrorAtMs': deps.now(), 'health.lastErrorCode': code })\n .catch(() => undefined)\n }\n console.error(`[outreach] a send from a mailbox failed (${code}); its sends wait for the next run`)\n return\n }\n\n const record: OutreachStepRecord = {\n stepIndex: enrollment.stepIndex,\n stepId: step?.id ?? '',\n kind: 'email',\n atMs: deps.now(),\n gmailMessageId: sent.gmailMessageId,\n gmailThreadId: sent.threadId,\n messageId: sent.messageId,\n subject: sent.subject,\n ...(composed.email.trackedLinks.length ? { links: composed.email.trackedLinks } : {}),\n }\n await completeStep(deps, firestore, run, {\n enrollment,\n sequence,\n record,\n sent: { messageId: sent.messageId, threadId: sent.threadId, subject: sent.subject },\n claimMessageId: messageId,\n })\n report.sent += 1\n await fileOutreachEmail(deps, {\n orgId: run.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n direction: 'outbound',\n subject: sent.subject,\n from: sender.address,\n to: composed.email.to,\n messageId: sent.messageId,\n inReplyTo: composed.email.inReplyTo ?? null,\n body: composed.email.text,\n atMs: record.atMs,\n byUid: mailbox.connectedByUid,\n byName: mailbox.displayName || null,\n })\n // The first email is the touch (AGL-3254): `sent` on the sequence's\n // campaigns, and the person's record credited to the first of them.\n // Judged on the enrollment as it was read, before this step's record.\n await creditOutreachFirstSend(deps, { enrollment, atMs: record.atMs })\n}\n\n/**\n * Settles a claim a run died holding. A task step's claim is dropped and\n * the step runs again (its task is filed once however often it is asked).\n * An email's claim looks for the message it minted in the mailbox: found,\n * the send happened and is recorded; not found, it did not, and the claim\n * and its reservation are handed back for the next run.\n */\nasync function recoverClaim(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n run: MailboxRun,\n enrollment: OutreachEnrollment,\n sequence: OutreachSequence,\n): Promise<boolean> {\n const claim = enrollment.sendClaim\n if (!claim) return false\n if (!claim.messageId) {\n await releaseClaim(deps, firestore, run, enrollment, false)\n return true\n }\n const client = await openClient(deps, run)\n if (!client) return false\n const bare = claim.messageId.replace(/^<|>$/g, '')\n const found = await client.listMessages({ q: `rfc822msgid:${bare}`, maxResults: 1, includeSpamTrash: true })\n const hit = found.messages[0]\n if (!hit) {\n await releaseClaim(deps, firestore, run, enrollment, true)\n return true\n }\n const message = await client.getMessage(hit.id, { metadataHeaders: ['Subject'] })\n const subject = message.headers.find((header) => header.name.toLowerCase() === 'subject')?.value ?? ''\n const step = sequence.steps[enrollment.stepIndex]\n const atMs = deps.now()\n const completed = await completeStep(deps, firestore, run, {\n enrollment,\n sequence,\n record: {\n stepIndex: enrollment.stepIndex,\n stepId: step?.id ?? '',\n kind: 'email',\n atMs,\n gmailMessageId: hit.id,\n gmailThreadId: hit.threadId,\n messageId: claim.messageId,\n subject,\n },\n sent: { messageId: claim.messageId, threadId: hit.threadId, subject },\n claimMessageId: claim.messageId,\n })\n // A recovered first email is still the first email (AGL-3254).\n if (completed) await creditOutreachFirstSend(deps, { enrollment, atMs })\n return completed\n}\n"],"names":["consentGroupForHost","CRM_COLLECTIONS","normalizeCrmEmailTemplate","FieldValue","randomUUID","composeOutreachEmail","planOutreachStepCompletion","evaluateOutreachGates","outreachSentToday","outreachTickAllowance","selectDueOutreachEnrollments","readOutreachEnrollCandidates","readOutreachGateLookups","mailboxRef","outreachEffectiveDailyCap","outreachLocalDay","effectiveOutreachAllowedCountries","OUTREACH_COLLECTIONS","readOutreachComplianceSettingsDoc","outreachEnrollmentLink","outreachEnrollmentPerson","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","Rfc5322MessageError","sendComposedOutreachEmail","creditOutreachFirstSend","newOutreachLinkId","outreachStoredLink","applyOutreachEvent","outreachSendDigest","withRecentSend","noteOutreachMailboxReconnectRequired","fileOutreachEmail","fileOutreachTask","OUTREACH_TASK_KIND_TO_RECORD","outreachListUnsubscribe","OUTREACH_SEND_JOB_ID","OUTREACH_SEND_SCAN_LIMIT","OUTREACH_CLAIM_STALE_MS","blankReport","due","sent","tasks","stopped","failed","held","recovered","runOutreachSendJob","deps","context","firestore","report","collectionGroup","enrollments","where","nowMs","orderBy","limit","get","size","byOrg","Map","doc","docs","mailboxes","orgId","ref","parent","id","enrollment","data","set","mailboxId","waiting","values","reduce","sum","list","length","now","deadlineMs","orgSnapshot","collection","org","exists","orgRefusal","settings","runMailbox","unusableMailbox","mailbox","status","member","connectedByUid","input","snapshot","undefined","unusable","outcome","enrollmentId","event","type","atMs","byUid","reason","detail","changed","sendRefusal","uid","run","opened","templates","sequenceIds","Set","map","sequenceId","sequenceSnapshots","getAll","sequences","forEach","sequenceSnapshot","index","sequence","candidates","claim","sendClaim","recoverClaim","push","allowance","selection","deferred","candidate","runTaskStep","emails","people","lookups","byHost","hostId","siteNames","host","consentGroup","contactGroupId","groupId","candidatesRead","lookupsRead","Promise","all","consentHostIds","hostIds","consentAwaitsConfirmation","awaitsConfirmation","person","personId","contactId","kind","leadId","email","answer","String","runEmailStep","siteName","uncheckable","Object","some","value","gateEvent","blocks","optedOut","find","block","code","join","trackingLinkOrigin","senderAddress","clickLinkOrigin","error","console","warn","senderOf","address","sendAs","name","displayName","openClient","openMailbox","ok","client","errorCode","claimStep","messageId","enrollmentRef","boxRef","runTransaction","transaction","current","box","stored","stepIndex","nextDueAtMs","health","timezone","update","token","updatedAtMs","releaseClaim","reserved","extra","sentOnDay","Math","max","Number","sentToday","completeStep","firstEmail","completed","plan","resolve","claimMessageId","stepRecords","record","completedAtMs","random","patch","slice","day","counts","daily","bounces","replies","recentSends","bounced","bumpOutreachSequenceStats","trackedLinks","links","increment","catch","step","steps","taskId","written","link","dedupeKey","title","notes","taskKind","dueAtMs","assigneeUid","stepId","templateBody","templateId","has","emailTemplates","body","composed","stop","counter","contact","target","convertedContactId","visible","gate","company","allowedCountries","personalLine","attestations","allowed","unsubscribe","enabled","listUnsubscribe","mintUrl","unsubscribeUrl","mailboxEmail","sender","trackClicks","linkOrigin","shortLinks","rewriteLink","linkIndex","url","clickLinkUrl","orgSettings","merge","lead","site","listUnsubscribeUrl","listUnsubscribeMailto","mailto","message","unresolvedFields","fields","field","batch","create","commit","domain","lastIndexOf","local","claimed","messageIdLocalPart","messageIdDomain","gmailMessageId","gmailThreadId","threadId","subject","direction","from","to","inReplyTo","text","byName","bare","replace","found","listMessages","q","maxResults","includeSpamTrash","hit","messages","getMessage","metadataHeaders","headers","header","toLowerCase"],"mappings":"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"}