@aglyn/plugins-outreach 1.0.0-beta.217 → 1.0.0-beta.219

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/compliance-section.js +72 -34
  3. package/src/lib/components/compliance-section.js.map +1 -1
  4. package/src/lib/components/enroll-dialog.d.ts +12 -2
  5. package/src/lib/components/enroll-dialog.js +72 -18
  6. package/src/lib/components/enroll-dialog.js.map +1 -1
  7. package/src/lib/components/enrollment-detail.d.ts +5 -1
  8. package/src/lib/components/enrollment-detail.js +12 -6
  9. package/src/lib/components/enrollment-detail.js.map +1 -1
  10. package/src/lib/components/outreach-console-sections.d.ts +1 -1
  11. package/src/lib/components/outreach-console-sections.js +1 -1
  12. package/src/lib/components/outreach-console-sections.js.map +1 -1
  13. package/src/lib/components/sequence-editor.js +85 -40
  14. package/src/lib/components/sequence-editor.js.map +1 -1
  15. package/src/lib/components/use-outreach-api.d.ts +2 -1
  16. package/src/lib/components/use-outreach-api.js +5 -3
  17. package/src/lib/components/use-outreach-api.js.map +1 -1
  18. package/src/lib/engine/compose.d.ts +1 -1
  19. package/src/lib/engine/compose.js +8 -4
  20. package/src/lib/engine/compose.js.map +1 -1
  21. package/src/lib/engine/enrollment-activity.d.ts +9 -1
  22. package/src/lib/engine/enrollment-activity.js +15 -2
  23. package/src/lib/engine/enrollment-activity.js.map +1 -1
  24. package/src/lib/engine/enrollment-state.d.ts +33 -2
  25. package/src/lib/engine/enrollment-state.js +66 -7
  26. package/src/lib/engine/enrollment-state.js.map +1 -1
  27. package/src/lib/engine/sending-capacity.d.ts +1 -1
  28. package/src/lib/engine/sending-capacity.js +1 -1
  29. package/src/lib/engine/sending-capacity.js.map +1 -1
  30. package/src/lib/engine/sequence-validation.d.ts +20 -3
  31. package/src/lib/engine/sequence-validation.js +30 -4
  32. package/src/lib/engine/sequence-validation.js.map +1 -1
  33. package/src/lib/enrollment/enroll-people.js +6 -4
  34. package/src/lib/enrollment/enroll-people.js.map +1 -1
  35. package/src/lib/enrollment/step-overrides.d.ts +4 -2
  36. package/src/lib/enrollment/step-overrides.js +11 -4
  37. package/src/lib/enrollment/step-overrides.js.map +1 -1
  38. package/src/lib/mailboxes/mailbox-api.d.ts +5 -1
  39. package/src/lib/mailboxes/mailbox-api.js.map +1 -1
  40. package/src/lib/mailboxes/mailbox-routes.d.ts +5 -2
  41. package/src/lib/mailboxes/mailbox-routes.js +10 -7
  42. package/src/lib/mailboxes/mailbox-routes.js.map +1 -1
  43. package/src/lib/mailboxes/mailbox-settings.d.ts +2 -2
  44. package/src/lib/mailboxes/mailbox-settings.js +1 -1
  45. package/src/lib/mailboxes/mailbox-settings.js.map +1 -1
  46. package/src/lib/model/enrollment-timeline.d.ts +1 -1
  47. package/src/lib/model/enrollment-timeline.js +22 -3
  48. package/src/lib/model/enrollment-timeline.js.map +1 -1
  49. package/src/lib/model/outreach-api.d.ts +19 -1
  50. package/src/lib/model/outreach-api.js.map +1 -1
  51. package/src/lib/model/outreach.types.d.ts +25 -0
  52. package/src/lib/model/outreach.types.js.map +1 -1
  53. package/src/lib/model/step-preview.d.ts +2 -0
  54. package/src/lib/model/step-preview.js +11 -7
  55. package/src/lib/model/step-preview.js.map +1 -1
  56. package/src/lib/model/stored-records.js +26 -0
  57. package/src/lib/model/stored-records.js.map +1 -1
  58. package/src/lib/plugin.js +1 -1
  59. package/src/lib/plugin.js.map +1 -1
  60. package/src/lib/routes/enroll-routes.d.ts +6 -0
  61. package/src/lib/routes/enroll-routes.js +21 -6
  62. package/src/lib/routes/enroll-routes.js.map +1 -1
  63. package/src/lib/routes/preview-routes.js +14 -5
  64. package/src/lib/routes/preview-routes.js.map +1 -1
  65. package/src/lib/routes/step-test-routes.js +3 -3
  66. package/src/lib/routes/step-test-routes.js.map +1 -1
  67. package/src/lib/testing/stand-in-crm-record-routes.d.ts +25 -0
  68. package/src/lib/testing/stand-in-crm-record-routes.js +46 -0
  69. package/src/lib/testing/stand-in-crm-record-routes.js.map +1 -0
  70. package/src/lib/transport/gmail-errors.d.ts +27 -0
  71. package/src/lib/transport/gmail-errors.js +56 -0
  72. package/src/lib/transport/gmail-errors.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequence-editor.tsx"],"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'use client'\n\nimport { pluginDocsHelp, type ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useOrgCampaigns, useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Autocomplete,\n Box,\n Button,\n Chip,\n FormControlLabel,\n Grid,\n Menu,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport {\n isInThreadEmailStep,\n OUTREACH_SEQUENCE_NAME_MAX,\n validateOutreachSequence,\n} from '../engine/sequence-validation'\nimport { outreachCountryLabel } from '../model/compliance-settings'\nimport {\n OUTREACH_MAX_EMAIL_STEPS,\n OUTREACH_MAX_STEPS,\n OUTREACH_TASK_KIND_LABELS,\n OUTREACH_TASK_KINDS,\n type OutreachSequence,\n type OutreachSequenceStep,\n type OutreachTaskKind,\n} from '../model/outreach.types'\nimport {\n emptyOutreachSequenceDraft,\n newOutreachEmailStep,\n newOutreachTaskStep,\n outreachSequenceDraftOf,\n type OutreachSequenceDraft,\n type OutreachSequenceIssue,\n} from '../model/sequence-draft'\nimport { OutreachSendWindowFields } from './send-window-fields'\nimport { OutreachSequenceMailboxPicker } from './sequence-mailbox-picker'\nimport { OutreachSequencePreview } from './sequence-preview'\nimport { OutreachStepCard } from './sequence-step-card'\nimport { OutreachStepTestDialog } from './step-test-dialog'\nimport { OutreachRouteError, useOutreachApi } from './use-outreach-api'\nimport { useOutreachEmailTemplates } from './use-outreach-crm'\nimport type { OutreachMailboxesResult } from './use-outreach-mailboxes'\nimport type { OutreachSettingsLoad } from './use-outreach-settings'\n\nexport interface OutreachSequenceEditorProps {\n orgId: string\n orgMount?: ConsolePluginOrgMount\n /** The organization document, for the consent group a test send's person picker searches (AGL-3325). */\n org?: Record<string, unknown> | null\n /** The stored sequence, or `null` for a new one. */\n sequence: OutreachSequence | null\n /** The organization's compliance settings: its countries, and the preview's footer. */\n settings: OutreachSettingsLoad\n /**\n * The organization's mailboxes, from the Mailboxes section's listener:\n * what the picker offers, and who the preview's emails are from.\n */\n mailboxes: OutreachMailboxesResult\n /** The Mailboxes section, where the picker sends a member to connect one. */\n mailboxesPath: string\n onSaved(sequence: OutreachSequence): void\n onCancel?(): void\n}\n\n/** The step a dotted issue path is about, and the field within it. */\nfunction stepIssueKey(path: string): { index: number; field: string } | null {\n const match = /^steps\\.(\\d+)(?:\\.(.+))?$/.exec(path)\n if (!match) return null\n const field = match[2] ?? 'step'\n return { index: Number(match[1]), field: field === 'id' ? 'step' : field }\n}\n\n/**\n * The sequence editor (AGL-2980): the name, the site and mailbox, the steps,\n * who it sends to and when, beside a live preview of each email.\n *\n * Every issue the engine's validator finds is shown beside its field — the\n * same validator the save route refuses with — errors once a save was\n * attempted, warnings as soon as they appear. What only the server can\n * judge (whose mailbox, which countries the organization allows, what may\n * no longer change once people are enrolled) comes back from the save and\n * is shown in the same places.\n */\nexport function OutreachSequenceEditor(props: OutreachSequenceEditorProps) {\n const { orgId, orgMount, sequence, settings } = props\n const api = useOutreachApi(orgId)\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n const uid = user?.uid ?? null\n const mountedHosts = orgMount?.hosts\n const hosts = useMemo(() => mountedHosts ?? [], [mountedHosts])\n const orgCountries = settings.settings?.allowedCountries ?? ['US']\n\n const [draft, setDraft] = useState<OutreachSequenceDraft>(() =>\n sequence\n ? outreachSequenceDraftOf(sequence)\n : emptyOutreachSequenceDraft({\n hostId: hosts.length === 1 ? hosts[0].id : '',\n allowedCountries: orgCountries,\n }),\n )\n const [attempted, setAttempted] = useState(false)\n const [serverIssues, setServerIssues] = useState<OutreachSequenceIssue[]>([])\n const [saving, setSaving] = useState(false)\n const [taskAnchor, setTaskAnchor] = useState<HTMLElement | null>(null)\n // The step whose test-send dialog is open (AGL-3325), by index.\n const [testing, setTesting] = useState<number | null>(null)\n\n // A stored sequence the listener updated (a save, a status change) is the\n // new starting point.\n useEffect(() => {\n if (sequence) setDraft(outreachSequenceDraftOf(sequence))\n }, [sequence])\n // A new sequence starts on the organization's countries once they are read.\n const settingsCountries = settings.settings?.allowedCountries.join(',')\n useEffect(() => {\n if (!sequence && settingsCountries) {\n setDraft((previous) => ({\n ...previous,\n settings: {\n ...previous.settings,\n allowedCountries: settingsCountries.split(','),\n },\n }))\n }\n }, [sequence, settingsCountries])\n useEffect(() => {\n if (!sequence && !draft.hostId && hosts.length === 1)\n setDraft((previous) => ({ ...previous, hostId: hosts[0].id }))\n }, [sequence, draft.hostId, hosts])\n\n // A new sequence starts on the member's own mailbox when they have exactly\n // one that is sending.\n const ownSending = props.mailboxes.mailboxes.filter(\n (entry) => entry.connectedByUid === uid && entry.status === 'connected',\n )\n const onlyOwnMailbox = ownSending.length === 1 ? ownSending[0].id : ''\n useEffect(() => {\n if (!sequence && !draft.mailboxId && onlyOwnMailbox)\n setDraft((previous) => ({ ...previous, mailboxId: onlyOwnMailbox }))\n }, [sequence, draft.mailboxId, onlyOwnMailbox])\n\n const templates = useOutreachEmailTemplates(orgId, draft.hostId || null, uid)\n const mailbox =\n props.mailboxes.mailboxes.find((entry) => entry.id === draft.mailboxId) ??\n null\n const archived = sequence?.status === 'archived'\n /*\n * The org's campaigns, for the picker (AGL-3254): read while the editor\n * is open. A sequence is the organization's record and so is a campaign,\n * so the choice is every campaign in the org, whichever site the\n * sequence sends as.\n */\n const campaigns = useOrgCampaigns(orgId, { enabled: true })\n\n const issues = useMemo(() => {\n const judged = [\n ...validateOutreachSequence(draft),\n ...serverIssues,\n ] as OutreachSequenceIssue[]\n return judged.filter(\n (issue) =>\n issue.severity === 'warning' ||\n attempted ||\n serverIssues.includes(issue),\n )\n }, [draft, serverIssues, attempted])\n const issueAt = (path: string) =>\n issues.find((issue) => issue.path === path)?.message\n const stepIssues = (index: number) => {\n const found: Record<string, string> = {}\n for (const issue of issues) {\n const key = stepIssueKey(issue.path)\n if (key?.index === index && !found[key.field])\n found[key.field] = issue.message\n }\n return found\n }\n const listIssue = issueAt('steps')\n const countriesIssue = issues.find((issue) =>\n issue.path.startsWith('settings.allowedCountries'),\n )?.message\n\n const update = (next: Partial<OutreachSequenceDraft>) => {\n setServerIssues([])\n setDraft((previous) => ({ ...previous, ...next }))\n }\n const setStep = (index: number, step: OutreachSequenceStep) =>\n update({\n steps: draft.steps.map((entry, position) =>\n position === index ? step : entry,\n ),\n })\n const moveStep = (index: number, direction: -1 | 1) => {\n const steps = [...draft.steps]\n const [moved] = steps.splice(index, 1)\n steps.splice(index + direction, 0, moved)\n update({ steps })\n }\n const emailCount = draft.steps.filter((step) => step.kind === 'email').length\n const full = draft.steps.length >= OUTREACH_MAX_STEPS\n const threadSubject = (index: number) => {\n for (let position = index - 1; position >= 0; position -= 1) {\n const step = draft.steps[position]\n if (step.kind === 'email' && !isInThreadEmailStep(draft.steps, position))\n return step.subject\n }\n return ''\n }\n\n const save = async () => {\n setAttempted(true)\n if (\n validateOutreachSequence(draft).some(\n (issue) => issue.severity === 'error',\n )\n )\n return\n setSaving(true)\n try {\n const answer = await api.saveSequence(sequence?.id ?? null, draft)\n setServerIssues([])\n setAttempted(false)\n enqueueSnackbar(\n answer.created ? 'Sequence created.' : 'Sequence saved.',\n { variant: 'success' },\n )\n props.onSaved(answer.sequence)\n } catch (error) {\n if (error instanceof OutreachRouteError && error.issues.length) {\n setServerIssues(\n error.issues.filter(\n (issue): issue is OutreachSequenceIssue => 'path' in issue,\n ),\n )\n }\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(false)\n }\n }\n\n const siteName = hosts.find((host) => host.id === draft.hostId)?.name ?? ''\n const countryOptions = [\n ...new Set([...orgCountries, ...draft.settings.allowedCountries]),\n ]\n /*\n * A test sends the step as STORED (AGL-3325): the route reads the saved\n * sequence, so a new one has nothing to send yet and an edited one sends\n * its last save — said on the button and in the dialog rather than found\n * out in the inbox.\n */\n const unsaved =\n sequence !== null &&\n JSON.stringify(draft) !== JSON.stringify(outreachSequenceDraftOf(sequence))\n const sendTestDisabledReason = !sequence\n ? 'Create the sequence to send a test of it.'\n : !draft.mailboxId\n ? 'Choose a mailbox to send the test from.'\n : null\n\n return (\n <Grid container spacing={2}>\n <Grid size={{ xs: 12, md: 7 }}>\n <Stack spacing={2}>\n {archived ? (\n <Alert severity=\"info\">\n This sequence is archived, so it can’t be edited.\n </Alert>\n ) : null}\n <CardDisplay\n header=\"Sequence\"\n help={pluginDocsHelp('sequences', { anchor: '#build-a-sequence' })}\n // The whole sequence (steps and sending rules included) saves\n // from its first card's header, where the console puts a card's\n // actions (AGL-3333).\n HeaderProps={{\n action: archived ? undefined : (\n <Stack direction=\"row\" spacing={1}>\n {props.onCancel ? (\n <Button\n size=\"small\"\n variant=\"text\"\n onClick={props.onCancel}\n disabled={saving}\n >\n Cancel\n </Button>\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void save()}\n disabled={saving}\n >\n {saving ? 'Saving…' : sequence ? 'Save' : 'Create sequence'}\n </Button>\n </Stack>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <TextField\n label=\"Name\"\n value={draft.name}\n onChange={(event) => update({ name: event.target.value })}\n disabled={archived}\n error={Boolean(issueAt('name'))}\n helperText={issueAt('name')}\n slotProps={{\n htmlInput: { maxLength: OUTREACH_SEQUENCE_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n select\n label=\"Site\"\n value={\n hosts.some((host) => host.id === draft.hostId)\n ? draft.hostId\n : ''\n }\n onChange={(event) => update({ hostId: event.target.value })}\n disabled={archived || !hosts.length}\n error={Boolean(issueAt('hostId'))}\n helperText={\n issueAt('hostId') ??\n 'The site whose contacts this sequence emails.'\n }\n fullWidth\n >\n {hosts.map((host) => (\n <MenuItem key={host.id} value={host.id}>\n {host.name}\n </MenuItem>\n ))}\n </TextField>\n <OutreachSequenceMailboxPicker\n orgId={orgId}\n uid={uid}\n mailboxes={props.mailboxes}\n mailboxesPath={props.mailboxesPath}\n value={draft.mailboxId}\n onChange={(mailboxId) => update({ mailboxId })}\n error={issueAt('mailboxId')}\n disabled={archived}\n />\n {/*\n The campaigns this sequence is part of (AGL-3254), picked the\n way a form's page picks them. Everyone enrolled joins them,\n and what the sequence produces is reported under them; it\n does not change who is enrolled or what is sent.\n */}\n <CampaignPicker\n options={campaigns.options}\n value={draft.campaignIds}\n onChange={(campaignIds) => update({ campaignIds })}\n label=\"Campaigns\"\n helperText={\n issueAt('campaignIds') ??\n 'The campaigns this sequence is part of. Everyone enrolled joins them, and its sends, replies, meetings and conversions count on their pages.'\n }\n disabled={archived}\n empty={campaigns.ready && !campaigns.options.length}\n emptyText=\"There are no campaigns yet. Create one from Marketing to file this sequence under it.\"\n />\n </Stack>\n </CardDisplay>\n\n {listIssue ? <Alert severity=\"error\">{listIssue}</Alert> : null}\n {draft.steps.map((step, index) => (\n <OutreachStepCard\n key={step.id || `step-${index}`}\n step={step}\n index={index}\n count={draft.steps.length}\n firstEmail={\n draft.steps.findIndex((entry) => entry.kind === 'email') ===\n index\n }\n inThread={isInThreadEmailStep(draft.steps, index)}\n threadSubject={threadSubject(index)}\n templates={templates.data}\n issues={stepIssues(index)}\n disabled={archived}\n onChange={(next) => setStep(index, next)}\n onMove={(direction) => moveStep(index, direction)}\n onRemove={() =>\n update({\n steps: draft.steps.filter(\n (_entry, position) => position !== index,\n ),\n })\n }\n onSendTest={\n step.kind === 'email' && !archived\n ? () => setTesting(index)\n : undefined\n }\n sendTestDisabledReason={sendTestDisabledReason}\n />\n ))}\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', rowGap: 1 }}\n >\n <Button\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n disabled={\n archived || full || emailCount >= OUTREACH_MAX_EMAIL_STEPS\n }\n onClick={() =>\n update({\n steps: [...draft.steps, newOutreachEmailStep(draft.steps)],\n })\n }\n >\n Add email\n </Button>\n <Button\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n disabled={archived || full}\n onClick={(event) => setTaskAnchor(event.currentTarget)}\n aria-haspopup=\"menu\"\n >\n Add task\n </Button>\n <Menu\n anchorEl={taskAnchor}\n open={Boolean(taskAnchor)}\n onClose={() => setTaskAnchor(null)}\n >\n {OUTREACH_TASK_KINDS.map((kind: OutreachTaskKind) => (\n <MenuItem\n key={kind}\n onClick={() => {\n setTaskAnchor(null)\n update({\n steps: [\n ...draft.steps,\n newOutreachTaskStep(draft.steps, kind),\n ],\n })\n }}\n >\n {OUTREACH_TASK_KIND_LABELS[kind]}\n </MenuItem>\n ))}\n </Menu>\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Up to ${OUTREACH_MAX_STEPS} steps, ${OUTREACH_MAX_EMAIL_STEPS} of them emails.`}\n </Typography>\n\n <CardDisplay\n header=\"Who it sends to, and when\"\n help={pluginDocsHelp('sequences', {\n anchor: '#build-a-sequence',\n excerpt:\n 'A sequence sends only to the countries both it and your organization allow, and in its mailbox’s sending hours unless it has its own.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Autocomplete<string, true>\n multiple\n options={countryOptions}\n value={draft.settings.allowedCountries}\n getOptionLabel={outreachCountryLabel}\n onChange={(_event, next) =>\n update({\n settings: { ...draft.settings, allowedCountries: next },\n })\n }\n disabled={archived}\n renderValue={(value, getItemProps) =>\n value.map((code, index) => {\n const { key, ...item } = getItemProps({ index })\n return (\n <Chip\n key={key}\n size=\"small\"\n label={outreachCountryLabel(code)}\n {...item}\n />\n )\n })\n }\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Countries\"\n error={Boolean(countriesIssue)}\n helperText={\n countriesIssue ??\n 'Among the countries your organization allows. Cold contacts are emailed only in the United States.'\n }\n />\n )}\n />\n <FormControlLabel\n control={\n <Switch\n checked={draft.settings.allowCustomers}\n onChange={(event) =>\n update({\n settings: {\n ...draft.settings,\n allowCustomers: event.target.checked,\n },\n })\n }\n disabled={archived}\n />\n }\n label=\"Include customers\"\n />\n {/*\n * Click tracking (AGL-3239), with its cost stated where it is\n * chosen rather than in a document nobody opens. Off by\n * default: it changes the link a cold recipient sees. The\n * emails stay plain text either way; opens are the separate\n * switch below.\n */}\n <Stack spacing={0}>\n <FormControlLabel\n control={\n <Switch\n checked={draft.settings.trackClicks}\n onChange={(event) =>\n update({\n settings: {\n ...draft.settings,\n trackClicks: event.target.checked,\n },\n })\n }\n disabled={archived}\n />\n }\n label=\"Count link clicks\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n Links in these emails are replaced with links of ours that\n forward to the same page, so clicks can be counted. The\n recipient sees the replacement, not your address. The\n emails stay plain text.\n </Typography>\n </Stack>\n {/*\n * Open counting (AGL-3395), off by default, its cost stated\n * where it is chosen: an HTML copy with a tracking image is the\n * least one-to-one thing a sequence email can carry, so it is\n * for measuring a slice, not for every sequence.\n */}\n <Stack spacing={0}>\n <FormControlLabel\n control={\n <Switch\n checked={draft.settings.countOpens}\n onChange={(event) =>\n update({\n settings: {\n ...draft.settings,\n countOpens: event.target.checked,\n },\n })\n }\n disabled={archived}\n />\n }\n label=\"Count opens\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n Each email also goes out as HTML — the same words — with a\n tiny tracking image, so opens can be counted. The plain-text\n version is unchanged. It makes the email look less like one\n written by hand, and some spam filters score HTML with a\n remote image against you, so turn it on to measure a slice\n of your sends rather than for every sequence. Gmail opens\n count; Apple Mail loads every image on arrival, so its loads\n are counted separately.\n </Typography>\n </Stack>\n {/*\n * The mail-client unsubscribe button (AGL-3296), off by\n * default: the header is what makes Apple Mail, Gmail and\n * Outlook present a one-to-one email as a mailing list. The\n * footer's \"reply 'no'\" line is the way out either way.\n */}\n <Stack spacing={0}>\n <FormControlLabel\n control={\n <Switch\n checked={draft.settings.listUnsubscribe}\n onChange={(event) =>\n update({\n settings: {\n ...draft.settings,\n listUnsubscribe: event.target.checked,\n },\n })\n }\n disabled={archived}\n />\n }\n label=\"Add a mail-client unsubscribe button\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n Adds the List-Unsubscribe header, so the recipient’s mail\n app shows its own Unsubscribe button. It’s the easiest way\n out for them, and fewer people mark you as spam when they\n have it, but mail apps then label the email as coming from\n a mailing list. Off, the footer’s “reply ‘no’” line is the\n way out, and it’s always there.\n </Typography>\n </Stack>\n <OutreachSendWindowFields\n window={draft.settings.window}\n onChange={(window) =>\n update({ settings: { ...draft.settings, window } })\n }\n daysIssue={issueAt('settings.window.days')}\n hoursIssue={issueAt('settings.window')}\n disabled={archived}\n />\n </Stack>\n </CardDisplay>\n\n </Stack>\n </Grid>\n {sequence && testing !== null ? (\n <OutreachStepTestDialog\n open\n onClose={() => setTesting(null)}\n orgId={orgId}\n sequence={sequence}\n stepIndex={testing}\n contactGroupId={\n sequence.hostId\n ? consentGroupForHost(props.org ?? null, sequence.hostId).groupId\n : ''\n }\n selfEmail={user?.email ?? null}\n unsaved={unsaved}\n api={api}\n />\n ) : null}\n <Grid size={{ xs: 12, md: 5 }}>\n <Box sx={{ position: { md: 'sticky' }, top: { md: 16 } }}>\n <OutreachSequencePreview\n steps={draft.steps}\n orgSettings={settings.settings}\n orgSettingsStatus={settings.status}\n templates={templates.data}\n sender={\n mailbox\n ? {\n name: mailbox.displayName,\n email: mailbox.sendAs || mailbox.email,\n }\n : null\n }\n siteName={siteName}\n />\n </Box>\n </Grid>\n </Grid>\n )\n}\nOutreachSequenceEditor.displayName = 'OutreachSequenceEditor'\n\nexport default OutreachSequenceEditor\n"],"names":["pluginDocsHelp","consentGroupForHost","mdiPlus","CardDisplay","MdiIcon","CampaignPicker","useSnackbar","useOrgCampaigns","useUser","Alert","Autocomplete","Box","Button","Chip","FormControlLabel","Grid","Menu","MenuItem","Stack","Switch","TextField","Typography","useEffect","useMemo","useState","isInThreadEmailStep","OUTREACH_SEQUENCE_NAME_MAX","validateOutreachSequence","outreachCountryLabel","OUTREACH_MAX_EMAIL_STEPS","OUTREACH_MAX_STEPS","OUTREACH_TASK_KIND_LABELS","OUTREACH_TASK_KINDS","emptyOutreachSequenceDraft","newOutreachEmailStep","newOutreachTaskStep","outreachSequenceDraftOf","OutreachSendWindowFields","OutreachSequenceMailboxPicker","OutreachSequencePreview","OutreachStepCard","OutreachStepTestDialog","OutreachRouteError","useOutreachApi","useOutreachEmailTemplates","stepIssueKey","path","match","exec","field","index","Number","OutreachSequenceEditor","props","issueAt","settings","issues","hosts","orgId","orgMount","sequence","api","enqueueSnackbar","data","user","uid","mountedHosts","orgCountries","allowedCountries","draft","setDraft","hostId","length","id","attempted","setAttempted","serverIssues","setServerIssues","saving","setSaving","taskAnchor","setTaskAnchor","testing","setTesting","settingsCountries","join","previous","split","ownSending","mailboxes","filter","entry","connectedByUid","status","onlyOwnMailbox","mailboxId","templates","mailbox","find","archived","campaigns","enabled","judged","issue","severity","includes","message","stepIssues","found","key","listIssue","countriesIssue","startsWith","update","next","setStep","step","steps","map","position","moveStep","direction","moved","splice","emailCount","kind","full","threadSubject","subject","save","some","answer","saveSequence","created","variant","onSaved","error","allowDuplicate","siteName","host","name","countryOptions","Set","unsaved","JSON","stringify","sendTestDisabledReason","container","spacing","size","xs","md","header","help","anchor","HeaderProps","action","undefined","onCancel","onClick","disabled","contentGutterX","contentGutterY","label","value","onChange","event","target","Boolean","helperText","slotProps","htmlInput","maxLength","fullWidth","select","mailboxesPath","options","campaignIds","empty","ready","emptyText","count","firstEmail","findIndex","inThread","onMove","onRemove","_entry","onSendTest","sx","flexWrap","rowGap","startIcon","fontSize","currentTarget","aria-haspopup","anchorEl","open","onClose","color","excerpt","multiple","getOptionLabel","_event","renderValue","getItemProps","code","item","renderInput","params","control","checked","allowCustomers","trackClicks","countOpens","listUnsubscribe","window","daysIssue","hoursIssue","stepIndex","contactGroupId","org","groupId","selfEmail","email","top","orgSettings","orgSettingsStatus","sender","displayName","sendAs"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequence-editor.tsx"],"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'use client'\n\nimport { pluginDocsHelp, type ConsolePluginOrgMount } from '@aglyn/aglyn'\nimport { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport CampaignPicker from '@aglyn/shared-ui-email-campaigns/components/campaign-picker.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useOrgCampaigns, useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Autocomplete,\n Box,\n Button,\n Chip,\n FormControlLabel,\n Grid,\n Menu,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { memo, useCallback, useEffect, useMemo, useState } from 'react'\nimport {\n isInThreadEmailStep,\n OUTREACH_SEQUENCE_NAME_MAX,\n validateOutreachSequence,\n} from '../engine/sequence-validation'\nimport { outreachCountryLabel } from '../model/compliance-settings'\nimport {\n OUTREACH_MAX_EMAIL_STEPS,\n OUTREACH_MAX_STEPS,\n OUTREACH_TASK_KIND_LABELS,\n OUTREACH_TASK_KINDS,\n type OutreachSequence,\n type OutreachSequenceStep,\n type OutreachTaskKind,\n} from '../model/outreach.types'\nimport {\n emptyOutreachSequenceDraft,\n newOutreachEmailStep,\n newOutreachTaskStep,\n outreachSequenceDraftOf,\n type OutreachSequenceDraft,\n type OutreachSequenceIssue,\n} from '../model/sequence-draft'\nimport { OutreachSendWindowFields } from './send-window-fields'\nimport { OutreachSequenceMailboxPicker } from './sequence-mailbox-picker'\nimport { OutreachSequencePreview } from './sequence-preview'\nimport { OutreachStepCard } from './sequence-step-card'\nimport { OutreachStepTestDialog } from './step-test-dialog'\nimport { OutreachRouteError, useOutreachApi } from './use-outreach-api'\nimport { useOutreachEmailTemplates } from './use-outreach-crm'\nimport type { OutreachMailboxesResult } from './use-outreach-mailboxes'\nimport type { OutreachSettingsLoad } from './use-outreach-settings'\n\nexport interface OutreachSequenceEditorProps {\n orgId: string\n orgMount?: ConsolePluginOrgMount\n /** The organization document, for the consent group a test send's person picker searches (AGL-3325). */\n org?: Record<string, unknown> | null\n /** The stored sequence, or `null` for a new one. */\n sequence: OutreachSequence | null\n /** The organization's compliance settings: its countries, and the preview's footer. */\n settings: OutreachSettingsLoad\n /**\n * The organization's mailboxes, from the Mailboxes section's listener:\n * what the picker offers, and who the preview's emails are from.\n */\n mailboxes: OutreachMailboxesResult\n /** The Mailboxes section, where the picker sends a member to connect one. */\n mailboxesPath: string\n onSaved(sequence: OutreachSequence): void\n onCancel?(): void\n}\n\n/** The step a dotted issue path is about, and the field within it. */\nfunction stepIssueKey(path: string): { index: number; field: string } | null {\n const match = /^steps\\.(\\d+)(?:\\.(.+))?$/.exec(path)\n if (!match) return null\n const field = match[2] ?? 'step'\n return { index: Number(match[1]), field: field === 'id' ? 'step' : field }\n}\n\ninterface OutreachCountriesFieldProps {\n options: readonly string[]\n value: readonly string[]\n onChange(next: string[]): void\n issue?: string\n disabled: boolean\n}\n\n/**\n * The countries a sequence sends to, as chips in an Autocomplete.\n *\n * MEMOIZED, AND EVERY PROP IT TAKES IS STABLE WHILE THE COUNTRIES ARE\n * (AGL-3423). A multiple Autocomplete hands its input a new chip array as\n * `startAdornment` on every render, and MUI's `InputBase` copies that into\n * its `FormControl` from a passive effect — a state update after every\n * commit this field takes part in. React 19 counts a commit that leaves such\n * an update pending as a nested update, and keystrokes the browser delivers\n * back to back (a held key, fast typing on a busy page) commit one after\n * another with nothing in between to clear the count: fifty-one of them and\n * the next keystroke's own `setState` throws \"Maximum update depth exceeded\"\n * (minified error #185), which is what typing a step's body reported. Kept\n * out of the editor's other renders, the field commits only when the\n * countries, their options or their issue change.\n */\nconst OutreachCountriesField = memo(function OutreachCountriesField(\n props: OutreachCountriesFieldProps,\n) {\n return (\n <Autocomplete<string, true>\n multiple\n options={props.options}\n value={props.value as string[]}\n getOptionLabel={outreachCountryLabel}\n onChange={(_event, next) => props.onChange(next)}\n disabled={props.disabled}\n renderValue={(value, getItemProps) =>\n value.map((code, index) => {\n const { key, ...item } = getItemProps({ index })\n return (\n <Chip\n key={key}\n size=\"small\"\n label={outreachCountryLabel(code)}\n {...item}\n />\n )\n })\n }\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Countries\"\n error={Boolean(props.issue)}\n helperText={\n props.issue ??\n 'Among the countries your organization allows. Cold contacts are emailed only in the United States.'\n }\n />\n )}\n />\n )\n})\n\n/**\n * The sequence editor (AGL-2980): the name, the site and mailbox, the steps,\n * who it sends to and when, beside a live preview of each email.\n *\n * Every issue the engine's validator finds is shown beside its field — the\n * same validator the save route refuses with — errors once a save was\n * attempted, warnings as soon as they appear. What only the server can\n * judge (whose mailbox, which countries the organization allows, what may\n * no longer change once people are enrolled) comes back from the save and\n * is shown in the same places.\n */\nexport function OutreachSequenceEditor(props: OutreachSequenceEditorProps) {\n const { orgId, orgMount, sequence, settings } = props\n const api = useOutreachApi(orgId)\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n const uid = user?.uid ?? null\n const mountedHosts = orgMount?.hosts\n const hosts = useMemo(() => mountedHosts ?? [], [mountedHosts])\n const allowedCountries = settings.settings?.allowedCountries\n const orgCountries = useMemo(\n () => allowedCountries ?? ['US'],\n [allowedCountries],\n )\n\n const [draft, setDraft] = useState<OutreachSequenceDraft>(() =>\n sequence\n ? outreachSequenceDraftOf(sequence)\n : emptyOutreachSequenceDraft({\n hostId: hosts.length === 1 ? hosts[0].id : '',\n allowedCountries: orgCountries,\n }),\n )\n const [attempted, setAttempted] = useState(false)\n const [serverIssues, setServerIssues] = useState<OutreachSequenceIssue[]>([])\n const [saving, setSaving] = useState(false)\n const [taskAnchor, setTaskAnchor] = useState<HTMLElement | null>(null)\n // The step whose test-send dialog is open (AGL-3325), by index.\n const [testing, setTesting] = useState<number | null>(null)\n\n // A stored sequence the listener updated (a save, a status change) is the\n // new starting point.\n useEffect(() => {\n if (sequence) setDraft(outreachSequenceDraftOf(sequence))\n }, [sequence])\n // A new sequence starts on the organization's countries once they are read.\n const settingsCountries = settings.settings?.allowedCountries.join(',')\n useEffect(() => {\n if (!sequence && settingsCountries) {\n setDraft((previous) => ({\n ...previous,\n settings: {\n ...previous.settings,\n allowedCountries: settingsCountries.split(','),\n },\n }))\n }\n }, [sequence, settingsCountries])\n useEffect(() => {\n if (!sequence && !draft.hostId && hosts.length === 1)\n setDraft((previous) => ({ ...previous, hostId: hosts[0].id }))\n }, [sequence, draft.hostId, hosts])\n\n // A new sequence starts on the member's own mailbox when they have exactly\n // one that is sending.\n const ownSending = props.mailboxes.mailboxes.filter(\n (entry) => entry.connectedByUid === uid && entry.status === 'connected',\n )\n const onlyOwnMailbox = ownSending.length === 1 ? ownSending[0].id : ''\n useEffect(() => {\n if (!sequence && !draft.mailboxId && onlyOwnMailbox)\n setDraft((previous) => ({ ...previous, mailboxId: onlyOwnMailbox }))\n }, [sequence, draft.mailboxId, onlyOwnMailbox])\n\n const templates = useOutreachEmailTemplates(orgId, draft.hostId || null, uid)\n const mailbox =\n props.mailboxes.mailboxes.find((entry) => entry.id === draft.mailboxId) ??\n null\n const archived = sequence?.status === 'archived'\n /*\n * The org's campaigns, for the picker (AGL-3254): read while the editor\n * is open. A sequence is the organization's record and so is a campaign,\n * so the choice is every campaign in the org, whichever site the\n * sequence sends as.\n */\n const campaigns = useOrgCampaigns(orgId, { enabled: true })\n\n const issues = useMemo(() => {\n const judged = [\n ...validateOutreachSequence(draft),\n ...serverIssues,\n ] as OutreachSequenceIssue[]\n return judged.filter(\n (issue) =>\n issue.severity === 'warning' ||\n attempted ||\n serverIssues.includes(issue),\n )\n }, [draft, serverIssues, attempted])\n const issueAt = (path: string) =>\n issues.find((issue) => issue.path === path)?.message\n const stepIssues = (index: number) => {\n const found: Record<string, string> = {}\n for (const issue of issues) {\n const key = stepIssueKey(issue.path)\n if (key?.index === index && !found[key.field])\n found[key.field] = issue.message\n }\n return found\n }\n const listIssue = issueAt('steps')\n const countriesIssue = issues.find((issue) =>\n issue.path.startsWith('settings.allowedCountries'),\n )?.message\n\n // An edit clears what the last save refused. The empty list stays the same\n // list, so an edit with nothing to clear does not change that state.\n const clearServerIssues = useCallback(\n () => setServerIssues((previous) => (previous.length ? [] : previous)),\n [],\n )\n const update = (next: Partial<OutreachSequenceDraft>) => {\n clearServerIssues()\n setDraft((previous) => ({ ...previous, ...next }))\n }\n // Stable, for the memoized countries field: it reads the settings it\n // replaces from the draft it is given rather than from this render's.\n const setCountries = useCallback(\n (next: string[]) => {\n clearServerIssues()\n setDraft((previous) => ({\n ...previous,\n settings: { ...previous.settings, allowedCountries: next },\n }))\n },\n [clearServerIssues],\n )\n const setStep = (index: number, step: OutreachSequenceStep) =>\n update({\n steps: draft.steps.map((entry, position) =>\n position === index ? step : entry,\n ),\n })\n const moveStep = (index: number, direction: -1 | 1) => {\n const steps = [...draft.steps]\n const [moved] = steps.splice(index, 1)\n steps.splice(index + direction, 0, moved)\n update({ steps })\n }\n const emailCount = draft.steps.filter((step) => step.kind === 'email').length\n const full = draft.steps.length >= OUTREACH_MAX_STEPS\n const threadSubject = (index: number) => {\n for (let position = index - 1; position >= 0; position -= 1) {\n const step = draft.steps[position]\n if (step.kind === 'email' && !isInThreadEmailStep(draft.steps, position))\n return step.subject\n }\n return ''\n }\n\n const save = async () => {\n setAttempted(true)\n if (\n validateOutreachSequence(draft).some(\n (issue) => issue.severity === 'error',\n )\n )\n return\n setSaving(true)\n try {\n const answer = await api.saveSequence(sequence?.id ?? null, draft)\n setServerIssues([])\n setAttempted(false)\n enqueueSnackbar(\n answer.created ? 'Sequence created.' : 'Sequence saved.',\n { variant: 'success' },\n )\n props.onSaved(answer.sequence)\n } catch (error) {\n if (error instanceof OutreachRouteError && error.issues.length) {\n setServerIssues(\n error.issues.filter(\n (issue): issue is OutreachSequenceIssue => 'path' in issue,\n ),\n )\n }\n enqueueSnackbar((error as Error).message, {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setSaving(false)\n }\n }\n\n const siteName = hosts.find((host) => host.id === draft.hostId)?.name ?? ''\n const draftCountries = draft.settings.allowedCountries\n const countryOptions = useMemo(\n () => [...new Set([...orgCountries, ...draftCountries])],\n [orgCountries, draftCountries],\n )\n /*\n * A test sends the step as STORED (AGL-3325): the route reads the saved\n * sequence, so a new one has nothing to send yet and an edited one sends\n * its last save — said on the button and in the dialog rather than found\n * out in the inbox.\n */\n const unsaved =\n sequence !== null &&\n JSON.stringify(draft) !== JSON.stringify(outreachSequenceDraftOf(sequence))\n const sendTestDisabledReason = !sequence\n ? 'Create the sequence to send a test of it.'\n : !draft.mailboxId\n ? 'Choose a mailbox to send the test from.'\n : null\n\n return (\n <Grid container spacing={2}>\n <Grid size={{ xs: 12, md: 7 }}>\n <Stack spacing={2}>\n {archived ? (\n <Alert severity=\"info\">\n This sequence is archived, so it can’t be edited.\n </Alert>\n ) : null}\n <CardDisplay\n header=\"Sequence\"\n help={pluginDocsHelp('sequences', { anchor: '#build-a-sequence' })}\n // The whole sequence (steps and sending rules included) saves\n // from its first card's header, where the console puts a card's\n // actions (AGL-3333).\n HeaderProps={{\n action: archived ? undefined : (\n <Stack direction=\"row\" spacing={1}>\n {props.onCancel ? (\n <Button\n size=\"small\"\n variant=\"text\"\n onClick={props.onCancel}\n disabled={saving}\n >\n Cancel\n </Button>\n ) : null}\n <Button\n size=\"small\"\n variant=\"contained\"\n onClick={() => void save()}\n disabled={saving}\n >\n {saving ? 'Saving…' : sequence ? 'Save' : 'Create sequence'}\n </Button>\n </Stack>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <TextField\n label=\"Name\"\n value={draft.name}\n onChange={(event) => update({ name: event.target.value })}\n disabled={archived}\n error={Boolean(issueAt('name'))}\n helperText={issueAt('name')}\n slotProps={{\n htmlInput: { maxLength: OUTREACH_SEQUENCE_NAME_MAX + 20 },\n }}\n fullWidth\n />\n <TextField\n select\n label=\"Site\"\n value={\n hosts.some((host) => host.id === draft.hostId)\n ? draft.hostId\n : ''\n }\n onChange={(event) => update({ hostId: event.target.value })}\n disabled={archived || !hosts.length}\n error={Boolean(issueAt('hostId'))}\n helperText={\n issueAt('hostId') ??\n 'The site whose contacts this sequence emails.'\n }\n fullWidth\n >\n {hosts.map((host) => (\n <MenuItem key={host.id} value={host.id}>\n {host.name}\n </MenuItem>\n ))}\n </TextField>\n <OutreachSequenceMailboxPicker\n orgId={orgId}\n uid={uid}\n mailboxes={props.mailboxes}\n mailboxesPath={props.mailboxesPath}\n value={draft.mailboxId}\n onChange={(mailboxId) => update({ mailboxId })}\n error={issueAt('mailboxId')}\n disabled={archived}\n />\n {/*\n The campaigns this sequence is part of (AGL-3254), picked the\n way a form's page picks them. Everyone enrolled joins them,\n and what the sequence produces is reported under them; it\n does not change who is enrolled or what is sent.\n */}\n <CampaignPicker\n options={campaigns.options}\n value={draft.campaignIds}\n onChange={(campaignIds) => update({ campaignIds })}\n label=\"Campaigns\"\n helperText={\n issueAt('campaignIds') ??\n 'The campaigns this sequence is part of. Everyone enrolled joins them, and its sends, replies, meetings and conversions count on their pages.'\n }\n disabled={archived}\n empty={campaigns.ready && !campaigns.options.length}\n emptyText=\"There are no campaigns yet. Create one from Marketing to file this sequence under it.\"\n />\n </Stack>\n </CardDisplay>\n\n {listIssue ? <Alert severity=\"error\">{listIssue}</Alert> : null}\n {draft.steps.map((step, index) => (\n <OutreachStepCard\n key={step.id || `step-${index}`}\n step={step}\n index={index}\n count={draft.steps.length}\n firstEmail={\n draft.steps.findIndex((entry) => entry.kind === 'email') ===\n index\n }\n inThread={isInThreadEmailStep(draft.steps, index)}\n threadSubject={threadSubject(index)}\n templates={templates.data}\n issues={stepIssues(index)}\n disabled={archived}\n onChange={(next) => setStep(index, next)}\n onMove={(direction) => moveStep(index, direction)}\n onRemove={() =>\n update({\n steps: draft.steps.filter(\n (_entry, position) => position !== index,\n ),\n })\n }\n onSendTest={\n step.kind === 'email' && !archived\n ? () => setTesting(index)\n : undefined\n }\n sendTestDisabledReason={sendTestDisabledReason}\n />\n ))}\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ flexWrap: 'wrap', rowGap: 1 }}\n >\n <Button\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n disabled={\n archived || full || emailCount >= OUTREACH_MAX_EMAIL_STEPS\n }\n onClick={() =>\n update({\n steps: [...draft.steps, newOutreachEmailStep(draft.steps)],\n })\n }\n >\n Add email\n </Button>\n <Button\n variant=\"outlined\"\n startIcon={<MdiIcon path={mdiPlus.path} fontSize=\"small\" />}\n disabled={archived || full}\n onClick={(event) => setTaskAnchor(event.currentTarget)}\n aria-haspopup=\"menu\"\n >\n Add task\n </Button>\n <Menu\n anchorEl={taskAnchor}\n open={Boolean(taskAnchor)}\n onClose={() => setTaskAnchor(null)}\n >\n {OUTREACH_TASK_KINDS.map((kind: OutreachTaskKind) => (\n <MenuItem\n key={kind}\n onClick={() => {\n setTaskAnchor(null)\n update({\n steps: [\n ...draft.steps,\n newOutreachTaskStep(draft.steps, kind),\n ],\n })\n }}\n >\n {OUTREACH_TASK_KIND_LABELS[kind]}\n </MenuItem>\n ))}\n </Menu>\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Up to ${OUTREACH_MAX_STEPS} steps, ${OUTREACH_MAX_EMAIL_STEPS} of them emails.`}\n </Typography>\n\n <CardDisplay\n header=\"Who it sends to, and when\"\n help={pluginDocsHelp('sequences', {\n anchor: '#build-a-sequence',\n excerpt:\n 'A sequence sends only to the countries both it and your organization allow, and in its mailbox’s sending hours unless it has its own.',\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <OutreachCountriesField\n options={countryOptions}\n value={draftCountries}\n onChange={setCountries}\n issue={countriesIssue}\n disabled={archived}\n />\n <FormControlLabel\n control={\n <Switch\n checked={draft.settings.allowCustomers}\n onChange={(event) =>\n update({\n settings: {\n ...draft.settings,\n allowCustomers: event.target.checked,\n },\n })\n }\n disabled={archived}\n />\n }\n label=\"Include customers\"\n />\n {/*\n * Click tracking (AGL-3239), with its cost stated where it is\n * chosen rather than in a document nobody opens. Off by\n * default: it changes the link a cold recipient sees. The\n * emails stay plain text either way; opens are the separate\n * switch below.\n */}\n <Stack spacing={0}>\n <FormControlLabel\n control={\n <Switch\n checked={draft.settings.trackClicks}\n onChange={(event) =>\n update({\n settings: {\n ...draft.settings,\n trackClicks: event.target.checked,\n },\n })\n }\n disabled={archived}\n />\n }\n label=\"Count link clicks\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n Links in these emails are replaced with links of ours that\n forward to the same page, so clicks can be counted. The\n recipient sees the replacement, not your address. The\n emails stay plain text.\n </Typography>\n </Stack>\n {/*\n * Open counting (AGL-3395), off by default, its cost stated\n * where it is chosen: an HTML copy with a tracking image is the\n * least one-to-one thing a sequence email can carry, so it is\n * for measuring a slice, not for every sequence.\n */}\n <Stack spacing={0}>\n <FormControlLabel\n control={\n <Switch\n checked={draft.settings.countOpens}\n onChange={(event) =>\n update({\n settings: {\n ...draft.settings,\n countOpens: event.target.checked,\n },\n })\n }\n disabled={archived}\n />\n }\n label=\"Count opens\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n Each email also goes out as HTML — the same words — with a\n tiny tracking image, so opens can be counted. The plain-text\n version is unchanged. It makes the email look less like one\n written by hand, and some spam filters score HTML with a\n remote image against you, so turn it on to measure a slice\n of your sends rather than for every sequence. Gmail opens\n count; Apple Mail loads every image on arrival, so its loads\n are counted separately.\n </Typography>\n </Stack>\n {/*\n * The mail-client unsubscribe button (AGL-3296), off by\n * default: the header is what makes Apple Mail, Gmail and\n * Outlook present a one-to-one email as a mailing list. The\n * footer's \"reply 'no'\" line is the way out either way.\n */}\n <Stack spacing={0}>\n <FormControlLabel\n control={\n <Switch\n checked={draft.settings.listUnsubscribe}\n onChange={(event) =>\n update({\n settings: {\n ...draft.settings,\n listUnsubscribe: event.target.checked,\n },\n })\n }\n disabled={archived}\n />\n }\n label=\"Add a mail-client unsubscribe button\"\n />\n <Typography variant=\"caption\" color=\"text.secondary\">\n Adds the List-Unsubscribe header, so the recipient’s mail\n app shows its own Unsubscribe button. It’s the easiest way\n out for them, and fewer people mark you as spam when they\n have it, but mail apps then label the email as coming from\n a mailing list. Off, the footer’s “reply ‘no’” line is the\n way out, and it’s always there.\n </Typography>\n </Stack>\n <OutreachSendWindowFields\n window={draft.settings.window}\n onChange={(window) =>\n update({ settings: { ...draft.settings, window } })\n }\n daysIssue={issueAt('settings.window.days')}\n hoursIssue={issueAt('settings.window')}\n disabled={archived}\n />\n </Stack>\n </CardDisplay>\n\n </Stack>\n </Grid>\n {sequence && testing !== null ? (\n <OutreachStepTestDialog\n open\n onClose={() => setTesting(null)}\n orgId={orgId}\n sequence={sequence}\n stepIndex={testing}\n contactGroupId={\n sequence.hostId\n ? consentGroupForHost(props.org ?? null, sequence.hostId).groupId\n : ''\n }\n selfEmail={user?.email ?? null}\n unsaved={unsaved}\n api={api}\n />\n ) : null}\n <Grid size={{ xs: 12, md: 5 }}>\n <Box sx={{ position: { md: 'sticky' }, top: { md: 16 } }}>\n <OutreachSequencePreview\n steps={draft.steps}\n orgSettings={settings.settings}\n orgSettingsStatus={settings.status}\n templates={templates.data}\n sender={\n mailbox\n ? {\n name: mailbox.displayName,\n email: mailbox.sendAs || mailbox.email,\n }\n : null\n }\n siteName={siteName}\n />\n </Box>\n </Grid>\n </Grid>\n )\n}\nOutreachSequenceEditor.displayName = 'OutreachSequenceEditor'\n\nexport default OutreachSequenceEditor\n"],"names":["pluginDocsHelp","consentGroupForHost","mdiPlus","CardDisplay","MdiIcon","CampaignPicker","useSnackbar","useOrgCampaigns","useUser","Alert","Autocomplete","Box","Button","Chip","FormControlLabel","Grid","Menu","MenuItem","Stack","Switch","TextField","Typography","memo","useCallback","useEffect","useMemo","useState","isInThreadEmailStep","OUTREACH_SEQUENCE_NAME_MAX","validateOutreachSequence","outreachCountryLabel","OUTREACH_MAX_EMAIL_STEPS","OUTREACH_MAX_STEPS","OUTREACH_TASK_KIND_LABELS","OUTREACH_TASK_KINDS","emptyOutreachSequenceDraft","newOutreachEmailStep","newOutreachTaskStep","outreachSequenceDraftOf","OutreachSendWindowFields","OutreachSequenceMailboxPicker","OutreachSequencePreview","OutreachStepCard","OutreachStepTestDialog","OutreachRouteError","useOutreachApi","useOutreachEmailTemplates","stepIssueKey","path","match","exec","field","index","Number","OutreachCountriesField","props","multiple","options","value","getOptionLabel","onChange","_event","next","disabled","renderValue","getItemProps","map","code","key","item","size","label","renderInput","params","error","Boolean","issue","helperText","OutreachSequenceEditor","issueAt","settings","issues","hosts","orgId","orgMount","sequence","api","enqueueSnackbar","data","user","uid","mountedHosts","allowedCountries","orgCountries","draft","setDraft","hostId","length","id","attempted","setAttempted","serverIssues","setServerIssues","saving","setSaving","taskAnchor","setTaskAnchor","testing","setTesting","settingsCountries","join","previous","split","ownSending","mailboxes","filter","entry","connectedByUid","status","onlyOwnMailbox","mailboxId","templates","mailbox","find","archived","campaigns","enabled","judged","severity","includes","message","stepIssues","found","listIssue","countriesIssue","startsWith","clearServerIssues","update","setCountries","setStep","step","steps","position","moveStep","direction","moved","splice","emailCount","kind","full","threadSubject","subject","save","some","answer","saveSequence","created","variant","onSaved","allowDuplicate","siteName","host","name","draftCountries","countryOptions","Set","unsaved","JSON","stringify","sendTestDisabledReason","container","spacing","xs","md","header","help","anchor","HeaderProps","action","undefined","onCancel","onClick","contentGutterX","contentGutterY","event","target","slotProps","htmlInput","maxLength","fullWidth","select","mailboxesPath","campaignIds","empty","ready","emptyText","count","firstEmail","findIndex","inThread","onMove","onRemove","_entry","onSendTest","sx","flexWrap","rowGap","startIcon","fontSize","currentTarget","aria-haspopup","anchorEl","open","onClose","color","excerpt","control","checked","allowCustomers","trackClicks","countOpens","listUnsubscribe","window","daysIssue","hoursIssue","stepIndex","contactGroupId","org","groupId","selfEmail","email","top","orgSettings","orgSettingsStatus","sender","displayName","sendAs"],"mappings":"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"}
@@ -24,7 +24,8 @@ export interface OutreachApi {
24
24
  ok: true;
25
25
  }>;
26
26
  previewEnrollment(sequenceId: string, source: OutreachEnrollSource): Promise<OutreachEnrollPreviewResponse>;
27
- enroll(sequenceId: string, people: OutreachEnrollPersonRequest[]): Promise<OutreachEnrollResponse>;
27
+ /** `startStepIndex` (AGL-3228) is the step everyone begins at, zero-based; the first when absent. */
28
+ enroll(sequenceId: string, people: OutreachEnrollPersonRequest[], startStepIndex?: number): Promise<OutreachEnrollResponse>;
28
29
  actOnEnrollment(enrollmentId: string, action: OutreachEnrollmentAction, detail?: string): Promise<OutreachEnrollmentActionResponse>;
29
30
  /** The mail gateway in front of one enrolled person's domain (AGL-3332). */
30
31
  readEnrollmentGateway(enrollmentId: string): Promise<OutreachEnrollmentGatewayResponse>;
@@ -111,12 +111,14 @@ import { OUTREACH_API_ROUTES } from "../constants/api-routes.js";
111
111
  source
112
112
  }
113
113
  }),
114
- enroll: (sequenceId, people)=>call(OUTREACH_API_ROUTES.enroll, {
114
+ enroll: (sequenceId, people, startStepIndex)=>call(OUTREACH_API_ROUTES.enroll, {
115
115
  method: 'POST',
116
- body: {
116
+ body: _extends({
117
117
  sequenceId,
118
118
  people
119
- }
119
+ }, startStepIndex ? {
120
+ startStepIndex
121
+ } : {})
120
122
  }),
121
123
  actOnEnrollment: (enrollmentId, action, detail)=>call(OUTREACH_API_ROUTES.enrollmentsAction, {
122
124
  method: 'POST',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-api.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'use client'\n\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { useCallback, useMemo } from 'react'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport type { OutreachComplianceIssue } from '../model/compliance-settings'\nimport type {\n OutreachCurateDraftRequest,\n OutreachCurateDraftResponse,\n OutreachCurateSaveRequest,\n OutreachCurateSaveResponse,\n OutreachDoNotContactDomainRequest,\n OutreachDoNotContactDomainResponse,\n OutreachDoNotContactDomainsResponse,\n OutreachEnrollmentAction,\n OutreachEnrollmentActionResponse,\n OutreachEnrollmentGatewayResponse,\n OutreachEnrollPersonRequest,\n OutreachEnrollPreviewResponse,\n OutreachEnrollResponse,\n OutreachEnrollSource,\n OutreachLinkDomainRequest,\n OutreachLinkDomainResponse,\n OutreachLinkDomainsResponse,\n OutreachPreviewRequest,\n OutreachPreviewResponse,\n OutreachRouteRefusal,\n OutreachRouteRefusalReason,\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n OutreachSettingsResponse,\n OutreachSettingsSaveRequest,\n OutreachSettingsSaveResponse,\n OutreachStepTestRequest,\n OutreachStepTestResponse,\n} from '../model/outreach-api'\nimport type {\n OutreachSequenceDraft,\n OutreachSequenceIssue,\n} from '../model/sequence-draft'\n\n/**\n * A route's refusal, carrying its sentence, its stable reason and any\n * field-level issues. `refused` is the one a page shows as \"you can't use\n * this here\" rather than as something that went wrong: no permission, no\n * entitlement, or the release gate answering as if the route did not exist.\n */\nexport class OutreachRouteError extends Error {\n readonly reason: OutreachRouteRefusalReason | 'unreachable' | 'not-found'\n readonly status: number\n readonly issues: ReadonlyArray<\n OutreachComplianceIssue | OutreachSequenceIssue\n >\n\n constructor(\n message: string,\n reason: OutreachRouteError['reason'],\n status: number,\n issues: OutreachRouteError['issues'] = [],\n ) {\n super(message)\n this.name = 'OutreachRouteError'\n this.reason = reason\n this.status = status\n this.issues = issues\n }\n\n get refused(): boolean {\n return (\n this.reason === 'permission' ||\n this.reason === 'entitlement' ||\n this.reason === 'not-org-wide' ||\n this.reason === 'not-a-member' ||\n // The dispatcher answers a route whose plugin is not released to this\n // workspace as though it did not exist.\n this.reason === 'not-found'\n )\n }\n}\n\n/** The routes, as the Outreach pages call them. */\nexport interface OutreachApi {\n readSettings(): Promise<OutreachSettingsResponse>\n saveSettings(\n input: Omit<OutreachSettingsSaveRequest, 'orgId'>,\n ): Promise<OutreachSettingsSaveResponse>\n saveSequence(\n sequenceId: string | null,\n sequence: OutreachSequenceDraft,\n ): Promise<OutreachSequenceSaveResponse>\n setSequenceStatus(\n sequenceId: string,\n action: OutreachSequenceAction,\n ): Promise<OutreachSequenceStatusResponse>\n deleteSequence(sequenceId: string): Promise<{ ok: true }>\n previewEnrollment(\n sequenceId: string,\n source: OutreachEnrollSource,\n ): Promise<OutreachEnrollPreviewResponse>\n enroll(\n sequenceId: string,\n people: OutreachEnrollPersonRequest[],\n ): Promise<OutreachEnrollResponse>\n actOnEnrollment(\n enrollmentId: string,\n action: OutreachEnrollmentAction,\n detail?: string,\n ): Promise<OutreachEnrollmentActionResponse>\n /** The mail gateway in front of one enrolled person's domain (AGL-3332). */\n readEnrollmentGateway(enrollmentId: string): Promise<OutreachEnrollmentGatewayResponse>\n previewEmail(\n input: Omit<OutreachPreviewRequest, 'orgId'>,\n ): Promise<OutreachPreviewResponse>\n /**\n * Sends one step of a saved sequence to the member as a test (AGL-3325):\n * the preview's email, through the sequence's mailbox, `[Test]` in front.\n */\n sendStepTest(\n input: Omit<OutreachStepTestRequest, 'orgId'>,\n ): Promise<OutreachStepTestResponse & { unresolvedFields?: string[] }>\n /**\n * One person's own copies of the sequence's email steps, drafted by the\n * workspace's AI (AGL-3324); nothing is stored until the member confirms.\n */\n curateDrafts(\n input: Omit<OutreachCurateDraftRequest, 'orgId'>,\n ): Promise<OutreachCurateDraftResponse>\n /** Stores a confirmed copy of one step on an enrollment, or clears it with `null`. */\n saveCuratedStep(\n enrollmentId: string,\n stepIndex: number,\n override: OutreachCurateSaveRequest['override'],\n ): Promise<OutreachCurateSaveResponse>\n /** The domains on the do-not-contact list (AGL-3244). */\n readDoNotContactDomains(): Promise<OutreachDoNotContactDomainsResponse>\n /** Puts a domain on the list, or takes one off. */\n changeDoNotContactDomain(\n action: OutreachDoNotContactDomainRequest['action'],\n domain: string,\n detail?: string,\n ): Promise<OutreachDoNotContactDomainResponse>\n /** Each mailbox domain's click-tracking host (AGL-3306). */\n readLinkDomains(): Promise<OutreachLinkDomainsResponse>\n /** Sets a link domain up, checks it, or removes it. */\n changeLinkDomain(\n action: OutreachLinkDomainRequest['action'],\n domain: string,\n ): Promise<OutreachLinkDomainResponse>\n}\n\n/**\n * The Outreach pages' one door to the settings, sequence and enrollment\n * routes (AGL-2980), with the member's session attached and the\n * organization named on every call. A refusal is thrown as an\n * {@link OutreachRouteError} carrying the route's own sentence, which is\n * written for the person reading it.\n */\nexport function useOutreachApi(orgId: string | null): OutreachApi {\n const { data: user } = useUser()\n\n const call = useCallback(\n async <T>(\n route: string,\n init: { method: 'GET' | 'POST'; body?: Record<string, unknown> },\n ): Promise<T> => {\n if (!orgId)\n throw new OutreachRouteError(\n 'Open an organization first.',\n 'org-required',\n 400,\n )\n const path =\n init.method === 'GET'\n ? `/api/${route}?orgId=${encodeURIComponent(orgId)}`\n : `/api/${route}`\n let response: Response\n try {\n response = await authorizedFetch(user, path, {\n method: init.method,\n ...(init.method === 'POST'\n ? {\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ orgId, ...init.body }),\n }\n : {}),\n })\n } catch {\n throw new OutreachRouteError(\n 'Sequences could not be reached. Try again.',\n 'unreachable',\n 0,\n )\n }\n const payload = (await response.json().catch(() => null)) as\n (T & { error?: undefined }) | Partial<OutreachRouteRefusal> | null\n if (!response.ok || !payload) {\n const refusal = (payload ?? {}) as Partial<OutreachRouteRefusal>\n throw new OutreachRouteError(\n refusal.error && refusal.reason\n ? refusal.error\n : response.status === 404\n ? \"Sequences isn't available to this workspace yet.\"\n : 'Sequences could not be reached. Try again.',\n refusal.reason ??\n (response.status === 404 ? 'not-found' : 'unreachable'),\n response.status,\n refusal.issues ?? [],\n )\n }\n return payload as T\n },\n [user, orgId],\n )\n\n return useMemo<OutreachApi>(\n () => ({\n readSettings: () => call(OUTREACH_API_ROUTES.settings, { method: 'GET' }),\n saveSettings: (input) =>\n call(OUTREACH_API_ROUTES.settings, {\n method: 'POST',\n body: { ...input },\n }),\n saveSequence: (sequenceId, sequence) =>\n call(OUTREACH_API_ROUTES.sequencesSave, {\n method: 'POST',\n body: { ...(sequenceId ? { sequenceId } : {}), sequence },\n }),\n setSequenceStatus: (sequenceId, action) =>\n call(OUTREACH_API_ROUTES.sequencesStatus, {\n method: 'POST',\n body: { sequenceId, action },\n }),\n deleteSequence: (sequenceId) =>\n call(OUTREACH_API_ROUTES.sequencesDelete, {\n method: 'POST',\n body: { sequenceId },\n }),\n previewEnrollment: (sequenceId, source) =>\n call(OUTREACH_API_ROUTES.enrollPreview, {\n method: 'POST',\n body: { sequenceId, source },\n }),\n enroll: (sequenceId, people) =>\n call(OUTREACH_API_ROUTES.enroll, {\n method: 'POST',\n body: { sequenceId, people },\n }),\n actOnEnrollment: (enrollmentId, action, detail) =>\n call(OUTREACH_API_ROUTES.enrollmentsAction, {\n method: 'POST',\n body: { enrollmentId, action, ...(detail ? { detail } : {}) },\n }),\n readEnrollmentGateway: (enrollmentId) =>\n call(OUTREACH_API_ROUTES.enrollmentsGateway, {\n method: 'POST',\n body: { enrollmentId },\n }),\n previewEmail: (input) =>\n call(OUTREACH_API_ROUTES.preview, {\n method: 'POST',\n body: { ...input },\n }),\n sendStepTest: (input) =>\n call(OUTREACH_API_ROUTES.stepTest, {\n method: 'POST',\n body: { ...input },\n }),\n curateDrafts: (input) =>\n call(OUTREACH_API_ROUTES.curateDraft, {\n method: 'POST',\n body: { ...input },\n }),\n saveCuratedStep: (enrollmentId, stepIndex, override) =>\n call(OUTREACH_API_ROUTES.curateSave, {\n method: 'POST',\n body: { enrollmentId, stepIndex, override },\n }),\n readDoNotContactDomains: () =>\n call(OUTREACH_API_ROUTES.doNotContactDomains, { method: 'GET' }),\n changeDoNotContactDomain: (action, domain, detail) =>\n call(OUTREACH_API_ROUTES.doNotContactDomains, {\n method: 'POST',\n body: { action, domain, ...(detail ? { detail } : {}) },\n }),\n readLinkDomains: () => call(OUTREACH_API_ROUTES.linkDomains, { method: 'GET' }),\n changeLinkDomain: (action, domain) =>\n call(OUTREACH_API_ROUTES.linkDomains, {\n method: 'POST',\n body: { action, domain },\n }),\n }),\n [call],\n )\n}\n"],"names":["authorizedFetch","useUser","useCallback","useMemo","OUTREACH_API_ROUTES","OutreachRouteError","Error","refused","reason","message","status","issues","name","useOutreachApi","orgId","data","user","call","route","init","path","method","encodeURIComponent","response","headers","body","JSON","stringify","payload","json","catch","ok","refusal","error","readSettings","settings","saveSettings","input","saveSequence","sequenceId","sequence","sequencesSave","setSequenceStatus","action","sequencesStatus","deleteSequence","sequencesDelete","previewEnrollment","source","enrollPreview","enroll","people","actOnEnrollment","enrollmentId","detail","enrollmentsAction","readEnrollmentGateway","enrollmentsGateway","previewEmail","preview","sendStepTest","stepTest","curateDrafts","curateDraft","saveCuratedStep","stepIndex","override","curateSave","readDoNotContactDomains","doNotContactDomains","changeDoNotContactDomain","domain","readLinkDomains","linkDomains","changeLinkDomain"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-api.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'use client'\n\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { useCallback, useMemo } from 'react'\nimport { OUTREACH_API_ROUTES } from '../constants/api-routes'\nimport type { OutreachComplianceIssue } from '../model/compliance-settings'\nimport type {\n OutreachCurateDraftRequest,\n OutreachCurateDraftResponse,\n OutreachCurateSaveRequest,\n OutreachCurateSaveResponse,\n OutreachDoNotContactDomainRequest,\n OutreachDoNotContactDomainResponse,\n OutreachDoNotContactDomainsResponse,\n OutreachEnrollmentAction,\n OutreachEnrollmentActionResponse,\n OutreachEnrollmentGatewayResponse,\n OutreachEnrollPersonRequest,\n OutreachEnrollPreviewResponse,\n OutreachEnrollResponse,\n OutreachEnrollSource,\n OutreachLinkDomainRequest,\n OutreachLinkDomainResponse,\n OutreachLinkDomainsResponse,\n OutreachPreviewRequest,\n OutreachPreviewResponse,\n OutreachRouteRefusal,\n OutreachRouteRefusalReason,\n OutreachSequenceAction,\n OutreachSequenceSaveResponse,\n OutreachSequenceStatusResponse,\n OutreachSettingsResponse,\n OutreachSettingsSaveRequest,\n OutreachSettingsSaveResponse,\n OutreachStepTestRequest,\n OutreachStepTestResponse,\n} from '../model/outreach-api'\nimport type {\n OutreachSequenceDraft,\n OutreachSequenceIssue,\n} from '../model/sequence-draft'\n\n/**\n * A route's refusal, carrying its sentence, its stable reason and any\n * field-level issues. `refused` is the one a page shows as \"you can't use\n * this here\" rather than as something that went wrong: no permission, no\n * entitlement, or the release gate answering as if the route did not exist.\n */\nexport class OutreachRouteError extends Error {\n readonly reason: OutreachRouteRefusalReason | 'unreachable' | 'not-found'\n readonly status: number\n readonly issues: ReadonlyArray<\n OutreachComplianceIssue | OutreachSequenceIssue\n >\n\n constructor(\n message: string,\n reason: OutreachRouteError['reason'],\n status: number,\n issues: OutreachRouteError['issues'] = [],\n ) {\n super(message)\n this.name = 'OutreachRouteError'\n this.reason = reason\n this.status = status\n this.issues = issues\n }\n\n get refused(): boolean {\n return (\n this.reason === 'permission' ||\n this.reason === 'entitlement' ||\n this.reason === 'not-org-wide' ||\n this.reason === 'not-a-member' ||\n // The dispatcher answers a route whose plugin is not released to this\n // workspace as though it did not exist.\n this.reason === 'not-found'\n )\n }\n}\n\n/** The routes, as the Outreach pages call them. */\nexport interface OutreachApi {\n readSettings(): Promise<OutreachSettingsResponse>\n saveSettings(\n input: Omit<OutreachSettingsSaveRequest, 'orgId'>,\n ): Promise<OutreachSettingsSaveResponse>\n saveSequence(\n sequenceId: string | null,\n sequence: OutreachSequenceDraft,\n ): Promise<OutreachSequenceSaveResponse>\n setSequenceStatus(\n sequenceId: string,\n action: OutreachSequenceAction,\n ): Promise<OutreachSequenceStatusResponse>\n deleteSequence(sequenceId: string): Promise<{ ok: true }>\n previewEnrollment(\n sequenceId: string,\n source: OutreachEnrollSource,\n ): Promise<OutreachEnrollPreviewResponse>\n /** `startStepIndex` (AGL-3228) is the step everyone begins at, zero-based; the first when absent. */\n enroll(\n sequenceId: string,\n people: OutreachEnrollPersonRequest[],\n startStepIndex?: number,\n ): Promise<OutreachEnrollResponse>\n actOnEnrollment(\n enrollmentId: string,\n action: OutreachEnrollmentAction,\n detail?: string,\n ): Promise<OutreachEnrollmentActionResponse>\n /** The mail gateway in front of one enrolled person's domain (AGL-3332). */\n readEnrollmentGateway(enrollmentId: string): Promise<OutreachEnrollmentGatewayResponse>\n previewEmail(\n input: Omit<OutreachPreviewRequest, 'orgId'>,\n ): Promise<OutreachPreviewResponse>\n /**\n * Sends one step of a saved sequence to the member as a test (AGL-3325):\n * the preview's email, through the sequence's mailbox, `[Test]` in front.\n */\n sendStepTest(\n input: Omit<OutreachStepTestRequest, 'orgId'>,\n ): Promise<OutreachStepTestResponse & { unresolvedFields?: string[] }>\n /**\n * One person's own copies of the sequence's email steps, drafted by the\n * workspace's AI (AGL-3324); nothing is stored until the member confirms.\n */\n curateDrafts(\n input: Omit<OutreachCurateDraftRequest, 'orgId'>,\n ): Promise<OutreachCurateDraftResponse>\n /** Stores a confirmed copy of one step on an enrollment, or clears it with `null`. */\n saveCuratedStep(\n enrollmentId: string,\n stepIndex: number,\n override: OutreachCurateSaveRequest['override'],\n ): Promise<OutreachCurateSaveResponse>\n /** The domains on the do-not-contact list (AGL-3244). */\n readDoNotContactDomains(): Promise<OutreachDoNotContactDomainsResponse>\n /** Puts a domain on the list, or takes one off. */\n changeDoNotContactDomain(\n action: OutreachDoNotContactDomainRequest['action'],\n domain: string,\n detail?: string,\n ): Promise<OutreachDoNotContactDomainResponse>\n /** Each mailbox domain's click-tracking host (AGL-3306). */\n readLinkDomains(): Promise<OutreachLinkDomainsResponse>\n /** Sets a link domain up, checks it, or removes it. */\n changeLinkDomain(\n action: OutreachLinkDomainRequest['action'],\n domain: string,\n ): Promise<OutreachLinkDomainResponse>\n}\n\n/**\n * The Outreach pages' one door to the settings, sequence and enrollment\n * routes (AGL-2980), with the member's session attached and the\n * organization named on every call. A refusal is thrown as an\n * {@link OutreachRouteError} carrying the route's own sentence, which is\n * written for the person reading it.\n */\nexport function useOutreachApi(orgId: string | null): OutreachApi {\n const { data: user } = useUser()\n\n const call = useCallback(\n async <T>(\n route: string,\n init: { method: 'GET' | 'POST'; body?: Record<string, unknown> },\n ): Promise<T> => {\n if (!orgId)\n throw new OutreachRouteError(\n 'Open an organization first.',\n 'org-required',\n 400,\n )\n const path =\n init.method === 'GET'\n ? `/api/${route}?orgId=${encodeURIComponent(orgId)}`\n : `/api/${route}`\n let response: Response\n try {\n response = await authorizedFetch(user, path, {\n method: init.method,\n ...(init.method === 'POST'\n ? {\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ orgId, ...init.body }),\n }\n : {}),\n })\n } catch {\n throw new OutreachRouteError(\n 'Sequences could not be reached. Try again.',\n 'unreachable',\n 0,\n )\n }\n const payload = (await response.json().catch(() => null)) as\n (T & { error?: undefined }) | Partial<OutreachRouteRefusal> | null\n if (!response.ok || !payload) {\n const refusal = (payload ?? {}) as Partial<OutreachRouteRefusal>\n throw new OutreachRouteError(\n refusal.error && refusal.reason\n ? refusal.error\n : response.status === 404\n ? \"Sequences isn't available to this workspace yet.\"\n : 'Sequences could not be reached. Try again.',\n refusal.reason ??\n (response.status === 404 ? 'not-found' : 'unreachable'),\n response.status,\n refusal.issues ?? [],\n )\n }\n return payload as T\n },\n [user, orgId],\n )\n\n return useMemo<OutreachApi>(\n () => ({\n readSettings: () => call(OUTREACH_API_ROUTES.settings, { method: 'GET' }),\n saveSettings: (input) =>\n call(OUTREACH_API_ROUTES.settings, {\n method: 'POST',\n body: { ...input },\n }),\n saveSequence: (sequenceId, sequence) =>\n call(OUTREACH_API_ROUTES.sequencesSave, {\n method: 'POST',\n body: { ...(sequenceId ? { sequenceId } : {}), sequence },\n }),\n setSequenceStatus: (sequenceId, action) =>\n call(OUTREACH_API_ROUTES.sequencesStatus, {\n method: 'POST',\n body: { sequenceId, action },\n }),\n deleteSequence: (sequenceId) =>\n call(OUTREACH_API_ROUTES.sequencesDelete, {\n method: 'POST',\n body: { sequenceId },\n }),\n previewEnrollment: (sequenceId, source) =>\n call(OUTREACH_API_ROUTES.enrollPreview, {\n method: 'POST',\n body: { sequenceId, source },\n }),\n enroll: (sequenceId, people, startStepIndex) =>\n call(OUTREACH_API_ROUTES.enroll, {\n method: 'POST',\n body: { sequenceId, people, ...(startStepIndex ? { startStepIndex } : {}) },\n }),\n actOnEnrollment: (enrollmentId, action, detail) =>\n call(OUTREACH_API_ROUTES.enrollmentsAction, {\n method: 'POST',\n body: { enrollmentId, action, ...(detail ? { detail } : {}) },\n }),\n readEnrollmentGateway: (enrollmentId) =>\n call(OUTREACH_API_ROUTES.enrollmentsGateway, {\n method: 'POST',\n body: { enrollmentId },\n }),\n previewEmail: (input) =>\n call(OUTREACH_API_ROUTES.preview, {\n method: 'POST',\n body: { ...input },\n }),\n sendStepTest: (input) =>\n call(OUTREACH_API_ROUTES.stepTest, {\n method: 'POST',\n body: { ...input },\n }),\n curateDrafts: (input) =>\n call(OUTREACH_API_ROUTES.curateDraft, {\n method: 'POST',\n body: { ...input },\n }),\n saveCuratedStep: (enrollmentId, stepIndex, override) =>\n call(OUTREACH_API_ROUTES.curateSave, {\n method: 'POST',\n body: { enrollmentId, stepIndex, override },\n }),\n readDoNotContactDomains: () =>\n call(OUTREACH_API_ROUTES.doNotContactDomains, { method: 'GET' }),\n changeDoNotContactDomain: (action, domain, detail) =>\n call(OUTREACH_API_ROUTES.doNotContactDomains, {\n method: 'POST',\n body: { action, domain, ...(detail ? { detail } : {}) },\n }),\n readLinkDomains: () => call(OUTREACH_API_ROUTES.linkDomains, { method: 'GET' }),\n changeLinkDomain: (action, domain) =>\n call(OUTREACH_API_ROUTES.linkDomains, {\n method: 'POST',\n body: { action, domain },\n }),\n }),\n [call],\n )\n}\n"],"names":["authorizedFetch","useUser","useCallback","useMemo","OUTREACH_API_ROUTES","OutreachRouteError","Error","refused","reason","message","status","issues","name","useOutreachApi","orgId","data","user","call","route","init","path","method","encodeURIComponent","response","headers","body","JSON","stringify","payload","json","catch","ok","refusal","error","readSettings","settings","saveSettings","input","saveSequence","sequenceId","sequence","sequencesSave","setSequenceStatus","action","sequencesStatus","deleteSequence","sequencesDelete","previewEnrollment","source","enrollPreview","enroll","people","startStepIndex","actOnEnrollment","enrollmentId","detail","enrollmentsAction","readEnrollmentGateway","enrollmentsGateway","previewEmail","preview","sendStepTest","stepTest","curateDrafts","curateDraft","saveCuratedStep","stepIndex","override","curateSave","readDoNotContactDomains","doNotContactDomains","changeDoNotContactDomain","domain","readLinkDomains","linkDomains","changeLinkDomain"],"mappings":"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"}
@@ -66,7 +66,7 @@ export interface OutreachComposeError {
66
66
  }
67
67
  export interface ComposeOutreachEmailInput {
68
68
  sequence: Pick<OutreachSequence, 'steps'>;
69
- enrollment: Pick<OutreachEnrollment, 'email' | 'stepIndex' | 'personalLine' | 'threadSubject' | 'messageIds' | 'gmailThreadId'> & Partial<Pick<OutreachEnrollment, 'stepOverrides'>>;
69
+ enrollment: Pick<OutreachEnrollment, 'email' | 'stepIndex' | 'personalLine' | 'threadSubject' | 'messageIds' | 'gmailThreadId'> & Partial<Pick<OutreachEnrollment, 'stepOverrides' | 'startStepIndex'>>;
70
70
  orgSettings: Pick<OutreachOrgSettings, 'legalName' | 'brandName' | 'postalAddress'> | null | undefined;
71
71
  /**
72
72
  * What the CRM resolver reads: the contact, the sending site's group id,
@@ -65,7 +65,7 @@ import { crmThreadSubject } from "@aglyn/aglyn/app-utils/crm-inbound";
65
65
  import { rewriteOutreachBodyLinks } from "./click-tracking.js";
66
66
  import { outreachOpenTrackedHtml } from "./open-tracking.js";
67
67
  import { normalizeOutreachPersonalLine } from "./gates.js";
68
- import { isInThreadEmailStep, OUTREACH_PERSONAL_LINE_FIELD, outreachSubjectHasReplyPrefix } from "./sequence-validation.js";
68
+ import { isInThreadEmailStep, OUTREACH_PERSONAL_LINE_FIELD, outreachSubjectHasReplyPrefix, outreachThreadStartSubject } from "./sequence-validation.js";
69
69
  /** The way out every email offers, word for word. */ export const OUTREACH_OPT_OUT_LINE = 'Not interested? Reply "no" and I won\'t email again.';
70
70
  /**
71
71
  * The sentence that says what the email is. "Sales email", not "business
@@ -201,7 +201,11 @@ const refuse = (code, message)=>({
201
201
  text: '',
202
202
  trackedLinks: []
203
203
  };
204
- if (isInThreadEmailStep(steps, stepIndex)) {
204
+ // An enrollment that began past the first step (AGL-3228) starts its
205
+ // thread with the first email it sends, under the subject the person
206
+ // already had by hand when the step was written as a reply.
207
+ const startStepIndex = Number.isInteger(input.enrollment.startStepIndex) ? Math.max(0, Number(input.enrollment.startStepIndex)) : 0;
208
+ if (isInThreadEmailStep(steps, stepIndex, startStepIndex)) {
205
209
  var _input_enrollment_messageIds, _input_enrollment_gmailThreadId;
206
210
  const threadSubject = crmThreadSubject(input.enrollment.threadSubject);
207
211
  const messageIds = ((_input_enrollment_messageIds = input.enrollment.messageIds) != null ? _input_enrollment_messageIds : []).map(outreachMessageIdHeader).filter((id)=>id !== null);
@@ -214,8 +218,8 @@ const refuse = (code, message)=>({
214
218
  email.inReplyTo = messageIds[messageIds.length - 1];
215
219
  email.references = messageIds.join(' ');
216
220
  } else {
217
- var _ref2, _ref3;
218
- const subject = oneLine(render(String((_ref2 = (_ref3 = override == null ? void 0 : override.subject) != null ? _ref3 : step.subject) != null ? _ref2 : '')));
221
+ var _ref2;
222
+ const subject = oneLine(render(String((_ref2 = override == null ? void 0 : override.subject) != null ? _ref2 : outreachThreadStartSubject(steps, stepIndex))));
219
223
  if (!subject) return refuse('missing_subject', 'This email has no subject.');
220
224
  if (outreachSubjectHasReplyPrefix(subject)) {
221
225
  return refuse('subject_reply_prefix', 'This email starts a thread, so its subject may not begin with "Re:" or "Fwd:".');
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/compose.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * WHAT EXACTLY IS SENT (AGL-2979).\n *\n * The composer turns a step, an enrollment and the organization's settings\n * into the message the transport sends: the recipient, the subject, the\n * plain text, and the headers that thread it and let the recipient leave.\n * The transport adds the From line and the MIME; nothing here knows either.\n *\n * ## The footer is not optional\n *\n * Every email ends with the organization's legal name, its postal address,\n * a sentence saying the email is a sales email, and the way out — reply\n * \"no\". Those are what CAN-SPAM requires of a commercial email (16 CFR 316:\n * identifiable as a solicitation, no fixed phrase), and\n * the composer appends them itself, after the merge, as literal text: no\n * template can leave them off, and no merge field can rewrite them. With no\n * postal address or no legal name, it refuses to compose at all.\n *\n * ## Threading\n *\n * An email after the first is a reply in the thread unless its step says\n * otherwise: `Re:` and the thread's own subject, `In-Reply-To` the last\n * message sent into the thread, `References` every one of them, and the\n * provider thread to send into. A `Re:` on an email that answers nothing is\n * a deception, so a reply step with no thread to answer is refused rather\n * than sent with a borrowed one, and a subject that starts with `Re:` or\n * `Fwd:` on an email that starts a thread is refused too.\n *\n * ## Merge fields\n *\n * The CRM's own resolver fills them, from the documents the runtime already\n * read, and the enrollment's personal line joins them as\n * `{{enrollment.personalLine}}` through the resolver's `extra` fields.\n * Fields that render empty are returned beside the message, for the\n * runtime to decide whether \"Hi ,\" is worth sending.\n *\n * ## A curated step\n *\n * An enrollment may carry its own copy of a step (AGL-3324): a subject and\n * a body drafted for that one person. The composer reads the copy in place\n * of the step's subject and body — and of the template the step names —\n * and then does everything it does to a step: the merge, the threading, the\n * click tracking, the footer. A curated in-thread email still goes out as\n * `Re:` the thread's subject, since it answers the same thread.\n *==========================================*/\n\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { CRM_EMAIL_SUBJECT_MAX } from '@aglyn/aglyn/app-utils/crm'\nimport {\n type CrmMergeContext,\n resolveCrmMergeFields,\n} from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { crmThreadSubject } from '@aglyn/aglyn/app-utils/crm-inbound'\nimport type {\n OutreachEnrollment,\n OutreachOrgSettings,\n OutreachSequence,\n OutreachStepOverride,\n} from '../model/outreach.types'\nimport { rewriteOutreachBodyLinks } from './click-tracking'\nimport { outreachOpenTrackedHtml } from './open-tracking'\nimport { normalizeOutreachPersonalLine } from './gates'\nimport {\n isInThreadEmailStep,\n OUTREACH_PERSONAL_LINE_FIELD,\n outreachSubjectHasReplyPrefix,\n} from './sequence-validation'\n\n/** The way out every email offers, word for word. */\nexport const OUTREACH_OPT_OUT_LINE = 'Not interested? Reply \"no\" and I won\\'t email again.'\n\n/**\n * The sentence that says what the email is. \"Sales email\", not \"business\n * solicitation\": an inbox-placement test on 2026-09-19 named the latter\n * phrase as a spam trigger, and the law asks that the email be identifiable\n * as a solicitation, not for any particular words (AGL-3228).\n */\nexport function outreachSolicitationStatement(senderName: string): string {\n return `This is a sales email from ${senderName}.`\n}\n\n/**\n * The message the transport sends. Header values are single lines already;\n * the text uses `\\n` line breaks, which the transport writes as CRLF.\n */\nexport interface ComposedOutreachEmail {\n /** The recipient's address, normalized. */\n to: string\n subject: string\n /** The plain-text body, footer included. */\n text: string\n /**\n * The same body as HTML with a tracking image (AGL-3395), sent beside\n * `text` as `multipart/alternative`. Absent on every email of a sequence\n * that does not count opens, which goes out as plain text alone.\n */\n html?: string\n /** The provider thread a reply step is sent into. */\n threadId?: string\n /** The `Message-ID` this email answers, angle brackets included. */\n inReplyTo?: string\n /** Every `Message-ID` in the thread so far, oldest first, space-separated. */\n references?: string\n /** The one-click unsubscribe URL, for `List-Unsubscribe` with `List-Unsubscribe-Post`. */\n listUnsubscribeUrl?: string\n /** A `mailto:` URI for `List-Unsubscribe`. */\n listUnsubscribeMailto?: string\n /**\n * The destinations whose links were rewritten for click tracking, in the\n * order a click token indexes them (AGL-3239). Empty when the sequence\n * does not track clicks, when the body carries no links, and when no\n * tracking link could be minted.\n */\n trackedLinks: string[]\n}\n\nexport type OutreachComposeErrorCode =\n | 'missing_postal_address'\n | 'missing_legal_name'\n | 'not_an_email_step'\n | 'invalid_recipient'\n | 'missing_subject'\n | 'subject_reply_prefix'\n | 'missing_body'\n | 'missing_thread'\n | 'invalid_unsubscribe_url'\n | 'invalid_unsubscribe_mailto'\n\nexport interface OutreachComposeError {\n code: OutreachComposeErrorCode\n message: string\n}\n\nexport interface ComposeOutreachEmailInput {\n sequence: Pick<OutreachSequence, 'steps'>\n enrollment: Pick<\n OutreachEnrollment,\n 'email' | 'stepIndex' | 'personalLine' | 'threadSubject' | 'messageIds' | 'gmailThreadId'\n > &\n Partial<Pick<OutreachEnrollment, 'stepOverrides'>>\n orgSettings: Pick<OutreachOrgSettings, 'legalName' | 'brandName' | 'postalAddress'> | null | undefined\n /**\n * What the CRM resolver reads: the contact, the sending site's group id,\n * the sender (the mailbox's display name and address), the site. Its\n * `extra` may carry fields of the caller's own; the personal line is added\n * here.\n */\n merge: CrmMergeContext | null | undefined\n /** The CRM template's body, read by the caller, when the step names a template. */\n templateBody?: string | null\n /** The signed one-click unsubscribe URL, minted by the runtime. */\n listUnsubscribeUrl?: string | null\n /** An address, or a `mailto:` URI, that unsubscribes whoever writes to it. */\n listUnsubscribeMailto?: string | null\n /**\n * Answers the tracking link for one link in the body, or `null` to leave\n * that one as it was written (AGL-3239).\n *\n * Absent leaves every link alone, which is what a sequence with\n * `trackClicks` off passes and what every caller that is not the sending\n * runtime passes — a PREVIEW must show the rep the body their recipient\n * reads, and minting a live tracking link for a preview would count the\n * rep's own click against the sequence.\n *\n * It runs on the step's rendered body and never on the footer, so the\n * organization's identification and the way out are never rewritten.\n */\n rewriteLink?: ((link: { url: string; index: number }) => string | null) | null\n /**\n * The tracking image's address, minted by the runtime for a sequence that\n * counts opens (AGL-3395). Given, the email gains an HTML part — the same\n * text, footer included, and the image; absent or `null`, it stays plain\n * text, which is what a preview and every other caller pass.\n */\n openPixelUrl?: string | null\n}\n\nexport interface OutreachComposeResult {\n email: ComposedOutreachEmail | null\n error: OutreachComposeError | null\n /** Merge fields that rendered empty, unique, in order of appearance. */\n unresolvedFields: string[]\n /**\n * The person's own copy of the step the email was written from\n * (AGL-3324), or `null` when the step was sent as the sequence wrote it.\n */\n override: OutreachStepOverride | null\n}\n\n/**\n * The enrollment's own copy of the step at `stepIndex` (AGL-3324), or\n * `null`. A copy with neither a subject nor a body is no copy.\n */\nexport function outreachStepOverrideFor(\n enrollment: Partial<Pick<OutreachEnrollment, 'stepOverrides'>> | null | undefined,\n stepIndex: number,\n): OutreachStepOverride | null {\n const override = enrollment?.stepOverrides?.[String(stepIndex)]\n if (!override || typeof override !== 'object') return null\n const hasSubject = typeof override.subject === 'string'\n const hasBody = typeof override.body === 'string'\n return hasSubject || hasBody ? override : null\n}\n\nconst oneLine = (value: unknown): string =>\n typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n\n/**\n * The footer every email ends with, or the reason it cannot be written.\n *\n * The postal address may be stored over several lines; the footer prints it\n * on one, the parts separated by commas.\n */\nexport function composeOutreachFooter(\n settings: Pick<OutreachOrgSettings, 'legalName' | 'brandName' | 'postalAddress'> | null | undefined,\n): { footer: string | null; error: OutreachComposeError | null } {\n const postalAddress = String(settings?.postalAddress ?? '')\n .split(/\\r\\n|\\r|\\n/)\n .map((line) => oneLine(line).replace(/,+$/, ''))\n .filter(Boolean)\n .join(', ')\n if (!postalAddress) {\n return {\n footer: null,\n error: {\n code: 'missing_postal_address',\n message:\n \"This email can't be sent without your organization's postal address. Add it in Sequences settings.\",\n },\n }\n }\n const legalName = oneLine(settings?.legalName)\n if (!legalName) {\n return {\n footer: null,\n error: {\n code: 'missing_legal_name',\n message:\n \"This email can't be sent without your organization's legal name. Add it in Sequences settings.\",\n },\n }\n }\n const senderName = oneLine(settings?.brandName) || legalName\n return {\n footer: `${legalName} · ${postalAddress}\\n${outreachSolicitationStatement(senderName)} ${OUTREACH_OPT_OUT_LINE}`,\n error: null,\n }\n}\n\n/** A `Message-ID` in its header form — `<id@host>` — or `null` for one that is not. */\nexport function outreachMessageIdHeader(value: unknown): string | null {\n const bare = String(value ?? '').trim().replace(/^<+/, '').replace(/>+$/, '')\n if (!bare || /[\\s<>]/.test(bare)) return null\n return `<${bare}>`\n}\n\nfunction unsubscribeUrl(value: unknown): string | null | undefined {\n const raw = String(value ?? '').trim()\n if (!raw) return undefined\n if (/[\\r\\n]/.test(raw)) return null\n try {\n const url = new URL(raw)\n return url.protocol === 'https:' ? url.href : null\n } catch {\n return null\n }\n}\n\nfunction unsubscribeMailto(value: unknown): string | null | undefined {\n const raw = String(value ?? '').trim()\n if (!raw) return undefined\n if (/[\\r\\n]/.test(raw)) return null\n if (!/^mailto:/i.test(raw)) {\n const address = normalizeContactEmail(raw)\n return address ? `mailto:${address}?subject=unsubscribe` : null\n }\n let target = raw.slice('mailto:'.length).split('?')[0]\n try {\n target = decodeURIComponent(target)\n } catch {\n return null\n }\n return normalizeContactEmail(target) ? raw : null\n}\n\nconst refuse = (\n code: OutreachComposeErrorCode,\n message: string,\n): OutreachComposeResult => ({ email: null, error: { code, message }, unresolvedFields: [], override: null })\n\n/** The email one step sends to one person — see the module note. */\nexport function composeOutreachEmail(input: ComposeOutreachEmailInput): OutreachComposeResult {\n const { footer, error } = composeOutreachFooter(input.orgSettings)\n if (error) return { email: null, error, unresolvedFields: [], override: null }\n\n const steps = input.sequence?.steps ?? []\n const stepIndex = input.enrollment?.stepIndex\n const step = steps[stepIndex]\n if (step?.kind !== 'email') {\n return refuse('not_an_email_step', 'The step this enrollment is on does not send an email.')\n }\n const to = normalizeContactEmail(input.enrollment.email)\n if (!to) {\n return refuse('invalid_recipient', \"This enrollment's address isn't a valid email address.\")\n }\n\n const context: CrmMergeContext = {\n ...(input.merge ?? {}),\n extra: {\n ...(input.merge?.extra ?? {}),\n [OUTREACH_PERSONAL_LINE_FIELD]: normalizeOutreachPersonalLine(input.enrollment.personalLine),\n },\n }\n const unresolved: string[] = []\n const render = (text: string): string => {\n const result = resolveCrmMergeFields(text, context)\n for (const key of result.unresolved) if (!unresolved.includes(key)) unresolved.push(key)\n return result.text\n }\n\n // This person's own copy of the step (AGL-3324), read in place of the\n // step's own words — and of its template — from here on.\n const override = outreachStepOverrideFor(input.enrollment, stepIndex)\n\n const email: ComposedOutreachEmail = { to, subject: '', text: '', trackedLinks: [] }\n if (isInThreadEmailStep(steps, stepIndex)) {\n const threadSubject = crmThreadSubject(input.enrollment.threadSubject)\n const messageIds = (input.enrollment.messageIds ?? [])\n .map(outreachMessageIdHeader)\n .filter((id): id is string => id !== null)\n const threadId = String(input.enrollment.gmailThreadId ?? '').trim()\n if (!threadSubject || !messageIds.length || !threadId) {\n return refuse(\n 'missing_thread',\n 'This email replies in the thread, and no earlier email in the thread was sent.',\n )\n }\n email.subject = `Re: ${threadSubject}`.slice(0, CRM_EMAIL_SUBJECT_MAX)\n email.threadId = threadId\n email.inReplyTo = messageIds[messageIds.length - 1]\n email.references = messageIds.join(' ')\n } else {\n const subject = oneLine(render(String(override?.subject ?? step.subject ?? '')))\n if (!subject) return refuse('missing_subject', 'This email has no subject.')\n if (outreachSubjectHasReplyPrefix(subject)) {\n return refuse(\n 'subject_reply_prefix',\n 'This email starts a thread, so its subject may not begin with \"Re:\" or \"Fwd:\".',\n )\n }\n email.subject = subject.slice(0, CRM_EMAIL_SUBJECT_MAX)\n }\n\n const source =\n typeof override?.body === 'string'\n ? override.body\n : step.templateId\n ? String(input.templateBody ?? '')\n : String(step.body ?? '')\n const body = render(source)\n .replace(/\\r\\n?/g, '\\n')\n .split('\\n')\n .map((line) => line.replace(/[ \\t]+$/, ''))\n .join('\\n')\n .replace(/^\\n+|\\n+$/g, '')\n if (!body.trim()) return refuse('missing_body', 'This email has no body.')\n /*\n * Click tracking rewrites the STEP'S links and nothing else (AGL-3239):\n * it runs here, on the rendered body, before the footer is appended. The\n * footer's opt-out `mailto:` and the organization's own identification are\n * never rewritten, and the one-click unsubscribe link never appears in a\n * body at all — it rides in `List-Unsubscribe`.\n */\n const tracked = input.rewriteLink\n ? rewriteOutreachBodyLinks(body, input.rewriteLink)\n : { text: body, links: [] }\n email.trackedLinks = tracked.links\n email.text = `${tracked.text}\\n\\n${footer}`\n // Built from the finished text, so the HTML part says exactly what the\n // plain-text part says — the footer and any rewritten link included.\n if (input.openPixelUrl) email.html = outreachOpenTrackedHtml(email.text, input.openPixelUrl)\n\n const url = unsubscribeUrl(input.listUnsubscribeUrl)\n if (url === null) {\n return refuse('invalid_unsubscribe_url', 'The unsubscribe link must be an https URL.')\n }\n if (url) email.listUnsubscribeUrl = url\n const mailto = unsubscribeMailto(input.listUnsubscribeMailto)\n if (mailto === null) {\n return refuse('invalid_unsubscribe_mailto', 'The unsubscribe address is not an email address.')\n }\n if (mailto) email.listUnsubscribeMailto = mailto\n\n return { email, error: null, unresolvedFields: unresolved, override }\n}\n"],"names":["normalizeContactEmail","CRM_EMAIL_SUBJECT_MAX","resolveCrmMergeFields","crmThreadSubject","rewriteOutreachBodyLinks","outreachOpenTrackedHtml","normalizeOutreachPersonalLine","isInThreadEmailStep","OUTREACH_PERSONAL_LINE_FIELD","outreachSubjectHasReplyPrefix","OUTREACH_OPT_OUT_LINE","outreachSolicitationStatement","senderName","outreachStepOverrideFor","enrollment","stepIndex","override","stepOverrides","String","hasSubject","subject","hasBody","body","oneLine","value","replace","trim","composeOutreachFooter","settings","postalAddress","split","map","line","filter","Boolean","join","footer","error","code","message","legalName","brandName","outreachMessageIdHeader","bare","test","unsubscribeUrl","raw","undefined","url","URL","protocol","href","unsubscribeMailto","address","target","slice","length","decodeURIComponent","refuse","email","unresolvedFields","composeOutreachEmail","input","step","orgSettings","steps","sequence","kind","to","context","merge","extra","personalLine","unresolved","render","text","result","key","includes","push","trackedLinks","threadSubject","messageIds","id","threadId","gmailThreadId","inReplyTo","references","source","templateId","templateBody","tracked","rewriteLink","links","openPixelUrl","html","listUnsubscribeUrl","mailto","listUnsubscribeMailto"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/compose.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * WHAT EXACTLY IS SENT (AGL-2979).\n *\n * The composer turns a step, an enrollment and the organization's settings\n * into the message the transport sends: the recipient, the subject, the\n * plain text, and the headers that thread it and let the recipient leave.\n * The transport adds the From line and the MIME; nothing here knows either.\n *\n * ## The footer is not optional\n *\n * Every email ends with the organization's legal name, its postal address,\n * a sentence saying the email is a sales email, and the way out — reply\n * \"no\". Those are what CAN-SPAM requires of a commercial email (16 CFR 316:\n * identifiable as a solicitation, no fixed phrase), and\n * the composer appends them itself, after the merge, as literal text: no\n * template can leave them off, and no merge field can rewrite them. With no\n * postal address or no legal name, it refuses to compose at all.\n *\n * ## Threading\n *\n * An email after the first is a reply in the thread unless its step says\n * otherwise: `Re:` and the thread's own subject, `In-Reply-To` the last\n * message sent into the thread, `References` every one of them, and the\n * provider thread to send into. A `Re:` on an email that answers nothing is\n * a deception, so a reply step with no thread to answer is refused rather\n * than sent with a borrowed one, and a subject that starts with `Re:` or\n * `Fwd:` on an email that starts a thread is refused too.\n *\n * ## Merge fields\n *\n * The CRM's own resolver fills them, from the documents the runtime already\n * read, and the enrollment's personal line joins them as\n * `{{enrollment.personalLine}}` through the resolver's `extra` fields.\n * Fields that render empty are returned beside the message, for the\n * runtime to decide whether \"Hi ,\" is worth sending.\n *\n * ## A curated step\n *\n * An enrollment may carry its own copy of a step (AGL-3324): a subject and\n * a body drafted for that one person. The composer reads the copy in place\n * of the step's subject and body — and of the template the step names —\n * and then does everything it does to a step: the merge, the threading, the\n * click tracking, the footer. A curated in-thread email still goes out as\n * `Re:` the thread's subject, since it answers the same thread.\n *==========================================*/\n\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { CRM_EMAIL_SUBJECT_MAX } from '@aglyn/aglyn/app-utils/crm'\nimport {\n type CrmMergeContext,\n resolveCrmMergeFields,\n} from '@aglyn/aglyn/app-utils/crm-email-templates'\nimport { crmThreadSubject } from '@aglyn/aglyn/app-utils/crm-inbound'\nimport type {\n OutreachEnrollment,\n OutreachOrgSettings,\n OutreachSequence,\n OutreachStepOverride,\n} from '../model/outreach.types'\nimport { rewriteOutreachBodyLinks } from './click-tracking'\nimport { outreachOpenTrackedHtml } from './open-tracking'\nimport { normalizeOutreachPersonalLine } from './gates'\nimport {\n isInThreadEmailStep,\n OUTREACH_PERSONAL_LINE_FIELD,\n outreachSubjectHasReplyPrefix,\n outreachThreadStartSubject,\n} from './sequence-validation'\n\n/** The way out every email offers, word for word. */\nexport const OUTREACH_OPT_OUT_LINE = 'Not interested? Reply \"no\" and I won\\'t email again.'\n\n/**\n * The sentence that says what the email is. \"Sales email\", not \"business\n * solicitation\": an inbox-placement test on 2026-09-19 named the latter\n * phrase as a spam trigger, and the law asks that the email be identifiable\n * as a solicitation, not for any particular words (AGL-3228).\n */\nexport function outreachSolicitationStatement(senderName: string): string {\n return `This is a sales email from ${senderName}.`\n}\n\n/**\n * The message the transport sends. Header values are single lines already;\n * the text uses `\\n` line breaks, which the transport writes as CRLF.\n */\nexport interface ComposedOutreachEmail {\n /** The recipient's address, normalized. */\n to: string\n subject: string\n /** The plain-text body, footer included. */\n text: string\n /**\n * The same body as HTML with a tracking image (AGL-3395), sent beside\n * `text` as `multipart/alternative`. Absent on every email of a sequence\n * that does not count opens, which goes out as plain text alone.\n */\n html?: string\n /** The provider thread a reply step is sent into. */\n threadId?: string\n /** The `Message-ID` this email answers, angle brackets included. */\n inReplyTo?: string\n /** Every `Message-ID` in the thread so far, oldest first, space-separated. */\n references?: string\n /** The one-click unsubscribe URL, for `List-Unsubscribe` with `List-Unsubscribe-Post`. */\n listUnsubscribeUrl?: string\n /** A `mailto:` URI for `List-Unsubscribe`. */\n listUnsubscribeMailto?: string\n /**\n * The destinations whose links were rewritten for click tracking, in the\n * order a click token indexes them (AGL-3239). Empty when the sequence\n * does not track clicks, when the body carries no links, and when no\n * tracking link could be minted.\n */\n trackedLinks: string[]\n}\n\nexport type OutreachComposeErrorCode =\n | 'missing_postal_address'\n | 'missing_legal_name'\n | 'not_an_email_step'\n | 'invalid_recipient'\n | 'missing_subject'\n | 'subject_reply_prefix'\n | 'missing_body'\n | 'missing_thread'\n | 'invalid_unsubscribe_url'\n | 'invalid_unsubscribe_mailto'\n\nexport interface OutreachComposeError {\n code: OutreachComposeErrorCode\n message: string\n}\n\nexport interface ComposeOutreachEmailInput {\n sequence: Pick<OutreachSequence, 'steps'>\n enrollment: Pick<\n OutreachEnrollment,\n 'email' | 'stepIndex' | 'personalLine' | 'threadSubject' | 'messageIds' | 'gmailThreadId'\n > &\n Partial<Pick<OutreachEnrollment, 'stepOverrides' | 'startStepIndex'>>\n orgSettings: Pick<OutreachOrgSettings, 'legalName' | 'brandName' | 'postalAddress'> | null | undefined\n /**\n * What the CRM resolver reads: the contact, the sending site's group id,\n * the sender (the mailbox's display name and address), the site. Its\n * `extra` may carry fields of the caller's own; the personal line is added\n * here.\n */\n merge: CrmMergeContext | null | undefined\n /** The CRM template's body, read by the caller, when the step names a template. */\n templateBody?: string | null\n /** The signed one-click unsubscribe URL, minted by the runtime. */\n listUnsubscribeUrl?: string | null\n /** An address, or a `mailto:` URI, that unsubscribes whoever writes to it. */\n listUnsubscribeMailto?: string | null\n /**\n * Answers the tracking link for one link in the body, or `null` to leave\n * that one as it was written (AGL-3239).\n *\n * Absent leaves every link alone, which is what a sequence with\n * `trackClicks` off passes and what every caller that is not the sending\n * runtime passes — a PREVIEW must show the rep the body their recipient\n * reads, and minting a live tracking link for a preview would count the\n * rep's own click against the sequence.\n *\n * It runs on the step's rendered body and never on the footer, so the\n * organization's identification and the way out are never rewritten.\n */\n rewriteLink?: ((link: { url: string; index: number }) => string | null) | null\n /**\n * The tracking image's address, minted by the runtime for a sequence that\n * counts opens (AGL-3395). Given, the email gains an HTML part — the same\n * text, footer included, and the image; absent or `null`, it stays plain\n * text, which is what a preview and every other caller pass.\n */\n openPixelUrl?: string | null\n}\n\nexport interface OutreachComposeResult {\n email: ComposedOutreachEmail | null\n error: OutreachComposeError | null\n /** Merge fields that rendered empty, unique, in order of appearance. */\n unresolvedFields: string[]\n /**\n * The person's own copy of the step the email was written from\n * (AGL-3324), or `null` when the step was sent as the sequence wrote it.\n */\n override: OutreachStepOverride | null\n}\n\n/**\n * The enrollment's own copy of the step at `stepIndex` (AGL-3324), or\n * `null`. A copy with neither a subject nor a body is no copy.\n */\nexport function outreachStepOverrideFor(\n enrollment: Partial<Pick<OutreachEnrollment, 'stepOverrides'>> | null | undefined,\n stepIndex: number,\n): OutreachStepOverride | null {\n const override = enrollment?.stepOverrides?.[String(stepIndex)]\n if (!override || typeof override !== 'object') return null\n const hasSubject = typeof override.subject === 'string'\n const hasBody = typeof override.body === 'string'\n return hasSubject || hasBody ? override : null\n}\n\nconst oneLine = (value: unknown): string =>\n typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n\n/**\n * The footer every email ends with, or the reason it cannot be written.\n *\n * The postal address may be stored over several lines; the footer prints it\n * on one, the parts separated by commas.\n */\nexport function composeOutreachFooter(\n settings: Pick<OutreachOrgSettings, 'legalName' | 'brandName' | 'postalAddress'> | null | undefined,\n): { footer: string | null; error: OutreachComposeError | null } {\n const postalAddress = String(settings?.postalAddress ?? '')\n .split(/\\r\\n|\\r|\\n/)\n .map((line) => oneLine(line).replace(/,+$/, ''))\n .filter(Boolean)\n .join(', ')\n if (!postalAddress) {\n return {\n footer: null,\n error: {\n code: 'missing_postal_address',\n message:\n \"This email can't be sent without your organization's postal address. Add it in Sequences settings.\",\n },\n }\n }\n const legalName = oneLine(settings?.legalName)\n if (!legalName) {\n return {\n footer: null,\n error: {\n code: 'missing_legal_name',\n message:\n \"This email can't be sent without your organization's legal name. Add it in Sequences settings.\",\n },\n }\n }\n const senderName = oneLine(settings?.brandName) || legalName\n return {\n footer: `${legalName} · ${postalAddress}\\n${outreachSolicitationStatement(senderName)} ${OUTREACH_OPT_OUT_LINE}`,\n error: null,\n }\n}\n\n/** A `Message-ID` in its header form — `<id@host>` — or `null` for one that is not. */\nexport function outreachMessageIdHeader(value: unknown): string | null {\n const bare = String(value ?? '').trim().replace(/^<+/, '').replace(/>+$/, '')\n if (!bare || /[\\s<>]/.test(bare)) return null\n return `<${bare}>`\n}\n\nfunction unsubscribeUrl(value: unknown): string | null | undefined {\n const raw = String(value ?? '').trim()\n if (!raw) return undefined\n if (/[\\r\\n]/.test(raw)) return null\n try {\n const url = new URL(raw)\n return url.protocol === 'https:' ? url.href : null\n } catch {\n return null\n }\n}\n\nfunction unsubscribeMailto(value: unknown): string | null | undefined {\n const raw = String(value ?? '').trim()\n if (!raw) return undefined\n if (/[\\r\\n]/.test(raw)) return null\n if (!/^mailto:/i.test(raw)) {\n const address = normalizeContactEmail(raw)\n return address ? `mailto:${address}?subject=unsubscribe` : null\n }\n let target = raw.slice('mailto:'.length).split('?')[0]\n try {\n target = decodeURIComponent(target)\n } catch {\n return null\n }\n return normalizeContactEmail(target) ? raw : null\n}\n\nconst refuse = (\n code: OutreachComposeErrorCode,\n message: string,\n): OutreachComposeResult => ({ email: null, error: { code, message }, unresolvedFields: [], override: null })\n\n/** The email one step sends to one person — see the module note. */\nexport function composeOutreachEmail(input: ComposeOutreachEmailInput): OutreachComposeResult {\n const { footer, error } = composeOutreachFooter(input.orgSettings)\n if (error) return { email: null, error, unresolvedFields: [], override: null }\n\n const steps = input.sequence?.steps ?? []\n const stepIndex = input.enrollment?.stepIndex\n const step = steps[stepIndex]\n if (step?.kind !== 'email') {\n return refuse('not_an_email_step', 'The step this enrollment is on does not send an email.')\n }\n const to = normalizeContactEmail(input.enrollment.email)\n if (!to) {\n return refuse('invalid_recipient', \"This enrollment's address isn't a valid email address.\")\n }\n\n const context: CrmMergeContext = {\n ...(input.merge ?? {}),\n extra: {\n ...(input.merge?.extra ?? {}),\n [OUTREACH_PERSONAL_LINE_FIELD]: normalizeOutreachPersonalLine(input.enrollment.personalLine),\n },\n }\n const unresolved: string[] = []\n const render = (text: string): string => {\n const result = resolveCrmMergeFields(text, context)\n for (const key of result.unresolved) if (!unresolved.includes(key)) unresolved.push(key)\n return result.text\n }\n\n // This person's own copy of the step (AGL-3324), read in place of the\n // step's own words — and of its template — from here on.\n const override = outreachStepOverrideFor(input.enrollment, stepIndex)\n\n const email: ComposedOutreachEmail = { to, subject: '', text: '', trackedLinks: [] }\n // An enrollment that began past the first step (AGL-3228) starts its\n // thread with the first email it sends, under the subject the person\n // already had by hand when the step was written as a reply.\n const startStepIndex = Number.isInteger(input.enrollment.startStepIndex)\n ? Math.max(0, Number(input.enrollment.startStepIndex))\n : 0\n if (isInThreadEmailStep(steps, stepIndex, startStepIndex)) {\n const threadSubject = crmThreadSubject(input.enrollment.threadSubject)\n const messageIds = (input.enrollment.messageIds ?? [])\n .map(outreachMessageIdHeader)\n .filter((id): id is string => id !== null)\n const threadId = String(input.enrollment.gmailThreadId ?? '').trim()\n if (!threadSubject || !messageIds.length || !threadId) {\n return refuse(\n 'missing_thread',\n 'This email replies in the thread, and no earlier email in the thread was sent.',\n )\n }\n email.subject = `Re: ${threadSubject}`.slice(0, CRM_EMAIL_SUBJECT_MAX)\n email.threadId = threadId\n email.inReplyTo = messageIds[messageIds.length - 1]\n email.references = messageIds.join(' ')\n } else {\n const subject = oneLine(render(String(override?.subject ?? outreachThreadStartSubject(steps, stepIndex))))\n if (!subject) return refuse('missing_subject', 'This email has no subject.')\n if (outreachSubjectHasReplyPrefix(subject)) {\n return refuse(\n 'subject_reply_prefix',\n 'This email starts a thread, so its subject may not begin with \"Re:\" or \"Fwd:\".',\n )\n }\n email.subject = subject.slice(0, CRM_EMAIL_SUBJECT_MAX)\n }\n\n const source =\n typeof override?.body === 'string'\n ? override.body\n : step.templateId\n ? String(input.templateBody ?? '')\n : String(step.body ?? '')\n const body = render(source)\n .replace(/\\r\\n?/g, '\\n')\n .split('\\n')\n .map((line) => line.replace(/[ \\t]+$/, ''))\n .join('\\n')\n .replace(/^\\n+|\\n+$/g, '')\n if (!body.trim()) return refuse('missing_body', 'This email has no body.')\n /*\n * Click tracking rewrites the STEP'S links and nothing else (AGL-3239):\n * it runs here, on the rendered body, before the footer is appended. The\n * footer's opt-out `mailto:` and the organization's own identification are\n * never rewritten, and the one-click unsubscribe link never appears in a\n * body at all — it rides in `List-Unsubscribe`.\n */\n const tracked = input.rewriteLink\n ? rewriteOutreachBodyLinks(body, input.rewriteLink)\n : { text: body, links: [] }\n email.trackedLinks = tracked.links\n email.text = `${tracked.text}\\n\\n${footer}`\n // Built from the finished text, so the HTML part says exactly what the\n // plain-text part says — the footer and any rewritten link included.\n if (input.openPixelUrl) email.html = outreachOpenTrackedHtml(email.text, input.openPixelUrl)\n\n const url = unsubscribeUrl(input.listUnsubscribeUrl)\n if (url === null) {\n return refuse('invalid_unsubscribe_url', 'The unsubscribe link must be an https URL.')\n }\n if (url) email.listUnsubscribeUrl = url\n const mailto = unsubscribeMailto(input.listUnsubscribeMailto)\n if (mailto === null) {\n return refuse('invalid_unsubscribe_mailto', 'The unsubscribe address is not an email address.')\n }\n if (mailto) email.listUnsubscribeMailto = mailto\n\n return { email, error: null, unresolvedFields: unresolved, override }\n}\n"],"names":["normalizeContactEmail","CRM_EMAIL_SUBJECT_MAX","resolveCrmMergeFields","crmThreadSubject","rewriteOutreachBodyLinks","outreachOpenTrackedHtml","normalizeOutreachPersonalLine","isInThreadEmailStep","OUTREACH_PERSONAL_LINE_FIELD","outreachSubjectHasReplyPrefix","outreachThreadStartSubject","OUTREACH_OPT_OUT_LINE","outreachSolicitationStatement","senderName","outreachStepOverrideFor","enrollment","stepIndex","override","stepOverrides","String","hasSubject","subject","hasBody","body","oneLine","value","replace","trim","composeOutreachFooter","settings","postalAddress","split","map","line","filter","Boolean","join","footer","error","code","message","legalName","brandName","outreachMessageIdHeader","bare","test","unsubscribeUrl","raw","undefined","url","URL","protocol","href","unsubscribeMailto","address","target","slice","length","decodeURIComponent","refuse","email","unresolvedFields","composeOutreachEmail","input","step","orgSettings","steps","sequence","kind","to","context","merge","extra","personalLine","unresolved","render","text","result","key","includes","push","trackedLinks","startStepIndex","Number","isInteger","Math","max","threadSubject","messageIds","id","threadId","gmailThreadId","inReplyTo","references","source","templateId","templateBody","tracked","rewriteLink","links","openPixelUrl","html","listUnsubscribeUrl","mailto","listUnsubscribeMailto"],"mappings":"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"}
@@ -44,15 +44,23 @@ export interface OutreachTimelineEntry {
44
44
  }
45
45
  /** The key the enrollment's own entry is filed under, restated by the backfill. */
46
46
  export declare function outreachEnrolledEntryKey(enrollmentId: string): string;
47
+ /**
48
+ * The line that says an enrollment began past the first step (AGL-3228),
49
+ * and which steps it skipped: "Began at step 3 — steps 1–2 skipped, already
50
+ * done by hand". `''` for one that began at step 1.
51
+ */
52
+ export declare function outreachStartedAtLine(startStepIndex: number | null | undefined): string;
47
53
  /**
48
54
  * "Enrolled in <sequence>", and the campaigns it carried the person into
49
55
  * when the sequence was in any, named. A sequence with no name — never
50
- * stored, but a document can lose one — reads "a sequence".
56
+ * stored, but a document can lose one — reads "a sequence". An enrollment
57
+ * that began past the first step says so on a line of its own (AGL-3228).
51
58
  */
52
59
  export declare function outreachEnrolledEntry(input: {
53
60
  enrollmentId: string;
54
61
  sequenceName: string;
55
62
  campaignNames: readonly string[];
63
+ startStepIndex?: number;
56
64
  }): OutreachTimelineEntry;
57
65
  /**
58
66
  * The key one curated step's entry is filed under (AGL-3324): once per
@@ -37,16 +37,29 @@
37
37
  /** The key the enrollment's own entry is filed under, restated by the backfill. */ export function outreachEnrolledEntryKey(enrollmentId) {
38
38
  return `enrolled:${enrollmentId}`;
39
39
  }
40
+ /**
41
+ * The line that says an enrollment began past the first step (AGL-3228),
42
+ * and which steps it skipped: "Began at step 3 — steps 1–2 skipped, already
43
+ * done by hand". `''` for one that began at step 1.
44
+ */ export function outreachStartedAtLine(startStepIndex) {
45
+ const start = Number(startStepIndex);
46
+ if (!Number.isInteger(start) || start < 1) return '';
47
+ const skipped = start === 1 ? 'step 1' : `steps 1–${start}`;
48
+ return `Began at step ${start + 1} — ${skipped} skipped, already done by hand`;
49
+ }
40
50
  /**
41
51
  * "Enrolled in <sequence>", and the campaigns it carried the person into
42
52
  * when the sequence was in any, named. A sequence with no name — never
43
- * stored, but a document can lose one — reads "a sequence".
53
+ * stored, but a document can lose one — reads "a sequence". An enrollment
54
+ * that began past the first step says so on a line of its own (AGL-3228).
44
55
  */ export function outreachEnrolledEntry(input) {
45
56
  const sequence = input.sequenceName.trim() || 'a sequence';
46
57
  const campaigns = input.campaignNames.map((name)=>name.trim()).filter(Boolean);
58
+ const enrolled = campaigns.length ? `Enrolled in ${sequence}\nFiled under ${campaigns.join(', ')}` : `Enrolled in ${sequence}`;
59
+ const startedAt = outreachStartedAtLine(input.startStepIndex);
47
60
  return {
48
61
  dedupeKey: outreachEnrolledEntryKey(input.enrollmentId),
49
- body: campaigns.length ? `Enrolled in ${sequence}\nFiled under ${campaigns.join(', ')}` : `Enrolled in ${sequence}`
62
+ body: startedAt ? `${enrolled}\n${startedAt}` : enrolled
50
63
  };
51
64
  }
52
65
  /**