@remit/data-ports 0.0.83 → 0.0.84

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 (174) hide show
  1. package/dist/account-settings.d.ts +371 -0
  2. package/dist/account-settings.js +114 -0
  3. package/dist/conformance/account.d.ts +3 -0
  4. package/dist/conformance/account.js +118 -0
  5. package/dist/conformance/calendar-collection.d.ts +3 -0
  6. package/dist/conformance/calendar-collection.js +251 -0
  7. package/dist/conformance/calendar-event-index.d.ts +3 -0
  8. package/dist/conformance/calendar-event-index.js +114 -0
  9. package/dist/conformance/calendar-feed-token.d.ts +3 -0
  10. package/dist/conformance/calendar-feed-token.js +97 -0
  11. package/dist/conformance/calendar-object.d.ts +3 -0
  12. package/dist/conformance/calendar-object.js +156 -0
  13. package/dist/conformance/calendar-suggestion.d.ts +3 -0
  14. package/dist/conformance/calendar-suggestion.js +255 -0
  15. package/{src/conformance/harness.ts → dist/conformance/harness.d.ts} +4 -4
  16. package/dist/conformance/harness.js +1 -0
  17. package/dist/conformance/index.js +8 -0
  18. package/dist/conformance/label.d.ts +3 -0
  19. package/dist/conformance/label.js +92 -0
  20. package/dist/conformance/sender-signer-standing.d.ts +3 -0
  21. package/dist/conformance/sender-signer-standing.js +81 -0
  22. package/dist/display-name.d.ts +1 -0
  23. package/dist/display-name.js +26 -0
  24. package/dist/errors.d.ts +150 -0
  25. package/dist/errors.js +181 -0
  26. package/dist/filter-ttl.d.ts +7 -0
  27. package/{src/filter-ttl.ts → dist/filter-ttl.js} +8 -11
  28. package/dist/folder-role.d.ts +239 -0
  29. package/dist/folder-role.js +276 -0
  30. package/dist/id.d.ts +124 -0
  31. package/{src/id.ts → dist/id.js} +41 -127
  32. package/dist/index.d.ts +37 -0
  33. package/dist/index.js +5 -0
  34. package/dist/interfaces/account-config.d.ts +15 -0
  35. package/dist/interfaces/account-config.js +1 -0
  36. package/dist/interfaces/account-export-request.d.ts +10 -0
  37. package/dist/interfaces/account-export-request.js +1 -0
  38. package/dist/interfaces/account-setting.d.ts +7 -0
  39. package/dist/interfaces/account-setting.js +1 -0
  40. package/dist/interfaces/account.d.ts +29 -0
  41. package/dist/interfaces/account.js +1 -0
  42. package/dist/interfaces/address.d.ts +69 -0
  43. package/dist/interfaces/address.js +1 -0
  44. package/dist/interfaces/calendar-collection.d.ts +38 -0
  45. package/dist/interfaces/calendar-collection.js +1 -0
  46. package/dist/interfaces/calendar-event-index.d.ts +19 -0
  47. package/dist/interfaces/calendar-event-index.js +1 -0
  48. package/dist/interfaces/calendar-feed-token.d.ts +31 -0
  49. package/dist/interfaces/calendar-feed-token.js +1 -0
  50. package/dist/interfaces/calendar-object.d.ts +36 -0
  51. package/dist/interfaces/calendar-object.js +1 -0
  52. package/dist/interfaces/calendar-store.d.ts +93 -0
  53. package/dist/interfaces/calendar-store.js +1 -0
  54. package/dist/interfaces/calendar-suggestion.d.ts +62 -0
  55. package/dist/interfaces/calendar-suggestion.js +1 -0
  56. package/dist/interfaces/config-import.d.ts +8 -0
  57. package/dist/interfaces/config-import.js +1 -0
  58. package/dist/interfaces/envelope.d.ts +24 -0
  59. package/dist/interfaces/envelope.js +1 -0
  60. package/dist/interfaces/filter.d.ts +28 -0
  61. package/dist/interfaces/filter.js +1 -0
  62. package/dist/interfaces/label.d.ts +17 -0
  63. package/dist/interfaces/label.js +1 -0
  64. package/dist/interfaces/mailbox-lock.d.ts +12 -0
  65. package/dist/interfaces/mailbox-lock.js +1 -0
  66. package/dist/interfaces/mailbox-special-use.d.ts +54 -0
  67. package/dist/interfaces/mailbox-special-use.js +1 -0
  68. package/dist/interfaces/mailbox.d.ts +79 -0
  69. package/dist/interfaces/mailbox.js +1 -0
  70. package/dist/interfaces/message-flag-push.d.ts +9 -0
  71. package/dist/interfaces/message-flag-push.js +1 -0
  72. package/dist/interfaces/message-flag.d.ts +14 -0
  73. package/dist/interfaces/message-flag.js +1 -0
  74. package/dist/interfaces/message-label.d.ts +21 -0
  75. package/dist/interfaces/message-label.js +1 -0
  76. package/dist/interfaces/message-placement-move.d.ts +8 -0
  77. package/dist/interfaces/message-placement-move.js +1 -0
  78. package/dist/interfaces/message.d.ts +60 -0
  79. package/dist/interfaces/message.js +1 -0
  80. package/dist/interfaces/organize-job-request.d.ts +10 -0
  81. package/dist/interfaces/organize-job-request.js +1 -0
  82. package/dist/interfaces/outbox-attachment.d.ts +101 -0
  83. package/dist/interfaces/outbox-attachment.js +7 -0
  84. package/dist/interfaces/outbox-message.d.ts +70 -0
  85. package/dist/interfaces/outbox-message.js +20 -0
  86. package/dist/interfaces/quarantine.d.ts +20 -0
  87. package/dist/interfaces/quarantine.js +1 -0
  88. package/dist/interfaces/sender-signer-standing.d.ts +26 -0
  89. package/dist/interfaces/sender-signer-standing.js +1 -0
  90. package/dist/interfaces/sync-warning.d.ts +11 -0
  91. package/dist/interfaces/sync-warning.js +1 -0
  92. package/dist/interfaces/thread-message.d.ts +202 -0
  93. package/dist/interfaces/thread-message.js +1 -0
  94. package/dist/mailbox-name.d.ts +35 -0
  95. package/dist/mailbox-name.js +68 -0
  96. package/dist/message-parts.d.ts +22 -0
  97. package/dist/message-parts.js +18 -0
  98. package/dist/message-settlement.d.ts +35 -0
  99. package/{src/message-settlement.ts → dist/message-settlement.js} +5 -19
  100. package/dist/mutation-events.d.ts +19 -0
  101. package/{src/mutation-events.ts → dist/mutation-events.js} +2 -5
  102. package/dist/sender-mute.d.ts +26 -0
  103. package/{src/sender-mute.ts → dist/sender-mute.js} +12 -34
  104. package/dist/sync-warning-state.d.ts +3 -0
  105. package/dist/sync-warning-state.js +5 -0
  106. package/dist/types.d.ts +491 -0
  107. package/dist/types.js +1 -0
  108. package/dist/update-manifest.d.ts +24 -0
  109. package/dist/update-manifest.js +20 -0
  110. package/dist/virtual-copy.d.ts +8 -0
  111. package/{src/virtual-copy.ts → dist/virtual-copy.js} +10 -15
  112. package/dist/wellknown.d.ts +47 -0
  113. package/{src/wellknown.ts → dist/wellknown.js} +26 -49
  114. package/package.json +44 -79
  115. package/src/account-settings.ts +0 -194
  116. package/src/conformance/account.ts +0 -104
  117. package/src/conformance/calendar-collection.ts +0 -342
  118. package/src/conformance/calendar-event-index.ts +0 -203
  119. package/src/conformance/calendar-feed-token.ts +0 -148
  120. package/src/conformance/calendar-object.ts +0 -269
  121. package/src/conformance/calendar-suggestion.ts +0 -490
  122. package/src/conformance/label.ts +0 -136
  123. package/src/conformance/sender-signer-standing.ts +0 -117
  124. package/src/display-name.test.ts +0 -101
  125. package/src/display-name.ts +0 -40
  126. package/src/errors.ts +0 -244
  127. package/src/folder-role.test.ts +0 -422
  128. package/src/folder-role.ts +0 -447
  129. package/src/id.test.ts +0 -352
  130. package/src/index.ts +0 -170
  131. package/src/interfaces/account-config.ts +0 -25
  132. package/src/interfaces/account-export-request.ts +0 -21
  133. package/src/interfaces/account-setting.ts +0 -14
  134. package/src/interfaces/account.ts +0 -43
  135. package/src/interfaces/address.ts +0 -122
  136. package/src/interfaces/calendar-collection.ts +0 -60
  137. package/src/interfaces/calendar-event-index.ts +0 -34
  138. package/src/interfaces/calendar-feed-token.ts +0 -38
  139. package/src/interfaces/calendar-object.ts +0 -58
  140. package/src/interfaces/calendar-suggestion.ts +0 -93
  141. package/src/interfaces/calendar-unit-of-work.ts +0 -47
  142. package/src/interfaces/config-import.ts +0 -16
  143. package/src/interfaces/envelope.ts +0 -32
  144. package/src/interfaces/filter-anchor-transaction.ts +0 -23
  145. package/src/interfaces/filter-anchor.ts +0 -11
  146. package/src/interfaces/filter.ts +0 -41
  147. package/src/interfaces/label.ts +0 -30
  148. package/src/interfaces/mailbox-lock.ts +0 -32
  149. package/src/interfaces/mailbox-special-use.ts +0 -60
  150. package/src/interfaces/mailbox.ts +0 -115
  151. package/src/interfaces/message-flag-push.ts +0 -17
  152. package/src/interfaces/message-flag.ts +0 -15
  153. package/src/interfaces/message-label.ts +0 -25
  154. package/src/interfaces/message-placement-move.ts +0 -15
  155. package/src/interfaces/message.ts +0 -65
  156. package/src/interfaces/organize-job-request.ts +0 -19
  157. package/src/interfaces/outbox-attachment.ts +0 -132
  158. package/src/interfaces/outbox-message.ts +0 -106
  159. package/src/interfaces/quarantine.ts +0 -22
  160. package/src/interfaces/sender-signer-standing.ts +0 -37
  161. package/src/interfaces/thread-message.ts +0 -264
  162. package/src/interfaces/unit-of-work.ts +0 -28
  163. package/src/mailbox-name.test.ts +0 -106
  164. package/src/mailbox-name.ts +0 -90
  165. package/src/mailbox-state-doors.test.ts +0 -104
  166. package/src/message-placement-doors.test.ts +0 -100
  167. package/src/message-settlement.test.ts +0 -132
  168. package/src/sender-mute.test.ts +0 -76
  169. package/src/types.ts +0 -820
  170. package/src/update-manifest.test.ts +0 -130
  171. package/src/update-manifest.ts +0 -22
  172. package/src/virtual-copy.test.ts +0 -34
  173. package/tsconfig.json +0 -8
  174. /package/{src/conformance/index.ts → dist/conformance/index.d.ts} +0 -0
@@ -1,447 +0,0 @@
1
- import {
2
- AccountSettingName,
3
- CanonicalMailboxRole,
4
- FolderAppointmentSource,
5
- MailboxSpecialUse,
6
- } from "@remit/domain-enums";
7
- import {
8
- type AccountSettingNameValue,
9
- composeSettingName,
10
- SETTING_NAME_SEPARATOR,
11
- } from "./account-settings.js";
12
- import {
13
- type MailboxNameCandidate,
14
- resolveMailboxByLeafName,
15
- } from "./mailbox-name.js";
16
-
17
- export type CanonicalMailboxRoleValue =
18
- (typeof CanonicalMailboxRole)[keyof typeof CanonicalMailboxRole];
19
-
20
- /** The fixed anchor set, in the RFC's canonical display order. */
21
- export const CANONICAL_ROLES: readonly CanonicalMailboxRoleValue[] =
22
- Object.values(CanonicalMailboxRole);
23
-
24
- const isCanonicalRole = (value: string): value is CanonicalMailboxRoleValue =>
25
- (CANONICAL_ROLES as readonly string[]).includes(value);
26
-
27
- /**
28
- * The stored `AccountSetting` name holding one role's appointment. Composed
29
- * here rather than at either reader, so the backend that writes it and the
30
- * repository that reads it can never disagree about the key.
31
- */
32
- export const composeFolderRoleAppointmentName = (
33
- accountId: string,
34
- role: CanonicalMailboxRoleValue,
35
- ): string =>
36
- composeSettingName(
37
- AccountSettingName.FolderRoleAppointment,
38
- `${accountId}${SETTING_NAME_SEPARATOR}${role}`,
39
- );
40
-
41
- /**
42
- * The stored `AccountSetting` name holding the path one role's appointed
43
- * mailbox had at the moment it was appointed. A sibling row rather than a field
44
- * on the appointment, so "never read by resolution" is structural: the
45
- * repository composes only the appointment name and cannot reach this one.
46
- */
47
- export const composeFolderRoleAppointmentLabelName = (
48
- accountId: string,
49
- role: CanonicalMailboxRoleValue,
50
- ): string =>
51
- composeSettingName(
52
- AccountSettingName.FolderRoleAppointmentLabel,
53
- `${accountId}${SETTING_NAME_SEPARATOR}${role}`,
54
- );
55
-
56
- const parseFolderRoleTarget = (
57
- base: AccountSettingNameValue,
58
- name: string,
59
- ): { accountId: string; role: CanonicalMailboxRoleValue } | undefined => {
60
- const [candidate, ...rest] = name.split(SETTING_NAME_SEPARATOR);
61
- if (candidate !== base) return undefined;
62
- const role = rest[rest.length - 1];
63
- const accountId = rest.slice(0, -1).join(SETTING_NAME_SEPARATOR);
64
- if (!accountId || !role || !isCanonicalRole(role)) return undefined;
65
- return { accountId, role };
66
- };
67
-
68
- /**
69
- * Split a stored appointment name back into its two-part target. Unlike the
70
- * single-target composites (`MailboxRole#<id>`), this setting composes two ids
71
- * after the base, so it parses the suffix itself rather than reusing
72
- * `targetIdOf`.
73
- */
74
- export const parseFolderRoleAppointmentName = (
75
- name: string,
76
- ): { accountId: string; role: CanonicalMailboxRoleValue } | undefined =>
77
- parseFolderRoleTarget(AccountSettingName.FolderRoleAppointment, name);
78
-
79
- /**
80
- * The label row's counterpart, so the config-wide settings batch can recover
81
- * the recorded path for a role the same way it recovers the appointment.
82
- */
83
- export const parseFolderRoleAppointmentLabelName = (
84
- name: string,
85
- ): { accountId: string; role: CanonicalMailboxRoleValue } | undefined =>
86
- parseFolderRoleTarget(AccountSettingName.FolderRoleAppointmentLabel, name);
87
-
88
- /**
89
- * RFC 6154 SPECIAL-USE flag per role. Inbox has no SPECIAL-USE flag (RFC 3501
90
- * reserves the name itself); a role with no entry here is matched by name hint
91
- * only.
92
- */
93
- export const ROLE_SPECIAL_USE: Partial<
94
- Record<CanonicalMailboxRoleValue, string>
95
- > = {
96
- [CanonicalMailboxRole.Drafts]: MailboxSpecialUse.Drafts,
97
- [CanonicalMailboxRole.Sent]: MailboxSpecialUse.Sent,
98
- [CanonicalMailboxRole.Archive]: MailboxSpecialUse.Archive,
99
- [CanonicalMailboxRole.Junk]: MailboxSpecialUse.Junk,
100
- [CanonicalMailboxRole.Trash]: MailboxSpecialUse.Trash,
101
- [CanonicalMailboxRole.All]: MailboxSpecialUse.All,
102
- [CanonicalMailboxRole.Flagged]: MailboxSpecialUse.Flagged,
103
- };
104
-
105
- /**
106
- * Weak name hints, ordered best-first and lower case: the last resort for a
107
- * provider that advertises no SPECIAL-USE and whose user has appointed
108
- * nothing. Deliberately small — a guess that seeds a proposal, never a
109
- * substitute for server truth or for what the user chose.
110
- */
111
- export const ROLE_NAME_HINTS: Partial<
112
- Record<CanonicalMailboxRoleValue, readonly string[]>
113
- > = {
114
- [CanonicalMailboxRole.Drafts]: ["drafts", "draft", "concepten"],
115
- [CanonicalMailboxRole.Sent]: [
116
- "sent",
117
- "sent items",
118
- "sent messages",
119
- "sent mail",
120
- ],
121
- [CanonicalMailboxRole.Archive]: ["archive", "archives", "all mail"],
122
- [CanonicalMailboxRole.Junk]: [
123
- "junk",
124
- "spam",
125
- "junk e-mail",
126
- "junk email",
127
- "bulk mail",
128
- ],
129
- // No bare "deleted" and no "bin": both are ordinary folder names a user
130
- // keeps mail in, and a wrong guess here files deletes into a folder the
131
- // user never meant as Trash. Gmail's en-GB "Bin" and its "[Gmail]/Trash"
132
- // both advertise \Trash, so the flag already covers them (#837).
133
- [CanonicalMailboxRole.Trash]: ["trash", "deleted items", "deleted messages"],
134
- [CanonicalMailboxRole.All]: ["all mail", "all"],
135
- };
136
-
137
- /**
138
- * The role a folder's own leaf name is most conventionally for. A name that
139
- * several roles list goes to the role that ranks it highest, so `All Mail` is
140
- * an All folder rather than a lookalike of Archive.
141
- *
142
- * `name` is the leaf segment, lower case.
143
- */
144
- export const roleForFolderName = (
145
- name: string,
146
- ): CanonicalMailboxRoleValue | undefined => {
147
- let best: { role: CanonicalMailboxRoleValue; rank: number } | undefined;
148
- for (const role of CANONICAL_ROLES) {
149
- const rank = ROLE_NAME_HINTS[role]?.indexOf(name) ?? -1;
150
- if (rank < 0) continue;
151
- if (!best || rank < best.rank) best = { role, rank };
152
- }
153
- return best?.role;
154
- };
155
-
156
- /**
157
- * Whether the mailbox sweep would read a folder with this leaf name as a
158
- * duplicate of a role's canonical folder — and so delete its row, and under D8
159
- * the mail in it, as soon as another folder carries the role's SPECIAL-USE
160
- * flag. It is the sweep's own predicate, so a rename refused on it and a folder
161
- * reaped by it can never disagree (docs/architecture/folder-rename-and-delete.md
162
- * D5).
163
- *
164
- * A role with no flag to be a duplicate of is not reserved: nothing reaps a
165
- * folder for it.
166
- */
167
- export const isReservedFolderName = (name: string): boolean => {
168
- const role = roleForFolderName(name.toLowerCase());
169
- return role !== undefined && ROLE_SPECIAL_USE[role] !== undefined;
170
- };
171
-
172
- /**
173
- * The pane where a user appoints a folder to a role, named as it is labelled.
174
- * Every refusal that asks for an appointment points here, in one wording — the
175
- * settings screen is titled "Folder roles", and copies of this sentence had
176
- * already drifted to "Folders".
177
- */
178
- export const FOLDER_ROLES_SETTINGS_PATH = "Settings › Folder roles";
179
-
180
- /**
181
- * Why an operation that files or destroys mail was refused. A delete, an
182
- * Empty Trash and the count the handler reports before one all act on the
183
- * folder the user appointed or the server flagged, and all refuse in these
184
- * words — one sentence, so two surfaces cannot tell a user two things about
185
- * one account.
186
- */
187
- export const NO_TRASH_FOLDER_REASON = `This account has no Trash folder, so nothing was deleted. Appoint one under ${FOLDER_ROLES_SETTINGS_PATH}, then try again.`;
188
-
189
- /**
190
- * The folder somebody chose as this account's Trash is no longer on the mail
191
- * server. Reader will not quietly file mail into whatever it would have picked
192
- * had nobody chosen: the user's choice went missing and only they can replace
193
- * it.
194
- */
195
- export const STALE_TRASH_FOLDER_REASON = `The folder appointed as this account's Trash is no longer on the mail server, so nothing was deleted. Appoint one under ${FOLDER_ROLES_SETTINGS_PATH}, then try again.`;
196
-
197
- /**
198
- * A folder matches the Trash name hint and nothing else — enough to file a
199
- * delete somewhere retrievable, never enough to erase what is already in it.
200
- * Emptying that folder and deleting a message already inside it both land here
201
- * (#876), so the sentence names neither verb: "nothing was deleted" is true of
202
- * both, the way its two siblings above are.
203
- */
204
- export const UNCONFIRMED_TRASH_FOLDER_REASON = `Nobody has confirmed which folder is this account's Trash, so nothing was deleted. Appoint one under ${FOLDER_ROLES_SETTINGS_PATH}, then try again.`;
205
-
206
- /**
207
- * The mailboxes holding Junk and Trash across a set of accounts, as ids. The
208
- * shape a SQL predicate needs when it must ask "is this message in Junk"
209
- * without spelling the rule a second time: resolution happens once, here, and
210
- * SQL compares mailbox ids.
211
- *
212
- * A role resolves to at most one mailbox per account, so each list holds one id
213
- * per account that has that folder — and no entry at all for an account that
214
- * has none.
215
- */
216
- export interface JunkRoleMailboxes {
217
- readonly junkMailboxIds: readonly string[];
218
- readonly trashMailboxIds: readonly string[];
219
- }
220
-
221
- /** The minimal mailbox shape role resolution reads. */
222
- export interface RoleMailboxCandidate extends MailboxNameCandidate {
223
- specialUse?: readonly string[];
224
- }
225
-
226
- /**
227
- * What an account with no appointment for the role resolves to, and equally
228
- * what a stale appointment falls back to — the fallback can never itself be an
229
- * appointment, stale or otherwise.
230
- */
231
- export type UnappointedRoleResolution<T> =
232
- | { kind: "flagged"; mailbox: T }
233
- | { kind: "reserved"; mailbox: T }
234
- | { kind: "proposed"; mailbox: T }
235
- | { kind: "none" };
236
-
237
- /**
238
- * What resolving a role for an account answered, and on what evidence. Total:
239
- * there is a member for every outcome, so a caller that must weigh the evidence
240
- * — Empty Trash may only expunge what somebody designated — reads the tag
241
- * instead of inferring it from a `null` that means four different things.
242
- *
243
- * `appointment_stale` is the case a nullable answer cannot express: the user
244
- * appointed a mailbox the account no longer holds. It carries the id they chose
245
- * so a surface can offer the repair, and the resolution that would have applied
246
- * had they appointed nothing.
247
- */
248
- export type RoleResolution<T> =
249
- | { kind: "appointed"; mailbox: T }
250
- | UnappointedRoleResolution<T>
251
- | {
252
- kind: "appointment_stale";
253
- appointedMailboxId: string;
254
- fallback: UnappointedRoleResolution<T>;
255
- };
256
-
257
- const resolveWithoutAppointment = <T extends RoleMailboxCandidate>(
258
- role: CanonicalMailboxRoleValue,
259
- mailboxes: readonly T[],
260
- ): UnappointedRoleResolution<T> => {
261
- const specialUse = ROLE_SPECIAL_USE[role];
262
- if (specialUse) {
263
- const flagged = mailboxes.find((m) => m.specialUse?.includes(specialUse));
264
- if (flagged) return { kind: "flagged", mailbox: flagged };
265
- }
266
-
267
- if (role === CanonicalMailboxRole.Inbox) {
268
- const inbox = mailboxes.find((m) => m.fullPath.toUpperCase() === "INBOX");
269
- return inbox ? { kind: "reserved", mailbox: inbox } : { kind: "none" };
270
- }
271
-
272
- const hints = ROLE_NAME_HINTS[role];
273
- if (!hints) return { kind: "none" };
274
- const proposed = resolveMailboxByLeafName(mailboxes, hints);
275
- return proposed ? { kind: "proposed", mailbox: proposed } : { kind: "none" };
276
- };
277
-
278
- /**
279
- * The single mailbox that holds a canonical role for an account (RFC 032
280
- * exclusive-folder-appointment, #976), in one precedence order every caller
281
- * shares:
282
- *
283
- * 1. the mailbox the user appointed;
284
- * 2. the server's own SPECIAL-USE flag (RFC 6154) — language-independent truth;
285
- * 3. the reserved `INBOX` name, for the Inbox role only (RFC 3501);
286
- * 4. a conventional name, matched on the folder's own leaf segment so it
287
- * resolves at any depth under any prefix.
288
- *
289
- * The last of those is a guess: a folder named `Deleted` is not evidence that
290
- * the user means it as Trash. Use it only where being wrong misfiles a message
291
- * a user can move back — never where it destroys mail.
292
- */
293
- export const resolveRoleForAccount = <T extends RoleMailboxCandidate>(
294
- role: CanonicalMailboxRoleValue,
295
- mailboxes: readonly T[],
296
- appointedMailboxId?: string,
297
- ): RoleResolution<T> => {
298
- if (!appointedMailboxId) return resolveWithoutAppointment(role, mailboxes);
299
-
300
- const appointed = mailboxes.find((m) => m.mailboxId === appointedMailboxId);
301
- if (appointed) return { kind: "appointed", mailbox: appointed };
302
-
303
- return {
304
- kind: "appointment_stale",
305
- appointedMailboxId,
306
- fallback: resolveWithoutAppointment(role, mailboxes),
307
- };
308
- };
309
-
310
- /**
311
- * How much evidence a Trash verb demands. `confirmed` is what an expunge
312
- * requires: somebody designated this folder, either the user or the server.
313
- * `resolved` additionally accepts the name guess, for filing mail somewhere the
314
- * user can retrieve it from. Trash only, deliberately — no other role and no
315
- * other verb weighs its evidence, and a matrix over eight roles would be five
316
- * gates nobody asked for.
317
- */
318
- export type TrashAssuranceLevel = "confirmed" | "resolved";
319
-
320
- /**
321
- * Why a canonical role names no folder an action may act on. `none`: the
322
- * account has no candidate at all. `stale`: the folder the user appointed is
323
- * gone from the server. `unconfirmed`: a folder matches by name, but nobody —
324
- * neither the user nor the server's own flag — ever said it holds the role.
325
- * These three and no others; a target that is merely unsettled is a different
326
- * refusal under its own code.
327
- */
328
- export type FolderRoleUnresolvedReason = "none" | "stale" | "unconfirmed";
329
-
330
- export type TrashGateOutcome<T> =
331
- | { allowed: true; mailbox: T }
332
- | { allowed: false; reason: FolderRoleUnresolvedReason };
333
-
334
- /**
335
- * The Trash folder a verb at this assurance level may act on, or which of the
336
- * three reasons stops it. One switch: the delete, the expunge and the count
337
- * reported before one all decide here, so no two of them can answer a user
338
- * differently about the same account.
339
- */
340
- export const trashMailboxAt = <T>(
341
- resolution: RoleResolution<T>,
342
- level: TrashAssuranceLevel,
343
- ): TrashGateOutcome<T> => {
344
- switch (resolution.kind) {
345
- case "appointed":
346
- case "flagged":
347
- return { allowed: true, mailbox: resolution.mailbox };
348
- case "proposed":
349
- return level === "resolved"
350
- ? { allowed: true, mailbox: resolution.mailbox }
351
- : { allowed: false, reason: "unconfirmed" };
352
- case "appointment_stale":
353
- return { allowed: false, reason: "stale" };
354
- default:
355
- return { allowed: false, reason: "none" };
356
- }
357
- };
358
-
359
- export const meetsTrashAssurance = <T>(
360
- resolution: RoleResolution<T>,
361
- level: TrashAssuranceLevel,
362
- ): boolean => trashMailboxAt(resolution, level).allowed;
363
-
364
- export type FolderAppointmentSourceValue =
365
- (typeof FolderAppointmentSource)[keyof typeof FolderAppointmentSource];
366
-
367
- /** The resolution each reported source came from (`toFolderAppointment`). */
368
- const RESOLUTION_FOR_SOURCE: Record<
369
- FolderAppointmentSourceValue,
370
- RoleResolution<null>
371
- > = {
372
- [FolderAppointmentSource.Appointed]: { kind: "appointed", mailbox: null },
373
- [FolderAppointmentSource.Flagged]: { kind: "flagged", mailbox: null },
374
- [FolderAppointmentSource.Reserved]: { kind: "reserved", mailbox: null },
375
- [FolderAppointmentSource.Proposed]: { kind: "proposed", mailbox: null },
376
- [FolderAppointmentSource.Stale]: {
377
- kind: "appointment_stale",
378
- appointedMailboxId: "",
379
- fallback: { kind: "none" },
380
- },
381
- [FolderAppointmentSource.None]: { kind: "none" },
382
- };
383
-
384
- /**
385
- * The same gate as {@link trashMailboxAt}, asked of the source `/config`
386
- * reports rather than of the resolution the server computed — so a client can
387
- * word a delete from the answer the service will give it.
388
- *
389
- * It runs that switch rather than restating it. A hand-written mirror gets
390
- * `Reserved` wrong: it reads as designated, and the gate accepts only the user's
391
- * appointment and the server's flag.
392
- */
393
- export const trashSourceMeetsAssurance = (
394
- source: FolderAppointmentSourceValue,
395
- level: TrashAssuranceLevel,
396
- ): boolean => meetsTrashAssurance(RESOLUTION_FOR_SOURCE[source], level);
397
-
398
- /**
399
- * The mailbox a role is CONFIRMED to hold: the one the user appointed, or the
400
- * one the server flagged (RFC 6154). No name guessing — `null` here means
401
- * nobody has said which folder this is, only that a folder happens to be named
402
- * something plausible. An operation that destroys mail resolves through this
403
- * and refuses when it comes back empty; `resolveMailboxForRole` adds the guess
404
- * on top for the operations where being wrong only misfiles a message.
405
- *
406
- * An appointment naming a mailbox this account does not hold — deleted,
407
- * renamed, or carried in from elsewhere — is stale, and answers `null` here
408
- * too: the fallback is the folder the user did not choose, which is fine to
409
- * file mail into and no basis at all for destroying it.
410
- */
411
- export const resolveConfirmedMailboxForRole = <T extends RoleMailboxCandidate>(
412
- role: CanonicalMailboxRoleValue,
413
- mailboxes: readonly T[],
414
- appointedMailboxId?: string,
415
- ): T | null => {
416
- const resolution = resolveRoleForAccount(role, mailboxes, appointedMailboxId);
417
- switch (resolution.kind) {
418
- case "appointed":
419
- case "flagged":
420
- case "reserved":
421
- return resolution.mailbox;
422
- default:
423
- return null;
424
- }
425
- };
426
-
427
- /**
428
- * The role's mailbox including the name guess: `null` only when nothing
429
- * matches at all, so the caller says the role has no folder rather than picking
430
- * one. A stale appointment answers with its fallback — every role but Trash
431
- * files mail on this read, and refusing them all would be a behaviour change
432
- * with no surface to explain it.
433
- *
434
- * Built on `resolveRoleForAccount` directly, never on the confirmed adapter: a
435
- * stale appointment must still reach the `\Sent` flag here, not fall past it
436
- * into a folder that merely reads like one.
437
- */
438
- export const resolveMailboxForRole = <T extends RoleMailboxCandidate>(
439
- role: CanonicalMailboxRoleValue,
440
- mailboxes: readonly T[],
441
- appointedMailboxId?: string,
442
- ): T | null => {
443
- const resolution = resolveRoleForAccount(role, mailboxes, appointedMailboxId);
444
- const effective =
445
- resolution.kind === "appointment_stale" ? resolution.fallback : resolution;
446
- return effective.kind === "none" ? null : effective.mailbox;
447
- };