@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 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/thread-classification.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 CAME BACK (AGL-2979).\n *\n * The runtime reads each enrollment's threads and asks what the new\n * messages mean. Each message is one of:\n *\n * - `self` — the mailbox's own: a step, or the rep writing in the thread.\n * - `hard_bounce` / `soft_bounce` — a delivery report (`./delivery-status.ts`).\n * - `delivery_report` — a mail system's message that reports no failure.\n * - `auto_reply` — an out-of-office or an autoresponder. It POSTPONES the\n * next step and does not stop the sequence: nobody answered.\n * - `opt_out` — a reply that asks to be left alone (`./opt-out-intent.ts`),\n * or a message sent to the unsubscribe address.\n * - `reply` — any other message from anybody else.\n *\n * ## Automatic replies\n *\n * Read from the headers the standards give an autoresponder —\n * `Auto-Submitted` with any value but `no` (RFC 3834), `X-Autoreply`,\n * `X-Autorespond`, `Precedence: auto_reply`, `bulk` or `junk` — and, for\n * the servers that set none, from an out-of-office subject in the usual\n * languages.\n *\n * ## The thread's outcome\n *\n * `opted_out` outranks `replied`, which outranks `bounced`, which outranks\n * `postpone`: an opt-out is a reply that also has to reach the do-not-contact\n * list, a person who wrote back matters more than a report about an earlier\n * send, and an out-of-office only matters when nothing else happened. The\n * bounces that report this recipient are counted either way, because mailbox\n * health counts every one.\n *==========================================*/\n\nimport { normalizeContactEmail } from '@aglyn/aglyn/app-utils/contacts'\nimport { crmThreadSubject, emailAddressOf, htmlToPlainText } from '@aglyn/aglyn/app-utils/crm-inbound'\nimport { readOutreachDeliveryReport, isMailerDaemonMessage } from './delivery-status'\nimport { detectOutreachOptOutIntent, detectOutreachOptOutSubject } from './opt-out-intent'\nimport { outreachHeader, type OutreachThreadMessage } from './thread-message'\n\nexport type OutreachMessageKind =\n | 'self'\n | 'hard_bounce'\n | 'soft_bounce'\n | 'delivery_report'\n | 'auto_reply'\n | 'opt_out'\n | 'reply'\n\nexport interface OutreachBounceDetail {\n /** The addresses the report says failed; empty when it names none. */\n recipients: string[]\n status: string | null\n diagnostic: string | null\n}\n\nexport interface OutreachMessageClassification {\n messageId: string\n atMs: number\n kind: OutreachMessageKind\n /** The sender's address, normalized, when the `From` carried one. */\n from: string | null\n /** Set on a bounce. */\n bounce: OutreachBounceDetail | null\n /** An opt-out that calls the email spam. */\n complaint: boolean\n /** What decided it, for the timeline and the log. */\n evidence: string | null\n}\n\nexport interface OutreachMessageContext {\n /** The mailbox's own addresses: the account's and every alias it sends as. */\n selfAddresses: readonly string[]\n /** The address a `List-Unsubscribe` mailto points at, when the runtime set one. */\n unsubscribeAddress?: string | null\n /** The subject the thread was started with, so a new subject can be read on its own. */\n threadSubject?: string | null\n}\n\nconst AUTO_REPLY_SUBJECT = new RegExp(\n [\n '^\\\\s*\\\\[?\\\\s*(',\n 'auto(matic)?[\\\\s-]*(reply|response|responder|answer)',\n '|out[\\\\s-]+of[\\\\s-]+(the[\\\\s-]+)?office',\n '|ooo\\\\b',\n \"|i('m| am) (currently )?(out of the office|away|on (vacation|leave|holiday))\",\n '|(on )?(vacation|holiday|annual leave)[\\\\s-]*(reply|response|notice|message|autoreply)',\n '|away from (the |my )?(office|desk)',\n '|abwesenheitsnotiz|abwesenheit',\n '|r[ée]ponse automatique|absence du bureau',\n '|respuesta autom[áa]tica|fuera de la oficina',\n '|risposta automatica|fuori ufficio',\n '|automatisch antwoord|afwezigheidsbericht',\n '|resposta autom[áa]tica|fora do escrit[óo]rio',\n '|autosvar|automaattinen vastaus',\n ')',\n ].join(''),\n 'i',\n)\n\n/**\n * Why a message is an automatic reply, or `null` when it is not one — see\n * the module note for the headers and subjects read.\n */\nexport function outreachAutoReplyEvidence(\n message: Pick<OutreachThreadMessage, 'headers' | 'subject'>,\n): string | null {\n const autoSubmitted = outreachHeader(message, 'Auto-Submitted').split(';')[0].trim().toLowerCase()\n if (autoSubmitted && autoSubmitted !== 'no') return `Auto-Submitted: ${autoSubmitted}`\n for (const name of ['X-Autoreply', 'X-Autorespond']) {\n const value = outreachHeader(message, name).trim()\n if (value && value.toLowerCase() !== 'no') return `${name}: ${value}`\n }\n const precedence = outreachHeader(message, 'Precedence').trim().toLowerCase()\n if (['auto_reply', 'bulk', 'junk'].includes(precedence)) return `Precedence: ${precedence}`\n const subject = String(message?.subject ?? '').replace(/^\\s*(re|fwd?)\\s*:\\s*/i, '')\n const match = AUTO_REPLY_SUBJECT.exec(subject)\n return match ? `Subject: ${match[0].trim()}` : null\n}\n\nfunction addressesIn(header: string): string[] {\n return header\n .split(',')\n .map((part) => emailAddressOf(part))\n .filter((address): address is string => address !== null)\n}\n\nfunction readableText(message: OutreachThreadMessage): string {\n if (message.textBody && message.textBody.trim()) return message.textBody\n if (message.htmlBody && message.htmlBody.trim()) return htmlToPlainText(message.htmlBody)\n return message.snippet ?? ''\n}\n\n/** What one message in an enrollment's thread is — see the module note. */\nexport function classifyOutreachMessage(\n message: OutreachThreadMessage,\n context: OutreachMessageContext,\n): OutreachMessageClassification {\n const from = emailAddressOf(message.from)\n const base = {\n messageId: String(message.id ?? ''),\n atMs: Number.isFinite(message.internalDateMs) ? message.internalDateMs : 0,\n from,\n bounce: null,\n complaint: false,\n evidence: null,\n }\n\n const report = readOutreachDeliveryReport(message)\n if (report) {\n if (!report.kind) return { ...base, kind: 'delivery_report', evidence: 'delivery report' }\n return {\n ...base,\n kind: report.kind === 'hard' ? 'hard_bounce' : 'soft_bounce',\n bounce: {\n recipients: report.failedAddresses,\n status: report.status,\n diagnostic: report.diagnostic,\n },\n evidence: report.status ? `Status: ${report.status}` : 'delivery report',\n }\n }\n\n const self = new Set(\n (context.selfAddresses ?? [])\n .map((address) => normalizeContactEmail(address))\n .filter((address): address is string => address !== null),\n )\n if (from && self.has(from)) return { ...base, kind: 'self' }\n if (isMailerDaemonMessage(message)) {\n return { ...base, kind: 'delivery_report', evidence: 'mail system message' }\n }\n\n const unsubscribe = normalizeContactEmail(context.unsubscribeAddress)\n if (unsubscribe && addressesIn(message.to ?? '').includes(unsubscribe)) {\n return { ...base, kind: 'opt_out', evidence: `To: ${unsubscribe}` }\n }\n\n const autoReply = outreachAutoReplyEvidence(message)\n if (autoReply) return { ...base, kind: 'auto_reply', evidence: autoReply }\n\n const intent = detectOutreachOptOutIntent(readableText(message))\n if (intent.optOut) {\n return { ...base, kind: 'opt_out', complaint: intent.complaint, evidence: intent.matched }\n }\n // A subject is read only on a message that starts fresh — no `Re:`, and\n // not the thread's own — and only when it says nothing but stop.\n const subject = String(message.subject ?? '').replace(/\\s+/g, ' ').trim()\n const fresh = crmThreadSubject(subject) === subject\n if (subject && fresh && subject !== crmThreadSubject(context.threadSubject)) {\n const subjectIntent = detectOutreachOptOutSubject(subject)\n if (subjectIntent.optOut) {\n return { ...base, kind: 'opt_out', evidence: `Subject: ${subjectIntent.matched}` }\n }\n }\n return { ...base, kind: 'reply' }\n}\n\nexport type OutreachThreadOutcome = 'none' | 'postpone' | 'bounced' | 'replied' | 'opted_out'\n\nexport interface OutreachThreadDecisionInput extends OutreachMessageContext {\n /** The thread's messages, in any order. */\n messages: readonly OutreachThreadMessage[]\n /** The enrollment's recipient. */\n recipient: string\n /** Messages received at or before this instant were handled by an earlier run. */\n afterMs?: number | null\n /** Ids of messages an earlier run handled. */\n handledMessageIds?: readonly string[]\n}\n\nexport interface OutreachThreadDecision {\n outcome: OutreachThreadOutcome\n /** The message that decided the outcome; `null` for `none`. */\n decidedBy: OutreachMessageClassification | null\n /** Every new message, classified, oldest first. */\n classifications: OutreachMessageClassification[]\n /** A new reply called the email spam: the mailbox is paused as well. */\n complaint: boolean\n /** New hard bounces that report this recipient. */\n hardBounces: number\n /** New soft bounces that report this recipient. */\n softBounces: number\n}\n\nconst RANK: Record<OutreachThreadOutcome, number> = {\n none: 0,\n postpone: 1,\n bounced: 2,\n replied: 3,\n opted_out: 4,\n}\n\nconst OUTCOME_OF: Partial<Record<OutreachMessageKind, OutreachThreadOutcome>> = {\n auto_reply: 'postpone',\n hard_bounce: 'bounced',\n reply: 'replied',\n opt_out: 'opted_out',\n}\n\n/** What the new messages in an enrollment's thread mean for it — see the module note. */\nexport function decideOutreachThread(input: OutreachThreadDecisionInput): OutreachThreadDecision {\n const recipient = normalizeContactEmail(input.recipient)\n const handled = new Set(input.handledMessageIds ?? [])\n const afterMs = typeof input.afterMs === 'number' ? input.afterMs : null\n const fresh = [...(input.messages ?? [])]\n .filter((message) => message && !handled.has(message.id))\n .filter((message) => afterMs === null || message.internalDateMs > afterMs)\n .sort((a, b) => a.internalDateMs - b.internalDateMs)\n\n const decision: OutreachThreadDecision = {\n outcome: 'none',\n decidedBy: null,\n classifications: [],\n complaint: false,\n hardBounces: 0,\n softBounces: 0,\n }\n for (const message of fresh) {\n const classification = classifyOutreachMessage(message, input)\n decision.classifications.push(classification)\n const bounce = classification.bounce\n const aboutRecipient =\n !bounce || !bounce.recipients.length || (recipient !== null && bounce.recipients.includes(recipient))\n if (classification.kind === 'hard_bounce' && aboutRecipient) decision.hardBounces += 1\n if (classification.kind === 'soft_bounce' && aboutRecipient) decision.softBounces += 1\n if (classification.complaint) decision.complaint = true\n const outcome = OUTCOME_OF[classification.kind]\n if (!outcome || (classification.kind === 'hard_bounce' && !aboutRecipient)) continue\n if (RANK[outcome] > RANK[decision.outcome]) {\n decision.outcome = outcome\n decision.decidedBy = classification\n }\n }\n return decision\n}\n"],"names":["normalizeContactEmail","crmThreadSubject","emailAddressOf","htmlToPlainText","readOutreachDeliveryReport","isMailerDaemonMessage","detectOutreachOptOutIntent","detectOutreachOptOutSubject","outreachHeader","AUTO_REPLY_SUBJECT","RegExp","join","outreachAutoReplyEvidence","message","autoSubmitted","split","trim","toLowerCase","name","value","precedence","includes","subject","String","replace","match","exec","addressesIn","header","map","part","filter","address","readableText","textBody","htmlBody","snippet","classifyOutreachMessage","context","from","base","messageId","id","atMs","Number","isFinite","internalDateMs","bounce","complaint","evidence","report","kind","recipients","failedAddresses","status","diagnostic","self","Set","selfAddresses","has","unsubscribe","unsubscribeAddress","to","autoReply","intent","optOut","matched","fresh","threadSubject","subjectIntent","RANK","none","postpone","bounced","replied","opted_out","OUTCOME_OF","auto_reply","hard_bounce","reply","opt_out","decideOutreachThread","input","recipient","handled","handledMessageIds","afterMs","messages","sort","a","b","decision","outcome","decidedBy","classifications","hardBounces","softBounces","classification","push","aboutRecipient","length"],"mappings":"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"}
@@ -0,0 +1,80 @@
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
+ /** One MIME part: its media type, and its body as text when it is textual. */
18
+ export interface OutreachThreadMessagePart {
19
+ /** Lowercase media type without parameters: `text/plain`, `message/delivery-status`. */
20
+ mimeType: string;
21
+ /** The decoded body, for a textual part whose body was delivered inline. */
22
+ text?: string;
23
+ }
24
+ export interface OutreachThreadMessage {
25
+ /** The provider's message id. */
26
+ id: string;
27
+ /** The provider's thread id. */
28
+ threadId?: string;
29
+ /** When the provider received the message, epoch ms — Gmail's `internalDate`. */
30
+ internalDateMs: number;
31
+ /** The raw `From` header value. */
32
+ from: string;
33
+ /** The raw `To` header value. */
34
+ to: string;
35
+ subject: string;
36
+ /** The top-level headers by name. Names are matched without regard to case. */
37
+ headers: Record<string, string>;
38
+ /** The provider's plain-text preview. */
39
+ snippet?: string;
40
+ labelIds?: string[];
41
+ /** The first `text/plain` body that is not an attachment, decoded. */
42
+ textBody?: string;
43
+ /** The first `text/html` body, decoded — read when there is no text body. */
44
+ htmlBody?: string;
45
+ /** Every part, depth first. */
46
+ parts?: OutreachThreadMessagePart[];
47
+ }
48
+ /** A header's value by name, matched without regard to case; `''` when absent. */
49
+ export declare function outreachHeader(message: Pick<OutreachThreadMessage, 'headers'>, name: string): string;
50
+ export interface GmailApiHeader {
51
+ name: string;
52
+ value: string;
53
+ }
54
+ export interface GmailApiMessagePart {
55
+ partId?: string;
56
+ mimeType?: string;
57
+ filename?: string;
58
+ headers?: GmailApiHeader[];
59
+ body?: {
60
+ size?: number;
61
+ data?: string;
62
+ attachmentId?: string;
63
+ };
64
+ parts?: GmailApiMessagePart[];
65
+ }
66
+ /** `users.messages.get` or a message of `users.threads.get`, with `format=full`. */
67
+ export interface GmailApiMessage {
68
+ id: string;
69
+ threadId?: string;
70
+ labelIds?: string[];
71
+ snippet?: string;
72
+ /** Epoch milliseconds, as a string. */
73
+ internalDate?: string | number;
74
+ payload?: GmailApiMessagePart;
75
+ }
76
+ /**
77
+ * The classifier's shape for a Gmail API message. Never throws: a body that
78
+ * does not decode is left out, and a message with no payload has no text.
79
+ */
80
+ export declare function outreachThreadMessageFromGmail(message: GmailApiMessage): OutreachThreadMessage;
@@ -0,0 +1,143 @@
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
+ * A MESSAGE IN A THREAD, AS THE CLASSIFIER READS IT (AGL-2979).
18
+ *
19
+ * The runtime reads an enrollment's thread from the provider and hands each
20
+ * message over in this shape: the headers, the text, and — for a delivery
21
+ * report — the parts, because a bounce's recipient and status code live in
22
+ * its `message/delivery-status` part and not in any header.
23
+ *
24
+ * {@link outreachThreadMessageFromGmail} builds it from a Gmail API message
25
+ * resource fetched with `format=full`, so the base64url bodies, the nested
26
+ * multiparts and the character sets are decoded in one place.
27
+ *==========================================*/ /** One MIME part: its media type, and its body as text when it is textual. */ import { _ as _extends } from "@swc/helpers/_/_extends";
28
+ /** A header's value by name, matched without regard to case; `''` when absent. */ export function outreachHeader(message, name) {
29
+ var _ref;
30
+ const wanted = name.toLowerCase();
31
+ for (const [key, value] of Object.entries((_ref = message == null ? void 0 : message.headers) != null ? _ref : {})){
32
+ if (key.toLowerCase() === wanted) return String(value != null ? value : '');
33
+ }
34
+ return '';
35
+ }
36
+ const TEXTUAL = /^(text\/|message\/(delivery-status|disposition-notification|global-delivery-status|global-headers|rfc822-headers)$)/;
37
+ function headerOf(headers, name) {
38
+ var _ref;
39
+ var _headers_find;
40
+ const wanted = name.toLowerCase();
41
+ return (_ref = headers == null ? void 0 : (_headers_find = headers.find((header)=>{
42
+ var _header_name;
43
+ return (header == null ? void 0 : (_header_name = header.name) == null ? void 0 : _header_name.toLowerCase()) === wanted;
44
+ })) == null ? void 0 : _headers_find.value) != null ? _ref : '';
45
+ }
46
+ function charsetOf(contentType) {
47
+ const match = /charset\s*=\s*"?([^";\s]+)"?/i.exec(contentType);
48
+ return match ? match[1].toLowerCase() : 'utf-8';
49
+ }
50
+ /** A base64url body decoded to text in the part's own character set. */ function decodeBody(data, charset) {
51
+ const base64 = data.replace(/-/g, '+').replace(/_/g, '/').replace(/\s+/g, '');
52
+ const padded = base64 + '='.repeat((4 - base64.length % 4) % 4);
53
+ const binary = atob(padded);
54
+ const bytes = new Uint8Array(binary.length);
55
+ for(let index = 0; index < binary.length; index += 1)bytes[index] = binary.charCodeAt(index);
56
+ try {
57
+ return new TextDecoder(charset).decode(bytes);
58
+ } catch (unused) {
59
+ return new TextDecoder('utf-8').decode(bytes);
60
+ }
61
+ }
62
+ const ENTITIES = {
63
+ amp: '&',
64
+ lt: '<',
65
+ gt: '>',
66
+ quot: '"',
67
+ apos: "'",
68
+ nbsp: ' '
69
+ };
70
+ /** Gmail's snippet arrives HTML-escaped: `wasn&#39;t`. */ function unescapeSnippet(snippet) {
71
+ return snippet.replace(/&(#\d+|#x[0-9a-f]+|[a-z]+);/gi, (whole, code)=>{
72
+ var _ENTITIES_code_toLowerCase;
73
+ if (/^#x/i.test(code)) return String.fromCodePoint(parseInt(code.slice(2), 16));
74
+ if (code.startsWith('#')) return String.fromCodePoint(parseInt(code.slice(1), 10));
75
+ return (_ENTITIES_code_toLowerCase = ENTITIES[code.toLowerCase()]) != null ? _ENTITIES_code_toLowerCase : whole;
76
+ });
77
+ }
78
+ /**
79
+ * The classifier's shape for a Gmail API message. Never throws: a body that
80
+ * does not decode is left out, and a message with no payload has no text.
81
+ */ export function outreachThreadMessageFromGmail(message) {
82
+ var _ref, _payload_headers, _ref1;
83
+ const payload = (_ref = message == null ? void 0 : message.payload) != null ? _ref : {};
84
+ const headers = {};
85
+ for (const header of (_payload_headers = payload.headers) != null ? _payload_headers : []){
86
+ var _header_value;
87
+ if ((header == null ? void 0 : header.name) && !(header.name in headers)) headers[header.name] = String((_header_value = header.value) != null ? _header_value : '');
88
+ }
89
+ const parts = [];
90
+ let textBody;
91
+ let htmlBody;
92
+ const walk = (part)=>{
93
+ var _part_mimeType, _part_parts;
94
+ var _part_body;
95
+ if (!part) return;
96
+ const mimeType = String((_part_mimeType = part.mimeType) != null ? _part_mimeType : '').toLowerCase().split(';')[0].trim();
97
+ const entry = {
98
+ mimeType
99
+ };
100
+ const data = (_part_body = part.body) == null ? void 0 : _part_body.data;
101
+ if (data && TEXTUAL.test(mimeType)) {
102
+ try {
103
+ entry.text = decodeBody(data, charsetOf(headerOf(part.headers, 'Content-Type')));
104
+ } catch (unused) {
105
+ // A body that is not valid base64 is treated as absent.
106
+ }
107
+ }
108
+ parts.push(entry);
109
+ const attachment = Boolean(part.filename) || /^attachment/i.test(headerOf(part.headers, 'Content-Disposition'));
110
+ if (entry.text !== undefined && !attachment) {
111
+ if (mimeType === 'text/plain' && textBody === undefined) textBody = entry.text;
112
+ if (mimeType === 'text/html' && htmlBody === undefined) htmlBody = entry.text;
113
+ }
114
+ for (const child of (_part_parts = part.parts) != null ? _part_parts : [])walk(child);
115
+ };
116
+ walk(message == null ? void 0 : message.payload);
117
+ const internalDateMs = Number(message == null ? void 0 : message.internalDate);
118
+ return _extends({
119
+ id: String((_ref1 = message == null ? void 0 : message.id) != null ? _ref1 : '')
120
+ }, (message == null ? void 0 : message.threadId) ? {
121
+ threadId: message.threadId
122
+ } : {}, {
123
+ internalDateMs: Number.isFinite(internalDateMs) ? internalDateMs : 0,
124
+ from: headerOf(payload.headers, 'From'),
125
+ to: headerOf(payload.headers, 'To'),
126
+ subject: headerOf(payload.headers, 'Subject'),
127
+ headers
128
+ }, (message == null ? void 0 : message.snippet) ? {
129
+ snippet: unescapeSnippet(message.snippet)
130
+ } : {}, (message == null ? void 0 : message.labelIds) ? {
131
+ labelIds: [
132
+ ...message.labelIds
133
+ ]
134
+ } : {}, textBody !== undefined ? {
135
+ textBody
136
+ } : {}, htmlBody !== undefined ? {
137
+ htmlBody
138
+ } : {}, {
139
+ parts
140
+ });
141
+ }
142
+
143
+ //# sourceMappingURL=thread-message.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/thread-message.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * A MESSAGE IN A THREAD, AS THE CLASSIFIER READS IT (AGL-2979).\n *\n * The runtime reads an enrollment's thread from the provider and hands each\n * message over in this shape: the headers, the text, and — for a delivery\n * report — the parts, because a bounce's recipient and status code live in\n * its `message/delivery-status` part and not in any header.\n *\n * {@link outreachThreadMessageFromGmail} builds it from a Gmail API message\n * resource fetched with `format=full`, so the base64url bodies, the nested\n * multiparts and the character sets are decoded in one place.\n *==========================================*/\n\n/** One MIME part: its media type, and its body as text when it is textual. */\nexport interface OutreachThreadMessagePart {\n /** Lowercase media type without parameters: `text/plain`, `message/delivery-status`. */\n mimeType: string\n /** The decoded body, for a textual part whose body was delivered inline. */\n text?: string\n}\n\nexport interface OutreachThreadMessage {\n /** The provider's message id. */\n id: string\n /** The provider's thread id. */\n threadId?: string\n /** When the provider received the message, epoch ms — Gmail's `internalDate`. */\n internalDateMs: number\n /** The raw `From` header value. */\n from: string\n /** The raw `To` header value. */\n to: string\n subject: string\n /** The top-level headers by name. Names are matched without regard to case. */\n headers: Record<string, string>\n /** The provider's plain-text preview. */\n snippet?: string\n labelIds?: string[]\n /** The first `text/plain` body that is not an attachment, decoded. */\n textBody?: string\n /** The first `text/html` body, decoded — read when there is no text body. */\n htmlBody?: string\n /** Every part, depth first. */\n parts?: OutreachThreadMessagePart[]\n}\n\n/** A header's value by name, matched without regard to case; `''` when absent. */\nexport function outreachHeader(\n message: Pick<OutreachThreadMessage, 'headers'>,\n name: string,\n): string {\n const wanted = name.toLowerCase()\n for (const [key, value] of Object.entries(message?.headers ?? {})) {\n if (key.toLowerCase() === wanted) return String(value ?? '')\n }\n return ''\n}\n\n/*==========================================\n * THE GMAIL API MESSAGE RESOURCE.\n *==========================================*/\n\nexport interface GmailApiHeader {\n name: string\n value: string\n}\n\nexport interface GmailApiMessagePart {\n partId?: string\n mimeType?: string\n filename?: string\n headers?: GmailApiHeader[]\n body?: { size?: number; data?: string; attachmentId?: string }\n parts?: GmailApiMessagePart[]\n}\n\n/** `users.messages.get` or a message of `users.threads.get`, with `format=full`. */\nexport interface GmailApiMessage {\n id: string\n threadId?: string\n labelIds?: string[]\n snippet?: string\n /** Epoch milliseconds, as a string. */\n internalDate?: string | number\n payload?: GmailApiMessagePart\n}\n\nconst TEXTUAL = /^(text\\/|message\\/(delivery-status|disposition-notification|global-delivery-status|global-headers|rfc822-headers)$)/\n\nfunction headerOf(headers: readonly GmailApiHeader[] | undefined, name: string): string {\n const wanted = name.toLowerCase()\n return headers?.find((header) => header?.name?.toLowerCase() === wanted)?.value ?? ''\n}\n\nfunction charsetOf(contentType: string): string {\n const match = /charset\\s*=\\s*\"?([^\";\\s]+)\"?/i.exec(contentType)\n return match ? match[1].toLowerCase() : 'utf-8'\n}\n\n/** A base64url body decoded to text in the part's own character set. */\nfunction decodeBody(data: string, charset: string): string {\n const base64 = data.replace(/-/g, '+').replace(/_/g, '/').replace(/\\s+/g, '')\n const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4)\n const binary = atob(padded)\n const bytes = new Uint8Array(binary.length)\n for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index)\n try {\n return new TextDecoder(charset).decode(bytes)\n } catch {\n return new TextDecoder('utf-8').decode(bytes)\n }\n}\n\nconst ENTITIES: Record<string, string> = { amp: '&', lt: '<', gt: '>', quot: '\"', apos: \"'\", nbsp: ' ' }\n\n/** Gmail's snippet arrives HTML-escaped: `wasn&#39;t`. */\nfunction unescapeSnippet(snippet: string): string {\n return snippet.replace(/&(#\\d+|#x[0-9a-f]+|[a-z]+);/gi, (whole, code: string) => {\n if (/^#x/i.test(code)) return String.fromCodePoint(parseInt(code.slice(2), 16))\n if (code.startsWith('#')) return String.fromCodePoint(parseInt(code.slice(1), 10))\n return ENTITIES[code.toLowerCase()] ?? whole\n })\n}\n\n/**\n * The classifier's shape for a Gmail API message. Never throws: a body that\n * does not decode is left out, and a message with no payload has no text.\n */\nexport function outreachThreadMessageFromGmail(message: GmailApiMessage): OutreachThreadMessage {\n const payload = message?.payload ?? {}\n const headers: Record<string, string> = {}\n for (const header of payload.headers ?? []) {\n if (header?.name && !(header.name in headers)) headers[header.name] = String(header.value ?? '')\n }\n const parts: OutreachThreadMessagePart[] = []\n let textBody: string | undefined\n let htmlBody: string | undefined\n const walk = (part: GmailApiMessagePart | undefined) => {\n if (!part) return\n const mimeType = String(part.mimeType ?? '').toLowerCase().split(';')[0].trim()\n const entry: OutreachThreadMessagePart = { mimeType }\n const data = part.body?.data\n if (data && TEXTUAL.test(mimeType)) {\n try {\n entry.text = decodeBody(data, charsetOf(headerOf(part.headers, 'Content-Type')))\n } catch {\n // A body that is not valid base64 is treated as absent.\n }\n }\n parts.push(entry)\n const attachment = Boolean(part.filename) || /^attachment/i.test(headerOf(part.headers, 'Content-Disposition'))\n if (entry.text !== undefined && !attachment) {\n if (mimeType === 'text/plain' && textBody === undefined) textBody = entry.text\n if (mimeType === 'text/html' && htmlBody === undefined) htmlBody = entry.text\n }\n for (const child of part.parts ?? []) walk(child)\n }\n walk(message?.payload)\n const internalDateMs = Number(message?.internalDate)\n return {\n id: String(message?.id ?? ''),\n ...(message?.threadId ? { threadId: message.threadId } : {}),\n internalDateMs: Number.isFinite(internalDateMs) ? internalDateMs : 0,\n from: headerOf(payload.headers, 'From'),\n to: headerOf(payload.headers, 'To'),\n subject: headerOf(payload.headers, 'Subject'),\n headers,\n ...(message?.snippet ? { snippet: unescapeSnippet(message.snippet) } : {}),\n ...(message?.labelIds ? { labelIds: [...message.labelIds] } : {}),\n ...(textBody !== undefined ? { textBody } : {}),\n ...(htmlBody !== undefined ? { htmlBody } : {}),\n parts,\n }\n}\n"],"names":["outreachHeader","message","name","wanted","toLowerCase","key","value","Object","entries","headers","String","TEXTUAL","headerOf","find","header","charsetOf","contentType","match","exec","decodeBody","data","charset","base64","replace","padded","repeat","length","binary","atob","bytes","Uint8Array","index","charCodeAt","TextDecoder","decode","ENTITIES","amp","lt","gt","quot","apos","nbsp","unescapeSnippet","snippet","whole","code","test","fromCodePoint","parseInt","slice","startsWith","outreachThreadMessageFromGmail","payload","parts","textBody","htmlBody","walk","part","mimeType","split","trim","entry","body","text","push","attachment","Boolean","filename","undefined","child","internalDateMs","Number","internalDate","id","threadId","isFinite","from","to","subject","labelIds"],"mappings":"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"}
@@ -0,0 +1,81 @@
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 OutreachGateLookups } from '../engine/gates';
18
+ import { type OutreachAttestations, type OutreachSequenceSettings } from '../model/outreach.types';
19
+ import type { OutreachEnrollBlock, OutreachEnrollPreviewPerson } from '../model/outreach-api';
20
+ type Firestore = FirebaseFirestore.Firestore;
21
+ /** One person as the preview and the enroll read them. */
22
+ export interface OutreachEnrollCandidate {
23
+ contactId: string;
24
+ /** The contact document, or `null` when it no longer exists. */
25
+ contact: Record<string, unknown> | null;
26
+ /** Whether the sequence's site may see the contact. */
27
+ visible: boolean;
28
+ /** The name the sequence's site knows them by; `''` for none. */
29
+ name: string;
30
+ /** The primary address, normalized, or `null` when there is none. */
31
+ email: string | null;
32
+ /** The company the contact is filed under at that site, when there is one. */
33
+ company: Record<string, unknown> | null;
34
+ }
35
+ /**
36
+ * The contacts, as the sequence's site knows them, and the company each is
37
+ * filed under there — two batched reads for everyone.
38
+ */
39
+ export declare function readOutreachEnrollCandidates(firestore: Firestore, input: {
40
+ orgId: string;
41
+ hostId: string;
42
+ contactGroupId: string;
43
+ contactIds: readonly string[];
44
+ }): Promise<OutreachEnrollCandidate[]>;
45
+ /** What stands in the way before the gates are asked anything. */
46
+ export declare function outreachCandidateBlocks(candidate: OutreachEnrollCandidate, facts: {
47
+ siteName: string;
48
+ alreadyInSequence: boolean;
49
+ }): OutreachEnrollBlock[];
50
+ export interface OutreachGateQuestion {
51
+ candidate: OutreachEnrollCandidate;
52
+ lookups: OutreachGateLookups;
53
+ /** The sequence's settings, its countries already capped by the organization's. */
54
+ settings: OutreachSequenceSettings;
55
+ contactGroupId: string;
56
+ }
57
+ /** Where one person stands before the rep confirms anything — see the module note. */
58
+ export declare function previewOutreachPerson(question: OutreachGateQuestion, facts: {
59
+ siteName: string;
60
+ alreadyInSequence: boolean;
61
+ }): OutreachEnrollPreviewPerson;
62
+ export interface OutreachEnrollDecision {
63
+ allowed: boolean;
64
+ blocks: OutreachEnrollBlock[];
65
+ cold: boolean;
66
+ /** The address as the gates normalized it. */
67
+ email: string | null;
68
+ /** What the rep confirmed, stamped with who and when. */
69
+ attestations: OutreachAttestations;
70
+ }
71
+ /** Confirm's answer for one person, with what the rep supplied — see the module note. */
72
+ export declare function decideOutreachEnrollment(question: OutreachGateQuestion, facts: {
73
+ siteName: string;
74
+ alreadyInSequence: boolean;
75
+ }, supplied: {
76
+ personalLine: string;
77
+ attestations: readonly unknown[];
78
+ uid: string;
79
+ nowMs: number;
80
+ }): OutreachEnrollDecision;
81
+ export {};
@@ -0,0 +1,250 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ /**
4
+ * @license
5
+ * Copyright 2026 Aglyn LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */ /*==========================================
19
+ * WHO CAN BE ENROLLED, AND WHAT THE REP STILL OWES (AGL-2980).
20
+ *
21
+ * The enroll dialog shows every person a view or a search named, each
22
+ * marked eligible, blocked, or waiting on the rep; Confirm asks the same
23
+ * question again, on the server, with what the rep supplied. Both answers
24
+ * come from the engine's `evaluateOutreachGates` — this module only reads
25
+ * the people and asks it the right questions.
26
+ *
27
+ * ## Three questions of one pure function
28
+ *
29
+ * The gates refuse a cold contact until the rep writes a personal line and
30
+ * ticks the attestations, and refuse an address in an unknown country until
31
+ * the rep confirms it is a US business address. So a preview asks:
32
+ *
33
+ * 1. as the person stands, with nothing confirmed — allowed means
34
+ * ELIGIBLE;
35
+ * 2. as if the rep confirmed everything they can — refused means BLOCKED,
36
+ * for the reasons that remain, which no tick can clear;
37
+ * 3. otherwise the person is WAITING ON THE REP, for what (1) found
38
+ * missing.
39
+ *
40
+ * The confirmation in (2) is hypothetical and never stored: Confirm stamps
41
+ * only what the rep ticked, with their uid and the time, and asks again.
42
+ *
43
+ * ## One enrollment per person per sequence, ever
44
+ *
45
+ * The enrollment's id is the sequence and the contact
46
+ * (`outreachEnrollmentId`), and it is written with `create()`. A person who
47
+ * finished, replied or was stopped is not sent the same emails a second
48
+ * time by being enrolled again, and two members confirming the same person
49
+ * at once write one enrollment.
50
+ *==========================================*/ import { normalizeContactEmail, contactDisplayName, readContactFacet } from "@aglyn/aglyn/app-utils/contacts";
51
+ import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
52
+ import { evaluateOutreachGates } from "../engine/gates.js";
53
+ import { outreachAttestationsFrom } from "../engine/enrollment-state.js";
54
+ import { OUTREACH_ATTESTATION_KINDS } from "../model/outreach.types.js";
55
+ /** `getAll` takes this many references at most per call. */ const GET_ALL_CHUNK = 300;
56
+ async function getAllChunked(firestore, refs) {
57
+ const snapshots = [];
58
+ for(let start = 0; start < refs.length; start += GET_ALL_CHUNK){
59
+ snapshots.push(...await firestore.getAll(...refs.slice(start, start + GET_ALL_CHUNK)));
60
+ }
61
+ return snapshots;
62
+ }
63
+ /**
64
+ * The contacts, as the sequence's site knows them, and the company each is
65
+ * filed under there — two batched reads for everyone.
66
+ */ export async function readOutreachEnrollCandidates(firestore, input) {
67
+ if (!input.contactIds.length) return [];
68
+ const org = firestore.collection('orgs').doc(input.orgId);
69
+ const snapshots = await getAllChunked(firestore, input.contactIds.map((id)=>org.collection('contacts').doc(id)));
70
+ const read = input.contactIds.map((contactId, index)=>{
71
+ var _readContactFacet_companyId;
72
+ var _snapshots_index;
73
+ const data = ((_snapshots_index = snapshots[index]) == null ? void 0 : _snapshots_index.exists) ? snapshots[index].data() : null;
74
+ return {
75
+ contactId,
76
+ contact: data,
77
+ visible: data ? visibleToHost(data['visibleTo'], input.hostId) : false,
78
+ name: data ? contactDisplayName(data, input.contactGroupId).trim() : '',
79
+ email: data ? normalizeContactEmail(data['email']) : null,
80
+ companyId: data ? String((_readContactFacet_companyId = readContactFacet(data, input.contactGroupId).companyId) != null ? _readContactFacet_companyId : '') : ''
81
+ };
82
+ });
83
+ const companyIds = [
84
+ ...new Set(read.map((entry)=>entry.companyId).filter(Boolean))
85
+ ];
86
+ const companies = new Map();
87
+ if (companyIds.length) {
88
+ const found = await getAllChunked(firestore, companyIds.map((id)=>org.collection('companies').doc(id)));
89
+ found.forEach((snapshot, index)=>{
90
+ if (snapshot.exists) companies.set(companyIds[index], snapshot.data());
91
+ });
92
+ }
93
+ return read.map((_0)=>{
94
+ let { companyId } = _0, candidate = _object_without_properties_loose(_0, [
95
+ "companyId"
96
+ ]);
97
+ var _companies_get;
98
+ return _extends({}, candidate, {
99
+ company: companyId ? (_companies_get = companies.get(companyId)) != null ? _companies_get : null : null
100
+ });
101
+ });
102
+ }
103
+ /** What stands in the way before the gates are asked anything. */ export function outreachCandidateBlocks(candidate, facts) {
104
+ if (!candidate.contact) {
105
+ return [
106
+ {
107
+ code: 'contact_missing',
108
+ reason: 'This contact no longer exists in the CRM.'
109
+ }
110
+ ];
111
+ }
112
+ const blocks = [];
113
+ if (!candidate.visible) {
114
+ blocks.push({
115
+ code: 'not_in_site',
116
+ reason: `This contact isn't in ${facts.siteName || "this sequence's site"}'s CRM.`
117
+ });
118
+ }
119
+ if (!candidate.email) {
120
+ blocks.push({
121
+ code: 'no_email',
122
+ reason: 'This contact has no email address.'
123
+ });
124
+ }
125
+ if (facts.alreadyInSequence) {
126
+ blocks.push({
127
+ code: 'already_in_sequence',
128
+ reason: "This person has already been in this sequence, so they won't be sent its emails again."
129
+ });
130
+ }
131
+ return blocks;
132
+ }
133
+ /** Every attestation, stamped for a hypothetical question only — never stored. */ const EVERYTHING_CONFIRMED = Object.fromEntries(OUTREACH_ATTESTATION_KINDS.map((kind)=>[
134
+ kind,
135
+ {
136
+ uid: 'preview',
137
+ atMs: 1
138
+ }
139
+ ]));
140
+ const blocksOf = (blocks)=>blocks.map(({ code, reason })=>({
141
+ code,
142
+ reason
143
+ }));
144
+ /** Where one person stands before the rep confirms anything — see the module note. */ export function previewOutreachPerson(question, facts) {
145
+ const { candidate } = question;
146
+ const person = {
147
+ contactId: candidate.contactId,
148
+ name: candidate.name,
149
+ email: candidate.email
150
+ };
151
+ const early = outreachCandidateBlocks(candidate, facts);
152
+ if (early.length) {
153
+ return _extends({}, person, {
154
+ cold: false,
155
+ country: null,
156
+ status: 'blocked',
157
+ blocks: early,
158
+ requires: {
159
+ personalLine: false,
160
+ attestations: []
161
+ }
162
+ });
163
+ }
164
+ const ask = (personalLine, attestations)=>{
165
+ var _candidate_email;
166
+ return evaluateOutreachGates({
167
+ email: (_candidate_email = candidate.email) != null ? _candidate_email : '',
168
+ contact: candidate.contact,
169
+ contactGroupId: question.contactGroupId,
170
+ company: candidate.company,
171
+ settings: question.settings,
172
+ personalLine,
173
+ attestations,
174
+ lookups: question.lookups
175
+ });
176
+ };
177
+ const asIs = ask('', {});
178
+ const known = {
179
+ cold: asIs.cold,
180
+ country: asIs.country.country
181
+ };
182
+ if (asIs.allowed) {
183
+ return _extends({}, person, known, {
184
+ status: 'eligible',
185
+ blocks: [],
186
+ requires: {
187
+ personalLine: false,
188
+ attestations: []
189
+ }
190
+ });
191
+ }
192
+ // The line is only checked for being present, so any sentence stands in.
193
+ const confirmed = ask('A personal line.', EVERYTHING_CONFIRMED);
194
+ if (!confirmed.allowed) {
195
+ return _extends({}, person, known, {
196
+ status: 'blocked',
197
+ blocks: blocksOf(confirmed.blocks),
198
+ requires: {
199
+ personalLine: false,
200
+ attestations: []
201
+ }
202
+ });
203
+ }
204
+ const attestations = asIs.cold ? [
205
+ ...asIs.missingAttestations
206
+ ] : asIs.blocks.some((block)=>block.code === 'country_unknown') ? [
207
+ 'us_business_address'
208
+ ] : [];
209
+ return _extends({}, person, known, {
210
+ status: 'needs_confirmation',
211
+ blocks: [],
212
+ requires: {
213
+ personalLine: asIs.blocks.some((block)=>block.code === 'personal_line_missing'),
214
+ attestations
215
+ }
216
+ });
217
+ }
218
+ /** Confirm's answer for one person, with what the rep supplied — see the module note. */ export function decideOutreachEnrollment(question, facts, supplied) {
219
+ var _question_candidate_email;
220
+ const attestations = outreachAttestationsFrom(supplied.attestations, supplied.uid, supplied.nowMs);
221
+ const early = outreachCandidateBlocks(question.candidate, facts);
222
+ if (early.length) {
223
+ return {
224
+ allowed: false,
225
+ blocks: early,
226
+ cold: false,
227
+ email: question.candidate.email,
228
+ attestations
229
+ };
230
+ }
231
+ const result = evaluateOutreachGates({
232
+ email: (_question_candidate_email = question.candidate.email) != null ? _question_candidate_email : '',
233
+ contact: question.candidate.contact,
234
+ contactGroupId: question.contactGroupId,
235
+ company: question.candidate.company,
236
+ settings: question.settings,
237
+ personalLine: supplied.personalLine,
238
+ attestations,
239
+ lookups: question.lookups
240
+ });
241
+ return {
242
+ allowed: result.allowed,
243
+ blocks: blocksOf(result.blocks),
244
+ cold: result.cold,
245
+ email: result.email,
246
+ attestations
247
+ };
248
+ }
249
+
250
+ //# sourceMappingURL=enroll-people.js.map