@aglyn/plugins-outreach 1.0.0-beta.143

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 (282) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +93 -0
  3. package/package.json +52 -0
  4. package/src/index.d.ts +22 -0
  5. package/src/index.js +29 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/compliance-section.d.ts +20 -0
  8. package/src/lib/components/compliance-section.js +291 -0
  9. package/src/lib/components/compliance-section.js.map +1 -0
  10. package/src/lib/components/enroll-dialog.d.ts +36 -0
  11. package/src/lib/components/enroll-dialog.js +656 -0
  12. package/src/lib/components/enroll-dialog.js.map +1 -0
  13. package/src/lib/components/enrollments-table.d.ts +28 -0
  14. package/src/lib/components/enrollments-table.js +375 -0
  15. package/src/lib/components/enrollments-table.js.map +1 -0
  16. package/src/lib/components/mailbox-card.d.ts +46 -0
  17. package/src/lib/components/mailbox-card.js +517 -0
  18. package/src/lib/components/mailbox-card.js.map +1 -0
  19. package/src/lib/components/mailboxes-section.d.ts +24 -0
  20. package/src/lib/components/mailboxes-section.js +258 -0
  21. package/src/lib/components/mailboxes-section.js.map +1 -0
  22. package/src/lib/components/outreach-console-page.d.ts +17 -0
  23. package/src/lib/components/outreach-console-page.js +81 -0
  24. package/src/lib/components/outreach-console-page.js.map +1 -0
  25. package/src/lib/components/outreach-console-sections.d.ts +43 -0
  26. package/src/lib/components/outreach-console-sections.js +55 -0
  27. package/src/lib/components/outreach-console-sections.js.map +1 -0
  28. package/src/lib/components/outreach-ui.d.ts +51 -0
  29. package/src/lib/components/outreach-ui.js +171 -0
  30. package/src/lib/components/outreach-ui.js.map +1 -0
  31. package/src/lib/components/send-window-fields.d.ts +25 -0
  32. package/src/lib/components/send-window-fields.js +178 -0
  33. package/src/lib/components/send-window-fields.js.map +1 -0
  34. package/src/lib/components/sequence-detail.d.ts +54 -0
  35. package/src/lib/components/sequence-detail.js +435 -0
  36. package/src/lib/components/sequence-detail.js.map +1 -0
  37. package/src/lib/components/sequence-editor.d.ts +37 -0
  38. package/src/lib/components/sequence-editor.js +485 -0
  39. package/src/lib/components/sequence-editor.js.map +1 -0
  40. package/src/lib/components/sequence-mailbox-picker.d.ts +44 -0
  41. package/src/lib/components/sequence-mailbox-picker.js +137 -0
  42. package/src/lib/components/sequence-mailbox-picker.js.map +1 -0
  43. package/src/lib/components/sequence-preview.d.ts +27 -0
  44. package/src/lib/components/sequence-preview.js +146 -0
  45. package/src/lib/components/sequence-preview.js.map +1 -0
  46. package/src/lib/components/sequence-step-card.d.ts +31 -0
  47. package/src/lib/components/sequence-step-card.js +326 -0
  48. package/src/lib/components/sequence-step-card.js.map +1 -0
  49. package/src/lib/components/sequences-section.d.ts +40 -0
  50. package/src/lib/components/sequences-section.js +310 -0
  51. package/src/lib/components/sequences-section.js.map +1 -0
  52. package/src/lib/components/use-outreach-api.d.ts +38 -0
  53. package/src/lib/components/use-outreach-api.js +139 -0
  54. package/src/lib/components/use-outreach-api.js.map +1 -0
  55. package/src/lib/components/use-outreach-crm.d.ts +50 -0
  56. package/src/lib/components/use-outreach-crm.js +200 -0
  57. package/src/lib/components/use-outreach-crm.js.map +1 -0
  58. package/src/lib/components/use-outreach-data.d.ts +55 -0
  59. package/src/lib/components/use-outreach-data.js +199 -0
  60. package/src/lib/components/use-outreach-data.js.map +1 -0
  61. package/src/lib/components/use-outreach-mailbox-api.d.ts +30 -0
  62. package/src/lib/components/use-outreach-mailbox-api.js +101 -0
  63. package/src/lib/components/use-outreach-mailbox-api.js.map +1 -0
  64. package/src/lib/components/use-outreach-mailboxes.d.ts +22 -0
  65. package/src/lib/components/use-outreach-mailboxes.js +68 -0
  66. package/src/lib/components/use-outreach-mailboxes.js.map +1 -0
  67. package/src/lib/components/use-outreach-settings.d.ts +17 -0
  68. package/src/lib/components/use-outreach-settings.js +64 -0
  69. package/src/lib/components/use-outreach-settings.js.map +1 -0
  70. package/src/lib/constants/api-routes.d.ts +68 -0
  71. package/src/lib/constants/api-routes.js +57 -0
  72. package/src/lib/constants/api-routes.js.map +1 -0
  73. package/src/lib/constants/bundle-common.d.ts +46 -0
  74. package/src/lib/constants/bundle-common.js +55 -0
  75. package/src/lib/constants/bundle-common.js.map +1 -0
  76. package/src/lib/constants/runtime-jobs.d.ts +24 -0
  77. package/src/lib/constants/runtime-jobs.js +24 -0
  78. package/src/lib/constants/runtime-jobs.js.map +1 -0
  79. package/src/lib/declarations.console-server.d.ts +38 -0
  80. package/src/lib/declarations.console-server.js +99 -0
  81. package/src/lib/declarations.console-server.js.map +1 -0
  82. package/src/lib/engine/compose.d.ts +86 -0
  83. package/src/lib/engine/compose.js +212 -0
  84. package/src/lib/engine/compose.js.map +1 -0
  85. package/src/lib/engine/delivery-status.d.ts +53 -0
  86. package/src/lib/engine/delivery-status.js +184 -0
  87. package/src/lib/engine/delivery-status.js.map +1 -0
  88. package/src/lib/engine/do-not-contact.d.ts +18 -0
  89. package/src/lib/engine/do-not-contact.js +34 -0
  90. package/src/lib/engine/do-not-contact.js.map +1 -0
  91. package/src/lib/engine/enrollment-state.d.ts +156 -0
  92. package/src/lib/engine/enrollment-state.js +303 -0
  93. package/src/lib/engine/enrollment-state.js.map +1 -0
  94. package/src/lib/engine/gates.d.ts +125 -0
  95. package/src/lib/engine/gates.js +232 -0
  96. package/src/lib/engine/gates.js.map +1 -0
  97. package/src/lib/engine/index.d.ts +28 -0
  98. package/src/lib/engine/index.js +33 -0
  99. package/src/lib/engine/index.js.map +1 -0
  100. package/src/lib/engine/mailbox-health.d.ts +49 -0
  101. package/src/lib/engine/mailbox-health.js +83 -0
  102. package/src/lib/engine/mailbox-health.js.map +1 -0
  103. package/src/lib/engine/opt-out-intent.d.ts +41 -0
  104. package/src/lib/engine/opt-out-intent.js +220 -0
  105. package/src/lib/engine/opt-out-intent.js.map +1 -0
  106. package/src/lib/engine/recipient-country.d.ts +41 -0
  107. package/src/lib/engine/recipient-country.js +171 -0
  108. package/src/lib/engine/recipient-country.js.map +1 -0
  109. package/src/lib/engine/schedule.d.ts +70 -0
  110. package/src/lib/engine/schedule.js +115 -0
  111. package/src/lib/engine/schedule.js.map +1 -0
  112. package/src/lib/engine/sending-capacity.d.ts +96 -0
  113. package/src/lib/engine/sending-capacity.js +172 -0
  114. package/src/lib/engine/sending-capacity.js.map +1 -0
  115. package/src/lib/engine/sequence-validation.d.ts +90 -0
  116. package/src/lib/engine/sequence-validation.js +291 -0
  117. package/src/lib/engine/sequence-validation.js.map +1 -0
  118. package/src/lib/engine/thread-classification.d.ts +78 -0
  119. package/src/lib/engine/thread-classification.js +224 -0
  120. package/src/lib/engine/thread-classification.js.map +1 -0
  121. package/src/lib/engine/thread-message.d.ts +80 -0
  122. package/src/lib/engine/thread-message.js +143 -0
  123. package/src/lib/engine/thread-message.js.map +1 -0
  124. package/src/lib/enrollment/enroll-people.d.ts +81 -0
  125. package/src/lib/enrollment/enroll-people.js +250 -0
  126. package/src/lib/enrollment/enroll-people.js.map +1 -0
  127. package/src/lib/enrollment/gate-lookups.d.ts +32 -0
  128. package/src/lib/enrollment/gate-lookups.js +154 -0
  129. package/src/lib/enrollment/gate-lookups.js.map +1 -0
  130. package/src/lib/mailboxes/mailbox-api.d.ts +126 -0
  131. package/src/lib/mailboxes/mailbox-api.js +66 -0
  132. package/src/lib/mailboxes/mailbox-api.js.map +1 -0
  133. package/src/lib/mailboxes/mailbox-credentials.d.ts +95 -0
  134. package/src/lib/mailboxes/mailbox-credentials.js +102 -0
  135. package/src/lib/mailboxes/mailbox-credentials.js.map +1 -0
  136. package/src/lib/mailboxes/mailbox-erasure.d.ts +69 -0
  137. package/src/lib/mailboxes/mailbox-erasure.js +148 -0
  138. package/src/lib/mailboxes/mailbox-erasure.js.map +1 -0
  139. package/src/lib/mailboxes/mailbox-gate.d.ts +75 -0
  140. package/src/lib/mailboxes/mailbox-gate.js +98 -0
  141. package/src/lib/mailboxes/mailbox-gate.js.map +1 -0
  142. package/src/lib/mailboxes/mailbox-revoke.d.ts +59 -0
  143. package/src/lib/mailboxes/mailbox-revoke.js +41 -0
  144. package/src/lib/mailboxes/mailbox-revoke.js.map +1 -0
  145. package/src/lib/mailboxes/mailbox-routes.d.ts +119 -0
  146. package/src/lib/mailboxes/mailbox-routes.js +605 -0
  147. package/src/lib/mailboxes/mailbox-routes.js.map +1 -0
  148. package/src/lib/mailboxes/mailbox-settings.d.ts +110 -0
  149. package/src/lib/mailboxes/mailbox-settings.js +212 -0
  150. package/src/lib/mailboxes/mailbox-settings.js.map +1 -0
  151. package/src/lib/mailboxes/mailbox-transport.d.ts +64 -0
  152. package/src/lib/mailboxes/mailbox-transport.js +94 -0
  153. package/src/lib/mailboxes/mailbox-transport.js.map +1 -0
  154. package/src/lib/mailboxes/oauth-redirect.d.ts +41 -0
  155. package/src/lib/mailboxes/oauth-redirect.js +63 -0
  156. package/src/lib/mailboxes/oauth-redirect.js.map +1 -0
  157. package/src/lib/mailboxes/oauth-state.d.ts +152 -0
  158. package/src/lib/mailboxes/oauth-state.js +212 -0
  159. package/src/lib/mailboxes/oauth-state.js.map +1 -0
  160. package/src/lib/mailboxes/outreach-config.d.ts +64 -0
  161. package/src/lib/mailboxes/outreach-config.js +77 -0
  162. package/src/lib/mailboxes/outreach-config.js.map +1 -0
  163. package/src/lib/mailboxes/register-mailbox-routes.d.ts +47 -0
  164. package/src/lib/mailboxes/register-mailbox-routes.js +135 -0
  165. package/src/lib/mailboxes/register-mailbox-routes.js.map +1 -0
  166. package/src/lib/model/compliance-settings.d.ts +78 -0
  167. package/src/lib/model/compliance-settings.js +155 -0
  168. package/src/lib/model/compliance-settings.js.map +1 -0
  169. package/src/lib/model/outreach-api.d.ts +210 -0
  170. package/src/lib/model/outreach-api.js +28 -0
  171. package/src/lib/model/outreach-api.js.map +1 -0
  172. package/src/lib/model/outreach.types.d.ts +563 -0
  173. package/src/lib/model/outreach.types.js +225 -0
  174. package/src/lib/model/outreach.types.js.map +1 -0
  175. package/src/lib/model/sequence-draft.d.ts +84 -0
  176. package/src/lib/model/sequence-draft.js +239 -0
  177. package/src/lib/model/sequence-draft.js.map +1 -0
  178. package/src/lib/model/step-preview.d.ts +45 -0
  179. package/src/lib/model/step-preview.js +98 -0
  180. package/src/lib/model/step-preview.js.map +1 -0
  181. package/src/lib/model/stored-records.d.ts +23 -0
  182. package/src/lib/model/stored-records.js +84 -0
  183. package/src/lib/model/stored-records.js.map +1 -0
  184. package/src/lib/plugin.d.ts +46 -0
  185. package/src/lib/plugin.js +86 -0
  186. package/src/lib/plugin.js.map +1 -0
  187. package/src/lib/routes/enroll-routes.d.ts +64 -0
  188. package/src/lib/routes/enroll-routes.js +350 -0
  189. package/src/lib/routes/enroll-routes.js.map +1 -0
  190. package/src/lib/routes/enrollment-routes.d.ts +19 -0
  191. package/src/lib/routes/enrollment-routes.js +171 -0
  192. package/src/lib/routes/enrollment-routes.js.map +1 -0
  193. package/src/lib/routes/platform-deps.d.ts +51 -0
  194. package/src/lib/routes/platform-deps.js +66 -0
  195. package/src/lib/routes/platform-deps.js.map +1 -0
  196. package/src/lib/routes/preview-routes.d.ts +33 -0
  197. package/src/lib/routes/preview-routes.js +117 -0
  198. package/src/lib/routes/preview-routes.js.map +1 -0
  199. package/src/lib/routes/register-routes.d.ts +22 -0
  200. package/src/lib/routes/register-routes.js +92 -0
  201. package/src/lib/routes/register-routes.js.map +1 -0
  202. package/src/lib/routes/route-deps.d.ts +49 -0
  203. package/src/lib/routes/route-deps.js +27 -0
  204. package/src/lib/routes/route-deps.js.map +1 -0
  205. package/src/lib/routes/route-gate.d.ts +94 -0
  206. package/src/lib/routes/route-gate.js +87 -0
  207. package/src/lib/routes/route-gate.js.map +1 -0
  208. package/src/lib/routes/route-http.d.ts +32 -0
  209. package/src/lib/routes/route-http.js +62 -0
  210. package/src/lib/routes/route-http.js.map +1 -0
  211. package/src/lib/routes/sequence-routes.d.ts +32 -0
  212. package/src/lib/routes/sequence-routes.js +359 -0
  213. package/src/lib/routes/sequence-routes.js.map +1 -0
  214. package/src/lib/routes/settings-routes.d.ts +30 -0
  215. package/src/lib/routes/settings-routes.js +73 -0
  216. package/src/lib/routes/settings-routes.js.map +1 -0
  217. package/src/lib/runtime/enrollment-events.d.ts +86 -0
  218. package/src/lib/runtime/enrollment-events.js +141 -0
  219. package/src/lib/runtime/enrollment-events.js.map +1 -0
  220. package/src/lib/runtime/fixtures/fake-gmail.d.ts +121 -0
  221. package/src/lib/runtime/fixtures/fake-gmail.js +312 -0
  222. package/src/lib/runtime/fixtures/fake-gmail.js.map +1 -0
  223. package/src/lib/runtime/mailbox-health-store.d.ts +47 -0
  224. package/src/lib/runtime/mailbox-health-store.js +168 -0
  225. package/src/lib/runtime/mailbox-health-store.js.map +1 -0
  226. package/src/lib/runtime/person-erasure.d.ts +39 -0
  227. package/src/lib/runtime/person-erasure.js +57 -0
  228. package/src/lib/runtime/person-erasure.js.map +1 -0
  229. package/src/lib/runtime/platform-runtime-deps.d.ts +29 -0
  230. package/src/lib/runtime/platform-runtime-deps.js +98 -0
  231. package/src/lib/runtime/platform-runtime-deps.js.map +1 -0
  232. package/src/lib/runtime/runtime-deps.d.ts +77 -0
  233. package/src/lib/runtime/runtime-deps.js +18 -0
  234. package/src/lib/runtime/runtime-deps.js.map +1 -0
  235. package/src/lib/runtime/send-job.d.ts +42 -0
  236. package/src/lib/runtime/send-job.js +780 -0
  237. package/src/lib/runtime/send-job.js.map +1 -0
  238. package/src/lib/runtime/sync-job.d.ts +41 -0
  239. package/src/lib/runtime/sync-job.js +480 -0
  240. package/src/lib/runtime/sync-job.js.map +1 -0
  241. package/src/lib/runtime/timeline.d.ts +67 -0
  242. package/src/lib/runtime/timeline.js +93 -0
  243. package/src/lib/runtime/timeline.js.map +1 -0
  244. package/src/lib/runtime/unsubscribe-link.d.ts +75 -0
  245. package/src/lib/runtime/unsubscribe-link.js +121 -0
  246. package/src/lib/runtime/unsubscribe-link.js.map +1 -0
  247. package/src/lib/runtime/unsubscribe-route.d.ts +24 -0
  248. package/src/lib/runtime/unsubscribe-route.js +103 -0
  249. package/src/lib/runtime/unsubscribe-route.js.map +1 -0
  250. package/src/lib/server.d.ts +53 -0
  251. package/src/lib/server.js +94 -0
  252. package/src/lib/server.js.map +1 -0
  253. package/src/lib/storage/compliance-settings-store.d.ts +44 -0
  254. package/src/lib/storage/compliance-settings-store.js +78 -0
  255. package/src/lib/storage/compliance-settings-store.js.map +1 -0
  256. package/src/lib/storage/do-not-contact-store.d.ts +60 -0
  257. package/src/lib/storage/do-not-contact-store.js +168 -0
  258. package/src/lib/storage/do-not-contact-store.js.map +1 -0
  259. package/src/lib/storage/outreach-records.d.ts +33 -0
  260. package/src/lib/storage/outreach-records.js +35 -0
  261. package/src/lib/storage/outreach-records.js.map +1 -0
  262. package/src/lib/subprocessors.d.ts +48 -0
  263. package/src/lib/subprocessors.js +70 -0
  264. package/src/lib/subprocessors.js.map +1 -0
  265. package/src/lib/transport/gmail-client.d.ts +145 -0
  266. package/src/lib/transport/gmail-client.js +229 -0
  267. package/src/lib/transport/gmail-client.js.map +1 -0
  268. package/src/lib/transport/gmail-errors.d.ts +84 -0
  269. package/src/lib/transport/gmail-errors.js +138 -0
  270. package/src/lib/transport/gmail-errors.js.map +1 -0
  271. package/src/lib/transport/google-oauth.d.ts +138 -0
  272. package/src/lib/transport/google-oauth.js +247 -0
  273. package/src/lib/transport/google-oauth.js.map +1 -0
  274. package/src/lib/transport/http.d.ts +53 -0
  275. package/src/lib/transport/http.js +79 -0
  276. package/src/lib/transport/http.js.map +1 -0
  277. package/src/lib/transport/rfc5322.d.ts +116 -0
  278. package/src/lib/transport/rfc5322.js +330 -0
  279. package/src/lib/transport/rfc5322.js.map +1 -0
  280. package/src/lib/transport/send-message.d.ts +69 -0
  281. package/src/lib/transport/send-message.js +77 -0
  282. package/src/lib/transport/send-message.js.map +1 -0
@@ -0,0 +1,780 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { consentGroupForHost } from "@aglyn/aglyn/app-utils/consent-groups";
18
+ import { CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
19
+ import { normalizeCrmEmailTemplate } from "@aglyn/aglyn/app-utils/crm-email-templates";
20
+ import { randomUUID } from "node:crypto";
21
+ import { composeOutreachEmail } from "../engine/compose.js";
22
+ import { planOutreachStepCompletion } from "../engine/enrollment-state.js";
23
+ import { evaluateOutreachGates } from "../engine/gates.js";
24
+ import { outreachSentToday, outreachTickAllowance, selectDueOutreachEnrollments } from "../engine/sending-capacity.js";
25
+ import { readOutreachEnrollCandidates } from "../enrollment/enroll-people.js";
26
+ import { readOutreachGateLookups } from "../enrollment/gate-lookups.js";
27
+ import { mailboxRef } from "../mailboxes/mailbox-credentials.js";
28
+ import { outreachEffectiveDailyCap, outreachLocalDay } from "../mailboxes/mailbox-settings.js";
29
+ import { markOutreachMailboxReconnectRequired } from "../mailboxes/mailbox-transport.js";
30
+ import { effectiveOutreachAllowedCountries } from "../model/compliance-settings.js";
31
+ import { OUTREACH_COLLECTIONS } from "../model/outreach.types.js";
32
+ import { readOutreachComplianceSettingsDoc } from "../storage/compliance-settings-store.js";
33
+ import { outreachOrgCollection, readStoredOutreachEnrollment, readStoredOutreachMailbox, readStoredOutreachSequence } from "../storage/outreach-records.js";
34
+ import { GmailTransportError, isReconnectRequired } from "../transport/gmail-errors.js";
35
+ import { Rfc5322MessageError } from "../transport/rfc5322.js";
36
+ import { sendComposedOutreachEmail } from "../transport/send-message.js";
37
+ import { applyOutreachEvent } from "./enrollment-events.js";
38
+ import { outreachSendDigest, withRecentSend } from "./mailbox-health-store.js";
39
+ import { fileOutreachEmail, fileOutreachTask, OUTREACH_TASK_KIND_TO_RECORD } from "./timeline.js";
40
+ import { outreachUnsubscribeMailbox } from "./unsubscribe-link.js";
41
+ /*==========================================
42
+ * THE SEND JOB (AGL-2981): every fifteen minutes, on the console.
43
+ *
44
+ * ## What comes due
45
+ *
46
+ * One collection-group read finds every active enrollment whose next step
47
+ * is due, oldest first, bounded per run. They are grouped by workspace and
48
+ * mailbox, and each group is judged before anything is sent:
49
+ *
50
+ * - the workspace: Outreach switched on, entitled and released to it;
51
+ * - the mailbox: an enrollment whose mailbox is gone, disconnected, or whose
52
+ * member left the organization is STOPPED — nothing will ever send it, and
53
+ * leaving it active would show a sequence that is not running. A paused
54
+ * mailbox, or one waiting to be reconnected, holds its enrollments;
55
+ * - the member it sends as: a lockdown of the platform, the workspace or the
56
+ * member holds everything.
57
+ *
58
+ * ## How much goes
59
+ *
60
+ * The engine's per-run allowance — the day's cap and ramp, spread across
61
+ * the window, at most five a run — picks the enrollments (follow-ups first);
62
+ * a task step is due whenever its time comes and spends no allowance.
63
+ *
64
+ * ## One send, exactly once
65
+ *
66
+ * Each email is CLAIMED before it is sent: a transaction over the enrollment
67
+ * and its mailbox checks the enrollment is still active on the same step
68
+ * with no live claim, and that the mailbox's day has room under its cap,
69
+ * then writes the claim and reserves the day's count. Two runs racing for
70
+ * one enrollment write one claim; two racing for one mailbox's last slot
71
+ * reserve it once. The claim carries the `Message-ID` minted for the send,
72
+ * so a claim a run died holding is settled by finding the message in the
73
+ * mailbox rather than by sending it a second time. A send that fails hands
74
+ * its reservation back.
75
+ *
76
+ * Before it is claimed, every gate is asked again — the do-not-contact
77
+ * list, the suppression lists, the sales topic, the roster, the other
78
+ * sequences — because a person enrolled on Monday can unsubscribe by
79
+ * Thursday. A lookup that could not be made holds the send; a gate that
80
+ * refuses stops the enrollment, saying why.
81
+ *==========================================*/ export { OUTREACH_SEND_JOB_ID } from "../constants/runtime-jobs.js";
82
+ /** The most due enrollments one run reads. */ export const OUTREACH_SEND_SCAN_LIMIT = 300;
83
+ /** How long a claim holds before a later run settles it. */ export const OUTREACH_CLAIM_STALE_MS = 10 * 60000;
84
+ const blankReport = ()=>({
85
+ due: 0,
86
+ sent: 0,
87
+ tasks: 0,
88
+ stopped: 0,
89
+ failed: 0,
90
+ held: 0,
91
+ recovered: 0
92
+ });
93
+ /** Runs the send job once — see the module note. */ export async function runOutreachSendJob(deps, context) {
94
+ const firestore = deps.firestore();
95
+ const report = blankReport();
96
+ const due = await firestore.collectionGroup(OUTREACH_COLLECTIONS.enrollments).where('status', '==', 'active').where('nextDueAtMs', '<=', context.nowMs).orderBy('nextDueAtMs').limit(OUTREACH_SEND_SCAN_LIMIT).get();
97
+ report.due = due.size;
98
+ const byOrg = new Map();
99
+ for (const doc of due.docs){
100
+ var _byOrg_get, _mailboxes_get;
101
+ var _doc_ref_parent_parent;
102
+ const orgId = (_doc_ref_parent_parent = doc.ref.parent.parent) == null ? void 0 : _doc_ref_parent_parent.id;
103
+ const enrollment = readStoredOutreachEnrollment(doc.id, doc.data());
104
+ if (!orgId || !enrollment) continue;
105
+ const mailboxes = (_byOrg_get = byOrg.get(orgId)) != null ? _byOrg_get : new Map();
106
+ mailboxes.set(enrollment.mailboxId, [
107
+ ...(_mailboxes_get = mailboxes.get(enrollment.mailboxId)) != null ? _mailboxes_get : [],
108
+ enrollment
109
+ ]);
110
+ byOrg.set(orgId, mailboxes);
111
+ }
112
+ for (const [orgId, mailboxes] of byOrg){
113
+ const waiting = [
114
+ ...mailboxes.values()
115
+ ].reduce((sum, list)=>sum + list.length, 0);
116
+ if (deps.now() >= context.deadlineMs) {
117
+ report.held += waiting;
118
+ continue;
119
+ }
120
+ const orgSnapshot = await firestore.collection('orgs').doc(orgId).get();
121
+ const org = orgSnapshot.exists ? orgSnapshot.data() : null;
122
+ if (!org || await deps.orgRefusal(orgId, org)) {
123
+ report.held += waiting;
124
+ continue;
125
+ }
126
+ const settings = await readOutreachComplianceSettingsDoc(firestore, orgId);
127
+ for (const [mailboxId, enrollments] of mailboxes){
128
+ await runMailbox(deps, firestore, {
129
+ orgId,
130
+ org,
131
+ mailboxId,
132
+ enrollments,
133
+ settings,
134
+ report,
135
+ context
136
+ });
137
+ }
138
+ }
139
+ return report;
140
+ }
141
+ /** The reason an unusable mailbox stops an enrollment, or `null` for a usable one. */ async function unusableMailbox(firestore, orgId, mailbox) {
142
+ if (!mailbox) return 'The mailbox this sequence sends from was removed.';
143
+ if (mailbox.status === 'disconnected') return 'The mailbox this sequence sends from was disconnected.';
144
+ const member = await firestore.collection('orgs').doc(orgId).collection('members').doc(mailbox.connectedByUid).get();
145
+ return member.exists ? null : 'The member who connected the mailbox this sequence sends from is no longer in the organization.';
146
+ }
147
+ async function runMailbox(deps, firestore, input) {
148
+ const { orgId, org, report, context } = input;
149
+ const nowMs = context.nowMs;
150
+ const snapshot = await mailboxRef(firestore, orgId, input.mailboxId).get();
151
+ const mailbox = readStoredOutreachMailbox(input.mailboxId, snapshot.exists ? snapshot.data() : undefined);
152
+ const unusable = await unusableMailbox(firestore, orgId, mailbox);
153
+ if (unusable || !mailbox) {
154
+ for (const enrollment of input.enrollments){
155
+ const outcome = await applyOutreachEvent(firestore, {
156
+ orgId,
157
+ enrollmentId: enrollment.id,
158
+ event: {
159
+ type: 'stop',
160
+ atMs: nowMs,
161
+ byUid: null,
162
+ reason: 'gate',
163
+ detail: unusable
164
+ },
165
+ nowMs
166
+ });
167
+ if (outcome.changed) report.stopped += 1;
168
+ }
169
+ return;
170
+ }
171
+ if (mailbox.status !== 'connected' || await deps.sendRefusal({
172
+ orgId,
173
+ org,
174
+ uid: mailbox.connectedByUid
175
+ })) {
176
+ report.held += input.enrollments.length;
177
+ return;
178
+ }
179
+ const run = {
180
+ orgId,
181
+ org,
182
+ mailbox,
183
+ settings: input.settings,
184
+ report,
185
+ deadlineMs: context.deadlineMs,
186
+ opened: null,
187
+ stopped: false,
188
+ templates: new Map()
189
+ };
190
+ const sequenceIds = [
191
+ ...new Set(input.enrollments.map((enrollment)=>enrollment.sequenceId))
192
+ ];
193
+ const sequenceSnapshots = await firestore.getAll(...sequenceIds.map((id)=>outreachOrgCollection(firestore, orgId, 'sequences').doc(id)));
194
+ const sequences = new Map();
195
+ sequenceSnapshots.forEach((sequenceSnapshot, index)=>{
196
+ const sequence = readStoredOutreachSequence(sequenceIds[index], sequenceSnapshot.exists ? sequenceSnapshot.data() : undefined);
197
+ if (sequence) sequences.set(sequence.id, sequence);
198
+ });
199
+ const candidates = [];
200
+ for (const enrollment of input.enrollments){
201
+ const sequence = sequences.get(enrollment.sequenceId);
202
+ const claim = enrollment.sendClaim;
203
+ if (claim && nowMs - claim.atMs < OUTREACH_CLAIM_STALE_MS) continue;
204
+ if (claim && sequence) {
205
+ if (await recoverClaim(deps, firestore, run, enrollment, sequence)) report.recovered += 1;
206
+ continue;
207
+ }
208
+ if (sequence) candidates.push({
209
+ enrollment,
210
+ sequence
211
+ });
212
+ }
213
+ const allowance = outreachTickAllowance({
214
+ mailbox,
215
+ nowMs
216
+ });
217
+ const selection = selectDueOutreachEnrollments({
218
+ candidates,
219
+ mailbox,
220
+ nowMs,
221
+ allowance: allowance.allowance
222
+ });
223
+ report.held += selection.deferred.length;
224
+ for (const candidate of selection.tasks){
225
+ if (deps.now() >= context.deadlineMs) {
226
+ report.held += 1;
227
+ continue;
228
+ }
229
+ if (await runTaskStep(deps, firestore, run, candidate)) report.tasks += 1;
230
+ }
231
+ if (!selection.emails.length) return;
232
+ // The people behind the emails, read once per site: the contact and its
233
+ // company, and every lookup the gates decide on.
234
+ const people = new Map();
235
+ const lookups = new Map();
236
+ const byHost = new Map();
237
+ for (const candidate of selection.emails){
238
+ var _byHost_get;
239
+ const enrollment = candidate.enrollment;
240
+ byHost.set(enrollment.hostId, [
241
+ ...(_byHost_get = byHost.get(enrollment.hostId)) != null ? _byHost_get : [],
242
+ enrollment
243
+ ]);
244
+ }
245
+ const siteNames = new Map();
246
+ for (const [hostId, enrollments] of byHost){
247
+ var _host_get;
248
+ const contactGroupId = consentGroupForHost(org, hostId).groupId;
249
+ const [candidatesRead, lookupsRead, host] = await Promise.all([
250
+ readOutreachEnrollCandidates(firestore, {
251
+ orgId,
252
+ hostId,
253
+ contactGroupId,
254
+ contactIds: enrollments.map((enrollment)=>enrollment.contactId)
255
+ }),
256
+ readOutreachGateLookups(firestore, {
257
+ orgId,
258
+ hostId,
259
+ people: enrollments.map((enrollment)=>({
260
+ contactId: enrollment.contactId,
261
+ email: enrollment.email
262
+ }))
263
+ }),
264
+ firestore.collection('hosts').doc(hostId).get()
265
+ ]);
266
+ for (const person of candidatesRead)people.set(person.contactId, person);
267
+ for (const [contactId, answer] of lookupsRead)lookups.set(contactId, answer);
268
+ siteNames.set(hostId, host.exists ? String((_host_get = host.get('name')) != null ? _host_get : '') : '');
269
+ }
270
+ for (const candidate of selection.emails){
271
+ var _people_get, _lookups_get, _siteNames_get;
272
+ if (run.stopped || deps.now() >= context.deadlineMs) {
273
+ report.held += 1;
274
+ continue;
275
+ }
276
+ const enrollment = candidate.enrollment;
277
+ await runEmailStep(deps, firestore, run, {
278
+ enrollment,
279
+ sequence: candidate.sequence,
280
+ person: (_people_get = people.get(enrollment.contactId)) != null ? _people_get : null,
281
+ lookups: (_lookups_get = lookups.get(enrollment.contactId)) != null ? _lookups_get : null,
282
+ siteName: (_siteNames_get = siteNames.get(enrollment.hostId)) != null ? _siteNames_get : ''
283
+ });
284
+ }
285
+ }
286
+ /** Any lookup the runtime could not make: the send waits for a run that can. */ function uncheckable(lookups) {
287
+ return !lookups || Object.values(lookups).some((value)=>value === null);
288
+ }
289
+ /** The event a gate's refusal ends an enrollment with. */ function gateEvent(blocks, atMs) {
290
+ const optedOut = blocks.find((block)=>block.code === 'do_not_contact' || block.code === 'sales_opted_out');
291
+ if (optedOut) return {
292
+ type: 'opt_out',
293
+ atMs,
294
+ reason: 'do_not_contact',
295
+ detail: optedOut.reason
296
+ };
297
+ return {
298
+ type: 'stop',
299
+ atMs,
300
+ byUid: null,
301
+ reason: 'gate',
302
+ detail: blocks.map((block)=>block.reason).join(' ')
303
+ };
304
+ }
305
+ /** The sender line a mailbox's mail goes out with. */ const senderOf = (mailbox)=>({
306
+ address: mailbox.sendAs || mailbox.email,
307
+ name: mailbox.displayName || null
308
+ });
309
+ async function openClient(deps, firestore, run) {
310
+ var _run, _opened;
311
+ const opened = (_opened = (_run = run).opened) != null ? _opened : _run.opened = await deps.openMailbox(run.mailbox.id);
312
+ if (opened.ok === true) return opened.client;
313
+ run.stopped = true;
314
+ if (opened.reason !== 'not-configured') {
315
+ await markOutreachMailboxReconnectRequired(firestore, {
316
+ orgId: run.orgId,
317
+ mailboxId: run.mailbox.id,
318
+ errorCode: opened.reason,
319
+ nowMs: deps.now()
320
+ });
321
+ }
322
+ return null;
323
+ }
324
+ /**
325
+ * Claims one step: the enrollment still active on the same step with no
326
+ * live claim, and — for an email — a slot under the mailbox's cap today,
327
+ * reserved in the same transaction.
328
+ */ async function claimStep(deps, firestore, run, enrollment, messageId) {
329
+ const enrollmentRef = outreachOrgCollection(firestore, run.orgId, 'enrollments').doc(enrollment.id);
330
+ const boxRef = mailboxRef(firestore, run.orgId, run.mailbox.id);
331
+ const nowMs = deps.now();
332
+ return firestore.runTransaction(async (transaction)=>{
333
+ const [current, box] = await Promise.all([
334
+ transaction.get(enrollmentRef),
335
+ transaction.get(boxRef)
336
+ ]);
337
+ const stored = readStoredOutreachEnrollment(enrollment.id, current.exists ? current.data() : undefined);
338
+ const claim = stored == null ? void 0 : stored.sendClaim;
339
+ if (!stored || stored.status !== 'active' || stored.stepIndex !== enrollment.stepIndex || stored.nextDueAtMs === null || stored.nextDueAtMs > nowMs || claim && nowMs - claim.atMs < OUTREACH_CLAIM_STALE_MS) {
340
+ return 'taken';
341
+ }
342
+ if (messageId) {
343
+ const mailbox = readStoredOutreachMailbox(run.mailbox.id, box.exists ? box.data() : undefined);
344
+ if (!mailbox || mailbox.status !== 'connected') return 'taken';
345
+ const sent = outreachSentToday(mailbox.health, nowMs, mailbox.timezone);
346
+ if (sent >= outreachEffectiveDailyCap(mailbox, nowMs)) return 'cap';
347
+ transaction.update(boxRef, {
348
+ 'health.sentToday': sent + 1,
349
+ 'health.sentOnDay': outreachLocalDay(nowMs, mailbox.timezone)
350
+ });
351
+ }
352
+ transaction.update(enrollmentRef, {
353
+ sendClaim: {
354
+ token: randomUUID(),
355
+ atMs: nowMs,
356
+ stepIndex: enrollment.stepIndex,
357
+ messageId
358
+ },
359
+ updatedAtMs: nowMs
360
+ });
361
+ return 'claimed';
362
+ });
363
+ }
364
+ /** Drops a claim, and hands an email's reservation back to the mailbox's day. */ async function releaseClaim(deps, firestore, run, enrollment, reserved, event = null) {
365
+ const nowMs = deps.now();
366
+ await applyOutreachEvent(firestore, {
367
+ orgId: run.orgId,
368
+ enrollmentId: enrollment.id,
369
+ event,
370
+ extra: ()=>({
371
+ sendClaim: null
372
+ }),
373
+ nowMs
374
+ });
375
+ if (!reserved) return;
376
+ const boxRef = mailboxRef(firestore, run.orgId, run.mailbox.id);
377
+ await firestore.runTransaction(async (transaction)=>{
378
+ var _mailbox_health;
379
+ const box = await transaction.get(boxRef);
380
+ const mailbox = readStoredOutreachMailbox(run.mailbox.id, box.exists ? box.data() : undefined);
381
+ if (!mailbox || ((_mailbox_health = mailbox.health) == null ? void 0 : _mailbox_health.sentOnDay) !== outreachLocalDay(nowMs, mailbox.timezone)) return;
382
+ transaction.update(boxRef, {
383
+ 'health.sentToday': Math.max(0, (Number(mailbox.health.sentToday) || 0) - 1)
384
+ });
385
+ });
386
+ }
387
+ /**
388
+ * Records a step that ran: the engine's completion patch, the claim
389
+ * cleared, the step's record kept — and for an email the mailbox's send on
390
+ * its day, its last-sent time and its window of recent sends. Refuses
391
+ * (answers false) when the claim is no longer this run's.
392
+ */ async function completeStep(deps, firestore, run, input) {
393
+ const enrollmentRef = outreachOrgCollection(firestore, run.orgId, 'enrollments').doc(input.enrollment.id);
394
+ const boxRef = mailboxRef(firestore, run.orgId, run.mailbox.id);
395
+ const nowMs = deps.now();
396
+ return firestore.runTransaction(async (transaction)=>{
397
+ var _stored_stepRecords, _plan_patch;
398
+ const [current, box] = await Promise.all([
399
+ transaction.get(enrollmentRef),
400
+ input.sent ? transaction.get(boxRef) : Promise.resolve(null)
401
+ ]);
402
+ const stored = readStoredOutreachEnrollment(input.enrollment.id, current.exists ? current.data() : undefined);
403
+ if (!(stored == null ? void 0 : stored.sendClaim) || stored.sendClaim.messageId !== input.claimMessageId) return false;
404
+ if (stored.stepIndex !== input.enrollment.stepIndex) return false;
405
+ // The step RAN, whatever happened to the enrollment while it did — a
406
+ // member paused it, a reply stopped it — so it is recorded as run, and a
407
+ // resume never sends it twice.
408
+ const plan = planOutreachStepCompletion({
409
+ enrollment: _extends({}, stored, {
410
+ status: 'active'
411
+ }),
412
+ sequence: input.sequence,
413
+ mailbox: run.mailbox,
414
+ completedAtMs: nowMs,
415
+ sent: input.sent,
416
+ random: deps.random
417
+ });
418
+ const patch = _extends({
419
+ sendClaim: null,
420
+ stepRecords: [
421
+ ...(_stored_stepRecords = stored.stepRecords) != null ? _stored_stepRecords : [],
422
+ input.record
423
+ ].slice(-20),
424
+ updatedAtMs: nowMs
425
+ }, (_plan_patch = plan.patch) != null ? _plan_patch : {});
426
+ if (stored.status !== 'active') {
427
+ patch['status'] = stored.status;
428
+ if (stored.status !== 'paused') patch['nextDueAtMs'] = null;
429
+ }
430
+ transaction.update(enrollmentRef, patch);
431
+ if (input.sent && (box == null ? void 0 : box.exists)) {
432
+ const mailbox = readStoredOutreachMailbox(run.mailbox.id, box.data());
433
+ if (mailbox) {
434
+ var _ref;
435
+ var _mailbox_health_daily, _mailbox_health, _mailbox_health1;
436
+ const day = outreachLocalDay(nowMs, mailbox.timezone);
437
+ const counts = (_ref = (_mailbox_health = mailbox.health) == null ? void 0 : (_mailbox_health_daily = _mailbox_health.daily) == null ? void 0 : _mailbox_health_daily[day]) != null ? _ref : {
438
+ sent: 0,
439
+ bounces: 0,
440
+ replies: 0
441
+ };
442
+ transaction.update(boxRef, {
443
+ 'health.lastSentAtMs': nowMs,
444
+ 'health.lastErrorAtMs': null,
445
+ 'health.lastErrorCode': null,
446
+ [`health.daily.${day}`]: _extends({}, counts, {
447
+ sent: (Number(counts.sent) || 0) + 1
448
+ }),
449
+ 'health.recentSends': withRecentSend((_mailbox_health1 = mailbox.health) == null ? void 0 : _mailbox_health1.recentSends, {
450
+ id: outreachSendDigest(input.sent.messageId),
451
+ atMs: nowMs,
452
+ bounced: false
453
+ }),
454
+ updatedAtMs: nowMs
455
+ });
456
+ }
457
+ }
458
+ return true;
459
+ });
460
+ }
461
+ /** One task step: the rep's task on the contact, then the step recorded. */ async function runTaskStep(deps, firestore, run, candidate) {
462
+ const enrollment = candidate.enrollment;
463
+ const sequence = candidate.sequence;
464
+ const step = sequence.steps[enrollment.stepIndex];
465
+ if (await claimStep(deps, firestore, run, enrollment, null) !== 'claimed') return false;
466
+ const nowMs = deps.now();
467
+ let taskId;
468
+ try {
469
+ var _OUTREACH_TASK_KIND_TO_RECORD_step_taskKind;
470
+ const written = await fileOutreachTask(deps, {
471
+ orgId: run.orgId,
472
+ hostId: enrollment.hostId,
473
+ contactId: enrollment.contactId,
474
+ dedupeKey: `step:${enrollment.id}:${step.id}`,
475
+ title: step.title,
476
+ notes: `From sequence step ${enrollment.stepIndex + 1}.`,
477
+ kind: (_OUTREACH_TASK_KIND_TO_RECORD_step_taskKind = OUTREACH_TASK_KIND_TO_RECORD[step.taskKind]) != null ? _OUTREACH_TASK_KIND_TO_RECORD_step_taskKind : 'todo',
478
+ dueAtMs: nowMs,
479
+ assigneeUid: run.mailbox.connectedByUid
480
+ });
481
+ taskId = (written == null ? void 0 : written.ok) ? written.id : null;
482
+ if ((written == null ? void 0 : written.ok) === false) console.warn(`[outreach] a task step's task was not filed: ${written.error}`);
483
+ } catch (error) {
484
+ // The record system failed rather than refused: the step waits for a
485
+ // run that can file its task.
486
+ console.error('[outreach] filing a task step failed; the step waits', error);
487
+ await releaseClaim(deps, firestore, run, enrollment, false);
488
+ return false;
489
+ }
490
+ return completeStep(deps, firestore, run, {
491
+ enrollment,
492
+ sequence,
493
+ record: {
494
+ stepIndex: enrollment.stepIndex,
495
+ stepId: step.id,
496
+ kind: 'task',
497
+ atMs: nowMs,
498
+ taskId
499
+ },
500
+ sent: null,
501
+ claimMessageId: null
502
+ });
503
+ }
504
+ /** A CRM template's body, read once per run. */ async function templateBody(firestore, run, templateId) {
505
+ var _run_templates_get;
506
+ if (!run.templates.has(templateId)) {
507
+ const snapshot = await firestore.collection('orgs').doc(run.orgId).collection(CRM_COLLECTIONS.emailTemplates).doc(templateId).get();
508
+ run.templates.set(templateId, snapshot.exists ? normalizeCrmEmailTemplate(snapshot.data()).body : null);
509
+ }
510
+ return (_run_templates_get = run.templates.get(templateId)) != null ? _run_templates_get : null;
511
+ }
512
+ async function runEmailStep(deps, firestore, run, input) {
513
+ var _ref, _composed_email_inReplyTo;
514
+ const { enrollment, sequence, person } = input;
515
+ const { report, mailbox } = run;
516
+ const nowMs = deps.now();
517
+ const stop = async (event, counter)=>{
518
+ const outcome = await applyOutreachEvent(firestore, {
519
+ orgId: run.orgId,
520
+ enrollmentId: enrollment.id,
521
+ event,
522
+ nowMs
523
+ });
524
+ if (outcome.changed) report[counter] += 1;
525
+ };
526
+ // Every gate again, on the person as they stand now.
527
+ if (!(person == null ? void 0 : person.contact)) {
528
+ return stop({
529
+ type: 'stop',
530
+ atMs: nowMs,
531
+ byUid: null,
532
+ reason: 'gate',
533
+ detail: 'This contact no longer exists in the CRM.'
534
+ }, 'stopped');
535
+ }
536
+ if (!person.visible) {
537
+ return stop({
538
+ type: 'stop',
539
+ atMs: nowMs,
540
+ byUid: null,
541
+ reason: 'gate',
542
+ detail: "This contact is no longer in the sequence's site's CRM."
543
+ }, 'stopped');
544
+ }
545
+ if (uncheckable(input.lookups)) {
546
+ report.held += 1;
547
+ return;
548
+ }
549
+ const contactGroupId = consentGroupForHost(run.org, enrollment.hostId).groupId;
550
+ const gate = evaluateOutreachGates({
551
+ email: enrollment.email,
552
+ contact: person.contact,
553
+ contactGroupId,
554
+ company: person.company,
555
+ settings: _extends({}, sequence.settings, {
556
+ allowedCountries: effectiveOutreachAllowedCountries(sequence.settings.allowedCountries, run.settings.allowedCountries)
557
+ }),
558
+ personalLine: enrollment.personalLine,
559
+ attestations: enrollment.attestations,
560
+ enrollmentId: enrollment.id,
561
+ lookups: input.lookups
562
+ });
563
+ if (!gate.allowed) return stop(gateEvent(gate.blocks, nowMs), 'stopped');
564
+ // The email, with its way out.
565
+ const listUnsubscribeUrl = deps.unsubscribeUrl({
566
+ orgId: run.orgId,
567
+ enrollmentId: enrollment.id
568
+ });
569
+ const mailto = outreachUnsubscribeMailbox(mailbox.email);
570
+ if (!listUnsubscribeUrl || !mailto) {
571
+ // No link can be minted — no secret, or no HTTPS console origin — and an
572
+ // email without its one-click way out does not leave.
573
+ console.error('[outreach] no unsubscribe link could be minted; nothing sends until one can');
574
+ run.stopped = true;
575
+ report.held += 1;
576
+ return;
577
+ }
578
+ const step = sequence.steps[enrollment.stepIndex];
579
+ const sender = senderOf(mailbox);
580
+ const composed = composeOutreachEmail({
581
+ sequence,
582
+ enrollment,
583
+ orgSettings: run.settings,
584
+ merge: {
585
+ contact: person.contact,
586
+ contactGroupId,
587
+ sender: {
588
+ name: mailbox.displayName,
589
+ email: sender.address
590
+ },
591
+ site: {
592
+ name: input.siteName
593
+ }
594
+ },
595
+ templateBody: (step == null ? void 0 : step.kind) === 'email' && step.templateId ? await templateBody(firestore, run, step.templateId) : null,
596
+ listUnsubscribeUrl,
597
+ listUnsubscribeMailto: mailto.uri
598
+ });
599
+ if (composed.error || !composed.email) {
600
+ var _composed_error, _composed_error1;
601
+ const code = (_composed_error = composed.error) == null ? void 0 : _composed_error.code;
602
+ if (code === 'missing_postal_address' || code === 'missing_legal_name') {
603
+ // The organization's footer: nothing of theirs sends until it is set.
604
+ run.stopped = true;
605
+ report.held += 1;
606
+ return;
607
+ }
608
+ if (code === 'invalid_recipient' || code === 'missing_thread' || code === 'not_an_email_step') {
609
+ var _composed_error2;
610
+ return stop({
611
+ type: 'send_failed',
612
+ atMs: nowMs,
613
+ detail: (_composed_error2 = composed.error) == null ? void 0 : _composed_error2.message
614
+ }, 'failed');
615
+ }
616
+ // The sequence's own words: the rep fixes the step and resumes.
617
+ return stop({
618
+ type: 'pause',
619
+ atMs: nowMs,
620
+ byUid: null,
621
+ detail: (_composed_error1 = composed.error) == null ? void 0 : _composed_error1.message
622
+ }, 'failed');
623
+ }
624
+ if (composed.unresolvedFields.length) {
625
+ const fields = composed.unresolvedFields.map((field)=>`{{${field}}}`).join(', ');
626
+ return stop({
627
+ type: 'pause',
628
+ atMs: nowMs,
629
+ byUid: null,
630
+ detail: `Paused before sending: this contact has nothing for ${fields}. Fill it in and resume.`
631
+ }, 'failed');
632
+ }
633
+ // Claimed, then sent, then recorded.
634
+ const domain = sender.address.slice(sender.address.lastIndexOf('@') + 1);
635
+ const local = randomUUID();
636
+ const messageId = `<${local}@${domain}>`;
637
+ const claimed = await claimStep(deps, firestore, run, enrollment, messageId);
638
+ if (claimed !== 'claimed') {
639
+ if (claimed === 'cap') run.stopped = true;
640
+ report.held += 1;
641
+ return;
642
+ }
643
+ const client = await openClient(deps, firestore, run);
644
+ if (!client) {
645
+ await releaseClaim(deps, firestore, run, enrollment, true);
646
+ report.held += 1;
647
+ return;
648
+ }
649
+ let sent;
650
+ try {
651
+ sent = await sendComposedOutreachEmail(client, composed.email, sender, {
652
+ messageIdLocalPart: local,
653
+ messageIdDomain: domain
654
+ });
655
+ } catch (error) {
656
+ if (error instanceof Rfc5322MessageError || error instanceof GmailTransportError && error.code === 'invalid_request') {
657
+ await releaseClaim(deps, firestore, run, enrollment, true, {
658
+ type: 'send_failed',
659
+ atMs: deps.now(),
660
+ detail: `Gmail refused the email: ${error.message}`
661
+ });
662
+ report.failed += 1;
663
+ return;
664
+ }
665
+ await releaseClaim(deps, firestore, run, enrollment, true);
666
+ report.held += 1;
667
+ run.stopped = true;
668
+ const code = error instanceof GmailTransportError ? error.code : 'unexpected';
669
+ if (isReconnectRequired(error)) {
670
+ await markOutreachMailboxReconnectRequired(firestore, {
671
+ orgId: run.orgId,
672
+ mailboxId: mailbox.id,
673
+ errorCode: code,
674
+ nowMs: deps.now()
675
+ });
676
+ } else {
677
+ await mailboxRef(firestore, run.orgId, mailbox.id).update({
678
+ 'health.lastErrorAtMs': deps.now(),
679
+ 'health.lastErrorCode': code
680
+ }).catch(()=>undefined);
681
+ }
682
+ console.error(`[outreach] a send from a mailbox failed (${code}); its sends wait for the next run`);
683
+ return;
684
+ }
685
+ const record = {
686
+ stepIndex: enrollment.stepIndex,
687
+ stepId: (_ref = step == null ? void 0 : step.id) != null ? _ref : '',
688
+ kind: 'email',
689
+ atMs: deps.now(),
690
+ gmailMessageId: sent.gmailMessageId,
691
+ gmailThreadId: sent.threadId,
692
+ messageId: sent.messageId,
693
+ subject: sent.subject
694
+ };
695
+ await completeStep(deps, firestore, run, {
696
+ enrollment,
697
+ sequence,
698
+ record,
699
+ sent: {
700
+ messageId: sent.messageId,
701
+ threadId: sent.threadId,
702
+ subject: sent.subject
703
+ },
704
+ claimMessageId: messageId
705
+ });
706
+ report.sent += 1;
707
+ await fileOutreachEmail(deps, {
708
+ orgId: run.orgId,
709
+ hostId: enrollment.hostId,
710
+ contactId: enrollment.contactId,
711
+ direction: 'outbound',
712
+ subject: sent.subject,
713
+ from: sender.address,
714
+ to: composed.email.to,
715
+ messageId: sent.messageId,
716
+ inReplyTo: (_composed_email_inReplyTo = composed.email.inReplyTo) != null ? _composed_email_inReplyTo : null,
717
+ body: composed.email.text,
718
+ atMs: record.atMs,
719
+ byUid: mailbox.connectedByUid,
720
+ byName: mailbox.displayName || null
721
+ });
722
+ }
723
+ /**
724
+ * Settles a claim a run died holding. A task step's claim is dropped and
725
+ * the step runs again (its task is filed once however often it is asked).
726
+ * An email's claim looks for the message it minted in the mailbox: found,
727
+ * the send happened and is recorded; not found, it did not, and the claim
728
+ * and its reservation are handed back for the next run.
729
+ */ async function recoverClaim(deps, firestore, run, enrollment, sequence) {
730
+ var _ref, _ref1;
731
+ var _message_headers_find;
732
+ const claim = enrollment.sendClaim;
733
+ if (!claim) return false;
734
+ if (!claim.messageId) {
735
+ await releaseClaim(deps, firestore, run, enrollment, false);
736
+ return true;
737
+ }
738
+ const client = await openClient(deps, firestore, run);
739
+ if (!client) return false;
740
+ const bare = claim.messageId.replace(/^<|>$/g, '');
741
+ const found = await client.listMessages({
742
+ q: `rfc822msgid:${bare}`,
743
+ maxResults: 1,
744
+ includeSpamTrash: true
745
+ });
746
+ const hit = found.messages[0];
747
+ if (!hit) {
748
+ await releaseClaim(deps, firestore, run, enrollment, true);
749
+ return true;
750
+ }
751
+ const message = await client.getMessage(hit.id, {
752
+ metadataHeaders: [
753
+ 'Subject'
754
+ ]
755
+ });
756
+ const subject = (_ref = (_message_headers_find = message.headers.find((header)=>header.name.toLowerCase() === 'subject')) == null ? void 0 : _message_headers_find.value) != null ? _ref : '';
757
+ const step = sequence.steps[enrollment.stepIndex];
758
+ return completeStep(deps, firestore, run, {
759
+ enrollment,
760
+ sequence,
761
+ record: {
762
+ stepIndex: enrollment.stepIndex,
763
+ stepId: (_ref1 = step == null ? void 0 : step.id) != null ? _ref1 : '',
764
+ kind: 'email',
765
+ atMs: deps.now(),
766
+ gmailMessageId: hit.id,
767
+ gmailThreadId: hit.threadId,
768
+ messageId: claim.messageId,
769
+ subject
770
+ },
771
+ sent: {
772
+ messageId: claim.messageId,
773
+ threadId: hit.threadId,
774
+ subject
775
+ },
776
+ claimMessageId: claim.messageId
777
+ });
778
+ }
779
+
780
+ //# sourceMappingURL=send-job.js.map