@remit/backend 0.0.143 → 0.0.145

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 (279) hide show
  1. package/dist/dev-server/content-handler.d.ts +38 -0
  2. package/dist/dev-server/content-handler.js +46 -0
  3. package/dist/src/api.d.ts +5 -0
  4. package/dist/src/api.js +73 -0
  5. package/dist/src/auth.d.ts +24 -0
  6. package/dist/src/auth.js +81 -0
  7. package/dist/src/config/api-config.d.ts +20 -0
  8. package/dist/src/config/api-config.js +1 -0
  9. package/dist/src/config/from-env.d.ts +2 -0
  10. package/dist/src/config/from-env.js +65 -0
  11. package/dist/src/config/msoauth-secrets-manager.d.ts +2 -0
  12. package/dist/src/config/msoauth-secrets-manager.js +28 -0
  13. package/dist/src/config/msoauth.d.ts +19 -0
  14. package/dist/src/config/msoauth.js +6 -0
  15. package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
  16. package/dist/src/derive/autoMoved.js +49 -0
  17. package/dist/src/derive/contentSignature.d.ts +40 -0
  18. package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +23 -77
  19. package/dist/src/derive/contentUrl.d.ts +33 -0
  20. package/dist/src/derive/contentUrl.js +27 -0
  21. package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
  22. package/dist/src/derive/deriveMuted.js +7 -0
  23. package/dist/src/derive/enrichThreadRows.d.ts +118 -0
  24. package/dist/src/derive/enrichThreadRows.js +151 -0
  25. package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
  26. package/dist/src/derive/filterThreadCriteria.js +24 -0
  27. package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
  28. package/dist/src/derive/pendingMoveCounts.js +53 -0
  29. package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
  30. package/dist/src/derive/senderTrust.js +19 -0
  31. package/dist/src/error.d.ts +21 -0
  32. package/dist/src/error.js +85 -0
  33. package/dist/src/handlers/account-guards.d.ts +36 -0
  34. package/dist/src/handlers/account-guards.js +107 -0
  35. package/dist/src/handlers/account-oauth.d.ts +15 -0
  36. package/dist/src/handlers/account-oauth.js +334 -0
  37. package/dist/src/handlers/account-overrides.d.ts +53 -0
  38. package/dist/src/handlers/account-overrides.js +233 -0
  39. package/dist/src/handlers/account-signature.d.ts +28 -0
  40. package/dist/src/handlers/account-signature.js +85 -0
  41. package/dist/src/handlers/account.d.ts +7 -0
  42. package/dist/src/handlers/account.js +337 -0
  43. package/dist/src/handlers/address.d.ts +35 -0
  44. package/dist/src/handlers/address.js +133 -0
  45. package/dist/src/handlers/calendar-event.d.ts +70 -0
  46. package/dist/src/handlers/calendar-event.js +417 -0
  47. package/dist/src/handlers/calendar-feed.d.ts +47 -0
  48. package/dist/src/handlers/calendar-feed.js +184 -0
  49. package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
  50. package/dist/src/handlers/calendar-suggestion.js +204 -0
  51. package/dist/src/handlers/calendar.d.ts +116 -0
  52. package/dist/src/handlers/calendar.js +323 -0
  53. package/dist/src/handlers/config.d.ts +43 -0
  54. package/dist/src/handlers/config.js +164 -0
  55. package/dist/src/handlers/ensure-account-config.d.ts +9 -0
  56. package/dist/src/handlers/ensure-account-config.js +27 -0
  57. package/dist/src/handlers/filter.d.ts +3 -0
  58. package/dist/src/handlers/filter.js +88 -0
  59. package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
  60. package/dist/src/handlers/folder-role-appointments.js +168 -0
  61. package/dist/src/handlers/folder-role-labels.d.ts +26 -0
  62. package/dist/src/handlers/folder-role-labels.js +40 -0
  63. package/dist/src/handlers/folder-role.d.ts +9 -0
  64. package/dist/src/handlers/folder-role.js +37 -0
  65. package/dist/src/handlers/index.d.ts +88 -0
  66. package/dist/src/handlers/index.js +62 -0
  67. package/dist/src/handlers/label.d.ts +43 -0
  68. package/dist/src/handlers/label.js +107 -0
  69. package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
  70. package/dist/src/handlers/mailbox-bindings.js +61 -0
  71. package/dist/src/handlers/mailbox.d.ts +23 -0
  72. package/dist/src/handlers/mailbox.js +211 -0
  73. package/dist/src/handlers/me.d.ts +2 -0
  74. package/dist/src/handlers/me.js +117 -0
  75. package/dist/src/handlers/message.d.ts +82 -0
  76. package/dist/src/handlers/message.js +595 -0
  77. package/dist/src/handlers/organize.d.ts +10 -0
  78. package/dist/src/handlers/organize.js +103 -0
  79. package/dist/src/handlers/outbox-attachment.d.ts +25 -0
  80. package/dist/src/handlers/outbox-attachment.js +77 -0
  81. package/dist/src/handlers/outbox.d.ts +4 -0
  82. package/dist/src/handlers/outbox.js +136 -0
  83. package/dist/src/handlers/sync-progress.d.ts +46 -0
  84. package/dist/src/handlers/sync-progress.js +63 -0
  85. package/dist/src/handlers/sync.d.ts +28 -0
  86. package/dist/src/handlers/sync.js +73 -0
  87. package/dist/src/handlers/system-update.d.ts +2 -0
  88. package/dist/src/handlers/system-update.js +159 -0
  89. package/dist/src/handlers/thread.d.ts +148 -0
  90. package/dist/src/handlers/thread.js +228 -0
  91. package/dist/src/handlers/unified-threads.d.ts +247 -0
  92. package/dist/src/handlers/unified-threads.js +406 -0
  93. package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
  94. package/dist/src/handlers/vip-suggestions.js +16 -0
  95. package/dist/src/index.d.ts +5 -0
  96. package/dist/src/index.js +53 -0
  97. package/{src/json.ts → dist/src/json.d.ts} +1 -4
  98. package/dist/src/json.js +10 -0
  99. package/dist/src/jwt-auth.d.ts +15 -0
  100. package/dist/src/jwt-auth.js +45 -0
  101. package/dist/src/lambda.d.ts +13 -0
  102. package/dist/src/lambda.js +33 -0
  103. package/dist/src/request.d.ts +9 -0
  104. package/dist/src/request.js +17 -0
  105. package/dist/src/response.d.ts +44 -0
  106. package/dist/src/response.js +80 -0
  107. package/dist/src/serve.d.ts +31 -0
  108. package/dist/src/serve.js +117 -0
  109. package/dist/src/service/account-deletion.d.ts +20 -0
  110. package/dist/src/service/account-deletion.js +33 -0
  111. package/dist/src/service/compose-sqlite.d.ts +3 -0
  112. package/dist/src/service/compose-sqlite.js +13 -0
  113. package/dist/src/service/create-remit-client.d.ts +109 -0
  114. package/dist/src/service/create-remit-client.js +188 -0
  115. package/dist/src/service/data-client.d.ts +12 -0
  116. package/dist/src/service/data-client.js +42 -0
  117. package/dist/src/service/export-identity.d.ts +4 -0
  118. package/dist/src/service/export-identity.js +33 -0
  119. package/dist/src/service/fire-and-forget.d.ts +54 -0
  120. package/dist/src/service/fire-and-forget.js +37 -0
  121. package/dist/src/service/organize.d.ts +181 -0
  122. package/dist/src/service/organize.js +367 -0
  123. package/dist/src/service/queues.d.ts +12 -0
  124. package/dist/src/service/queues.js +1 -0
  125. package/dist/src/service/refusal.d.ts +4 -0
  126. package/dist/src/service/refusal.js +20 -0
  127. package/dist/src/service/sender-category.d.ts +51 -0
  128. package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
  129. package/dist/src/service/shared-deps.d.ts +6 -0
  130. package/dist/src/service/shared-deps.js +24 -0
  131. package/dist/src/service/trigger-sync.d.ts +57 -0
  132. package/dist/src/service/trigger-sync.js +31 -0
  133. package/dist/src/types.d.ts +72 -0
  134. package/dist/src/types.js +1 -0
  135. package/package.json +125 -98
  136. package/.env.test +0 -4
  137. package/dev-server/content-auth.test.ts +0 -113
  138. package/dev-server/content-auth.ts +0 -54
  139. package/dev-server/content-handler.test.ts +0 -105
  140. package/dev-server/content-handler.ts +0 -79
  141. package/dev-server/content-path.test.ts +0 -37
  142. package/dev-server/content-path.ts +0 -27
  143. package/dev-server/cors.test.ts +0 -48
  144. package/dev-server/cors.ts +0 -25
  145. package/dev-server/keep-alive.test.ts +0 -24
  146. package/dev-server/lambda-helpers.ts +0 -69
  147. package/dev-server/log-output.test.ts +0 -173
  148. package/dev-server/oauth-callback-route.test.ts +0 -66
  149. package/dev-server/relational-health.test.ts +0 -83
  150. package/dev-server/relational-health.ts +0 -39
  151. package/dev-server/server.ts +0 -384
  152. package/dev-server/sync-age.test.ts +0 -91
  153. package/dev-server/sync-age.ts +0 -55
  154. package/dev-server/upload-handler.ts +0 -132
  155. package/dev-server/upload-route.test.ts +0 -196
  156. package/scripts/backfill-classification.ts +0 -91
  157. package/scripts/backfill-list-id.ts +0 -83
  158. package/scripts/config-save.ts +0 -128
  159. package/src/auth.test.ts +0 -12
  160. package/src/auth.ts +0 -92
  161. package/src/config/msoauth.test.ts +0 -48
  162. package/src/config/msoauth.ts +0 -48
  163. package/src/data-backend.test.ts +0 -23
  164. package/src/data-backend.ts +0 -14
  165. package/src/derive/autoMoved.test.ts +0 -104
  166. package/src/derive/contentSignature.test.ts +0 -161
  167. package/src/derive/contentUrl.test.ts +0 -172
  168. package/src/derive/contentUrl.ts +0 -86
  169. package/src/derive/deriveMuted.test.ts +0 -34
  170. package/src/derive/enrichThreadRows.test.ts +0 -360
  171. package/src/derive/enrichThreadRows.ts +0 -234
  172. package/src/derive/filterThreadCriteria.test.ts +0 -126
  173. package/src/derive/filterThreadCriteria.ts +0 -51
  174. package/src/derive/senderTrust.test.ts +0 -67
  175. package/src/error.test.ts +0 -201
  176. package/src/error.ts +0 -123
  177. package/src/handlers/account-guards.test.ts +0 -67
  178. package/src/handlers/account-guards.ts +0 -169
  179. package/src/handlers/account-oauth-callback.test.ts +0 -155
  180. package/src/handlers/account-oauth-consent.test.ts +0 -336
  181. package/src/handlers/account-oauth-start.test.ts +0 -191
  182. package/src/handlers/account-oauth.test.ts +0 -439
  183. package/src/handlers/account-oauth.ts +0 -635
  184. package/src/handlers/account-overrides.test.ts +0 -88
  185. package/src/handlers/account-overrides.ts +0 -323
  186. package/src/handlers/account-ownership.ts +0 -25
  187. package/src/handlers/account-signature.ts +0 -117
  188. package/src/handlers/account-synced-services.test.ts +0 -245
  189. package/src/handlers/account.ts +0 -552
  190. package/src/handlers/address.test.ts +0 -381
  191. package/src/handlers/address.ts +0 -200
  192. package/src/handlers/calendar-event.test.ts +0 -685
  193. package/src/handlers/calendar-event.ts +0 -738
  194. package/src/handlers/calendar-feed-validators.test.ts +0 -228
  195. package/src/handlers/calendar-feed.test.ts +0 -439
  196. package/src/handlers/calendar-feed.ts +0 -291
  197. package/src/handlers/calendar-sqlite-fixture.ts +0 -36
  198. package/src/handlers/calendar-suggestion.test.ts +0 -1269
  199. package/src/handlers/calendar-suggestion.ts +0 -425
  200. package/src/handlers/calendar.test.ts +0 -1080
  201. package/src/handlers/calendar.ts +0 -517
  202. package/src/handlers/config.test.ts +0 -245
  203. package/src/handlers/config.ts +0 -332
  204. package/src/handlers/ensure-account-config.ts +0 -30
  205. package/src/handlers/filter.test.ts +0 -644
  206. package/src/handlers/filter.ts +0 -518
  207. package/src/handlers/folder-role-appointments.test.ts +0 -438
  208. package/src/handlers/folder-role-appointments.ts +0 -249
  209. package/src/handlers/folder-role-labels.ts +0 -63
  210. package/src/handlers/folder-role.test.ts +0 -209
  211. package/src/handlers/folder-role.ts +0 -80
  212. package/src/handlers/index.ts +0 -83
  213. package/src/handlers/label.test.ts +0 -112
  214. package/src/handlers/label.ts +0 -210
  215. package/src/handlers/mailbox-bindings.ts +0 -111
  216. package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
  217. package/src/handlers/mailbox-settled-target.test.ts +0 -251
  218. package/src/handlers/mailbox.test.ts +0 -280
  219. package/src/handlers/mailbox.ts +0 -581
  220. package/src/handlers/me.test.ts +0 -81
  221. package/src/handlers/me.ts +0 -203
  222. package/src/handlers/message-read-backfill.test.ts +0 -103
  223. package/src/handlers/message.test.ts +0 -131
  224. package/src/handlers/message.ts +0 -1131
  225. package/src/handlers/organize.test.ts +0 -277
  226. package/src/handlers/organize.ts +0 -197
  227. package/src/handlers/outbox-attachment.test.ts +0 -765
  228. package/src/handlers/outbox-attachment.ts +0 -126
  229. package/src/handlers/outbox-list.test.ts +0 -179
  230. package/src/handlers/outbox.test.ts +0 -772
  231. package/src/handlers/outbox.ts +0 -258
  232. package/src/handlers/search.test.ts +0 -161
  233. package/src/handlers/search.ts +0 -105
  234. package/src/handlers/sync-progress.test.ts +0 -205
  235. package/src/handlers/sync-progress.ts +0 -99
  236. package/src/handlers/sync.test.ts +0 -168
  237. package/src/handlers/sync.ts +0 -118
  238. package/src/handlers/system-update.test.ts +0 -497
  239. package/src/handlers/system-update.ts +0 -230
  240. package/src/handlers/thread.test.ts +0 -396
  241. package/src/handlers/thread.ts +0 -427
  242. package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
  243. package/src/handlers/unified-threads.test.ts +0 -785
  244. package/src/handlers/unified-threads.ts +0 -669
  245. package/src/index.test.ts +0 -83
  246. package/src/index.ts +0 -228
  247. package/src/jwt-auth.test.ts +0 -152
  248. package/src/jwt-auth.ts +0 -132
  249. package/src/log-redaction.test.ts +0 -144
  250. package/src/openapi-spec.test.ts +0 -118
  251. package/src/request-context.test.ts +0 -61
  252. package/src/request-context.ts +0 -33
  253. package/src/request-pipeline.test.ts +0 -88
  254. package/src/request.ts +0 -10
  255. package/src/response.test.ts +0 -130
  256. package/src/response.ts +0 -144
  257. package/src/service/compose-sqlite.ts +0 -98
  258. package/src/service/config-import.ts +0 -39
  259. package/src/service/create-remit-client.ts +0 -522
  260. package/src/service/data-client.test.ts +0 -97
  261. package/src/service/data-client.ts +0 -58
  262. package/src/service/export-identity.ts +0 -39
  263. package/src/service/filter.ts +0 -40
  264. package/src/service/fire-and-forget.test.ts +0 -126
  265. package/src/service/fire-and-forget.ts +0 -79
  266. package/src/service/localhost-env-config.test.ts +0 -36
  267. package/src/service/organize-queue.ts +0 -98
  268. package/src/service/organize.test.ts +0 -1451
  269. package/src/service/organize.ts +0 -1025
  270. package/src/service/read-path-filter-config.test.ts +0 -243
  271. package/src/service/semantic-capability.test.ts +0 -78
  272. package/src/service/semantic-capability.ts +0 -95
  273. package/src/service/sender-category.test.ts +0 -235
  274. package/src/service/sqs.ts +0 -4
  275. package/src/service/trigger-sync.test.ts +0 -205
  276. package/src/service/trigger-sync.ts +0 -100
  277. package/src/stdout-capture-fixture.ts +0 -40
  278. package/src/types.ts +0 -271
  279. package/tsconfig.json +0 -7
@@ -0,0 +1,107 @@
1
+ import { ownedAccount } from "@remit/mailbox-service";
2
+ import { unwrap } from "../service/refusal.js";
3
+ /**
4
+ * Every filter in the account whose action applies this label (issue #26). The
5
+ * count backs `LabelResponse.filterCount`; the list backs the cascade delete.
6
+ */
7
+ export const findFiltersForLabel = async (deps, accountConfigId, labelId) => {
8
+ const filters = await deps.filter.listByAccountConfig(accountConfigId);
9
+ return filters.filter((filter) => filter.actionLabelId === labelId);
10
+ };
11
+ /**
12
+ * `filterCount` is computed here, not stored (issue #26) — every other field
13
+ * comes straight off the `LabelItem` row.
14
+ */
15
+ const toLabelResponse = (item, filterCount) => ({
16
+ labelId: item.labelId,
17
+ accountConfigId: item.accountConfigId,
18
+ name: item.name,
19
+ color: item.color,
20
+ filterCount,
21
+ createdAt: item.createdAt,
22
+ updatedAt: item.updatedAt,
23
+ });
24
+ /**
25
+ * Delete a label and cascade in both directions (issue #26): every filter
26
+ * whose `actionLabelId` is this label is deleted outright — never left
27
+ * dangling, never blocking the delete — and every `MessageLabel` row for it is
28
+ * removed. The label row itself is deleted last so a failure mid-cascade never
29
+ * leaves the label gone while its filters or applied-to-messages still exist.
30
+ */
31
+ export const deleteLabelWithCascade = async (deps, accountConfigId, labelId) => {
32
+ const filters = await findFiltersForLabel(deps, accountConfigId, labelId);
33
+ for (const filter of filters) {
34
+ await deps.filter.delete(accountConfigId, filter.filterId);
35
+ }
36
+ await deps.messageLabel.removeAllByLabelId(accountConfigId, labelId);
37
+ await deps.label.delete(accountConfigId, labelId);
38
+ };
39
+ export const LabelOperations = {
40
+ LabelOperations_listLabels: async (context, ctx) => {
41
+ const accountConfigId = ctx.identity.accountConfigId;
42
+ const { accountId } = context.request.params;
43
+ const { continuationToken } = context.request.query;
44
+ const client = ctx.client;
45
+ const account = await client.account.get(accountId);
46
+ unwrap(ownedAccount(account, accountConfigId, "read"));
47
+ const page = await client.label.listPageByAccountConfig(accountConfigId, {
48
+ continuationToken,
49
+ });
50
+ const filters = await client.filter.listByAccountConfig(accountConfigId);
51
+ const filterCountByLabelId = new Map();
52
+ for (const filter of filters) {
53
+ const labelId = filter.actionLabelId;
54
+ filterCountByLabelId.set(labelId, (filterCountByLabelId.get(labelId) ?? 0) + 1);
55
+ }
56
+ return {
57
+ items: page.items.map((item) => toLabelResponse(item, filterCountByLabelId.get(item.labelId) ?? 0)),
58
+ continuationToken: page.continuationToken,
59
+ };
60
+ },
61
+ LabelOperations_createLabel: async (context, ctx) => {
62
+ const accountConfigId = ctx.identity.accountConfigId;
63
+ const { accountId } = context.request.params;
64
+ const input = context.request.requestBody;
65
+ const client = ctx.client;
66
+ const account = await client.account.get(accountId);
67
+ unwrap(ownedAccount(account, accountConfigId, "act"));
68
+ const label = await client.label.create({
69
+ accountConfigId,
70
+ name: input.name,
71
+ color: input.color,
72
+ });
73
+ return toLabelResponse(label, 0);
74
+ },
75
+ };
76
+ export const LabelDetailOperations = {
77
+ LabelDetailOperations_getLabel: async (context, ctx) => {
78
+ const accountConfigId = ctx.identity.accountConfigId;
79
+ const { accountId, labelId } = context.request.params;
80
+ const client = ctx.client;
81
+ const account = await client.account.get(accountId);
82
+ unwrap(ownedAccount(account, accountConfigId, "read"));
83
+ const label = await client.label.get(accountConfigId, labelId);
84
+ const filters = await findFiltersForLabel(client, accountConfigId, labelId);
85
+ return toLabelResponse(label, filters.length);
86
+ },
87
+ LabelDetailOperations_updateLabel: async (context, ctx) => {
88
+ const accountConfigId = ctx.identity.accountConfigId;
89
+ const { accountId, labelId } = context.request.params;
90
+ const body = context.request.requestBody;
91
+ const client = ctx.client;
92
+ const account = await client.account.get(accountId);
93
+ unwrap(ownedAccount(account, accountConfigId, "act"));
94
+ const updated = await client.label.update(accountConfigId, labelId, body);
95
+ const filters = await findFiltersForLabel(client, accountConfigId, labelId);
96
+ return toLabelResponse(updated, filters.length);
97
+ },
98
+ LabelDetailOperations_deleteLabel: async (context, ctx) => {
99
+ const accountConfigId = ctx.identity.accountConfigId;
100
+ const { accountId, labelId } = context.request.params;
101
+ const client = ctx.client;
102
+ const account = await client.account.get(accountId);
103
+ unwrap(ownedAccount(account, accountConfigId, "act"));
104
+ await deleteLabelWithCascade(client, accountConfigId, labelId);
105
+ return { statusCode: 204 };
106
+ },
107
+ };
@@ -0,0 +1,41 @@
1
+ import type { IAccountSettingRepository, IFilterRepository, MailboxItem } from "@remit/data-ports";
2
+ import type { CanonicalMailboxRoleValue } from "@remit/data-ports/folder-role";
3
+ /**
4
+ * What a folder is bound to: the filters that move mail into it, and the
5
+ * canonical roles it has been appointed to.
6
+ */
7
+ export interface MailboxBindings {
8
+ filterNames: string[];
9
+ roles: CanonicalMailboxRoleValue[];
10
+ }
11
+ /**
12
+ * `filter.actionMailboxId` has no reverse index, so the filters are listed for
13
+ * the account config and matched — they are few and per-config, so this is not
14
+ * a mailbox scan. Role appointments are `account_setting` rows whose *value* is
15
+ * a mailboxId, which a `mailbox_id` column scan cannot find, so the existing
16
+ * per-account appointment map answers for them.
17
+ */
18
+ export declare const loadMailboxBindings: (client: {
19
+ filter: Pick<IFilterRepository, "listByAccountConfig">;
20
+ accountSetting: Pick<IAccountSettingRepository, "get">;
21
+ }, accountConfigId: string, accountId: string, mailboxId: string) => Promise<MailboxBindings>;
22
+ /**
23
+ * Refuse a delete while any durable reference is bound to the folder (D16).
24
+ *
25
+ * This is what makes the settled-target rule total: a binding is only created
26
+ * against a settled folder and only removed by the user, so a `mailboxId` that
27
+ * names nothing never exists and no reader of a filter's `actionMailboxId` or a
28
+ * role appointment needs a missing-target branch. It removes code rather than
29
+ * adding a check.
30
+ *
31
+ * The alternatives both cost more than they look. `Filter` has no enabled flag
32
+ * and its `actionMailboxId` sentinel means "no move action", so clearing the
33
+ * target silently turns a move-filter into one that does something else;
34
+ * deleting the user's filters outright is not a decision a folder delete gets
35
+ * to make.
36
+ *
37
+ * The message **names** each binding rather than counting them, because a count
38
+ * is not actionable — and this fires on ordinary configurations, since pointing
39
+ * a filter at a folder is what filters are for.
40
+ */
41
+ export declare const assertNoBindings: (mailbox: Pick<MailboxItem, "fullPath">, bindings: MailboxBindings) => void;
@@ -0,0 +1,61 @@
1
+ import { FILTER_NO_ACTION } from "@remit/data-ports";
2
+ import { BadRequestError } from "@remit/data-ports/errors";
3
+ import { loadFolderAppointmentsForAccount } from "./folder-role-appointments.js";
4
+ /**
5
+ * `filter.actionMailboxId` has no reverse index, so the filters are listed for
6
+ * the account config and matched — they are few and per-config, so this is not
7
+ * a mailbox scan. Role appointments are `account_setting` rows whose *value* is
8
+ * a mailboxId, which a `mailbox_id` column scan cannot find, so the existing
9
+ * per-account appointment map answers for them.
10
+ */
11
+ export const loadMailboxBindings = async (client, accountConfigId, accountId, mailboxId) => {
12
+ const [filters, appointments] = await Promise.all([
13
+ client.filter.listByAccountConfig(accountConfigId),
14
+ loadFolderAppointmentsForAccount(client.accountSetting, accountConfigId, accountId),
15
+ ]);
16
+ const filterNames = filters
17
+ .filter((filter) => filter.actionMailboxId !== FILTER_NO_ACTION &&
18
+ filter.actionMailboxId === mailboxId)
19
+ .map((filter) => filter.name);
20
+ const roles = [];
21
+ for (const [role, appointment] of appointments) {
22
+ if (appointment.mailboxId === mailboxId) {
23
+ roles.push(role);
24
+ }
25
+ }
26
+ return { filterNames, roles };
27
+ };
28
+ const asList = (items) => items.length === 1
29
+ ? items[0]
30
+ : `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
31
+ /**
32
+ * Refuse a delete while any durable reference is bound to the folder (D16).
33
+ *
34
+ * This is what makes the settled-target rule total: a binding is only created
35
+ * against a settled folder and only removed by the user, so a `mailboxId` that
36
+ * names nothing never exists and no reader of a filter's `actionMailboxId` or a
37
+ * role appointment needs a missing-target branch. It removes code rather than
38
+ * adding a check.
39
+ *
40
+ * The alternatives both cost more than they look. `Filter` has no enabled flag
41
+ * and its `actionMailboxId` sentinel means "no move action", so clearing the
42
+ * target silently turns a move-filter into one that does something else;
43
+ * deleting the user's filters outright is not a decision a folder delete gets
44
+ * to make.
45
+ *
46
+ * The message **names** each binding rather than counting them, because a count
47
+ * is not actionable — and this fires on ordinary configurations, since pointing
48
+ * a filter at a folder is what filters are for.
49
+ */
50
+ export const assertNoBindings = (mailbox, bindings) => {
51
+ const clauses = [];
52
+ if (bindings.filterNames.length > 0) {
53
+ clauses.push(`the ${bindings.filterNames.length === 1 ? "filter" : "filters"} ${asList(bindings.filterNames.map((name) => `“${name}”`))}`);
54
+ }
55
+ if (bindings.roles.length > 0) {
56
+ clauses.push(`the ${asList(bindings.roles)} ${bindings.roles.length === 1 ? "role" : "roles"}`);
57
+ }
58
+ if (clauses.length === 0)
59
+ return;
60
+ throw new BadRequestError(`“${mailbox.fullPath}” is used by ${asList(clauses)}.`);
61
+ };
@@ -0,0 +1,23 @@
1
+ import type { RenameMailboxInput } from "@remit/api-openapi-types";
2
+ import type { IAccountSettingRepository, MailboxItem } from "@remit/data-ports";
3
+ import { type FolderPathDeps } from "@remit/mailbox-service";
4
+ import type { MailboxDetailOperationIds, MailboxOperationIds, OperationHandler, TrashOperationIds } from "../types.js";
5
+ /**
6
+ * The mute flag and the display-name override are user preferences that live
7
+ * in per-mailbox AccountSetting rows (RFC 032), not on the Mailbox entity.
8
+ * Pick only those keys from a PATCH body: each follows the same `null` → remove,
9
+ * value → set, absent/undefined → no-op semantics as UpdateAddressFlagsInput.
10
+ * The canonical role a folder fills is appointed separately — see
11
+ * FolderRoleOperations.appointFolderRole (RFC 032 exclusive-folder-appointment).
12
+ */
13
+ export declare const pickMailboxOverrideChanges: (body: RenameMailboxInput) => {
14
+ displayNameOverride?: string | null;
15
+ muted?: RenameMailboxInput["muted"];
16
+ };
17
+ export type MailboxPatchClient = FolderPathDeps & {
18
+ accountSetting: Pick<IAccountSettingRepository, "get" | "upsert" | "delete">;
19
+ };
20
+ export declare const applyMailboxPatch: (client: MailboxPatchClient, accountConfigId: string, mailboxId: string, accountId: string, body: RenameMailboxInput) => Promise<MailboxItem>;
21
+ export declare const MailboxOperations: Record<MailboxOperationIds, OperationHandler<MailboxOperationIds, "sync">>;
22
+ export declare const MailboxDetailOperations: Record<MailboxDetailOperationIds, OperationHandler<MailboxDetailOperationIds, "sync">>;
23
+ export declare const TrashOperations: Record<TrashOperationIds, OperationHandler<TrashOperationIds, "sync">>;
@@ -0,0 +1,211 @@
1
+ import { BadRequestError, ConflictError } from "@remit/data-ports/errors";
2
+ import { MessageSystemFlag } from "@remit/domain-enums";
3
+ import { changeFolderPath, createFolder, folderInAccount, folderInFlight, ownedAccount, ownedFolder, } from "@remit/mailbox-service";
4
+ import { applyPendingMoveCountPrediction, } from "../derive/pendingMoveCounts.js";
5
+ import { unwrap } from "../service/refusal.js";
6
+ import { applyMailboxOverrideChanges, loadMailboxOverrides, loadMailboxOverridesForConfig, } from "./account-overrides.js";
7
+ import { assertNoBindings, loadMailboxBindings } from "./mailbox-bindings.js";
8
+ /**
9
+ * The mute flag and the display-name override are user preferences that live
10
+ * in per-mailbox AccountSetting rows (RFC 032), not on the Mailbox entity.
11
+ * Pick only those keys from a PATCH body: each follows the same `null` → remove,
12
+ * value → set, absent/undefined → no-op semantics as UpdateAddressFlagsInput.
13
+ * The canonical role a folder fills is appointed separately — see
14
+ * FolderRoleOperations.appointFolderRole (RFC 032 exclusive-folder-appointment).
15
+ */
16
+ export const pickMailboxOverrideChanges = (body) => {
17
+ const changes = {};
18
+ if (Object.hasOwn(body, "displayNameOverride")) {
19
+ changes.displayNameOverride = body.displayNameOverride;
20
+ }
21
+ if (Object.hasOwn(body, "muted")) {
22
+ changes.muted = body.muted;
23
+ }
24
+ return changes;
25
+ };
26
+ // Overrides land first and independently, so an override-only PATCH never
27
+ // touches the folder's mutation state.
28
+ export const applyMailboxPatch = async (client, accountConfigId, mailboxId, accountId, body) => {
29
+ const { fullPath } = body;
30
+ const applyOverrides = async () => {
31
+ const changes = pickMailboxOverrideChanges(body);
32
+ if (Object.keys(changes).length === 0)
33
+ return;
34
+ await applyMailboxOverrideChanges(client.accountSetting, accountConfigId, mailboxId, changes);
35
+ };
36
+ if (!fullPath) {
37
+ const folder = unwrap(await ownedFolder(client, accountConfigId, accountId, mailboxId));
38
+ await applyOverrides();
39
+ return folder;
40
+ }
41
+ const { outcome, mailbox } = unwrap(await changeFolderPath(client, accountConfigId, {
42
+ accountId,
43
+ mailboxId,
44
+ fullPath,
45
+ }));
46
+ await applyOverrides();
47
+ if (outcome === "AlreadyRecorded") {
48
+ throw new ConflictError(folderInFlight(mailbox));
49
+ }
50
+ return mailbox;
51
+ };
52
+ /**
53
+ * Every pending placement move (issue #1271) for an account, on whichever
54
+ * backend is active (`RemitClient.placementMove` is present on both — see
55
+ * `create-remit-client.ts`). Read-only; feeds
56
+ * `applyPendingMoveCountPrediction`'s read-time adjustment only, never mutates
57
+ * stored counts (epic #1281 invariant 4). Markers are written by the
58
+ * imap-worker bulk sync path through `RemitClient.placementMove` on every
59
+ * backend, so this is a real signal on the self-host stack too.
60
+ */
61
+ const loadPendingMoves = (client, accountId) => client.placementMove.listByAccountId(accountId);
62
+ /**
63
+ * Every pending `\Seen` flag-push marker (issue #1273) for an account —
64
+ * `\Flagged` (star) markers are excluded, since only read/unread state feeds
65
+ * `unseenCount`'s prediction. `RemitClient.flagPush` is present and WRITTEN
66
+ * on both backends, so this is a real signal on the self-host stack too.
67
+ */
68
+ const loadPendingUnseenFlagPushes = async (client, accountId) => {
69
+ const pushes = await client.flagPush.listByAccountId(accountId);
70
+ return pushes
71
+ .filter((push) => push.flagName === MessageSystemFlag.Seen)
72
+ .map((push) => ({ mailboxId: push.mailboxId, operation: push.operation }));
73
+ };
74
+ const toMailboxResponse = (mailbox, overrides = {}) => ({
75
+ mailboxId: mailbox.mailboxId,
76
+ accountId: mailbox.accountId,
77
+ namespaceType: mailbox.namespaceType,
78
+ namespacePrefix: mailbox.namespacePrefix,
79
+ hierarchyDelimiter: mailbox.hierarchyDelimiter,
80
+ fullPath: mailbox.fullPath,
81
+ messageCount: mailbox.messageCount,
82
+ unseenCount: mailbox.unseenCount,
83
+ deletedCount: mailbox.deletedCount,
84
+ specialUse: mailbox.specialUse ? Array.from(mailbox.specialUse) : undefined,
85
+ lastSyncUid: mailbox.lastSyncUid,
86
+ highWaterMarkUid: mailbox.highWaterMarkUid,
87
+ lastMessageSyncAt: mailbox.lastMessageSyncAt,
88
+ syncStatus: mailbox.syncStatus,
89
+ // The row is the only thing that knows what a rename is aiming at, and
90
+ // `fullPath` stays the confirmed one throughout (D2), so without this a
91
+ // client cannot tell a create in flight from a rename in flight, name the
92
+ // target of a failed rename, or offer the right retry.
93
+ pendingPath: mailbox.pendingPath,
94
+ syncFailureReason: mailbox.syncFailureReason,
95
+ muted: overrides.muted,
96
+ displayNameOverride: overrides.displayNameOverride,
97
+ createdAt: mailbox.createdAt,
98
+ updatedAt: mailbox.updatedAt,
99
+ });
100
+ export const MailboxOperations = {
101
+ MailboxOperations_listMailboxes: async (context, ctx) => {
102
+ const accountConfigId = ctx.identity.accountConfigId;
103
+ const { accountId } = context.request.params;
104
+ const { continuationToken } = context.request.query;
105
+ const client = ctx.client;
106
+ const account = await client.account.get(accountId);
107
+ unwrap(ownedAccount(account, accountConfigId, "read"));
108
+ const result = await client.mailbox.listByAccount(accountId, {
109
+ continuationToken,
110
+ });
111
+ // Overrides (mute / display-name / role) live in per-mailbox AccountSetting
112
+ // rows (RFC 032). Load the whole config's set in one query and key it by
113
+ // mailboxId so each mailbox surfaces its overrides without an N+1.
114
+ const overridesByMailbox = await loadMailboxOverridesForConfig(client.accountSetting, accountConfigId);
115
+ // Read-time prediction for any pending placement move (issue #1271) and
116
+ // pending \Seen flag push (issue #1273) — adjusts messageCount /
117
+ // unseenCount only, never the stored row (epic #1281 invariant 4).
118
+ const pendingMoves = await loadPendingMoves(client, accountId);
119
+ const pendingUnseenFlagPushes = await loadPendingUnseenFlagPushes(client, accountId);
120
+ // A folder mid-mutation is listed, labelled (D11). Hiding a `deleting` one
121
+ // made a delete that does not settle look like a folder that silently
122
+ // vanished, while the server still held it.
123
+ const items = applyPendingMoveCountPrediction(result.items, pendingMoves, pendingUnseenFlagPushes);
124
+ return {
125
+ items: items.map((mailbox) => toMailboxResponse(mailbox, overridesByMailbox.get(mailbox.mailboxId) ?? {})),
126
+ continuationToken: result.continuationToken,
127
+ };
128
+ },
129
+ MailboxOperations_createMailbox: async (context, ctx) => {
130
+ const accountConfigId = ctx.identity.accountConfigId;
131
+ const { accountId } = context.request.params;
132
+ const { namespaceType, fullPath } = context.request
133
+ .requestBody;
134
+ const created = await createFolder(ctx.client, accountConfigId, {
135
+ accountId,
136
+ namespaceType,
137
+ fullPath,
138
+ });
139
+ const { outcome, mailbox } = unwrap(created);
140
+ if (outcome === "Existing") {
141
+ throw new BadRequestError(`A folder named “${fullPath}” is already there.`);
142
+ }
143
+ return toMailboxResponse(mailbox);
144
+ },
145
+ };
146
+ export const MailboxDetailOperations = {
147
+ MailboxDetailOperations_getMailbox: async (context, ctx) => {
148
+ const accountConfigId = ctx.identity.accountConfigId;
149
+ const { accountId, mailboxId } = context.request.params;
150
+ const client = ctx.client;
151
+ const account = await client.account.get(accountId);
152
+ unwrap(ownedAccount(account, accountConfigId, "read"));
153
+ const mailbox = await client.mailbox.get(accountId, mailboxId);
154
+ unwrap(folderInAccount(mailbox, accountId, "read"));
155
+ const overrides = await loadMailboxOverrides(client.accountSetting, accountConfigId, mailboxId);
156
+ // Read-time prediction for any pending placement move (issue #1271) and
157
+ // pending \Seen flag push (issue #1273) — adjusts messageCount /
158
+ // unseenCount only, never the stored row (epic #1281 invariant 4).
159
+ const pendingMoves = await loadPendingMoves(client, accountId);
160
+ const pendingUnseenFlagPushes = await loadPendingUnseenFlagPushes(client, accountId);
161
+ const adjusted = applyPendingMoveCountPrediction([mailbox], pendingMoves, pendingUnseenFlagPushes)[0] ?? mailbox;
162
+ return toMailboxResponse(adjusted, overrides);
163
+ },
164
+ MailboxDetailOperations_renameMailbox: async (context, ctx) => {
165
+ const accountConfigId = ctx.identity.accountConfigId;
166
+ const { accountId, mailboxId } = context.request.params;
167
+ const body = context.request.requestBody;
168
+ const client = ctx.client;
169
+ const mailbox = await applyMailboxPatch(client, accountConfigId, mailboxId, accountId, body);
170
+ const overrides = await loadMailboxOverrides(client.accountSetting, accountConfigId, mailboxId);
171
+ return toMailboxResponse(mailbox, overrides);
172
+ },
173
+ MailboxDetailOperations_deleteMailbox: async (context, ctx) => {
174
+ const accountConfigId = ctx.identity.accountConfigId;
175
+ const { accountId, mailboxId } = context.request.params;
176
+ const client = ctx.client;
177
+ const account = await client.account.get(accountId);
178
+ unwrap(ownedAccount(account, accountConfigId, "act"));
179
+ const mailbox = await client.mailbox.get(accountId, mailboxId);
180
+ unwrap(folderInAccount(mailbox, accountId, "act"));
181
+ // Cheapest first (D4, D13). None of the three becomes possible by
182
+ // retrying the same request, so all three are 400 and all three run
183
+ // before anything is recorded.
184
+ if (mailbox.fullPath.toUpperCase() === "INBOX") {
185
+ throw new BadRequestError("The inbox can't be deleted.");
186
+ }
187
+ const children = await client.mailbox.findByPathPrefix(accountId, mailbox.fullPath, mailbox.hierarchyDelimiter);
188
+ if (children.length > 0) {
189
+ // The server would keep the parent's name as a placeholder and leave the
190
+ // children untouched — an outcome no local row describes (D9).
191
+ throw new BadRequestError(`“${mailbox.fullPath}” has folders inside it. Delete those first.`);
192
+ }
193
+ assertNoBindings(mailbox, await loadMailboxBindings(client, accountConfigId, accountId, mailboxId));
194
+ await client.mailboxQueue.deleteMailbox(mailboxId, accountId);
195
+ return { statusCode: 204 };
196
+ },
197
+ };
198
+ export const TrashOperations = {
199
+ TrashOperations_emptyTrash: async (context, ctx) => {
200
+ const accountConfigId = ctx.identity.accountConfigId;
201
+ const { accountId } = context.request.params;
202
+ const client = ctx.client;
203
+ const account = await client.account.get(accountId);
204
+ unwrap(ownedAccount(account, accountConfigId, "act"));
205
+ // The service resolves Trash, marks the rows and counts them, all off one
206
+ // read, and refuses with a coded 409 when the role is unresolved. Counting
207
+ // here as well would be a second answer free to disagree with the one that
208
+ // acts, and "0 deleted" reads as success to a user whose Trash is untouched.
209
+ return client.messageMove.emptyTrash(accountConfigId, accountId);
210
+ },
211
+ };
@@ -0,0 +1,2 @@
1
+ import type { MeOperationIds, OperationHandler } from "../types.js";
2
+ export declare const MeOperations: Record<MeOperationIds, OperationHandler<MeOperationIds, "accountFanout">>;
@@ -0,0 +1,117 @@
1
+ import { BadRequestError, NotFoundError } from "@remit/data-ports/errors";
2
+ import { SYNC_WARNING_SURFACE_ATTEMPTS } from "@remit/data-ports/sync-warning-state";
3
+ import { logger } from "@remit/logger";
4
+ import { toVipSuggestionEntry } from "./vip-suggestions.js";
5
+ const DOWNLOAD_URL_TTL_SECONDS = 60 * 60;
6
+ const SURFACED_WARNING_CAP = 200;
7
+ export const MeOperations = {
8
+ MeOperations_listVipSuggestions: async (_context, ctx) => {
9
+ const accountConfigId = ctx.identity.accountConfigId;
10
+ const { address } = ctx.client;
11
+ const items = await address.listSuggestedVips({ accountConfigId });
12
+ return { suggestions: items.map(toVipSuggestionEntry) };
13
+ },
14
+ MeOperations_listQuarantine: async (_context, ctx) => {
15
+ const accountConfigId = ctx.identity.accountConfigId;
16
+ const { quarantine, syncWarning } = ctx.client;
17
+ const [entries, warnings] = await Promise.all([
18
+ quarantine.listByAccountConfigId(accountConfigId),
19
+ syncWarning.listSurfacedByAccountConfigId(accountConfigId, SYNC_WARNING_SURFACE_ATTEMPTS, SURFACED_WARNING_CAP),
20
+ ]);
21
+ return {
22
+ entries,
23
+ warnings: warnings.items,
24
+ warningsTotal: warnings.total,
25
+ };
26
+ },
27
+ MeOperations_deleteMe: async (context, ctx) => {
28
+ const accountConfigId = ctx.identity.accountConfigId;
29
+ const { confirmEmail } = context.request
30
+ .requestBody;
31
+ const callerEmail = ctx.identity.email;
32
+ if (typeof confirmEmail !== "string" || confirmEmail.trim() === "") {
33
+ throw new BadRequestError("confirmEmail is required");
34
+ }
35
+ if (confirmEmail.toLowerCase() !== callerEmail.toLowerCase()) {
36
+ throw new BadRequestError("confirmEmail does not match your account email");
37
+ }
38
+ const { accountConfig } = ctx.client;
39
+ try {
40
+ await accountConfig.update(accountConfigId, {
41
+ state: "deleting",
42
+ deletedAt: Date.now(),
43
+ });
44
+ }
45
+ catch (err) {
46
+ // If already in "deleting" state, treat as idempotent
47
+ if (err instanceof Error &&
48
+ (err.name === "ConditionalCheckFailedException" ||
49
+ err.message.includes("ConditionalCheckFailed"))) {
50
+ return {
51
+ statusCode: 202,
52
+ message: "Account deletion already in progress",
53
+ };
54
+ }
55
+ throw err;
56
+ }
57
+ await ctx.client.queues.accountFanout.send({
58
+ body: { type: "AccountDelete", accountConfigId },
59
+ });
60
+ // biome-ignore lint/plugin/no-logger-info: account deletion is an audit-grade signal
61
+ logger.info({ accountConfigId }, "Account deletion initiated");
62
+ return {
63
+ statusCode: 202,
64
+ message: "Account deletion initiated",
65
+ };
66
+ },
67
+ MeOperations_createExport: async (_context, ctx) => {
68
+ const accountConfigId = ctx.identity.accountConfigId;
69
+ const userId = ctx.identity.sub;
70
+ const { accountExportRequest } = ctx.client;
71
+ const exportRequest = await accountExportRequest.create({
72
+ accountConfigId,
73
+ userId,
74
+ state: "Pending",
75
+ });
76
+ await ctx.client.queues.accountFanout.send({
77
+ body: {
78
+ type: "AccountExport",
79
+ accountConfigId,
80
+ accountExportRequestId: exportRequest.accountExportRequestId,
81
+ },
82
+ });
83
+ // biome-ignore lint/plugin/no-logger-info: account export is an audit-grade signal
84
+ logger.info({
85
+ accountConfigId,
86
+ accountExportRequestId: exportRequest.accountExportRequestId,
87
+ }, "Account export initiated");
88
+ return {
89
+ statusCode: 202,
90
+ exportId: exportRequest.accountExportRequestId,
91
+ };
92
+ },
93
+ MeOperations_getExport: async (context, ctx) => {
94
+ const accountConfigId = ctx.identity.accountConfigId;
95
+ const exportId = context.request.params.exportId;
96
+ const { accountExportRequest, storage } = ctx.client;
97
+ const exportRequest = await accountExportRequest.get(exportId);
98
+ if (exportRequest.accountConfigId !== accountConfigId) {
99
+ throw new NotFoundError(`Export not found: ${exportId}`);
100
+ }
101
+ const downloadUrl = exportRequest.state === "Ready" && exportRequest.objectKey
102
+ ? await storage.getPresignedDownloadUrl(exportRequest.objectKey, DOWNLOAD_URL_TTL_SECONDS)
103
+ : undefined;
104
+ return {
105
+ accountExportRequestId: exportRequest.accountExportRequestId,
106
+ accountConfigId: exportRequest.accountConfigId,
107
+ userId: exportRequest.userId,
108
+ state: exportRequest.state,
109
+ createdAt: exportRequest.createdAt,
110
+ updatedAt: exportRequest.updatedAt,
111
+ objectKey: exportRequest.objectKey,
112
+ downloadUrl,
113
+ expiresAt: exportRequest.expiresAt,
114
+ errorMessage: exportRequest.errorMessage,
115
+ };
116
+ },
117
+ };
@@ -0,0 +1,82 @@
1
+ import type { BodyPartResponse } from "@remit/api-openapi-types";
2
+ import { type ContentSigner } from "../derive/contentSignature.js";
3
+ import type { MessageBulkOperationIds, MessageOperationIds, OperationHandler } from "../types.js";
4
+ export declare const isStorageNotFoundError: (error: unknown) => boolean;
5
+ /**
6
+ * Decode the raw `.eml` bytes into a string for inspection. RFC822/MIME
7
+ * sources are 7-bit/8-bit ASCII-compatible (non-ASCII bytes are encoded via
8
+ * quoted-printable / base64 inside the body), so `latin1` round-trips every
9
+ * byte 1:1 without loss or replacement characters — unlike `utf8`, which would
10
+ * mangle raw 8-bit bytes. Headers and structure stay byte-accurate.
11
+ */
12
+ export declare const decodeRawEml: (body: Buffer) => string;
13
+ /**
14
+ * Extract the accountConfigId + accountId segments from a bodyStorageKey URI
15
+ * such as
16
+ * `s3://bucket/accounts/{accountConfigId}/{accountId}/messages/{messageId}/body.eml`.
17
+ * Returns null when the URI shape doesn't match — caller falls back to the
18
+ * slow path without writing a parsed-body cache.
19
+ */
20
+ export declare const extractAccountIdsFromBodyKey: (uri: string) => {
21
+ accountConfigId: string;
22
+ accountId: string;
23
+ } | null;
24
+ /**
25
+ * Map a list of stored `BodyPart` rows to API `BodyPartResponse` objects,
26
+ * populating `contentUrl` from the CloudFront distribution domain. Pure
27
+ * function so the URL-construction contract can be pinned in tests without
28
+ * standing up the full handler. The contract requires every `contentUrl`
29
+ * to be a real URL (#299), so the API refuses to start without the domain.
30
+ */
31
+ export interface BodyPartLike {
32
+ bodyPartId: string;
33
+ mediaType: string;
34
+ mediaSubtype: string;
35
+ sizeOctets: number;
36
+ disposition?: string;
37
+ dispositionFilename?: string;
38
+ isMultipart: boolean;
39
+ contentId?: string;
40
+ partPath: string;
41
+ }
42
+ export declare const isMediaType: (value: unknown) => value is BodyPartResponse["mediaType"];
43
+ export declare const isContentDisposition: (value: unknown) => value is NonNullable<BodyPartResponse["disposition"]>;
44
+ export declare const FORWARDED_MESSAGE_FILENAME = "Forwarded message.eml";
45
+ export declare const buildBodyPartResponses: (parts: readonly BodyPartLike[], context: {
46
+ contentDeliveryDomain: string;
47
+ accountConfigId: string;
48
+ accountId: string;
49
+ messageId: string;
50
+ sign?: ContentSigner;
51
+ }) => BodyPartResponse[];
52
+ export interface SpamReportBulkFailure {
53
+ messageId: string;
54
+ reason: string;
55
+ }
56
+ export interface SpamReportBulkOutcome {
57
+ successCount: number;
58
+ failureCount: number;
59
+ failures?: SpamReportBulkFailure[];
60
+ }
61
+ /**
62
+ * The fallback for a failure nobody designed copy for — an AWS SDK message
63
+ * naming a queue URL, an `ECONNREFUSED host:port`, a programmer error.
64
+ * Everything reaching it is flattened, same policy as `error.ts`'s "Internal
65
+ * server error" flattening, because `SpamReportBulkResult.reason` is shown to
66
+ * the user as-is. It says "try again" because retrying is the only thing left
67
+ * to say about a cause we cannot name; an outcome the service DOES understand
68
+ * carries its own text through `DESIGNED_FAILURES` instead of landing here.
69
+ */
70
+ export declare const GENERIC_FAILURE_REASON = "This message could not be processed. Please try again. If it keeps failing, report it.";
71
+ /**
72
+ * Drive one report-spam/not-spam operation per message, concurrently. Each
73
+ * message's own wait for its move to settle (SpamReportService's R2 wait) is
74
+ * bounded on its own, but running the batch sequentially would let those
75
+ * waits accumulate across the whole request — 50 messages could each wait
76
+ * seconds, serializing into minutes and dying at the proxy. Running them
77
+ * concurrently instead bounds the whole request by a single message's wait,
78
+ * regardless of batch size.
79
+ */
80
+ export declare const settleSpamReportBulk: (messageIds: string[], run: (messageId: string) => Promise<void>) => Promise<SpamReportBulkOutcome>;
81
+ export declare const MessageOperations: Record<MessageOperationIds, OperationHandler<MessageOperationIds, "sync" | "messageManagement" | "body">>;
82
+ export declare const MessageBulkOperations: Record<MessageBulkOperationIds, OperationHandler<MessageBulkOperationIds, "sync" | "messageManagement" | "body">>;