@remit/data-ports 0.0.83 → 0.0.85
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.
- package/dist/account-settings.d.ts +371 -0
- package/dist/account-settings.js +114 -0
- package/dist/conformance/account.d.ts +3 -0
- package/dist/conformance/account.js +118 -0
- package/dist/conformance/calendar-collection.d.ts +3 -0
- package/dist/conformance/calendar-collection.js +251 -0
- package/dist/conformance/calendar-event-index.d.ts +3 -0
- package/dist/conformance/calendar-event-index.js +114 -0
- package/dist/conformance/calendar-feed-token.d.ts +3 -0
- package/dist/conformance/calendar-feed-token.js +97 -0
- package/dist/conformance/calendar-object.d.ts +3 -0
- package/dist/conformance/calendar-object.js +156 -0
- package/dist/conformance/calendar-suggestion.d.ts +3 -0
- package/dist/conformance/calendar-suggestion.js +255 -0
- package/{src/conformance/harness.ts → dist/conformance/harness.d.ts} +4 -4
- package/dist/conformance/harness.js +1 -0
- package/dist/conformance/index.js +8 -0
- package/dist/conformance/label.d.ts +3 -0
- package/dist/conformance/label.js +92 -0
- package/dist/conformance/sender-signer-standing.d.ts +3 -0
- package/dist/conformance/sender-signer-standing.js +81 -0
- package/dist/display-name.d.ts +1 -0
- package/dist/display-name.js +26 -0
- package/dist/errors.d.ts +150 -0
- package/dist/errors.js +181 -0
- package/dist/filter-ttl.d.ts +7 -0
- package/{src/filter-ttl.ts → dist/filter-ttl.js} +8 -11
- package/dist/folder-role.d.ts +239 -0
- package/dist/folder-role.js +276 -0
- package/dist/id.d.ts +124 -0
- package/{src/id.ts → dist/id.js} +41 -127
- package/dist/index.d.ts +37 -0
- package/dist/index.js +5 -0
- package/dist/interfaces/account-config.d.ts +15 -0
- package/dist/interfaces/account-config.js +1 -0
- package/dist/interfaces/account-export-request.d.ts +10 -0
- package/dist/interfaces/account-export-request.js +1 -0
- package/dist/interfaces/account-setting.d.ts +8 -0
- package/dist/interfaces/account-setting.js +1 -0
- package/dist/interfaces/account.d.ts +29 -0
- package/dist/interfaces/account.js +1 -0
- package/dist/interfaces/address.d.ts +70 -0
- package/dist/interfaces/address.js +1 -0
- package/dist/interfaces/calendar-collection.d.ts +38 -0
- package/dist/interfaces/calendar-collection.js +1 -0
- package/dist/interfaces/calendar-event-index.d.ts +19 -0
- package/dist/interfaces/calendar-event-index.js +1 -0
- package/dist/interfaces/calendar-feed-token.d.ts +31 -0
- package/dist/interfaces/calendar-feed-token.js +1 -0
- package/dist/interfaces/calendar-object.d.ts +36 -0
- package/dist/interfaces/calendar-object.js +1 -0
- package/dist/interfaces/calendar-store.d.ts +93 -0
- package/dist/interfaces/calendar-store.js +1 -0
- package/dist/interfaces/calendar-suggestion.d.ts +62 -0
- package/dist/interfaces/calendar-suggestion.js +1 -0
- package/dist/interfaces/config-import.d.ts +8 -0
- package/dist/interfaces/config-import.js +1 -0
- package/dist/interfaces/envelope.d.ts +24 -0
- package/dist/interfaces/envelope.js +1 -0
- package/dist/interfaces/filter.d.ts +28 -0
- package/dist/interfaces/filter.js +1 -0
- package/dist/interfaces/label.d.ts +17 -0
- package/dist/interfaces/label.js +1 -0
- package/dist/interfaces/mailbox-lock.d.ts +12 -0
- package/dist/interfaces/mailbox-lock.js +1 -0
- package/dist/interfaces/mailbox-special-use.d.ts +54 -0
- package/dist/interfaces/mailbox-special-use.js +1 -0
- package/dist/interfaces/mailbox.d.ts +79 -0
- package/dist/interfaces/mailbox.js +1 -0
- package/dist/interfaces/message-flag-push.d.ts +9 -0
- package/dist/interfaces/message-flag-push.js +1 -0
- package/dist/interfaces/message-flag.d.ts +14 -0
- package/dist/interfaces/message-flag.js +1 -0
- package/dist/interfaces/message-label.d.ts +21 -0
- package/dist/interfaces/message-label.js +1 -0
- package/dist/interfaces/message-placement-move.d.ts +8 -0
- package/dist/interfaces/message-placement-move.js +1 -0
- package/dist/interfaces/message.d.ts +60 -0
- package/dist/interfaces/message.js +1 -0
- package/dist/interfaces/organize-job-request.d.ts +10 -0
- package/dist/interfaces/organize-job-request.js +1 -0
- package/dist/interfaces/outbox-attachment.d.ts +101 -0
- package/dist/interfaces/outbox-attachment.js +7 -0
- package/dist/interfaces/outbox-message.d.ts +70 -0
- package/dist/interfaces/outbox-message.js +20 -0
- package/dist/interfaces/quarantine.d.ts +20 -0
- package/dist/interfaces/quarantine.js +1 -0
- package/dist/interfaces/sender-signer-standing.d.ts +26 -0
- package/dist/interfaces/sender-signer-standing.js +1 -0
- package/dist/interfaces/sync-warning.d.ts +11 -0
- package/dist/interfaces/sync-warning.js +1 -0
- package/dist/interfaces/thread-message.d.ts +202 -0
- package/dist/interfaces/thread-message.js +1 -0
- package/dist/mailbox-name.d.ts +35 -0
- package/dist/mailbox-name.js +68 -0
- package/dist/message-parts.d.ts +22 -0
- package/dist/message-parts.js +18 -0
- package/dist/message-settlement.d.ts +35 -0
- package/{src/message-settlement.ts → dist/message-settlement.js} +5 -19
- package/dist/mutation-events.d.ts +19 -0
- package/{src/mutation-events.ts → dist/mutation-events.js} +2 -5
- package/dist/sender-mute.d.ts +26 -0
- package/{src/sender-mute.ts → dist/sender-mute.js} +12 -34
- package/dist/sync-warning-state.d.ts +3 -0
- package/dist/sync-warning-state.js +5 -0
- package/dist/types.d.ts +491 -0
- package/dist/types.js +1 -0
- package/dist/update-manifest.d.ts +24 -0
- package/dist/update-manifest.js +20 -0
- package/dist/virtual-copy.d.ts +8 -0
- package/{src/virtual-copy.ts → dist/virtual-copy.js} +10 -15
- package/dist/wellknown.d.ts +47 -0
- package/{src/wellknown.ts → dist/wellknown.js} +26 -49
- package/package.json +44 -79
- package/src/account-settings.ts +0 -194
- package/src/conformance/account.ts +0 -104
- package/src/conformance/calendar-collection.ts +0 -342
- package/src/conformance/calendar-event-index.ts +0 -203
- package/src/conformance/calendar-feed-token.ts +0 -148
- package/src/conformance/calendar-object.ts +0 -269
- package/src/conformance/calendar-suggestion.ts +0 -490
- package/src/conformance/label.ts +0 -136
- package/src/conformance/sender-signer-standing.ts +0 -117
- package/src/display-name.test.ts +0 -101
- package/src/display-name.ts +0 -40
- package/src/errors.ts +0 -244
- package/src/folder-role.test.ts +0 -422
- package/src/folder-role.ts +0 -447
- package/src/id.test.ts +0 -352
- package/src/index.ts +0 -170
- package/src/interfaces/account-config.ts +0 -25
- package/src/interfaces/account-export-request.ts +0 -21
- package/src/interfaces/account-setting.ts +0 -14
- package/src/interfaces/account.ts +0 -43
- package/src/interfaces/address.ts +0 -122
- package/src/interfaces/calendar-collection.ts +0 -60
- package/src/interfaces/calendar-event-index.ts +0 -34
- package/src/interfaces/calendar-feed-token.ts +0 -38
- package/src/interfaces/calendar-object.ts +0 -58
- package/src/interfaces/calendar-suggestion.ts +0 -93
- package/src/interfaces/calendar-unit-of-work.ts +0 -47
- package/src/interfaces/config-import.ts +0 -16
- package/src/interfaces/envelope.ts +0 -32
- package/src/interfaces/filter-anchor-transaction.ts +0 -23
- package/src/interfaces/filter-anchor.ts +0 -11
- package/src/interfaces/filter.ts +0 -41
- package/src/interfaces/label.ts +0 -30
- package/src/interfaces/mailbox-lock.ts +0 -32
- package/src/interfaces/mailbox-special-use.ts +0 -60
- package/src/interfaces/mailbox.ts +0 -115
- package/src/interfaces/message-flag-push.ts +0 -17
- package/src/interfaces/message-flag.ts +0 -15
- package/src/interfaces/message-label.ts +0 -25
- package/src/interfaces/message-placement-move.ts +0 -15
- package/src/interfaces/message.ts +0 -65
- package/src/interfaces/organize-job-request.ts +0 -19
- package/src/interfaces/outbox-attachment.ts +0 -132
- package/src/interfaces/outbox-message.ts +0 -106
- package/src/interfaces/quarantine.ts +0 -22
- package/src/interfaces/sender-signer-standing.ts +0 -37
- package/src/interfaces/thread-message.ts +0 -264
- package/src/interfaces/unit-of-work.ts +0 -28
- package/src/mailbox-name.test.ts +0 -106
- package/src/mailbox-name.ts +0 -90
- package/src/mailbox-state-doors.test.ts +0 -104
- package/src/message-placement-doors.test.ts +0 -100
- package/src/message-settlement.test.ts +0 -132
- package/src/sender-mute.test.ts +0 -76
- package/src/types.ts +0 -820
- package/src/update-manifest.test.ts +0 -130
- package/src/update-manifest.ts +0 -22
- package/src/virtual-copy.test.ts +0 -34
- package/tsconfig.json +0 -8
- /package/{src/conformance/index.ts → dist/conformance/index.d.ts} +0 -0
package/src/folder-role.ts
DELETED
|
@@ -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
|
-
};
|