@remit/backend 0.0.143 → 0.0.144

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 (269) 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 +7 -0
  4. package/dist/src/api.js +144 -0
  5. package/dist/src/auth.d.ts +30 -0
  6. package/dist/src/auth.js +73 -0
  7. package/dist/src/config/msoauth.d.ts +30 -0
  8. package/dist/src/config/msoauth.js +30 -0
  9. package/{src/data-backend.ts → dist/src/data-backend.d.ts} +2 -4
  10. package/dist/src/data-backend.js +12 -0
  11. package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
  12. package/dist/src/derive/autoMoved.js +49 -0
  13. package/dist/src/derive/contentSignature.d.ts +41 -0
  14. package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +27 -72
  15. package/dist/src/derive/contentUrl.d.ts +46 -0
  16. package/dist/src/derive/contentUrl.js +46 -0
  17. package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
  18. package/dist/src/derive/deriveMuted.js +7 -0
  19. package/dist/src/derive/enrichThreadRows.d.ts +118 -0
  20. package/dist/src/derive/enrichThreadRows.js +151 -0
  21. package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
  22. package/dist/src/derive/filterThreadCriteria.js +24 -0
  23. package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
  24. package/dist/src/derive/pendingMoveCounts.js +53 -0
  25. package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
  26. package/dist/src/derive/senderTrust.js +19 -0
  27. package/dist/src/error.d.ts +21 -0
  28. package/dist/src/error.js +85 -0
  29. package/dist/src/handlers/account-guards.d.ts +36 -0
  30. package/dist/src/handlers/account-guards.js +107 -0
  31. package/dist/src/handlers/account-oauth.d.ts +14 -0
  32. package/dist/src/handlers/account-oauth.js +387 -0
  33. package/dist/src/handlers/account-overrides.d.ts +53 -0
  34. package/dist/src/handlers/account-overrides.js +233 -0
  35. package/dist/src/handlers/account-signature.d.ts +28 -0
  36. package/dist/src/handlers/account-signature.js +85 -0
  37. package/dist/src/handlers/account.d.ts +6 -0
  38. package/dist/src/handlers/account.js +350 -0
  39. package/dist/src/handlers/address.d.ts +35 -0
  40. package/dist/src/handlers/address.js +141 -0
  41. package/dist/src/handlers/calendar-event.d.ts +70 -0
  42. package/dist/src/handlers/calendar-event.js +425 -0
  43. package/dist/src/handlers/calendar-feed.d.ts +47 -0
  44. package/dist/src/handlers/calendar-feed.js +192 -0
  45. package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
  46. package/dist/src/handlers/calendar-suggestion.js +212 -0
  47. package/dist/src/handlers/calendar.d.ts +116 -0
  48. package/dist/src/handlers/calendar.js +330 -0
  49. package/dist/src/handlers/config.d.ts +43 -0
  50. package/dist/src/handlers/config.js +176 -0
  51. package/dist/src/handlers/ensure-account-config.d.ts +9 -0
  52. package/dist/src/handlers/ensure-account-config.js +27 -0
  53. package/dist/src/handlers/filter.d.ts +3 -0
  54. package/dist/src/handlers/filter.js +103 -0
  55. package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
  56. package/dist/src/handlers/folder-role-appointments.js +168 -0
  57. package/dist/src/handlers/folder-role-labels.d.ts +26 -0
  58. package/dist/src/handlers/folder-role-labels.js +40 -0
  59. package/dist/src/handlers/folder-role.d.ts +9 -0
  60. package/dist/src/handlers/folder-role.js +40 -0
  61. package/dist/src/handlers/index.d.ts +2 -0
  62. package/dist/src/handlers/index.js +60 -0
  63. package/dist/src/handlers/label.d.ts +43 -0
  64. package/dist/src/handlers/label.js +114 -0
  65. package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
  66. package/dist/src/handlers/mailbox-bindings.js +61 -0
  67. package/dist/src/handlers/mailbox.d.ts +23 -0
  68. package/dist/src/handlers/mailbox.js +219 -0
  69. package/dist/src/handlers/me.d.ts +2 -0
  70. package/dist/src/handlers/me.js +138 -0
  71. package/dist/src/handlers/message.d.ts +84 -0
  72. package/dist/src/handlers/message.js +607 -0
  73. package/dist/src/handlers/organize.d.ts +10 -0
  74. package/dist/src/handlers/organize.js +112 -0
  75. package/dist/src/handlers/outbox-attachment.d.ts +23 -0
  76. package/dist/src/handlers/outbox-attachment.js +81 -0
  77. package/dist/src/handlers/outbox.d.ts +4 -0
  78. package/dist/src/handlers/outbox.js +144 -0
  79. package/dist/src/handlers/sync-progress.d.ts +46 -0
  80. package/dist/src/handlers/sync-progress.js +63 -0
  81. package/dist/src/handlers/sync.d.ts +28 -0
  82. package/dist/src/handlers/sync.js +82 -0
  83. package/dist/src/handlers/system-update.d.ts +2 -0
  84. package/dist/src/handlers/system-update.js +172 -0
  85. package/dist/src/handlers/thread.d.ts +148 -0
  86. package/dist/src/handlers/thread.js +233 -0
  87. package/dist/src/handlers/unified-threads.d.ts +247 -0
  88. package/dist/src/handlers/unified-threads.js +409 -0
  89. package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
  90. package/dist/src/handlers/vip-suggestions.js +16 -0
  91. package/dist/src/index.d.ts +2 -0
  92. package/dist/src/index.js +37 -0
  93. package/{src/json.ts → dist/src/json.d.ts} +1 -4
  94. package/dist/src/json.js +10 -0
  95. package/dist/src/jwt-auth.d.ts +18 -0
  96. package/dist/src/jwt-auth.js +97 -0
  97. package/dist/src/request-context.d.ts +9 -0
  98. package/dist/src/request-context.js +22 -0
  99. package/dist/src/request.d.ts +3 -0
  100. package/dist/src/request.js +7 -0
  101. package/dist/src/response.d.ts +27 -0
  102. package/dist/src/response.js +87 -0
  103. package/dist/src/service/account-deletion.d.ts +22 -0
  104. package/dist/src/service/account-deletion.js +39 -0
  105. package/dist/src/service/compose-sqlite.d.ts +2 -0
  106. package/dist/src/service/compose-sqlite.js +11 -0
  107. package/dist/src/service/create-remit-client.d.ts +95 -0
  108. package/dist/src/service/create-remit-client.js +207 -0
  109. package/dist/src/service/data-client.d.ts +12 -0
  110. package/dist/src/service/data-client.js +44 -0
  111. package/dist/src/service/export-identity.d.ts +3 -0
  112. package/dist/src/service/export-identity.js +35 -0
  113. package/dist/src/service/fire-and-forget.d.ts +54 -0
  114. package/dist/src/service/fire-and-forget.js +38 -0
  115. package/dist/src/service/organize.d.ts +184 -0
  116. package/dist/src/service/organize.js +384 -0
  117. package/dist/src/service/refusal.d.ts +4 -0
  118. package/dist/src/service/refusal.js +20 -0
  119. package/dist/src/service/sender-category.d.ts +51 -0
  120. package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
  121. package/dist/src/service/sqs.d.ts +1 -0
  122. package/{src/service/sqs.ts → dist/src/service/sqs.js} +0 -1
  123. package/dist/src/service/trigger-sync.d.ts +51 -0
  124. package/dist/src/service/trigger-sync.js +39 -0
  125. package/dist/src/types.d.ts +49 -0
  126. package/dist/src/types.js +1 -0
  127. package/package.json +113 -98
  128. package/.env.test +0 -4
  129. package/dev-server/content-auth.test.ts +0 -113
  130. package/dev-server/content-auth.ts +0 -54
  131. package/dev-server/content-handler.test.ts +0 -105
  132. package/dev-server/content-handler.ts +0 -79
  133. package/dev-server/content-path.test.ts +0 -37
  134. package/dev-server/content-path.ts +0 -27
  135. package/dev-server/cors.test.ts +0 -48
  136. package/dev-server/cors.ts +0 -25
  137. package/dev-server/keep-alive.test.ts +0 -24
  138. package/dev-server/lambda-helpers.ts +0 -69
  139. package/dev-server/log-output.test.ts +0 -173
  140. package/dev-server/oauth-callback-route.test.ts +0 -66
  141. package/dev-server/relational-health.test.ts +0 -83
  142. package/dev-server/relational-health.ts +0 -39
  143. package/dev-server/server.ts +0 -384
  144. package/dev-server/sync-age.test.ts +0 -91
  145. package/dev-server/sync-age.ts +0 -55
  146. package/dev-server/upload-handler.ts +0 -132
  147. package/dev-server/upload-route.test.ts +0 -196
  148. package/scripts/backfill-classification.ts +0 -91
  149. package/scripts/backfill-list-id.ts +0 -83
  150. package/scripts/config-save.ts +0 -128
  151. package/src/auth.test.ts +0 -12
  152. package/src/auth.ts +0 -92
  153. package/src/config/msoauth.test.ts +0 -48
  154. package/src/config/msoauth.ts +0 -48
  155. package/src/data-backend.test.ts +0 -23
  156. package/src/derive/autoMoved.test.ts +0 -104
  157. package/src/derive/contentSignature.test.ts +0 -161
  158. package/src/derive/contentUrl.test.ts +0 -172
  159. package/src/derive/contentUrl.ts +0 -86
  160. package/src/derive/deriveMuted.test.ts +0 -34
  161. package/src/derive/enrichThreadRows.test.ts +0 -360
  162. package/src/derive/enrichThreadRows.ts +0 -234
  163. package/src/derive/filterThreadCriteria.test.ts +0 -126
  164. package/src/derive/filterThreadCriteria.ts +0 -51
  165. package/src/derive/senderTrust.test.ts +0 -67
  166. package/src/error.test.ts +0 -201
  167. package/src/error.ts +0 -123
  168. package/src/handlers/account-guards.test.ts +0 -67
  169. package/src/handlers/account-guards.ts +0 -169
  170. package/src/handlers/account-oauth-callback.test.ts +0 -155
  171. package/src/handlers/account-oauth-consent.test.ts +0 -336
  172. package/src/handlers/account-oauth-start.test.ts +0 -191
  173. package/src/handlers/account-oauth.test.ts +0 -439
  174. package/src/handlers/account-oauth.ts +0 -635
  175. package/src/handlers/account-overrides.test.ts +0 -88
  176. package/src/handlers/account-overrides.ts +0 -323
  177. package/src/handlers/account-ownership.ts +0 -25
  178. package/src/handlers/account-signature.ts +0 -117
  179. package/src/handlers/account-synced-services.test.ts +0 -245
  180. package/src/handlers/account.ts +0 -552
  181. package/src/handlers/address.test.ts +0 -381
  182. package/src/handlers/address.ts +0 -200
  183. package/src/handlers/calendar-event.test.ts +0 -685
  184. package/src/handlers/calendar-event.ts +0 -738
  185. package/src/handlers/calendar-feed-validators.test.ts +0 -228
  186. package/src/handlers/calendar-feed.test.ts +0 -439
  187. package/src/handlers/calendar-feed.ts +0 -291
  188. package/src/handlers/calendar-sqlite-fixture.ts +0 -36
  189. package/src/handlers/calendar-suggestion.test.ts +0 -1269
  190. package/src/handlers/calendar-suggestion.ts +0 -425
  191. package/src/handlers/calendar.test.ts +0 -1080
  192. package/src/handlers/calendar.ts +0 -517
  193. package/src/handlers/config.test.ts +0 -245
  194. package/src/handlers/config.ts +0 -332
  195. package/src/handlers/ensure-account-config.ts +0 -30
  196. package/src/handlers/filter.test.ts +0 -644
  197. package/src/handlers/filter.ts +0 -518
  198. package/src/handlers/folder-role-appointments.test.ts +0 -438
  199. package/src/handlers/folder-role-appointments.ts +0 -249
  200. package/src/handlers/folder-role-labels.ts +0 -63
  201. package/src/handlers/folder-role.test.ts +0 -209
  202. package/src/handlers/folder-role.ts +0 -80
  203. package/src/handlers/index.ts +0 -83
  204. package/src/handlers/label.test.ts +0 -112
  205. package/src/handlers/label.ts +0 -210
  206. package/src/handlers/mailbox-bindings.ts +0 -111
  207. package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
  208. package/src/handlers/mailbox-settled-target.test.ts +0 -251
  209. package/src/handlers/mailbox.test.ts +0 -280
  210. package/src/handlers/mailbox.ts +0 -581
  211. package/src/handlers/me.test.ts +0 -81
  212. package/src/handlers/me.ts +0 -203
  213. package/src/handlers/message-read-backfill.test.ts +0 -103
  214. package/src/handlers/message.test.ts +0 -131
  215. package/src/handlers/message.ts +0 -1131
  216. package/src/handlers/organize.test.ts +0 -277
  217. package/src/handlers/organize.ts +0 -197
  218. package/src/handlers/outbox-attachment.test.ts +0 -765
  219. package/src/handlers/outbox-attachment.ts +0 -126
  220. package/src/handlers/outbox-list.test.ts +0 -179
  221. package/src/handlers/outbox.test.ts +0 -772
  222. package/src/handlers/outbox.ts +0 -258
  223. package/src/handlers/search.test.ts +0 -161
  224. package/src/handlers/search.ts +0 -105
  225. package/src/handlers/sync-progress.test.ts +0 -205
  226. package/src/handlers/sync-progress.ts +0 -99
  227. package/src/handlers/sync.test.ts +0 -168
  228. package/src/handlers/sync.ts +0 -118
  229. package/src/handlers/system-update.test.ts +0 -497
  230. package/src/handlers/system-update.ts +0 -230
  231. package/src/handlers/thread.test.ts +0 -396
  232. package/src/handlers/thread.ts +0 -427
  233. package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
  234. package/src/handlers/unified-threads.test.ts +0 -785
  235. package/src/handlers/unified-threads.ts +0 -669
  236. package/src/index.test.ts +0 -83
  237. package/src/index.ts +0 -228
  238. package/src/jwt-auth.test.ts +0 -152
  239. package/src/jwt-auth.ts +0 -132
  240. package/src/log-redaction.test.ts +0 -144
  241. package/src/openapi-spec.test.ts +0 -118
  242. package/src/request-context.test.ts +0 -61
  243. package/src/request-context.ts +0 -33
  244. package/src/request-pipeline.test.ts +0 -88
  245. package/src/request.ts +0 -10
  246. package/src/response.test.ts +0 -130
  247. package/src/response.ts +0 -144
  248. package/src/service/compose-sqlite.ts +0 -98
  249. package/src/service/config-import.ts +0 -39
  250. package/src/service/create-remit-client.ts +0 -522
  251. package/src/service/data-client.test.ts +0 -97
  252. package/src/service/data-client.ts +0 -58
  253. package/src/service/export-identity.ts +0 -39
  254. package/src/service/filter.ts +0 -40
  255. package/src/service/fire-and-forget.test.ts +0 -126
  256. package/src/service/fire-and-forget.ts +0 -79
  257. package/src/service/localhost-env-config.test.ts +0 -36
  258. package/src/service/organize-queue.ts +0 -98
  259. package/src/service/organize.test.ts +0 -1451
  260. package/src/service/organize.ts +0 -1025
  261. package/src/service/read-path-filter-config.test.ts +0 -243
  262. package/src/service/semantic-capability.test.ts +0 -78
  263. package/src/service/semantic-capability.ts +0 -95
  264. package/src/service/sender-category.test.ts +0 -235
  265. package/src/service/trigger-sync.test.ts +0 -205
  266. package/src/service/trigger-sync.ts +0 -100
  267. package/src/stdout-capture-fixture.ts +0 -40
  268. package/src/types.ts +0 -271
  269. 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
- });