@aglyn/plugins-outreach 1.0.0-beta.274 → 1.0.0-beta.275
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/README.md +7 -2
- package/package.json +14 -14
- package/src/lib/components/enrollment-actions.d.ts +11 -4
- package/src/lib/components/enrollment-actions.js +16 -0
- package/src/lib/components/enrollment-actions.js.map +1 -1
- package/src/lib/components/enrollment-detail.js +40 -13
- package/src/lib/components/enrollment-detail.js.map +1 -1
- package/src/lib/components/mailbox-card.js +13 -2
- package/src/lib/components/mailbox-card.js.map +1 -1
- package/src/lib/components/record-reply-button.d.ts +52 -0
- package/src/lib/components/record-reply-button.js +92 -0
- package/src/lib/components/record-reply-button.js.map +1 -0
- package/src/lib/components/reply-dialog.d.ts +66 -0
- package/src/lib/components/reply-dialog.js +353 -0
- package/src/lib/components/reply-dialog.js.map +1 -0
- package/src/lib/components/use-outreach-api.d.ts +5 -1
- package/src/lib/components/use-outreach-api.js +10 -0
- package/src/lib/components/use-outreach-api.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +8 -0
- package/src/lib/constants/api-routes.js +6 -0
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/engine/index.d.ts +1 -0
- package/src/lib/engine/index.js +1 -0
- package/src/lib/engine/index.js.map +1 -1
- package/src/lib/engine/manual-reply.d.ts +108 -0
- package/src/lib/engine/manual-reply.js +301 -0
- package/src/lib/engine/manual-reply.js.map +1 -0
- package/src/lib/model/enrollment-timeline.d.ts +3 -1
- package/src/lib/model/enrollment-timeline.js +83 -7
- package/src/lib/model/enrollment-timeline.js.map +1 -1
- package/src/lib/model/outreach-api.d.ts +66 -2
- package/src/lib/model/outreach-api.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +85 -3
- package/src/lib/model/outreach.types.js +6 -0
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/stored-records.d.ts +7 -1
- package/src/lib/model/stored-records.js +37 -1
- package/src/lib/model/stored-records.js.map +1 -1
- package/src/lib/plugin.d.ts +4 -0
- package/src/lib/plugin.js +17 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/routes/register-routes.d.ts +6 -0
- package/src/lib/routes/register-routes.js +18 -0
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/reply-routes.d.ts +76 -0
- package/src/lib/routes/reply-routes.js +563 -0
- package/src/lib/routes/reply-routes.js.map +1 -0
- package/src/lib/runtime/sync-job.js +14 -0
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/transport/send-message.d.ts +11 -0
- package/src/lib/transport/send-message.js +22 -0
- package/src/lib/transport/send-message.js.map +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/routes/reply-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 type { PluginWebApiHandler } from '@aglyn/aglyn/server'\nimport { randomUUID } from 'node:crypto'\nimport {\n composeOutreachManualReply,\n mergeOutreachInboundMessages,\n outreachEnrollmentHasInbound,\n outreachInboundMessagesOf,\n outreachMailboxAddress,\n outreachManualReplyRefusal,\n outreachMessageToAnswer,\n outreachReplySubject,\n type OutreachManualReplyRefusal,\n} from '../engine/manual-reply'\nimport { outreachThreadMessageFromGmail, type OutreachThreadMessage } from '../engine/thread-message'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport { outreachLocalDay } from '../mailboxes/mailbox-settings'\nimport {\n markOutreachMailboxReconnectRequired,\n type OpenedOutreachMailbox,\n} from '../mailboxes/mailbox-transport'\nimport type {\n OutreachEnrollmentReplyResponse,\n OutreachEnrollmentThreadResponse,\n OutreachReplyDraft,\n} from '../model/outreach-api'\nimport type {\n OutreachEnrollment,\n OutreachInboundMessage,\n OutreachMailbox,\n OutreachStepRecord,\n} from '../model/outreach.types'\nimport { fileOutreachEmail } from '../runtime/timeline'\nimport { outreachUnsubscribeMailbox } from '../runtime/unsubscribe-link'\nimport {\n lookupOutreachDoNotContact,\n lookupOutreachDoNotContactDomains,\n} from '../storage/do-not-contact-store'\nimport {\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n} from '../storage/outreach-records'\nimport type { OutreachMailClient } from '../transport/mail-client'\nimport { GmailTransportError, googleFailureAnswer, isReconnectRequired } from '../transport/gmail-errors'\nimport { Rfc5322MessageError } from '../transport/rfc5322'\nimport { sendOutreachReply } from '../transport/send-message'\nimport type { OutreachEnrollRouteDeps } from './enroll-routes'\nimport type { OutreachRouteCaller } from './route-gate'\nimport { outreachRouteGate } from './route-gate'\nimport {\n outreachMethodNotAllowed,\n outreachOk,\n outreachRefusal,\n readOutreachDocumentId,\n readOutreachJsonBody,\n} from './route-http'\n\n/**\n * ANSWERING A PERSON WHO WROTE BACK (AGL-4020): `outreach/enrollments/thread`\n * and `outreach/enrollments/reply`.\n *\n * A person who answered a sequence email is answered by a member, from the\n * enrollment's own mailbox, in the same thread — never from the platform's\n * mail service, which carries no prospecting, and never as a new thread.\n * The engine writes the answer (`engine/manual-reply.ts`); these routes read\n * what it answers, check what still refuses, and send it.\n *\n * ## What the person wrote\n *\n * The sync keeps every message of theirs on the enrollment. A reply synced\n * before they were kept is on the enrollment only as an id, so when the\n * enrollment says someone wrote back and keeps nothing they wrote, the\n * `thread` route reads the enrollment's threads in the mailbox — and, when\n * they answered in a thread of their own, the messages from their address\n * since they were enrolled — and keeps what it finds. That read is made only\n * for a member who may send from the mailbox: nobody reads another member's\n * mail through these routes.\n *\n * ## The answer is no step of the sequence\n *\n * It is kept as a `manual_reply` step record and filed on the person's CRM\n * record. It never restarts the sequence or moves its step; it never counts\n * against the mailbox's daily cap, joins the sends a bounce rate is judged\n * over, or bumps the sequence's figures. The mailbox counts it apart, on\n * `health.daily.<day>.manualReplies`, so it can never pause the mailbox.\n * Its `Message-ID` joins the enrollment's when it went into the current\n * thread, so a later step of an enrollment still running answers it, and\n * the enrollment's `lastSentAtMs` moves, which keeps the thread watched for\n * what the person writes next.\n *\n * ## Once\n *\n * The composer mints a key per answer, which becomes its `Message-ID`. The\n * answer is claimed in a transaction before it is sent: a retry of the same\n * answer that already went is answered with its record, and a second answer\n * while one is in flight is refused. A send that fails hands the claim back.\n */\n\n/** Answers one mailbox may send in an hour: a conversation, not a campaign. */\nexport const OUTREACH_MANUAL_REPLIES_PER_HOUR = 30\n\n/** How long a claim on an answer holds before another answer may go. */\nconst REPLY_CLAIM_STALE_MS = 2 * 60_000\n\n/** The most of an enrollment's threads one read opens, newest first. */\nconst THREADS_READ_MAX = 5\n\n/** The most messages from the person's address one read opens, outside those threads. */\nconst OWN_THREAD_MESSAGES_MAX = 10\n\n/** A reply key as the composer mints one: a UUID, or anything as plain. */\nconst REPLY_KEY = /^[A-Za-z0-9][A-Za-z0-9-]{7,63}$/\n\n/** The routes' reach beyond the shared route deps: the mailbox, and the platform's rate limit. */\nexport interface OutreachReplyDeps {\n /** Opens a mailbox's client from its sealed grant. */\n openMailbox(mailboxId: string): Promise<OpenedOutreachMailbox>\n consumeRateLimit(key: string, options: { limit: number; windowMs: number }): Promise<{ allowed: boolean }>\n}\n\ntype Firestore = FirebaseFirestore.Firestore\n\ninterface Loaded {\n enrollment: OutreachEnrollment\n mailbox: OutreachMailbox | null\n}\n\n/** The enrollment, and the mailbox an answer goes from: its own, else its sequence's. */\nasync function loadEnrollment(firestore: Firestore, orgId: string, enrollmentId: string): Promise<Loaded | null> {\n const snapshot = await outreachOrgCollection(firestore, orgId, 'enrollments').doc(enrollmentId).get()\n const enrollment = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!enrollment) return null\n let mailboxId = enrollment.mailboxId\n if (!mailboxId && enrollment.sequenceId) {\n const sequence = await outreachOrgCollection(firestore, orgId, 'sequences').doc(enrollment.sequenceId).get()\n mailboxId = String(sequence.exists ? (sequence.get('mailboxId') ?? '') : '')\n }\n if (!mailboxId) return { enrollment, mailbox: null }\n const box = await mailboxRef(firestore, orgId, mailboxId).get()\n return { enrollment, mailbox: readStoredOutreachMailbox(mailboxId, box.exists ? box.data() : undefined) }\n}\n\n/** The member is the mailbox's own, or an organization owner or admin. */\nconst mayUseMailbox = (caller: OutreachRouteCaller, mailbox: OutreachMailbox | null) =>\n Boolean(mailbox) && (mailbox?.connectedByUid === caller.uid || caller.isOrgAdmin)\n\n/** The mailbox's own addresses: the account's, and every alias it sends as. */\nconst selfAddressesOf = (mailbox: OutreachMailbox) =>\n [mailbox.email, mailbox.sendAs, ...(mailbox.sendAsOptions ?? []).map((option) => option.email)].filter(Boolean)\n\n/**\n * What the person wrote, read from the mailbox: the enrollment's threads,\n * newest first, and — when none holds a message of theirs — the messages\n * from their address since they were enrolled.\n */\nasync function readMailboxInbound(\n client: OutreachMailClient,\n enrollment: OutreachEnrollment,\n mailbox: OutreachMailbox,\n): Promise<OutreachInboundMessage[]> {\n const context = {\n selfAddresses: selfAddressesOf(mailbox),\n unsubscribeAddress: outreachUnsubscribeMailbox(mailbox.email)?.address ?? null,\n threadSubject: enrollment.threadSubject,\n }\n const threads = [\n ...new Set([...(enrollment.gmailThreadIds ?? []), enrollment.gmailThreadId].filter((id): id is string => Boolean(id))),\n ]\n .reverse()\n .slice(0, THREADS_READ_MAX)\n const messages: OutreachThreadMessage[] = []\n for (const threadId of threads) {\n const thread = await client.getFullThread(threadId)\n messages.push(...thread.messages.map(outreachThreadMessageFromGmail))\n }\n let found = outreachInboundMessagesOf(messages, context)\n if (!found.length && enrollment.email) {\n const listed = await client.searchMessages(\n { afterMs: enrollment.createdAtMs || 0, from: [enrollment.email] },\n { maxResults: OWN_THREAD_MESSAGES_MAX },\n )\n const own: OutreachThreadMessage[] = []\n for (const stub of listed.messages.slice(0, OWN_THREAD_MESSAGES_MAX)) {\n own.push(outreachThreadMessageFromGmail(await client.getFullMessage(stub.id)))\n }\n found = outreachInboundMessagesOf(own, context)\n }\n return found\n}\n\n/** Keeps what the mailbox held on the enrollment; answers the enrollment as stored. */\nasync function keepInbound(\n firestore: Firestore,\n orgId: string,\n enrollment: OutreachEnrollment,\n found: readonly OutreachInboundMessage[],\n nowMs: number,\n): Promise<OutreachEnrollment> {\n const ref = outreachOrgCollection(firestore, orgId, 'enrollments').doc(enrollment.id)\n return firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const current = readStoredOutreachEnrollment(enrollment.id, snapshot.exists ? snapshot.data() : undefined)\n if (!current) return enrollment\n const inboundMessages = mergeOutreachInboundMessages(current.inboundMessages, found)\n transaction.update(ref, { inboundMessages, updatedAtMs: nowMs })\n return { ...current, inboundMessages, updatedAtMs: nowMs }\n })\n}\n\ntype InboundRead =\n | { ok: true; enrollment: OutreachEnrollment; readMailbox: boolean }\n | { ok: false; response: Response }\n\n/**\n * The enrollment with what the person wrote: as kept, or — when it says\n * they wrote back and keeps nothing — read from the mailbox and kept.\n */\nasync function withInbound(\n deps: OutreachEnrollRouteDeps,\n replyDeps: OutreachReplyDeps,\n caller: OutreachRouteCaller,\n loaded: Loaded,\n): Promise<InboundRead> {\n const { enrollment, mailbox } = loaded\n const unread =\n !enrollment.inboundMessages?.length &&\n outreachEnrollmentHasInbound(enrollment) &&\n mailbox !== null &&\n (mailbox.status === 'connected' || mailbox.status === 'paused') &&\n mayUseMailbox(caller, mailbox)\n if (!unread || !mailbox) return { ok: true, enrollment, readMailbox: false }\n const firestore = deps.firestore()\n const nowMs = deps.now()\n const opened = await replyDeps.openMailbox(mailbox.id)\n if (opened.ok === false) {\n if (opened.reason === 'not-configured') return { ok: true, enrollment, readMailbox: false }\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: caller.orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n mailbox.status = 'reconnect_required'\n return { ok: true, enrollment, readMailbox: false }\n }\n let found: OutreachInboundMessage[]\n try {\n found = await readMailboxInbound(opened.client, enrollment, mailbox)\n } catch (error) {\n if (isReconnectRequired(error)) {\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: caller.orgId,\n mailboxId: mailbox.id,\n errorCode: error instanceof GmailTransportError ? error.code : 'invalid_grant',\n nowMs,\n })\n mailbox.status = 'reconnect_required'\n return { ok: true, enrollment, readMailbox: false }\n }\n if (!(error instanceof GmailTransportError)) throw error\n const answer = googleFailureAnswer(error, 'reply thread read')\n return { ok: false, response: outreachRefusal(answer.status, answer.reason, answer.error) }\n }\n if (!found.length) return { ok: true, enrollment, readMailbox: true }\n return { ok: true, enrollment: await keepInbound(firestore, caller.orgId, enrollment, found, nowMs), readMailbox: true }\n}\n\n/** Every gate an answer still passes — see `outreachManualReplyRefusal`. */\nasync function refusalFor(\n firestore: Firestore,\n caller: OutreachRouteCaller,\n enrollment: OutreachEnrollment,\n mailbox: OutreachMailbox | null,\n to: string | null,\n): Promise<OutreachManualReplyRefusal | null> {\n const recipients = [...new Set([enrollment.email, to].filter((address): address is string => Boolean(address)))]\n const [addresses, domains] = await Promise.all([\n lookupOutreachDoNotContact(firestore, caller.orgId, recipients),\n lookupOutreachDoNotContactDomains(firestore, caller.orgId, recipients),\n ])\n const fold = (answers: Map<string, boolean | null>): boolean | null => {\n const values = recipients.map((address) => answers.get(address) ?? null)\n if (values.includes(true)) return true\n return values.includes(null) ? null : false\n }\n return outreachManualReplyRefusal({\n enrollment,\n doNotContact: fold(addresses),\n doNotContactDomain: fold(domains),\n mailbox,\n mayUseMailbox: mayUseMailbox(caller, mailbox),\n })\n}\n\n/** Where an answer to the newest message would go, from what, under what subject. */\nfunction draftFor(enrollment: OutreachEnrollment, mailbox: OutreachMailbox | null): OutreachReplyDraft | null {\n const answering = outreachMessageToAnswer(enrollment.inboundMessages)\n if (!answering || !mailbox) return null\n return {\n answerId: answering.id,\n to: answering.replyTo ?? answering.from,\n subject: outreachReplySubject(answering, enrollment.threadSubject),\n // A mailbox connected before Microsoft ones existed names no provider: it is Google's.\n from: { address: outreachMailboxAddress(mailbox), name: mailbox.displayName || '', provider: mailbox.provider ?? 'google' },\n }\n}\n\nconst REFUSAL_STATUS: Record<OutreachManualReplyRefusal['code'], number> = {\n 'opted-out': 409,\n bounced: 409,\n 'do-not-contact': 409,\n unchecked: 503,\n 'mailbox-unavailable': 409,\n permission: 403,\n}\n\n/** `outreach/enrollments/thread` — see the module note. */\nexport function createOutreachEnrollmentThreadRoute(\n deps: OutreachEnrollRouteDeps,\n replyDeps: OutreachReplyDeps,\n): PluginWebApiHandler {\n return 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)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n if (!enrollmentId) return outreachRefusal(400, 'invalid-request', 'Name the enrollment.')\n const firestore = deps.firestore()\n const loaded = await loadEnrollment(firestore, caller.orgId, enrollmentId)\n if (!loaded) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n const read = await withInbound(deps, replyDeps, caller, loaded)\n if (read.ok === false) return read.response\n const { enrollment, readMailbox } = read\n const draft = draftFor(enrollment, loaded.mailbox)\n const refusal = draft ? await refusalFor(firestore, caller, enrollment, loaded.mailbox, draft.to) : null\n return outreachOk({\n ok: true,\n enrollment,\n inboundMessages: enrollment.inboundMessages ?? [],\n draft,\n refusal,\n readMailbox,\n } satisfies OutreachEnrollmentThreadResponse)\n }\n}\n\n/** `outreach/enrollments/reply` — see the module note. */\nexport function createOutreachEnrollmentReplyRoute(\n deps: OutreachEnrollRouteDeps,\n replyDeps: OutreachReplyDeps,\n): PluginWebApiHandler {\n return 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)\n if (caller instanceof Response) return caller\n const enrollmentId = readOutreachDocumentId(body['enrollmentId'])\n if (!enrollmentId) return outreachRefusal(400, 'invalid-request', 'Name the enrollment.')\n const named = typeof body['replyKey'] === 'string' ? body['replyKey'].trim() : ''\n if (named && !REPLY_KEY.test(named)) return outreachRefusal(400, 'invalid-request', 'That reply key is not one the composer mints.')\n const replyKey = named || randomUUID()\n const answerId = typeof body['answerId'] === 'string' ? body['answerId'].trim() : null\n\n const firestore = deps.firestore()\n const loaded = await loadEnrollment(firestore, caller.orgId, enrollmentId)\n if (!loaded) return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n const read = await withInbound(deps, replyDeps, caller, loaded)\n if (read.ok === false) return read.response\n const { mailbox } = loaded\n const answering = outreachMessageToAnswer(read.enrollment.inboundMessages, answerId)\n if (!answering) {\n return outreachRefusal(\n 409,\n 'nothing-to-answer',\n answerId\n ? 'That message of theirs is not one this enrollment kept.'\n : 'Nothing they wrote could be read, so there is nothing to answer here yet.',\n )\n }\n const composed = composeOutreachManualReply({\n enrollment: read.enrollment,\n answering,\n body: body['body'],\n timeZone: mailbox?.timezone,\n })\n if (composed.error || !composed.reply) {\n return outreachRefusal(400, 'invalid-request', composed.error?.message ?? 'The answer could not be written.')\n }\n const reply = composed.reply\n const refused = await refusalFor(firestore, caller, read.enrollment, mailbox, reply.to)\n if (refused || !mailbox) {\n const refusal = refused ?? { code: 'mailbox-unavailable' as const, message: 'The mailbox is no longer connected.' }\n return outreachRefusal(\n REFUSAL_STATUS[refusal.code],\n refusal.code === 'permission' ? 'permission' : 'reply-refused',\n refusal.message,\n )\n }\n const limited = await replyDeps.consumeRateLimit(`outreach-manual-reply:${mailbox.id}`, {\n limit: OUTREACH_MANUAL_REPLIES_PER_HOUR,\n windowMs: 60 * 60_000,\n })\n if (!limited.allowed) {\n return outreachRefusal(\n 429,\n 'rate-limited',\n `A mailbox can send ${OUTREACH_MANUAL_REPLIES_PER_HOUR} answers an hour. Try again later.`,\n )\n }\n\n const sender = { address: outreachMailboxAddress(mailbox), name: mailbox.displayName || null }\n const domain = sender.address.slice(sender.address.lastIndexOf('@') + 1)\n const messageId = `<${replyKey}@${domain}>`\n const ref = outreachOrgCollection(firestore, caller.orgId, 'enrollments').doc(enrollmentId)\n\n // Claimed before it is sent: a retry of this answer, or a second one in flight.\n const claim = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const current = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!current) return { kind: 'gone' as const }\n const sent = (current.stepRecords ?? []).find(\n (record) => record.kind === 'manual_reply' && (record.replyKey === replyKey || record.messageId === messageId),\n )\n if (sent) return { kind: 'sent' as const, enrollment: current, record: sent }\n const held = current.replyClaim\n if (held && deps.now() - held.atMs < REPLY_CLAIM_STALE_MS) return { kind: 'busy' as const }\n transaction.update(ref, { replyClaim: { messageId, byUid: caller.uid, atMs: deps.now() } })\n return { kind: 'claimed' as const }\n })\n if (claim.kind === 'gone') return outreachRefusal(404, 'enrollment-not-found', 'That enrollment no longer exists.')\n if (claim.kind === 'busy') {\n return outreachRefusal(409, 'reply-in-flight', 'An answer to them is already being sent. Wait a moment and look again.')\n }\n if (claim.kind === 'sent') {\n return outreachOk({\n ok: true,\n enrollment: claim.enrollment,\n record: claim.record,\n sentTo: claim.record.to ?? reply.to,\n subject: claim.record.subject ?? reply.subject,\n alreadySent: true,\n } satisfies OutreachEnrollmentReplyResponse)\n }\n const release = () =>\n ref.update({ replyClaim: null }).catch((error: unknown) => {\n console.error('[outreach] an answer’s claim could not be handed back', error)\n })\n\n const nowMs = deps.now()\n const opened = await replyDeps.openMailbox(mailbox.id)\n if (opened.ok === false) {\n await release()\n if (opened.reason === 'not-configured') {\n return outreachRefusal(503, 'mailbox-unavailable', 'Mailboxes are not set up on this deployment.')\n }\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: caller.orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n `${sender.address} needs to be connected again. Reconnect it on the Mailboxes page to answer from it.`,\n )\n }\n let sent: Awaited<ReturnType<typeof sendOutreachReply>>\n try {\n sent = await sendOutreachReply(opened.client, reply, sender, {\n messageIdLocalPart: replyKey,\n messageIdDomain: domain,\n })\n } catch (error) {\n await release()\n if (error instanceof Rfc5322MessageError) {\n return outreachRefusal(400, 'invalid-request', `The answer could not be written: ${error.message}`)\n }\n if (error instanceof GmailTransportError && error.reconnectRequired) {\n await markOutreachMailboxReconnectRequired(firestore, {\n orgId: caller.orgId,\n mailboxId: mailbox.id,\n errorCode: error.code,\n nowMs,\n })\n return outreachRefusal(\n 409,\n 'mailbox-unavailable',\n `The mail provider refused ${sender.address}’s access. Reconnect it on the Mailboxes page to answer from it.`,\n )\n }\n const answer = googleFailureAnswer(error, 'manual reply send')\n return outreachRefusal(answer.status, answer.reason, answer.error)\n }\n\n const atMs = deps.now()\n const record: OutreachStepRecord = {\n stepIndex: read.enrollment.stepIndex,\n stepId: '',\n kind: 'manual_reply',\n atMs,\n byUid: caller.uid,\n from: sender.address,\n to: reply.to,\n body: reply.body,\n replyKey,\n gmailMessageId: sent.gmailMessageId,\n gmailThreadId: sent.threadId,\n messageId: sent.messageId,\n subject: sent.subject,\n ...(reply.inReplyTo ? { inReplyTo: reply.inReplyTo } : {}),\n }\n const enrollment = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const current = readStoredOutreachEnrollment(enrollmentId, snapshot.exists ? snapshot.data() : undefined)\n if (!current) return null\n const patch: Record<string, unknown> = {\n stepRecords: [...(current.stepRecords ?? []), record].slice(-20),\n replyClaim: null,\n // The thread is watched for what they write next, and the\n // enrollments table reads this as the last thing that happened.\n lastSentAtMs: atMs,\n updatedAtMs: atMs,\n }\n if (sent.threadId && !current.gmailThreadIds.includes(sent.threadId)) {\n patch['gmailThreadIds'] = [...current.gmailThreadIds, sent.threadId]\n }\n // Into the current thread: a later step answers this answer.\n if (sent.threadId && sent.threadId === current.gmailThreadId && !current.messageIds.includes(sent.messageId)) {\n patch['messageIds'] = [...current.messageIds, sent.messageId]\n }\n transaction.update(ref, patch)\n return { ...current, ...patch } as OutreachEnrollment\n })\n await countManualReply(firestore, caller.orgId, mailbox, atMs)\n await fileOutreachEmail(deps, {\n orgId: caller.orgId,\n hostId: read.enrollment.hostId,\n link: outreachEnrollmentLink(read.enrollment),\n direction: 'outbound',\n subject: sent.subject,\n from: sender.address,\n to: reply.to,\n messageId: sent.messageId,\n inReplyTo: reply.inReplyTo,\n body: reply.body,\n atMs,\n byUid: caller.uid,\n byName: mailbox.connectedByUid === caller.uid ? mailbox.displayName || null : null,\n })\n return outreachOk({\n ok: true,\n enrollment: enrollment ?? read.enrollment,\n record,\n sentTo: reply.to,\n subject: sent.subject,\n alreadySent: false,\n } satisfies OutreachEnrollmentReplyResponse)\n }\n}\n\n/**\n * One more answer on the mailbox's local day, in\n * `health.daily.<day>.manualReplies` beside the day's sends and never in\n * them. Bookkeeping beside the act: the answer has left, so a failure is\n * logged.\n */\nasync function countManualReply(\n firestore: Firestore,\n orgId: string,\n mailbox: OutreachMailbox,\n nowMs: number,\n): Promise<void> {\n const ref = mailboxRef(firestore, orgId, mailbox.id)\n try {\n await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const stored = readStoredOutreachMailbox(mailbox.id, snapshot.exists ? snapshot.data() : undefined)\n if (!stored) return\n const day = outreachLocalDay(nowMs, stored.timezone)\n const counts = stored.health?.daily?.[day] ?? { sent: 0, bounces: 0, replies: 0 }\n const manualReplies = Math.max(0, Number(counts.manualReplies) || 0) + 1\n transaction.update(ref, { [`health.daily.${day}`]: { ...counts, manualReplies }, updatedAtMs: nowMs })\n })\n } catch (error) {\n console.warn('[outreach] an answer could not be counted on its mailbox', error)\n }\n}\n"],"names":["randomUUID","composeOutreachManualReply","mergeOutreachInboundMessages","outreachEnrollmentHasInbound","outreachInboundMessagesOf","outreachMailboxAddress","outreachManualReplyRefusal","outreachMessageToAnswer","outreachReplySubject","outreachThreadMessageFromGmail","mailboxRef","outreachLocalDay","markOutreachMailboxReconnectRequired","fileOutreachEmail","outreachUnsubscribeMailbox","lookupOutreachDoNotContact","lookupOutreachDoNotContactDomains","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","GmailTransportError","googleFailureAnswer","isReconnectRequired","Rfc5322MessageError","sendOutreachReply","outreachRouteGate","outreachMethodNotAllowed","outreachOk","outreachRefusal","readOutreachDocumentId","readOutreachJsonBody","OUTREACH_MANUAL_REPLIES_PER_HOUR","REPLY_CLAIM_STALE_MS","THREADS_READ_MAX","OWN_THREAD_MESSAGES_MAX","REPLY_KEY","loadEnrollment","firestore","orgId","enrollmentId","snapshot","doc","get","enrollment","exists","data","undefined","mailboxId","sequenceId","sequence","String","mailbox","box","mayUseMailbox","caller","Boolean","connectedByUid","uid","isOrgAdmin","selfAddressesOf","email","sendAs","sendAsOptions","map","option","filter","readMailboxInbound","client","context","selfAddresses","unsubscribeAddress","address","threadSubject","threads","Set","gmailThreadIds","gmailThreadId","id","reverse","slice","messages","threadId","thread","getFullThread","push","found","length","listed","searchMessages","afterMs","createdAtMs","from","maxResults","own","stub","getFullMessage","keepInbound","nowMs","ref","runTransaction","transaction","current","inboundMessages","update","updatedAtMs","withInbound","deps","replyDeps","loaded","unread","status","ok","readMailbox","now","opened","openMailbox","reason","errorCode","error","code","answer","response","refusalFor","to","recipients","addresses","domains","Promise","all","fold","answers","values","includes","doNotContact","doNotContactDomain","draftFor","answering","answerId","replyTo","subject","name","displayName","provider","REFUSAL_STATUS","bounced","unchecked","permission","createOutreachEnrollmentThreadRoute","request","method","body","gate","Response","read","draft","refusal","createOutreachEnrollmentReplyRoute","named","trim","test","replyKey","composed","timeZone","timezone","reply","message","refused","limited","consumeRateLimit","limit","windowMs","allowed","sender","domain","lastIndexOf","messageId","claim","kind","sent","stepRecords","find","record","held","replyClaim","atMs","byUid","sentTo","alreadySent","release","catch","console","messageIdLocalPart","messageIdDomain","reconnectRequired","stepIndex","stepId","gmailMessageId","inReplyTo","patch","lastSentAtMs","messageIds","countManualReply","hostId","link","direction","byName","stored","day","counts","health","daily","bounces","replies","manualReplies","Math","max","Number","warn"],"mappings":"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"}
|
|
@@ -21,6 +21,7 @@ import { readOutreachDeliveryReport } from "../engine/delivery-status.js";
|
|
|
21
21
|
import { outreachEmailDomain } from "../engine/do-not-contact-domain.js";
|
|
22
22
|
import { outreachStoppedEntry } from "../engine/enrollment-activity.js";
|
|
23
23
|
import { isOutreachGatewayBlock, outreachGatewayBlockDetail } from "../engine/gateway-block.js";
|
|
24
|
+
import { mergeOutreachInboundMessages, outreachInboundMessagesOf } from "../engine/manual-reply.js";
|
|
24
25
|
import { effectiveOutreachWindow, postponeOutreachForAutoReply } from "../engine/schedule.js";
|
|
25
26
|
import { decideOutreachThread } from "../engine/thread-classification.js";
|
|
26
27
|
import { outreachHeader, outreachThreadMessageFromGmail } from "../engine/thread-message.js";
|
|
@@ -48,6 +49,9 @@ import { outreachUnsubscribeMailbox } from "./unsubscribe-link.js";
|
|
|
48
49
|
* - a REPLY stops the enrollment, files the message on the contact's
|
|
49
50
|
* timeline (once per `Message-ID`, under the capture address's own id for
|
|
50
51
|
* it) and gives the rep the task "Reply from <name>";
|
|
52
|
+
* - every message the person wrote — a reply, an automatic answer, an
|
|
53
|
+
* opt-out — is kept on the enrollment, its sender, time, threading ids
|
|
54
|
+
* and trimmed text, for the console to show and answer (AGL-4020);
|
|
51
55
|
* - an OUT-OF-OFFICE postpones the next step a working week from the reply;
|
|
52
56
|
* - an OPT-OUT — a reply asking to be left alone, or a message to the
|
|
53
57
|
* `+unsubscribe` address the `mailto:` names — puts the address on the
|
|
@@ -402,6 +406,15 @@ async function syncMailbox(deps, firestore, input) {
|
|
|
402
406
|
});
|
|
403
407
|
const freshIds = decision.classifications.map((classification)=>classification.messageId).filter(Boolean);
|
|
404
408
|
if (!freshIds.length) return;
|
|
409
|
+
// What they wrote, kept on the enrollment so the console can show it and
|
|
410
|
+
// answer it in the thread (AGL-4020) — every message of theirs among
|
|
411
|
+
// these, so a thread read whole also keeps a reply synced before they
|
|
412
|
+
// were kept.
|
|
413
|
+
const inbound = outreachInboundMessagesOf(messages, {
|
|
414
|
+
selfAddresses: context.selfAddresses,
|
|
415
|
+
unsubscribeAddress: context.unsubscribeAddress,
|
|
416
|
+
threadSubject: enrollment.threadSubject
|
|
417
|
+
});
|
|
405
418
|
const decidedBy = decision.decidedBy;
|
|
406
419
|
const decidingMessage = decidedBy ? (_messages_find = messages.find((message)=>message.id === decidedBy.messageId)) != null ? _messages_find : null : null;
|
|
407
420
|
const lastSentMessageId = (_enrollment_messageIds_ = enrollment.messageIds[enrollment.messageIds.length - 1]) != null ? _enrollment_messageIds_ : null;
|
|
@@ -617,6 +630,7 @@ async function syncMailbox(deps, firestore, input) {
|
|
|
617
630
|
...freshIds
|
|
618
631
|
].slice(-ENROLLMENT_HANDLED_MAX)
|
|
619
632
|
};
|
|
633
|
+
if (inbound.length) extra['inboundMessages'] = mergeOutreachInboundMessages(current.inboundMessages, inbound);
|
|
620
634
|
const waiting = current.status === 'active' || current.status === 'paused';
|
|
621
635
|
if (postponedTo !== null && waiting && current.nextDueAtMs !== null && postponedTo > current.nextDueAtMs) {
|
|
622
636
|
extra['nextDueAtMs'] = postponedTo;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/sync-job.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { isPublicMailboxDomain } from '@aglyn/aglyn/app-utils/crm'\nimport { emailAddressOf, emailExcerpt } from '@aglyn/aglyn/app-utils/email-text'\nimport { outreachMessageIdHeader } from '../engine/compose'\nimport { readOutreachDeliveryReport } from '../engine/delivery-status'\nimport { outreachEmailDomain } from '../engine/do-not-contact-domain'\nimport { outreachStoppedEntry } from '../engine/enrollment-activity'\nimport type { OutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport { isOutreachGatewayBlock, outreachGatewayBlockDetail } from '../engine/gateway-block'\nimport { effectiveOutreachWindow, postponeOutreachForAutoReply } from '../engine/schedule'\nimport {\n decideOutreachThread,\n type OutreachMessageClassification,\n type OutreachThreadDecision,\n} from '../engine/thread-classification'\nimport { outreachHeader, outreachThreadMessageFromGmail, type OutreachThreadMessage } from '../engine/thread-message'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachEnrollment,\n type OutreachMailbox,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport { addOutreachDoNotContact, addOutreachDoNotContactDomain } from '../storage/do-not-contact-store'\nimport {\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport type { OutreachMailClient, OutreachMailSearch, OutreachMailStub } from '../transport/mail-client'\nimport { GmailTransportError, isReconnectRequired } from '../transport/gmail-errors'\nimport { creditOutreachReply } from './campaign-credit'\nimport { applyOutreachEvent, recordOutreachOptOut } from './enrollment-events'\nimport { creditOutreachGatewayDeliveries, recordOutreachGatewayBlock } from './gateway-ledger'\nimport { outreachMailboxSendingDomain } from '../storage/domain-intel-store'\nimport {\n applyOutreachMailboxHealth,\n emptyOutreachHealthDelta,\n type OutreachMailboxHealthDelta,\n} from './mailbox-health-store'\nimport { noteOutreachMailboxReconnectRequired } from './mailbox-notices'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { stampRecordEmailReply } from '@aglyn/aglyn/plugin-manager/plugin-record-email-state'\nimport { fileOutreachEmail, fileOutreachNote, fileOutreachTask, markOutreachEmailDelivery } from './timeline'\nimport { outreachUnsubscribeMailbox } from './unsubscribe-link'\n\n/*==========================================\n * THE SYNC JOB (AGL-2981): every fifteen minutes, on the console.\n *\n * For each connected or paused mailbox, what came back since the last run —\n * read with `format=full`, turned into the engine's message shape and\n * classified by `decideOutreachThread`:\n *\n * - a REPLY stops the enrollment, files the message on the contact's\n * timeline (once per `Message-ID`, under the capture address's own id for\n * it) and gives the rep the task \"Reply from <name>\";\n * - an OUT-OF-OFFICE postpones the next step a working week from the reply;\n * - an OPT-OUT — a reply asking to be left alone, or a message to the\n * `+unsubscribe` address the `mailto:` names — puts the address on the\n * organization's do-not-contact list and off the site's `sales` topic,\n * and stops every open enrollment of it;\n * - a HARD BOUNCE stops the enrollment, files the address on the platform's\n * suppression list and the do-not-contact list — and, when its diagnostic\n * reads as the domain's mail gateway refusing the sender rather than one\n * address being unknown (`engine/gateway-block`, AGL-3244), files the\n * DOMAIN on the do-not-contact list too, and says so on the enrollment —\n * and counts the block on the gateway ledger (AGL-3326), under the\n * gateway the bounce's `Remote-MTA` names.\n *\n * And once the mail is read, every email step of a watched enrollment that\n * is a day old with no bounce against it is credited to the ledger as\n * delivered (`./gateway-ledger`): the silence that says a gateway let the\n * sender through.\n *\n * Every verdict a list takes is said on the record the person is as well\n * (AGL-3245): the lead and the contact carry `emailState` through the\n * platform's stamp, and a bounce or a complaint lands on the send's own\n * timeline entry, so a member reading the record sees what the runtime saw.\n *\n * Four reads find those, each narrowed by the provider rather than read in\n * full (`OutreachMailSearch`, AGL-3489):\n * the threads of this mailbox's enrollments that have new mail; delivery\n * reports outside those threads (a receiving server's own bounce), by\n * searching for the mail systems that send them; messages to the\n * unsubscribe address; and messages from the people this mailbox is\n * writing to, in a thread of their own.\n *\n * Every write it makes is idempotent — the do-not-contact list keeps its\n * first entry, a topic opt-out keeps its first moment, a timeline entry is\n * filed once per message — and the messages it handled are marked AFTER\n * their effects, on the enrollment and on the mailbox, so a run that dies\n * part-way repeats harmless work rather than skipping some. The mailbox's\n * health takes the run's bounces and complaints in one write at the end,\n * which is where it pauses itself.\n *==========================================*/\n\nexport { OUTREACH_SYNC_JOB_ID } from '../constants/runtime-jobs'\n\n/** The furthest back a run reads: a mailbox's first run, or one that fell behind. */\nexport const OUTREACH_SYNC_LOOKBACK_MS = 7 * 24 * 60 * 60_000\n\n/** How far a run reads back past the previous one, for mail Gmail indexed late. */\nexport const OUTREACH_SYNC_OVERLAP_MS = 2 * 60 * 60_000\n\n/** How long after its last send an enrollment's mail is still watched. */\nexport const OUTREACH_SYNC_WATCH_MS = 45 * 24 * 60 * 60_000\n\n/** The most pages of one search a run reads. */\nconst SEARCH_PAGES = 5\n/** Handled message ids a mailbox keeps, and an enrollment keeps. */\nconst MAILBOX_HANDLED_MAX = 500\n\n/** How far a quiet mailbox's window may lag before its document is moved anyway. */\nconst MAILBOX_SYNC_WINDOW_REFRESH_MS = 60 * 60_000\nconst ENROLLMENT_HANDLED_MAX = 100\n/** Addresses in one `from:` search. */\nconst FROM_CHUNK = 15\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface OutreachSyncReport {\n [key: string]: number\n mailboxes: number\n threads: number\n replies: number\n optOuts: number\n bounces: number\n /** Hard bounces that read as a gateway block, each filing its domain (AGL-3244). */\n gatewayBlocks: number\n /** Email steps a day old with no bounce, credited to the gateway ledger (AGL-3326). */\n deliveries: number\n postponed: number\n paused: number\n}\n\ninterface SyncContext {\n deps: OutreachRuntimeDeps\n firestore: Firestore\n orgId: string\n mailbox: OutreachMailbox\n client: OutreachMailClient\n selfAddresses: string[]\n unsubscribeAddress: string | null\n sequences: Map<string, OutreachSequence | null>\n delta: OutreachMailboxHealthDelta\n report: OutreachSyncReport\n nowMs: number\n}\n\n/** Runs the sync job once — see the module note. */\nexport async function runOutreachSyncJob(\n deps: OutreachRuntimeDeps,\n context: { nowMs: number; deadlineMs: number },\n): Promise<OutreachSyncReport> {\n const firestore = deps.firestore()\n const report: OutreachSyncReport = {\n mailboxes: 0,\n threads: 0,\n replies: 0,\n optOuts: 0,\n bounces: 0,\n gatewayBlocks: 0,\n deliveries: 0,\n postponed: 0,\n paused: 0,\n }\n const all = await firestore.collectionGroup(OUTREACH_COLLECTIONS.mailboxes).get()\n const byOrg = new Map<string, OutreachMailbox[]>()\n for (const doc of all.docs) {\n const orgId = doc.ref.parent.parent?.id\n const mailbox = readStoredOutreachMailbox(doc.id, doc.data())\n if (!orgId || !mailbox || (mailbox.status !== 'connected' && mailbox.status !== 'paused')) continue\n byOrg.set(orgId, [...(byOrg.get(orgId) ?? []), mailbox])\n }\n for (const [orgId, mailboxes] of byOrg) {\n if (deps.now() >= context.deadlineMs) break\n const orgSnapshot = await firestore.collection('orgs').doc(orgId).get()\n const org = (orgSnapshot.exists ? orgSnapshot.data() : null) as Record<string, unknown> | null\n if (!org || (await deps.orgRefusal(orgId, org))) continue\n for (const mailbox of mailboxes) {\n if (deps.now() >= context.deadlineMs) break\n try {\n await syncMailbox(deps, firestore, { orgId, mailbox, report, nowMs: context.nowMs })\n report.mailboxes += 1\n } catch (error) {\n // One mailbox's failure is its own; the next mailbox is read. A grant\n // Google refused is the mailbox's to reconnect.\n if (isReconnectRequired(error)) {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: error instanceof GmailTransportError ? error.code : 'invalid_grant',\n nowMs: context.nowMs,\n })\n }\n console.error('[outreach] syncing a mailbox failed', error)\n }\n }\n }\n return report\n}\n\n/** Every stub a search finds, a few pages deep. */\nasync function search(client: OutreachMailClient, criteria: OutreachMailSearch): Promise<OutreachMailStub[]> {\n const found: OutreachMailStub[] = []\n let pageToken: string | null = null\n for (let page = 0; page < SEARCH_PAGES; page += 1) {\n const list = await client.searchMessages(criteria, { maxResults: 100, pageToken })\n found.push(...list.messages.filter((message) => message.id))\n pageToken = list.nextPageToken\n if (!pageToken) break\n }\n return found\n}\n\nasync function syncMailbox(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n input: { orgId: string; mailbox: OutreachMailbox; report: OutreachSyncReport; nowMs: number },\n): Promise<void> {\n const { orgId, mailbox, report, nowMs } = input\n const opened = await deps.openMailbox(mailbox.id)\n if (opened.ok === false) {\n if (opened.reason !== 'not-configured') {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n }\n return\n }\n const unsubscribe = outreachUnsubscribeMailbox(mailbox.email)\n const context: SyncContext = {\n deps,\n firestore,\n orgId,\n mailbox,\n client: opened.client,\n selfAddresses: [\n mailbox.email,\n mailbox.sendAs,\n ...(mailbox.sendAsOptions ?? []).map((option) => option.email),\n ].filter(Boolean),\n unsubscribeAddress: unsubscribe?.address ?? null,\n sequences: new Map(),\n delta: emptyOutreachHealthDelta(),\n report,\n nowMs,\n }\n\n // The enrollments this mailbox is writing to, by thread and by address.\n const watched = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('mailboxId', '==', mailbox.id)\n .where('lastSentAtMs', '>=', nowMs - OUTREACH_SYNC_WATCH_MS)\n .get()\n const byThread = new Map<string, OutreachEnrollment>()\n const byAddress = new Map<string, OutreachEnrollment>()\n const watchedEnrollments: OutreachEnrollment[] = []\n for (const doc of watched.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment) continue\n watchedEnrollments.push(enrollment)\n for (const threadId of enrollment.gmailThreadIds) byThread.set(threadId, enrollment)\n const known = byAddress.get(enrollment.email)\n if (!known || (enrollment.lastSentAtMs ?? 0) > (known.lastSentAtMs ?? 0)) byAddress.set(enrollment.email, enrollment)\n }\n\n const sinceMs = Math.max(Number(mailbox.sync?.throughMs) || 0, nowMs - OUTREACH_SYNC_LOOKBACK_MS) - OUTREACH_SYNC_OVERLAP_MS\n // Every message this run or an earlier one handled outside an enrollment's\n // thread; one message is handled once however many searches find it.\n const handled = new Set(mailbox.sync?.handledMessageIds ?? [])\n const newlyHandled: string[] = []\n const markHandled = (id: string) => {\n if (handled.has(id)) return\n handled.add(id)\n newlyHandled.push(id)\n }\n const fresh = (stub: OutreachMailStub) => !handled.has(stub.id) && !byThread.has(stub.threadId)\n\n // 1. The enrollments' own threads that have new mail.\n const inbound = await search(context.client, { afterMs: sinceMs, notFromSelf: true })\n for (const threadId of new Set(inbound.map((stub) => stub.threadId))) {\n const enrollment = byThread.get(threadId)\n if (!enrollment) continue\n const thread = await context.client.getFullThread(threadId)\n report.threads += 1\n await applyMessages(context, enrollment, thread.messages.map(outreachThreadMessageFromGmail))\n }\n\n // 2. Delivery reports outside those threads: a receiving server's bounce.\n for (const stub of await search(context.client, { afterMs: sinceMs, from: ['mailer-daemon', 'postmaster'] })) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const delivery = readOutreachDeliveryReport(message)\n if (delivery?.kind === 'hard') {\n for (const address of delivery.failedAddresses) {\n const enrollment = byAddress.get(address)\n if (enrollment) await applyMessages(context, enrollment, [message])\n }\n }\n markHandled(stub.id)\n }\n\n // 3. Messages to the unsubscribe address: whoever wrote asked to leave.\n if (unsubscribe) {\n for (const stub of await search(context.client, { afterMs: sinceMs, to: unsubscribe.address })) {\n if (handled.has(stub.id)) continue\n const metadata = await context.client.getMessage(stub.id, { metadataHeaders: ['From'] })\n const from = emailAddressOf(metadata.headers.find((header) => header.name.toLowerCase() === 'from')?.value ?? '')\n const address = normalizeContactEmail(from)\n if (address && !context.selfAddresses.includes(address)) {\n const enrollment = byAddress.get(address) ?? null\n await recordOutreachOptOut(deps, {\n orgId,\n email: address,\n source: 'unsubscribe',\n enrollment,\n hostIds: enrollment ? [enrollment.hostId] : [],\n detail: 'Wrote to the unsubscribe address.',\n nowMs,\n })\n report.optOuts += 1\n }\n markHandled(stub.id)\n }\n }\n\n // 4. The people this mailbox writes to, answering in a thread of their own.\n const addresses = [...byAddress.keys()]\n for (let start = 0; start < addresses.length; start += FROM_CHUNK) {\n const chunk = addresses.slice(start, start + FROM_CHUNK)\n for (const stub of await search(context.client, { afterMs: sinceMs, from: chunk })) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const enrollment = byAddress.get(normalizeContactEmail(emailAddressOf(message.from)) ?? '')\n if (enrollment) await applyMessages(context, enrollment, [message])\n markHandled(stub.id)\n }\n }\n\n // 5. What went through: a day of silence after a send is a delivery\n // (AGL-3326), judged on the enrollments as this run left them.\n try {\n report.deliveries += await creditOutreachGatewayDeliveries(firestore, {\n orgId,\n enrollments: watchedEnrollments,\n sendingDomain: outreachMailboxSendingDomain(mailbox),\n resolveMx: deps.resolveMx,\n nowMs,\n })\n } catch (error) {\n console.error('[outreach] the gateway ledger could not credit deliveries', error)\n }\n\n const paused = await applyOutreachMailboxHealth(deps, { orgId, mailboxId: mailbox.id, delta: context.delta })\n if (paused) report.paused += 1\n // The window is moved only when something was handled or an hour has\n // passed: a quiet mailbox re-reading an hour's window is a few list calls,\n // where rewriting every mailbox every run was a write per mailbox every\n // fifteen minutes with nothing in it (AGL-3932). The handled ids keep a\n // re-read from acting twice.\n if (newlyHandled.length || nowMs - (mailbox.sync?.throughMs ?? 0) >= MAILBOX_SYNC_WINDOW_REFRESH_MS) {\n await mailboxRef(firestore, orgId, mailbox.id).update({\n sync: {\n throughMs: nowMs,\n handledMessageIds: [...(mailbox.sync?.handledMessageIds ?? []), ...newlyHandled].slice(-MAILBOX_HANDLED_MAX),\n },\n })\n }\n}\n\n/** The sequence an enrollment runs through, read once per run. */\nasync function sequenceOf(context: SyncContext, sequenceId: string): Promise<OutreachSequence | null> {\n if (!context.sequences.has(sequenceId)) {\n const snapshot = await outreachOrgCollection(context.firestore, context.orgId, 'sequences').doc(sequenceId).get()\n context.sequences.set(sequenceId, readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined))\n }\n return context.sequences.get(sequenceId) ?? null\n}\n\n/** A message's `Message-ID` in header form, or `null` when it carries none. */\nconst messageIdOf = (message: OutreachThreadMessage) => outreachMessageIdHeader(outreachHeader(message, 'Message-ID'))\n\n/** A bounce's diagnostic with the recipient's address taken out of it. */\nexport function scrubbedOutreachDiagnostic(diagnostic: string | null, email: string): string | null {\n if (!diagnostic) return null\n const pattern = new RegExp(email.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'gi')\n return diagnostic.replace(pattern, 'the address')\n}\n\n/** Files a message the person wrote on their timeline. */\nasync function fileInbound(context: SyncContext, enrollment: OutreachEnrollment, message: OutreachThreadMessage) {\n const messageId = messageIdOf(message)\n if (!messageId) return\n await fileOutreachEmail(context.deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n direction: 'inbound',\n subject: message.subject,\n from: emailAddressOf(message.from) ?? message.from,\n to: context.mailbox.sendAs || context.mailbox.email,\n messageId,\n inReplyTo: outreachMessageIdHeader(outreachHeader(message, 'In-Reply-To')),\n body: emailExcerpt(message.textBody ?? message.snippet ?? '', message.htmlBody ?? ''),\n atMs: message.internalDateMs,\n byUid: '',\n })\n}\n\n/**\n * What the new messages of one enrollment mean, done: the effects first,\n * each idempotent, then the event and the handled ids in one transaction.\n */\nasync function applyMessages(\n context: SyncContext,\n enrollment: OutreachEnrollment,\n messages: readonly OutreachThreadMessage[],\n): Promise<void> {\n const { deps, firestore, report, nowMs } = context\n const decision: OutreachThreadDecision = decideOutreachThread({\n messages,\n recipient: enrollment.email,\n handledMessageIds: enrollment.syncedMessageIds ?? [],\n selfAddresses: context.selfAddresses,\n unsubscribeAddress: context.unsubscribeAddress,\n threadSubject: enrollment.threadSubject,\n })\n const freshIds = decision.classifications.map((classification) => classification.messageId).filter(Boolean)\n if (!freshIds.length) return\n const decidedBy: OutreachMessageClassification | null = decision.decidedBy\n const decidingMessage = decidedBy ? messages.find((message) => message.id === decidedBy.messageId) ?? null : null\n const lastSentMessageId = enrollment.messageIds[enrollment.messageIds.length - 1] ?? null\n\n for (const classification of decision.classifications) {\n const aboutRecipient =\n !classification.bounce?.recipients.length || classification.bounce.recipients.includes(enrollment.email)\n if (classification.kind === 'hard_bounce' && aboutRecipient) {\n context.delta.bounces.push({ atMs: classification.atMs || nowMs, messageId: lastSentMessageId })\n }\n if (classification.complaint) {\n context.delta.complaintAtMs = Math.max(context.delta.complaintAtMs ?? 0, classification.atMs || nowMs)\n }\n }\n\n let event: OutreachEnrollmentEvent | null = null\n let postponedTo: number | null = null\n switch (decision.outcome) {\n case 'opted_out': {\n if (decidingMessage) await fileInbound(context, enrollment, decidingMessage)\n const complaint = decidedBy?.complaint === true\n await recordOutreachOptOut(deps, {\n orgId: context.orgId,\n email: enrollment.email,\n source: decidedBy?.evidence?.startsWith('To:') ? 'unsubscribe' : 'opt_out_reply',\n enrollment,\n hostIds: [enrollment.hostId],\n detail: decidedBy?.evidence ?? null,\n nowMs,\n complaint,\n })\n // A reply that called the email spam marks the send it answered\n // (AGL-3245), as the campaign webhook would mark a reported one.\n if (complaint) {\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'complained',\n atMs: decidedBy?.atMs || nowMs,\n detail: decidedBy?.evidence ?? null,\n })\n }\n report.optOuts += 1\n break\n }\n case 'replied': {\n if (decidingMessage) {\n await fileInbound(context, enrollment, decidingMessage)\n try {\n await fileOutreachTask(deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: `reply:${messageIdOf(decidingMessage) ?? decidingMessage.id}`,\n title: `Reply from ${enrollment.contactName || enrollment.email}`,\n notes: emailExcerpt(decidingMessage.textBody ?? decidingMessage.snippet ?? '', decidingMessage.htmlBody ?? '').slice(0, 500),\n kind: 'email',\n dueAtMs: nowMs,\n assigneeUid: context.mailbox.connectedByUid,\n })\n } catch (error) {\n console.error('[outreach] the reply task could not be filed', error)\n }\n }\n // A lead somebody wrote back to is being worked (AGL-3234): the record\n // system hears of the reply, and moves a lead nobody had worked to\n // Working (AGL-3080). Never throws.\n if (enrollment.target === 'lead' && enrollment.leadId && enrollment.email) {\n await stampRecordEmailReply({\n orgId: context.orgId,\n hostId: enrollment.hostId,\n emails: [enrollment.email],\n })\n }\n context.delta.replies += 1\n event = { type: 'reply', atMs: decidedBy?.atMs || nowMs, detail: decidedBy?.evidence ?? null }\n report.replies += 1\n // Once per enrollment (AGL-3254): a reply moves the status to\n // `replied`, and nothing is synced for it again.\n await creditOutreachReply(deps, { enrollment, atMs: event.atMs })\n break\n }\n case 'bounced': {\n const diagnostic = scrubbedOutreachDiagnostic(decidedBy?.bounce?.diagnostic ?? decidedBy?.bounce?.status ?? null, enrollment.email)\n await deps.suppressBouncedEmail({ email: enrollment.email, hostId: enrollment.hostId })\n await addOutreachDoNotContact(firestore, {\n orgId: context.orgId,\n email: enrollment.email,\n reason: 'hard_bounce',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n let detail = diagnostic\n // A gateway block is the domain's verdict on the sender (AGL-3244):\n // the domain is filed beside the address, and the enrollment says so.\n // Never a public mailbox provider's domain — Gmail refusing one\n // message on policy is not a reason to stop writing to Gmail.\n const domain = outreachEmailDomain(enrollment.email)\n const gateway = isOutreachGatewayBlock(decidedBy?.bounce ?? null)\n if (domain && !isPublicMailboxDomain(domain) && gateway) {\n await addOutreachDoNotContactDomain(firestore, {\n orgId: context.orgId,\n domain,\n reason: 'gateway_block',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n detail = outreachGatewayBlockDetail(domain, diagnostic)\n report.gatewayBlocks += 1\n }\n // The ledger learns the refusal (AGL-3326), whichever domain it was\n // at: a public provider refusing on policy is still that provider's\n // verdict on the sender, even though its domain is never listed.\n if (gateway) {\n await recordOutreachGatewayBlock(firestore, {\n orgId: context.orgId,\n email: enrollment.email,\n remoteMta: decidedBy?.bounce?.remoteMta ?? null,\n sendingDomain: outreachMailboxSendingDomain(context.mailbox),\n diagnostic,\n resolveMx: deps.resolveMx,\n nowMs,\n })\n }\n const bouncedAtMs = decidedBy?.atMs || nowMs\n // The record the person is says so too (AGL-3245): the verdict on the\n // lead and the contact, and the bounce on the send's own timeline\n // entry, so the record reads Sent, then Bounced.\n await deps.stampRecordEmailState({\n orgId: context.orgId,\n email: enrollment.email,\n state: {\n status: gateway ? 'blocked' : 'bounced',\n atMs: bouncedAtMs,\n source: 'sequence',\n detail: diagnostic,\n enrollmentId: enrollment.id,\n },\n })\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'bounced',\n atMs: bouncedAtMs,\n detail: diagnostic,\n })\n event = { type: 'bounce', atMs: bouncedAtMs, detail }\n report.bounces += 1\n break\n }\n case 'postpone': {\n const sequence = await sequenceOf(context, enrollment.sequenceId)\n postponedTo = postponeOutreachForAutoReply({\n nextDueAtMs: enrollment.nextDueAtMs,\n receivedAtMs: decidedBy?.atMs || nowMs,\n timeZone: context.mailbox.timezone,\n window: effectiveOutreachWindow(sequence?.settings.window, context.mailbox.window),\n random: deps.random,\n })\n break\n }\n default:\n break\n }\n\n const outcome = await applyOutreachEvent(firestore, {\n orgId: context.orgId,\n enrollmentId: enrollment.id,\n event,\n extra: (current) => {\n const extra: Record<string, unknown> = {\n syncedMessageIds: [...(current.syncedMessageIds ?? []), ...freshIds].slice(-ENROLLMENT_HANDLED_MAX),\n }\n const waiting = current.status === 'active' || current.status === 'paused'\n if (postponedTo !== null && waiting && current.nextDueAtMs !== null && postponedTo > current.nextDueAtMs) {\n extra['nextDueAtMs'] = postponedTo\n }\n return extra\n },\n nowMs,\n })\n if (postponedTo !== null && outcome.enrollment?.nextDueAtMs === postponedTo) report.postponed += 1\n // The person's record says the sequence ended (AGL-3274), once, under the\n // reply that ended it — after the transaction moved the status, so a run\n // that reads the same reply again finds nothing to file. An opt-out's\n // line is `recordOutreachOptOut`'s, beside the lists it writes.\n if (event?.type === 'reply' && outcome.changed) {\n const sequence = await sequenceOf(context, enrollment.sequenceId)\n const entry = outreachStoppedEntry({\n enrollmentId: enrollment.id,\n sequenceName: sequence?.name ?? '',\n reason: 'replied',\n })\n await fileOutreachNote(deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: event.atMs,\n })\n }\n // The run's own view of the enrollment moves with it, so a second message\n // of it in this run is judged against what this one did.\n if (outcome.enrollment) Object.assign(enrollment, outcome.enrollment)\n}\n"],"names":["normalizeContactEmail","isPublicMailboxDomain","emailAddressOf","emailExcerpt","outreachMessageIdHeader","readOutreachDeliveryReport","outreachEmailDomain","outreachStoppedEntry","isOutreachGatewayBlock","outreachGatewayBlockDetail","effectiveOutreachWindow","postponeOutreachForAutoReply","decideOutreachThread","outreachHeader","outreachThreadMessageFromGmail","mailboxRef","OUTREACH_COLLECTIONS","addOutreachDoNotContact","addOutreachDoNotContactDomain","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","creditOutreachReply","applyOutreachEvent","recordOutreachOptOut","creditOutreachGatewayDeliveries","recordOutreachGatewayBlock","outreachMailboxSendingDomain","applyOutreachMailboxHealth","emptyOutreachHealthDelta","noteOutreachMailboxReconnectRequired","stampRecordEmailReply","fileOutreachEmail","fileOutreachNote","fileOutreachTask","markOutreachEmailDelivery","outreachUnsubscribeMailbox","OUTREACH_SYNC_JOB_ID","OUTREACH_SYNC_LOOKBACK_MS","OUTREACH_SYNC_OVERLAP_MS","OUTREACH_SYNC_WATCH_MS","SEARCH_PAGES","MAILBOX_HANDLED_MAX","MAILBOX_SYNC_WINDOW_REFRESH_MS","ENROLLMENT_HANDLED_MAX","FROM_CHUNK","runOutreachSyncJob","deps","context","firestore","report","mailboxes","threads","replies","optOuts","bounces","gatewayBlocks","deliveries","postponed","paused","all","collectionGroup","get","byOrg","Map","doc","docs","orgId","ref","parent","id","mailbox","data","status","set","now","deadlineMs","orgSnapshot","collection","org","exists","orgRefusal","syncMailbox","nowMs","error","mailboxId","errorCode","code","console","search","client","criteria","found","pageToken","page","list","searchMessages","maxResults","push","messages","filter","message","nextPageToken","input","opened","openMailbox","ok","reason","unsubscribe","email","selfAddresses","sendAs","sendAsOptions","map","option","Boolean","unsubscribeAddress","address","sequences","delta","watched","where","byThread","byAddress","watchedEnrollments","enrollment","known","threadId","gmailThreadIds","lastSentAtMs","sinceMs","Math","max","Number","sync","throughMs","handled","Set","handledMessageIds","newlyHandled","markHandled","has","add","fresh","stub","inbound","afterMs","notFromSelf","thread","getFullThread","applyMessages","from","getFullMessage","delivery","kind","failedAddresses","to","metadata","getMessage","metadataHeaders","headers","find","header","name","toLowerCase","value","includes","source","hostIds","hostId","detail","addresses","keys","start","length","chunk","slice","enrollments","sendingDomain","resolveMx","update","sequenceOf","sequenceId","snapshot","undefined","messageIdOf","scrubbedOutreachDiagnostic","diagnostic","pattern","RegExp","replace","fileInbound","messageId","link","direction","subject","inReplyTo","body","textBody","snippet","htmlBody","atMs","internalDateMs","byUid","outcome","decision","recipient","syncedMessageIds","threadSubject","freshIds","classifications","classification","decidedBy","decidingMessage","lastSentMessageId","messageIds","aboutRecipient","bounce","recipients","complaint","complaintAtMs","event","postponedTo","evidence","startsWith","state","dedupeKey","title","contactName","notes","dueAtMs","assigneeUid","connectedByUid","target","leadId","emails","type","suppressBouncedEmail","enrollmentId","domain","gateway","remoteMta","bouncedAtMs","stampRecordEmailState","sequence","nextDueAtMs","receivedAtMs","timeZone","timezone","window","settings","random","extra","current","waiting","changed","entry","sequenceName","Object","assign"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/sync-job.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { isPublicMailboxDomain } from '@aglyn/aglyn/app-utils/crm'\nimport { emailAddressOf, emailExcerpt } from '@aglyn/aglyn/app-utils/email-text'\nimport { outreachMessageIdHeader } from '../engine/compose'\nimport { readOutreachDeliveryReport } from '../engine/delivery-status'\nimport { outreachEmailDomain } from '../engine/do-not-contact-domain'\nimport { outreachStoppedEntry } from '../engine/enrollment-activity'\nimport type { OutreachEnrollmentEvent } from '../engine/enrollment-state'\nimport { isOutreachGatewayBlock, outreachGatewayBlockDetail } from '../engine/gateway-block'\nimport { mergeOutreachInboundMessages, outreachInboundMessagesOf } from '../engine/manual-reply'\nimport { effectiveOutreachWindow, postponeOutreachForAutoReply } from '../engine/schedule'\nimport {\n decideOutreachThread,\n type OutreachMessageClassification,\n type OutreachThreadDecision,\n} from '../engine/thread-classification'\nimport { outreachHeader, outreachThreadMessageFromGmail, type OutreachThreadMessage } from '../engine/thread-message'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport {\n OUTREACH_COLLECTIONS,\n type OutreachEnrollment,\n type OutreachMailbox,\n type OutreachSequence,\n} from '../model/outreach.types'\nimport { addOutreachDoNotContact, addOutreachDoNotContactDomain } from '../storage/do-not-contact-store'\nimport {\n outreachEnrollmentLink,\n outreachOrgCollection,\n readStoredOutreachEnrollment,\n readStoredOutreachMailbox,\n readStoredOutreachSequence,\n} from '../storage/outreach-records'\nimport type { OutreachMailClient, OutreachMailSearch, OutreachMailStub } from '../transport/mail-client'\nimport { GmailTransportError, isReconnectRequired } from '../transport/gmail-errors'\nimport { creditOutreachReply } from './campaign-credit'\nimport { applyOutreachEvent, recordOutreachOptOut } from './enrollment-events'\nimport { creditOutreachGatewayDeliveries, recordOutreachGatewayBlock } from './gateway-ledger'\nimport { outreachMailboxSendingDomain } from '../storage/domain-intel-store'\nimport {\n applyOutreachMailboxHealth,\n emptyOutreachHealthDelta,\n type OutreachMailboxHealthDelta,\n} from './mailbox-health-store'\nimport { noteOutreachMailboxReconnectRequired } from './mailbox-notices'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\nimport { stampRecordEmailReply } from '@aglyn/aglyn/plugin-manager/plugin-record-email-state'\nimport { fileOutreachEmail, fileOutreachNote, fileOutreachTask, markOutreachEmailDelivery } from './timeline'\nimport { outreachUnsubscribeMailbox } from './unsubscribe-link'\n\n/*==========================================\n * THE SYNC JOB (AGL-2981): every fifteen minutes, on the console.\n *\n * For each connected or paused mailbox, what came back since the last run —\n * read with `format=full`, turned into the engine's message shape and\n * classified by `decideOutreachThread`:\n *\n * - a REPLY stops the enrollment, files the message on the contact's\n * timeline (once per `Message-ID`, under the capture address's own id for\n * it) and gives the rep the task \"Reply from <name>\";\n * - every message the person wrote — a reply, an automatic answer, an\n * opt-out — is kept on the enrollment, its sender, time, threading ids\n * and trimmed text, for the console to show and answer (AGL-4020);\n * - an OUT-OF-OFFICE postpones the next step a working week from the reply;\n * - an OPT-OUT — a reply asking to be left alone, or a message to the\n * `+unsubscribe` address the `mailto:` names — puts the address on the\n * organization's do-not-contact list and off the site's `sales` topic,\n * and stops every open enrollment of it;\n * - a HARD BOUNCE stops the enrollment, files the address on the platform's\n * suppression list and the do-not-contact list — and, when its diagnostic\n * reads as the domain's mail gateway refusing the sender rather than one\n * address being unknown (`engine/gateway-block`, AGL-3244), files the\n * DOMAIN on the do-not-contact list too, and says so on the enrollment —\n * and counts the block on the gateway ledger (AGL-3326), under the\n * gateway the bounce's `Remote-MTA` names.\n *\n * And once the mail is read, every email step of a watched enrollment that\n * is a day old with no bounce against it is credited to the ledger as\n * delivered (`./gateway-ledger`): the silence that says a gateway let the\n * sender through.\n *\n * Every verdict a list takes is said on the record the person is as well\n * (AGL-3245): the lead and the contact carry `emailState` through the\n * platform's stamp, and a bounce or a complaint lands on the send's own\n * timeline entry, so a member reading the record sees what the runtime saw.\n *\n * Four reads find those, each narrowed by the provider rather than read in\n * full (`OutreachMailSearch`, AGL-3489):\n * the threads of this mailbox's enrollments that have new mail; delivery\n * reports outside those threads (a receiving server's own bounce), by\n * searching for the mail systems that send them; messages to the\n * unsubscribe address; and messages from the people this mailbox is\n * writing to, in a thread of their own.\n *\n * Every write it makes is idempotent — the do-not-contact list keeps its\n * first entry, a topic opt-out keeps its first moment, a timeline entry is\n * filed once per message — and the messages it handled are marked AFTER\n * their effects, on the enrollment and on the mailbox, so a run that dies\n * part-way repeats harmless work rather than skipping some. The mailbox's\n * health takes the run's bounces and complaints in one write at the end,\n * which is where it pauses itself.\n *==========================================*/\n\nexport { OUTREACH_SYNC_JOB_ID } from '../constants/runtime-jobs'\n\n/** The furthest back a run reads: a mailbox's first run, or one that fell behind. */\nexport const OUTREACH_SYNC_LOOKBACK_MS = 7 * 24 * 60 * 60_000\n\n/** How far a run reads back past the previous one, for mail Gmail indexed late. */\nexport const OUTREACH_SYNC_OVERLAP_MS = 2 * 60 * 60_000\n\n/** How long after its last send an enrollment's mail is still watched. */\nexport const OUTREACH_SYNC_WATCH_MS = 45 * 24 * 60 * 60_000\n\n/** The most pages of one search a run reads. */\nconst SEARCH_PAGES = 5\n/** Handled message ids a mailbox keeps, and an enrollment keeps. */\nconst MAILBOX_HANDLED_MAX = 500\n\n/** How far a quiet mailbox's window may lag before its document is moved anyway. */\nconst MAILBOX_SYNC_WINDOW_REFRESH_MS = 60 * 60_000\nconst ENROLLMENT_HANDLED_MAX = 100\n/** Addresses in one `from:` search. */\nconst FROM_CHUNK = 15\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface OutreachSyncReport {\n [key: string]: number\n mailboxes: number\n threads: number\n replies: number\n optOuts: number\n bounces: number\n /** Hard bounces that read as a gateway block, each filing its domain (AGL-3244). */\n gatewayBlocks: number\n /** Email steps a day old with no bounce, credited to the gateway ledger (AGL-3326). */\n deliveries: number\n postponed: number\n paused: number\n}\n\ninterface SyncContext {\n deps: OutreachRuntimeDeps\n firestore: Firestore\n orgId: string\n mailbox: OutreachMailbox\n client: OutreachMailClient\n selfAddresses: string[]\n unsubscribeAddress: string | null\n sequences: Map<string, OutreachSequence | null>\n delta: OutreachMailboxHealthDelta\n report: OutreachSyncReport\n nowMs: number\n}\n\n/** Runs the sync job once — see the module note. */\nexport async function runOutreachSyncJob(\n deps: OutreachRuntimeDeps,\n context: { nowMs: number; deadlineMs: number },\n): Promise<OutreachSyncReport> {\n const firestore = deps.firestore()\n const report: OutreachSyncReport = {\n mailboxes: 0,\n threads: 0,\n replies: 0,\n optOuts: 0,\n bounces: 0,\n gatewayBlocks: 0,\n deliveries: 0,\n postponed: 0,\n paused: 0,\n }\n const all = await firestore.collectionGroup(OUTREACH_COLLECTIONS.mailboxes).get()\n const byOrg = new Map<string, OutreachMailbox[]>()\n for (const doc of all.docs) {\n const orgId = doc.ref.parent.parent?.id\n const mailbox = readStoredOutreachMailbox(doc.id, doc.data())\n if (!orgId || !mailbox || (mailbox.status !== 'connected' && mailbox.status !== 'paused')) continue\n byOrg.set(orgId, [...(byOrg.get(orgId) ?? []), mailbox])\n }\n for (const [orgId, mailboxes] of byOrg) {\n if (deps.now() >= context.deadlineMs) break\n const orgSnapshot = await firestore.collection('orgs').doc(orgId).get()\n const org = (orgSnapshot.exists ? orgSnapshot.data() : null) as Record<string, unknown> | null\n if (!org || (await deps.orgRefusal(orgId, org))) continue\n for (const mailbox of mailboxes) {\n if (deps.now() >= context.deadlineMs) break\n try {\n await syncMailbox(deps, firestore, { orgId, mailbox, report, nowMs: context.nowMs })\n report.mailboxes += 1\n } catch (error) {\n // One mailbox's failure is its own; the next mailbox is read. A grant\n // Google refused is the mailbox's to reconnect.\n if (isReconnectRequired(error)) {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: error instanceof GmailTransportError ? error.code : 'invalid_grant',\n nowMs: context.nowMs,\n })\n }\n console.error('[outreach] syncing a mailbox failed', error)\n }\n }\n }\n return report\n}\n\n/** Every stub a search finds, a few pages deep. */\nasync function search(client: OutreachMailClient, criteria: OutreachMailSearch): Promise<OutreachMailStub[]> {\n const found: OutreachMailStub[] = []\n let pageToken: string | null = null\n for (let page = 0; page < SEARCH_PAGES; page += 1) {\n const list = await client.searchMessages(criteria, { maxResults: 100, pageToken })\n found.push(...list.messages.filter((message) => message.id))\n pageToken = list.nextPageToken\n if (!pageToken) break\n }\n return found\n}\n\nasync function syncMailbox(\n deps: OutreachRuntimeDeps,\n firestore: Firestore,\n input: { orgId: string; mailbox: OutreachMailbox; report: OutreachSyncReport; nowMs: number },\n): Promise<void> {\n const { orgId, mailbox, report, nowMs } = input\n const opened = await deps.openMailbox(mailbox.id)\n if (opened.ok === false) {\n if (opened.reason !== 'not-configured') {\n await noteOutreachMailboxReconnectRequired(deps, {\n orgId,\n mailboxId: mailbox.id,\n errorCode: opened.reason,\n nowMs,\n })\n }\n return\n }\n const unsubscribe = outreachUnsubscribeMailbox(mailbox.email)\n const context: SyncContext = {\n deps,\n firestore,\n orgId,\n mailbox,\n client: opened.client,\n selfAddresses: [\n mailbox.email,\n mailbox.sendAs,\n ...(mailbox.sendAsOptions ?? []).map((option) => option.email),\n ].filter(Boolean),\n unsubscribeAddress: unsubscribe?.address ?? null,\n sequences: new Map(),\n delta: emptyOutreachHealthDelta(),\n report,\n nowMs,\n }\n\n // The enrollments this mailbox is writing to, by thread and by address.\n const watched = await outreachOrgCollection(firestore, orgId, 'enrollments')\n .where('mailboxId', '==', mailbox.id)\n .where('lastSentAtMs', '>=', nowMs - OUTREACH_SYNC_WATCH_MS)\n .get()\n const byThread = new Map<string, OutreachEnrollment>()\n const byAddress = new Map<string, OutreachEnrollment>()\n const watchedEnrollments: OutreachEnrollment[] = []\n for (const doc of watched.docs) {\n const enrollment = readStoredOutreachEnrollment(doc.id, doc.data())\n if (!enrollment) continue\n watchedEnrollments.push(enrollment)\n for (const threadId of enrollment.gmailThreadIds) byThread.set(threadId, enrollment)\n const known = byAddress.get(enrollment.email)\n if (!known || (enrollment.lastSentAtMs ?? 0) > (known.lastSentAtMs ?? 0)) byAddress.set(enrollment.email, enrollment)\n }\n\n const sinceMs = Math.max(Number(mailbox.sync?.throughMs) || 0, nowMs - OUTREACH_SYNC_LOOKBACK_MS) - OUTREACH_SYNC_OVERLAP_MS\n // Every message this run or an earlier one handled outside an enrollment's\n // thread; one message is handled once however many searches find it.\n const handled = new Set(mailbox.sync?.handledMessageIds ?? [])\n const newlyHandled: string[] = []\n const markHandled = (id: string) => {\n if (handled.has(id)) return\n handled.add(id)\n newlyHandled.push(id)\n }\n const fresh = (stub: OutreachMailStub) => !handled.has(stub.id) && !byThread.has(stub.threadId)\n\n // 1. The enrollments' own threads that have new mail.\n const inbound = await search(context.client, { afterMs: sinceMs, notFromSelf: true })\n for (const threadId of new Set(inbound.map((stub) => stub.threadId))) {\n const enrollment = byThread.get(threadId)\n if (!enrollment) continue\n const thread = await context.client.getFullThread(threadId)\n report.threads += 1\n await applyMessages(context, enrollment, thread.messages.map(outreachThreadMessageFromGmail))\n }\n\n // 2. Delivery reports outside those threads: a receiving server's bounce.\n for (const stub of await search(context.client, { afterMs: sinceMs, from: ['mailer-daemon', 'postmaster'] })) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const delivery = readOutreachDeliveryReport(message)\n if (delivery?.kind === 'hard') {\n for (const address of delivery.failedAddresses) {\n const enrollment = byAddress.get(address)\n if (enrollment) await applyMessages(context, enrollment, [message])\n }\n }\n markHandled(stub.id)\n }\n\n // 3. Messages to the unsubscribe address: whoever wrote asked to leave.\n if (unsubscribe) {\n for (const stub of await search(context.client, { afterMs: sinceMs, to: unsubscribe.address })) {\n if (handled.has(stub.id)) continue\n const metadata = await context.client.getMessage(stub.id, { metadataHeaders: ['From'] })\n const from = emailAddressOf(metadata.headers.find((header) => header.name.toLowerCase() === 'from')?.value ?? '')\n const address = normalizeContactEmail(from)\n if (address && !context.selfAddresses.includes(address)) {\n const enrollment = byAddress.get(address) ?? null\n await recordOutreachOptOut(deps, {\n orgId,\n email: address,\n source: 'unsubscribe',\n enrollment,\n hostIds: enrollment ? [enrollment.hostId] : [],\n detail: 'Wrote to the unsubscribe address.',\n nowMs,\n })\n report.optOuts += 1\n }\n markHandled(stub.id)\n }\n }\n\n // 4. The people this mailbox writes to, answering in a thread of their own.\n const addresses = [...byAddress.keys()]\n for (let start = 0; start < addresses.length; start += FROM_CHUNK) {\n const chunk = addresses.slice(start, start + FROM_CHUNK)\n for (const stub of await search(context.client, { afterMs: sinceMs, from: chunk })) {\n if (!fresh(stub)) continue\n const message = outreachThreadMessageFromGmail(await context.client.getFullMessage(stub.id))\n const enrollment = byAddress.get(normalizeContactEmail(emailAddressOf(message.from)) ?? '')\n if (enrollment) await applyMessages(context, enrollment, [message])\n markHandled(stub.id)\n }\n }\n\n // 5. What went through: a day of silence after a send is a delivery\n // (AGL-3326), judged on the enrollments as this run left them.\n try {\n report.deliveries += await creditOutreachGatewayDeliveries(firestore, {\n orgId,\n enrollments: watchedEnrollments,\n sendingDomain: outreachMailboxSendingDomain(mailbox),\n resolveMx: deps.resolveMx,\n nowMs,\n })\n } catch (error) {\n console.error('[outreach] the gateway ledger could not credit deliveries', error)\n }\n\n const paused = await applyOutreachMailboxHealth(deps, { orgId, mailboxId: mailbox.id, delta: context.delta })\n if (paused) report.paused += 1\n // The window is moved only when something was handled or an hour has\n // passed: a quiet mailbox re-reading an hour's window is a few list calls,\n // where rewriting every mailbox every run was a write per mailbox every\n // fifteen minutes with nothing in it (AGL-3932). The handled ids keep a\n // re-read from acting twice.\n if (newlyHandled.length || nowMs - (mailbox.sync?.throughMs ?? 0) >= MAILBOX_SYNC_WINDOW_REFRESH_MS) {\n await mailboxRef(firestore, orgId, mailbox.id).update({\n sync: {\n throughMs: nowMs,\n handledMessageIds: [...(mailbox.sync?.handledMessageIds ?? []), ...newlyHandled].slice(-MAILBOX_HANDLED_MAX),\n },\n })\n }\n}\n\n/** The sequence an enrollment runs through, read once per run. */\nasync function sequenceOf(context: SyncContext, sequenceId: string): Promise<OutreachSequence | null> {\n if (!context.sequences.has(sequenceId)) {\n const snapshot = await outreachOrgCollection(context.firestore, context.orgId, 'sequences').doc(sequenceId).get()\n context.sequences.set(sequenceId, readStoredOutreachSequence(sequenceId, snapshot.exists ? snapshot.data() : undefined))\n }\n return context.sequences.get(sequenceId) ?? null\n}\n\n/** A message's `Message-ID` in header form, or `null` when it carries none. */\nconst messageIdOf = (message: OutreachThreadMessage) => outreachMessageIdHeader(outreachHeader(message, 'Message-ID'))\n\n/** A bounce's diagnostic with the recipient's address taken out of it. */\nexport function scrubbedOutreachDiagnostic(diagnostic: string | null, email: string): string | null {\n if (!diagnostic) return null\n const pattern = new RegExp(email.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'gi')\n return diagnostic.replace(pattern, 'the address')\n}\n\n/** Files a message the person wrote on their timeline. */\nasync function fileInbound(context: SyncContext, enrollment: OutreachEnrollment, message: OutreachThreadMessage) {\n const messageId = messageIdOf(message)\n if (!messageId) return\n await fileOutreachEmail(context.deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n direction: 'inbound',\n subject: message.subject,\n from: emailAddressOf(message.from) ?? message.from,\n to: context.mailbox.sendAs || context.mailbox.email,\n messageId,\n inReplyTo: outreachMessageIdHeader(outreachHeader(message, 'In-Reply-To')),\n body: emailExcerpt(message.textBody ?? message.snippet ?? '', message.htmlBody ?? ''),\n atMs: message.internalDateMs,\n byUid: '',\n })\n}\n\n/**\n * What the new messages of one enrollment mean, done: the effects first,\n * each idempotent, then the event and the handled ids in one transaction.\n */\nasync function applyMessages(\n context: SyncContext,\n enrollment: OutreachEnrollment,\n messages: readonly OutreachThreadMessage[],\n): Promise<void> {\n const { deps, firestore, report, nowMs } = context\n const decision: OutreachThreadDecision = decideOutreachThread({\n messages,\n recipient: enrollment.email,\n handledMessageIds: enrollment.syncedMessageIds ?? [],\n selfAddresses: context.selfAddresses,\n unsubscribeAddress: context.unsubscribeAddress,\n threadSubject: enrollment.threadSubject,\n })\n const freshIds = decision.classifications.map((classification) => classification.messageId).filter(Boolean)\n if (!freshIds.length) return\n // What they wrote, kept on the enrollment so the console can show it and\n // answer it in the thread (AGL-4020) — every message of theirs among\n // these, so a thread read whole also keeps a reply synced before they\n // were kept.\n const inbound = outreachInboundMessagesOf(messages, {\n selfAddresses: context.selfAddresses,\n unsubscribeAddress: context.unsubscribeAddress,\n threadSubject: enrollment.threadSubject,\n })\n const decidedBy: OutreachMessageClassification | null = decision.decidedBy\n const decidingMessage = decidedBy ? messages.find((message) => message.id === decidedBy.messageId) ?? null : null\n const lastSentMessageId = enrollment.messageIds[enrollment.messageIds.length - 1] ?? null\n\n for (const classification of decision.classifications) {\n const aboutRecipient =\n !classification.bounce?.recipients.length || classification.bounce.recipients.includes(enrollment.email)\n if (classification.kind === 'hard_bounce' && aboutRecipient) {\n context.delta.bounces.push({ atMs: classification.atMs || nowMs, messageId: lastSentMessageId })\n }\n if (classification.complaint) {\n context.delta.complaintAtMs = Math.max(context.delta.complaintAtMs ?? 0, classification.atMs || nowMs)\n }\n }\n\n let event: OutreachEnrollmentEvent | null = null\n let postponedTo: number | null = null\n switch (decision.outcome) {\n case 'opted_out': {\n if (decidingMessage) await fileInbound(context, enrollment, decidingMessage)\n const complaint = decidedBy?.complaint === true\n await recordOutreachOptOut(deps, {\n orgId: context.orgId,\n email: enrollment.email,\n source: decidedBy?.evidence?.startsWith('To:') ? 'unsubscribe' : 'opt_out_reply',\n enrollment,\n hostIds: [enrollment.hostId],\n detail: decidedBy?.evidence ?? null,\n nowMs,\n complaint,\n })\n // A reply that called the email spam marks the send it answered\n // (AGL-3245), as the campaign webhook would mark a reported one.\n if (complaint) {\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'complained',\n atMs: decidedBy?.atMs || nowMs,\n detail: decidedBy?.evidence ?? null,\n })\n }\n report.optOuts += 1\n break\n }\n case 'replied': {\n if (decidingMessage) {\n await fileInbound(context, enrollment, decidingMessage)\n try {\n await fileOutreachTask(deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: `reply:${messageIdOf(decidingMessage) ?? decidingMessage.id}`,\n title: `Reply from ${enrollment.contactName || enrollment.email}`,\n notes: emailExcerpt(decidingMessage.textBody ?? decidingMessage.snippet ?? '', decidingMessage.htmlBody ?? '').slice(0, 500),\n kind: 'email',\n dueAtMs: nowMs,\n assigneeUid: context.mailbox.connectedByUid,\n })\n } catch (error) {\n console.error('[outreach] the reply task could not be filed', error)\n }\n }\n // A lead somebody wrote back to is being worked (AGL-3234): the record\n // system hears of the reply, and moves a lead nobody had worked to\n // Working (AGL-3080). Never throws.\n if (enrollment.target === 'lead' && enrollment.leadId && enrollment.email) {\n await stampRecordEmailReply({\n orgId: context.orgId,\n hostId: enrollment.hostId,\n emails: [enrollment.email],\n })\n }\n context.delta.replies += 1\n event = { type: 'reply', atMs: decidedBy?.atMs || nowMs, detail: decidedBy?.evidence ?? null }\n report.replies += 1\n // Once per enrollment (AGL-3254): a reply moves the status to\n // `replied`, and nothing is synced for it again.\n await creditOutreachReply(deps, { enrollment, atMs: event.atMs })\n break\n }\n case 'bounced': {\n const diagnostic = scrubbedOutreachDiagnostic(decidedBy?.bounce?.diagnostic ?? decidedBy?.bounce?.status ?? null, enrollment.email)\n await deps.suppressBouncedEmail({ email: enrollment.email, hostId: enrollment.hostId })\n await addOutreachDoNotContact(firestore, {\n orgId: context.orgId,\n email: enrollment.email,\n reason: 'hard_bounce',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n let detail = diagnostic\n // A gateway block is the domain's verdict on the sender (AGL-3244):\n // the domain is filed beside the address, and the enrollment says so.\n // Never a public mailbox provider's domain — Gmail refusing one\n // message on policy is not a reason to stop writing to Gmail.\n const domain = outreachEmailDomain(enrollment.email)\n const gateway = isOutreachGatewayBlock(decidedBy?.bounce ?? null)\n if (domain && !isPublicMailboxDomain(domain) && gateway) {\n await addOutreachDoNotContactDomain(firestore, {\n orgId: context.orgId,\n domain,\n reason: 'gateway_block',\n source: 'runtime',\n nowMs,\n enrollmentId: enrollment.id,\n sequenceId: enrollment.sequenceId,\n detail: diagnostic,\n })\n detail = outreachGatewayBlockDetail(domain, diagnostic)\n report.gatewayBlocks += 1\n }\n // The ledger learns the refusal (AGL-3326), whichever domain it was\n // at: a public provider refusing on policy is still that provider's\n // verdict on the sender, even though its domain is never listed.\n if (gateway) {\n await recordOutreachGatewayBlock(firestore, {\n orgId: context.orgId,\n email: enrollment.email,\n remoteMta: decidedBy?.bounce?.remoteMta ?? null,\n sendingDomain: outreachMailboxSendingDomain(context.mailbox),\n diagnostic,\n resolveMx: deps.resolveMx,\n nowMs,\n })\n }\n const bouncedAtMs = decidedBy?.atMs || nowMs\n // The record the person is says so too (AGL-3245): the verdict on the\n // lead and the contact, and the bounce on the send's own timeline\n // entry, so the record reads Sent, then Bounced.\n await deps.stampRecordEmailState({\n orgId: context.orgId,\n email: enrollment.email,\n state: {\n status: gateway ? 'blocked' : 'bounced',\n atMs: bouncedAtMs,\n source: 'sequence',\n detail: diagnostic,\n enrollmentId: enrollment.id,\n },\n })\n await markOutreachEmailDelivery(deps, {\n orgId: context.orgId,\n messageId: lastSentMessageId,\n state: 'bounced',\n atMs: bouncedAtMs,\n detail: diagnostic,\n })\n event = { type: 'bounce', atMs: bouncedAtMs, detail }\n report.bounces += 1\n break\n }\n case 'postpone': {\n const sequence = await sequenceOf(context, enrollment.sequenceId)\n postponedTo = postponeOutreachForAutoReply({\n nextDueAtMs: enrollment.nextDueAtMs,\n receivedAtMs: decidedBy?.atMs || nowMs,\n timeZone: context.mailbox.timezone,\n window: effectiveOutreachWindow(sequence?.settings.window, context.mailbox.window),\n random: deps.random,\n })\n break\n }\n default:\n break\n }\n\n const outcome = await applyOutreachEvent(firestore, {\n orgId: context.orgId,\n enrollmentId: enrollment.id,\n event,\n extra: (current) => {\n const extra: Record<string, unknown> = {\n syncedMessageIds: [...(current.syncedMessageIds ?? []), ...freshIds].slice(-ENROLLMENT_HANDLED_MAX),\n }\n if (inbound.length) extra['inboundMessages'] = mergeOutreachInboundMessages(current.inboundMessages, inbound)\n const waiting = current.status === 'active' || current.status === 'paused'\n if (postponedTo !== null && waiting && current.nextDueAtMs !== null && postponedTo > current.nextDueAtMs) {\n extra['nextDueAtMs'] = postponedTo\n }\n return extra\n },\n nowMs,\n })\n if (postponedTo !== null && outcome.enrollment?.nextDueAtMs === postponedTo) report.postponed += 1\n // The person's record says the sequence ended (AGL-3274), once, under the\n // reply that ended it — after the transaction moved the status, so a run\n // that reads the same reply again finds nothing to file. An opt-out's\n // line is `recordOutreachOptOut`'s, beside the lists it writes.\n if (event?.type === 'reply' && outcome.changed) {\n const sequence = await sequenceOf(context, enrollment.sequenceId)\n const entry = outreachStoppedEntry({\n enrollmentId: enrollment.id,\n sequenceName: sequence?.name ?? '',\n reason: 'replied',\n })\n await fileOutreachNote(deps, {\n orgId: context.orgId,\n hostId: enrollment.hostId,\n link: outreachEnrollmentLink(enrollment),\n dedupeKey: entry.dedupeKey,\n body: entry.body,\n atMs: event.atMs,\n })\n }\n // The run's own view of the enrollment moves with it, so a second message\n // of it in this run is judged against what this one did.\n if (outcome.enrollment) Object.assign(enrollment, outcome.enrollment)\n}\n"],"names":["normalizeContactEmail","isPublicMailboxDomain","emailAddressOf","emailExcerpt","outreachMessageIdHeader","readOutreachDeliveryReport","outreachEmailDomain","outreachStoppedEntry","isOutreachGatewayBlock","outreachGatewayBlockDetail","mergeOutreachInboundMessages","outreachInboundMessagesOf","effectiveOutreachWindow","postponeOutreachForAutoReply","decideOutreachThread","outreachHeader","outreachThreadMessageFromGmail","mailboxRef","OUTREACH_COLLECTIONS","addOutreachDoNotContact","addOutreachDoNotContactDomain","outreachEnrollmentLink","outreachOrgCollection","readStoredOutreachEnrollment","readStoredOutreachMailbox","readStoredOutreachSequence","GmailTransportError","isReconnectRequired","creditOutreachReply","applyOutreachEvent","recordOutreachOptOut","creditOutreachGatewayDeliveries","recordOutreachGatewayBlock","outreachMailboxSendingDomain","applyOutreachMailboxHealth","emptyOutreachHealthDelta","noteOutreachMailboxReconnectRequired","stampRecordEmailReply","fileOutreachEmail","fileOutreachNote","fileOutreachTask","markOutreachEmailDelivery","outreachUnsubscribeMailbox","OUTREACH_SYNC_JOB_ID","OUTREACH_SYNC_LOOKBACK_MS","OUTREACH_SYNC_OVERLAP_MS","OUTREACH_SYNC_WATCH_MS","SEARCH_PAGES","MAILBOX_HANDLED_MAX","MAILBOX_SYNC_WINDOW_REFRESH_MS","ENROLLMENT_HANDLED_MAX","FROM_CHUNK","runOutreachSyncJob","deps","context","firestore","report","mailboxes","threads","replies","optOuts","bounces","gatewayBlocks","deliveries","postponed","paused","all","collectionGroup","get","byOrg","Map","doc","docs","orgId","ref","parent","id","mailbox","data","status","set","now","deadlineMs","orgSnapshot","collection","org","exists","orgRefusal","syncMailbox","nowMs","error","mailboxId","errorCode","code","console","search","client","criteria","found","pageToken","page","list","searchMessages","maxResults","push","messages","filter","message","nextPageToken","input","opened","openMailbox","ok","reason","unsubscribe","email","selfAddresses","sendAs","sendAsOptions","map","option","Boolean","unsubscribeAddress","address","sequences","delta","watched","where","byThread","byAddress","watchedEnrollments","enrollment","known","threadId","gmailThreadIds","lastSentAtMs","sinceMs","Math","max","Number","sync","throughMs","handled","Set","handledMessageIds","newlyHandled","markHandled","has","add","fresh","stub","inbound","afterMs","notFromSelf","thread","getFullThread","applyMessages","from","getFullMessage","delivery","kind","failedAddresses","to","metadata","getMessage","metadataHeaders","headers","find","header","name","toLowerCase","value","includes","source","hostIds","hostId","detail","addresses","keys","start","length","chunk","slice","enrollments","sendingDomain","resolveMx","update","sequenceOf","sequenceId","snapshot","undefined","messageIdOf","scrubbedOutreachDiagnostic","diagnostic","pattern","RegExp","replace","fileInbound","messageId","link","direction","subject","inReplyTo","body","textBody","snippet","htmlBody","atMs","internalDateMs","byUid","outcome","decision","recipient","syncedMessageIds","threadSubject","freshIds","classifications","classification","decidedBy","decidingMessage","lastSentMessageId","messageIds","aboutRecipient","bounce","recipients","complaint","complaintAtMs","event","postponedTo","evidence","startsWith","state","dedupeKey","title","contactName","notes","dueAtMs","assigneeUid","connectedByUid","target","leadId","emails","type","suppressBouncedEmail","enrollmentId","domain","gateway","remoteMta","bouncedAtMs","stampRecordEmailState","sequence","nextDueAtMs","receivedAtMs","timeZone","timezone","window","settings","random","extra","current","inboundMessages","waiting","changed","entry","sequenceName","Object","assign"],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import type { ComposedOutreachEmail } from '../engine/compose';
|
|
18
|
+
import type { OutreachManualReply } from '../engine/manual-reply';
|
|
18
19
|
import type { OutreachMailClient } from './mail-client';
|
|
19
20
|
import { type OutreachComposedMessage, type OutreachMailAddress } from './rfc5322';
|
|
20
21
|
/** What one sent message is known by afterwards. */
|
|
@@ -57,6 +58,16 @@ export interface SendOutreachMessageOptions {
|
|
|
57
58
|
* built by the same RFC 5322 writer and none can bypass its header checks.
|
|
58
59
|
*/
|
|
59
60
|
export declare function sendOutreachMessage(client: Pick<OutreachMailClient, 'sendMessage'>, message: OutreachComposedMessage, options?: SendOutreachMessageOptions): Promise<OutreachSentMessage>;
|
|
61
|
+
/**
|
|
62
|
+
* Sends a member's answer to a person who wrote back (AGL-4020), as the
|
|
63
|
+
* engine wrote it (`composeOutreachManualReply`): into the provider thread of
|
|
64
|
+
* the message it answers, with `In-Reply-To` that message's `Message-ID` and
|
|
65
|
+
* `References` the whole chain, so it threads in both mailboxes — by thread
|
|
66
|
+
* id in Gmail's, and by the headers in Exchange's and the person's. Plain
|
|
67
|
+
* text, and no `List-Unsubscribe`: it is a one-to-one answer, not a
|
|
68
|
+
* sequence's email.
|
|
69
|
+
*/
|
|
70
|
+
export declare function sendOutreachReply(client: Pick<OutreachMailClient, 'sendMessage'>, reply: Pick<OutreachManualReply, 'to' | 'subject' | 'text' | 'threadId' | 'inReplyTo' | 'references'>, sender: OutreachMailAddress, options?: Omit<SendOutreachMessageOptions, 'threadId'>): Promise<OutreachSentMessage>;
|
|
60
71
|
/**
|
|
61
72
|
* Sends the email the engine composed (`composeOutreachEmail`) from a
|
|
62
73
|
* mailbox, as the sending runtime does for a sequence step.
|
|
@@ -39,6 +39,28 @@ import { buildRfc5322Message, encodeGmailRawMessage } from "./rfc5322.js";
|
|
|
39
39
|
subject: built.subject
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Sends a member's answer to a person who wrote back (AGL-4020), as the
|
|
44
|
+
* engine wrote it (`composeOutreachManualReply`): into the provider thread of
|
|
45
|
+
* the message it answers, with `In-Reply-To` that message's `Message-ID` and
|
|
46
|
+
* `References` the whole chain, so it threads in both mailboxes — by thread
|
|
47
|
+
* id in Gmail's, and by the headers in Exchange's and the person's. Plain
|
|
48
|
+
* text, and no `List-Unsubscribe`: it is a one-to-one answer, not a
|
|
49
|
+
* sequence's email.
|
|
50
|
+
*/ export function sendOutreachReply(client, reply, sender, options = {}) {
|
|
51
|
+
return sendOutreachMessage(client, {
|
|
52
|
+
from: sender,
|
|
53
|
+
to: reply.to,
|
|
54
|
+
subject: reply.subject,
|
|
55
|
+
text: reply.text,
|
|
56
|
+
headers: {
|
|
57
|
+
'In-Reply-To': reply.inReplyTo,
|
|
58
|
+
References: reply.references.length ? reply.references.join(' ') : null
|
|
59
|
+
}
|
|
60
|
+
}, _extends({}, options, {
|
|
61
|
+
threadId: reply.threadId
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
42
64
|
/**
|
|
43
65
|
* Sends the email the engine composed (`composeOutreachEmail`) from a
|
|
44
66
|
* mailbox, as the sending runtime does for a sequence step.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transport/send-message.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 type { ComposedOutreachEmail } from '../engine/compose'\nimport type { OutreachMailClient } from './mail-client'\nimport { listUnsubscribeHeaders } from '@aglyn/shared-util-email/list-unsubscribe'\nimport {\n buildRfc5322Message,\n encodeGmailRawMessage,\n type OutreachComposedMessage,\n type OutreachMailAddress,\n} from './rfc5322'\n\n/** What one sent message is known by afterwards. */\nexport interface OutreachSentMessage {\n /** The provider's id for the message in the sender's mailbox. */\n gmailMessageId: string\n /**\n * The provider's thread — Gmail's thread id, Graph's conversation id —\n * which the next step of the sequence replies into.\n */\n threadId: string\n /**\n * The RFC 5322 `Message-ID` it went out under, for `In-Reply-To` and\n * `References`: the one it was written with, unless the provider says it\n * sent it under another.\n */\n messageId: string\n /**\n * The subject as sent — the thread's own subject when this message started\n * the thread, which a later step answers with `Re:`.\n */\n subject: string\n}\n\nexport interface SendOutreachMessageOptions {\n /** The provider thread to send into; a new thread when absent. */\n threadId?: string | null\n /** The `Date` header. Now when omitted. */\n date?: Date\n /** The Message-ID's domain. The From address's domain when omitted. */\n messageIdDomain?: string\n /**\n * The Message-ID's local part, minted by the caller so it knows the id\n * before the send answers (AGL-2981). A random one when omitted.\n */\n messageIdLocalPart?: string\n}\n\n/**\n * Builds a composed message and sends it through a mailbox's client\n * (AGL-2978). The one door a send goes through, so every Outreach message is\n * built by the same RFC 5322 writer and none can bypass its header checks.\n */\nexport async function sendOutreachMessage(\n client: Pick<OutreachMailClient, 'sendMessage'>,\n message: OutreachComposedMessage,\n options: SendOutreachMessageOptions = {},\n): Promise<OutreachSentMessage> {\n const built = buildRfc5322Message(message, {\n date: options.date,\n messageIdDomain: options.messageIdDomain,\n messageIdLocalPart: options.messageIdLocalPart,\n })\n const sent = await client.sendMessage({\n raw: encodeGmailRawMessage(built.raw),\n threadId: options.threadId ?? null,\n })\n const reported = String(sent.internetMessageId ?? '').trim()\n return {\n gmailMessageId: sent.id,\n threadId: sent.threadId,\n messageId: reported || built.messageId,\n subject: built.subject,\n }\n}\n\n/**\n * Sends the email the engine composed (`composeOutreachEmail`) from a\n * mailbox, as the sending runtime does for a sequence step.\n *\n * The engine decides what is said and how it threads; this adds what the\n * engine leaves to the transport — the From line, from the mailbox's send-as\n * address and display name, and the MIME — and maps the rest onto headers:\n *\n * - a reply step goes into the engine's `threadId`, with its `In-Reply-To`\n * and `References`;\n * - the unsubscribe URL and mailto become one `List-Unsubscribe`, and the\n * URL adds `List-Unsubscribe-Post: List-Unsubscribe=One-Click` (RFC 8058).\n *\n * Answers the provider's thread id, the `Message-ID` and the subject as sent,\n * which the runtime records on the enrollment for the next step to answer.\n */\nexport function sendComposedOutreachEmail(\n client: Pick<OutreachMailClient, 'sendMessage'>,\n email: ComposedOutreachEmail,\n sender: OutreachMailAddress,\n options: Omit<SendOutreachMessageOptions, 'threadId'> = {},\n): Promise<OutreachSentMessage> {\n // The pair campaigns write too (AGL-3307): both URIs in one\n // `List-Unsubscribe`, and the one-click `Post` only beside the URL.\n const unsubscribe = listUnsubscribeHeaders({\n url: email.listUnsubscribeUrl,\n mailto: email.listUnsubscribeMailto,\n })\n return sendOutreachMessage(\n client,\n {\n from: sender,\n to: email.to,\n subject: email.subject,\n text: email.text,\n html: email.html ?? null,\n headers: {\n 'In-Reply-To': email.inReplyTo,\n References: email.references,\n 'List-Unsubscribe': unsubscribe['List-Unsubscribe'] ?? null,\n 'List-Unsubscribe-Post': unsubscribe['List-Unsubscribe-Post'] ?? null,\n },\n },\n { ...options, threadId: email.threadId ?? null },\n )\n}\n"],"names":["listUnsubscribeHeaders","buildRfc5322Message","encodeGmailRawMessage","sendOutreachMessage","client","message","options","sent","built","date","messageIdDomain","messageIdLocalPart","sendMessage","raw","threadId","reported","String","internetMessageId","trim","gmailMessageId","id","messageId","subject","
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/transport/send-message.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 type { ComposedOutreachEmail } from '../engine/compose'\nimport type { OutreachManualReply } from '../engine/manual-reply'\nimport type { OutreachMailClient } from './mail-client'\nimport { listUnsubscribeHeaders } from '@aglyn/shared-util-email/list-unsubscribe'\nimport {\n buildRfc5322Message,\n encodeGmailRawMessage,\n type OutreachComposedMessage,\n type OutreachMailAddress,\n} from './rfc5322'\n\n/** What one sent message is known by afterwards. */\nexport interface OutreachSentMessage {\n /** The provider's id for the message in the sender's mailbox. */\n gmailMessageId: string\n /**\n * The provider's thread — Gmail's thread id, Graph's conversation id —\n * which the next step of the sequence replies into.\n */\n threadId: string\n /**\n * The RFC 5322 `Message-ID` it went out under, for `In-Reply-To` and\n * `References`: the one it was written with, unless the provider says it\n * sent it under another.\n */\n messageId: string\n /**\n * The subject as sent — the thread's own subject when this message started\n * the thread, which a later step answers with `Re:`.\n */\n subject: string\n}\n\nexport interface SendOutreachMessageOptions {\n /** The provider thread to send into; a new thread when absent. */\n threadId?: string | null\n /** The `Date` header. Now when omitted. */\n date?: Date\n /** The Message-ID's domain. The From address's domain when omitted. */\n messageIdDomain?: string\n /**\n * The Message-ID's local part, minted by the caller so it knows the id\n * before the send answers (AGL-2981). A random one when omitted.\n */\n messageIdLocalPart?: string\n}\n\n/**\n * Builds a composed message and sends it through a mailbox's client\n * (AGL-2978). The one door a send goes through, so every Outreach message is\n * built by the same RFC 5322 writer and none can bypass its header checks.\n */\nexport async function sendOutreachMessage(\n client: Pick<OutreachMailClient, 'sendMessage'>,\n message: OutreachComposedMessage,\n options: SendOutreachMessageOptions = {},\n): Promise<OutreachSentMessage> {\n const built = buildRfc5322Message(message, {\n date: options.date,\n messageIdDomain: options.messageIdDomain,\n messageIdLocalPart: options.messageIdLocalPart,\n })\n const sent = await client.sendMessage({\n raw: encodeGmailRawMessage(built.raw),\n threadId: options.threadId ?? null,\n })\n const reported = String(sent.internetMessageId ?? '').trim()\n return {\n gmailMessageId: sent.id,\n threadId: sent.threadId,\n messageId: reported || built.messageId,\n subject: built.subject,\n }\n}\n\n/**\n * Sends a member's answer to a person who wrote back (AGL-4020), as the\n * engine wrote it (`composeOutreachManualReply`): into the provider thread of\n * the message it answers, with `In-Reply-To` that message's `Message-ID` and\n * `References` the whole chain, so it threads in both mailboxes — by thread\n * id in Gmail's, and by the headers in Exchange's and the person's. Plain\n * text, and no `List-Unsubscribe`: it is a one-to-one answer, not a\n * sequence's email.\n */\nexport function sendOutreachReply(\n client: Pick<OutreachMailClient, 'sendMessage'>,\n reply: Pick<OutreachManualReply, 'to' | 'subject' | 'text' | 'threadId' | 'inReplyTo' | 'references'>,\n sender: OutreachMailAddress,\n options: Omit<SendOutreachMessageOptions, 'threadId'> = {},\n): Promise<OutreachSentMessage> {\n return sendOutreachMessage(\n client,\n {\n from: sender,\n to: reply.to,\n subject: reply.subject,\n text: reply.text,\n headers: {\n 'In-Reply-To': reply.inReplyTo,\n References: reply.references.length ? reply.references.join(' ') : null,\n },\n },\n { ...options, threadId: reply.threadId },\n )\n}\n\n/**\n * Sends the email the engine composed (`composeOutreachEmail`) from a\n * mailbox, as the sending runtime does for a sequence step.\n *\n * The engine decides what is said and how it threads; this adds what the\n * engine leaves to the transport — the From line, from the mailbox's send-as\n * address and display name, and the MIME — and maps the rest onto headers:\n *\n * - a reply step goes into the engine's `threadId`, with its `In-Reply-To`\n * and `References`;\n * - the unsubscribe URL and mailto become one `List-Unsubscribe`, and the\n * URL adds `List-Unsubscribe-Post: List-Unsubscribe=One-Click` (RFC 8058).\n *\n * Answers the provider's thread id, the `Message-ID` and the subject as sent,\n * which the runtime records on the enrollment for the next step to answer.\n */\nexport function sendComposedOutreachEmail(\n client: Pick<OutreachMailClient, 'sendMessage'>,\n email: ComposedOutreachEmail,\n sender: OutreachMailAddress,\n options: Omit<SendOutreachMessageOptions, 'threadId'> = {},\n): Promise<OutreachSentMessage> {\n // The pair campaigns write too (AGL-3307): both URIs in one\n // `List-Unsubscribe`, and the one-click `Post` only beside the URL.\n const unsubscribe = listUnsubscribeHeaders({\n url: email.listUnsubscribeUrl,\n mailto: email.listUnsubscribeMailto,\n })\n return sendOutreachMessage(\n client,\n {\n from: sender,\n to: email.to,\n subject: email.subject,\n text: email.text,\n html: email.html ?? null,\n headers: {\n 'In-Reply-To': email.inReplyTo,\n References: email.references,\n 'List-Unsubscribe': unsubscribe['List-Unsubscribe'] ?? null,\n 'List-Unsubscribe-Post': unsubscribe['List-Unsubscribe-Post'] ?? null,\n },\n },\n { ...options, threadId: email.threadId ?? null },\n )\n}\n"],"names":["listUnsubscribeHeaders","buildRfc5322Message","encodeGmailRawMessage","sendOutreachMessage","client","message","options","sent","built","date","messageIdDomain","messageIdLocalPart","sendMessage","raw","threadId","reported","String","internetMessageId","trim","gmailMessageId","id","messageId","subject","sendOutreachReply","reply","sender","from","to","text","headers","inReplyTo","References","references","length","join","sendComposedOutreachEmail","email","unsubscribe","url","listUnsubscribeUrl","mailto","listUnsubscribeMailto","html"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAKD,SAASA,sBAAsB,QAAQ,4CAA2C;AAClF,SACEC,mBAAmB,EACnBC,qBAAqB,QAGhB,eAAW;AAsClB;;;;CAIC,GACD,OAAO,eAAeC,oBACpBC,MAA+C,EAC/CC,OAAgC,EAChCC,UAAsC,CAAC,CAAC;QAS5BA,mBAEYC;IATxB,MAAMC,QAAQP,oBAAoBI,SAAS;QACzCI,MAAMH,QAAQG,IAAI;QAClBC,iBAAiBJ,QAAQI,eAAe;QACxCC,oBAAoBL,QAAQK,kBAAkB;IAChD;IACA,MAAMJ,OAAO,MAAMH,OAAOQ,WAAW,CAAC;QACpCC,KAAKX,sBAAsBM,MAAMK,GAAG;QACpCC,QAAQ,GAAER,oBAAAA,QAAQQ,QAAQ,YAAhBR,oBAAoB;IAChC;IACA,MAAMS,WAAWC,QAAOT,0BAAAA,KAAKU,iBAAiB,YAAtBV,0BAA0B,IAAIW,IAAI;IAC1D,OAAO;QACLC,gBAAgBZ,KAAKa,EAAE;QACvBN,UAAUP,KAAKO,QAAQ;QACvBO,WAAWN,YAAYP,MAAMa,SAAS;QACtCC,SAASd,MAAMc,OAAO;IACxB;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,kBACdnB,MAA+C,EAC/CoB,KAAqG,EACrGC,MAA2B,EAC3BnB,UAAwD,CAAC,CAAC;IAE1D,OAAOH,oBACLC,QACA;QACEsB,MAAMD;QACNE,IAAIH,MAAMG,EAAE;QACZL,SAASE,MAAMF,OAAO;QACtBM,MAAMJ,MAAMI,IAAI;QAChBC,SAAS;YACP,eAAeL,MAAMM,SAAS;YAC9BC,YAAYP,MAAMQ,UAAU,CAACC,MAAM,GAAGT,MAAMQ,UAAU,CAACE,IAAI,CAAC,OAAO;QACrE;IACF,GACA,aAAK5B;QAASQ,UAAUU,MAAMV,QAAQ;;AAE1C;AAEA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASqB,0BACd/B,MAA+C,EAC/CgC,KAA4B,EAC5BX,MAA2B,EAC3BnB,UAAwD,CAAC,CAAC;QAehD8B,aAIgBC,8BACKA,kCAGLD;IArB1B,4DAA4D;IAC5D,oEAAoE;IACpE,MAAMC,cAAcrC,uBAAuB;QACzCsC,KAAKF,MAAMG,kBAAkB;QAC7BC,QAAQJ,MAAMK,qBAAqB;IACrC;IACA,OAAOtC,oBACLC,QACA;QACEsB,MAAMD;QACNE,IAAIS,MAAMT,EAAE;QACZL,SAASc,MAAMd,OAAO;QACtBM,MAAMQ,MAAMR,IAAI;QAChBc,IAAI,GAAEN,cAAAA,MAAMM,IAAI,YAAVN,cAAc;QACpBP,SAAS;YACP,eAAeO,MAAMN,SAAS;YAC9BC,YAAYK,MAAMJ,UAAU;YAC5B,kBAAkB,GAAEK,+BAAAA,WAAW,CAAC,mBAAmB,YAA/BA,+BAAmC;YACvD,uBAAuB,GAAEA,mCAAAA,WAAW,CAAC,wBAAwB,YAApCA,mCAAwC;QACnE;IACF,GACA,aAAK/B;QAASQ,QAAQ,GAAEsB,kBAAAA,MAAMtB,QAAQ,YAAdsB,kBAAkB;;AAE9C"}
|