@aglyn/plugins-outreach 1.0.0-beta.224 → 1.0.0-beta.225

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 (106) hide show
  1. package/README.md +10 -4
  2. package/package.json +14 -14
  3. package/src/lib/components/enrollment-detail.d.ts +8 -0
  4. package/src/lib/components/enrollment-detail.js +17 -5
  5. package/src/lib/components/enrollment-detail.js.map +1 -1
  6. package/src/lib/components/mailbox-card.d.ts +3 -3
  7. package/src/lib/components/mailbox-card.js +11 -5
  8. package/src/lib/components/mailbox-card.js.map +1 -1
  9. package/src/lib/components/mailboxes-section.d.ts +6 -5
  10. package/src/lib/components/mailboxes-section.js +61 -26
  11. package/src/lib/components/mailboxes-section.js.map +1 -1
  12. package/src/lib/components/sequence-detail.js +1 -1
  13. package/src/lib/components/sequence-detail.js.map +1 -1
  14. package/src/lib/components/sequence-editor.js +29 -4
  15. package/src/lib/components/sequence-editor.js.map +1 -1
  16. package/src/lib/components/sequence-mailbox-picker.d.ts +21 -0
  17. package/src/lib/components/sequence-mailbox-picker.js +74 -13
  18. package/src/lib/components/sequence-mailbox-picker.js.map +1 -1
  19. package/src/lib/components/use-outreach-mailbox-api.d.ts +10 -2
  20. package/src/lib/components/use-outreach-mailbox-api.js +7 -2
  21. package/src/lib/components/use-outreach-mailbox-api.js.map +1 -1
  22. package/src/lib/engine/enrollment-state.d.ts +6 -0
  23. package/src/lib/engine/enrollment-state.js +1 -1
  24. package/src/lib/engine/enrollment-state.js.map +1 -1
  25. package/src/lib/engine/index.d.ts +1 -0
  26. package/src/lib/engine/index.js +1 -0
  27. package/src/lib/engine/index.js.map +1 -1
  28. package/src/lib/engine/mailbox-notice.d.ts +5 -2
  29. package/src/lib/engine/mailbox-notice.js +6 -2
  30. package/src/lib/engine/mailbox-notice.js.map +1 -1
  31. package/src/lib/engine/mailbox-rotation.d.ts +41 -0
  32. package/src/lib/engine/mailbox-rotation.js +71 -0
  33. package/src/lib/engine/mailbox-rotation.js.map +1 -0
  34. package/src/lib/mailboxes/mailbox-api.d.ts +30 -11
  35. package/src/lib/mailboxes/mailbox-api.js +7 -3
  36. package/src/lib/mailboxes/mailbox-api.js.map +1 -1
  37. package/src/lib/mailboxes/mailbox-credentials.d.ts +15 -12
  38. package/src/lib/mailboxes/mailbox-credentials.js +12 -8
  39. package/src/lib/mailboxes/mailbox-credentials.js.map +1 -1
  40. package/src/lib/mailboxes/mailbox-erasure.js +3 -0
  41. package/src/lib/mailboxes/mailbox-erasure.js.map +1 -1
  42. package/src/lib/mailboxes/mailbox-revoke.d.ts +10 -5
  43. package/src/lib/mailboxes/mailbox-revoke.js +1 -0
  44. package/src/lib/mailboxes/mailbox-revoke.js.map +1 -1
  45. package/src/lib/mailboxes/mailbox-routes.d.ts +11 -3
  46. package/src/lib/mailboxes/mailbox-routes.js +218 -35
  47. package/src/lib/mailboxes/mailbox-routes.js.map +1 -1
  48. package/src/lib/mailboxes/mailbox-settings.d.ts +6 -2
  49. package/src/lib/mailboxes/mailbox-settings.js +5 -1
  50. package/src/lib/mailboxes/mailbox-settings.js.map +1 -1
  51. package/src/lib/mailboxes/mailbox-transport.d.ts +15 -9
  52. package/src/lib/mailboxes/mailbox-transport.js +47 -17
  53. package/src/lib/mailboxes/mailbox-transport.js.map +1 -1
  54. package/src/lib/mailboxes/oauth-state.d.ts +11 -3
  55. package/src/lib/mailboxes/oauth-state.js +17 -8
  56. package/src/lib/mailboxes/oauth-state.js.map +1 -1
  57. package/src/lib/mailboxes/outreach-config.d.ts +30 -0
  58. package/src/lib/mailboxes/outreach-config.js +53 -10
  59. package/src/lib/mailboxes/outreach-config.js.map +1 -1
  60. package/src/lib/mailboxes/register-mailbox-routes.js +2 -1
  61. package/src/lib/mailboxes/register-mailbox-routes.js.map +1 -1
  62. package/src/lib/model/outreach.types.d.ts +13 -2
  63. package/src/lib/model/outreach.types.js +7 -2
  64. package/src/lib/model/outreach.types.js.map +1 -1
  65. package/src/lib/model/sequence-draft.d.ts +6 -1
  66. package/src/lib/model/sequence-draft.js +16 -4
  67. package/src/lib/model/sequence-draft.js.map +1 -1
  68. package/src/lib/routes/enroll-routes.js +52 -1
  69. package/src/lib/routes/enroll-routes.js.map +1 -1
  70. package/src/lib/routes/sequence-routes.js +11 -3
  71. package/src/lib/routes/sequence-routes.js.map +1 -1
  72. package/src/lib/runtime/fixtures/fake-gmail.d.ts +18 -3
  73. package/src/lib/runtime/fixtures/fake-gmail.js +21 -1
  74. package/src/lib/runtime/fixtures/fake-gmail.js.map +1 -1
  75. package/src/lib/runtime/mailbox-notices.js +2 -2
  76. package/src/lib/runtime/mailbox-notices.js.map +1 -1
  77. package/src/lib/runtime/send-job.js +2 -8
  78. package/src/lib/runtime/send-job.js.map +1 -1
  79. package/src/lib/runtime/sync-job.js +24 -11
  80. package/src/lib/runtime/sync-job.js.map +1 -1
  81. package/src/lib/subprocessors.d.ts +12 -1
  82. package/src/lib/subprocessors.js +25 -2
  83. package/src/lib/subprocessors.js.map +1 -1
  84. package/src/lib/transfer/sequences-package.js +7 -2
  85. package/src/lib/transfer/sequences-package.js.map +1 -1
  86. package/src/lib/transport/gmail-client.d.ts +4 -11
  87. package/src/lib/transport/gmail-client.js +42 -17
  88. package/src/lib/transport/gmail-client.js.map +1 -1
  89. package/src/lib/transport/graph-client.d.ts +87 -0
  90. package/src/lib/transport/graph-client.js +359 -0
  91. package/src/lib/transport/graph-client.js.map +1 -0
  92. package/src/lib/transport/mail-client.d.ts +138 -0
  93. package/src/lib/transport/mail-client.js +34 -0
  94. package/src/lib/transport/mail-client.js.map +1 -0
  95. package/src/lib/transport/microsoft-errors.d.ts +32 -0
  96. package/src/lib/transport/microsoft-errors.js +94 -0
  97. package/src/lib/transport/microsoft-errors.js.map +1 -0
  98. package/src/lib/transport/microsoft-oauth.d.ts +140 -0
  99. package/src/lib/transport/microsoft-oauth.js +225 -0
  100. package/src/lib/transport/microsoft-oauth.js.map +1 -0
  101. package/src/lib/transport/mime-message.d.ts +30 -0
  102. package/src/lib/transport/mime-message.js +235 -0
  103. package/src/lib/transport/mime-message.js.map +1 -0
  104. package/src/lib/transport/send-message.d.ts +15 -8
  105. package/src/lib/transport/send-message.js +4 -3
  106. package/src/lib/transport/send-message.js.map +1 -1
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
- import { Alert, MenuItem, Stack, TextField, Typography } from "@mui/material";
18
+ import { Alert, Checkbox, ListItemText, MenuItem, Stack, TextField, Typography } from "@mui/material";
19
19
  import { useEffect, useState } from "react";
20
20
  import { OUTREACH_MAILBOX_STATUS_LABELS } from "../mailboxes/mailbox-settings.js";
21
21
  import { outreachMailboxActivationIssue } from "../model/sequence-draft.js";
@@ -39,18 +39,10 @@ import { useOutreachMailboxApi } from "./use-outreach-mailbox-api.js";
39
39
  return mailboxes.filter((mailbox)=>mailbox.id === input.value || mailbox.status !== 'disconnected' && (input.canManageAll || input.uid !== null && mailbox.connectedByUid === input.uid));
40
40
  }
41
41
  /**
42
- * The mailbox a sequence sends from (AGL-2980), chosen from the Mailboxes
43
- * section's own listener and route (AGL-2978).
44
- *
45
- * Whether the member may choose a colleague's mailbox is the mailbox
46
- * routes' answer (`availability().canManageAll`); until it arrives, or when
47
- * it cannot be had, the member is offered their own, which is always
48
- * allowed. A chosen mailbox that is paused or waiting to be reconnected can
49
- * hold a draft, and the field says what activating will need.
50
- */ export function OutreachSequenceMailboxPicker(props) {
51
- var _offered_find, _ref, _props_error;
52
- const { mailboxes, uid, value } = props;
53
- const api = useOutreachMailboxApi(props.orgId);
42
+ * Whether the member may choose a colleague's mailbox: the mailbox routes'
43
+ * `availability().canManageAll`, false until it arrives or when it cannot be had.
44
+ */ function useCanManageAllMailboxes(orgId) {
45
+ const api = useOutreachMailboxApi(orgId);
54
46
  const [canManageAll, setCanManageAll] = useState(false);
55
47
  useEffect(()=>{
56
48
  let current = true;
@@ -63,6 +55,21 @@ import { useOutreachMailboxApi } from "./use-outreach-mailbox-api.js";
63
55
  }, [
64
56
  api
65
57
  ]);
58
+ return canManageAll;
59
+ }
60
+ /**
61
+ * The mailbox a sequence sends from (AGL-2980), chosen from the Mailboxes
62
+ * section's own listener and route (AGL-2978).
63
+ *
64
+ * Whether the member may choose a colleague's mailbox is the mailbox
65
+ * routes' answer (`availability().canManageAll`); until it arrives, or when
66
+ * it cannot be had, the member is offered their own, which is always
67
+ * allowed. A chosen mailbox that is paused or waiting to be reconnected can
68
+ * hold a draft, and the field says what activating will need.
69
+ */ export function OutreachSequenceMailboxPicker(props) {
70
+ var _offered_find, _ref, _props_error;
71
+ const { mailboxes, uid, value } = props;
72
+ const canManageAll = useCanManageAllMailboxes(props.orgId);
66
73
  if (mailboxes.status === 'loading') {
67
74
  return /*#__PURE__*/ _jsx(TextField, {
68
75
  label: "Mailbox",
@@ -132,6 +139,60 @@ import { useOutreachMailboxApi } from "./use-outreach-mailbox-api.js";
132
139
  });
133
140
  }
134
141
  OutreachSequenceMailboxPicker.displayName = 'OutreachSequenceMailboxPicker';
142
+ /**
143
+ * The other mailboxes a sequence sends from in rotation (AGL-3489): each
144
+ * person enrolled is given one of them or the sequence's own, and all of
145
+ * their emails go from it. Offered once the sequence has a mailbox, from
146
+ * the same mailboxes the member may choose for it.
147
+ */ export function OutreachSequenceRotationPicker(props) {
148
+ var _props_error;
149
+ const { mailboxes, uid, mailboxId } = props;
150
+ const canManageAll = useCanManageAllMailboxes(props.orgId);
151
+ if (mailboxes.status !== 'ready' || !mailboxId) return null;
152
+ const chosen = props.value.filter((id)=>id !== mailboxId);
153
+ const offered = mailboxes.mailboxes.filter((mailbox)=>mailbox.id !== mailboxId && (chosen.includes(mailbox.id) || mailbox.status !== 'disconnected' && (canManageAll || uid !== null && mailbox.connectedByUid === uid)));
154
+ if (!offered.length && !chosen.length) return null;
155
+ const labelOf = (id)=>{
156
+ const mailbox = mailboxes.mailboxes.find((entry)=>entry.id === id);
157
+ return mailbox ? mailbox.sendAs || mailbox.email : 'A mailbox no longer connected';
158
+ };
159
+ return /*#__PURE__*/ _jsx(TextField, {
160
+ select: true,
161
+ label: "Also send from",
162
+ value: chosen,
163
+ onChange: (event)=>{
164
+ const next = event.target.value;
165
+ props.onChange((Array.isArray(next) ? next : String(next).split(',')).filter(Boolean));
166
+ },
167
+ disabled: props.disabled,
168
+ error: Boolean(props.error),
169
+ helperText: (_props_error = props.error) != null ? _props_error : 'Each person enrolled is given one of these mailboxes or the one above, whichever has the fewest people waiting, and all of their emails go from it.',
170
+ slotProps: {
171
+ select: {
172
+ multiple: true,
173
+ displayEmpty: true,
174
+ renderValue: (selected)=>selected.length ? selected.map(labelOf).join(', ') : 'No other mailboxes'
175
+ },
176
+ inputLabel: {
177
+ shrink: true
178
+ }
179
+ },
180
+ fullWidth: true,
181
+ children: offered.map((mailbox)=>/*#__PURE__*/ _jsxs(MenuItem, {
182
+ value: mailbox.id,
183
+ children: [
184
+ /*#__PURE__*/ _jsx(Checkbox, {
185
+ size: "small",
186
+ checked: chosen.includes(mailbox.id)
187
+ }),
188
+ /*#__PURE__*/ _jsx(ListItemText, {
189
+ primary: outreachMailboxOptionLabel(mailbox)
190
+ })
191
+ ]
192
+ }, mailbox.id))
193
+ });
194
+ }
195
+ OutreachSequenceRotationPicker.displayName = 'OutreachSequenceRotationPicker';
135
196
  export default OutreachSequenceMailboxPicker;
136
197
 
137
198
  //# sourceMappingURL=sequence-mailbox-picker.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequence-mailbox-picker.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 { Alert, MenuItem, Stack, TextField, Typography } from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { OUTREACH_MAILBOX_STATUS_LABELS } from '../mailboxes/mailbox-settings'\nimport type { OutreachMailbox } from '../model/outreach.types'\nimport { outreachMailboxActivationIssue } from '../model/sequence-draft'\nimport { OutreachLink } from './outreach-ui'\nimport { useOutreachMailboxApi } from './use-outreach-mailbox-api'\nimport type { OutreachMailboxesResult } from './use-outreach-mailboxes'\n\nexport interface OutreachSequenceMailboxPickerProps {\n orgId: string\n /** The member choosing, whose own mailboxes are offered. */\n uid: string | null\n /** The organization's mailboxes, from the Mailboxes section's own listener. */\n mailboxes: OutreachMailboxesResult\n /** The Mailboxes section, where a mailbox is connected, resumed or reconnected. */\n mailboxesPath: string\n value: string\n onChange(mailboxId: string): void\n error?: string\n disabled?: boolean\n}\n\n/** A mailbox as its line in the menu reads: who mail is from, and any status that stops it sending. */\nexport function outreachMailboxOptionLabel(mailbox: OutreachMailbox): string {\n const address = mailbox.sendAs || mailbox.email\n const name = mailbox.displayName?.trim()\n const from = name ? `${name} <${address}>` : address\n return mailbox.status === 'connected'\n ? from\n : `${from} — ${OUTREACH_MAILBOX_STATUS_LABELS[mailbox.status] ?? mailbox.status}`\n}\n\n/**\n * The mailboxes a member may choose for a sequence: their own, or every\n * one for an organization owner or admin — the two the save route lets\n * send from a mailbox — never a disconnected one. The mailbox a sequence\n * already names stays in the list whoever connected it, so the field never\n * reads as empty.\n */\nexport function outreachOfferedMailboxes(\n mailboxes: readonly OutreachMailbox[],\n input: { uid: string | null; canManageAll: boolean; value: string },\n): OutreachMailbox[] {\n return mailboxes.filter(\n (mailbox) =>\n mailbox.id === input.value ||\n (mailbox.status !== 'disconnected' &&\n (input.canManageAll || (input.uid !== null && mailbox.connectedByUid === input.uid))),\n )\n}\n\n/**\n * The mailbox a sequence sends from (AGL-2980), chosen from the Mailboxes\n * section's own listener and route (AGL-2978).\n *\n * Whether the member may choose a colleague's mailbox is the mailbox\n * routes' answer (`availability().canManageAll`); until it arrives, or when\n * it cannot be had, the member is offered their own, which is always\n * allowed. A chosen mailbox that is paused or waiting to be reconnected can\n * hold a draft, and the field says what activating will need.\n */\nexport function OutreachSequenceMailboxPicker(props: OutreachSequenceMailboxPickerProps) {\n const { mailboxes, uid, value } = props\n const api = useOutreachMailboxApi(props.orgId)\n const [canManageAll, setCanManageAll] = useState(false)\n\n useEffect(() => {\n let current = true\n api\n .availability()\n .then((answer) => {\n if (current) setCanManageAll(answer.canManageAll)\n })\n .catch(() => undefined)\n return () => {\n current = false\n }\n }, [api])\n\n if (mailboxes.status === 'loading') {\n return <TextField label=\"Mailbox\" value=\"\" disabled helperText=\"Loading mailboxes…\" fullWidth />\n }\n if (mailboxes.status === 'error') {\n return (\n <Alert severity=\"error\">The mailboxes could not be loaded. Reload the page to try again.</Alert>\n )\n }\n\n const offered = outreachOfferedMailboxes(mailboxes.mailboxes, { uid, canManageAll, value })\n if (!offered.length) {\n return (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\" component=\"p\">\n Mailbox\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n A sequence sends from your own connected mailbox, and you haven’t connected one yet.\n </Typography>\n <Typography variant=\"body2\">\n <OutreachLink href={props.mailboxesPath}>Connect a mailbox in Mailboxes</OutreachLink>\n </Typography>\n {props.error ? (\n <Typography variant=\"body2\" color=\"error\">\n {props.error}\n </Typography>\n ) : null}\n </Stack>\n )\n }\n\n const chosen = offered.find((mailbox) => mailbox.id === value) ?? null\n // A chosen mailbox that cannot send, or one that no longer exists, says\n // what activating the sequence will need.\n const standing = value ? outreachMailboxActivationIssue(value, chosen) : null\n return (\n <TextField\n select\n label=\"Mailbox\"\n value={chosen ? value : ''}\n onChange={(event) => props.onChange(event.target.value)}\n disabled={props.disabled}\n error={Boolean(props.error)}\n helperText={\n props.error ??\n standing?.message ??\n 'Every email in the sequence is sent from it, in its sending hours and timezone.'\n }\n fullWidth\n >\n {offered.map((mailbox) => (\n <MenuItem key={mailbox.id} value={mailbox.id}>\n {outreachMailboxOptionLabel(mailbox)}\n </MenuItem>\n ))}\n </TextField>\n )\n}\nOutreachSequenceMailboxPicker.displayName = 'OutreachSequenceMailboxPicker'\n\nexport default OutreachSequenceMailboxPicker\n"],"names":["Alert","MenuItem","Stack","TextField","Typography","useEffect","useState","OUTREACH_MAILBOX_STATUS_LABELS","outreachMailboxActivationIssue","OutreachLink","useOutreachMailboxApi","outreachMailboxOptionLabel","mailbox","address","sendAs","email","name","displayName","trim","from","status","outreachOfferedMailboxes","mailboxes","input","filter","id","value","canManageAll","uid","connectedByUid","OutreachSequenceMailboxPicker","props","offered","api","orgId","setCanManageAll","current","availability","then","answer","catch","undefined","label","disabled","helperText","fullWidth","severity","length","spacing","variant","component","color","href","mailboxesPath","error","chosen","find","standing","select","onChange","event","target","Boolean","message","map"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/sequence-mailbox-picker.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 { Alert, Checkbox, ListItemText, MenuItem, Stack, TextField, Typography } from '@mui/material'\nimport { useEffect, useState } from 'react'\nimport { OUTREACH_MAILBOX_STATUS_LABELS } from '../mailboxes/mailbox-settings'\nimport type { OutreachMailbox } from '../model/outreach.types'\nimport { outreachMailboxActivationIssue } from '../model/sequence-draft'\nimport { OutreachLink } from './outreach-ui'\nimport { useOutreachMailboxApi } from './use-outreach-mailbox-api'\nimport type { OutreachMailboxesResult } from './use-outreach-mailboxes'\n\nexport interface OutreachSequenceMailboxPickerProps {\n orgId: string\n /** The member choosing, whose own mailboxes are offered. */\n uid: string | null\n /** The organization's mailboxes, from the Mailboxes section's own listener. */\n mailboxes: OutreachMailboxesResult\n /** The Mailboxes section, where a mailbox is connected, resumed or reconnected. */\n mailboxesPath: string\n value: string\n onChange(mailboxId: string): void\n error?: string\n disabled?: boolean\n}\n\n/** A mailbox as its line in the menu reads: who mail is from, and any status that stops it sending. */\nexport function outreachMailboxOptionLabel(mailbox: OutreachMailbox): string {\n const address = mailbox.sendAs || mailbox.email\n const name = mailbox.displayName?.trim()\n const from = name ? `${name} <${address}>` : address\n return mailbox.status === 'connected'\n ? from\n : `${from} — ${OUTREACH_MAILBOX_STATUS_LABELS[mailbox.status] ?? mailbox.status}`\n}\n\n/**\n * The mailboxes a member may choose for a sequence: their own, or every\n * one for an organization owner or admin — the two the save route lets\n * send from a mailbox — never a disconnected one. The mailbox a sequence\n * already names stays in the list whoever connected it, so the field never\n * reads as empty.\n */\nexport function outreachOfferedMailboxes(\n mailboxes: readonly OutreachMailbox[],\n input: { uid: string | null; canManageAll: boolean; value: string },\n): OutreachMailbox[] {\n return mailboxes.filter(\n (mailbox) =>\n mailbox.id === input.value ||\n (mailbox.status !== 'disconnected' &&\n (input.canManageAll || (input.uid !== null && mailbox.connectedByUid === input.uid))),\n )\n}\n\n/**\n * Whether the member may choose a colleague's mailbox: the mailbox routes'\n * `availability().canManageAll`, false until it arrives or when it cannot be had.\n */\nfunction useCanManageAllMailboxes(orgId: string): boolean {\n const api = useOutreachMailboxApi(orgId)\n const [canManageAll, setCanManageAll] = useState(false)\n useEffect(() => {\n let current = true\n api\n .availability()\n .then((answer) => {\n if (current) setCanManageAll(answer.canManageAll)\n })\n .catch(() => undefined)\n return () => {\n current = false\n }\n }, [api])\n return canManageAll\n}\n\n/**\n * The mailbox a sequence sends from (AGL-2980), chosen from the Mailboxes\n * section's own listener and route (AGL-2978).\n *\n * Whether the member may choose a colleague's mailbox is the mailbox\n * routes' answer (`availability().canManageAll`); until it arrives, or when\n * it cannot be had, the member is offered their own, which is always\n * allowed. A chosen mailbox that is paused or waiting to be reconnected can\n * hold a draft, and the field says what activating will need.\n */\nexport function OutreachSequenceMailboxPicker(props: OutreachSequenceMailboxPickerProps) {\n const { mailboxes, uid, value } = props\n const canManageAll = useCanManageAllMailboxes(props.orgId)\n\n if (mailboxes.status === 'loading') {\n return <TextField label=\"Mailbox\" value=\"\" disabled helperText=\"Loading mailboxes…\" fullWidth />\n }\n if (mailboxes.status === 'error') {\n return (\n <Alert severity=\"error\">The mailboxes could not be loaded. Reload the page to try again.</Alert>\n )\n }\n\n const offered = outreachOfferedMailboxes(mailboxes.mailboxes, { uid, canManageAll, value })\n if (!offered.length) {\n return (\n <Stack spacing={0.5}>\n <Typography variant=\"subtitle2\" component=\"p\">\n Mailbox\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n A sequence sends from your own connected mailbox, and you haven’t connected one yet.\n </Typography>\n <Typography variant=\"body2\">\n <OutreachLink href={props.mailboxesPath}>Connect a mailbox in Mailboxes</OutreachLink>\n </Typography>\n {props.error ? (\n <Typography variant=\"body2\" color=\"error\">\n {props.error}\n </Typography>\n ) : null}\n </Stack>\n )\n }\n\n const chosen = offered.find((mailbox) => mailbox.id === value) ?? null\n // A chosen mailbox that cannot send, or one that no longer exists, says\n // what activating the sequence will need.\n const standing = value ? outreachMailboxActivationIssue(value, chosen) : null\n return (\n <TextField\n select\n label=\"Mailbox\"\n value={chosen ? value : ''}\n onChange={(event) => props.onChange(event.target.value)}\n disabled={props.disabled}\n error={Boolean(props.error)}\n helperText={\n props.error ??\n standing?.message ??\n 'Every email in the sequence is sent from it, in its sending hours and timezone.'\n }\n fullWidth\n >\n {offered.map((mailbox) => (\n <MenuItem key={mailbox.id} value={mailbox.id}>\n {outreachMailboxOptionLabel(mailbox)}\n </MenuItem>\n ))}\n </TextField>\n )\n}\nOutreachSequenceMailboxPicker.displayName = 'OutreachSequenceMailboxPicker'\n\nexport interface OutreachSequenceRotationPickerProps {\n orgId: string\n uid: string | null\n mailboxes: OutreachMailboxesResult\n /** The sequence's own mailbox, which is never offered here. */\n mailboxId: string\n value: readonly string[]\n onChange(mailboxIds: string[]): void\n error?: string\n disabled?: boolean\n}\n\n/**\n * The other mailboxes a sequence sends from in rotation (AGL-3489): each\n * person enrolled is given one of them or the sequence's own, and all of\n * their emails go from it. Offered once the sequence has a mailbox, from\n * the same mailboxes the member may choose for it.\n */\nexport function OutreachSequenceRotationPicker(props: OutreachSequenceRotationPickerProps) {\n const { mailboxes, uid, mailboxId } = props\n const canManageAll = useCanManageAllMailboxes(props.orgId)\n if (mailboxes.status !== 'ready' || !mailboxId) return null\n const chosen = props.value.filter((id) => id !== mailboxId)\n const offered = mailboxes.mailboxes.filter(\n (mailbox) =>\n mailbox.id !== mailboxId &&\n (chosen.includes(mailbox.id) ||\n (mailbox.status !== 'disconnected' &&\n (canManageAll || (uid !== null && mailbox.connectedByUid === uid)))),\n )\n if (!offered.length && !chosen.length) return null\n const labelOf = (id: string) => {\n const mailbox = mailboxes.mailboxes.find((entry) => entry.id === id)\n return mailbox ? mailbox.sendAs || mailbox.email : 'A mailbox no longer connected'\n }\n return (\n <TextField\n select\n label=\"Also send from\"\n value={chosen}\n onChange={(event) => {\n const next = event.target.value as unknown\n props.onChange((Array.isArray(next) ? next : String(next).split(',')).filter(Boolean))\n }}\n disabled={props.disabled}\n error={Boolean(props.error)}\n helperText={\n props.error ??\n 'Each person enrolled is given one of these mailboxes or the one above, whichever has the fewest people waiting, and all of their emails go from it.'\n }\n slotProps={{\n select: {\n multiple: true,\n displayEmpty: true,\n renderValue: (selected) =>\n (selected as string[]).length ? (selected as string[]).map(labelOf).join(', ') : 'No other mailboxes',\n },\n inputLabel: { shrink: true },\n }}\n fullWidth\n >\n {offered.map((mailbox) => (\n <MenuItem key={mailbox.id} value={mailbox.id}>\n <Checkbox size=\"small\" checked={chosen.includes(mailbox.id)} />\n <ListItemText primary={outreachMailboxOptionLabel(mailbox)} />\n </MenuItem>\n ))}\n </TextField>\n )\n}\nOutreachSequenceRotationPicker.displayName = 'OutreachSequenceRotationPicker'\n\nexport default OutreachSequenceMailboxPicker\n"],"names":["Alert","Checkbox","ListItemText","MenuItem","Stack","TextField","Typography","useEffect","useState","OUTREACH_MAILBOX_STATUS_LABELS","outreachMailboxActivationIssue","OutreachLink","useOutreachMailboxApi","outreachMailboxOptionLabel","mailbox","address","sendAs","email","name","displayName","trim","from","status","outreachOfferedMailboxes","mailboxes","input","filter","id","value","canManageAll","uid","connectedByUid","useCanManageAllMailboxes","orgId","api","setCanManageAll","current","availability","then","answer","catch","undefined","OutreachSequenceMailboxPicker","props","offered","label","disabled","helperText","fullWidth","severity","length","spacing","variant","component","color","href","mailboxesPath","error","chosen","find","standing","select","onChange","event","target","Boolean","message","map","OutreachSequenceRotationPicker","mailboxId","includes","labelOf","entry","next","Array","isArray","String","split","slotProps","multiple","displayEmpty","renderValue","selected","join","inputLabel","shrink","size","checked","primary"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import type { OutreachApiRefusalReason, OutreachConnectCompleteResponse, OutreachMailboxAvailability, OutreachMailboxDisconnectResponse, OutreachMailboxReadinessResponse, OutreachMailboxResponse, OutreachMailboxSettingsRequest, OutreachMailboxTestResponse } from '../mailboxes/mailbox-api';
2
+ import type { OutreachMailboxProvider } from '../model/outreach.types';
2
3
  /** A mailbox route's refusal, carrying the sentence and the stable reason. */
3
4
  export declare class OutreachApiError extends Error {
4
5
  readonly reason: OutreachApiRefusalReason | 'unreachable';
@@ -8,8 +9,15 @@ export declare class OutreachApiError extends Error {
8
9
  /** The mailbox routes, as the Mailboxes panel calls them. */
9
10
  export interface OutreachMailboxApi {
10
11
  availability(): Promise<OutreachMailboxAvailability>;
11
- /** Google's consent address for a new connect, or a reconnect. */
12
- connect(): Promise<string>;
12
+ /**
13
+ * The provider's consent address for a new connect, or a reconnect —
14
+ * Google's unless Microsoft is named (AGL-3489). `loginHint` suggests the
15
+ * account, such as the mailbox being reconnected.
16
+ */
17
+ connect(options?: {
18
+ provider?: OutreachMailboxProvider;
19
+ loginHint?: string;
20
+ }): Promise<string>;
13
21
  complete(input: {
14
22
  code: string;
15
23
  state: string;
@@ -63,8 +63,13 @@ import { OUTREACH_API_ROUTES } from "../constants/api-routes.js";
63
63
  availability: ()=>call(OUTREACH_API_ROUTES.mailboxesAvailability, {
64
64
  method: 'GET'
65
65
  }),
66
- connect: async ()=>(await call(OUTREACH_API_ROUTES.mailboxesConnect, {
67
- method: 'POST'
66
+ connect: async (options)=>(await call(OUTREACH_API_ROUTES.mailboxesConnect, {
67
+ method: 'POST',
68
+ body: _extends({}, (options == null ? void 0 : options.provider) && options.provider !== 'google' ? {
69
+ provider: options.provider
70
+ } : {}, (options == null ? void 0 : options.loginHint) ? {
71
+ loginHint: options.loginHint
72
+ } : {})
68
73
  })).url,
69
74
  complete: (input)=>call(OUTREACH_API_ROUTES.mailboxesConnectComplete, {
70
75
  method: 'POST',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-mailbox-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 {\n OutreachApiRefusalReason,\n OutreachConnectCompleteResponse,\n OutreachConnectResponse,\n OutreachMailboxAvailability,\n OutreachMailboxDisconnectResponse,\n OutreachMailboxReadinessResponse,\n OutreachMailboxResponse,\n OutreachMailboxSettingsRequest,\n OutreachMailboxTestResponse,\n} from '../mailboxes/mailbox-api'\n\n/** A mailbox route's refusal, carrying the sentence and the stable reason. */\nexport class OutreachApiError extends Error {\n readonly reason: OutreachApiRefusalReason | 'unreachable'\n readonly status: number\n\n constructor(message: string, reason: OutreachApiRefusalReason | 'unreachable', status: number) {\n super(message)\n this.name = 'OutreachApiError'\n this.reason = reason\n this.status = status\n }\n}\n\n/** The mailbox routes, as the Mailboxes panel calls them. */\nexport interface OutreachMailboxApi {\n availability(): Promise<OutreachMailboxAvailability>\n /** Google's consent address for a new connect, or a reconnect. */\n connect(): Promise<string>\n complete(input: { code: string; state: string; timezone?: string }): Promise<OutreachConnectCompleteResponse>\n saveSettings(\n input: Omit<OutreachMailboxSettingsRequest, 'orgId'>,\n ): Promise<OutreachMailboxResponse>\n setPaused(mailboxId: string, paused: boolean): Promise<OutreachMailboxResponse>\n /** A test from the mailbox — to `to`, or to the account's own address. */\n sendTest(mailboxId: string, to?: string): Promise<OutreachMailboxTestResponse>\n disconnect(mailboxId: string): Promise<OutreachMailboxDisconnectResponse>\n /** SPF, DKIM and DMARC for the address it sends as; `fresh` skips the server's brief memory. */\n readiness(mailboxId: string, fresh?: boolean): Promise<OutreachMailboxReadinessResponse>\n}\n\n/**\n * The Mailboxes panel's one door to the mailbox routes (AGL-2978), with the\n * member's session attached and the organization named on every call — the\n * dispatcher's release gate reads it. A refusal is thrown as an\n * {@link OutreachApiError} carrying the route's own sentence, which is\n * written for the person reading it.\n */\nexport function useOutreachMailboxApi(orgId: string | null): OutreachMailboxApi {\n const { data: user } = useUser()\n\n const call = useCallback(\n async <T>(route: string, init: { method: 'GET' | 'POST'; body?: Record<string, unknown> }): Promise<T> => {\n if (!orgId) throw new OutreachApiError('Open an organization first.', 'org-required', 400)\n const path =\n init.method === 'GET'\n ? `/api/${route}?orgId=${encodeURIComponent(orgId)}`\n : `/api/${route}`\n const 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 const payload = (await response.json().catch(() => null)) as\n | (T & { error?: undefined })\n | { error?: string; reason?: OutreachApiRefusalReason }\n | null\n if (!response.ok || !payload) {\n const refusal = (payload ?? {}) as { error?: string; reason?: OutreachApiRefusalReason }\n throw new OutreachApiError(\n refusal.error || 'Sequences could not be reached. Try again.',\n refusal.reason ?? 'unreachable',\n response.status,\n )\n }\n return payload as T\n },\n [user, orgId],\n )\n\n return useMemo<OutreachMailboxApi>(\n () => ({\n availability: () =>\n call<OutreachMailboxAvailability>(OUTREACH_API_ROUTES.mailboxesAvailability, { method: 'GET' }),\n connect: async () =>\n (await call<OutreachConnectResponse>(OUTREACH_API_ROUTES.mailboxesConnect, { method: 'POST' })).url,\n complete: (input) =>\n call<OutreachConnectCompleteResponse>(OUTREACH_API_ROUTES.mailboxesConnectComplete, {\n method: 'POST',\n body: input,\n }),\n saveSettings: (input) =>\n call<OutreachMailboxResponse>(OUTREACH_API_ROUTES.mailboxesSettings, {\n method: 'POST',\n body: input as unknown as Record<string, unknown>,\n }),\n setPaused: (mailboxId, paused) =>\n call<OutreachMailboxResponse>(OUTREACH_API_ROUTES.mailboxesStatus, {\n method: 'POST',\n body: { mailboxId, paused },\n }),\n sendTest: (mailboxId, to) =>\n call<OutreachMailboxTestResponse>(OUTREACH_API_ROUTES.mailboxesTest, {\n method: 'POST',\n body: { mailboxId, ...(to ? { to } : {}) },\n }),\n disconnect: (mailboxId) =>\n call<OutreachMailboxDisconnectResponse>(OUTREACH_API_ROUTES.mailboxesDisconnect, {\n method: 'POST',\n body: { mailboxId },\n }),\n readiness: (mailboxId, fresh) =>\n call<OutreachMailboxReadinessResponse>(OUTREACH_API_ROUTES.mailboxesReadiness, {\n method: 'POST',\n body: { mailboxId, ...(fresh ? { fresh: true } : {}) },\n }),\n }),\n [call],\n )\n}\n"],"names":["authorizedFetch","useUser","useCallback","useMemo","OUTREACH_API_ROUTES","OutreachApiError","Error","message","reason","status","name","useOutreachMailboxApi","orgId","data","user","call","route","init","path","method","encodeURIComponent","response","headers","body","JSON","stringify","payload","json","catch","ok","refusal","error","availability","mailboxesAvailability","connect","mailboxesConnect","url","complete","input","mailboxesConnectComplete","saveSettings","mailboxesSettings","setPaused","mailboxId","paused","mailboxesStatus","sendTest","to","mailboxesTest","disconnect","mailboxesDisconnect","readiness","fresh","mailboxesReadiness"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAASA,eAAe,QAAQ,2CAA0C;AAC1E,SAASC,OAAO,QAAQ,iCAAgC;AACxD,SAASC,WAAW,EAAEC,OAAO,QAAQ,QAAO;AAC5C,SAASC,mBAAmB,QAAQ,6BAAyB;AAa7D,4EAA4E,GAC5E,OAAO,MAAMC,yBAAyBC;IAIpC,YAAYC,OAAe,EAAEC,MAAgD,EAAEC,MAAc,CAAE;QAC7F,KAAK,CAACF;QACN,IAAI,CAACG,IAAI,GAAG;QACZ,IAAI,CAACF,MAAM,GAAGA;QACd,IAAI,CAACC,MAAM,GAAGA;IAChB;AACF;AAmBA;;;;;;CAMC,GACD,OAAO,SAASE,sBAAsBC,KAAoB;IACxD,MAAM,EAAEC,MAAMC,IAAI,EAAE,GAAGb;IAEvB,MAAMc,OAAOb,YACX,OAAUc,OAAeC;QACvB,IAAI,CAACL,OAAO,MAAM,IAAIP,iBAAiB,+BAA+B,gBAAgB;QACtF,MAAMa,OACJD,KAAKE,MAAM,KAAK,QACZ,CAAC,KAAK,EAAEH,MAAM,OAAO,EAAEI,mBAAmBR,QAAQ,GAClD,CAAC,KAAK,EAAEI,OAAO;QACrB,MAAMK,WAAW,MAAMrB,gBAAgBc,MAAMI,MAAM;YACjDC,QAAQF,KAAKE,MAAM;WACfF,KAAKE,MAAM,KAAK,SAChB;YACEG,SAAS;gBAAE,gBAAgB;YAAmB;YAC9CC,MAAMC,KAAKC,SAAS,CAAC;gBAAEb;eAAUK,KAAKM,IAAI;QAC5C,IACA,CAAC;QAEP,MAAMG,UAAW,MAAML,SAASM,IAAI,GAAGC,KAAK,CAAC,IAAM;QAInD,IAAI,CAACP,SAASQ,EAAE,IAAI,CAACH,SAAS;gBAI1BI;YAHF,MAAMA,UAAWJ,kBAAAA,UAAW,CAAC;YAC7B,MAAM,IAAIrB,iBACRyB,QAAQC,KAAK,IAAI,+CACjBD,kBAAAA,QAAQtB,MAAM,YAAdsB,kBAAkB,eAClBT,SAASZ,MAAM;QAEnB;QACA,OAAOiB;IACT,GACA;QAACZ;QAAMF;KAAM;IAGf,OAAOT,QACL,IAAO,CAAA;YACL6B,cAAc,IACZjB,KAAkCX,oBAAoB6B,qBAAqB,EAAE;oBAAEd,QAAQ;gBAAM;YAC/Fe,SAAS,UACP,AAAC,CAAA,MAAMnB,KAA8BX,oBAAoB+B,gBAAgB,EAAE;oBAAEhB,QAAQ;gBAAO,EAAC,EAAGiB,GAAG;YACrGC,UAAU,CAACC,QACTvB,KAAsCX,oBAAoBmC,wBAAwB,EAAE;oBAClFpB,QAAQ;oBACRI,MAAMe;gBACR;YACFE,cAAc,CAACF,QACbvB,KAA8BX,oBAAoBqC,iBAAiB,EAAE;oBACnEtB,QAAQ;oBACRI,MAAMe;gBACR;YACFI,WAAW,CAACC,WAAWC,SACrB7B,KAA8BX,oBAAoByC,eAAe,EAAE;oBACjE1B,QAAQ;oBACRI,MAAM;wBAAEoB;wBAAWC;oBAAO;gBAC5B;YACFE,UAAU,CAACH,WAAWI,KACpBhC,KAAkCX,oBAAoB4C,aAAa,EAAE;oBACnE7B,QAAQ;oBACRI,MAAM;wBAAEoB;uBAAeI,KAAK;wBAAEA;oBAAG,IAAI,CAAC;gBACxC;YACFE,YAAY,CAACN,YACX5B,KAAwCX,oBAAoB8C,mBAAmB,EAAE;oBAC/E/B,QAAQ;oBACRI,MAAM;wBAAEoB;oBAAU;gBACpB;YACFQ,WAAW,CAACR,WAAWS,QACrBrC,KAAuCX,oBAAoBiD,kBAAkB,EAAE;oBAC7ElC,QAAQ;oBACRI,MAAM;wBAAEoB;uBAAeS,QAAQ;wBAAEA,OAAO;oBAAK,IAAI,CAAC;gBACpD;QACJ,CAAA,GACA;QAACrC;KAAK;AAEV"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/components/use-outreach-mailbox-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 {\n OutreachApiRefusalReason,\n OutreachConnectCompleteResponse,\n OutreachConnectResponse,\n OutreachMailboxAvailability,\n OutreachMailboxDisconnectResponse,\n OutreachMailboxReadinessResponse,\n OutreachMailboxResponse,\n OutreachMailboxSettingsRequest,\n OutreachMailboxTestResponse,\n} from '../mailboxes/mailbox-api'\nimport type { OutreachMailboxProvider } from '../model/outreach.types'\n\n/** A mailbox route's refusal, carrying the sentence and the stable reason. */\nexport class OutreachApiError extends Error {\n readonly reason: OutreachApiRefusalReason | 'unreachable'\n readonly status: number\n\n constructor(message: string, reason: OutreachApiRefusalReason | 'unreachable', status: number) {\n super(message)\n this.name = 'OutreachApiError'\n this.reason = reason\n this.status = status\n }\n}\n\n/** The mailbox routes, as the Mailboxes panel calls them. */\nexport interface OutreachMailboxApi {\n availability(): Promise<OutreachMailboxAvailability>\n /**\n * The provider's consent address for a new connect, or a reconnect —\n * Google's unless Microsoft is named (AGL-3489). `loginHint` suggests the\n * account, such as the mailbox being reconnected.\n */\n connect(options?: { provider?: OutreachMailboxProvider; loginHint?: string }): Promise<string>\n complete(input: { code: string; state: string; timezone?: string }): Promise<OutreachConnectCompleteResponse>\n saveSettings(\n input: Omit<OutreachMailboxSettingsRequest, 'orgId'>,\n ): Promise<OutreachMailboxResponse>\n setPaused(mailboxId: string, paused: boolean): Promise<OutreachMailboxResponse>\n /** A test from the mailbox — to `to`, or to the account's own address. */\n sendTest(mailboxId: string, to?: string): Promise<OutreachMailboxTestResponse>\n disconnect(mailboxId: string): Promise<OutreachMailboxDisconnectResponse>\n /** SPF, DKIM and DMARC for the address it sends as; `fresh` skips the server's brief memory. */\n readiness(mailboxId: string, fresh?: boolean): Promise<OutreachMailboxReadinessResponse>\n}\n\n/**\n * The Mailboxes panel's one door to the mailbox routes (AGL-2978), with the\n * member's session attached and the organization named on every call — the\n * dispatcher's release gate reads it. A refusal is thrown as an\n * {@link OutreachApiError} carrying the route's own sentence, which is\n * written for the person reading it.\n */\nexport function useOutreachMailboxApi(orgId: string | null): OutreachMailboxApi {\n const { data: user } = useUser()\n\n const call = useCallback(\n async <T>(route: string, init: { method: 'GET' | 'POST'; body?: Record<string, unknown> }): Promise<T> => {\n if (!orgId) throw new OutreachApiError('Open an organization first.', 'org-required', 400)\n const path =\n init.method === 'GET'\n ? `/api/${route}?orgId=${encodeURIComponent(orgId)}`\n : `/api/${route}`\n const 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 const payload = (await response.json().catch(() => null)) as\n | (T & { error?: undefined })\n | { error?: string; reason?: OutreachApiRefusalReason }\n | null\n if (!response.ok || !payload) {\n const refusal = (payload ?? {}) as { error?: string; reason?: OutreachApiRefusalReason }\n throw new OutreachApiError(\n refusal.error || 'Sequences could not be reached. Try again.',\n refusal.reason ?? 'unreachable',\n response.status,\n )\n }\n return payload as T\n },\n [user, orgId],\n )\n\n return useMemo<OutreachMailboxApi>(\n () => ({\n availability: () =>\n call<OutreachMailboxAvailability>(OUTREACH_API_ROUTES.mailboxesAvailability, { method: 'GET' }),\n connect: async (options) =>\n (\n await call<OutreachConnectResponse>(OUTREACH_API_ROUTES.mailboxesConnect, {\n method: 'POST',\n body: {\n ...(options?.provider && options.provider !== 'google' ? { provider: options.provider } : {}),\n ...(options?.loginHint ? { loginHint: options.loginHint } : {}),\n },\n })\n ).url,\n complete: (input) =>\n call<OutreachConnectCompleteResponse>(OUTREACH_API_ROUTES.mailboxesConnectComplete, {\n method: 'POST',\n body: input,\n }),\n saveSettings: (input) =>\n call<OutreachMailboxResponse>(OUTREACH_API_ROUTES.mailboxesSettings, {\n method: 'POST',\n body: input as unknown as Record<string, unknown>,\n }),\n setPaused: (mailboxId, paused) =>\n call<OutreachMailboxResponse>(OUTREACH_API_ROUTES.mailboxesStatus, {\n method: 'POST',\n body: { mailboxId, paused },\n }),\n sendTest: (mailboxId, to) =>\n call<OutreachMailboxTestResponse>(OUTREACH_API_ROUTES.mailboxesTest, {\n method: 'POST',\n body: { mailboxId, ...(to ? { to } : {}) },\n }),\n disconnect: (mailboxId) =>\n call<OutreachMailboxDisconnectResponse>(OUTREACH_API_ROUTES.mailboxesDisconnect, {\n method: 'POST',\n body: { mailboxId },\n }),\n readiness: (mailboxId, fresh) =>\n call<OutreachMailboxReadinessResponse>(OUTREACH_API_ROUTES.mailboxesReadiness, {\n method: 'POST',\n body: { mailboxId, ...(fresh ? { fresh: true } : {}) },\n }),\n }),\n [call],\n )\n}\n"],"names":["authorizedFetch","useUser","useCallback","useMemo","OUTREACH_API_ROUTES","OutreachApiError","Error","message","reason","status","name","useOutreachMailboxApi","orgId","data","user","call","route","init","path","method","encodeURIComponent","response","headers","body","JSON","stringify","payload","json","catch","ok","refusal","error","availability","mailboxesAvailability","connect","options","mailboxesConnect","provider","loginHint","url","complete","input","mailboxesConnectComplete","saveSettings","mailboxesSettings","setPaused","mailboxId","paused","mailboxesStatus","sendTest","to","mailboxesTest","disconnect","mailboxesDisconnect","readiness","fresh","mailboxesReadiness"],"mappings":"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"}
@@ -106,7 +106,13 @@ export declare function outreachAttestationsFrom(kinds: readonly unknown[], uid:
106
106
  export interface OutreachEnrollmentDraft {
107
107
  id: string;
108
108
  sequence: Pick<OutreachSequence, 'id' | 'hostId' | 'mailboxId' | 'steps' | 'settings' | 'campaignIds'>;
109
+ /** The mailbox the enrollment sends from, whose calendar its steps are scheduled on. */
109
110
  mailbox: ScheduleMailbox;
111
+ /**
112
+ * That mailbox's id when the sequence rotates (AGL-3489): one of its
113
+ * mailboxes, `engine/mailbox-rotation`'s pick. The sequence's own when absent.
114
+ */
115
+ mailboxId?: string | null;
110
116
  /** The record the person is (AGL-3234); a contact when absent. */
111
117
  target?: OutreachEnrollmentTarget;
112
118
  /** The contact's id; `''` for a lead. */
@@ -242,7 +242,7 @@ const detailOf = (value)=>{
242
242
  contactName: String((_draft_contactName = draft.contactName) != null ? _draft_contactName : '').replace(/\s+/g, ' ').trim().slice(0, 200),
243
243
  email: String((_draft_email = draft.email) != null ? _draft_email : '').trim().toLowerCase(),
244
244
  hostId: draft.sequence.hostId,
245
- mailboxId: draft.sequence.mailboxId,
245
+ mailboxId: draft.mailboxId || draft.sequence.mailboxId,
246
246
  stepIndex: start,
247
247
  nextDueAtMs: planOutreachFirstDue({
248
248
  sequence: draft.sequence,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/enrollment-state.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 * ONE PERSON'S RUN THROUGH A SEQUENCE (AGL-2979).\n *\n * ## The state machine\n *\n * active ──► paused ──► active\n * │ │\n * ├──────────┴──► replied · bounced · opted_out · stopped\n * └─► finished · failed\n *\n * An enrollment sends only while `active`. Everything that ends the sending\n * records why (`stopReason`, `stopDetail`, when, and who). Ending the\n * sending does not end what can still be learned from the thread: a reply,\n * a bounce or an opt-out that arrives after the last step, after a member\n * stopped it or after a send failed is still recorded, and a person who\n * replied and then asked to be left alone moves on to `opted_out` — an\n * opt-out is the one fact every later sequence has to be able to find.\n * `bounced` and `opted_out` are final.\n *\n * Every function returns a PATCH — the fields to write — and never the\n * whole document, so a writer applies it with its own SDK and its own\n * timestamps.\n *==========================================*/\n\nimport {\n OUTREACH_ATTESTATION_KINDS,\n OUTREACH_STOP_REASONS_BY_STATUS,\n type OutreachAttestationKind,\n type OutreachAttestations,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachEnrollmentTarget,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachStopReason,\n} from '../model/outreach.types'\nimport { normalizeContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { normalizeOutreachPersonalLine } from './gates'\nimport { effectiveOutreachWindow, type OutreachRandom, scheduleOutreachDue } from './schedule'\nimport { isInThreadEmailStep } from './sequence-validation'\n\n/** Where each status may go next. */\nexport const OUTREACH_ENROLLMENT_TRANSITIONS: Readonly<\n Record<OutreachEnrollmentStatus, readonly OutreachEnrollmentStatus[]>\n> = {\n active: ['paused', 'finished', 'replied', 'bounced', 'opted_out', 'stopped', 'failed'],\n paused: ['active', 'replied', 'bounced', 'opted_out', 'stopped'],\n finished: ['replied', 'bounced', 'opted_out'],\n stopped: ['replied', 'bounced', 'opted_out'],\n failed: ['replied', 'bounced', 'opted_out'],\n replied: ['opted_out'],\n bounced: [],\n opted_out: [],\n}\n\nexport function canTransitionOutreachEnrollment(\n from: OutreachEnrollmentStatus,\n to: OutreachEnrollmentStatus,\n): boolean {\n return OUTREACH_ENROLLMENT_TRANSITIONS[from]?.includes(to) === true\n}\n\n/** Something that happened to an enrollment. `atMs` is when it happened. */\nexport type OutreachEnrollmentEvent =\n | {\n type: 'pause'\n atMs: number\n byUid: string | null\n /** `manual` unless the engine held the send on its gateway (AGL-3326). */\n reason?: Extract<OutreachStopReason, 'manual' | 'gateway_blocked_here'>\n detail?: string | null\n }\n | { type: 'resume'; atMs: number; byUid: string | null }\n | {\n type: 'stop'\n atMs: number\n byUid: string | null\n reason?: Extract<OutreachStopReason, 'manual' | 'gate' | 'sequence_archived'>\n detail?: string | null\n }\n | { type: 'reply'; atMs: number; detail?: string | null }\n | {\n type: 'opt_out'\n atMs: number\n reason: Extract<OutreachStopReason, 'opt_out_reply' | 'unsubscribe' | 'do_not_contact'>\n detail?: string | null\n }\n | { type: 'bounce'; atMs: number; detail?: string | null }\n | { type: 'send_failed'; atMs: number; detail?: string | null }\n\n/** The status fields an event writes. */\nexport interface OutreachEnrollmentStatusPatch {\n status: OutreachEnrollmentStatus\n stopReason: OutreachStopReason | null\n stopDetail: string | null\n stoppedAtMs: number | null\n stoppedByUid: string | null\n nextDueAtMs: number | null\n}\n\n/**\n * `patch` is what to write, or `null` when there is nothing to write — the\n * enrollment is already there, or the event is not allowed, in which case\n * `error` says why.\n */\nexport interface OutreachEnrollmentEventResult {\n patch: OutreachEnrollmentStatusPatch | null\n error: string | null\n}\n\nconst STATUS_WORDS: Record<OutreachEnrollmentStatus, string> = {\n active: 'is active',\n paused: 'is paused',\n finished: 'finished',\n replied: 'got a reply',\n bounced: 'bounced',\n opted_out: 'opted out',\n stopped: 'was stopped',\n failed: 'failed',\n}\n\nconst EVENT_TARGET: Record<OutreachEnrollmentEvent['type'], OutreachEnrollmentStatus> = {\n pause: 'paused',\n resume: 'active',\n stop: 'stopped',\n reply: 'replied',\n opt_out: 'opted_out',\n bounce: 'bounced',\n send_failed: 'failed',\n}\n\nconst detailOf = (value: unknown): string | null => {\n const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n return text ? text.slice(0, 500) : null\n}\n\n/** What an event does to an enrollment's status — see the module note. */\nexport function applyOutreachEnrollmentEvent(\n enrollment: Pick<OutreachEnrollment, 'status' | 'nextDueAtMs'>,\n event: OutreachEnrollmentEvent,\n): OutreachEnrollmentEventResult {\n const from = enrollment.status\n const to = EVENT_TARGET[event.type]\n if (!to) return { patch: null, error: 'That is not something that happens to an enrollment.' }\n if (from === to) return { patch: null, error: null }\n if (!canTransitionOutreachEnrollment(from, to)) {\n return {\n patch: null,\n error: `This enrollment ${STATUS_WORDS[from]}, so it can't be marked as one that ${STATUS_WORDS[to]}.`,\n }\n }\n if (event.type === 'resume') {\n return {\n patch: {\n status: 'active',\n stopReason: null,\n stopDetail: null,\n stoppedAtMs: null,\n stoppedByUid: null,\n nextDueAtMs: enrollment.nextDueAtMs,\n },\n error: null,\n }\n }\n const reason: OutreachStopReason =\n event.type === 'pause'\n ? (event.reason ?? 'manual')\n : event.type === 'stop'\n ? (event.reason ?? 'manual')\n : event.type === 'reply'\n ? 'reply'\n : event.type === 'opt_out'\n ? event.reason\n : event.type === 'bounce'\n ? 'hard_bounce'\n : 'send_failed'\n const allowed = OUTREACH_STOP_REASONS_BY_STATUS[to as keyof typeof OUTREACH_STOP_REASONS_BY_STATUS]\n if (!allowed?.includes(reason)) {\n return { patch: null, error: `\"${reason}\" isn't a reason an enrollment ${STATUS_WORDS[to]}.` }\n }\n const byUid = 'byUid' in event && typeof event.byUid === 'string' && event.byUid ? event.byUid : null\n return {\n patch: {\n status: to,\n stopReason: reason,\n stopDetail: detailOf(event.detail),\n stoppedAtMs: event.atMs,\n stoppedByUid: byUid,\n // A paused enrollment keeps its place, so resuming it picks up where it\n // stopped; every other status has nothing left waiting.\n nextDueAtMs: to === 'paused' ? enrollment.nextDueAtMs : null,\n },\n error: null,\n }\n}\n\ntype ScheduleMailbox = Pick<OutreachMailbox, 'timezone' | 'window'>\ntype ScheduleSequence = Pick<OutreachSequence, 'steps' | 'settings'>\n\n/**\n * The step an enrollment begins at (AGL-3228), read from a request: the\n * first when the request names none, or the zero-based index it names —\n * or, as `refusal`, why that is not a step of this sequence.\n */\nexport function readOutreachStartStepIndex(\n raw: unknown,\n steps: readonly unknown[] | null | undefined,\n): { startStepIndex: number; refusal: null } | { startStepIndex: null; refusal: string } {\n if (raw === undefined || raw === null) return { startStepIndex: 0, refusal: null }\n const count = steps?.length ?? 0\n const index = typeof raw === 'number' ? raw : Number.NaN\n if (!Number.isInteger(index) || index < 0) {\n return { startStepIndex: null, refusal: 'Name the step to start at by its number in the sequence.' }\n }\n if (index >= count) {\n return {\n startStepIndex: null,\n refusal: `This sequence has ${count} ${count === 1 ? 'step' : 'steps'}, so enrolling can't start at step ${index + 1}.`,\n }\n }\n return { startStepIndex: index, refusal: null }\n}\n\n/**\n * When a new enrollment's first step comes due, or `null` when it cannot be\n * placed: the step it begins at (AGL-3228; the first unless it says so),\n * that step's own delay counted from the moment of enrollment.\n */\nexport function planOutreachFirstDue(input: {\n sequence: ScheduleSequence\n mailbox: ScheduleMailbox\n enrolledAtMs: number\n random: OutreachRandom\n startStepIndex?: number\n}): number | null {\n const first = input.sequence.steps?.[input.startStepIndex ?? 0]\n if (!first) return null\n return scheduleOutreachDue({\n fromMs: input.enrolledAtMs,\n delayBusinessDays: first.delayBusinessDays,\n timeZone: input.mailbox.timezone,\n window: effectiveOutreachWindow(input.sequence.settings?.window, input.mailbox.window),\n random: input.random,\n })\n}\n\n/** Stamps the attestations a rep ticked with who ticked them and when; unknown kinds are dropped. */\nexport function outreachAttestationsFrom(\n kinds: readonly unknown[],\n uid: string,\n atMs: number,\n): OutreachAttestations {\n const attestations: OutreachAttestations = {}\n for (const kind of kinds ?? []) {\n if ((OUTREACH_ATTESTATION_KINDS as readonly unknown[]).includes(kind)) {\n attestations[kind as OutreachAttestationKind] = { uid, atMs }\n }\n }\n return attestations\n}\n\nexport interface OutreachEnrollmentDraft {\n id: string\n sequence: Pick<OutreachSequence, 'id' | 'hostId' | 'mailboxId' | 'steps' | 'settings' | 'campaignIds'>\n mailbox: ScheduleMailbox\n /** The record the person is (AGL-3234); a contact when absent. */\n target?: OutreachEnrollmentTarget\n /** The contact's id; `''` for a lead. */\n contactId: string\n /** The lead's person key, for an enrollment made on a lead. */\n leadId?: string | null\n /** The contact's name as the sending site knows it; `''` for none. */\n contactName?: string | null\n /** The address, as the gates normalized it. */\n email: string\n /** The gates' `cold`. */\n cold: boolean\n personalLine: string\n attestations: OutreachAttestations\n enrolledByUid: string\n nowMs: number\n random: OutreachRandom\n /**\n * The step to begin at (AGL-3228), zero-based; the first when absent.\n * The caller has checked it names a step — {@link readOutreachStartStepIndex}.\n */\n startStepIndex?: number\n}\n\n/**\n * A new enrollment document, first step scheduled — one builder for every\n * door that enrolls, so an enrollment made from a contact and one made from\n * a list are the same shape. Call it after the gates allowed the person.\n *\n * An enrollment that begins past the first step (AGL-3228) is for someone\n * who already had the earlier steps by hand: those are marked skipped, the\n * step it begins at is scheduled by its own delay from now, and\n * `lastSentAtMs` is the enrollment — the moment the earlier steps are\n * accounted for — so the sync watches this mailbox for the person's reply,\n * bounce or opt-out from the start, as it does after a send.\n */\nexport function buildOutreachEnrollment(draft: OutreachEnrollmentDraft): OutreachEnrollment {\n const start = draft.startStepIndex ?? 0\n const steps = draft.sequence.steps ?? []\n const enrollment: OutreachEnrollment = {\n id: draft.id,\n sequenceId: draft.sequence.id,\n target: draft.target ?? 'contact',\n contactId: draft.contactId,\n leadId: draft.leadId ?? null,\n contactName: String(draft.contactName ?? '').replace(/\\s+/g, ' ').trim().slice(0, 200),\n email: String(draft.email ?? '').trim().toLowerCase(),\n hostId: draft.sequence.hostId,\n mailboxId: draft.sequence.mailboxId,\n stepIndex: start,\n nextDueAtMs: planOutreachFirstDue({\n sequence: draft.sequence,\n mailbox: draft.mailbox,\n enrolledAtMs: draft.nowMs,\n random: draft.random,\n startStepIndex: start,\n }),\n status: 'active',\n stopReason: null,\n stopDetail: null,\n stoppedAtMs: null,\n stoppedByUid: null,\n personalLine: normalizeOutreachPersonalLine(draft.personalLine),\n cold: draft.cold === true,\n attestations: { ...(draft.attestations ?? {}) },\n enrolledByUid: draft.enrolledByUid,\n gmailThreadId: null,\n gmailThreadIds: [],\n threadSubject: null,\n messageIds: [],\n lastSentAtMs: null,\n // The sequence's campaigns as they stand now (AGL-3254): what this\n // enrollment's outcomes are credited to, whatever the sequence joins later.\n campaignIds: normalizeContainerIds(draft.sequence.campaignIds),\n createdAtMs: draft.nowMs,\n updatedAtMs: draft.nowMs,\n }\n if (start > 0) {\n enrollment.startStepIndex = start\n enrollment.skippedSteps = steps.slice(0, start).map((step, stepIndex) => ({\n stepIndex,\n stepId: step.id,\n kind: step.kind,\n atMs: draft.nowMs,\n }))\n enrollment.lastSentAtMs = draft.nowMs\n }\n return enrollment\n}\n\n/** The step an enrollment began at (AGL-3228): the first, unless it began later. */\nexport function outreachEnrollmentStartStepIndex(\n enrollment: Pick<OutreachEnrollment, 'startStepIndex'> | null | undefined,\n): number {\n const start = Number(enrollment?.startStepIndex)\n return Number.isInteger(start) && start > 0 ? start : 0\n}\n\n/** What the provider reported for an email step that was sent. */\nexport interface OutreachSentEmail {\n /** The `Message-ID` header the message went out with, angle brackets included. */\n messageId: string\n /** The provider's thread id. */\n threadId: string\n /** The subject as sent. */\n subject: string\n}\n\nexport interface OutreachStepCompletionInput {\n enrollment: Pick<\n OutreachEnrollment,\n 'status' | 'stepIndex' | 'gmailThreadId' | 'gmailThreadIds' | 'messageIds' | 'threadSubject'\n > &\n Partial<Pick<OutreachEnrollment, 'startStepIndex'>>\n sequence: ScheduleSequence\n mailbox: ScheduleMailbox\n /** When the step ran. */\n completedAtMs: number\n /** For an email step, what was sent; not read for a task. */\n sent?: OutreachSentEmail | null\n random: OutreachRandom\n}\n\n/** The fields a completed step writes. */\nexport interface OutreachStepCompletionPatch {\n status: 'active' | 'finished'\n stepIndex: number\n nextDueAtMs: number | null\n lastSentAtMs: number\n gmailThreadId?: string\n gmailThreadIds?: string[]\n threadSubject?: string\n messageIds?: string[]\n}\n\nexport interface OutreachStepCompletionResult {\n patch: OutreachStepCompletionPatch | null\n /**\n * Why there is no patch, or — beside one — that the next step could not\n * be placed on the calendar (a window that never opens, a zone `Intl` does\n * not know), which leaves the enrollment with nothing due until a member\n * fixes the window and resumes it.\n */\n error: string | null\n}\n\n/**\n * What running the current step writes: the send recorded on the thread\n * fields, and the next step scheduled — or the enrollment `finished` when\n * that was the last.\n */\nexport function planOutreachStepCompletion(\n input: OutreachStepCompletionInput,\n): OutreachStepCompletionResult {\n const { enrollment, sequence } = input\n if (enrollment.status !== 'active') {\n return { patch: null, error: 'Only an active enrollment runs its steps.' }\n }\n const steps = sequence.steps ?? []\n const step = steps[enrollment.stepIndex]\n if (!step) return { patch: null, error: 'This enrollment has no step at its position.' }\n const patch: OutreachStepCompletionPatch = {\n status: 'active',\n stepIndex: enrollment.stepIndex + 1,\n nextDueAtMs: null,\n lastSentAtMs: input.completedAtMs,\n }\n if (step.kind === 'email') {\n const sent = input.sent\n const messageId = String(sent?.messageId ?? '').trim()\n const threadId = String(sent?.threadId ?? '').trim()\n if (!messageId || !threadId) {\n return { patch: null, error: 'An email step needs the sent message and thread ids.' }\n }\n const threadIds = [...(enrollment.gmailThreadIds ?? [])]\n if (!threadIds.includes(threadId)) threadIds.push(threadId)\n patch.gmailThreadId = threadId\n patch.gmailThreadIds = threadIds\n if (\n isInThreadEmailStep(steps, enrollment.stepIndex, outreachEnrollmentStartStepIndex(enrollment)) &&\n enrollment.threadSubject\n ) {\n patch.messageIds = [...(enrollment.messageIds ?? []), messageId]\n } else {\n patch.threadSubject = String(sent.subject ?? '').trim()\n patch.messageIds = [messageId]\n }\n }\n const next = steps[patch.stepIndex]\n if (!next) {\n patch.status = 'finished'\n return { patch, error: null }\n }\n patch.nextDueAtMs = scheduleOutreachDue({\n fromMs: input.completedAtMs,\n delayBusinessDays: next.delayBusinessDays,\n timeZone: input.mailbox.timezone,\n window: effectiveOutreachWindow(sequence.settings?.window, input.mailbox.window),\n random: input.random,\n })\n return {\n patch,\n error:\n patch.nextDueAtMs === null\n ? \"The next step couldn't be scheduled: check the sending window and the mailbox's timezone.\"\n : null,\n }\n}\n"],"names":["OUTREACH_ATTESTATION_KINDS","OUTREACH_STOP_REASONS_BY_STATUS","normalizeContainerIds","normalizeOutreachPersonalLine","effectiveOutreachWindow","scheduleOutreachDue","isInThreadEmailStep","OUTREACH_ENROLLMENT_TRANSITIONS","active","paused","finished","stopped","failed","replied","bounced","opted_out","canTransitionOutreachEnrollment","from","to","includes","STATUS_WORDS","EVENT_TARGET","pause","resume","stop","reply","opt_out","bounce","send_failed","detailOf","value","text","replace","trim","slice","applyOutreachEnrollmentEvent","enrollment","event","status","type","patch","error","stopReason","stopDetail","stoppedAtMs","stoppedByUid","nextDueAtMs","reason","allowed","byUid","detail","atMs","readOutreachStartStepIndex","raw","steps","undefined","startStepIndex","refusal","count","length","index","Number","NaN","isInteger","planOutreachFirstDue","input","first","sequence","fromMs","enrolledAtMs","delayBusinessDays","timeZone","mailbox","timezone","window","settings","random","outreachAttestationsFrom","kinds","uid","attestations","kind","buildOutreachEnrollment","draft","start","id","sequenceId","target","contactId","leadId","contactName","String","email","toLowerCase","hostId","mailboxId","stepIndex","nowMs","personalLine","cold","enrolledByUid","gmailThreadId","gmailThreadIds","threadSubject","messageIds","lastSentAtMs","campaignIds","createdAtMs","updatedAtMs","skippedSteps","map","step","stepId","outreachEnrollmentStartStepIndex","planOutreachStepCompletion","completedAtMs","sent","messageId","threadId","threadIds","push","subject","next"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/enrollment-state.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 * ONE PERSON'S RUN THROUGH A SEQUENCE (AGL-2979).\n *\n * ## The state machine\n *\n * active ──► paused ──► active\n * │ │\n * ├──────────┴──► replied · bounced · opted_out · stopped\n * └─► finished · failed\n *\n * An enrollment sends only while `active`. Everything that ends the sending\n * records why (`stopReason`, `stopDetail`, when, and who). Ending the\n * sending does not end what can still be learned from the thread: a reply,\n * a bounce or an opt-out that arrives after the last step, after a member\n * stopped it or after a send failed is still recorded, and a person who\n * replied and then asked to be left alone moves on to `opted_out` — an\n * opt-out is the one fact every later sequence has to be able to find.\n * `bounced` and `opted_out` are final.\n *\n * Every function returns a PATCH — the fields to write — and never the\n * whole document, so a writer applies it with its own SDK and its own\n * timestamps.\n *==========================================*/\n\nimport {\n OUTREACH_ATTESTATION_KINDS,\n OUTREACH_STOP_REASONS_BY_STATUS,\n type OutreachAttestationKind,\n type OutreachAttestations,\n type OutreachEnrollment,\n type OutreachEnrollmentStatus,\n type OutreachEnrollmentTarget,\n type OutreachMailbox,\n type OutreachSequence,\n type OutreachStopReason,\n} from '../model/outreach.types'\nimport { normalizeContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\nimport { normalizeOutreachPersonalLine } from './gates'\nimport { effectiveOutreachWindow, type OutreachRandom, scheduleOutreachDue } from './schedule'\nimport { isInThreadEmailStep } from './sequence-validation'\n\n/** Where each status may go next. */\nexport const OUTREACH_ENROLLMENT_TRANSITIONS: Readonly<\n Record<OutreachEnrollmentStatus, readonly OutreachEnrollmentStatus[]>\n> = {\n active: ['paused', 'finished', 'replied', 'bounced', 'opted_out', 'stopped', 'failed'],\n paused: ['active', 'replied', 'bounced', 'opted_out', 'stopped'],\n finished: ['replied', 'bounced', 'opted_out'],\n stopped: ['replied', 'bounced', 'opted_out'],\n failed: ['replied', 'bounced', 'opted_out'],\n replied: ['opted_out'],\n bounced: [],\n opted_out: [],\n}\n\nexport function canTransitionOutreachEnrollment(\n from: OutreachEnrollmentStatus,\n to: OutreachEnrollmentStatus,\n): boolean {\n return OUTREACH_ENROLLMENT_TRANSITIONS[from]?.includes(to) === true\n}\n\n/** Something that happened to an enrollment. `atMs` is when it happened. */\nexport type OutreachEnrollmentEvent =\n | {\n type: 'pause'\n atMs: number\n byUid: string | null\n /** `manual` unless the engine held the send on its gateway (AGL-3326). */\n reason?: Extract<OutreachStopReason, 'manual' | 'gateway_blocked_here'>\n detail?: string | null\n }\n | { type: 'resume'; atMs: number; byUid: string | null }\n | {\n type: 'stop'\n atMs: number\n byUid: string | null\n reason?: Extract<OutreachStopReason, 'manual' | 'gate' | 'sequence_archived'>\n detail?: string | null\n }\n | { type: 'reply'; atMs: number; detail?: string | null }\n | {\n type: 'opt_out'\n atMs: number\n reason: Extract<OutreachStopReason, 'opt_out_reply' | 'unsubscribe' | 'do_not_contact'>\n detail?: string | null\n }\n | { type: 'bounce'; atMs: number; detail?: string | null }\n | { type: 'send_failed'; atMs: number; detail?: string | null }\n\n/** The status fields an event writes. */\nexport interface OutreachEnrollmentStatusPatch {\n status: OutreachEnrollmentStatus\n stopReason: OutreachStopReason | null\n stopDetail: string | null\n stoppedAtMs: number | null\n stoppedByUid: string | null\n nextDueAtMs: number | null\n}\n\n/**\n * `patch` is what to write, or `null` when there is nothing to write — the\n * enrollment is already there, or the event is not allowed, in which case\n * `error` says why.\n */\nexport interface OutreachEnrollmentEventResult {\n patch: OutreachEnrollmentStatusPatch | null\n error: string | null\n}\n\nconst STATUS_WORDS: Record<OutreachEnrollmentStatus, string> = {\n active: 'is active',\n paused: 'is paused',\n finished: 'finished',\n replied: 'got a reply',\n bounced: 'bounced',\n opted_out: 'opted out',\n stopped: 'was stopped',\n failed: 'failed',\n}\n\nconst EVENT_TARGET: Record<OutreachEnrollmentEvent['type'], OutreachEnrollmentStatus> = {\n pause: 'paused',\n resume: 'active',\n stop: 'stopped',\n reply: 'replied',\n opt_out: 'opted_out',\n bounce: 'bounced',\n send_failed: 'failed',\n}\n\nconst detailOf = (value: unknown): string | null => {\n const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n return text ? text.slice(0, 500) : null\n}\n\n/** What an event does to an enrollment's status — see the module note. */\nexport function applyOutreachEnrollmentEvent(\n enrollment: Pick<OutreachEnrollment, 'status' | 'nextDueAtMs'>,\n event: OutreachEnrollmentEvent,\n): OutreachEnrollmentEventResult {\n const from = enrollment.status\n const to = EVENT_TARGET[event.type]\n if (!to) return { patch: null, error: 'That is not something that happens to an enrollment.' }\n if (from === to) return { patch: null, error: null }\n if (!canTransitionOutreachEnrollment(from, to)) {\n return {\n patch: null,\n error: `This enrollment ${STATUS_WORDS[from]}, so it can't be marked as one that ${STATUS_WORDS[to]}.`,\n }\n }\n if (event.type === 'resume') {\n return {\n patch: {\n status: 'active',\n stopReason: null,\n stopDetail: null,\n stoppedAtMs: null,\n stoppedByUid: null,\n nextDueAtMs: enrollment.nextDueAtMs,\n },\n error: null,\n }\n }\n const reason: OutreachStopReason =\n event.type === 'pause'\n ? (event.reason ?? 'manual')\n : event.type === 'stop'\n ? (event.reason ?? 'manual')\n : event.type === 'reply'\n ? 'reply'\n : event.type === 'opt_out'\n ? event.reason\n : event.type === 'bounce'\n ? 'hard_bounce'\n : 'send_failed'\n const allowed = OUTREACH_STOP_REASONS_BY_STATUS[to as keyof typeof OUTREACH_STOP_REASONS_BY_STATUS]\n if (!allowed?.includes(reason)) {\n return { patch: null, error: `\"${reason}\" isn't a reason an enrollment ${STATUS_WORDS[to]}.` }\n }\n const byUid = 'byUid' in event && typeof event.byUid === 'string' && event.byUid ? event.byUid : null\n return {\n patch: {\n status: to,\n stopReason: reason,\n stopDetail: detailOf(event.detail),\n stoppedAtMs: event.atMs,\n stoppedByUid: byUid,\n // A paused enrollment keeps its place, so resuming it picks up where it\n // stopped; every other status has nothing left waiting.\n nextDueAtMs: to === 'paused' ? enrollment.nextDueAtMs : null,\n },\n error: null,\n }\n}\n\ntype ScheduleMailbox = Pick<OutreachMailbox, 'timezone' | 'window'>\ntype ScheduleSequence = Pick<OutreachSequence, 'steps' | 'settings'>\n\n/**\n * The step an enrollment begins at (AGL-3228), read from a request: the\n * first when the request names none, or the zero-based index it names —\n * or, as `refusal`, why that is not a step of this sequence.\n */\nexport function readOutreachStartStepIndex(\n raw: unknown,\n steps: readonly unknown[] | null | undefined,\n): { startStepIndex: number; refusal: null } | { startStepIndex: null; refusal: string } {\n if (raw === undefined || raw === null) return { startStepIndex: 0, refusal: null }\n const count = steps?.length ?? 0\n const index = typeof raw === 'number' ? raw : Number.NaN\n if (!Number.isInteger(index) || index < 0) {\n return { startStepIndex: null, refusal: 'Name the step to start at by its number in the sequence.' }\n }\n if (index >= count) {\n return {\n startStepIndex: null,\n refusal: `This sequence has ${count} ${count === 1 ? 'step' : 'steps'}, so enrolling can't start at step ${index + 1}.`,\n }\n }\n return { startStepIndex: index, refusal: null }\n}\n\n/**\n * When a new enrollment's first step comes due, or `null` when it cannot be\n * placed: the step it begins at (AGL-3228; the first unless it says so),\n * that step's own delay counted from the moment of enrollment.\n */\nexport function planOutreachFirstDue(input: {\n sequence: ScheduleSequence\n mailbox: ScheduleMailbox\n enrolledAtMs: number\n random: OutreachRandom\n startStepIndex?: number\n}): number | null {\n const first = input.sequence.steps?.[input.startStepIndex ?? 0]\n if (!first) return null\n return scheduleOutreachDue({\n fromMs: input.enrolledAtMs,\n delayBusinessDays: first.delayBusinessDays,\n timeZone: input.mailbox.timezone,\n window: effectiveOutreachWindow(input.sequence.settings?.window, input.mailbox.window),\n random: input.random,\n })\n}\n\n/** Stamps the attestations a rep ticked with who ticked them and when; unknown kinds are dropped. */\nexport function outreachAttestationsFrom(\n kinds: readonly unknown[],\n uid: string,\n atMs: number,\n): OutreachAttestations {\n const attestations: OutreachAttestations = {}\n for (const kind of kinds ?? []) {\n if ((OUTREACH_ATTESTATION_KINDS as readonly unknown[]).includes(kind)) {\n attestations[kind as OutreachAttestationKind] = { uid, atMs }\n }\n }\n return attestations\n}\n\nexport interface OutreachEnrollmentDraft {\n id: string\n sequence: Pick<OutreachSequence, 'id' | 'hostId' | 'mailboxId' | 'steps' | 'settings' | 'campaignIds'>\n /** The mailbox the enrollment sends from, whose calendar its steps are scheduled on. */\n mailbox: ScheduleMailbox\n /**\n * That mailbox's id when the sequence rotates (AGL-3489): one of its\n * mailboxes, `engine/mailbox-rotation`'s pick. The sequence's own when absent.\n */\n mailboxId?: string | null\n /** The record the person is (AGL-3234); a contact when absent. */\n target?: OutreachEnrollmentTarget\n /** The contact's id; `''` for a lead. */\n contactId: string\n /** The lead's person key, for an enrollment made on a lead. */\n leadId?: string | null\n /** The contact's name as the sending site knows it; `''` for none. */\n contactName?: string | null\n /** The address, as the gates normalized it. */\n email: string\n /** The gates' `cold`. */\n cold: boolean\n personalLine: string\n attestations: OutreachAttestations\n enrolledByUid: string\n nowMs: number\n random: OutreachRandom\n /**\n * The step to begin at (AGL-3228), zero-based; the first when absent.\n * The caller has checked it names a step — {@link readOutreachStartStepIndex}.\n */\n startStepIndex?: number\n}\n\n/**\n * A new enrollment document, first step scheduled — one builder for every\n * door that enrolls, so an enrollment made from a contact and one made from\n * a list are the same shape. Call it after the gates allowed the person.\n *\n * An enrollment that begins past the first step (AGL-3228) is for someone\n * who already had the earlier steps by hand: those are marked skipped, the\n * step it begins at is scheduled by its own delay from now, and\n * `lastSentAtMs` is the enrollment — the moment the earlier steps are\n * accounted for — so the sync watches this mailbox for the person's reply,\n * bounce or opt-out from the start, as it does after a send.\n */\nexport function buildOutreachEnrollment(draft: OutreachEnrollmentDraft): OutreachEnrollment {\n const start = draft.startStepIndex ?? 0\n const steps = draft.sequence.steps ?? []\n const enrollment: OutreachEnrollment = {\n id: draft.id,\n sequenceId: draft.sequence.id,\n target: draft.target ?? 'contact',\n contactId: draft.contactId,\n leadId: draft.leadId ?? null,\n contactName: String(draft.contactName ?? '').replace(/\\s+/g, ' ').trim().slice(0, 200),\n email: String(draft.email ?? '').trim().toLowerCase(),\n hostId: draft.sequence.hostId,\n mailboxId: draft.mailboxId || draft.sequence.mailboxId,\n stepIndex: start,\n nextDueAtMs: planOutreachFirstDue({\n sequence: draft.sequence,\n mailbox: draft.mailbox,\n enrolledAtMs: draft.nowMs,\n random: draft.random,\n startStepIndex: start,\n }),\n status: 'active',\n stopReason: null,\n stopDetail: null,\n stoppedAtMs: null,\n stoppedByUid: null,\n personalLine: normalizeOutreachPersonalLine(draft.personalLine),\n cold: draft.cold === true,\n attestations: { ...(draft.attestations ?? {}) },\n enrolledByUid: draft.enrolledByUid,\n gmailThreadId: null,\n gmailThreadIds: [],\n threadSubject: null,\n messageIds: [],\n lastSentAtMs: null,\n // The sequence's campaigns as they stand now (AGL-3254): what this\n // enrollment's outcomes are credited to, whatever the sequence joins later.\n campaignIds: normalizeContainerIds(draft.sequence.campaignIds),\n createdAtMs: draft.nowMs,\n updatedAtMs: draft.nowMs,\n }\n if (start > 0) {\n enrollment.startStepIndex = start\n enrollment.skippedSteps = steps.slice(0, start).map((step, stepIndex) => ({\n stepIndex,\n stepId: step.id,\n kind: step.kind,\n atMs: draft.nowMs,\n }))\n enrollment.lastSentAtMs = draft.nowMs\n }\n return enrollment\n}\n\n/** The step an enrollment began at (AGL-3228): the first, unless it began later. */\nexport function outreachEnrollmentStartStepIndex(\n enrollment: Pick<OutreachEnrollment, 'startStepIndex'> | null | undefined,\n): number {\n const start = Number(enrollment?.startStepIndex)\n return Number.isInteger(start) && start > 0 ? start : 0\n}\n\n/** What the provider reported for an email step that was sent. */\nexport interface OutreachSentEmail {\n /** The `Message-ID` header the message went out with, angle brackets included. */\n messageId: string\n /** The provider's thread id. */\n threadId: string\n /** The subject as sent. */\n subject: string\n}\n\nexport interface OutreachStepCompletionInput {\n enrollment: Pick<\n OutreachEnrollment,\n 'status' | 'stepIndex' | 'gmailThreadId' | 'gmailThreadIds' | 'messageIds' | 'threadSubject'\n > &\n Partial<Pick<OutreachEnrollment, 'startStepIndex'>>\n sequence: ScheduleSequence\n mailbox: ScheduleMailbox\n /** When the step ran. */\n completedAtMs: number\n /** For an email step, what was sent; not read for a task. */\n sent?: OutreachSentEmail | null\n random: OutreachRandom\n}\n\n/** The fields a completed step writes. */\nexport interface OutreachStepCompletionPatch {\n status: 'active' | 'finished'\n stepIndex: number\n nextDueAtMs: number | null\n lastSentAtMs: number\n gmailThreadId?: string\n gmailThreadIds?: string[]\n threadSubject?: string\n messageIds?: string[]\n}\n\nexport interface OutreachStepCompletionResult {\n patch: OutreachStepCompletionPatch | null\n /**\n * Why there is no patch, or — beside one — that the next step could not\n * be placed on the calendar (a window that never opens, a zone `Intl` does\n * not know), which leaves the enrollment with nothing due until a member\n * fixes the window and resumes it.\n */\n error: string | null\n}\n\n/**\n * What running the current step writes: the send recorded on the thread\n * fields, and the next step scheduled — or the enrollment `finished` when\n * that was the last.\n */\nexport function planOutreachStepCompletion(\n input: OutreachStepCompletionInput,\n): OutreachStepCompletionResult {\n const { enrollment, sequence } = input\n if (enrollment.status !== 'active') {\n return { patch: null, error: 'Only an active enrollment runs its steps.' }\n }\n const steps = sequence.steps ?? []\n const step = steps[enrollment.stepIndex]\n if (!step) return { patch: null, error: 'This enrollment has no step at its position.' }\n const patch: OutreachStepCompletionPatch = {\n status: 'active',\n stepIndex: enrollment.stepIndex + 1,\n nextDueAtMs: null,\n lastSentAtMs: input.completedAtMs,\n }\n if (step.kind === 'email') {\n const sent = input.sent\n const messageId = String(sent?.messageId ?? '').trim()\n const threadId = String(sent?.threadId ?? '').trim()\n if (!messageId || !threadId) {\n return { patch: null, error: 'An email step needs the sent message and thread ids.' }\n }\n const threadIds = [...(enrollment.gmailThreadIds ?? [])]\n if (!threadIds.includes(threadId)) threadIds.push(threadId)\n patch.gmailThreadId = threadId\n patch.gmailThreadIds = threadIds\n if (\n isInThreadEmailStep(steps, enrollment.stepIndex, outreachEnrollmentStartStepIndex(enrollment)) &&\n enrollment.threadSubject\n ) {\n patch.messageIds = [...(enrollment.messageIds ?? []), messageId]\n } else {\n patch.threadSubject = String(sent.subject ?? '').trim()\n patch.messageIds = [messageId]\n }\n }\n const next = steps[patch.stepIndex]\n if (!next) {\n patch.status = 'finished'\n return { patch, error: null }\n }\n patch.nextDueAtMs = scheduleOutreachDue({\n fromMs: input.completedAtMs,\n delayBusinessDays: next.delayBusinessDays,\n timeZone: input.mailbox.timezone,\n window: effectiveOutreachWindow(sequence.settings?.window, input.mailbox.window),\n random: input.random,\n })\n return {\n patch,\n error:\n patch.nextDueAtMs === null\n ? \"The next step couldn't be scheduled: check the sending window and the mailbox's timezone.\"\n : null,\n }\n}\n"],"names":["OUTREACH_ATTESTATION_KINDS","OUTREACH_STOP_REASONS_BY_STATUS","normalizeContainerIds","normalizeOutreachPersonalLine","effectiveOutreachWindow","scheduleOutreachDue","isInThreadEmailStep","OUTREACH_ENROLLMENT_TRANSITIONS","active","paused","finished","stopped","failed","replied","bounced","opted_out","canTransitionOutreachEnrollment","from","to","includes","STATUS_WORDS","EVENT_TARGET","pause","resume","stop","reply","opt_out","bounce","send_failed","detailOf","value","text","replace","trim","slice","applyOutreachEnrollmentEvent","enrollment","event","status","type","patch","error","stopReason","stopDetail","stoppedAtMs","stoppedByUid","nextDueAtMs","reason","allowed","byUid","detail","atMs","readOutreachStartStepIndex","raw","steps","undefined","startStepIndex","refusal","count","length","index","Number","NaN","isInteger","planOutreachFirstDue","input","first","sequence","fromMs","enrolledAtMs","delayBusinessDays","timeZone","mailbox","timezone","window","settings","random","outreachAttestationsFrom","kinds","uid","attestations","kind","buildOutreachEnrollment","draft","start","id","sequenceId","target","contactId","leadId","contactName","String","email","toLowerCase","hostId","mailboxId","stepIndex","nowMs","personalLine","cold","enrolledByUid","gmailThreadId","gmailThreadIds","threadSubject","messageIds","lastSentAtMs","campaignIds","createdAtMs","updatedAtMs","skippedSteps","map","step","stepId","outreachEnrollmentStartStepIndex","planOutreachStepCompletion","completedAtMs","sent","messageId","threadId","threadIds","push","subject","next"],"mappings":"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"}
@@ -30,3 +30,4 @@ export * from './mailbox-health';
30
30
  export * from './mailbox-notice';
31
31
  export * from './gateway-block';
32
32
  export * from './do-not-contact-domain';
33
+ export * from './mailbox-rotation';
@@ -33,5 +33,6 @@ export * from "./mailbox-health.js";
33
33
  export * from "./mailbox-notice.js";
34
34
  export * from "./gateway-block.js";
35
35
  export * from "./do-not-contact-domain.js";
36
+ export * from "./mailbox-rotation.js";
36
37
 
37
38
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * The Outreach engine (AGL-2979): pure decisions, no I/O, safe for the\n * console and the server alike. `./do-not-contact` is deliberately absent —\n * it hashes with `node:crypto` — and is imported by its own path.\n */\nexport * from './sequence-validation'\nexport * from './recipient-country'\nexport * from './gates'\nexport * from './schedule'\nexport * from './sending-capacity'\nexport * from './next-send'\nexport * from './enrollment-state'\nexport * from './compose'\nexport * from './thread-message'\nexport * from './delivery-status'\nexport * from './opt-out-intent'\nexport * from './thread-classification'\nexport * from './mailbox-health'\nexport * from './mailbox-notice'\nexport * from './gateway-block'\nexport * from './do-not-contact-domain'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;CAIC,GACD,cAAc,2BAAuB;AACrC,cAAc,yBAAqB;AACnC,cAAc,aAAS;AACvB,cAAc,gBAAY;AAC1B,cAAc,wBAAoB;AAClC,cAAc,iBAAa;AAC3B,cAAc,wBAAoB;AAClC,cAAc,eAAW;AACzB,cAAc,sBAAkB;AAChC,cAAc,uBAAmB;AACjC,cAAc,sBAAkB;AAChC,cAAc,6BAAyB;AACvC,cAAc,sBAAkB;AAChC,cAAc,sBAAkB;AAChC,cAAc,qBAAiB;AAC/B,cAAc,6BAAyB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * The Outreach engine (AGL-2979): pure decisions, no I/O, safe for the\n * console and the server alike. `./do-not-contact` is deliberately absent —\n * it hashes with `node:crypto` — and is imported by its own path.\n */\nexport * from './sequence-validation'\nexport * from './recipient-country'\nexport * from './gates'\nexport * from './schedule'\nexport * from './sending-capacity'\nexport * from './next-send'\nexport * from './enrollment-state'\nexport * from './compose'\nexport * from './thread-message'\nexport * from './delivery-status'\nexport * from './opt-out-intent'\nexport * from './thread-classification'\nexport * from './mailbox-health'\nexport * from './mailbox-notice'\nexport * from './gateway-block'\nexport * from './do-not-contact-domain'\nexport * from './mailbox-rotation'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;CAIC,GACD,cAAc,2BAAuB;AACrC,cAAc,yBAAqB;AACnC,cAAc,aAAS;AACvB,cAAc,gBAAY;AAC1B,cAAc,wBAAoB;AAClC,cAAc,iBAAa;AAC3B,cAAc,wBAAoB;AAClC,cAAc,eAAW;AACzB,cAAc,sBAAkB;AAChC,cAAc,uBAAmB;AACjC,cAAc,sBAAkB;AAChC,cAAc,6BAAyB;AACvC,cAAc,sBAAkB;AAChC,cAAc,sBAAkB;AAChC,cAAc,qBAAiB;AAC/B,cAAc,6BAAyB;AACvC,cAAc,wBAAoB"}
@@ -38,8 +38,11 @@ export interface OutreachMailboxNoticeEmail {
38
38
  }
39
39
  /** The mailbox as the notice names it: `Avery Quinn <avery@example.com>`, the send-as beside it when it differs. */
40
40
  export declare function outreachMailboxNoticeName(mailbox: OutreachMailboxNotice['mailbox']): string;
41
- /** What the notice says when Google stopped accepting the mailbox's connection. */
42
- export declare function outreachReconnectRequiredSentence(errorCode: string | null | undefined): string;
41
+ /**
42
+ * What the notice says when the provider stopped accepting the mailbox's
43
+ * connection: Google's, or Microsoft's for a Microsoft 365 mailbox (AGL-3489).
44
+ */
45
+ export declare function outreachReconnectRequiredSentence(errorCode: string | null | undefined, provider?: string | null | undefined): string;
43
46
  /** The email, composed — see the module note. */
44
47
  export declare function composeOutreachMailboxNotice(notice: OutreachMailboxNotice, options: {
45
48
  mailboxesUrl: string | null;
@@ -32,9 +32,13 @@
32
32
  const sendAs = String((_mailbox_sendAs = mailbox.sendAs) != null ? _mailbox_sendAs : '').trim().toLowerCase();
33
33
  return sendAs && sendAs !== String(mailbox.email).trim().toLowerCase() ? `${own}, sending as ${sendAs}` : own;
34
34
  }
35
- /** What the notice says when Google stopped accepting the mailbox's connection. */ export function outreachReconnectRequiredSentence(errorCode) {
35
+ /**
36
+ * What the notice says when the provider stopped accepting the mailbox's
37
+ * connection: Google's, or Microsoft's for a Microsoft 365 mailbox (AGL-3489).
38
+ */ export function outreachReconnectRequiredSentence(errorCode, provider = 'google') {
36
39
  const code = String(errorCode != null ? errorCode : '').trim();
37
- return 'Google stopped accepting this mailbox’s connection' + (code ? ` (${code})` : '') + ' — the password changed, access was removed from the Google account, or an administrator revoked it. ' + 'Nothing sends from it until it is connected again.';
40
+ const name = provider === 'microsoft' ? 'Microsoft' : 'Google';
41
+ return `${name} stopped accepting this mailbox’s connection` + (code ? ` (${code})` : '') + ` — the password changed, access was removed from the ${name} account, or an administrator revoked it. ` + 'Nothing sends from it until it is connected again.';
38
42
  }
39
43
  function waitingSentence(waiting) {
40
44
  const count = Math.max(0, Math.floor(Number(waiting) || 0));
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/mailbox-notice.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 THE MAILBOX'S OWNER IS TOLD (AGL-3244).\n *\n * A mailbox that pauses itself, or that Google stops accepting, holds every\n * enrollment on it until a person acts — and the person is the member whose\n * mailbox it is, who is not watching the Mailboxes page. So the moment the\n * runtime judges it, they are emailed: the engine's own sentence for the\n * pause, the mailbox by name, how many enrollments are waiting on it, and\n * the link to the page where Resume and Reconnect live.\n *\n * Plain text, one screen long, and the reason in the first line: it is read\n * on a phone between meetings.\n *==========================================*/\n\nexport type OutreachMailboxNoticeKind = 'auto_pause' | 'reconnect_required'\n\nexport interface OutreachMailboxNotice {\n kind: OutreachMailboxNoticeKind\n mailbox: {\n /** The account's own address. */\n email: string\n /** The address mail goes out as, when it is not the account's. */\n sendAs: string\n displayName: string\n }\n /**\n * For a pause, the engine's own sentence — `autoPause.message`; for a\n * reconnect, {@link outreachReconnectRequiredSentence}.\n */\n message: string\n /** Enrollments active on the mailbox, waiting for it to send again. */\n waiting: number\n}\n\nexport interface OutreachMailboxNoticeEmail {\n subject: string\n text: string\n}\n\n/** The mailbox as the notice names it: `Avery Quinn <avery@example.com>`, the send-as beside it when it differs. */\nexport function outreachMailboxNoticeName(mailbox: OutreachMailboxNotice['mailbox']): string {\n const name = String(mailbox.displayName ?? '').trim()\n const own = name ? `${name} <${mailbox.email}>` : mailbox.email\n const sendAs = String(mailbox.sendAs ?? '')\n .trim()\n .toLowerCase()\n return sendAs && sendAs !== String(mailbox.email).trim().toLowerCase() ? `${own}, sending as ${sendAs}` : own\n}\n\n/** What the notice says when Google stopped accepting the mailbox's connection. */\nexport function outreachReconnectRequiredSentence(errorCode: string | null | undefined): string {\n const code = String(errorCode ?? '').trim()\n return (\n 'Google stopped accepting this mailbox’s connection' +\n (code ? ` (${code})` : '') +\n ' — the password changed, access was removed from the Google account, or an administrator revoked it. ' +\n 'Nothing sends from it until it is connected again.'\n )\n}\n\nfunction waitingSentence(waiting: number): string {\n const count = Math.max(0, Math.floor(Number(waiting) || 0))\n if (count === 0) return 'No enrollment is waiting on it right now.'\n return count === 1\n ? '1 enrollment is active on it and waiting; its next email goes out once the mailbox sends again.'\n : `${count} enrollments are active on it and waiting; their next emails go out once the mailbox sends again.`\n}\n\n/** The email, composed — see the module note. */\nexport function composeOutreachMailboxNotice(\n notice: OutreachMailboxNotice,\n options: { mailboxesUrl: string | null },\n): OutreachMailboxNoticeEmail {\n const name = outreachMailboxNoticeName(notice.mailbox)\n const paused = notice.kind === 'auto_pause'\n const subject = paused\n ? `Sequences paused your mailbox ${notice.mailbox.email}`\n : `Your mailbox ${notice.mailbox.email} needs reconnecting`\n const action = paused\n ? 'Re-check the addresses in the sequences it sends, then open Mailboxes and select Resume.'\n : 'Open Mailboxes and select Reconnect to connect it again.'\n const where = options.mailboxesUrl\n ? `${action}\\n\\n${options.mailboxesUrl}`\n : `${action} Mailboxes is under Sequences in your workspace’s console.`\n const text = [\n paused ? `Sequences paused the mailbox ${name}.` : `The mailbox ${name} needs reconnecting.`,\n String(notice.message ?? '').trim(),\n waitingSentence(notice.waiting),\n where,\n 'You are getting this because the mailbox is yours, or you manage the organization it belongs to.',\n ]\n .filter(Boolean)\n .join('\\n\\n')\n return { subject, text }\n}\n"],"names":["outreachMailboxNoticeName","mailbox","name","String","displayName","trim","own","email","sendAs","toLowerCase","outreachReconnectRequiredSentence","errorCode","code","waitingSentence","waiting","count","Math","max","floor","Number","composeOutreachMailboxNotice","notice","options","paused","kind","subject","action","where","mailboxesUrl","text","message","filter","Boolean","join"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;4CAY4C,GA2B5C,kHAAkH,GAClH,OAAO,SAASA,0BAA0BC,OAAyC;QAC7DA,sBAEEA;IAFtB,MAAMC,OAAOC,QAAOF,uBAAAA,QAAQG,WAAW,YAAnBH,uBAAuB,IAAII,IAAI;IACnD,MAAMC,MAAMJ,OAAO,GAAGA,KAAK,EAAE,EAAED,QAAQM,KAAK,CAAC,CAAC,CAAC,GAAGN,QAAQM,KAAK;IAC/D,MAAMC,SAASL,QAAOF,kBAAAA,QAAQO,MAAM,YAAdP,kBAAkB,IACrCI,IAAI,GACJI,WAAW;IACd,OAAOD,UAAUA,WAAWL,OAAOF,QAAQM,KAAK,EAAEF,IAAI,GAAGI,WAAW,KAAK,GAAGH,IAAI,aAAa,EAAEE,QAAQ,GAAGF;AAC5G;AAEA,iFAAiF,GACjF,OAAO,SAASI,kCAAkCC,SAAoC;IACpF,MAAMC,OAAOT,OAAOQ,oBAAAA,YAAa,IAAIN,IAAI;IACzC,OACE,uDACCO,CAAAA,OAAO,CAAC,EAAE,EAAEA,KAAK,CAAC,CAAC,GAAG,EAAC,IACxB,0GACA;AAEJ;AAEA,SAASC,gBAAgBC,OAAe;IACtC,MAAMC,QAAQC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACC,OAAOL,YAAY;IACxD,IAAIC,UAAU,GAAG,OAAO;IACxB,OAAOA,UAAU,IACb,oGACA,GAAGA,MAAM,iGAAiG,CAAC;AACjH;AAEA,+CAA+C,GAC/C,OAAO,SAASK,6BACdC,MAA6B,EAC7BC,OAAwC;QAe/BD;IAbT,MAAMnB,OAAOF,0BAA0BqB,OAAOpB,OAAO;IACrD,MAAMsB,SAASF,OAAOG,IAAI,KAAK;IAC/B,MAAMC,UAAUF,SACZ,CAAC,8BAA8B,EAAEF,OAAOpB,OAAO,CAACM,KAAK,EAAE,GACvD,CAAC,aAAa,EAAEc,OAAOpB,OAAO,CAACM,KAAK,CAAC,mBAAmB,CAAC;IAC7D,MAAMmB,SAASH,SACX,6FACA;IACJ,MAAMI,QAAQL,QAAQM,YAAY,GAC9B,GAAGF,OAAO,IAAI,EAAEJ,QAAQM,YAAY,EAAE,GACtC,GAAGF,OAAO,0DAA0D,CAAC;IACzE,MAAMG,OAAO;QACXN,SAAS,CAAC,6BAA6B,EAAErB,KAAK,CAAC,CAAC,GAAG,CAAC,YAAY,EAAEA,KAAK,oBAAoB,CAAC;QAC5FC,QAAOkB,kBAAAA,OAAOS,OAAO,YAAdT,kBAAkB,IAAIhB,IAAI;QACjCQ,gBAAgBQ,OAAOP,OAAO;QAC9Ba;QACA;KACD,CACEI,MAAM,CAACC,SACPC,IAAI,CAAC;IACR,OAAO;QAAER;QAASI;IAAK;AACzB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/mailbox-notice.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 THE MAILBOX'S OWNER IS TOLD (AGL-3244).\n *\n * A mailbox that pauses itself, or that Google stops accepting, holds every\n * enrollment on it until a person acts — and the person is the member whose\n * mailbox it is, who is not watching the Mailboxes page. So the moment the\n * runtime judges it, they are emailed: the engine's own sentence for the\n * pause, the mailbox by name, how many enrollments are waiting on it, and\n * the link to the page where Resume and Reconnect live.\n *\n * Plain text, one screen long, and the reason in the first line: it is read\n * on a phone between meetings.\n *==========================================*/\n\nexport type OutreachMailboxNoticeKind = 'auto_pause' | 'reconnect_required'\n\nexport interface OutreachMailboxNotice {\n kind: OutreachMailboxNoticeKind\n mailbox: {\n /** The account's own address. */\n email: string\n /** The address mail goes out as, when it is not the account's. */\n sendAs: string\n displayName: string\n }\n /**\n * For a pause, the engine's own sentence — `autoPause.message`; for a\n * reconnect, {@link outreachReconnectRequiredSentence}.\n */\n message: string\n /** Enrollments active on the mailbox, waiting for it to send again. */\n waiting: number\n}\n\nexport interface OutreachMailboxNoticeEmail {\n subject: string\n text: string\n}\n\n/** The mailbox as the notice names it: `Avery Quinn <avery@example.com>`, the send-as beside it when it differs. */\nexport function outreachMailboxNoticeName(mailbox: OutreachMailboxNotice['mailbox']): string {\n const name = String(mailbox.displayName ?? '').trim()\n const own = name ? `${name} <${mailbox.email}>` : mailbox.email\n const sendAs = String(mailbox.sendAs ?? '')\n .trim()\n .toLowerCase()\n return sendAs && sendAs !== String(mailbox.email).trim().toLowerCase() ? `${own}, sending as ${sendAs}` : own\n}\n\n/**\n * What the notice says when the provider stopped accepting the mailbox's\n * connection: Google's, or Microsoft's for a Microsoft 365 mailbox (AGL-3489).\n */\nexport function outreachReconnectRequiredSentence(\n errorCode: string | null | undefined,\n provider: string | null | undefined = 'google',\n): string {\n const code = String(errorCode ?? '').trim()\n const name = provider === 'microsoft' ? 'Microsoft' : 'Google'\n return (\n `${name} stopped accepting this mailbox’s connection` +\n (code ? ` (${code})` : '') +\n ` — the password changed, access was removed from the ${name} account, or an administrator revoked it. ` +\n 'Nothing sends from it until it is connected again.'\n )\n}\n\nfunction waitingSentence(waiting: number): string {\n const count = Math.max(0, Math.floor(Number(waiting) || 0))\n if (count === 0) return 'No enrollment is waiting on it right now.'\n return count === 1\n ? '1 enrollment is active on it and waiting; its next email goes out once the mailbox sends again.'\n : `${count} enrollments are active on it and waiting; their next emails go out once the mailbox sends again.`\n}\n\n/** The email, composed — see the module note. */\nexport function composeOutreachMailboxNotice(\n notice: OutreachMailboxNotice,\n options: { mailboxesUrl: string | null },\n): OutreachMailboxNoticeEmail {\n const name = outreachMailboxNoticeName(notice.mailbox)\n const paused = notice.kind === 'auto_pause'\n const subject = paused\n ? `Sequences paused your mailbox ${notice.mailbox.email}`\n : `Your mailbox ${notice.mailbox.email} needs reconnecting`\n const action = paused\n ? 'Re-check the addresses in the sequences it sends, then open Mailboxes and select Resume.'\n : 'Open Mailboxes and select Reconnect to connect it again.'\n const where = options.mailboxesUrl\n ? `${action}\\n\\n${options.mailboxesUrl}`\n : `${action} Mailboxes is under Sequences in your workspace’s console.`\n const text = [\n paused ? `Sequences paused the mailbox ${name}.` : `The mailbox ${name} needs reconnecting.`,\n String(notice.message ?? '').trim(),\n waitingSentence(notice.waiting),\n where,\n 'You are getting this because the mailbox is yours, or you manage the organization it belongs to.',\n ]\n .filter(Boolean)\n .join('\\n\\n')\n return { subject, text }\n}\n"],"names":["outreachMailboxNoticeName","mailbox","name","String","displayName","trim","own","email","sendAs","toLowerCase","outreachReconnectRequiredSentence","errorCode","provider","code","waitingSentence","waiting","count","Math","max","floor","Number","composeOutreachMailboxNotice","notice","options","paused","kind","subject","action","where","mailboxesUrl","text","message","filter","Boolean","join"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;4CAY4C,GA2B5C,kHAAkH,GAClH,OAAO,SAASA,0BAA0BC,OAAyC;QAC7DA,sBAEEA;IAFtB,MAAMC,OAAOC,QAAOF,uBAAAA,QAAQG,WAAW,YAAnBH,uBAAuB,IAAII,IAAI;IACnD,MAAMC,MAAMJ,OAAO,GAAGA,KAAK,EAAE,EAAED,QAAQM,KAAK,CAAC,CAAC,CAAC,GAAGN,QAAQM,KAAK;IAC/D,MAAMC,SAASL,QAAOF,kBAAAA,QAAQO,MAAM,YAAdP,kBAAkB,IACrCI,IAAI,GACJI,WAAW;IACd,OAAOD,UAAUA,WAAWL,OAAOF,QAAQM,KAAK,EAAEF,IAAI,GAAGI,WAAW,KAAK,GAAGH,IAAI,aAAa,EAAEE,QAAQ,GAAGF;AAC5G;AAEA;;;CAGC,GACD,OAAO,SAASI,kCACdC,SAAoC,EACpCC,WAAsC,QAAQ;IAE9C,MAAMC,OAAOV,OAAOQ,oBAAAA,YAAa,IAAIN,IAAI;IACzC,MAAMH,OAAOU,aAAa,cAAc,cAAc;IACtD,OACE,GAAGV,KAAK,4CAA4C,CAAC,GACpDW,CAAAA,OAAO,CAAC,EAAE,EAAEA,KAAK,CAAC,CAAC,GAAG,EAAC,IACxB,CAAC,qDAAqD,EAAEX,KAAK,0CAA0C,CAAC,GACxG;AAEJ;AAEA,SAASY,gBAAgBC,OAAe;IACtC,MAAMC,QAAQC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACC,OAAOL,YAAY;IACxD,IAAIC,UAAU,GAAG,OAAO;IACxB,OAAOA,UAAU,IACb,oGACA,GAAGA,MAAM,iGAAiG,CAAC;AACjH;AAEA,+CAA+C,GAC/C,OAAO,SAASK,6BACdC,MAA6B,EAC7BC,OAAwC;QAe/BD;IAbT,MAAMpB,OAAOF,0BAA0BsB,OAAOrB,OAAO;IACrD,MAAMuB,SAASF,OAAOG,IAAI,KAAK;IAC/B,MAAMC,UAAUF,SACZ,CAAC,8BAA8B,EAAEF,OAAOrB,OAAO,CAACM,KAAK,EAAE,GACvD,CAAC,aAAa,EAAEe,OAAOrB,OAAO,CAACM,KAAK,CAAC,mBAAmB,CAAC;IAC7D,MAAMoB,SAASH,SACX,6FACA;IACJ,MAAMI,QAAQL,QAAQM,YAAY,GAC9B,GAAGF,OAAO,IAAI,EAAEJ,QAAQM,YAAY,EAAE,GACtC,GAAGF,OAAO,0DAA0D,CAAC;IACzE,MAAMG,OAAO;QACXN,SAAS,CAAC,6BAA6B,EAAEtB,KAAK,CAAC,CAAC,GAAG,CAAC,YAAY,EAAEA,KAAK,oBAAoB,CAAC;QAC5FC,QAAOmB,kBAAAA,OAAOS,OAAO,YAAdT,kBAAkB,IAAIjB,IAAI;QACjCS,gBAAgBQ,OAAOP,OAAO;QAC9Ba;QACA;KACD,CACEI,MAAM,CAACC,SACPC,IAAI,CAAC;IACR,OAAO;QAAER;QAASI;IAAK;AACzB"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { OutreachMailboxStatus, OutreachSequence } from '../model/outreach.types';
18
+ /** The most mailboxes one sequence sends from, its own included. */
19
+ export declare const OUTREACH_SEQUENCE_MAX_MAILBOXES = 20;
20
+ /**
21
+ * The mailboxes a sequence sends from: its own first, then the ones it
22
+ * rotates through, each once, `''` dropped.
23
+ */
24
+ export declare function outreachSequenceMailboxIds(sequence: Pick<OutreachSequence, 'mailboxId'> & {
25
+ mailboxIds?: readonly string[] | null;
26
+ }): string[];
27
+ /** One mailbox of a rotation, as the enroll door read it. */
28
+ export interface OutreachRotationCandidate {
29
+ id: string;
30
+ status: OutreachMailboxStatus;
31
+ /** Active enrollments on it now, across every sequence. */
32
+ activeEnrollments: number;
33
+ /** Whether the sequence's hours ever open in the mailbox's timezone. */
34
+ schedulable: boolean;
35
+ }
36
+ /**
37
+ * Assigns mailboxes to new enrollments one at a time, each call the next
38
+ * person's, counting the ones it has handed out. `null` when no candidate
39
+ * is connected and schedulable — the caller then uses the sequence's own.
40
+ */
41
+ export declare function createOutreachMailboxRotation(candidates: readonly OutreachRotationCandidate[]): () => string | null;