@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,785 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, test } from "node:test";
3
- import type { MessageCategory } from "@remit/api-openapi-types";
4
- import type {
5
- AccountItem,
6
- AccountSettingItem,
7
- MailboxItem,
8
- SearchOptions,
9
- } from "@remit/data-ports";
10
- import {
11
- buildInboxMailboxMap,
12
- buildListAllThreadsOptions,
13
- buildListStarredThreadsOptions,
14
- buildSearchAllThreadsOptions,
15
- dedupeByMessageId,
16
- executeUnifiedThreadListing,
17
- type InboxMapClient,
18
- type UnifiedThreadClient,
19
- type UnifiedThreadParams,
20
- } from "./unified-threads.js";
21
-
22
- // #44: Flagged filtered client-side over the newest 50 unified-inbox rows, so a
23
- // star outside that window — or in any folder that is not INBOX — never showed.
24
- // The starred scope is every non-muted mailbox, served by its own access
25
- // pattern.
26
-
27
- const CONFIG_ID = "cfg-1";
28
-
29
- const account = (accountId: string): AccountItem =>
30
- ({ accountId }) as unknown as AccountItem;
31
-
32
- const mailbox = (
33
- mailboxId: string,
34
- accountId: string,
35
- fullPath: string,
36
- specialUse?: string[],
37
- ): MailboxItem =>
38
- ({ mailboxId, accountId, fullPath, specialUse }) as unknown as MailboxItem;
39
-
40
- const mutedSetting = (
41
- name: "AccountMuted" | "MailboxMuted",
42
- targetId: string,
43
- ): AccountSettingItem =>
44
- ({
45
- name: `${name}#${targetId}`,
46
- value: { kind: "MutedFlag", value: { value: true, setAt: 0 } },
47
- }) as unknown as AccountSettingItem;
48
-
49
- const buildClient = (
50
- accounts: AccountItem[],
51
- mailboxesByAccount: Record<string, MailboxItem[]>,
52
- settings: AccountSettingItem[] = [],
53
- ): InboxMapClient =>
54
- ({
55
- account: { listMailSourcesByAccountConfig: async () => accounts },
56
- mailbox: {
57
- listAllByAccount: async (accountId: string) =>
58
- mailboxesByAccount[accountId] ?? [],
59
- },
60
- accountSetting: { listByAccountConfig: async () => settings },
61
- }) as unknown as InboxMapClient;
62
-
63
- describe("buildInboxMailboxMap", () => {
64
- test("separates the INBOX scope from the wider starred scope", async () => {
65
- const client = buildClient([account("a1")], {
66
- a1: [
67
- mailbox("m-inbox", "a1", "INBOX"),
68
- mailbox("m-archive", "a1", "Archive", ["Archive"]),
69
- mailbox("m-sub", "a1", "INBOX/Receipts"),
70
- ],
71
- });
72
-
73
- const { inboxMailboxIds, starredMailboxIds, mailboxIdToAccountId } =
74
- await buildInboxMailboxMap(CONFIG_ID, client);
75
-
76
- assert.deepEqual([...inboxMailboxIds], ["m-inbox"]);
77
- assert.deepEqual([...starredMailboxIds].sort(), [
78
- "m-archive",
79
- "m-inbox",
80
- "m-sub",
81
- ]);
82
- // The map must cover every mailbox, or a starred row from Archive could
83
- // not resolve its accountId.
84
- assert.equal(mailboxIdToAccountId.get("m-archive"), "a1");
85
- });
86
-
87
- // A star on mail in Spam or Trash is not something the starred view should
88
- // resurface; Gmail's All Mail is a second copy of everything, so including it
89
- // renders every starred Gmail message twice.
90
- test("the starred scope excludes All Mail, Junk and Trash", async () => {
91
- const client = buildClient([account("a1")], {
92
- a1: [
93
- mailbox("m-inbox", "a1", "INBOX"),
94
- mailbox("m-all", "a1", "[Gmail]/All Mail", ["All"]),
95
- mailbox("m-junk", "a1", "[Gmail]/Spam", ["Junk"]),
96
- mailbox("m-trash", "a1", "[Gmail]/Trash", ["Trash"]),
97
- mailbox("m-sent", "a1", "[Gmail]/Sent Mail", ["Sent"]),
98
- ],
99
- });
100
-
101
- const { starredMailboxIds, mailboxIdToAccountId } =
102
- await buildInboxMailboxMap(CONFIG_ID, client);
103
-
104
- assert.deepEqual([...starredMailboxIds].sort(), ["m-inbox", "m-sent"]);
105
- // Excluded mailboxes still resolve an accountId — the map is the wider set.
106
- assert.equal(mailboxIdToAccountId.get("m-trash"), "a1");
107
- });
108
-
109
- test("a mailbox carrying several special uses is excluded on any one of them", async () => {
110
- const client = buildClient([account("a1")], {
111
- a1: [mailbox("m-archive-all", "a1", "Archive", ["Archive", "All"])],
112
- });
113
-
114
- const { starredMailboxIds } = await buildInboxMailboxMap(CONFIG_ID, client);
115
- assert.equal(starredMailboxIds.size, 0);
116
- });
117
-
118
- // #49: the unified list backing the unscoped search was INBOX-only, so a
119
- // search from the daily brief could not see Archive, Sent, Spam or any
120
- // custom folder. The scope is defined by what it excludes — anything not on
121
- // that list is searched, Drafts included.
122
- test("the search scope reaches every folder but Trash", async () => {
123
- const client = buildClient([account("a1")], {
124
- a1: [
125
- mailbox("m-inbox", "a1", "INBOX"),
126
- mailbox("m-sub", "a1", "INBOX/Receipts"),
127
- mailbox("m-archive", "a1", "Archive", ["Archive"]),
128
- mailbox("m-sent", "a1", "[Gmail]/Sent Mail", ["Sent"]),
129
- mailbox("m-junk", "a1", "[Gmail]/Spam", ["Junk"]),
130
- mailbox("m-drafts", "a1", "[Gmail]/Drafts", ["Drafts"]),
131
- mailbox("m-custom", "a1", "Projects/Remit"),
132
- mailbox("m-trash", "a1", "[Gmail]/Trash", ["Trash"]),
133
- ],
134
- });
135
-
136
- const { searchMailboxIds } = await buildInboxMailboxMap(CONFIG_ID, client);
137
-
138
- assert.deepEqual(
139
- [...searchMailboxIds].sort(),
140
- [
141
- "m-archive",
142
- "m-custom",
143
- "m-drafts",
144
- "m-inbox",
145
- "m-junk",
146
- "m-sent",
147
- "m-sub",
148
- ],
149
- "Spam and Drafts are in scope; Trash is not",
150
- );
151
- });
152
-
153
- // Trash is matched by path too, for servers that do not advertise the
154
- // special-use attribute.
155
- test("Trash is excluded without a special-use attribute", async () => {
156
- const client = buildClient([account("a1")], {
157
- a1: [
158
- mailbox("m-inbox", "a1", "INBOX"),
159
- mailbox("m-trash", "a1", "[Gmail]/Trash"),
160
- // Whole path, never a prefix — a user's own folder is real mail.
161
- mailbox("m-trash-talk", "a1", "Trash talk"),
162
- ],
163
- });
164
-
165
- const { searchMailboxIds } = await buildInboxMailboxMap(CONFIG_ID, client);
166
- assert.deepEqual([...searchMailboxIds].sort(), ["m-inbox", "m-trash-talk"]);
167
- });
168
-
169
- // A row is keyed by (threadId, messageId), both mailbox-independent, so
170
- // Gmail's copies collapse into ONE row owned by whichever mailbox synced it
171
- // first. Barring the virtual folders from the scope would therefore delete
172
- // the only row such a message has — mail that exists and cannot be found.
173
- test("the virtual copies stay IN the search scope, so mail they own is findable", async () => {
174
- const client = buildClient([account("a1")], {
175
- a1: [
176
- mailbox("m-inbox", "a1", "INBOX"),
177
- mailbox("m-all", "a1", "[Gmail]/All Mail", ["All"]),
178
- mailbox("m-starred", "a1", "[Gmail]/Starred", ["Flagged"]),
179
- mailbox("m-important", "a1", "[Gmail]/Important", ["Important"]),
180
- ],
181
- });
182
-
183
- const { searchMailboxIds, virtualCopyMailboxIds } =
184
- await buildInboxMailboxMap(CONFIG_ID, client);
185
-
186
- assert.deepEqual(
187
- [...searchMailboxIds].sort(),
188
- ["m-all", "m-important", "m-inbox", "m-starred"],
189
- "nothing is barred — duplicates are dropped after the read instead",
190
- );
191
- assert.deepEqual(
192
- [...virtualCopyMailboxIds].sort(),
193
- ["m-all", "m-important", "m-starred"],
194
- "they are still identified, to decide which duplicate to drop",
195
- );
196
- });
197
-
198
- test("a user folder named like a virtual copy is not treated as one", async () => {
199
- const client = buildClient([account("a1")], {
200
- a1: [
201
- mailbox("m-starred-ideas", "a1", "Starred ideas"),
202
- mailbox("m-important-clients", "a1", "Important clients"),
203
- ],
204
- });
205
-
206
- const { virtualCopyMailboxIds } = await buildInboxMailboxMap(
207
- CONFIG_ID,
208
- client,
209
- );
210
- assert.equal(virtualCopyMailboxIds.size, 0);
211
- });
212
-
213
- // The starred scope drops Junk; the search scope keeps it. Two sets, two
214
- // judgements — a star in Spam is noise, a search for mail that landed in
215
- // Spam is the whole point.
216
- test("the search scope and the starred scope differ on Junk", async () => {
217
- const client = buildClient([account("a1")], {
218
- a1: [
219
- mailbox("m-inbox", "a1", "INBOX"),
220
- mailbox("m-junk", "a1", "[Gmail]/Spam", ["Junk"]),
221
- ],
222
- });
223
-
224
- const { searchMailboxIds, starredMailboxIds } = await buildInboxMailboxMap(
225
- CONFIG_ID,
226
- client,
227
- );
228
-
229
- assert.equal(searchMailboxIds.has("m-junk"), true);
230
- assert.equal(starredMailboxIds.has("m-junk"), false);
231
- });
232
-
233
- test("the search scope spans every non-muted account", async () => {
234
- const client = buildClient([account("a1"), account("a2")], {
235
- a1: [mailbox("m1-archive", "a1", "Archive", ["Archive"])],
236
- a2: [mailbox("m2-sent", "a2", "Sent", ["Sent"])],
237
- });
238
-
239
- const { searchMailboxIds } = await buildInboxMailboxMap(CONFIG_ID, client);
240
-
241
- assert.deepEqual([...searchMailboxIds].sort(), ["m1-archive", "m2-sent"]);
242
- });
243
-
244
- test("muted mailboxes are excluded from the search scope too", async () => {
245
- const client = buildClient(
246
- [account("a1")],
247
- {
248
- a1: [
249
- mailbox("m-inbox", "a1", "INBOX"),
250
- mailbox("m-muted", "a1", "Archive"),
251
- ],
252
- },
253
- [mutedSetting("MailboxMuted", "m-muted")],
254
- );
255
-
256
- const { searchMailboxIds } = await buildInboxMailboxMap(CONFIG_ID, client);
257
- assert.deepEqual([...searchMailboxIds], ["m-inbox"]);
258
- });
259
-
260
- test("muted mailboxes are excluded from both scopes", async () => {
261
- const client = buildClient(
262
- [account("a1")],
263
- {
264
- a1: [
265
- mailbox("m-inbox", "a1", "INBOX"),
266
- mailbox("m-muted", "a1", "Archive"),
267
- ],
268
- },
269
- [mutedSetting("MailboxMuted", "m-muted")],
270
- );
271
-
272
- const { inboxMailboxIds, starredMailboxIds } = await buildInboxMailboxMap(
273
- CONFIG_ID,
274
- client,
275
- );
276
-
277
- assert.deepEqual([...inboxMailboxIds], ["m-inbox"]);
278
- assert.deepEqual([...starredMailboxIds], ["m-inbox"]);
279
- });
280
-
281
- test("a muted account contributes no mailboxes", async () => {
282
- const client = buildClient(
283
- [account("a1")],
284
- { a1: [mailbox("m-inbox", "a1", "INBOX")] },
285
- [mutedSetting("AccountMuted", "a1")],
286
- );
287
-
288
- const { starredMailboxIds } = await buildInboxMailboxMap(CONFIG_ID, client);
289
- assert.equal(starredMailboxIds.size, 0);
290
- });
291
- });
292
-
293
- describe("buildListStarredThreadsOptions", () => {
294
- test("scopes to every supplied mailbox, not just the inbox", () => {
295
- const options = buildListStarredThreadsOptions(
296
- {},
297
- new Set(["m-inbox", "m-archive"]),
298
- );
299
-
300
- assert.deepEqual([...options.mailboxIds].sort(), ["m-archive", "m-inbox"]);
301
- assert.equal(options.order, "desc");
302
- assert.equal(options.excludeDeleted, true);
303
- });
304
-
305
- test("carries the caller's paging through", () => {
306
- const options = buildListStarredThreadsOptions(
307
- { continuationToken: "tok", order: "asc", limit: 10 },
308
- new Set(["m-inbox"]),
309
- );
310
-
311
- assert.equal(options.continuationToken, "tok");
312
- assert.equal(options.order, "asc");
313
- assert.equal(options.limit, 10);
314
- });
315
-
316
- test("defaults the page size to the unified default", () => {
317
- const starred = buildListStarredThreadsOptions({}, new Set(["m-inbox"]));
318
- const unified = buildListAllThreadsOptions({}, new Set(["m-inbox"]));
319
- assert.equal(starred.limit, unified.limit);
320
- });
321
- });
322
-
323
- // A backend that keys a row by its mailbox returns one row per Gmail copy, so
324
- // a starred, Important inbox message arrives four times and crowds genuine
325
- // matches off the page. Dropping the extras here — rather than barring the
326
- // virtual folders from the scope — keeps the message reachable when the only
327
- // row it has sits in one of them.
328
- describe("dedupeByMessageId", () => {
329
- const row = (messageId: string, mailboxId: string) => ({
330
- messageId,
331
- mailboxId,
332
- });
333
- const virtual = new Set(["m-all", "m-starred", "m-important"]);
334
- const isVirtual = (r: { mailboxId: string }) => virtual.has(r.mailboxId);
335
-
336
- test("a starred, Important Gmail inbox message collapses to one row", () => {
337
- const deduped = dedupeByMessageId(
338
- [
339
- row("msg-1", "m-inbox"),
340
- row("msg-1", "m-all"),
341
- row("msg-1", "m-starred"),
342
- row("msg-1", "m-important"),
343
- ],
344
- isVirtual,
345
- );
346
-
347
- assert.equal(deduped.length, 1);
348
- assert.equal(deduped[0].mailboxId, "m-inbox");
349
- });
350
-
351
- test("the real folder wins even when a virtual copy came first", () => {
352
- const deduped = dedupeByMessageId(
353
- [row("msg-1", "m-all"), row("msg-1", "m-archive")],
354
- isVirtual,
355
- );
356
-
357
- assert.deepEqual(
358
- deduped.map((r) => r.mailboxId),
359
- ["m-archive"],
360
- );
361
- });
362
-
363
- // The case the scope exclusion would have broken: nothing but a virtual
364
- // copy holds this message, so that row must survive.
365
- test("a message that exists only in a virtual folder is kept", () => {
366
- const deduped = dedupeByMessageId([row("msg-1", "m-starred")], isVirtual);
367
-
368
- assert.deepEqual(
369
- deduped.map((r) => r.mailboxId),
370
- ["m-starred"],
371
- );
372
- });
373
-
374
- test("the same mail in two real folders keeps the first, newest by order", () => {
375
- const deduped = dedupeByMessageId(
376
- [row("msg-1", "m-archive"), row("msg-1", "m-custom")],
377
- isVirtual,
378
- );
379
-
380
- assert.deepEqual(
381
- deduped.map((r) => r.mailboxId),
382
- ["m-archive"],
383
- );
384
- });
385
-
386
- test("distinct messages are all kept, in order", () => {
387
- const deduped = dedupeByMessageId(
388
- [
389
- row("msg-1", "m-inbox"),
390
- row("msg-2", "m-archive"),
391
- row("msg-3", "m-junk"),
392
- ],
393
- isVirtual,
394
- );
395
-
396
- assert.deepEqual(
397
- deduped.map((r) => r.messageId),
398
- ["msg-1", "msg-2", "msg-3"],
399
- );
400
- });
401
-
402
- test("an empty page stays empty", () => {
403
- assert.deepEqual(dedupeByMessageId([], isVirtual), []);
404
- });
405
- });
406
-
407
- describe("buildSearchAllThreadsOptions", () => {
408
- test("carries the whole search scope, not just the inbox", () => {
409
- const options = buildSearchAllThreadsOptions(
410
- {},
411
- new Set(["m-inbox", "m-archive", "m-junk"]),
412
- );
413
-
414
- assert.deepEqual([...options.mailboxIds].sort(), [
415
- "m-archive",
416
- "m-inbox",
417
- "m-junk",
418
- ]);
419
- assert.equal(options.order, "desc");
420
- assert.equal(options.excludeDeleted, true);
421
- });
422
-
423
- test("carries the caller's paging through", () => {
424
- const options = buildSearchAllThreadsOptions(
425
- { continuationToken: "tok", order: "asc", limit: 10 },
426
- new Set(["m-inbox"]),
427
- );
428
-
429
- assert.equal(options.continuationToken, "tok");
430
- assert.equal(options.order, "asc");
431
- assert.equal(options.limit, 10);
432
- });
433
-
434
- test("defaults the page size to the unified default", () => {
435
- const search = buildSearchAllThreadsOptions({}, new Set(["m-inbox"]));
436
- const unified = buildListAllThreadsOptions({}, new Set(["m-inbox"]));
437
- assert.equal(search.limit, unified.limit);
438
- });
439
- });
440
-
441
- // #308: the cross-account collections filtered and counted in the browser, over
442
- // the pages a client happened to have loaded. Both are the query's job now.
443
- describe("executeUnifiedThreadListing", () => {
444
- interface Call {
445
- mode: "listByDate" | "listByStarred" | "searchByDate" | "count";
446
- search?: SearchOptions;
447
- limit?: number;
448
- continuationToken?: string;
449
- inboxMailboxIds?: Set<string>;
450
- mailboxIds?: Set<string>;
451
- }
452
-
453
- const emptyPage = { items: [], continuationToken: undefined };
454
-
455
- const buildListingClient = (calls: Call[], count = 0) =>
456
- ({
457
- account: {
458
- listMailSourcesByAccountConfig: async () => [
459
- account("a1"),
460
- account("a2"),
461
- ],
462
- },
463
- mailbox: {
464
- listAllByAccount: async (accountId: string) =>
465
- accountId === "a1"
466
- ? [
467
- mailbox("m-inbox", "a1", "INBOX"),
468
- mailbox("m-archive", "a1", "Archive"),
469
- ]
470
- : [
471
- mailbox("m2-inbox", "a2", "INBOX"),
472
- mailbox("m2-archive", "a2", "Archive"),
473
- mailbox("m2-trash", "a2", "Trash", ["Trash"]),
474
- ],
475
- },
476
- accountSetting: { listByAccountConfig: async () => [] },
477
- message: { get: async () => [] },
478
- address: { getAddress: async () => [] },
479
- messageLabel: { listByMessageIds: async () => [] },
480
- label: { listByAccountConfig: async () => [] },
481
- threadMessage: {
482
- listByDate: async (
483
- _config: string,
484
- options?: { search?: SearchOptions; limit?: number },
485
- ) => {
486
- calls.push({ mode: "listByDate", ...options });
487
- return emptyPage;
488
- },
489
- listByStarred: async (
490
- _config: string,
491
- options?: { search?: SearchOptions; limit?: number },
492
- ) => {
493
- calls.push({ mode: "listByStarred", ...options });
494
- return emptyPage;
495
- },
496
- searchByDate: async (
497
- _config: string,
498
- search: SearchOptions,
499
- options?: { limit?: number },
500
- ) => {
501
- calls.push({ mode: "searchByDate", search, ...options });
502
- return emptyPage;
503
- },
504
- countThreadsInScope: async (
505
- _config: string,
506
- search: SearchOptions,
507
- options?: { mailboxIds?: Set<string> },
508
- ) => {
509
- calls.push({ mode: "count", search, ...options });
510
- return count;
511
- },
512
- },
513
- }) as unknown as UnifiedThreadClient;
514
-
515
- const params = (
516
- overrides: Partial<UnifiedThreadParams> = {},
517
- ): UnifiedThreadParams => ({
518
- starredOnly: false,
519
- count: false,
520
- results: true,
521
- ...overrides,
522
- });
523
-
524
- test("sends the row filters into the starred query", async () => {
525
- const calls: Call[] = [];
526
- await executeUnifiedThreadListing(buildListingClient(calls), CONFIG_ID, {
527
- ...params({ starredOnly: true }),
528
- category: ["social", "personal"] as MessageCategory[],
529
- unread: true,
530
- attachments: true,
531
- });
532
-
533
- assert.equal(calls.length, 1);
534
- assert.equal(calls[0].mode, "listByStarred");
535
- assert.deepEqual(calls[0].search, {
536
- starred: true,
537
- category: ["social", "personal"],
538
- unread: true,
539
- attachments: true,
540
- });
541
- });
542
-
543
- test("sends the row filters into the INBOX query", async () => {
544
- const calls: Call[] = [];
545
- await executeUnifiedThreadListing(buildListingClient(calls), CONFIG_ID, {
546
- ...params(),
547
- category: ["uncategorized"] as MessageCategory[],
548
- });
549
-
550
- assert.equal(calls[0].mode, "listByDate");
551
- assert.deepEqual(calls[0].search, { category: ["uncategorized"] });
552
- });
553
-
554
- test("sends the row filters into the search query", async () => {
555
- const calls: Call[] = [];
556
- await executeUnifiedThreadListing(buildListingClient(calls), CONFIG_ID, {
557
- ...params({ starredOnly: true }),
558
- searchText: "invoice",
559
- unread: true,
560
- });
561
-
562
- assert.equal(calls[0].mode, "searchByDate");
563
- assert.deepEqual(calls[0].search, {
564
- query: "invoice",
565
- starred: true,
566
- unread: true,
567
- });
568
- });
569
-
570
- // The defect the count replaces: a page-length figure grew with every press
571
- // of "load more" while the header presented it as a total.
572
- test("counts the same however far the caller has paged", async () => {
573
- const calls: Call[] = [];
574
- const first = await executeUnifiedThreadListing(
575
- buildListingClient(calls, 42),
576
- CONFIG_ID,
577
- params({ starredOnly: true, count: true, unread: true }),
578
- );
579
- const later = await executeUnifiedThreadListing(
580
- buildListingClient(calls, 42),
581
- CONFIG_ID,
582
- params({
583
- starredOnly: true,
584
- count: true,
585
- unread: true,
586
- continuationToken: "page-3",
587
- limit: 10,
588
- }),
589
- );
590
-
591
- assert.equal(first.count, 42);
592
- assert.equal(later.count, 42);
593
- });
594
-
595
- test("reads the count alone when results are not wanted", async () => {
596
- const calls: Call[] = [];
597
- const response = await executeUnifiedThreadListing(
598
- buildListingClient(calls, 7),
599
- CONFIG_ID,
600
- params({ starredOnly: true, count: true, results: false }),
601
- );
602
-
603
- assert.deepEqual(
604
- calls.map((call) => call.mode),
605
- ["count"],
606
- );
607
- assert.equal(response.items, undefined);
608
- assert.equal(response.count, 7);
609
- });
610
-
611
- test("omits the count when it was not asked for", async () => {
612
- const calls: Call[] = [];
613
- const response = await executeUnifiedThreadListing(
614
- buildListingClient(calls, 7),
615
- CONFIG_ID,
616
- params({ starredOnly: true }),
617
- );
618
-
619
- assert.equal(response.count, undefined);
620
- assert.deepEqual(response.items, []);
621
- });
622
-
623
- // #1128: the one free-text parameter matches subject and From at once, so
624
- // `from:` and `subject:` could not be asked for separately and the Flagged
625
- // view applied both over the pages it had already fetched.
626
- test("sends from and subject as their own terms", async () => {
627
- const calls: Call[] = [];
628
- await executeUnifiedThreadListing(buildListingClient(calls), CONFIG_ID, {
629
- ...params({ starredOnly: true }),
630
- from: "alice",
631
- subject: "invoice",
632
- });
633
-
634
- assert.deepEqual(calls[0].search, {
635
- from: "alice",
636
- subject: "invoice",
637
- starred: true,
638
- });
639
- });
640
-
641
- test("blank field text asks for nothing", async () => {
642
- const calls: Call[] = [];
643
- await executeUnifiedThreadListing(buildListingClient(calls), CONFIG_ID, {
644
- ...params(),
645
- from: "",
646
- subject: "",
647
- });
648
-
649
- assert.deepEqual(calls[0].search, {});
650
- });
651
-
652
- // #1137: the count answered a wider set than the list, because the browser
653
- // dropped muted senders after the rows arrived and nothing told the server.
654
- test("the muted term reaches the listing and the count alike", async () => {
655
- const calls: Call[] = [];
656
- await executeUnifiedThreadListing(buildListingClient(calls), CONFIG_ID, {
657
- ...params({ count: true }),
658
- muted: false,
659
- });
660
-
661
- const listing = calls.find((call) => call.mode === "listByDate");
662
- const counting = calls.find((call) => call.mode === "count");
663
- assert.deepEqual(listing?.search, { muted: false });
664
- assert.deepEqual(counting?.search, { muted: false });
665
- });
666
-
667
- test("an unstated muted term filters nothing", async () => {
668
- const calls: Call[] = [];
669
- await executeUnifiedThreadListing(
670
- buildListingClient(calls),
671
- CONFIG_ID,
672
- params(),
673
- );
674
-
675
- assert.equal(Object.hasOwn(calls[0].search ?? {}, "muted"), false);
676
- });
677
-
678
- // #1136: the account pill narrowed the rows after they arrived, so a count
679
- // over every account was not the size of a list showing one and the brief
680
- // withheld every number rather than overstate.
681
- test("an account scope narrows the listing and the count to that account", async () => {
682
- const calls: Call[] = [];
683
- await executeUnifiedThreadListing(
684
- buildListingClient(calls, 3),
685
- CONFIG_ID,
686
- params({ accountId: "a2", count: true }),
687
- );
688
-
689
- const listing = calls.find((call) => call.mode === "listByDate");
690
- const counting = calls.find((call) => call.mode === "count");
691
- assert.deepEqual([...(listing?.inboxMailboxIds ?? [])], ["m2-inbox"]);
692
- assert.deepEqual([...(counting?.mailboxIds ?? [])].sort(), ["m2-inbox"]);
693
- });
694
-
695
- test("an account scope narrows the search scope too", async () => {
696
- const calls: Call[] = [];
697
- await executeUnifiedThreadListing(buildListingClient(calls), CONFIG_ID, {
698
- ...params({ accountId: "a2" }),
699
- searchText: "invoice",
700
- });
701
-
702
- assert.equal(calls[0].mode, "searchByDate");
703
- assert.deepEqual([...(calls[0].mailboxIds ?? [])].sort(), [
704
- "m2-archive",
705
- "m2-inbox",
706
- ]);
707
- });
708
-
709
- test("no account scope leaves the cross-account aggregate", async () => {
710
- const calls: Call[] = [];
711
- await executeUnifiedThreadListing(
712
- buildListingClient(calls),
713
- CONFIG_ID,
714
- params(),
715
- );
716
-
717
- assert.deepEqual([...(calls[0].inboxMailboxIds ?? [])].sort(), [
718
- "m-inbox",
719
- "m2-inbox",
720
- ]);
721
- });
722
-
723
- // An account the config does not read narrows to nothing, which is the
724
- // honest answer — never the whole aggregate under another account's name.
725
- test("an unknown account matches nothing", async () => {
726
- const calls: Call[] = [];
727
- const response = await executeUnifiedThreadListing(
728
- buildListingClient(calls, 99),
729
- CONFIG_ID,
730
- params({ accountId: "a-other", count: true }),
731
- );
732
-
733
- assert.deepEqual(calls, []);
734
- assert.deepEqual(response.items, []);
735
- assert.equal(response.count, 0);
736
- });
737
-
738
- // #313: the Spam offer stated the junk share of the page the client had
739
- // loaded, offered as a folder total. A count of one folder under the search's
740
- // own criteria is what makes that number the folder's.
741
- test("a folder scope narrows the search and the count to that folder", async () => {
742
- const calls: Call[] = [];
743
- const response = await executeUnifiedThreadListing(
744
- buildListingClient(calls, 12),
745
- CONFIG_ID,
746
- { ...params({ mailboxId: "m2-archive", count: true }), searchText: "x" },
747
- );
748
-
749
- const searching = calls.find((call) => call.mode === "searchByDate");
750
- const counting = calls.find((call) => call.mode === "count");
751
- assert.deepEqual([...(searching?.mailboxIds ?? [])], ["m2-archive"]);
752
- assert.deepEqual([...(counting?.mailboxIds ?? [])], ["m2-archive"]);
753
- assert.equal(response.count, 12);
754
- });
755
-
756
- test("a folder scope intersects the account scope rather than replacing it", async () => {
757
- const calls: Call[] = [];
758
- const response = await executeUnifiedThreadListing(
759
- buildListingClient(calls, 99),
760
- CONFIG_ID,
761
- {
762
- ...params({ accountId: "a1", mailboxId: "m2-archive", count: true }),
763
- searchText: "x",
764
- },
765
- );
766
-
767
- assert.deepEqual(calls, []);
768
- assert.equal(response.count, 0);
769
- });
770
-
771
- // Trash is the one folder the unscoped search never reaches, so a caller
772
- // asking what it holds of a search is asking about mail the search does not
773
- // see. Answering with the folder's own contents would widen the scope.
774
- test("a folder the search scope excludes matches nothing", async () => {
775
- const calls: Call[] = [];
776
- const response = await executeUnifiedThreadListing(
777
- buildListingClient(calls, 99),
778
- CONFIG_ID,
779
- { ...params({ mailboxId: "m2-trash", count: true }), searchText: "x" },
780
- );
781
-
782
- assert.deepEqual(calls, []);
783
- assert.equal(response.count, 0);
784
- });
785
- });