@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
@@ -1,427 +0,0 @@
1
- import type {
2
- MessageCategory,
3
- SenderTrust,
4
- ThreadSearchResponse,
5
- } from "@remit/api-openapi-types";
6
- import type { ResultList, ThreadMessageItem } from "@remit/data-ports";
7
- import { NotFoundError } from "@remit/data-ports/errors";
8
- import { isValidMessageId } from "@remit/data-ports/id";
9
- import type { APIGatewayProxyEvent } from "aws-lambda";
10
- import type { Context } from "openapi-backend";
11
- import { getAccountConfigIdFromEvent } from "../auth.js";
12
- import {
13
- type EnrichClient,
14
- enrichThreadRows,
15
- } from "../derive/enrichThreadRows.js";
16
- import {
17
- filterByOffRowCriteria,
18
- hasOffRowCriteria,
19
- } from "../derive/filterThreadCriteria.js";
20
- import { getClient } from "../service/data-client.js";
21
- import type {
22
- OperationHandler,
23
- ThreadDetailOperationIds,
24
- ThreadOperationIds,
25
- } from "../types.js";
26
-
27
- const DEFAULT_THREADS_PAGE_SIZE = 50;
28
-
29
- // Project to fields used by ListThreadsResponse — keep in sync with handler mapping.
30
- // Includes ThreadMessage table key fields (accountConfigId, threadMessageId) and
31
- // the lsi2 index components (mailboxId, sentDate) so ElectroDB can materialize
32
- // entities and pagination cursors from projected reads. accountConfigId +
33
- // fromEmail are required at read time to derive the From Address row for
34
- // senderTrust enrichment (see enrichThreadRows).
35
- const THREAD_LIST_ATTRIBUTES: ReadonlyArray<keyof ThreadMessageItem> = [
36
- "threadMessageId",
37
- "threadId",
38
- "messageId",
39
- "accountConfigId",
40
- "mailboxId",
41
- "fromEmail",
42
- "fromName",
43
- "subject",
44
- "sentDate",
45
- "isRead",
46
- "hasAttachment",
47
- "star",
48
- "hasStars",
49
- "isDeleted",
50
- "snippet",
51
- "category",
52
- "createdAt",
53
- "updatedAt",
54
- ];
55
-
56
- /**
57
- * Build the `listByMailbox` options for the thread list handler.
58
- *
59
- * Exposed as a pure helper so the `excludeDeleted: true` default — which is
60
- * the entire point of the #212 fix on the handler side — is testable without
61
- * standing up DynamoDB. The defaults must stay opt-out from the user (a
62
- * future Trash / All-Mail view can flip the flag) and opt-in at the
63
- * service layer.
64
- */
65
- export const buildListThreadsOptions = (query: {
66
- continuationToken?: string;
67
- order?: "asc" | "desc";
68
- }) => ({
69
- order: query.order ?? ("desc" as const),
70
- continuationToken: query.continuationToken,
71
- limit: DEFAULT_THREADS_PAGE_SIZE,
72
- attributes: [...THREAD_LIST_ATTRIBUTES],
73
- excludeDeleted: true,
74
- });
75
-
76
- /**
77
- * Build the `searchByMailboxWindow` options for the thread search handler.
78
- * Same #212 `excludeDeleted` default as `buildListThreadsOptions`. The caller's
79
- * `limit` is forwarded raw and clamped server-side (THREAD_SEARCH_MAX_LIMIT).
80
- */
81
- export const buildSearchThreadsOptions = (query: {
82
- continuationToken?: string;
83
- order?: "asc" | "desc";
84
- limit?: number;
85
- }) => ({
86
- order: query.order ?? ("desc" as const),
87
- continuationToken: query.continuationToken,
88
- limit: query.limit,
89
- attributes: [...THREAD_LIST_ATTRIBUTES],
90
- excludeDeleted: true,
91
- });
92
-
93
- const toArray = <T>(value: T | T[] | undefined): T[] | undefined => {
94
- if (value === undefined) return undefined;
95
- return Array.isArray(value) ? value : [value];
96
- };
97
-
98
- type WindowOptions = {
99
- order?: "asc" | "desc";
100
- limit?: number;
101
- continuationToken?: string;
102
- attributes?: Array<keyof ThreadMessageItem>;
103
- excludeDeleted?: boolean;
104
- };
105
-
106
- type ThreadSearch = {
107
- query?: string;
108
- subject?: string;
109
- from?: string;
110
- unread?: boolean;
111
- starred?: boolean;
112
- attachments?: boolean;
113
- category?: MessageCategory[];
114
- };
115
-
116
- /**
117
- * Minimal client surface `executeThreadSearch` needs. Declared structurally
118
- * (like `EnrichClient`) so the orchestration — index/window read, count, and
119
- * the off-row enrich/filter — is testable with an in-memory fake.
120
- */
121
- export interface ThreadSearchClient extends EnrichClient {
122
- threadMessage: {
123
- searchByMailboxWindow(
124
- accountConfigId: string,
125
- mailboxId: string,
126
- search: ThreadSearch,
127
- options?: WindowOptions,
128
- ): Promise<ResultList<ThreadMessageItem>>;
129
- countByMailbox(
130
- accountConfigId: string,
131
- mailboxId: string,
132
- search: ThreadSearch,
133
- options?: {
134
- excludeDeleted?: boolean;
135
- order?: "asc" | "desc";
136
- },
137
- ): Promise<number>;
138
- };
139
- }
140
-
141
- export type ThreadSearchParams = {
142
- continuationToken?: string;
143
- order?: "asc" | "desc";
144
- query?: string;
145
- subject?: string;
146
- from?: string;
147
- unread?: boolean;
148
- starred?: boolean;
149
- attachments?: boolean;
150
- senderTrust?: SenderTrust[];
151
- category?: MessageCategory[];
152
- dkimMismatch?: boolean;
153
- count?: boolean;
154
- results?: boolean;
155
- limit?: number;
156
- };
157
-
158
- /**
159
- * Run a per-mailbox thread search: select the index/window read, optionally
160
- * count, and resolve off-row criteria by enriching+filtering the window. The
161
- * `results` toggle omits `items` (count-only) and `count` adds the match count.
162
- *
163
- * `count` names the whole match, never the page: a page size bounds the rows a
164
- * response carries and has no bearing on how many messages match (#509).
165
- *
166
- * An off-row criterion is the one shape that has no such number. `senderTrust`
167
- * and `dkimMismatch` are resolved by enriching a window, so counting them means
168
- * enriching every candidate row in the mailbox. `count` is therefore absent
169
- * (#305) — a page's post-filter length labelled `count` is a page length
170
- * presented as a total, which is the defect the field exists to avoid. Absence
171
- * is the same answer the response already gives for a count nobody asked for,
172
- * so no caller needs a second shape to read it. A count-only request under an
173
- * off-row criterion therefore reads nothing at all.
174
- */
175
- export const executeThreadSearch = async (
176
- client: ThreadSearchClient,
177
- accountConfigId: string,
178
- mailboxId: string,
179
- params: ThreadSearchParams,
180
- ): Promise<ThreadSearchResponse> => {
181
- const search = {
182
- query: params.query,
183
- subject: params.subject,
184
- from: params.from,
185
- unread: params.unread,
186
- starred: params.starred,
187
- attachments: params.attachments,
188
- category: params.category,
189
- };
190
- const offRow = {
191
- senderTrust: params.senderTrust,
192
- dkimMismatch: params.dkimMismatch,
193
- };
194
- const wantCount = params.count === true;
195
- const wantResults = params.results !== false;
196
- const options = buildSearchThreadsOptions({
197
- continuationToken: params.continuationToken,
198
- order: params.order,
199
- limit: params.limit,
200
- });
201
-
202
- if (hasOffRowCriteria(offRow)) {
203
- if (!wantResults) return {};
204
- const window = await client.threadMessage.searchByMailboxWindow(
205
- accountConfigId,
206
- mailboxId,
207
- search,
208
- options,
209
- );
210
- const enriched = await enrichThreadRows(
211
- window.items,
212
- client,
213
- accountConfigId,
214
- );
215
- return {
216
- items: filterByOffRowCriteria(enriched, offRow),
217
- continuationToken: window.continuationToken,
218
- };
219
- }
220
-
221
- const response: ThreadSearchResponse = {};
222
-
223
- if (wantResults) {
224
- const window = await client.threadMessage.searchByMailboxWindow(
225
- accountConfigId,
226
- mailboxId,
227
- search,
228
- options,
229
- );
230
- response.items = await enrichThreadRows(
231
- window.items,
232
- client,
233
- accountConfigId,
234
- );
235
- response.continuationToken = window.continuationToken;
236
- }
237
-
238
- if (wantCount) {
239
- response.count = await client.threadMessage.countByMailbox(
240
- accountConfigId,
241
- mailboxId,
242
- search,
243
- { excludeDeleted: true, order: params.order },
244
- );
245
- }
246
-
247
- return response;
248
- };
249
-
250
- /**
251
- * Build the `listByThread` options for the thread-messages handler. Same
252
- * #212 default as the listing handlers — a soft-deleted message inside a
253
- * conversation should not surface in the conversation pane either.
254
- *
255
- * A conversation is never scoped to one mailbox: the user's own replies live
256
- * in Sent, filed messages live in their folder, and all of them belong to the
257
- * same thread (#46).
258
- *
259
- * The default order is `asc`: a conversation is read in the order it happened,
260
- * so a reply follows the message it answers (#81). The ordering is the query's,
261
- * not the handler's — `listByThread` sorts on sentDate with `threadMessageId`
262
- * breaking ties, so it holds across pages rather than within one response.
263
- */
264
- export const buildListThreadMessagesOptions = (query: {
265
- order?: "asc" | "desc";
266
- }) => ({
267
- order: query.order ?? ("asc" as const),
268
- excludeDeleted: true,
269
- });
270
-
271
- /**
272
- * Collapse rows that are the same piece of mail to one entry.
273
- *
274
- * A ThreadMessage row is keyed by (threadId, messageId), and a messageId is
275
- * derived from the account plus the RFC 5322 Message-ID, so the same mail
276
- * synced from two folders is one row — the mailbox a thread spans is not what
277
- * duplicates it. `MessageMoveService.copyMessage` is the exception: a copy is a
278
- * per-folder placement with its own derived id (`deriveCopyMessageId`), so a
279
- * copied message is a second row in the same thread carrying the same
280
- * `messageIdHeader`. Reading the whole thread rather than one mailbox of it
281
- * (#46) is what makes both rows visible at once, and two entries for one message
282
- * is never what a conversation should show.
283
- *
284
- * The oldest row wins, so the original outlives the copy, with `threadMessageId`
285
- * breaking ties. Both are independent of sort order, so `asc` and `desc` keep
286
- * the same message rather than each keeping a different copy of it.
287
- *
288
- * A row without a usable `messageIdHeader` is never merged: the fallback
289
- * identity for headerless mail is per-row by construction, so two such rows are
290
- * two distinct messages.
291
- */
292
- export const dedupeThreadMessages = <
293
- T extends Pick<
294
- ThreadMessageItem,
295
- "threadMessageId" | "messageIdHeader" | "createdAt"
296
- >,
297
- >(
298
- rows: T[],
299
- ): T[] => {
300
- const winners = new Map<string, T>();
301
-
302
- for (const row of rows) {
303
- if (!isValidMessageId(row.messageIdHeader)) continue;
304
- const header = (row.messageIdHeader as string).trim();
305
- const held = winners.get(header);
306
- if (
307
- !held ||
308
- row.createdAt < held.createdAt ||
309
- (row.createdAt === held.createdAt &&
310
- row.threadMessageId < held.threadMessageId)
311
- ) {
312
- winners.set(header, row);
313
- }
314
- }
315
-
316
- return rows.filter((row) => {
317
- if (!isValidMessageId(row.messageIdHeader)) return true;
318
- return winners.get((row.messageIdHeader as string).trim()) === row;
319
- });
320
- };
321
-
322
- export const ThreadOperations: Record<
323
- ThreadOperationIds,
324
- OperationHandler<ThreadOperationIds>
325
- > = {
326
- ThreadOperations_listThreads: async (
327
- context: Context,
328
- ...args: unknown[]
329
- ) => {
330
- const event = args[0] as APIGatewayProxyEvent;
331
- const accountConfigId = getAccountConfigIdFromEvent(event);
332
- const { mailboxId } = context.request.params as { mailboxId: string };
333
- const { continuationToken, order } = context.request.query as {
334
- continuationToken?: string;
335
- order?: "asc" | "desc";
336
- };
337
-
338
- const client = await getClient();
339
- const result = await client.threadMessage.listByMailbox(
340
- accountConfigId,
341
- mailboxId,
342
- buildListThreadsOptions({ continuationToken, order }),
343
- );
344
-
345
- const items = await enrichThreadRows(result.items, client, accountConfigId);
346
-
347
- return {
348
- items,
349
- continuationToken: result.continuationToken,
350
- };
351
- },
352
-
353
- ThreadOperations_searchThreads: async (
354
- context: Context,
355
- ...args: unknown[]
356
- ) => {
357
- const event = args[0] as APIGatewayProxyEvent;
358
- const accountConfigId = getAccountConfigIdFromEvent(event);
359
- const { mailboxId } = context.request.params as { mailboxId: string };
360
- const raw = context.request.query as {
361
- continuationToken?: string;
362
- order?: "asc" | "desc";
363
- query?: string;
364
- subject?: string;
365
- from?: string;
366
- unread?: boolean;
367
- starred?: boolean;
368
- attachments?: boolean;
369
- senderTrust?: SenderTrust | SenderTrust[];
370
- category?: MessageCategory | MessageCategory[];
371
- dkimMismatch?: boolean;
372
- count?: boolean;
373
- results?: boolean;
374
- limit?: number;
375
- };
376
-
377
- return executeThreadSearch(await getClient(), accountConfigId, mailboxId, {
378
- ...raw,
379
- senderTrust: toArray(raw.senderTrust),
380
- category: toArray(raw.category),
381
- });
382
- },
383
- };
384
-
385
- export const ThreadDetailOperations: Record<
386
- ThreadDetailOperationIds,
387
- OperationHandler<ThreadDetailOperationIds>
388
- > = {
389
- ThreadDetailOperations_listThreadMessages: async (
390
- context,
391
- ...args: unknown[]
392
- ) => {
393
- const event = args[0] as APIGatewayProxyEvent;
394
- const accountConfigId = getAccountConfigIdFromEvent(event);
395
- const { threadId } = context.request.params as { threadId: string };
396
- const { order } = context.request.query as {
397
- order?: "asc" | "desc";
398
- };
399
-
400
- const client = await getClient();
401
- const result = await client.threadMessage.listByThread(
402
- threadId,
403
- accountConfigId,
404
- buildListThreadMessagesOptions({ order }),
405
- );
406
-
407
- // Defense-in-depth: the query is already scoped to accountConfigId; assert
408
- // no foreign row slips into the response. `read`-class → 404, no existence
409
- // leak, consistent with the mailbox/message guards.
410
- for (const row of result.items) {
411
- if (row.accountConfigId !== accountConfigId) {
412
- throw new NotFoundError(`Thread not found: ${threadId}`);
413
- }
414
- }
415
-
416
- const items = await enrichThreadRows(
417
- dedupeThreadMessages(result.items),
418
- client,
419
- accountConfigId,
420
- );
421
-
422
- return {
423
- items,
424
- continuationToken: result.continuationToken,
425
- };
426
- },
427
- };
@@ -1,197 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { randomUUID } from "node:crypto";
3
- import { readFileSync } from "node:fs";
4
- import { afterEach, beforeEach, describe, it } from "node:test";
5
- import type { AccountItem } from "@remit/data-ports";
6
- import {
7
- AccountAuthType,
8
- AccountService,
9
- ConnectionState,
10
- MailboxCursorState,
11
- } from "@remit/domain-enums";
12
- import {
13
- AccountRepo,
14
- AccountSettingRepo,
15
- AddressRepo,
16
- DrizzleMessageRepository,
17
- DrizzleThreadMessageRepository,
18
- LabelRepo,
19
- MailboxRepo,
20
- MessageLabelRepo,
21
- } from "@remit/drizzle-service";
22
- import { createShippedSqliteDb } from "@remit/drizzle-service/test-sqlite";
23
- import type { APIGatewayProxyEvent } from "aws-lambda";
24
- import type { Context } from "openapi-backend";
25
- import { deriveAccountConfigId } from "../auth.js";
26
- import {
27
- _resetForTest,
28
- type RemitClient,
29
- setClient,
30
- } from "../service/data-client.js";
31
- import { ThreadOperations } from "./thread.js";
32
- import {
33
- buildInboxMailboxMap,
34
- executeUnifiedThreadListing,
35
- type UnifiedThreadClient,
36
- } from "./unified-threads.js";
37
-
38
- const SUB = "cognito-sub-1181";
39
- const ACCOUNT_CONFIG_ID = deriveAccountConfigId(SUB);
40
- const NOW = 1_700_000_000_000;
41
-
42
- let client: UnifiedThreadClient & RemitClient;
43
- let close: () => void;
44
-
45
- beforeEach(() => {
46
- const store = createShippedSqliteDb();
47
- store.sqlite.exec(
48
- readFileSync(
49
- new URL(
50
- "../../../../npm-scripts/sqlite-search-index.sql",
51
- import.meta.url,
52
- ),
53
- "utf8",
54
- ),
55
- );
56
- close = store.close;
57
- const db = store.db as never;
58
- client = {
59
- account: new AccountRepo(store.db),
60
- accountSetting: new AccountSettingRepo(store.db),
61
- mailbox: new MailboxRepo(store.db),
62
- address: new AddressRepo(store.db),
63
- message: new DrizzleMessageRepository(db),
64
- threadMessage: new DrizzleThreadMessageRepository(db),
65
- label: new LabelRepo(store.db),
66
- messageLabel: new MessageLabelRepo(store.db),
67
- } as unknown as UnifiedThreadClient & RemitClient;
68
- setClient(client);
69
- });
70
-
71
- afterEach(() => {
72
- _resetForTest();
73
- close();
74
- });
75
-
76
- const seedAccountWithInboxThread = async (
77
- syncedServices: AccountItem["syncedServices"],
78
- ): Promise<{ accountId: string; mailboxId: string; threadId: string }> => {
79
- const label = syncedServices.join("-");
80
- const account = await client.account.create({
81
- accountConfigId: ACCOUNT_CONFIG_ID,
82
- username: `${label}@example.com`,
83
- email: `${label}@example.com`,
84
- authType: AccountAuthType.OauthMicrosoft,
85
- syncedServices,
86
- imapHost: "outlook.office365.com",
87
- imapPort: 993,
88
- imapTls: true,
89
- imapStartTls: false,
90
- isActive: true,
91
- connectionState: ConnectionState.Authenticated,
92
- });
93
- const created = await client.mailbox.create({
94
- accountId: account.accountId,
95
- namespacePrefix: "",
96
- hierarchyDelimiter: "/",
97
- fullPath: "INBOX",
98
- uidValidity: 1,
99
- uidNext: 2,
100
- highestModseq: "0",
101
- messageCount: 1,
102
- unseenCount: 1,
103
- deletedCount: 0,
104
- totalSize: 10,
105
- lastSyncUid: 1,
106
- highWaterMarkUid: 1,
107
- lastMessageSyncAt: NOW,
108
- cursorState: MailboxCursorState.normal,
109
- } as Parameters<MailboxRepo["create"]>[0]);
110
- if (created.outcome !== "Created") throw new Error("INBOX path taken");
111
- const mailbox = created.mailbox;
112
- const messageId = randomUUID();
113
- await client.message.create({
114
- messageId,
115
- mailboxId: mailbox.mailboxId,
116
- uid: 1,
117
- sequenceNumber: 1,
118
- rfc822Size: 10,
119
- internalDate: NOW,
120
- envelopeId: randomUUID(),
121
- rootBodyPartId: randomUUID(),
122
- status: "active",
123
- syncStatus: "synced",
124
- });
125
- const threadId = randomUUID();
126
- await client.threadMessage.create({
127
- accountConfigId: ACCOUNT_CONFIG_ID,
128
- threadId,
129
- messageId,
130
- mailboxId: mailbox.mailboxId,
131
- uid: 1,
132
- referenceOrder: 0,
133
- internalDate: NOW,
134
- sentDate: NOW,
135
- subject: `hello from ${label}`,
136
- isRead: false,
137
- isDeleted: false,
138
- hasAttachment: false,
139
- hasStars: false,
140
- });
141
- return {
142
- accountId: account.accountId,
143
- mailboxId: mailbox.mailboxId,
144
- threadId,
145
- };
146
- };
147
-
148
- const unifiedThreadIds = async (searchText?: string): Promise<string[]> => {
149
- const response = await executeUnifiedThreadListing(
150
- client,
151
- ACCOUNT_CONFIG_ID,
152
- {
153
- starredOnly: false,
154
- searchText,
155
- count: false,
156
- results: true,
157
- },
158
- );
159
- return (response.items ?? []).map((item) => item.threadId).sort();
160
- };
161
-
162
- describe("a mail-off account in the unified surfaces (#1181)", () => {
163
- it("leaves the unified list and search, and its mailbox still opens directly", async () => {
164
- const mailOnly = await seedAccountWithInboxThread([AccountService.Mail]);
165
- const both = await seedAccountWithInboxThread([
166
- AccountService.Mail,
167
- AccountService.Calendar,
168
- ]);
169
- const calendarOnly = await seedAccountWithInboxThread([
170
- AccountService.Calendar,
171
- ]);
172
- const mailSources = [mailOnly.threadId, both.threadId].sort();
173
-
174
- assert.deepEqual(await unifiedThreadIds(), mailSources);
175
- assert.deepEqual(await unifiedThreadIds("hello"), mailSources);
176
-
177
- const scope = await buildInboxMailboxMap(ACCOUNT_CONFIG_ID, client);
178
- assert.equal(scope.starredMailboxIds.has(calendarOnly.mailboxId), false);
179
-
180
- const listThreads = ThreadOperations.ThreadOperations_listThreads as (
181
- context: Context,
182
- event: APIGatewayProxyEvent,
183
- ) => Promise<unknown>;
184
- const opened = (await listThreads(
185
- {
186
- request: { params: { mailboxId: calendarOnly.mailboxId }, query: {} },
187
- } as unknown as Context,
188
- {
189
- requestContext: { authorizer: { claims: { sub: SUB } } },
190
- } as unknown as APIGatewayProxyEvent,
191
- )) as { items: { threadId: string }[] };
192
- assert.deepEqual(
193
- opened.items.map((item) => item.threadId),
194
- [calendarOnly.threadId],
195
- );
196
- });
197
- });