@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,312 @@
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
+ */ const encode = (text)=>Buffer.from(text, 'utf8').toString('base64url');
17
+ /** A plain header block and body out of a raw RFC 5322 message. */ function parseRaw(raw) {
18
+ const text = Buffer.from(raw, 'base64url').toString('utf8');
19
+ const split = text.indexOf('\r\n\r\n');
20
+ const head = split >= 0 ? text.slice(0, split) : text;
21
+ const body = split >= 0 ? text.slice(split + 4) : '';
22
+ const headers = [];
23
+ for (const line of head.split('\r\n')){
24
+ if (/^[ \t]/.test(line) && headers.length) {
25
+ headers[headers.length - 1].value += ` ${line.trim()}`;
26
+ continue;
27
+ }
28
+ const colon = line.indexOf(':');
29
+ if (colon > 0) headers.push({
30
+ name: line.slice(0, colon),
31
+ value: line.slice(colon + 1).trim()
32
+ });
33
+ }
34
+ return {
35
+ headers,
36
+ body
37
+ };
38
+ }
39
+ const headerValue = (headers, name)=>{
40
+ var _ref;
41
+ var _headers_find;
42
+ return (_ref = (_headers_find = headers.find((header)=>header.name.toLowerCase() === name.toLowerCase())) == null ? void 0 : _headers_find.value) != null ? _ref : '';
43
+ };
44
+ const addressOf = (value)=>{
45
+ var _ref;
46
+ var _exec;
47
+ return ((_ref = (_exec = /<([^>]+)>/.exec(value)) == null ? void 0 : _exec[1]) != null ? _ref : value).trim().toLowerCase();
48
+ };
49
+ /** One `from:`/`to:` value, or a parenthesized `a OR b` list of them. */ function termValues(value) {
50
+ const inner = value.startsWith('(') && value.endsWith(')') ? value.slice(1, -1) : value;
51
+ return inner.split(/\s+OR\s+/).map((part)=>part.trim().toLowerCase()).filter(Boolean);
52
+ }
53
+ /** The query's terms, a parenthesized value kept whole. */ function terms(q) {
54
+ const found = [];
55
+ let current = '';
56
+ let depth = 0;
57
+ for (const character of q){
58
+ if (character === '(') depth += 1;
59
+ if (character === ')') depth -= 1;
60
+ if (character === ' ' && depth === 0) {
61
+ if (current) found.push(current);
62
+ current = '';
63
+ continue;
64
+ }
65
+ current += character;
66
+ }
67
+ if (current) found.push(current);
68
+ return found;
69
+ }
70
+ export class FakeGmail {
71
+ nextId(prefix) {
72
+ this.sequence += 1;
73
+ return `${prefix}${this.sequence.toString(16).padStart(6, '0')}`;
74
+ }
75
+ /** Files an inbound message, as the mailbox would receive it. */ deliver(input) {
76
+ var _input_threadId, _input_messageId, _input_headers, _input_text, _input_text1;
77
+ const id = this.nextId('in');
78
+ const threadId = (_input_threadId = input.threadId) != null ? _input_threadId : this.nextId('th');
79
+ const headers = [
80
+ {
81
+ name: 'From',
82
+ value: input.from
83
+ },
84
+ {
85
+ name: 'To',
86
+ value: input.to
87
+ },
88
+ {
89
+ name: 'Subject',
90
+ value: input.subject
91
+ },
92
+ {
93
+ name: 'Message-ID',
94
+ value: (_input_messageId = input.messageId) != null ? _input_messageId : `<${id}@mail.example.net>`
95
+ },
96
+ ...input.contentType ? [
97
+ {
98
+ name: 'Content-Type',
99
+ value: input.contentType
100
+ }
101
+ ] : [],
102
+ ...Object.entries((_input_headers = input.headers) != null ? _input_headers : {}).map(([name, value])=>({
103
+ name,
104
+ value
105
+ }))
106
+ ];
107
+ const payload = input.parts ? {
108
+ mimeType: 'multipart/report',
109
+ headers,
110
+ parts: input.parts
111
+ } : {
112
+ mimeType: 'text/plain',
113
+ headers,
114
+ body: {
115
+ data: encode((_input_text = input.text) != null ? _input_text : '')
116
+ }
117
+ };
118
+ const message = {
119
+ id,
120
+ threadId,
121
+ labelIds: [
122
+ 'INBOX'
123
+ ],
124
+ snippet: ((_input_text1 = input.text) != null ? _input_text1 : '').slice(0, 100),
125
+ internalDate: String(input.atMs),
126
+ payload
127
+ };
128
+ this.messages.set(id, {
129
+ message,
130
+ from: addressOf(input.from),
131
+ to: addressOf(input.to),
132
+ internalDateMs: input.atMs,
133
+ sent: false,
134
+ labelIds: [
135
+ 'INBOX'
136
+ ]
137
+ });
138
+ return message;
139
+ }
140
+ /** Every message the runtime sent, parsed. */ sentHeaders(index) {
141
+ var _ref;
142
+ var _this_sent_index;
143
+ return Object.fromEntries(((_ref = (_this_sent_index = this.sent[index]) == null ? void 0 : _this_sent_index.headers) != null ? _ref : []).map((header)=>[
144
+ header.name,
145
+ header.value
146
+ ]));
147
+ }
148
+ matches(stored, q) {
149
+ for (const term of terms(q)){
150
+ const negated = term.startsWith('-');
151
+ const [operator, ...rest] = (negated ? term.slice(1) : term).split(':');
152
+ const value = rest.join(':');
153
+ let hit;
154
+ if (operator === 'after') hit = stored.internalDateMs > Number(value) * 1000;
155
+ else if (operator === 'from') {
156
+ hit = value === 'me' ? this.self.has(stored.from) : termValues(value).some((wanted)=>stored.from.includes(wanted));
157
+ } else if (operator === 'to') hit = termValues(value).some((wanted)=>stored.to.includes(wanted));
158
+ else if (operator === 'rfc822msgid') {
159
+ var _ref;
160
+ var _stored_message_payload;
161
+ const header = headerValue((_ref = (_stored_message_payload = stored.message.payload) == null ? void 0 : _stored_message_payload.headers) != null ? _ref : [], 'Message-ID');
162
+ hit = header.replace(/^<|>$/g, '') === value;
163
+ } else hit = true;
164
+ if (negated ? hit : !hit) return false;
165
+ }
166
+ return true;
167
+ }
168
+ async getAccessToken() {
169
+ return 'fake-access-token';
170
+ }
171
+ async getProfile() {
172
+ var _;
173
+ return {
174
+ emailAddress: (_ = [
175
+ ...this.self
176
+ ][0]) != null ? _ : '',
177
+ messagesTotal: this.messages.size,
178
+ threadsTotal: 0,
179
+ historyId: '1'
180
+ };
181
+ }
182
+ async listSendAs() {
183
+ return [];
184
+ }
185
+ async sendMessage(input) {
186
+ var _input_threadId;
187
+ this.calls.push('send');
188
+ if (this.failNextSend) {
189
+ const failure = this.failNextSend;
190
+ this.failNextSend = null;
191
+ throw failure;
192
+ }
193
+ const { headers, body } = parseRaw(input.raw);
194
+ const id = this.nextId('out');
195
+ const threadId = input.threadId || this.nextId('th');
196
+ this.sent.push({
197
+ raw: input.raw,
198
+ threadId: (_input_threadId = input.threadId) != null ? _input_threadId : null,
199
+ headers,
200
+ body
201
+ });
202
+ const at = Date.now();
203
+ this.messages.set(id, {
204
+ message: {
205
+ id,
206
+ threadId,
207
+ labelIds: [
208
+ 'SENT'
209
+ ],
210
+ internalDate: String(at),
211
+ payload: {
212
+ mimeType: 'text/plain',
213
+ headers,
214
+ body: {
215
+ data: encode(body)
216
+ }
217
+ }
218
+ },
219
+ from: addressOf(headerValue(headers, 'From')),
220
+ to: addressOf(headerValue(headers, 'To')),
221
+ internalDateMs: at,
222
+ sent: true,
223
+ labelIds: [
224
+ 'SENT'
225
+ ]
226
+ });
227
+ return {
228
+ id,
229
+ threadId,
230
+ labelIds: [
231
+ 'SENT'
232
+ ]
233
+ };
234
+ }
235
+ async getThread(threadId) {
236
+ const full = await this.getFullThread(threadId);
237
+ return {
238
+ id: threadId,
239
+ historyId: null,
240
+ messages: full.messages.map((message)=>this.metadataOf(message))
241
+ };
242
+ }
243
+ async getFullThread(threadId) {
244
+ this.calls.push(`thread:${threadId}`);
245
+ const messages = [
246
+ ...this.messages.values()
247
+ ].filter((stored)=>stored.message.threadId === threadId).sort((a, b)=>a.internalDateMs - b.internalDateMs).map((stored)=>stored.message);
248
+ return {
249
+ id: threadId,
250
+ historyId: null,
251
+ messages
252
+ };
253
+ }
254
+ metadataOf(message) {
255
+ var _message_threadId, _message_labelIds, _message_snippet, _ref;
256
+ var _message_payload;
257
+ return {
258
+ id: message.id,
259
+ threadId: (_message_threadId = message.threadId) != null ? _message_threadId : '',
260
+ labelIds: (_message_labelIds = message.labelIds) != null ? _message_labelIds : [],
261
+ snippet: (_message_snippet = message.snippet) != null ? _message_snippet : '',
262
+ internalDateMs: Number(message.internalDate) || null,
263
+ headers: ((_ref = (_message_payload = message.payload) == null ? void 0 : _message_payload.headers) != null ? _ref : []).map((header)=>({
264
+ name: header.name,
265
+ value: header.value
266
+ }))
267
+ };
268
+ }
269
+ async getMessage(messageId) {
270
+ const stored = this.messages.get(messageId);
271
+ if (!stored) throw new Error(`no message ${messageId}`);
272
+ return this.metadataOf(stored.message);
273
+ }
274
+ async getFullMessage(messageId) {
275
+ this.calls.push(`message:${messageId}`);
276
+ const stored = this.messages.get(messageId);
277
+ if (!stored) throw new Error(`no message ${messageId}`);
278
+ return stored.message;
279
+ }
280
+ async listMessages(options) {
281
+ var _options_maxResults;
282
+ this.calls.push(`list:${options.q}`);
283
+ const found = [
284
+ ...this.messages.values()
285
+ ].filter((stored)=>this.matches(stored, options.q)).sort((a, b)=>b.internalDateMs - a.internalDateMs).slice(0, (_options_maxResults = options.maxResults) != null ? _options_maxResults : 100).map((stored)=>{
286
+ var _stored_message_threadId;
287
+ return {
288
+ id: stored.message.id,
289
+ threadId: (_stored_message_threadId = stored.message.threadId) != null ? _stored_message_threadId : ''
290
+ };
291
+ });
292
+ return {
293
+ messages: found,
294
+ nextPageToken: null,
295
+ resultSizeEstimate: found.length
296
+ };
297
+ }
298
+ async revoke() {
299
+ return 'revoked';
300
+ }
301
+ constructor(options){
302
+ var _options_failNextSend;
303
+ this.messages = new Map();
304
+ this.sent = [];
305
+ this.calls = [];
306
+ this.sequence = 0;
307
+ this.self = new Set(options.self.map((address)=>address.toLowerCase()));
308
+ this.failNextSend = (_options_failNextSend = options.failNextSend) != null ? _options_failNextSend : null;
309
+ }
310
+ }
311
+
312
+ //# sourceMappingURL=fake-gmail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/outreach/src/lib/runtime/fixtures/fake-gmail.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\nimport type { GmailApiMessage, GmailApiMessagePart } from '../../engine/thread-message'\nimport type { GmailClient, GmailMessageMetadata } from '../../transport/gmail-client'\n\n/**\n * A MAILBOX IN MEMORY, for the runtime's specs (AGL-2981).\n *\n * Implements the whole `GmailClient` the runtime reaches a mailbox through,\n * over messages held here: a send is parsed back out of the raw RFC 5322 it\n * was handed and filed in its thread, and mail \"arrives\" through\n * {@link FakeGmail.deliver}. The searches the runtime makes are answered with\n * the Gmail operators it uses — `after:`, `-from:me`, `from:`, `to:`,\n * `rfc822msgid:` — so a spec exercises the real queries. Nothing reaches\n * Google and nothing is sent anywhere.\n */\n\ninterface Stored {\n message: GmailApiMessage\n from: string\n to: string\n internalDateMs: number\n sent: boolean\n labelIds: string[]\n}\n\nconst encode = (text: string) => Buffer.from(text, 'utf8').toString('base64url')\n\n/** A plain header block and body out of a raw RFC 5322 message. */\nfunction parseRaw(raw: string): { headers: Array<{ name: string; value: string }>; body: string } {\n const text = Buffer.from(raw, 'base64url').toString('utf8')\n const split = text.indexOf('\\r\\n\\r\\n')\n const head = split >= 0 ? text.slice(0, split) : text\n const body = split >= 0 ? text.slice(split + 4) : ''\n const headers: Array<{ name: string; value: string }> = []\n for (const line of head.split('\\r\\n')) {\n if (/^[ \\t]/.test(line) && headers.length) {\n headers[headers.length - 1].value += ` ${line.trim()}`\n continue\n }\n const colon = line.indexOf(':')\n if (colon > 0) headers.push({ name: line.slice(0, colon), value: line.slice(colon + 1).trim() })\n }\n return { headers, body }\n}\n\nconst headerValue = (headers: Array<{ name: string; value: string }>, name: string) =>\n headers.find((header) => header.name.toLowerCase() === name.toLowerCase())?.value ?? ''\n\nconst addressOf = (value: string) => (/<([^>]+)>/.exec(value)?.[1] ?? value).trim().toLowerCase()\n\n/** One `from:`/`to:` value, or a parenthesized `a OR b` list of them. */\nfunction termValues(value: string): string[] {\n const inner = value.startsWith('(') && value.endsWith(')') ? value.slice(1, -1) : value\n return inner\n .split(/\\s+OR\\s+/)\n .map((part) => part.trim().toLowerCase())\n .filter(Boolean)\n}\n\n/** The query's terms, a parenthesized value kept whole. */\nfunction terms(q: string): string[] {\n const found: string[] = []\n let current = ''\n let depth = 0\n for (const character of q) {\n if (character === '(') depth += 1\n if (character === ')') depth -= 1\n if (character === ' ' && depth === 0) {\n if (current) found.push(current)\n current = ''\n continue\n }\n current += character\n }\n if (current) found.push(current)\n return found\n}\n\nexport interface FakeGmailOptions {\n /** The mailbox's own addresses: what `from:me` means. */\n self: readonly string[]\n /** Makes the next `sendMessage` throw this. */\n failNextSend?: Error | null\n}\n\nexport class FakeGmail implements GmailClient {\n readonly messages = new Map<string, Stored>()\n readonly sent: Array<{ raw: string; threadId: string | null; headers: Array<{ name: string; value: string }>; body: string }> = []\n readonly calls: string[] = []\n failNextSend: Error | null\n private readonly self: Set<string>\n private sequence = 0\n\n constructor(options: FakeGmailOptions) {\n this.self = new Set(options.self.map((address) => address.toLowerCase()))\n this.failNextSend = options.failNextSend ?? null\n }\n\n private nextId(prefix: string): string {\n this.sequence += 1\n return `${prefix}${this.sequence.toString(16).padStart(6, '0')}`\n }\n\n /** Files an inbound message, as the mailbox would receive it. */\n deliver(input: {\n threadId?: string\n from: string\n to: string\n subject: string\n text?: string\n atMs: number\n messageId?: string\n headers?: Record<string, string>\n /** A multipart/report's parts, for a delivery status notification. */\n parts?: GmailApiMessagePart[]\n contentType?: string\n }): GmailApiMessage {\n const id = this.nextId('in')\n const threadId = input.threadId ?? this.nextId('th')\n const headers = [\n { name: 'From', value: input.from },\n { name: 'To', value: input.to },\n { name: 'Subject', value: input.subject },\n { name: 'Message-ID', value: input.messageId ?? `<${id}@mail.example.net>` },\n ...(input.contentType ? [{ name: 'Content-Type', value: input.contentType }] : []),\n ...Object.entries(input.headers ?? {}).map(([name, value]) => ({ name, value })),\n ]\n const payload: GmailApiMessagePart = input.parts\n ? { mimeType: 'multipart/report', headers, parts: input.parts }\n : { mimeType: 'text/plain', headers, body: { data: encode(input.text ?? '') } }\n const message: GmailApiMessage = {\n id,\n threadId,\n labelIds: ['INBOX'],\n snippet: (input.text ?? '').slice(0, 100),\n internalDate: String(input.atMs),\n payload,\n }\n this.messages.set(id, {\n message,\n from: addressOf(input.from),\n to: addressOf(input.to),\n internalDateMs: input.atMs,\n sent: false,\n labelIds: ['INBOX'],\n })\n return message\n }\n\n /** Every message the runtime sent, parsed. */\n sentHeaders(index: number): Record<string, string> {\n return Object.fromEntries((this.sent[index]?.headers ?? []).map((header) => [header.name, header.value]))\n }\n\n private matches(stored: Stored, q: string): boolean {\n for (const term of terms(q)) {\n const negated = term.startsWith('-')\n const [operator, ...rest] = (negated ? term.slice(1) : term).split(':')\n const value = rest.join(':')\n let hit: boolean\n if (operator === 'after') hit = stored.internalDateMs > Number(value) * 1000\n else if (operator === 'from') {\n hit =\n value === 'me'\n ? this.self.has(stored.from)\n : termValues(value).some((wanted) => stored.from.includes(wanted))\n } else if (operator === 'to') hit = termValues(value).some((wanted) => stored.to.includes(wanted))\n else if (operator === 'rfc822msgid') {\n const header = headerValue(stored.message.payload?.headers ?? [], 'Message-ID')\n hit = header.replace(/^<|>$/g, '') === value\n } else hit = true\n if (negated ? hit : !hit) return false\n }\n return true\n }\n\n async getAccessToken() {\n return 'fake-access-token'\n }\n\n async getProfile() {\n return { emailAddress: [...this.self][0] ?? '', messagesTotal: this.messages.size, threadsTotal: 0, historyId: '1' }\n }\n\n async listSendAs() {\n return []\n }\n\n async sendMessage(input: { raw: string; threadId?: string | null }) {\n this.calls.push('send')\n if (this.failNextSend) {\n const failure = this.failNextSend\n this.failNextSend = null\n throw failure\n }\n const { headers, body } = parseRaw(input.raw)\n const id = this.nextId('out')\n const threadId = input.threadId || this.nextId('th')\n this.sent.push({ raw: input.raw, threadId: input.threadId ?? null, headers, body })\n const at = Date.now()\n this.messages.set(id, {\n message: {\n id,\n threadId,\n labelIds: ['SENT'],\n internalDate: String(at),\n payload: { mimeType: 'text/plain', headers, body: { data: encode(body) } },\n },\n from: addressOf(headerValue(headers, 'From')),\n to: addressOf(headerValue(headers, 'To')),\n internalDateMs: at,\n sent: true,\n labelIds: ['SENT'],\n })\n return { id, threadId, labelIds: ['SENT'] }\n }\n\n async getThread(threadId: string) {\n const full = await this.getFullThread(threadId)\n return { id: threadId, historyId: null, messages: full.messages.map((message) => this.metadataOf(message)) }\n }\n\n async getFullThread(threadId: string) {\n this.calls.push(`thread:${threadId}`)\n const messages = [...this.messages.values()]\n .filter((stored) => stored.message.threadId === threadId)\n .sort((a, b) => a.internalDateMs - b.internalDateMs)\n .map((stored) => stored.message)\n return { id: threadId, historyId: null, messages }\n }\n\n private metadataOf(message: GmailApiMessage): GmailMessageMetadata {\n return {\n id: message.id,\n threadId: message.threadId ?? '',\n labelIds: message.labelIds ?? [],\n snippet: message.snippet ?? '',\n internalDateMs: Number(message.internalDate) || null,\n headers: (message.payload?.headers ?? []).map((header) => ({ name: header.name, value: header.value })),\n }\n }\n\n async getMessage(messageId: string) {\n const stored = this.messages.get(messageId)\n if (!stored) throw new Error(`no message ${messageId}`)\n return this.metadataOf(stored.message)\n }\n\n async getFullMessage(messageId: string) {\n this.calls.push(`message:${messageId}`)\n const stored = this.messages.get(messageId)\n if (!stored) throw new Error(`no message ${messageId}`)\n return stored.message\n }\n\n async listMessages(options: { q: string; maxResults?: number; pageToken?: string | null }) {\n this.calls.push(`list:${options.q}`)\n const found = [...this.messages.values()]\n .filter((stored) => this.matches(stored, options.q))\n .sort((a, b) => b.internalDateMs - a.internalDateMs)\n .slice(0, options.maxResults ?? 100)\n .map((stored) => ({ id: stored.message.id, threadId: stored.message.threadId ?? '' }))\n return { messages: found, nextPageToken: null, resultSizeEstimate: found.length }\n }\n\n async revoke(): Promise<'revoked'> {\n return 'revoked'\n }\n}\n"],"names":["encode","text","Buffer","from","toString","parseRaw","raw","split","indexOf","head","slice","body","headers","line","test","length","value","trim","colon","push","name","headerValue","find","header","toLowerCase","addressOf","exec","termValues","inner","startsWith","endsWith","map","part","filter","Boolean","terms","q","found","current","depth","character","FakeGmail","nextId","prefix","sequence","padStart","deliver","input","id","threadId","to","subject","messageId","contentType","Object","entries","payload","parts","mimeType","data","message","labelIds","snippet","internalDate","String","atMs","messages","set","internalDateMs","sent","sentHeaders","index","fromEntries","matches","stored","term","negated","operator","rest","join","hit","Number","self","has","some","wanted","includes","replace","getAccessToken","getProfile","emailAddress","messagesTotal","size","threadsTotal","historyId","listSendAs","sendMessage","calls","failNextSend","failure","at","Date","now","getThread","full","getFullThread","metadataOf","values","sort","a","b","getMessage","get","Error","getFullMessage","listMessages","options","maxResults","nextPageToken","resultSizeEstimate","revoke","Map","Set","address"],"mappings":"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"}
@@ -0,0 +1,47 @@
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 { decideOutreachMailboxHealth, type OutreachMailboxHealthDecision } from '../engine/mailbox-health';
18
+ import type { OutreachMailbox, OutreachRecentSend } from '../model/outreach.types';
19
+ import type { OutreachRuntimeDeps } from './runtime-deps';
20
+ /** The digest a send is named by in `recentSends`. */
21
+ export declare function outreachSendDigest(messageId: string): string;
22
+ /** The window with one more send in it, the oldest dropped past the limit. */
23
+ export declare function withRecentSend(recent: readonly OutreachRecentSend[] | null | undefined, send: OutreachRecentSend): OutreachRecentSend[];
24
+ /** The engine's inputs, read off a mailbox as stored. */
25
+ export declare function outreachMailboxHealthInput(mailbox: Pick<OutreachMailbox, 'health' | 'timezone'>, nowMs: number): Parameters<typeof decideOutreachMailboxHealth>[0];
26
+ /** What one run learned about a mailbox, applied in one write. */
27
+ export interface OutreachMailboxHealthDelta {
28
+ /** Hard bounces, each with when it arrived and the send it reports, when known. */
29
+ bounces: Array<{
30
+ atMs: number;
31
+ messageId: string | null;
32
+ }>;
33
+ replies: number;
34
+ /** The latest complaint this run read, epoch ms. */
35
+ complaintAtMs: number | null;
36
+ }
37
+ export declare const emptyOutreachHealthDelta: () => OutreachMailboxHealthDelta;
38
+ /**
39
+ * Applies a run's evidence to a mailbox and pauses it when the engine says
40
+ * so — one transaction — then writes the activity line a pause earns.
41
+ * Answers the decision that paused it, or `null`.
42
+ */
43
+ export declare function applyOutreachMailboxHealth(deps: Pick<OutreachRuntimeDeps, 'firestore' | 'now' | 'logOrgActivity'>, input: {
44
+ orgId: string;
45
+ mailboxId: string;
46
+ delta: OutreachMailboxHealthDelta;
47
+ }): Promise<OutreachMailboxHealthDecision | null>;
@@ -0,0 +1,168 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { createHash } from "node:crypto";
18
+ import { decideOutreachMailboxHealth, OUTREACH_BOUNCE_RATE_WINDOW_SENDS } from "../engine/mailbox-health.js";
19
+ import { mailboxRef } from "../mailboxes/mailbox-credentials.js";
20
+ import { outreachLastSevenDays, outreachLocalDay } from "../mailboxes/mailbox-settings.js";
21
+ /**
22
+ * WHAT A MAILBOX'S HEALTH IS JUDGED ON, KEPT AS IT HAPPENS (AGL-2981).
23
+ *
24
+ * The engine's `decideOutreachMailboxHealth` is pure: it needs the hard
25
+ * bounces in the mailbox's current day, its last fifty sends and which of
26
+ * them bounced, and when a reply last called its mail spam. This is where
27
+ * the runtime keeps those on the mailbox document:
28
+ *
29
+ * - a send appends `{ id, atMs, bounced: false }` to `health.recentSends`,
30
+ * the id a digest of its `Message-ID`, never its recipient, and the list
31
+ * keeps the last {@link OUTREACH_BOUNCE_RATE_WINDOW_SENDS};
32
+ * - a hard bounce marks its send there, when the send is still in the
33
+ * window, and counts on `health.daily[<its local day>].bounces`;
34
+ * - a complaint moves `health.lastComplaintAtMs`.
35
+ *
36
+ * The health is judged when NEW evidence arrives — a bounce or a complaint
37
+ * this run recorded — and never otherwise. A member who re-checked the list
38
+ * and resumed the mailbox is not paused again by the evidence they already
39
+ * answered; the next bounce is new evidence, and is judged with the window
40
+ * it lands in.
41
+ *
42
+ * A pause is the mailbox's own: status `paused` with `autoPause` beside it,
43
+ * which the Mailboxes card shows as the reason, and a line on the
44
+ * organization's activity feed under `outreach:mailbox`.
45
+ */ /** The activity target a mailbox's rows are filed under (AGL-2978). */ const MAILBOX_TARGET = 'outreach:mailbox';
46
+ /** How many local days of `health.daily` the runtime keeps. */ const DAILY_DAYS_KEPT = 14;
47
+ /** The digest a send is named by in `recentSends`. */ export function outreachSendDigest(messageId) {
48
+ return createHash('sha256').update(String(messageId).trim()).digest('hex').slice(0, 16);
49
+ }
50
+ /** The window with one more send in it, the oldest dropped past the limit. */ export function withRecentSend(recent, send) {
51
+ return [
52
+ ...(recent != null ? recent : []).filter((entry)=>(entry == null ? void 0 : entry.id) !== send.id),
53
+ send
54
+ ].slice(-OUTREACH_BOUNCE_RATE_WINDOW_SENDS);
55
+ }
56
+ /** The engine's inputs, read off a mailbox as stored. */ export function outreachMailboxHealthInput(mailbox, nowMs) {
57
+ var _ref, _ref1, _ref2;
58
+ var _mailbox_health, _mailbox_health_daily_today, _mailbox_health_daily, _mailbox_health1, _mailbox_health2;
59
+ const recent = ((_ref = (_mailbox_health = mailbox.health) == null ? void 0 : _mailbox_health.recentSends) != null ? _ref : []).slice(-OUTREACH_BOUNCE_RATE_WINDOW_SENDS);
60
+ const today = outreachLocalDay(nowMs, mailbox.timezone);
61
+ return {
62
+ bouncesToday: Number((_ref1 = (_mailbox_health1 = mailbox.health) == null ? void 0 : (_mailbox_health_daily = _mailbox_health1.daily) == null ? void 0 : (_mailbox_health_daily_today = _mailbox_health_daily[today]) == null ? void 0 : _mailbox_health_daily_today.bounces) != null ? _ref1 : 0) || 0,
63
+ recentSends: recent.length,
64
+ recentHardBounces: recent.filter((send)=>(send == null ? void 0 : send.bounced) === true).length,
65
+ lastComplaintAtMs: (_ref2 = (_mailbox_health2 = mailbox.health) == null ? void 0 : _mailbox_health2.lastComplaintAtMs) != null ? _ref2 : null,
66
+ nowMs
67
+ };
68
+ }
69
+ /** The daily map with the days older than the runtime keeps dropped. */ function prunedDaily(daily, nowMs, timezone) {
70
+ const kept = new Set();
71
+ for(let week = 0; week * 7 < DAILY_DAYS_KEPT; week += 1){
72
+ for (const day of outreachLastSevenDays(nowMs - week * 7 * 86400000, timezone))kept.add(day);
73
+ }
74
+ return Object.fromEntries(Object.entries(daily != null ? daily : {}).filter(([day])=>kept.has(day)));
75
+ }
76
+ const bump = (daily, day, field, by = 1)=>{
77
+ var _daily_day;
78
+ const counts = (_daily_day = daily[day]) != null ? _daily_day : {
79
+ sent: 0,
80
+ bounces: 0,
81
+ replies: 0
82
+ };
83
+ daily[day] = _extends({}, counts, {
84
+ [field]: Math.max(0, Number(counts[field]) || 0) + by
85
+ });
86
+ };
87
+ export const emptyOutreachHealthDelta = ()=>({
88
+ bounces: [],
89
+ replies: 0,
90
+ complaintAtMs: null
91
+ });
92
+ /**
93
+ * Applies a run's evidence to a mailbox and pauses it when the engine says
94
+ * so — one transaction — then writes the activity line a pause earns.
95
+ * Answers the decision that paused it, or `null`.
96
+ */ export async function applyOutreachMailboxHealth(deps, input) {
97
+ const { delta } = input;
98
+ if (!delta.bounces.length && !delta.replies && delta.complaintAtMs === null) return null;
99
+ const firestore = deps.firestore();
100
+ const nowMs = deps.now();
101
+ const ref = mailboxRef(firestore, input.orgId, input.mailboxId);
102
+ const outcome = await firestore.runTransaction(async (transaction)=>{
103
+ var _ref, _ref1;
104
+ var _mailbox_health, _mailbox_health1, _mailbox_health2, _mailbox_health3, _mailbox_health4, _mailbox_health5;
105
+ const snapshot = await transaction.get(ref);
106
+ if (!snapshot.exists) return null;
107
+ const mailbox = _extends({}, snapshot.data(), {
108
+ id: snapshot.id
109
+ });
110
+ const timezone = mailbox.timezone;
111
+ const daily = prunedDaily((_mailbox_health = mailbox.health) == null ? void 0 : _mailbox_health.daily, nowMs, timezone);
112
+ let recent = [
113
+ ...(_ref = (_mailbox_health1 = mailbox.health) == null ? void 0 : _mailbox_health1.recentSends) != null ? _ref : []
114
+ ];
115
+ for (const bounce of delta.bounces){
116
+ bump(daily, outreachLocalDay(bounce.atMs, timezone), 'bounces');
117
+ if (bounce.messageId) {
118
+ const id = outreachSendDigest(bounce.messageId);
119
+ recent = recent.map((send)=>(send == null ? void 0 : send.id) === id ? _extends({}, send, {
120
+ bounced: true
121
+ }) : send);
122
+ }
123
+ }
124
+ if (delta.replies) bump(daily, outreachLocalDay(nowMs, timezone), 'replies', delta.replies);
125
+ const lastComplaintAtMs = delta.complaintAtMs === null ? (_ref1 = (_mailbox_health2 = mailbox.health) == null ? void 0 : _mailbox_health2.lastComplaintAtMs) != null ? _ref1 : null : Math.max(delta.complaintAtMs, Number((_mailbox_health3 = mailbox.health) == null ? void 0 : _mailbox_health3.lastComplaintAtMs) || 0);
126
+ const health = _extends({}, mailbox.health, {
127
+ bounces: Math.max(0, Number((_mailbox_health4 = mailbox.health) == null ? void 0 : _mailbox_health4.bounces) || 0) + delta.bounces.length,
128
+ replies: Math.max(0, Number((_mailbox_health5 = mailbox.health) == null ? void 0 : _mailbox_health5.replies) || 0) + delta.replies,
129
+ daily,
130
+ recentSends: recent,
131
+ lastComplaintAtMs
132
+ });
133
+ const judged = _extends({}, mailbox, {
134
+ health
135
+ });
136
+ const newEvidence = delta.bounces.length > 0 || delta.complaintAtMs !== null;
137
+ const decision = newEvidence && mailbox.status === 'connected' ? decideOutreachMailboxHealth(outreachMailboxHealthInput(judged, nowMs)) : null;
138
+ const update = {
139
+ health,
140
+ updatedAtMs: nowMs
141
+ };
142
+ if ((decision == null ? void 0 : decision.pause) && decision.reason && decision.message) {
143
+ update['status'] = 'paused';
144
+ update['autoPause'] = {
145
+ reason: decision.reason,
146
+ message: decision.message,
147
+ atMs: nowMs,
148
+ untilMs: decision.pausedUntilMs
149
+ };
150
+ }
151
+ transaction.update(ref, update);
152
+ return (decision == null ? void 0 : decision.pause) ? {
153
+ decision,
154
+ email: mailbox.email
155
+ } : null;
156
+ });
157
+ if (!outcome) return null;
158
+ await deps.logOrgActivity(input.orgId, {
159
+ uid: null
160
+ }, `Paused a mailbox in Sequences automatically: ${outcome.decision.message}`, {
161
+ type: MAILBOX_TARGET,
162
+ id: input.mailboxId,
163
+ name: outcome.email
164
+ });
165
+ return outcome.decision;
166
+ }
167
+
168
+ //# sourceMappingURL=mailbox-health-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/runtime/mailbox-health-store.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\nimport { createHash } from 'node:crypto'\nimport {\n decideOutreachMailboxHealth,\n OUTREACH_BOUNCE_RATE_WINDOW_SENDS,\n type OutreachMailboxHealthDecision,\n} from '../engine/mailbox-health'\nimport { mailboxRef } from '../mailboxes/mailbox-credentials'\nimport { outreachLastSevenDays, outreachLocalDay } from '../mailboxes/mailbox-settings'\nimport type {\n OutreachMailbox,\n OutreachMailboxDailyHealth,\n OutreachRecentSend,\n} from '../model/outreach.types'\nimport type { OutreachRuntimeDeps } from './runtime-deps'\n\n/**\n * WHAT A MAILBOX'S HEALTH IS JUDGED ON, KEPT AS IT HAPPENS (AGL-2981).\n *\n * The engine's `decideOutreachMailboxHealth` is pure: it needs the hard\n * bounces in the mailbox's current day, its last fifty sends and which of\n * them bounced, and when a reply last called its mail spam. This is where\n * the runtime keeps those on the mailbox document:\n *\n * - a send appends `{ id, atMs, bounced: false }` to `health.recentSends`,\n * the id a digest of its `Message-ID`, never its recipient, and the list\n * keeps the last {@link OUTREACH_BOUNCE_RATE_WINDOW_SENDS};\n * - a hard bounce marks its send there, when the send is still in the\n * window, and counts on `health.daily[<its local day>].bounces`;\n * - a complaint moves `health.lastComplaintAtMs`.\n *\n * The health is judged when NEW evidence arrives — a bounce or a complaint\n * this run recorded — and never otherwise. A member who re-checked the list\n * and resumed the mailbox is not paused again by the evidence they already\n * answered; the next bounce is new evidence, and is judged with the window\n * it lands in.\n *\n * A pause is the mailbox's own: status `paused` with `autoPause` beside it,\n * which the Mailboxes card shows as the reason, and a line on the\n * organization's activity feed under `outreach:mailbox`.\n */\n\n/** The activity target a mailbox's rows are filed under (AGL-2978). */\nconst MAILBOX_TARGET = 'outreach:mailbox' as const\n\n/** How many local days of `health.daily` the runtime keeps. */\nconst DAILY_DAYS_KEPT = 14\n\n/** The digest a send is named by in `recentSends`. */\nexport function outreachSendDigest(messageId: string): string {\n return createHash('sha256').update(String(messageId).trim()).digest('hex').slice(0, 16)\n}\n\n/** The window with one more send in it, the oldest dropped past the limit. */\nexport function withRecentSend(\n recent: readonly OutreachRecentSend[] | null | undefined,\n send: OutreachRecentSend,\n): OutreachRecentSend[] {\n return [...(recent ?? []).filter((entry) => entry?.id !== send.id), send].slice(\n -OUTREACH_BOUNCE_RATE_WINDOW_SENDS,\n )\n}\n\n/** The engine's inputs, read off a mailbox as stored. */\nexport function outreachMailboxHealthInput(\n mailbox: Pick<OutreachMailbox, 'health' | 'timezone'>,\n nowMs: number,\n): Parameters<typeof decideOutreachMailboxHealth>[0] {\n const recent = (mailbox.health?.recentSends ?? []).slice(-OUTREACH_BOUNCE_RATE_WINDOW_SENDS)\n const today = outreachLocalDay(nowMs, mailbox.timezone)\n return {\n bouncesToday: Number(mailbox.health?.daily?.[today]?.bounces ?? 0) || 0,\n recentSends: recent.length,\n recentHardBounces: recent.filter((send) => send?.bounced === true).length,\n lastComplaintAtMs: mailbox.health?.lastComplaintAtMs ?? null,\n nowMs,\n }\n}\n\n/** The daily map with the days older than the runtime keeps dropped. */\nfunction prunedDaily(\n daily: Record<string, OutreachMailboxDailyHealth> | undefined,\n nowMs: number,\n timezone: string,\n): Record<string, OutreachMailboxDailyHealth> {\n const kept = new Set<string>()\n for (let week = 0; week * 7 < DAILY_DAYS_KEPT; week += 1) {\n for (const day of outreachLastSevenDays(nowMs - week * 7 * 86_400_000, timezone)) kept.add(day)\n }\n return Object.fromEntries(Object.entries(daily ?? {}).filter(([day]) => kept.has(day)))\n}\n\nconst bump = (\n daily: Record<string, OutreachMailboxDailyHealth>,\n day: string,\n field: keyof OutreachMailboxDailyHealth,\n by = 1,\n) => {\n const counts = daily[day] ?? { sent: 0, bounces: 0, replies: 0 }\n daily[day] = { ...counts, [field]: Math.max(0, Number(counts[field]) || 0) + by }\n}\n\n/** What one run learned about a mailbox, applied in one write. */\nexport interface OutreachMailboxHealthDelta {\n /** Hard bounces, each with when it arrived and the send it reports, when known. */\n bounces: Array<{ atMs: number; messageId: string | null }>\n replies: number\n /** The latest complaint this run read, epoch ms. */\n complaintAtMs: number | null\n}\n\nexport const emptyOutreachHealthDelta = (): OutreachMailboxHealthDelta => ({\n bounces: [],\n replies: 0,\n complaintAtMs: null,\n})\n\n/**\n * Applies a run's evidence to a mailbox and pauses it when the engine says\n * so — one transaction — then writes the activity line a pause earns.\n * Answers the decision that paused it, or `null`.\n */\nexport async function applyOutreachMailboxHealth(\n deps: Pick<OutreachRuntimeDeps, 'firestore' | 'now' | 'logOrgActivity'>,\n input: { orgId: string; mailboxId: string; delta: OutreachMailboxHealthDelta },\n): Promise<OutreachMailboxHealthDecision | null> {\n const { delta } = input\n if (!delta.bounces.length && !delta.replies && delta.complaintAtMs === null) return null\n const firestore = deps.firestore()\n const nowMs = deps.now()\n const ref = mailboxRef(firestore, input.orgId, input.mailboxId)\n const outcome = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n if (!snapshot.exists) return null\n const mailbox = { ...(snapshot.data() as OutreachMailbox), id: snapshot.id }\n const timezone = mailbox.timezone\n const daily = prunedDaily(mailbox.health?.daily, nowMs, timezone)\n let recent = [...(mailbox.health?.recentSends ?? [])]\n for (const bounce of delta.bounces) {\n bump(daily, outreachLocalDay(bounce.atMs, timezone), 'bounces')\n if (bounce.messageId) {\n const id = outreachSendDigest(bounce.messageId)\n recent = recent.map((send) => (send?.id === id ? { ...send, bounced: true } : send))\n }\n }\n if (delta.replies) bump(daily, outreachLocalDay(nowMs, timezone), 'replies', delta.replies)\n const lastComplaintAtMs =\n delta.complaintAtMs === null\n ? (mailbox.health?.lastComplaintAtMs ?? null)\n : Math.max(delta.complaintAtMs, Number(mailbox.health?.lastComplaintAtMs) || 0)\n const health = {\n ...mailbox.health,\n bounces: Math.max(0, Number(mailbox.health?.bounces) || 0) + delta.bounces.length,\n replies: Math.max(0, Number(mailbox.health?.replies) || 0) + delta.replies,\n daily,\n recentSends: recent,\n lastComplaintAtMs,\n }\n const judged = { ...mailbox, health }\n const newEvidence = delta.bounces.length > 0 || delta.complaintAtMs !== null\n const decision =\n newEvidence && mailbox.status === 'connected'\n ? decideOutreachMailboxHealth(outreachMailboxHealthInput(judged, nowMs))\n : null\n const update: Record<string, unknown> = { health, updatedAtMs: nowMs }\n if (decision?.pause && decision.reason && decision.message) {\n update['status'] = 'paused'\n update['autoPause'] = {\n reason: decision.reason,\n message: decision.message,\n atMs: nowMs,\n untilMs: decision.pausedUntilMs,\n }\n }\n transaction.update(ref, update)\n return decision?.pause ? { decision, email: mailbox.email } : null\n })\n if (!outcome) return null\n await deps.logOrgActivity(\n input.orgId,\n { uid: null },\n `Paused a mailbox in Sequences automatically: ${outcome.decision.message}`,\n { type: MAILBOX_TARGET, id: input.mailboxId, name: outcome.email },\n )\n return outcome.decision\n}\n"],"names":["createHash","decideOutreachMailboxHealth","OUTREACH_BOUNCE_RATE_WINDOW_SENDS","mailboxRef","outreachLastSevenDays","outreachLocalDay","MAILBOX_TARGET","DAILY_DAYS_KEPT","outreachSendDigest","messageId","update","String","trim","digest","slice","withRecentSend","recent","send","filter","entry","id","outreachMailboxHealthInput","mailbox","nowMs","health","recentSends","today","timezone","bouncesToday","Number","daily","bounces","length","recentHardBounces","bounced","lastComplaintAtMs","prunedDaily","kept","Set","week","day","add","Object","fromEntries","entries","has","bump","field","by","counts","sent","replies","Math","max","emptyOutreachHealthDelta","complaintAtMs","applyOutreachMailboxHealth","deps","input","delta","firestore","now","ref","orgId","mailboxId","outcome","runTransaction","transaction","snapshot","get","exists","data","bounce","atMs","map","judged","newEvidence","decision","status","updatedAtMs","pause","reason","message","untilMs","pausedUntilMs","email","logOrgActivity","uid","type","name"],"mappings":"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"}
@@ -0,0 +1,39 @@
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 { PluginPersonEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure';
18
+ /**
19
+ * OUTREACH'S SHARE OF A PERSON ERASURE (AGL-2981).
20
+ *
21
+ * The person's ENROLLMENTS go: each names them — their address, their
22
+ * contact, the personal line a rep wrote about them, the attestations — and
23
+ * is about them. They are found by the address and by the contacts the
24
+ * erasure is deleting, and deleted whatever their status.
25
+ *
26
+ * Their DO-NOT-CONTACT entry STAYS, on the rule every suppression list
27
+ * follows through an erasure: it is keyed by `personKey`, carries no
28
+ * address, and is the one promise the workspace made the person that has to
29
+ * outlive their data. What in it could point back at them is cleared: the
30
+ * detail — a member's note, a bounce's diagnostic — and the enrollment it
31
+ * came from, which no longer exists.
32
+ *
33
+ * A dry run counts both and writes nothing.
34
+ */
35
+ type Firestore = FirebaseFirestore.Firestore;
36
+ export declare function createOutreachPersonEraser(deps: {
37
+ firestore(): Firestore;
38
+ }): PluginPersonEraser;
39
+ export {};