@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,184 @@
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
+ * BOUNCES (AGL-2979).
18
+ *
19
+ * A mail server that cannot deliver says so in a DELIVERY STATUS
20
+ * NOTIFICATION (RFC 3464): a `multipart/report; report-type=delivery-status`
21
+ * message, usually from `mailer-daemon@` or `postmaster@`, whose
22
+ * `message/delivery-status` part carries one block per recipient —
23
+ * `Final-Recipient`, `Action` and `Status`, an enhanced code like `5.1.1`.
24
+ * Gmail writes one into the sender's own thread when a message it relayed is
25
+ * refused, and a receiving server that accepted a message and then gave up
26
+ * sends one of its own.
27
+ *
28
+ * ## Hard and soft
29
+ *
30
+ * A `5.x.x` status on a `failed` action is HARD: the address does not
31
+ * exist, the domain refuses the sender, the mailbox is closed — trying again
32
+ * will not help, and the enrollment stops. A `4.x.x` status, or a `delayed`
33
+ * action, is SOFT: the server is busy or the mailbox is full, the sending
34
+ * server is still retrying, and nothing changes. A report whose action is
35
+ * `delivered`, `relayed` or `expanded` is not a bounce at all.
36
+ *
37
+ * ## Reports that are not standard
38
+ *
39
+ * Some servers send a bounce as plain prose. A message from a mailer daemon
40
+ * whose subject or text reads like one is read for the status code the prose
41
+ * quotes (`550 5.1.1`, `421 4.7.0`), and failing that for the words that say
42
+ * permanent or temporary — soft when it says it will retry, hard otherwise.
43
+ *==========================================*/ import { emailAddressOf } from "@aglyn/aglyn/app-utils/crm-inbound";
44
+ import { outreachHeader } from "./thread-message.js";
45
+ const DAEMON_LOCAL_PART = /^(mailer-daemon|postmaster|mail-daemon|maildaemon)$/i;
46
+ const DAEMON_NAME = /\bmail delivery (subsystem|system|service)\b/i;
47
+ const BOUNCE_WORDS = /undeliver|delivery status notification|delivery (has )?failed|delivery failure|failure notice|returned mail|mail delivery failed|could not be delivered|wasn't delivered|was not delivered|not delivered|address not found|message blocked/i;
48
+ /** Whether a message comes from a mail system rather than from a person. */ export function isMailerDaemonMessage(message) {
49
+ var _ref;
50
+ const raw = String((_ref = message == null ? void 0 : message.from) != null ? _ref : '');
51
+ const address = emailAddressOf(raw);
52
+ const local = address ? address.slice(0, address.lastIndexOf('@')) : '';
53
+ return DAEMON_LOCAL_PART.test(local) || DAEMON_NAME.test(raw);
54
+ }
55
+ function isDeliveryStatusReport(message) {
56
+ var _message_parts;
57
+ const contentType = outreachHeader(message, 'Content-Type').toLowerCase();
58
+ if (contentType.includes('multipart/report') && /report-type\s*=\s*"?delivery-status"?/.test(contentType)) {
59
+ return true;
60
+ }
61
+ return ((_message_parts = message.parts) != null ? _message_parts : []).some((part)=>part.mimeType === 'message/delivery-status' || part.mimeType === 'message/global-delivery-status');
62
+ }
63
+ /** `Name: value` fields, continuation lines folded in, one list per blank-line block. */ function fieldBlocks(text) {
64
+ const blocks = [];
65
+ let current = {};
66
+ let last = null;
67
+ const close = ()=>{
68
+ if (Object.keys(current).length) blocks.push(current);
69
+ current = {};
70
+ last = null;
71
+ };
72
+ for (const line of text.replace(/\r\n?/g, '\n').split('\n')){
73
+ if (!line.trim()) {
74
+ close();
75
+ continue;
76
+ }
77
+ if (/^[ \t]/.test(line) && last) {
78
+ current[last] = `${current[last]} ${line.trim()}`;
79
+ continue;
80
+ }
81
+ const field = /^([A-Za-z][A-Za-z0-9-]*)\s*:\s*(.*)$/.exec(line);
82
+ if (field) {
83
+ last = field[1].toLowerCase();
84
+ if (!(last in current)) current[last] = field[2].trim();
85
+ }
86
+ }
87
+ close();
88
+ return blocks;
89
+ }
90
+ /** `rfc822; someone@example.com` → the address. */ function typedAddress(value) {
91
+ if (!value) return null;
92
+ const typed = /^[a-z0-9-]+\s*;\s*(.*)$/i.exec(value);
93
+ return emailAddressOf(typed ? typed[1] : value);
94
+ }
95
+ /** An enhanced status code, any class: what a `Status` field holds. */ const STATUS_CODE = /\b([245])\.(\d{1,3})\.(\d{1,3})\b/;
96
+ /** A failure's enhanced status code, quoted somewhere in prose. */ const FAILURE_STATUS_CODE = /\b([45])\.(\d{1,3})\.(\d{1,3})\b/;
97
+ /** A failure's basic SMTP reply code, as a transcript quotes it: `550-` or `421 `. */ const FAILURE_REPLY_CODE = /\b([45])[0-5]\d(?=[ -])/;
98
+ function kindFor(action, status) {
99
+ if (action === 'delivered' || action === 'relayed' || action === 'expanded') return null;
100
+ const statusClass = status ? status.charAt(0) : null;
101
+ if (statusClass === '4' || action === 'delayed') return 'soft';
102
+ if (statusClass === '5') return action === null || action === 'failed' ? 'hard' : 'soft';
103
+ if (action === 'failed') return 'hard';
104
+ return null;
105
+ }
106
+ const SEVERITY = {
107
+ soft: 1,
108
+ hard: 2
109
+ };
110
+ function failedRecipientsHeader(message) {
111
+ return outreachHeader(message, 'X-Failed-Recipients').split(/[,\s]+/).map((value)=>emailAddressOf(value)).filter((value)=>value !== null);
112
+ }
113
+ function prose(message) {
114
+ return [
115
+ message.subject,
116
+ message.textBody,
117
+ message.snippet
118
+ ].filter((value)=>typeof value === 'string' && value).join('\n');
119
+ }
120
+ /**
121
+ * What a delivery report says, or `null` when the message is not one — see
122
+ * the module note for how a standard report and a prose bounce are read.
123
+ */ export function readOutreachDeliveryReport(message) {
124
+ var _message_parts, _ref, _ref1, _ref2;
125
+ const standard = isDeliveryStatusReport(message);
126
+ const daemon = isMailerDaemonMessage(message);
127
+ if (!standard && !(daemon && BOUNCE_WORDS.test(prose(message)))) return null;
128
+ const statusText = ((_message_parts = message.parts) != null ? _message_parts : []).filter((part)=>part.mimeType === 'message/delivery-status' || part.mimeType === 'message/global-delivery-status').map((part)=>{
129
+ var _part_text;
130
+ return (_part_text = part.text) != null ? _part_text : '';
131
+ }).join('\n\n');
132
+ // The first block describes the report itself (`Reporting-MTA`,
133
+ // `Arrival-Date`); each later one describes a recipient.
134
+ const recipients = fieldBlocks(statusText).filter((block)=>block['final-recipient'] || block['original-recipient'] || block['action'] || block['status']).map((block)=>{
135
+ var _block_status, _typedAddress;
136
+ const action = block['action'] ? block['action'].toLowerCase().split(/\s/)[0] : null;
137
+ const statusMatch = STATUS_CODE.exec((_block_status = block['status']) != null ? _block_status : '');
138
+ const status = statusMatch ? statusMatch[0] : null;
139
+ const diagnostic = block['diagnostic-code'] ? block['diagnostic-code'].replace(/^[a-z0-9-]+\s*;\s*/i, '') : null;
140
+ return {
141
+ address: (_typedAddress = typedAddress(block['final-recipient'])) != null ? _typedAddress : typedAddress(block['original-recipient']),
142
+ action,
143
+ status,
144
+ diagnostic,
145
+ kind: kindFor(action, status)
146
+ };
147
+ });
148
+ let worst = null;
149
+ for (const recipient of recipients){
150
+ if (recipient.kind && (!worst || SEVERITY[recipient.kind] > SEVERITY[worst.kind])) {
151
+ worst = recipient;
152
+ }
153
+ }
154
+ if (!worst && !recipients.length) {
155
+ var _ref3, _ref4;
156
+ // A report without readable blocks, or a bounce written as prose: the
157
+ // code the text quotes, else the words.
158
+ const text = prose(message);
159
+ const code = FAILURE_STATUS_CODE.exec(text);
160
+ const reply = FAILURE_REPLY_CODE.exec(text);
161
+ const statusClass = (_ref3 = (_ref4 = code == null ? void 0 : code[1]) != null ? _ref4 : reply == null ? void 0 : reply[1]) != null ? _ref3 : null;
162
+ const retrying = /\b(delay(ed)?|will (keep )?retry|not (been )?delivered yet|temporar(y|ily))\b/i.test(text);
163
+ const kind = statusClass === '5' ? 'hard' : statusClass === '4' || retrying ? 'soft' : 'hard';
164
+ return {
165
+ kind,
166
+ recipients: [],
167
+ failedAddresses: failedRecipientsHeader(message),
168
+ status: code ? code[0] : null,
169
+ diagnostic: null
170
+ };
171
+ }
172
+ // Only the addresses the report's own kind is about: a report that
173
+ // hard-bounced one recipient and delayed another does not bounce both.
174
+ const failedAddresses = worst ? recipients.filter((recipient)=>recipient.kind === worst.kind && recipient.address).map((recipient)=>recipient.address) : [];
175
+ return {
176
+ kind: (_ref = worst == null ? void 0 : worst.kind) != null ? _ref : null,
177
+ recipients,
178
+ failedAddresses: failedAddresses.length || !worst ? failedAddresses : failedRecipientsHeader(message),
179
+ status: (_ref1 = worst == null ? void 0 : worst.status) != null ? _ref1 : null,
180
+ diagnostic: (_ref2 = worst == null ? void 0 : worst.diagnostic) != null ? _ref2 : null
181
+ };
182
+ }
183
+
184
+ //# sourceMappingURL=delivery-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/delivery-status.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 * BOUNCES (AGL-2979).\n *\n * A mail server that cannot deliver says so in a DELIVERY STATUS\n * NOTIFICATION (RFC 3464): a `multipart/report; report-type=delivery-status`\n * message, usually from `mailer-daemon@` or `postmaster@`, whose\n * `message/delivery-status` part carries one block per recipient —\n * `Final-Recipient`, `Action` and `Status`, an enhanced code like `5.1.1`.\n * Gmail writes one into the sender's own thread when a message it relayed is\n * refused, and a receiving server that accepted a message and then gave up\n * sends one of its own.\n *\n * ## Hard and soft\n *\n * A `5.x.x` status on a `failed` action is HARD: the address does not\n * exist, the domain refuses the sender, the mailbox is closed — trying again\n * will not help, and the enrollment stops. A `4.x.x` status, or a `delayed`\n * action, is SOFT: the server is busy or the mailbox is full, the sending\n * server is still retrying, and nothing changes. A report whose action is\n * `delivered`, `relayed` or `expanded` is not a bounce at all.\n *\n * ## Reports that are not standard\n *\n * Some servers send a bounce as plain prose. A message from a mailer daemon\n * whose subject or text reads like one is read for the status code the prose\n * quotes (`550 5.1.1`, `421 4.7.0`), and failing that for the words that say\n * permanent or temporary — soft when it says it will retry, hard otherwise.\n *==========================================*/\n\nimport { emailAddressOf } from '@aglyn/aglyn/app-utils/crm-inbound'\nimport { outreachHeader, type OutreachThreadMessage } from './thread-message'\n\nexport type OutreachBounceKind = 'hard' | 'soft'\n\n/** What a delivery report says about one recipient. */\nexport interface OutreachDeliveryRecipient {\n /** `Final-Recipient`, or `Original-Recipient` when that is all there is; normalized. */\n address: string | null\n /** `Action`, lowercased: `failed`, `delayed`, `delivered`, `relayed`, `expanded`. */\n action: string | null\n /** `Status`: the enhanced code, `5.1.1`. */\n status: string | null\n /** `Diagnostic-Code`, without its type prefix. */\n diagnostic: string | null\n /** `hard`, `soft`, or `null` for a block that reports no failure. */\n kind: OutreachBounceKind | null\n}\n\nexport interface OutreachDeliveryReport {\n /** The worst failure the report carries, or `null` when it reports none. */\n kind: OutreachBounceKind | null\n /** Every recipient the report names, in order. */\n recipients: OutreachDeliveryRecipient[]\n /**\n * The addresses `kind` is about, normalized — `X-Failed-Recipients` when no\n * block names one. Empty when nothing says which recipient failed.\n */\n failedAddresses: string[]\n /** The status code of the worst failure, when one was found. */\n status: string | null\n /** The diagnostic of the worst failure, when one was given. */\n diagnostic: string | null\n}\n\nconst DAEMON_LOCAL_PART = /^(mailer-daemon|postmaster|mail-daemon|maildaemon)$/i\nconst DAEMON_NAME = /\\bmail delivery (subsystem|system|service)\\b/i\nconst BOUNCE_WORDS =\n /undeliver|delivery status notification|delivery (has )?failed|delivery failure|failure notice|returned mail|mail delivery failed|could not be delivered|wasn't delivered|was not delivered|not delivered|address not found|message blocked/i\n\n/** Whether a message comes from a mail system rather than from a person. */\nexport function isMailerDaemonMessage(message: Pick<OutreachThreadMessage, 'from'>): boolean {\n const raw = String(message?.from ?? '')\n const address = emailAddressOf(raw)\n const local = address ? address.slice(0, address.lastIndexOf('@')) : ''\n return DAEMON_LOCAL_PART.test(local) || DAEMON_NAME.test(raw)\n}\n\nfunction isDeliveryStatusReport(message: OutreachThreadMessage): boolean {\n const contentType = outreachHeader(message, 'Content-Type').toLowerCase()\n if (\n contentType.includes('multipart/report') &&\n /report-type\\s*=\\s*\"?delivery-status\"?/.test(contentType)\n ) {\n return true\n }\n return (message.parts ?? []).some(\n (part) =>\n part.mimeType === 'message/delivery-status' ||\n part.mimeType === 'message/global-delivery-status',\n )\n}\n\n/** `Name: value` fields, continuation lines folded in, one list per blank-line block. */\nfunction fieldBlocks(text: string): Array<Record<string, string>> {\n const blocks: Array<Record<string, string>> = []\n let current: Record<string, string> = {}\n let last: string | null = null\n const close = () => {\n if (Object.keys(current).length) blocks.push(current)\n current = {}\n last = null\n }\n for (const line of text.replace(/\\r\\n?/g, '\\n').split('\\n')) {\n if (!line.trim()) {\n close()\n continue\n }\n if (/^[ \\t]/.test(line) && last) {\n current[last] = `${current[last]} ${line.trim()}`\n continue\n }\n const field = /^([A-Za-z][A-Za-z0-9-]*)\\s*:\\s*(.*)$/.exec(line)\n if (field) {\n last = field[1].toLowerCase()\n if (!(last in current)) current[last] = field[2].trim()\n }\n }\n close()\n return blocks\n}\n\n/** `rfc822; someone@example.com` → the address. */\nfunction typedAddress(value: string | undefined): string | null {\n if (!value) return null\n const typed = /^[a-z0-9-]+\\s*;\\s*(.*)$/i.exec(value)\n return emailAddressOf(typed ? typed[1] : value)\n}\n\n/** An enhanced status code, any class: what a `Status` field holds. */\nconst STATUS_CODE = /\\b([245])\\.(\\d{1,3})\\.(\\d{1,3})\\b/\n/** A failure's enhanced status code, quoted somewhere in prose. */\nconst FAILURE_STATUS_CODE = /\\b([45])\\.(\\d{1,3})\\.(\\d{1,3})\\b/\n/** A failure's basic SMTP reply code, as a transcript quotes it: `550-` or `421 `. */\nconst FAILURE_REPLY_CODE = /\\b([45])[0-5]\\d(?=[ -])/\n\nfunction kindFor(action: string | null, status: string | null): OutreachBounceKind | null {\n if (action === 'delivered' || action === 'relayed' || action === 'expanded') return null\n const statusClass = status ? status.charAt(0) : null\n if (statusClass === '4' || action === 'delayed') return 'soft'\n if (statusClass === '5') return action === null || action === 'failed' ? 'hard' : 'soft'\n if (action === 'failed') return 'hard'\n return null\n}\n\nconst SEVERITY: Record<OutreachBounceKind, number> = { soft: 1, hard: 2 }\n\nfunction failedRecipientsHeader(message: OutreachThreadMessage): string[] {\n return outreachHeader(message, 'X-Failed-Recipients')\n .split(/[,\\s]+/)\n .map((value) => emailAddressOf(value))\n .filter((value): value is string => value !== null)\n}\n\nfunction prose(message: OutreachThreadMessage): string {\n return [message.subject, message.textBody, message.snippet]\n .filter((value) => typeof value === 'string' && value)\n .join('\\n')\n}\n\n/**\n * What a delivery report says, or `null` when the message is not one — see\n * the module note for how a standard report and a prose bounce are read.\n */\nexport function readOutreachDeliveryReport(\n message: OutreachThreadMessage,\n): OutreachDeliveryReport | null {\n const standard = isDeliveryStatusReport(message)\n const daemon = isMailerDaemonMessage(message)\n if (!standard && !(daemon && BOUNCE_WORDS.test(prose(message)))) return null\n\n const statusText = (message.parts ?? [])\n .filter(\n (part) =>\n part.mimeType === 'message/delivery-status' ||\n part.mimeType === 'message/global-delivery-status',\n )\n .map((part) => part.text ?? '')\n .join('\\n\\n')\n // The first block describes the report itself (`Reporting-MTA`,\n // `Arrival-Date`); each later one describes a recipient.\n const recipients: OutreachDeliveryRecipient[] = fieldBlocks(statusText)\n .filter(\n (block) =>\n block['final-recipient'] || block['original-recipient'] || block['action'] || block['status'],\n )\n .map((block) => {\n const action = block['action'] ? block['action'].toLowerCase().split(/\\s/)[0] : null\n const statusMatch = STATUS_CODE.exec(block['status'] ?? '')\n const status = statusMatch ? statusMatch[0] : null\n const diagnostic = block['diagnostic-code']\n ? block['diagnostic-code'].replace(/^[a-z0-9-]+\\s*;\\s*/i, '')\n : null\n return {\n address: typedAddress(block['final-recipient']) ?? typedAddress(block['original-recipient']),\n action,\n status,\n diagnostic,\n kind: kindFor(action, status),\n }\n })\n\n let worst: OutreachDeliveryRecipient | null = null\n for (const recipient of recipients) {\n if (recipient.kind && (!worst || SEVERITY[recipient.kind] > SEVERITY[worst.kind])) {\n worst = recipient\n }\n }\n\n if (!worst && !recipients.length) {\n // A report without readable blocks, or a bounce written as prose: the\n // code the text quotes, else the words.\n const text = prose(message)\n const code = FAILURE_STATUS_CODE.exec(text)\n const reply = FAILURE_REPLY_CODE.exec(text)\n const statusClass = code?.[1] ?? reply?.[1] ?? null\n const retrying =\n /\\b(delay(ed)?|will (keep )?retry|not (been )?delivered yet|temporar(y|ily))\\b/i.test(text)\n const kind: OutreachBounceKind =\n statusClass === '5' ? 'hard' : statusClass === '4' || retrying ? 'soft' : 'hard'\n return {\n kind,\n recipients: [],\n failedAddresses: failedRecipientsHeader(message),\n status: code ? code[0] : null,\n diagnostic: null,\n }\n }\n\n // Only the addresses the report's own kind is about: a report that\n // hard-bounced one recipient and delayed another does not bounce both.\n const failedAddresses = worst\n ? recipients\n .filter((recipient) => recipient.kind === worst.kind && recipient.address)\n .map((recipient) => recipient.address as string)\n : []\n return {\n kind: worst?.kind ?? null,\n recipients,\n failedAddresses:\n failedAddresses.length || !worst ? failedAddresses : failedRecipientsHeader(message),\n status: worst?.status ?? null,\n diagnostic: worst?.diagnostic ?? null,\n }\n}\n"],"names":["emailAddressOf","outreachHeader","DAEMON_LOCAL_PART","DAEMON_NAME","BOUNCE_WORDS","isMailerDaemonMessage","message","raw","String","from","address","local","slice","lastIndexOf","test","isDeliveryStatusReport","contentType","toLowerCase","includes","parts","some","part","mimeType","fieldBlocks","text","blocks","current","last","close","Object","keys","length","push","line","replace","split","trim","field","exec","typedAddress","value","typed","STATUS_CODE","FAILURE_STATUS_CODE","FAILURE_REPLY_CODE","kindFor","action","status","statusClass","charAt","SEVERITY","soft","hard","failedRecipientsHeader","map","filter","prose","subject","textBody","snippet","join","readOutreachDeliveryReport","standard","daemon","statusText","recipients","block","statusMatch","diagnostic","kind","worst","recipient","code","reply","retrying","failedAddresses"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;4CA2B4C,GAE5C,SAASA,cAAc,QAAQ,qCAAoC;AACnE,SAASC,cAAc,QAAoC,sBAAkB;AAkC7E,MAAMC,oBAAoB;AAC1B,MAAMC,cAAc;AACpB,MAAMC,eACJ;AAEF,0EAA0E,GAC1E,OAAO,SAASC,sBAAsBC,OAA4C;;IAChF,MAAMC,MAAMC,eAAOF,2BAAAA,QAASG,IAAI,mBAAI;IACpC,MAAMC,UAAUV,eAAeO;IAC/B,MAAMI,QAAQD,UAAUA,QAAQE,KAAK,CAAC,GAAGF,QAAQG,WAAW,CAAC,QAAQ;IACrE,OAAOX,kBAAkBY,IAAI,CAACH,UAAUR,YAAYW,IAAI,CAACP;AAC3D;AAEA,SAASQ,uBAAuBT,OAA8B;QAQpDA;IAPR,MAAMU,cAAcf,eAAeK,SAAS,gBAAgBW,WAAW;IACvE,IACED,YAAYE,QAAQ,CAAC,uBACrB,wCAAwCJ,IAAI,CAACE,cAC7C;QACA,OAAO;IACT;IACA,OAAO,EAACV,iBAAAA,QAAQa,KAAK,YAAbb,iBAAiB,EAAE,EAAEc,IAAI,CAC/B,CAACC,OACCA,KAAKC,QAAQ,KAAK,6BAClBD,KAAKC,QAAQ,KAAK;AAExB;AAEA,uFAAuF,GACvF,SAASC,YAAYC,IAAY;IAC/B,MAAMC,SAAwC,EAAE;IAChD,IAAIC,UAAkC,CAAC;IACvC,IAAIC,OAAsB;IAC1B,MAAMC,QAAQ;QACZ,IAAIC,OAAOC,IAAI,CAACJ,SAASK,MAAM,EAAEN,OAAOO,IAAI,CAACN;QAC7CA,UAAU,CAAC;QACXC,OAAO;IACT;IACA,KAAK,MAAMM,QAAQT,KAAKU,OAAO,CAAC,UAAU,MAAMC,KAAK,CAAC,MAAO;QAC3D,IAAI,CAACF,KAAKG,IAAI,IAAI;YAChBR;YACA;QACF;QACA,IAAI,SAASd,IAAI,CAACmB,SAASN,MAAM;YAC/BD,OAAO,CAACC,KAAK,GAAG,GAAGD,OAAO,CAACC,KAAK,CAAC,CAAC,EAAEM,KAAKG,IAAI,IAAI;YACjD;QACF;QACA,MAAMC,QAAQ,uCAAuCC,IAAI,CAACL;QAC1D,IAAII,OAAO;YACTV,OAAOU,KAAK,CAAC,EAAE,CAACpB,WAAW;YAC3B,IAAI,CAAEU,CAAAA,QAAQD,OAAM,GAAIA,OAAO,CAACC,KAAK,GAAGU,KAAK,CAAC,EAAE,CAACD,IAAI;QACvD;IACF;IACAR;IACA,OAAOH;AACT;AAEA,iDAAiD,GACjD,SAASc,aAAaC,KAAyB;IAC7C,IAAI,CAACA,OAAO,OAAO;IACnB,MAAMC,QAAQ,2BAA2BH,IAAI,CAACE;IAC9C,OAAOxC,eAAeyC,QAAQA,KAAK,CAAC,EAAE,GAAGD;AAC3C;AAEA,qEAAqE,GACrE,MAAME,cAAc;AACpB,iEAAiE,GACjE,MAAMC,sBAAsB;AAC5B,oFAAoF,GACpF,MAAMC,qBAAqB;AAE3B,SAASC,QAAQC,MAAqB,EAAEC,MAAqB;IAC3D,IAAID,WAAW,eAAeA,WAAW,aAAaA,WAAW,YAAY,OAAO;IACpF,MAAME,cAAcD,SAASA,OAAOE,MAAM,CAAC,KAAK;IAChD,IAAID,gBAAgB,OAAOF,WAAW,WAAW,OAAO;IACxD,IAAIE,gBAAgB,KAAK,OAAOF,WAAW,QAAQA,WAAW,WAAW,SAAS;IAClF,IAAIA,WAAW,UAAU,OAAO;IAChC,OAAO;AACT;AAEA,MAAMI,WAA+C;IAAEC,MAAM;IAAGC,MAAM;AAAE;AAExE,SAASC,uBAAuB/C,OAA8B;IAC5D,OAAOL,eAAeK,SAAS,uBAC5B6B,KAAK,CAAC,UACNmB,GAAG,CAAC,CAACd,QAAUxC,eAAewC,QAC9Be,MAAM,CAAC,CAACf,QAA2BA,UAAU;AAClD;AAEA,SAASgB,MAAMlD,OAA8B;IAC3C,OAAO;QAACA,QAAQmD,OAAO;QAAEnD,QAAQoD,QAAQ;QAAEpD,QAAQqD,OAAO;KAAC,CACxDJ,MAAM,CAAC,CAACf,QAAU,OAAOA,UAAU,YAAYA,OAC/CoB,IAAI,CAAC;AACV;AAEA;;;CAGC,GACD,OAAO,SAASC,2BACdvD,OAA8B;QAMVA;IAJpB,MAAMwD,WAAW/C,uBAAuBT;IACxC,MAAMyD,SAAS1D,sBAAsBC;IACrC,IAAI,CAACwD,YAAY,CAAEC,CAAAA,UAAU3D,aAAaU,IAAI,CAAC0C,MAAMlD,SAAQ,GAAI,OAAO;IAExE,MAAM0D,aAAa,EAAC1D,iBAAAA,QAAQa,KAAK,YAAbb,iBAAiB,EAAE,EACpCiD,MAAM,CACL,CAAClC,OACCA,KAAKC,QAAQ,KAAK,6BAClBD,KAAKC,QAAQ,KAAK,kCAErBgC,GAAG,CAAC,CAACjC;YAASA;gBAAAA,aAAAA,KAAKG,IAAI,YAATH,aAAa;OAC3BuC,IAAI,CAAC;IACR,gEAAgE;IAChE,yDAAyD;IACzD,MAAMK,aAA0C1C,YAAYyC,YACzDT,MAAM,CACL,CAACW,QACCA,KAAK,CAAC,kBAAkB,IAAIA,KAAK,CAAC,qBAAqB,IAAIA,KAAK,CAAC,SAAS,IAAIA,KAAK,CAAC,SAAS,EAEhGZ,GAAG,CAAC,CAACY;YAEiCA,eAM1B3B;QAPX,MAAMO,SAASoB,KAAK,CAAC,SAAS,GAAGA,KAAK,CAAC,SAAS,CAACjD,WAAW,GAAGkB,KAAK,CAAC,KAAK,CAAC,EAAE,GAAG;QAChF,MAAMgC,cAAczB,YAAYJ,IAAI,EAAC4B,gBAAAA,KAAK,CAAC,SAAS,YAAfA,gBAAmB;QACxD,MAAMnB,SAASoB,cAAcA,WAAW,CAAC,EAAE,GAAG;QAC9C,MAAMC,aAAaF,KAAK,CAAC,kBAAkB,GACvCA,KAAK,CAAC,kBAAkB,CAAChC,OAAO,CAAC,uBAAuB,MACxD;QACJ,OAAO;YACLxB,OAAO,GAAE6B,gBAAAA,aAAa2B,KAAK,CAAC,kBAAkB,aAArC3B,gBAA0CA,aAAa2B,KAAK,CAAC,qBAAqB;YAC3FpB;YACAC;YACAqB;YACAC,MAAMxB,QAAQC,QAAQC;QACxB;IACF;IAEF,IAAIuB,QAA0C;IAC9C,KAAK,MAAMC,aAAaN,WAAY;QAClC,IAAIM,UAAUF,IAAI,IAAK,CAAA,CAACC,SAASpB,QAAQ,CAACqB,UAAUF,IAAI,CAAC,GAAGnB,QAAQ,CAACoB,MAAMD,IAAI,CAAC,AAAD,GAAI;YACjFC,QAAQC;QACV;IACF;IAEA,IAAI,CAACD,SAAS,CAACL,WAAWlC,MAAM,EAAE;YAMZyC;QALpB,sEAAsE;QACtE,wCAAwC;QACxC,MAAMhD,OAAOgC,MAAMlD;QACnB,MAAMkE,OAAO7B,oBAAoBL,IAAI,CAACd;QACtC,MAAMiD,QAAQ7B,mBAAmBN,IAAI,CAACd;QACtC,MAAMwB,eAAcwB,iBAAAA,wBAAAA,IAAM,CAAC,EAAE,oBAAIC,yBAAAA,KAAO,CAAC,EAAE,YAAvBD,QAA2B;QAC/C,MAAME,WACJ,iFAAiF5D,IAAI,CAACU;QACxF,MAAM6C,OACJrB,gBAAgB,MAAM,SAASA,gBAAgB,OAAO0B,WAAW,SAAS;QAC5E,OAAO;YACLL;YACAJ,YAAY,EAAE;YACdU,iBAAiBtB,uBAAuB/C;YACxCyC,QAAQyB,OAAOA,IAAI,CAAC,EAAE,GAAG;YACzBJ,YAAY;QACd;IACF;IAEA,mEAAmE;IACnE,uEAAuE;IACvE,MAAMO,kBAAkBL,QACpBL,WACGV,MAAM,CAAC,CAACgB,YAAcA,UAAUF,IAAI,KAAKC,MAAMD,IAAI,IAAIE,UAAU7D,OAAO,EACxE4C,GAAG,CAAC,CAACiB,YAAcA,UAAU7D,OAAO,IACvC,EAAE;IACN,OAAO;QACL2D,IAAI,UAAEC,yBAAAA,MAAOD,IAAI,mBAAI;QACrBJ;QACAU,iBACEA,gBAAgB5C,MAAM,IAAI,CAACuC,QAAQK,kBAAkBtB,uBAAuB/C;QAC9EyC,MAAM,WAAEuB,yBAAAA,MAAOvB,MAAM,oBAAI;QACzBqB,UAAU,WAAEE,yBAAAA,MAAOF,UAAU,oBAAI;IACnC;AACF"}
@@ -0,0 +1,18 @@
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
+ /** The key, or `null` for anything that is not a usable address. */
18
+ export declare function outreachDoNotContactKey(email: unknown): string | null;
@@ -0,0 +1,34 @@
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
+ * The document id of an address on the organization's Outreach
18
+ * do-not-contact list (AGL-2979).
19
+ *
20
+ * `personKey` — `sha256` of the normalized address, as full hex — which is
21
+ * exactly how `emailSuppressions/{key}`, `hosts/{hostId}/suppressions/{key}`
22
+ * and `hosts/{hostId}/topicOptOuts/{key}` are keyed, so the runtime reads all
23
+ * four lists for one person with one key and a document id never carries the
24
+ * address itself.
25
+ *
26
+ * SERVER ONLY, and so not in the engine's barrel: `personKey` hashes with
27
+ * `node:crypto`, which cannot ship to a browser. Import it by its own path,
28
+ * `@aglyn/plugins-outreach/engine/do-not-contact`.
29
+ */ import { personKey } from "@aglyn/aglyn/app-utils/person-key";
30
+ /** The key, or `null` for anything that is not a usable address. */ export function outreachDoNotContactKey(email) {
31
+ return personKey(email);
32
+ }
33
+
34
+ //# sourceMappingURL=do-not-contact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/engine/do-not-contact.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The document id of an address on the organization's Outreach\n * do-not-contact list (AGL-2979).\n *\n * `personKey` — `sha256` of the normalized address, as full hex — which is\n * exactly how `emailSuppressions/{key}`, `hosts/{hostId}/suppressions/{key}`\n * and `hosts/{hostId}/topicOptOuts/{key}` are keyed, so the runtime reads all\n * four lists for one person with one key and a document id never carries the\n * address itself.\n *\n * SERVER ONLY, and so not in the engine's barrel: `personKey` hashes with\n * `node:crypto`, which cannot ship to a browser. Import it by its own path,\n * `@aglyn/plugins-outreach/engine/do-not-contact`.\n */\n\nimport { personKey } from '@aglyn/aglyn/app-utils/person-key'\n\n/** The key, or `null` for anything that is not a usable address. */\nexport function outreachDoNotContactKey(email: unknown): string | null {\n return personKey(email)\n}\n"],"names":["personKey","outreachDoNotContactKey","email"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;CAaC,GAED,SAASA,SAAS,QAAQ,oCAAmC;AAE7D,kEAAkE,GAClE,OAAO,SAASC,wBAAwBC,KAAc;IACpD,OAAOF,UAAUE;AACnB"}
@@ -0,0 +1,156 @@
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 OutreachAttestations, type OutreachEnrollment, type OutreachEnrollmentStatus, type OutreachMailbox, type OutreachSequence, type OutreachStopReason } from '../model/outreach.types';
18
+ import { type OutreachRandom } from './schedule';
19
+ /** Where each status may go next. */
20
+ export declare const OUTREACH_ENROLLMENT_TRANSITIONS: Readonly<Record<OutreachEnrollmentStatus, readonly OutreachEnrollmentStatus[]>>;
21
+ export declare function canTransitionOutreachEnrollment(from: OutreachEnrollmentStatus, to: OutreachEnrollmentStatus): boolean;
22
+ /** Something that happened to an enrollment. `atMs` is when it happened. */
23
+ export type OutreachEnrollmentEvent = {
24
+ type: 'pause';
25
+ atMs: number;
26
+ byUid: string | null;
27
+ detail?: string | null;
28
+ } | {
29
+ type: 'resume';
30
+ atMs: number;
31
+ byUid: string | null;
32
+ } | {
33
+ type: 'stop';
34
+ atMs: number;
35
+ byUid: string | null;
36
+ reason?: Extract<OutreachStopReason, 'manual' | 'gate' | 'sequence_archived'>;
37
+ detail?: string | null;
38
+ } | {
39
+ type: 'reply';
40
+ atMs: number;
41
+ detail?: string | null;
42
+ } | {
43
+ type: 'opt_out';
44
+ atMs: number;
45
+ reason: Extract<OutreachStopReason, 'opt_out_reply' | 'unsubscribe' | 'do_not_contact'>;
46
+ detail?: string | null;
47
+ } | {
48
+ type: 'bounce';
49
+ atMs: number;
50
+ detail?: string | null;
51
+ } | {
52
+ type: 'send_failed';
53
+ atMs: number;
54
+ detail?: string | null;
55
+ };
56
+ /** The status fields an event writes. */
57
+ export interface OutreachEnrollmentStatusPatch {
58
+ status: OutreachEnrollmentStatus;
59
+ stopReason: OutreachStopReason | null;
60
+ stopDetail: string | null;
61
+ stoppedAtMs: number | null;
62
+ stoppedByUid: string | null;
63
+ nextDueAtMs: number | null;
64
+ }
65
+ /**
66
+ * `patch` is what to write, or `null` when there is nothing to write — the
67
+ * enrollment is already there, or the event is not allowed, in which case
68
+ * `error` says why.
69
+ */
70
+ export interface OutreachEnrollmentEventResult {
71
+ patch: OutreachEnrollmentStatusPatch | null;
72
+ error: string | null;
73
+ }
74
+ /** What an event does to an enrollment's status — see the module note. */
75
+ export declare function applyOutreachEnrollmentEvent(enrollment: Pick<OutreachEnrollment, 'status' | 'nextDueAtMs'>, event: OutreachEnrollmentEvent): OutreachEnrollmentEventResult;
76
+ type ScheduleMailbox = Pick<OutreachMailbox, 'timezone' | 'window'>;
77
+ type ScheduleSequence = Pick<OutreachSequence, 'steps' | 'settings'>;
78
+ /** When a new enrollment's first step comes due, or `null` when it cannot be placed. */
79
+ export declare function planOutreachFirstDue(input: {
80
+ sequence: ScheduleSequence;
81
+ mailbox: ScheduleMailbox;
82
+ enrolledAtMs: number;
83
+ random: OutreachRandom;
84
+ }): number | null;
85
+ /** Stamps the attestations a rep ticked with who ticked them and when; unknown kinds are dropped. */
86
+ export declare function outreachAttestationsFrom(kinds: readonly unknown[], uid: string, atMs: number): OutreachAttestations;
87
+ export interface OutreachEnrollmentDraft {
88
+ id: string;
89
+ sequence: Pick<OutreachSequence, 'id' | 'hostId' | 'mailboxId' | 'steps' | 'settings'>;
90
+ mailbox: ScheduleMailbox;
91
+ contactId: string;
92
+ /** The contact's name as the sending site knows it; `''` for none. */
93
+ contactName?: string | null;
94
+ /** The address, as the gates normalized it. */
95
+ email: string;
96
+ /** The gates' `cold`. */
97
+ cold: boolean;
98
+ personalLine: string;
99
+ attestations: OutreachAttestations;
100
+ enrolledByUid: string;
101
+ nowMs: number;
102
+ random: OutreachRandom;
103
+ }
104
+ /**
105
+ * A new enrollment document, first step scheduled — one builder for every
106
+ * door that enrolls, so an enrollment made from a contact and one made from
107
+ * a list are the same shape. Call it after the gates allowed the person.
108
+ */
109
+ export declare function buildOutreachEnrollment(draft: OutreachEnrollmentDraft): OutreachEnrollment;
110
+ /** What the provider reported for an email step that was sent. */
111
+ export interface OutreachSentEmail {
112
+ /** The `Message-ID` header the message went out with, angle brackets included. */
113
+ messageId: string;
114
+ /** The provider's thread id. */
115
+ threadId: string;
116
+ /** The subject as sent. */
117
+ subject: string;
118
+ }
119
+ export interface OutreachStepCompletionInput {
120
+ enrollment: Pick<OutreachEnrollment, 'status' | 'stepIndex' | 'gmailThreadId' | 'gmailThreadIds' | 'messageIds' | 'threadSubject'>;
121
+ sequence: ScheduleSequence;
122
+ mailbox: ScheduleMailbox;
123
+ /** When the step ran. */
124
+ completedAtMs: number;
125
+ /** For an email step, what was sent; not read for a task. */
126
+ sent?: OutreachSentEmail | null;
127
+ random: OutreachRandom;
128
+ }
129
+ /** The fields a completed step writes. */
130
+ export interface OutreachStepCompletionPatch {
131
+ status: 'active' | 'finished';
132
+ stepIndex: number;
133
+ nextDueAtMs: number | null;
134
+ lastSentAtMs: number;
135
+ gmailThreadId?: string;
136
+ gmailThreadIds?: string[];
137
+ threadSubject?: string;
138
+ messageIds?: string[];
139
+ }
140
+ export interface OutreachStepCompletionResult {
141
+ patch: OutreachStepCompletionPatch | null;
142
+ /**
143
+ * Why there is no patch, or — beside one — that the next step could not
144
+ * be placed on the calendar (a window that never opens, a zone `Intl` does
145
+ * not know), which leaves the enrollment with nothing due until a member
146
+ * fixes the window and resumes it.
147
+ */
148
+ error: string | null;
149
+ }
150
+ /**
151
+ * What running the current step writes: the send recorded on the thread
152
+ * fields, and the next step scheduled — or the enrollment `finished` when
153
+ * that was the last.
154
+ */
155
+ export declare function planOutreachStepCompletion(input: OutreachStepCompletionInput): OutreachStepCompletionResult;
156
+ export {};