@remit/data-ports 0.0.82 → 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.
- 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 +7 -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 +69 -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 -226
- 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 -56
- 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
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { AccountSettingName, CanonicalMailboxRole, FolderAppointmentSource, MailboxSpecialUse, } from "@remit/domain-enums";
|
|
2
|
+
import { composeSettingName, SETTING_NAME_SEPARATOR, } from "./account-settings.js";
|
|
3
|
+
import { resolveMailboxByLeafName, } from "./mailbox-name.js";
|
|
4
|
+
/** The fixed anchor set, in the RFC's canonical display order. */
|
|
5
|
+
export const CANONICAL_ROLES = Object.values(CanonicalMailboxRole);
|
|
6
|
+
const isCanonicalRole = (value) => CANONICAL_ROLES.includes(value);
|
|
7
|
+
/**
|
|
8
|
+
* The stored `AccountSetting` name holding one role's appointment. Composed
|
|
9
|
+
* here rather than at either reader, so the backend that writes it and the
|
|
10
|
+
* repository that reads it can never disagree about the key.
|
|
11
|
+
*/
|
|
12
|
+
export const composeFolderRoleAppointmentName = (accountId, role) => composeSettingName(AccountSettingName.FolderRoleAppointment, `${accountId}${SETTING_NAME_SEPARATOR}${role}`);
|
|
13
|
+
/**
|
|
14
|
+
* The stored `AccountSetting` name holding the path one role's appointed
|
|
15
|
+
* mailbox had at the moment it was appointed. A sibling row rather than a field
|
|
16
|
+
* on the appointment, so "never read by resolution" is structural: the
|
|
17
|
+
* repository composes only the appointment name and cannot reach this one.
|
|
18
|
+
*/
|
|
19
|
+
export const composeFolderRoleAppointmentLabelName = (accountId, role) => composeSettingName(AccountSettingName.FolderRoleAppointmentLabel, `${accountId}${SETTING_NAME_SEPARATOR}${role}`);
|
|
20
|
+
const parseFolderRoleTarget = (base, name) => {
|
|
21
|
+
const [candidate, ...rest] = name.split(SETTING_NAME_SEPARATOR);
|
|
22
|
+
if (candidate !== base)
|
|
23
|
+
return undefined;
|
|
24
|
+
const role = rest[rest.length - 1];
|
|
25
|
+
const accountId = rest.slice(0, -1).join(SETTING_NAME_SEPARATOR);
|
|
26
|
+
if (!accountId || !role || !isCanonicalRole(role))
|
|
27
|
+
return undefined;
|
|
28
|
+
return { accountId, role };
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Split a stored appointment name back into its two-part target. Unlike the
|
|
32
|
+
* single-target composites (`MailboxRole#<id>`), this setting composes two ids
|
|
33
|
+
* after the base, so it parses the suffix itself rather than reusing
|
|
34
|
+
* `targetIdOf`.
|
|
35
|
+
*/
|
|
36
|
+
export const parseFolderRoleAppointmentName = (name) => parseFolderRoleTarget(AccountSettingName.FolderRoleAppointment, name);
|
|
37
|
+
/**
|
|
38
|
+
* The label row's counterpart, so the config-wide settings batch can recover
|
|
39
|
+
* the recorded path for a role the same way it recovers the appointment.
|
|
40
|
+
*/
|
|
41
|
+
export const parseFolderRoleAppointmentLabelName = (name) => parseFolderRoleTarget(AccountSettingName.FolderRoleAppointmentLabel, name);
|
|
42
|
+
/**
|
|
43
|
+
* RFC 6154 SPECIAL-USE flag per role. Inbox has no SPECIAL-USE flag (RFC 3501
|
|
44
|
+
* reserves the name itself); a role with no entry here is matched by name hint
|
|
45
|
+
* only.
|
|
46
|
+
*/
|
|
47
|
+
export const ROLE_SPECIAL_USE = {
|
|
48
|
+
[CanonicalMailboxRole.Drafts]: MailboxSpecialUse.Drafts,
|
|
49
|
+
[CanonicalMailboxRole.Sent]: MailboxSpecialUse.Sent,
|
|
50
|
+
[CanonicalMailboxRole.Archive]: MailboxSpecialUse.Archive,
|
|
51
|
+
[CanonicalMailboxRole.Junk]: MailboxSpecialUse.Junk,
|
|
52
|
+
[CanonicalMailboxRole.Trash]: MailboxSpecialUse.Trash,
|
|
53
|
+
[CanonicalMailboxRole.All]: MailboxSpecialUse.All,
|
|
54
|
+
[CanonicalMailboxRole.Flagged]: MailboxSpecialUse.Flagged,
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Weak name hints, ordered best-first and lower case: the last resort for a
|
|
58
|
+
* provider that advertises no SPECIAL-USE and whose user has appointed
|
|
59
|
+
* nothing. Deliberately small — a guess that seeds a proposal, never a
|
|
60
|
+
* substitute for server truth or for what the user chose.
|
|
61
|
+
*/
|
|
62
|
+
export const ROLE_NAME_HINTS = {
|
|
63
|
+
[CanonicalMailboxRole.Drafts]: ["drafts", "draft", "concepten"],
|
|
64
|
+
[CanonicalMailboxRole.Sent]: [
|
|
65
|
+
"sent",
|
|
66
|
+
"sent items",
|
|
67
|
+
"sent messages",
|
|
68
|
+
"sent mail",
|
|
69
|
+
],
|
|
70
|
+
[CanonicalMailboxRole.Archive]: ["archive", "archives", "all mail"],
|
|
71
|
+
[CanonicalMailboxRole.Junk]: [
|
|
72
|
+
"junk",
|
|
73
|
+
"spam",
|
|
74
|
+
"junk e-mail",
|
|
75
|
+
"junk email",
|
|
76
|
+
"bulk mail",
|
|
77
|
+
],
|
|
78
|
+
// No bare "deleted" and no "bin": both are ordinary folder names a user
|
|
79
|
+
// keeps mail in, and a wrong guess here files deletes into a folder the
|
|
80
|
+
// user never meant as Trash. Gmail's en-GB "Bin" and its "[Gmail]/Trash"
|
|
81
|
+
// both advertise \Trash, so the flag already covers them (#837).
|
|
82
|
+
[CanonicalMailboxRole.Trash]: ["trash", "deleted items", "deleted messages"],
|
|
83
|
+
[CanonicalMailboxRole.All]: ["all mail", "all"],
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* The role a folder's own leaf name is most conventionally for. A name that
|
|
87
|
+
* several roles list goes to the role that ranks it highest, so `All Mail` is
|
|
88
|
+
* an All folder rather than a lookalike of Archive.
|
|
89
|
+
*
|
|
90
|
+
* `name` is the leaf segment, lower case.
|
|
91
|
+
*/
|
|
92
|
+
export const roleForFolderName = (name) => {
|
|
93
|
+
let best;
|
|
94
|
+
for (const role of CANONICAL_ROLES) {
|
|
95
|
+
const rank = ROLE_NAME_HINTS[role]?.indexOf(name) ?? -1;
|
|
96
|
+
if (rank < 0)
|
|
97
|
+
continue;
|
|
98
|
+
if (!best || rank < best.rank)
|
|
99
|
+
best = { role, rank };
|
|
100
|
+
}
|
|
101
|
+
return best?.role;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Whether the mailbox sweep would read a folder with this leaf name as a
|
|
105
|
+
* duplicate of a role's canonical folder — and so delete its row, and under D8
|
|
106
|
+
* the mail in it, as soon as another folder carries the role's SPECIAL-USE
|
|
107
|
+
* flag. It is the sweep's own predicate, so a rename refused on it and a folder
|
|
108
|
+
* reaped by it can never disagree (docs/architecture/folder-rename-and-delete.md
|
|
109
|
+
* D5).
|
|
110
|
+
*
|
|
111
|
+
* A role with no flag to be a duplicate of is not reserved: nothing reaps a
|
|
112
|
+
* folder for it.
|
|
113
|
+
*/
|
|
114
|
+
export const isReservedFolderName = (name) => {
|
|
115
|
+
const role = roleForFolderName(name.toLowerCase());
|
|
116
|
+
return role !== undefined && ROLE_SPECIAL_USE[role] !== undefined;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* The pane where a user appoints a folder to a role, named as it is labelled.
|
|
120
|
+
* Every refusal that asks for an appointment points here, in one wording — the
|
|
121
|
+
* settings screen is titled "Folder roles", and copies of this sentence had
|
|
122
|
+
* already drifted to "Folders".
|
|
123
|
+
*/
|
|
124
|
+
export const FOLDER_ROLES_SETTINGS_PATH = "Settings › Folder roles";
|
|
125
|
+
/**
|
|
126
|
+
* Why an operation that files or destroys mail was refused. A delete, an
|
|
127
|
+
* Empty Trash and the count the handler reports before one all act on the
|
|
128
|
+
* folder the user appointed or the server flagged, and all refuse in these
|
|
129
|
+
* words — one sentence, so two surfaces cannot tell a user two things about
|
|
130
|
+
* one account.
|
|
131
|
+
*/
|
|
132
|
+
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.`;
|
|
133
|
+
/**
|
|
134
|
+
* The folder somebody chose as this account's Trash is no longer on the mail
|
|
135
|
+
* server. Reader will not quietly file mail into whatever it would have picked
|
|
136
|
+
* had nobody chosen: the user's choice went missing and only they can replace
|
|
137
|
+
* it.
|
|
138
|
+
*/
|
|
139
|
+
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.`;
|
|
140
|
+
/**
|
|
141
|
+
* A folder matches the Trash name hint and nothing else — enough to file a
|
|
142
|
+
* delete somewhere retrievable, never enough to erase what is already in it.
|
|
143
|
+
* Emptying that folder and deleting a message already inside it both land here
|
|
144
|
+
* (#876), so the sentence names neither verb: "nothing was deleted" is true of
|
|
145
|
+
* both, the way its two siblings above are.
|
|
146
|
+
*/
|
|
147
|
+
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.`;
|
|
148
|
+
const resolveWithoutAppointment = (role, mailboxes) => {
|
|
149
|
+
const specialUse = ROLE_SPECIAL_USE[role];
|
|
150
|
+
if (specialUse) {
|
|
151
|
+
const flagged = mailboxes.find((m) => m.specialUse?.includes(specialUse));
|
|
152
|
+
if (flagged)
|
|
153
|
+
return { kind: "flagged", mailbox: flagged };
|
|
154
|
+
}
|
|
155
|
+
if (role === CanonicalMailboxRole.Inbox) {
|
|
156
|
+
const inbox = mailboxes.find((m) => m.fullPath.toUpperCase() === "INBOX");
|
|
157
|
+
return inbox ? { kind: "reserved", mailbox: inbox } : { kind: "none" };
|
|
158
|
+
}
|
|
159
|
+
const hints = ROLE_NAME_HINTS[role];
|
|
160
|
+
if (!hints)
|
|
161
|
+
return { kind: "none" };
|
|
162
|
+
const proposed = resolveMailboxByLeafName(mailboxes, hints);
|
|
163
|
+
return proposed ? { kind: "proposed", mailbox: proposed } : { kind: "none" };
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* The single mailbox that holds a canonical role for an account (RFC 032
|
|
167
|
+
* exclusive-folder-appointment, #976), in one precedence order every caller
|
|
168
|
+
* shares:
|
|
169
|
+
*
|
|
170
|
+
* 1. the mailbox the user appointed;
|
|
171
|
+
* 2. the server's own SPECIAL-USE flag (RFC 6154) — language-independent truth;
|
|
172
|
+
* 3. the reserved `INBOX` name, for the Inbox role only (RFC 3501);
|
|
173
|
+
* 4. a conventional name, matched on the folder's own leaf segment so it
|
|
174
|
+
* resolves at any depth under any prefix.
|
|
175
|
+
*
|
|
176
|
+
* The last of those is a guess: a folder named `Deleted` is not evidence that
|
|
177
|
+
* the user means it as Trash. Use it only where being wrong misfiles a message
|
|
178
|
+
* a user can move back — never where it destroys mail.
|
|
179
|
+
*/
|
|
180
|
+
export const resolveRoleForAccount = (role, mailboxes, appointedMailboxId) => {
|
|
181
|
+
if (!appointedMailboxId)
|
|
182
|
+
return resolveWithoutAppointment(role, mailboxes);
|
|
183
|
+
const appointed = mailboxes.find((m) => m.mailboxId === appointedMailboxId);
|
|
184
|
+
if (appointed)
|
|
185
|
+
return { kind: "appointed", mailbox: appointed };
|
|
186
|
+
return {
|
|
187
|
+
kind: "appointment_stale",
|
|
188
|
+
appointedMailboxId,
|
|
189
|
+
fallback: resolveWithoutAppointment(role, mailboxes),
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* The Trash folder a verb at this assurance level may act on, or which of the
|
|
194
|
+
* three reasons stops it. One switch: the delete, the expunge and the count
|
|
195
|
+
* reported before one all decide here, so no two of them can answer a user
|
|
196
|
+
* differently about the same account.
|
|
197
|
+
*/
|
|
198
|
+
export const trashMailboxAt = (resolution, level) => {
|
|
199
|
+
switch (resolution.kind) {
|
|
200
|
+
case "appointed":
|
|
201
|
+
case "flagged":
|
|
202
|
+
return { allowed: true, mailbox: resolution.mailbox };
|
|
203
|
+
case "proposed":
|
|
204
|
+
return level === "resolved"
|
|
205
|
+
? { allowed: true, mailbox: resolution.mailbox }
|
|
206
|
+
: { allowed: false, reason: "unconfirmed" };
|
|
207
|
+
case "appointment_stale":
|
|
208
|
+
return { allowed: false, reason: "stale" };
|
|
209
|
+
default:
|
|
210
|
+
return { allowed: false, reason: "none" };
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
export const meetsTrashAssurance = (resolution, level) => trashMailboxAt(resolution, level).allowed;
|
|
214
|
+
/** The resolution each reported source came from (`toFolderAppointment`). */
|
|
215
|
+
const RESOLUTION_FOR_SOURCE = {
|
|
216
|
+
[FolderAppointmentSource.Appointed]: { kind: "appointed", mailbox: null },
|
|
217
|
+
[FolderAppointmentSource.Flagged]: { kind: "flagged", mailbox: null },
|
|
218
|
+
[FolderAppointmentSource.Reserved]: { kind: "reserved", mailbox: null },
|
|
219
|
+
[FolderAppointmentSource.Proposed]: { kind: "proposed", mailbox: null },
|
|
220
|
+
[FolderAppointmentSource.Stale]: {
|
|
221
|
+
kind: "appointment_stale",
|
|
222
|
+
appointedMailboxId: "",
|
|
223
|
+
fallback: { kind: "none" },
|
|
224
|
+
},
|
|
225
|
+
[FolderAppointmentSource.None]: { kind: "none" },
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* The same gate as {@link trashMailboxAt}, asked of the source `/config`
|
|
229
|
+
* reports rather than of the resolution the server computed — so a client can
|
|
230
|
+
* word a delete from the answer the service will give it.
|
|
231
|
+
*
|
|
232
|
+
* It runs that switch rather than restating it. A hand-written mirror gets
|
|
233
|
+
* `Reserved` wrong: it reads as designated, and the gate accepts only the user's
|
|
234
|
+
* appointment and the server's flag.
|
|
235
|
+
*/
|
|
236
|
+
export const trashSourceMeetsAssurance = (source, level) => meetsTrashAssurance(RESOLUTION_FOR_SOURCE[source], level);
|
|
237
|
+
/**
|
|
238
|
+
* The mailbox a role is CONFIRMED to hold: the one the user appointed, or the
|
|
239
|
+
* one the server flagged (RFC 6154). No name guessing — `null` here means
|
|
240
|
+
* nobody has said which folder this is, only that a folder happens to be named
|
|
241
|
+
* something plausible. An operation that destroys mail resolves through this
|
|
242
|
+
* and refuses when it comes back empty; `resolveMailboxForRole` adds the guess
|
|
243
|
+
* on top for the operations where being wrong only misfiles a message.
|
|
244
|
+
*
|
|
245
|
+
* An appointment naming a mailbox this account does not hold — deleted,
|
|
246
|
+
* renamed, or carried in from elsewhere — is stale, and answers `null` here
|
|
247
|
+
* too: the fallback is the folder the user did not choose, which is fine to
|
|
248
|
+
* file mail into and no basis at all for destroying it.
|
|
249
|
+
*/
|
|
250
|
+
export const resolveConfirmedMailboxForRole = (role, mailboxes, appointedMailboxId) => {
|
|
251
|
+
const resolution = resolveRoleForAccount(role, mailboxes, appointedMailboxId);
|
|
252
|
+
switch (resolution.kind) {
|
|
253
|
+
case "appointed":
|
|
254
|
+
case "flagged":
|
|
255
|
+
case "reserved":
|
|
256
|
+
return resolution.mailbox;
|
|
257
|
+
default:
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* The role's mailbox including the name guess: `null` only when nothing
|
|
263
|
+
* matches at all, so the caller says the role has no folder rather than picking
|
|
264
|
+
* one. A stale appointment answers with its fallback — every role but Trash
|
|
265
|
+
* files mail on this read, and refusing them all would be a behaviour change
|
|
266
|
+
* with no surface to explain it.
|
|
267
|
+
*
|
|
268
|
+
* Built on `resolveRoleForAccount` directly, never on the confirmed adapter: a
|
|
269
|
+
* stale appointment must still reach the `\Sent` flag here, not fall past it
|
|
270
|
+
* into a folder that merely reads like one.
|
|
271
|
+
*/
|
|
272
|
+
export const resolveMailboxForRole = (role, mailboxes, appointedMailboxId) => {
|
|
273
|
+
const resolution = resolveRoleForAccount(role, mailboxes, appointedMailboxId);
|
|
274
|
+
const effective = resolution.kind === "appointment_stale" ? resolution.fallback : resolution;
|
|
275
|
+
return effective.kind === "none" ? null : effective.mailbox;
|
|
276
|
+
};
|
package/dist/id.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { MessageIdSource, SyncWarningItem } from "./types.js";
|
|
2
|
+
export declare const base36uuid: () => string;
|
|
3
|
+
export declare const base36uuidv5: (name: string, namespace: string) => string;
|
|
4
|
+
export declare const REMIT_NAMESPACE = "9e89694d-214b-4d9b-99f5-214b4d9b99f5";
|
|
5
|
+
export declare const deriveAccountConfigId: (userId: string) => string;
|
|
6
|
+
export declare const deriveAddressId: (accountConfigId: string, email: string) => string;
|
|
7
|
+
/**
|
|
8
|
+
* The stored form of a calendar's URL segment (issue #15). A DAV path segment
|
|
9
|
+
* is matched case-insensitively by the clients that bookmark one, so the folded
|
|
10
|
+
* form is what both the derived id and the stored column are built from — the
|
|
11
|
+
* id derivation folding on its own would let `findByUrlSegment` and a `get` by
|
|
12
|
+
* derived id disagree about the same path.
|
|
13
|
+
*/
|
|
14
|
+
export declare const normalizeCalendarUrlSegment: (urlSegment: string) => string;
|
|
15
|
+
/**
|
|
16
|
+
* Identity of a calendar collection (issue #15). Derived from the account
|
|
17
|
+
* config and the collection's URL segment, so the CalDAV path a client
|
|
18
|
+
* bookmarks resolves to a row with no lookup table, and provisioning the
|
|
19
|
+
* default collection twice — two concurrent first uses of the same account —
|
|
20
|
+
* writes the same row rather than a second, unreachable one.
|
|
21
|
+
*/
|
|
22
|
+
export declare const deriveCalendarId: (accountConfigId: string, urlSegment: string) => string;
|
|
23
|
+
/**
|
|
24
|
+
* Identity of a calendar's feed address (issue #1067). Derived from the
|
|
25
|
+
* collection, so one calendar holds one active token by construction: rotating
|
|
26
|
+
* rewrites the row it already has instead of leaving a second live address
|
|
27
|
+
* nobody can see to revoke.
|
|
28
|
+
*/
|
|
29
|
+
export declare const deriveCalendarFeedTokenId: (calendarId: string) => string;
|
|
30
|
+
/**
|
|
31
|
+
* Identity of a calendar resource (issue #15). Derived from its collection and
|
|
32
|
+
* its resource name — the last path segment of the resource's URL — so a
|
|
33
|
+
* repeated PUT of the same resource rewrites its own row instead of forking a
|
|
34
|
+
* duplicate the collection can never reach.
|
|
35
|
+
*
|
|
36
|
+
* The resource name, not the iCalendar UID, is the name: CalDAV addresses a
|
|
37
|
+
* resource by URL, and a client is free to store the same UID under a second
|
|
38
|
+
* name (RFC 4791 4.1). Keying on the UID would make those two resources one
|
|
39
|
+
* row and silently lose the second.
|
|
40
|
+
*/
|
|
41
|
+
export declare const deriveCalendarObjectId: (calendarId: string, resourceName: string) => string;
|
|
42
|
+
/**
|
|
43
|
+
* Identity of a suggestion read out of a message (issue #1033). Derived from
|
|
44
|
+
* the message, the MIME part the bytes came from and the event's iCalendar
|
|
45
|
+
* UID, so re-reading the same message converges on the row it already wrote
|
|
46
|
+
* instead of stacking a second card on the message every pass.
|
|
47
|
+
*
|
|
48
|
+
* All three parts are needed. The message alone would merge two invitations
|
|
49
|
+
* sent in one mail; the part alone is not stable enough to key on across a
|
|
50
|
+
* re-parse; and the UID keeps two events sent as one part apart.
|
|
51
|
+
*
|
|
52
|
+
* The message is in the seed, so identity is per message, not per event: a
|
|
53
|
+
* later message carrying the same UID derives its own id, and the earlier
|
|
54
|
+
* suggestion survives beside it as `Superseded`. The user can see which
|
|
55
|
+
* revision they are being asked about, and the older message still has a card.
|
|
56
|
+
* The same holds for a plain resend — a redelivered copy is a new message and
|
|
57
|
+
* so a new card, which the SEQUENCE comparison then declines to supersede
|
|
58
|
+
* anything for.
|
|
59
|
+
*/
|
|
60
|
+
export declare const deriveCalendarSuggestionId: (messageId: string, bodyPartId: string, icalUid: string) => string;
|
|
61
|
+
export declare const bareMessageId: (value: string) => string;
|
|
62
|
+
export declare const wireMessageId: (value: string) => string;
|
|
63
|
+
export declare const isValidMessageId: (messageId: string | undefined) => boolean;
|
|
64
|
+
export declare const normalizeMessageIdHeader: (source: MessageIdSource) => string;
|
|
65
|
+
export declare const deriveMessageId: (accountId: string, messageIdHeader: string) => string;
|
|
66
|
+
export declare const deriveMessageIdFromSource: (accountId: string, source: MessageIdSource) => string;
|
|
67
|
+
/**
|
|
68
|
+
* Identity of a message copied into another folder (issue #75). Derived from
|
|
69
|
+
* the source message and the destination mailbox so a copy is deterministic:
|
|
70
|
+
* copying the same mail into the same folder twice — a replayed COPY event or a
|
|
71
|
+
* repeated user copy — resolves to one row instead of a fresh unreachable
|
|
72
|
+
* duplicate each time. A random id, which is what the copy path used before,
|
|
73
|
+
* gave every attempt a new identity that no later sync or delete could reach.
|
|
74
|
+
*
|
|
75
|
+
* The destination mailbox is part of the name so a copy of one mail into two
|
|
76
|
+
* folders is two rows, one per folder. It also keeps this id out of the space
|
|
77
|
+
* sync assigns: sync keys a message on `deriveMessageId(accountId,
|
|
78
|
+
* messageIdHeader)`, which is folder-independent, so the copy row can never
|
|
79
|
+
* collide with, or be overwritten by, the canonical synced row.
|
|
80
|
+
*/
|
|
81
|
+
export declare const deriveCopyMessageId: (sourceMessageId: string, destinationMailboxId: string) => string;
|
|
82
|
+
/**
|
|
83
|
+
* Identity of a conversation (issue #1017). Keyed on the account config, not on
|
|
84
|
+
* the account: two mailboxes connected to one config hold the same conversation,
|
|
85
|
+
* and keying on the account gave each of them its own thread rooted at the same
|
|
86
|
+
* header — so a reply sent from the second account opened a conversation of one
|
|
87
|
+
* beside the one it answered.
|
|
88
|
+
*
|
|
89
|
+
* Messages stay per account (`deriveMessageId`). Each account keeps its own copy
|
|
90
|
+
* of a mail, and the copies meet in one thread through the shared root header.
|
|
91
|
+
*/
|
|
92
|
+
export declare const deriveThreadId: (accountConfigId: string, rootMessageIdHeader: string) => string;
|
|
93
|
+
export declare const deriveEnvelopeId: (messageId: string) => string;
|
|
94
|
+
/**
|
|
95
|
+
* Canonical IMAP path for the root MIME node. BODYSTRUCTURE leaves the
|
|
96
|
+
* root's `part` blank; we assign "0" so it has a stable, unambiguous key
|
|
97
|
+
* across the codebase (BodyPart row id, S3 key, seed scripts).
|
|
98
|
+
*/
|
|
99
|
+
export declare const ROOT_PART_PATH = "0";
|
|
100
|
+
export declare const deriveBodyPartId: (messageId: string, partPath: string) => string;
|
|
101
|
+
/**
|
|
102
|
+
* Identity of a quarantined message (issue #72). Derived rather than generated
|
|
103
|
+
* so re-quarantining the same message rewrites its own row: the sync path can
|
|
104
|
+
* write without first checking whether an entry already exists.
|
|
105
|
+
*
|
|
106
|
+
* `uidValidity` is part of the name because a uid alone does not identify a
|
|
107
|
+
* message — only uidValidity + uid does, forever (RFC 9051 2.3.1.1). A mailbox
|
|
108
|
+
* keeps its `mailboxId` across a UIDVALIDITY bump (the sync path updates the
|
|
109
|
+
* same row), so without this a stale entry would derive the same id as a later,
|
|
110
|
+
* unrelated message and suppress it from a sync round.
|
|
111
|
+
*/
|
|
112
|
+
export declare const deriveQuarantineId: (accountId: string, mailboxId: string, uidValidity: number, uid: number) => string;
|
|
113
|
+
/**
|
|
114
|
+
* The `sha256:`-prefixed Message-ID hash a quarantine record carries, or
|
|
115
|
+
* undefined when the message declared no usable Message-ID.
|
|
116
|
+
*
|
|
117
|
+
* The sync path coerces a missing Message-ID to `""`, and hashing that would
|
|
118
|
+
* give every Message-ID-less message the same hash — so the one field whose
|
|
119
|
+
* purpose is correlating reports of the same message would silently correlate
|
|
120
|
+
* unrelated ones. Absent is the honest value, and this is the only way the
|
|
121
|
+
* value is produced, so the empty hash cannot be written by accident.
|
|
122
|
+
*/
|
|
123
|
+
export declare const quarantineMessageIdHash: (messageIdHeader: string | undefined) => string | undefined;
|
|
124
|
+
export declare const deriveSyncWarningId: (accountId: string, mailboxId: string, uidValidity: number, uid: number, stage: SyncWarningItem["stage"]) => string;
|
package/{src/id.ts → dist/id.js}
RENAMED
|
@@ -1,26 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import shortUuid from "short-uuid";
|
|
3
3
|
import { v5 as uuidv5 } from "uuid";
|
|
4
|
-
import type { MessageIdSource } from "./types.js";
|
|
5
|
-
|
|
6
4
|
const translator = shortUuid.createTranslator(shortUuid.constants.uuid25Base36);
|
|
7
|
-
|
|
8
|
-
export const
|
|
9
|
-
|
|
10
|
-
export const base36uuidv5 = (name: string, namespace: string): string =>
|
|
11
|
-
translator.fromUUID(uuidv5(name, namespace));
|
|
12
|
-
|
|
5
|
+
export const base36uuid = () => translator.generate();
|
|
6
|
+
export const base36uuidv5 = (name, namespace) => translator.fromUUID(uuidv5(name, namespace));
|
|
13
7
|
export const REMIT_NAMESPACE = "9e89694d-214b-4d9b-99f5-214b4d9b99f5";
|
|
14
|
-
|
|
15
|
-
export const deriveAddressId = (
|
|
16
|
-
accountConfigId: string,
|
|
17
|
-
email: string,
|
|
18
|
-
): string =>
|
|
19
|
-
base36uuidv5(
|
|
20
|
-
`address:${accountConfigId}:${email.toLowerCase()}`,
|
|
21
|
-
REMIT_NAMESPACE,
|
|
22
|
-
);
|
|
23
|
-
|
|
8
|
+
export const deriveAccountConfigId = (userId) => base36uuidv5(`account:${userId}`, REMIT_NAMESPACE);
|
|
9
|
+
export const deriveAddressId = (accountConfigId, email) => base36uuidv5(`address:${accountConfigId}:${email.toLowerCase()}`, REMIT_NAMESPACE);
|
|
24
10
|
/**
|
|
25
11
|
* The stored form of a calendar's URL segment (issue #15). A DAV path segment
|
|
26
12
|
* is matched case-insensitively by the clients that bookmark one, so the folded
|
|
@@ -28,9 +14,7 @@ export const deriveAddressId = (
|
|
|
28
14
|
* id derivation folding on its own would let `findByUrlSegment` and a `get` by
|
|
29
15
|
* derived id disagree about the same path.
|
|
30
16
|
*/
|
|
31
|
-
export const normalizeCalendarUrlSegment = (urlSegment
|
|
32
|
-
urlSegment.trim().toLowerCase();
|
|
33
|
-
|
|
17
|
+
export const normalizeCalendarUrlSegment = (urlSegment) => urlSegment.trim().toLowerCase();
|
|
34
18
|
/**
|
|
35
19
|
* Identity of a calendar collection (issue #15). Derived from the account
|
|
36
20
|
* config and the collection's URL segment, so the CalDAV path a client
|
|
@@ -38,24 +22,14 @@ export const normalizeCalendarUrlSegment = (urlSegment: string): string =>
|
|
|
38
22
|
* default collection twice — two concurrent first uses of the same account —
|
|
39
23
|
* writes the same row rather than a second, unreachable one.
|
|
40
24
|
*/
|
|
41
|
-
export const deriveCalendarId = (
|
|
42
|
-
accountConfigId: string,
|
|
43
|
-
urlSegment: string,
|
|
44
|
-
): string =>
|
|
45
|
-
base36uuidv5(
|
|
46
|
-
`calendar:${accountConfigId}:${normalizeCalendarUrlSegment(urlSegment)}`,
|
|
47
|
-
REMIT_NAMESPACE,
|
|
48
|
-
);
|
|
49
|
-
|
|
25
|
+
export const deriveCalendarId = (accountConfigId, urlSegment) => base36uuidv5(`calendar:${accountConfigId}:${normalizeCalendarUrlSegment(urlSegment)}`, REMIT_NAMESPACE);
|
|
50
26
|
/**
|
|
51
27
|
* Identity of a calendar's feed address (issue #1067). Derived from the
|
|
52
28
|
* collection, so one calendar holds one active token by construction: rotating
|
|
53
29
|
* rewrites the row it already has instead of leaving a second live address
|
|
54
30
|
* nobody can see to revoke.
|
|
55
31
|
*/
|
|
56
|
-
export const deriveCalendarFeedTokenId = (calendarId
|
|
57
|
-
base36uuidv5(`calendarfeedtoken:${calendarId}`, REMIT_NAMESPACE);
|
|
58
|
-
|
|
32
|
+
export const deriveCalendarFeedTokenId = (calendarId) => base36uuidv5(`calendarfeedtoken:${calendarId}`, REMIT_NAMESPACE);
|
|
59
33
|
/**
|
|
60
34
|
* Identity of a calendar resource (issue #15). Derived from its collection and
|
|
61
35
|
* its resource name — the last path segment of the resource's URL — so a
|
|
@@ -67,12 +41,7 @@ export const deriveCalendarFeedTokenId = (calendarId: string): string =>
|
|
|
67
41
|
* name (RFC 4791 4.1). Keying on the UID would make those two resources one
|
|
68
42
|
* row and silently lose the second.
|
|
69
43
|
*/
|
|
70
|
-
export const deriveCalendarObjectId = (
|
|
71
|
-
calendarId: string,
|
|
72
|
-
resourceName: string,
|
|
73
|
-
): string =>
|
|
74
|
-
base36uuidv5(`calendarobject:${calendarId}:${resourceName}`, REMIT_NAMESPACE);
|
|
75
|
-
|
|
44
|
+
export const deriveCalendarObjectId = (calendarId, resourceName) => base36uuidv5(`calendarobject:${calendarId}:${resourceName}`, REMIT_NAMESPACE);
|
|
76
45
|
/**
|
|
77
46
|
* Identity of a suggestion read out of a message (issue #1033). Derived from
|
|
78
47
|
* the message, the MIME part the bytes came from and the event's iCalendar
|
|
@@ -91,51 +60,27 @@ export const deriveCalendarObjectId = (
|
|
|
91
60
|
* so a new card, which the SEQUENCE comparison then declines to supersede
|
|
92
61
|
* anything for.
|
|
93
62
|
*/
|
|
94
|
-
export const deriveCalendarSuggestionId = (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
63
|
+
export const deriveCalendarSuggestionId = (messageId, bodyPartId, icalUid) => base36uuidv5(`calendarsuggestion:${messageId}:${bodyPartId}:${icalUid}`, REMIT_NAMESPACE);
|
|
64
|
+
export const bareMessageId = (value) => value.replace(/^[\s<]+|[\s>]+$/g, "");
|
|
65
|
+
export const wireMessageId = (value) => `<${bareMessageId(value)}>`;
|
|
66
|
+
export const isValidMessageId = (messageId) => messageId !== undefined && bareMessageId(messageId) !== "";
|
|
67
|
+
export const normalizeMessageIdHeader = (source) => {
|
|
68
|
+
if (isValidMessageId(source.messageId)) {
|
|
69
|
+
return source.messageId;
|
|
70
|
+
}
|
|
71
|
+
const parts = [
|
|
72
|
+
"generated",
|
|
73
|
+
source.mailboxId,
|
|
74
|
+
source.uid.toString(),
|
|
75
|
+
source.date || "",
|
|
76
|
+
source.subject || "",
|
|
77
|
+
source.fromMailbox || "",
|
|
78
|
+
source.fromHost || "",
|
|
79
|
+
];
|
|
80
|
+
return parts.join(":");
|
|
108
81
|
};
|
|
109
|
-
|
|
110
|
-
export const
|
|
111
|
-
if (isValidMessageId(source.messageId)) {
|
|
112
|
-
return source.messageId as string;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const parts = [
|
|
116
|
-
"generated",
|
|
117
|
-
source.mailboxId,
|
|
118
|
-
source.uid.toString(),
|
|
119
|
-
source.date || "",
|
|
120
|
-
source.subject || "",
|
|
121
|
-
source.fromMailbox || "",
|
|
122
|
-
source.fromHost || "",
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
return parts.join(":");
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
export const deriveMessageId = (
|
|
129
|
-
accountId: string,
|
|
130
|
-
messageIdHeader: string,
|
|
131
|
-
): string =>
|
|
132
|
-
base36uuidv5(`message:${accountId}:${messageIdHeader}`, REMIT_NAMESPACE);
|
|
133
|
-
|
|
134
|
-
export const deriveMessageIdFromSource = (
|
|
135
|
-
accountId: string,
|
|
136
|
-
source: MessageIdSource,
|
|
137
|
-
): string => deriveMessageId(accountId, normalizeMessageIdHeader(source));
|
|
138
|
-
|
|
82
|
+
export const deriveMessageId = (accountId, messageIdHeader) => base36uuidv5(`message:${accountId}:${messageIdHeader}`, REMIT_NAMESPACE);
|
|
83
|
+
export const deriveMessageIdFromSource = (accountId, source) => deriveMessageId(accountId, normalizeMessageIdHeader(source));
|
|
139
84
|
/**
|
|
140
85
|
* Identity of a message copied into another folder (issue #75). Derived from
|
|
141
86
|
* the source message and the destination mailbox so a copy is deterministic:
|
|
@@ -150,15 +95,7 @@ export const deriveMessageIdFromSource = (
|
|
|
150
95
|
* messageIdHeader)`, which is folder-independent, so the copy row can never
|
|
151
96
|
* collide with, or be overwritten by, the canonical synced row.
|
|
152
97
|
*/
|
|
153
|
-
export const deriveCopyMessageId = (
|
|
154
|
-
sourceMessageId: string,
|
|
155
|
-
destinationMailboxId: string,
|
|
156
|
-
): string =>
|
|
157
|
-
base36uuidv5(
|
|
158
|
-
`messagecopy:${sourceMessageId}:${destinationMailboxId}`,
|
|
159
|
-
REMIT_NAMESPACE,
|
|
160
|
-
);
|
|
161
|
-
|
|
98
|
+
export const deriveCopyMessageId = (sourceMessageId, destinationMailboxId) => base36uuidv5(`messagecopy:${sourceMessageId}:${destinationMailboxId}`, REMIT_NAMESPACE);
|
|
162
99
|
/**
|
|
163
100
|
* Identity of a conversation (issue #1017). Keyed on the account config, not on
|
|
164
101
|
* the account: two mailboxes connected to one config hold the same conversation,
|
|
@@ -169,28 +106,15 @@ export const deriveCopyMessageId = (
|
|
|
169
106
|
* Messages stay per account (`deriveMessageId`). Each account keeps its own copy
|
|
170
107
|
* of a mail, and the copies meet in one thread through the shared root header.
|
|
171
108
|
*/
|
|
172
|
-
export const deriveThreadId = (
|
|
173
|
-
|
|
174
|
-
rootMessageIdHeader: string,
|
|
175
|
-
): string =>
|
|
176
|
-
base36uuidv5(
|
|
177
|
-
`thread:${accountConfigId}:${rootMessageIdHeader.toLowerCase()}`,
|
|
178
|
-
REMIT_NAMESPACE,
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
export const deriveEnvelopeId = (messageId: string): string =>
|
|
182
|
-
base36uuidv5(`envelope:${messageId}`, REMIT_NAMESPACE);
|
|
183
|
-
|
|
109
|
+
export const deriveThreadId = (accountConfigId, rootMessageIdHeader) => base36uuidv5(`thread:${accountConfigId}:${rootMessageIdHeader.toLowerCase()}`, REMIT_NAMESPACE);
|
|
110
|
+
export const deriveEnvelopeId = (messageId) => base36uuidv5(`envelope:${messageId}`, REMIT_NAMESPACE);
|
|
184
111
|
/**
|
|
185
112
|
* Canonical IMAP path for the root MIME node. BODYSTRUCTURE leaves the
|
|
186
113
|
* root's `part` blank; we assign "0" so it has a stable, unambiguous key
|
|
187
114
|
* across the codebase (BodyPart row id, S3 key, seed scripts).
|
|
188
115
|
*/
|
|
189
116
|
export const ROOT_PART_PATH = "0";
|
|
190
|
-
|
|
191
|
-
export const deriveBodyPartId = (messageId: string, partPath: string): string =>
|
|
192
|
-
base36uuidv5(`bodypart:${messageId}:${partPath}`, REMIT_NAMESPACE);
|
|
193
|
-
|
|
117
|
+
export const deriveBodyPartId = (messageId, partPath) => base36uuidv5(`bodypart:${messageId}:${partPath}`, REMIT_NAMESPACE);
|
|
194
118
|
/**
|
|
195
119
|
* Identity of a quarantined message (issue #72). Derived rather than generated
|
|
196
120
|
* so re-quarantining the same message rewrites its own row: the sync path can
|
|
@@ -202,17 +126,7 @@ export const deriveBodyPartId = (messageId: string, partPath: string): string =>
|
|
|
202
126
|
* same row), so without this a stale entry would derive the same id as a later,
|
|
203
127
|
* unrelated message and suppress it from a sync round.
|
|
204
128
|
*/
|
|
205
|
-
export const deriveQuarantineId = (
|
|
206
|
-
accountId: string,
|
|
207
|
-
mailboxId: string,
|
|
208
|
-
uidValidity: number,
|
|
209
|
-
uid: number,
|
|
210
|
-
): string =>
|
|
211
|
-
base36uuidv5(
|
|
212
|
-
`quarantine:${accountId}:${mailboxId}:${uidValidity}:${uid}`,
|
|
213
|
-
REMIT_NAMESPACE,
|
|
214
|
-
);
|
|
215
|
-
|
|
129
|
+
export const deriveQuarantineId = (accountId, mailboxId, uidValidity, uid) => base36uuidv5(`quarantine:${accountId}:${mailboxId}:${uidValidity}:${uid}`, REMIT_NAMESPACE);
|
|
216
130
|
/**
|
|
217
131
|
* The `sha256:`-prefixed Message-ID hash a quarantine record carries, or
|
|
218
132
|
* undefined when the message declared no usable Message-ID.
|
|
@@ -223,12 +137,12 @@ export const deriveQuarantineId = (
|
|
|
223
137
|
* unrelated ones. Absent is the honest value, and this is the only way the
|
|
224
138
|
* value is produced, so the empty hash cannot be written by accident.
|
|
225
139
|
*/
|
|
226
|
-
export const quarantineMessageIdHash = (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return `sha256:${digest}`;
|
|
140
|
+
export const quarantineMessageIdHash = (messageIdHeader) => {
|
|
141
|
+
if (!messageIdHeader || !isValidMessageId(messageIdHeader))
|
|
142
|
+
return undefined;
|
|
143
|
+
const digest = createHash("sha256")
|
|
144
|
+
.update(messageIdHeader.trim())
|
|
145
|
+
.digest("hex");
|
|
146
|
+
return `sha256:${digest}`;
|
|
234
147
|
};
|
|
148
|
+
export const deriveSyncWarningId = (accountId, mailboxId, uidValidity, uid, stage) => base36uuidv5(`syncwarning:${accountId}:${mailboxId}:${uidValidity}:${uid}:${stage}`, REMIT_NAMESPACE);
|