@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
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
CreateThreadMessageInput,
|
|
3
|
-
ResultList,
|
|
4
|
-
SearchOptions,
|
|
5
|
-
ThreadMessageFieldTerm,
|
|
6
|
-
ThreadMessageItem,
|
|
7
|
-
UpdateThreadMessageInput,
|
|
8
|
-
} from "../types.js";
|
|
9
|
-
|
|
10
|
-
export interface IThreadMessageRepository {
|
|
11
|
-
create(input: CreateThreadMessageInput): Promise<ThreadMessageItem>;
|
|
12
|
-
get(
|
|
13
|
-
accountConfigId: string,
|
|
14
|
-
threadMessageId: string,
|
|
15
|
-
): Promise<ThreadMessageItem>;
|
|
16
|
-
get(
|
|
17
|
-
accountConfigId: string,
|
|
18
|
-
threadMessageIds: string[],
|
|
19
|
-
): Promise<ThreadMessageItem[]>;
|
|
20
|
-
update(
|
|
21
|
-
accountConfigId: string,
|
|
22
|
-
threadMessageId: string,
|
|
23
|
-
input: UpdateThreadMessageInput,
|
|
24
|
-
options?: {
|
|
25
|
-
composites?: {
|
|
26
|
-
sentDate?: number;
|
|
27
|
-
mailboxId?: string;
|
|
28
|
-
isRead?: boolean;
|
|
29
|
-
isDeleted?: boolean;
|
|
30
|
-
hasStars?: boolean;
|
|
31
|
-
hasAttachment?: boolean;
|
|
32
|
-
};
|
|
33
|
-
},
|
|
34
|
-
): Promise<ThreadMessageItem>;
|
|
35
|
-
delete(accountConfigId: string, threadMessageId: string): Promise<void>;
|
|
36
|
-
deleteMany(
|
|
37
|
-
keys: Array<{ accountConfigId: string; threadMessageId: string }>,
|
|
38
|
-
): Promise<void>;
|
|
39
|
-
listByAccount(
|
|
40
|
-
accountConfigId: string,
|
|
41
|
-
options?: { limit?: number; continuationToken?: string },
|
|
42
|
-
): Promise<ResultList<ThreadMessageItem>>;
|
|
43
|
-
listByDate(
|
|
44
|
-
accountConfigId: string,
|
|
45
|
-
options?: {
|
|
46
|
-
order?: "asc" | "desc";
|
|
47
|
-
limit?: number;
|
|
48
|
-
continuationToken?: string;
|
|
49
|
-
inboxMailboxIds?: Set<string>;
|
|
50
|
-
excludeDeleted?: boolean;
|
|
51
|
-
/**
|
|
52
|
-
* Row criteria applied inside the query, so a page is a page of
|
|
53
|
-
* matches however rare the criterion is. Filtering the rows a page
|
|
54
|
-
* returned instead is what makes a listing look empty whenever the
|
|
55
|
-
* matching mail sits below the newest page (#308).
|
|
56
|
-
*/
|
|
57
|
-
search?: SearchOptions;
|
|
58
|
-
},
|
|
59
|
-
): Promise<ResultList<ThreadMessageItem>>;
|
|
60
|
-
/**
|
|
61
|
-
* Search rows for a config across every mailbox in `mailboxIds` — the
|
|
62
|
-
* cross-account, cross-folder counterpart of `searchByMailbox`. A row whose
|
|
63
|
-
* From matches any `query` term ranks above one that only mentions the
|
|
64
|
-
* terms, and each tier runs in date order.
|
|
65
|
-
*
|
|
66
|
-
* The caller supplies the mailbox scope, so a search reaching Archive, Sent, Spam and
|
|
67
|
-
* custom folders is a matter of which set is passed. An empty or absent
|
|
68
|
-
* `mailboxIds` means no narrowing (every mailbox of the config), which is why
|
|
69
|
-
* handlers pass an explicitly built set rather than relying on the default.
|
|
70
|
-
*
|
|
71
|
-
* Matching follows `searchByMailbox`: `query` splits on whitespace and every
|
|
72
|
-
* term must match subject OR From, case-insensitively. `limit` is a page size
|
|
73
|
-
* over MATCHES, clamped server-side, so a short page means the matches ran
|
|
74
|
-
* out — never that a read window was exhausted.
|
|
75
|
-
*
|
|
76
|
-
* Rows are per mailbox, not per conversation: the same mail filed in two
|
|
77
|
-
* folders is two rows sharing a `threadId`.
|
|
78
|
-
*/
|
|
79
|
-
searchByDate(
|
|
80
|
-
accountConfigId: string,
|
|
81
|
-
search: SearchOptions,
|
|
82
|
-
options?: {
|
|
83
|
-
order?: "asc" | "desc";
|
|
84
|
-
limit?: number;
|
|
85
|
-
continuationToken?: string;
|
|
86
|
-
mailboxIds?: Set<string>;
|
|
87
|
-
excludeDeleted?: boolean;
|
|
88
|
-
},
|
|
89
|
-
): Promise<ResultList<ThreadMessageItem>>;
|
|
90
|
-
/**
|
|
91
|
-
* Rows of a config whose sender, subject or `List-Id` column matches a set
|
|
92
|
-
* of substring terms, newest first, across every mailbox — the store-side
|
|
93
|
-
* narrowing a rule back-apply reads with.
|
|
94
|
-
*
|
|
95
|
-
* The terms are evaluated in the query, so `limit` is a page size over
|
|
96
|
-
* MATCHES and the scan reaches the whole config however old the matching
|
|
97
|
-
* mail is. Paging a date-ordered listing and filtering the rows it returned
|
|
98
|
-
* is what makes a rule for a quiet sender report a handful of matches on a
|
|
99
|
-
* mailbox holding thousands (#459).
|
|
100
|
-
*
|
|
101
|
-
* `operator` combines the terms — `"and"` requires every one, `"or"` any.
|
|
102
|
-
* An empty `terms` narrows nothing under either operator: the whole config,
|
|
103
|
-
* newest first.
|
|
104
|
-
*
|
|
105
|
-
* A term an implementation cannot evaluate faithfully is IGNORED, never
|
|
106
|
-
* approximated: the result may be wider than the terms asked for and never
|
|
107
|
-
* narrower, so the caller's own matcher stays the judge.
|
|
108
|
-
*
|
|
109
|
-
* Matching follows `searchByMailbox`: accent- and case-insensitive
|
|
110
|
-
* substring over the same folded columns.
|
|
111
|
-
*
|
|
112
|
-
* Rows are per mailbox, not per conversation: the same mail filed in two
|
|
113
|
-
* folders is two rows sharing a `threadId`. `mailboxIds` narrows the rows
|
|
114
|
-
* to those folders; omitting it reads every mailbox.
|
|
115
|
-
*/
|
|
116
|
-
listByFieldTerms(
|
|
117
|
-
accountConfigId: string,
|
|
118
|
-
terms: readonly ThreadMessageFieldTerm[],
|
|
119
|
-
options?: {
|
|
120
|
-
operator?: "and" | "or";
|
|
121
|
-
order?: "asc" | "desc";
|
|
122
|
-
limit?: number;
|
|
123
|
-
continuationToken?: string;
|
|
124
|
-
excludeDeleted?: boolean;
|
|
125
|
-
mailboxIds?: readonly string[];
|
|
126
|
-
},
|
|
127
|
-
): Promise<ResultList<ThreadMessageItem>>;
|
|
128
|
-
/**
|
|
129
|
-
* List starred rows for a config, newest first, across every mailbox.
|
|
130
|
-
*
|
|
131
|
-
* Backed by the `byStarred` index (pk = accountConfigId, sk = hasStars +
|
|
132
|
-
* sentDate). Starredness is read from `hasStars` — the boolean of record —
|
|
133
|
-
* never from the presentation-only `star` colour. `mailboxIds` narrows the
|
|
134
|
-
* result to a caller-supplied set (used to drop muted mailboxes and the
|
|
135
|
-
* folders a star never surfaces from); omitting it returns every mailbox.
|
|
136
|
-
*
|
|
137
|
-
* Rows are per mailbox, not per conversation: the same mail filed in two
|
|
138
|
-
* folders is two rows sharing a `threadId`. Callers that render one row per
|
|
139
|
-
* conversation must collapse by `threadId` across the pages they have
|
|
140
|
-
* accumulated — this cannot be done inside a single page.
|
|
141
|
-
*/
|
|
142
|
-
listByStarred(
|
|
143
|
-
accountConfigId: string,
|
|
144
|
-
options?: {
|
|
145
|
-
order?: "asc" | "desc";
|
|
146
|
-
limit?: number;
|
|
147
|
-
continuationToken?: string;
|
|
148
|
-
mailboxIds?: Set<string>;
|
|
149
|
-
excludeDeleted?: boolean;
|
|
150
|
-
/** Row criteria applied inside the query; see `listByDate`. */
|
|
151
|
-
search?: SearchOptions;
|
|
152
|
-
},
|
|
153
|
-
): Promise<ResultList<ThreadMessageItem>>;
|
|
154
|
-
/**
|
|
155
|
-
* COUNT of matching CONVERSATIONS over the same predicate and the same
|
|
156
|
-
* mailbox scope the three cross-account listing modes read with — the scoped
|
|
157
|
-
* counterpart of `countByMailbox`.
|
|
158
|
-
*
|
|
159
|
-
* A page size bounds the rows a response carries and has no bearing on how
|
|
160
|
-
* much matches, so a count returns no rows and is answered in full.
|
|
161
|
-
*
|
|
162
|
-
* Distinct on `threadId`, unlike `countByMailbox`, because the cross-account
|
|
163
|
-
* listing is collapsed by thread before it is rendered: a row is per mailbox,
|
|
164
|
-
* so one message reachable through a real folder and a virtual copy of it is
|
|
165
|
-
* several rows, and two matching messages of one conversation are two more.
|
|
166
|
-
*/
|
|
167
|
-
countThreadsInScope(
|
|
168
|
-
accountConfigId: string,
|
|
169
|
-
search: SearchOptions,
|
|
170
|
-
options?: {
|
|
171
|
-
mailboxIds?: Set<string>;
|
|
172
|
-
excludeDeleted?: boolean;
|
|
173
|
-
},
|
|
174
|
-
): Promise<number>;
|
|
175
|
-
/**
|
|
176
|
-
* Every message of a thread, across all mailboxes of the account. A
|
|
177
|
-
* conversation spans INBOX, Sent and any folder its messages were filed
|
|
178
|
-
* in, so this listing is deliberately not scoped to a mailbox (#46).
|
|
179
|
-
*
|
|
180
|
-
* Ordered by `sentDate` with `threadMessageId` breaking ties, ascending
|
|
181
|
-
* unless `order` says otherwise — a conversation reads in the order it
|
|
182
|
-
* happened (#81). Every implementation sorts in the query, so the order
|
|
183
|
-
* holds across pages and not merely within one page.
|
|
184
|
-
*/
|
|
185
|
-
listByThread(
|
|
186
|
-
threadId: string,
|
|
187
|
-
accountConfigId: string,
|
|
188
|
-
options?: {
|
|
189
|
-
order?: "asc" | "desc";
|
|
190
|
-
limit?: number;
|
|
191
|
-
continuationToken?: string;
|
|
192
|
-
excludeDeleted?: boolean;
|
|
193
|
-
},
|
|
194
|
-
): Promise<ResultList<ThreadMessageItem>>;
|
|
195
|
-
findByMessageId(
|
|
196
|
-
accountConfigId: string,
|
|
197
|
-
messageId: string,
|
|
198
|
-
): Promise<ThreadMessageItem | null>;
|
|
199
|
-
findAllByMessageId(
|
|
200
|
-
accountConfigId: string,
|
|
201
|
-
messageId: string,
|
|
202
|
-
): Promise<ThreadMessageItem[]>;
|
|
203
|
-
getByMessageId(
|
|
204
|
-
accountConfigId: string,
|
|
205
|
-
messageId: string,
|
|
206
|
-
): Promise<ThreadMessageItem>;
|
|
207
|
-
listByMailbox(
|
|
208
|
-
accountConfigId: string,
|
|
209
|
-
mailboxId: string,
|
|
210
|
-
options?: {
|
|
211
|
-
order?: "asc" | "desc";
|
|
212
|
-
limit?: number;
|
|
213
|
-
continuationToken?: string;
|
|
214
|
-
attributes?: string[];
|
|
215
|
-
excludeDeleted?: boolean;
|
|
216
|
-
},
|
|
217
|
-
): Promise<ResultList<ThreadMessageItem>>;
|
|
218
|
-
countByThread(accountConfigId: string, threadId: string): Promise<number>;
|
|
219
|
-
searchByMailbox(
|
|
220
|
-
accountConfigId: string,
|
|
221
|
-
mailboxId: string,
|
|
222
|
-
search: SearchOptions,
|
|
223
|
-
options?: {
|
|
224
|
-
order?: "asc" | "desc";
|
|
225
|
-
count?: number;
|
|
226
|
-
continuationToken?: string;
|
|
227
|
-
excludeDeleted?: boolean;
|
|
228
|
-
},
|
|
229
|
-
): Promise<ResultList<ThreadMessageItem>>;
|
|
230
|
-
searchByMailboxWindow(
|
|
231
|
-
accountConfigId: string,
|
|
232
|
-
mailboxId: string,
|
|
233
|
-
search: SearchOptions,
|
|
234
|
-
options?: {
|
|
235
|
-
order?: "asc" | "desc";
|
|
236
|
-
limit?: number;
|
|
237
|
-
continuationToken?: string;
|
|
238
|
-
attributes?: string[];
|
|
239
|
-
excludeDeleted?: boolean;
|
|
240
|
-
},
|
|
241
|
-
): Promise<ResultList<ThreadMessageItem>>;
|
|
242
|
-
/**
|
|
243
|
-
* COUNT of matching MESSAGES in one mailbox, under the same predicate
|
|
244
|
-
* `searchByMailboxWindow` reads with — the per-mailbox counterpart of
|
|
245
|
-
* `countThreadsInScope`, and not distinct on `threadId` because the listing
|
|
246
|
-
* it answers for renders a row per message.
|
|
247
|
-
*
|
|
248
|
-
* The number is over the whole mailbox. No page size bounds it and no cursor
|
|
249
|
-
* narrows it, so it does not shrink as the caller pages and does not change
|
|
250
|
-
* when the page size does (#305). There is nothing to cap: the count is one
|
|
251
|
-
* aggregate read that returns no rows.
|
|
252
|
-
*/
|
|
253
|
-
countByMailbox(
|
|
254
|
-
accountConfigId: string,
|
|
255
|
-
mailboxId: string,
|
|
256
|
-
search: SearchOptions,
|
|
257
|
-
options?: {
|
|
258
|
-
excludeDeleted?: boolean;
|
|
259
|
-
order?: "asc" | "desc";
|
|
260
|
-
},
|
|
261
|
-
): Promise<number>;
|
|
262
|
-
listAllByAccount(accountConfigId: string): Promise<ThreadMessageItem[]>;
|
|
263
|
-
deleteAllByAccount(accountConfigId: string): Promise<number>;
|
|
264
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { IAddressRepository } from "./address.js";
|
|
2
|
-
import type { IEnvelopeRepository } from "./envelope.js";
|
|
3
|
-
import type { IMessageRepository } from "./message.js";
|
|
4
|
-
import type { IThreadMessageRepository } from "./thread-message.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The repositories a `saveMessage` write set touches, bound to a single unit of
|
|
8
|
-
* work. On Postgres these are transaction-bound so every write — including the
|
|
9
|
-
* transactional-outbox rows the message write appends — commits or rolls back
|
|
10
|
-
* together.
|
|
11
|
-
*/
|
|
12
|
-
export interface UnitOfWorkRepositories {
|
|
13
|
-
message: IMessageRepository;
|
|
14
|
-
envelope: IEnvelopeRepository;
|
|
15
|
-
address: IAddressRepository;
|
|
16
|
-
threadMessage: IThreadMessageRepository;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Runs a set of related writes as one atomic unit. The callback receives
|
|
21
|
-
* repositories bound to the unit of work; a throw rolls the whole set back.
|
|
22
|
-
*
|
|
23
|
-
* Backends without cross-entity transactions supply a pass-through
|
|
24
|
-
* implementation that runs the callback against the plain repositories.
|
|
25
|
-
*/
|
|
26
|
-
export interface IUnitOfWork {
|
|
27
|
-
transaction<T>(fn: (repos: UnitOfWorkRepositories) => Promise<T>): Promise<T>;
|
|
28
|
-
}
|
package/src/mailbox-name.test.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { mailboxLeafName, resolveMailboxByLeafName } from "./mailbox-name.js";
|
|
4
|
-
|
|
5
|
-
const JUNK_NAMES = ["junk", "spam", "junk e-mail", "junk email", "bulk mail"];
|
|
6
|
-
|
|
7
|
-
const mailbox = (
|
|
8
|
-
mailboxId: string,
|
|
9
|
-
fullPath: string,
|
|
10
|
-
hierarchyDelimiter = "/",
|
|
11
|
-
) => ({ mailboxId, fullPath, hierarchyDelimiter });
|
|
12
|
-
|
|
13
|
-
describe("resolveMailboxByLeafName", () => {
|
|
14
|
-
it("resolves a Junk folder nested under INBOX", () => {
|
|
15
|
-
// The layout that broke report-spam's name fallback: an INBOX-prefixed
|
|
16
|
-
// namespace where Junk is `INBOX/Spam`, which never equals "spam".
|
|
17
|
-
const found = resolveMailboxByLeafName(
|
|
18
|
-
[
|
|
19
|
-
mailbox("m-inbox", "INBOX"),
|
|
20
|
-
mailbox("m-spam", "INBOX/Spam"),
|
|
21
|
-
mailbox("m-sent", "INBOX/Sent"),
|
|
22
|
-
],
|
|
23
|
-
JUNK_NAMES,
|
|
24
|
-
);
|
|
25
|
-
assert.equal(found?.mailboxId, "m-spam");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("resolves under a non-slash delimiter and a non-INBOX prefix", () => {
|
|
29
|
-
const found = resolveMailboxByLeafName(
|
|
30
|
-
[mailbox("m-junk", "Mail.Junk E-mail", ".")],
|
|
31
|
-
JUNK_NAMES,
|
|
32
|
-
);
|
|
33
|
-
assert.equal(found?.mailboxId, "m-junk");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("treats a flat namespace with no delimiter as its own leaf", () => {
|
|
37
|
-
const found = resolveMailboxByLeafName(
|
|
38
|
-
[mailbox("m-junk", "Junk", "")],
|
|
39
|
-
JUNK_NAMES,
|
|
40
|
-
);
|
|
41
|
-
assert.equal(found?.mailboxId, "m-junk");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("prefers the shallowest match over a deeper one", () => {
|
|
45
|
-
const found = resolveMailboxByLeafName(
|
|
46
|
-
[
|
|
47
|
-
mailbox("m-buried", "Archive/2019/Spam"),
|
|
48
|
-
mailbox("m-real", "INBOX/Junk"),
|
|
49
|
-
],
|
|
50
|
-
JUNK_NAMES,
|
|
51
|
-
);
|
|
52
|
-
assert.equal(found?.mailboxId, "m-real");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("prefers the better name at equal depth", () => {
|
|
56
|
-
const found = resolveMailboxByLeafName(
|
|
57
|
-
[mailbox("m-bulk", "INBOX/Bulk Mail"), mailbox("m-junk", "INBOX/Junk")],
|
|
58
|
-
JUNK_NAMES,
|
|
59
|
-
);
|
|
60
|
-
assert.equal(found?.mailboxId, "m-junk");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("answers null when no folder carries a conventional name", () => {
|
|
64
|
-
const found = resolveMailboxByLeafName(
|
|
65
|
-
[mailbox("m-inbox", "INBOX"), mailbox("m-work", "INBOX/Work")],
|
|
66
|
-
JUNK_NAMES,
|
|
67
|
-
);
|
|
68
|
-
assert.equal(found, null);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe("mailboxLeafName", () => {
|
|
73
|
-
it("takes the leaf under a dot-delimited namespace", () => {
|
|
74
|
-
assert.equal(
|
|
75
|
-
mailboxLeafName({
|
|
76
|
-
fullPath: "INBOX.Projects.Q3",
|
|
77
|
-
hierarchyDelimiter: ".",
|
|
78
|
-
}),
|
|
79
|
-
"Q3",
|
|
80
|
-
);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("takes the leaf under a slash-delimited namespace", () => {
|
|
84
|
-
assert.equal(
|
|
85
|
-
mailboxLeafName({
|
|
86
|
-
fullPath: "INBOX/Sent Messages",
|
|
87
|
-
hierarchyDelimiter: "/",
|
|
88
|
-
}),
|
|
89
|
-
"Sent Messages",
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("keeps the whole name when the server reports no delimiter", () => {
|
|
94
|
-
assert.equal(
|
|
95
|
-
mailboxLeafName({ fullPath: "Projects/Q3", hierarchyDelimiter: "" }),
|
|
96
|
-
"Projects/Q3",
|
|
97
|
-
);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("keeps a top-level name that carries no delimiter", () => {
|
|
101
|
-
assert.equal(
|
|
102
|
-
mailboxLeafName({ fullPath: "INBOX", hierarchyDelimiter: "." }),
|
|
103
|
-
"INBOX",
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
});
|
package/src/mailbox-name.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
export interface MailboxPath {
|
|
2
|
-
fullPath: string;
|
|
3
|
-
hierarchyDelimiter: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface MailboxNameCandidate extends MailboxPath {
|
|
7
|
-
mailboxId: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// A flat mailbox namespace reports no delimiter at all — ImapFlow gives `""`
|
|
11
|
-
// for a NIL LIST delimiter — and splitting on "" returns single characters, so
|
|
12
|
-
// the path is its own leaf.
|
|
13
|
-
const segments = (mailbox: MailboxPath): string[] =>
|
|
14
|
-
mailbox.hierarchyDelimiter.length === 0
|
|
15
|
-
? [mailbox.fullPath]
|
|
16
|
-
: mailbox.fullPath.split(mailbox.hierarchyDelimiter);
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The folder's own name: the last segment of its path under the delimiter its
|
|
20
|
-
* own server reports (`INBOX/Spam` → `Spam`, `INBOX.Projects.Q3` → `Q3`).
|
|
21
|
-
*/
|
|
22
|
-
export const mailboxLeafName = (mailbox: MailboxPath): string => {
|
|
23
|
-
const parts = segments(mailbox);
|
|
24
|
-
return parts[parts.length - 1] || mailbox.fullPath;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Where a path under a renamed folder lands, or `undefined` when the rename did
|
|
29
|
-
* not move it. IMAP RENAME moves the whole subtree in one command, so a path
|
|
30
|
-
* inside the renamed branch moves exactly as far as its prefix does, and a path
|
|
31
|
-
* merely starting with the same characters (`Workshop` beside `Work`) does not
|
|
32
|
-
* move at all.
|
|
33
|
-
*
|
|
34
|
-
* Sliced rather than replaced. `String.prototype.replace` with a string pattern
|
|
35
|
-
* expands `$&`, `` $` ``, `$'` and `$$` in the replacement, so a rename to a
|
|
36
|
-
* folder whose name contains one of those wrote a different path locally from
|
|
37
|
-
* the one the RENAME carried to the server — the row and the server diverge and
|
|
38
|
-
* the next sweep inserts a second row for the real path.
|
|
39
|
-
*/
|
|
40
|
-
export const rebaseMailboxPath = (
|
|
41
|
-
recorded: string,
|
|
42
|
-
oldPath: string,
|
|
43
|
-
newPath: string,
|
|
44
|
-
delimiter: string,
|
|
45
|
-
): string | undefined => {
|
|
46
|
-
if (recorded === oldPath) return newPath;
|
|
47
|
-
if (delimiter.length === 0) return undefined;
|
|
48
|
-
if (!recorded.startsWith(`${oldPath}${delimiter}`)) return undefined;
|
|
49
|
-
return `${newPath}${recorded.slice(oldPath.length)}`;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const rank = (
|
|
53
|
-
mailbox: MailboxNameCandidate,
|
|
54
|
-
names: readonly string[],
|
|
55
|
-
): number | null => {
|
|
56
|
-
const parts = segments(mailbox);
|
|
57
|
-
const nameIndex = names.indexOf(mailboxLeafName(mailbox).toLowerCase());
|
|
58
|
-
if (nameIndex < 0) return null;
|
|
59
|
-
// Depth outranks the name: a "Spam" buried under Trash or an archive must
|
|
60
|
-
// never beat the account's real "Junk" one level up.
|
|
61
|
-
return parts.length * names.length + nameIndex;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* A special-use folder by conventional name, for servers that advertise no
|
|
66
|
-
* special-use flag for it. Matches the folder's own leaf segment against its
|
|
67
|
-
* mailbox's own hierarchy delimiter, so it resolves at any depth under any
|
|
68
|
-
* prefix (`INBOX/Spam`, `Mail.Junk E-mail`, `[Gmail]/Spam`) without knowing
|
|
69
|
-
* which prefixes a server uses. `names` is ordered best-first and must be
|
|
70
|
-
* lower case.
|
|
71
|
-
*/
|
|
72
|
-
export const resolveMailboxByLeafName = <T extends MailboxNameCandidate>(
|
|
73
|
-
mailboxes: readonly T[],
|
|
74
|
-
names: readonly string[],
|
|
75
|
-
): T | null => {
|
|
76
|
-
let best: { mailbox: T; rank: number } | null = null;
|
|
77
|
-
for (const mailbox of mailboxes) {
|
|
78
|
-
const candidate = rank(mailbox, names);
|
|
79
|
-
if (candidate === null) continue;
|
|
80
|
-
if (
|
|
81
|
-
!best ||
|
|
82
|
-
candidate < best.rank ||
|
|
83
|
-
(candidate === best.rank &&
|
|
84
|
-
mailbox.fullPath.localeCompare(best.mailbox.fullPath) < 0)
|
|
85
|
-
) {
|
|
86
|
-
best = { mailbox, rank: candidate };
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return best?.mailbox ?? null;
|
|
90
|
-
};
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The doors onto a folder's mutation state, proved by the compiler.
|
|
3
|
-
*
|
|
4
|
-
* `docs/architecture/folder-rename-and-delete.md` D3 says every transition of a
|
|
5
|
-
* folder's state is a conditional write. A rule like that is worth exactly as
|
|
6
|
-
* much as the thing enforcing it, and a convention enforces nothing: every
|
|
7
|
-
* writer in the tree before this reached for the general-purpose `update()`,
|
|
8
|
-
* because nothing stopped them.
|
|
9
|
-
*
|
|
10
|
-
* So `syncStatus` and `pendingPath` are gone from `UpdateMailboxInput`, and
|
|
11
|
-
* `pendingPath` from `CreateMailboxInput` too — a create records no rename
|
|
12
|
-
* target (T1). What is left is `transition` and `transitionSubtree`, both of
|
|
13
|
-
* which take a predicate. Every `@ts-expect-error` below is a door that is
|
|
14
|
-
* shut: put the field back and these stop erroring, which fails the build.
|
|
15
|
-
*
|
|
16
|
-
* `@ts-expect-error` is the assertion here, so this file is checked rather than
|
|
17
|
-
* run. The one runtime case exists to keep the suite honest about having loaded
|
|
18
|
-
* it.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import assert from "node:assert/strict";
|
|
22
|
-
import { test } from "node:test";
|
|
23
|
-
import type {
|
|
24
|
-
CreateMailboxInput,
|
|
25
|
-
MailboxStatePredicate,
|
|
26
|
-
MailboxSubtreeTransitionIntent,
|
|
27
|
-
MailboxTransitionIntent,
|
|
28
|
-
UpdateMailboxInput,
|
|
29
|
-
} from "./types.js";
|
|
30
|
-
|
|
31
|
-
// --- The general-purpose writer refuses both state fields -------------------
|
|
32
|
-
|
|
33
|
-
// @ts-expect-error `syncStatus` is mutation state: write it through a transition
|
|
34
|
-
const syncStatus: UpdateMailboxInput = { syncStatus: "pending" };
|
|
35
|
-
|
|
36
|
-
// @ts-expect-error `pendingPath` is mutation state
|
|
37
|
-
const pendingPath: UpdateMailboxInput = { pendingPath: "Archive/2025" };
|
|
38
|
-
|
|
39
|
-
// The insert records a state — the intent recorders say which — but never a
|
|
40
|
-
// rename target, because a create has none.
|
|
41
|
-
const created: CreateMailboxInput = {
|
|
42
|
-
accountId: "acct-1",
|
|
43
|
-
namespacePrefix: "",
|
|
44
|
-
hierarchyDelimiter: "/",
|
|
45
|
-
fullPath: "Archive",
|
|
46
|
-
uidValidity: 1,
|
|
47
|
-
uidNext: 1,
|
|
48
|
-
highestModseq: "0",
|
|
49
|
-
messageCount: 0,
|
|
50
|
-
unseenCount: 0,
|
|
51
|
-
deletedCount: 0,
|
|
52
|
-
totalSize: 0,
|
|
53
|
-
lastSyncUid: 0,
|
|
54
|
-
highWaterMarkUid: 0,
|
|
55
|
-
lastMessageSyncAt: 0,
|
|
56
|
-
syncStatus: "pending",
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// @ts-expect-error a create records no rename target (T1)
|
|
60
|
-
const createdWithTarget: CreateMailboxInput = { ...created, pendingPath: "X" };
|
|
61
|
-
|
|
62
|
-
// Everything else about a folder still goes through `update()`, unchanged.
|
|
63
|
-
const ordinary: UpdateMailboxInput = {
|
|
64
|
-
fullPath: "Archive/2025",
|
|
65
|
-
messageCount: 12,
|
|
66
|
-
cursorState: "cursor_invalid",
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
// --- The transitions are the doors, and they take a predicate ---------------
|
|
70
|
-
|
|
71
|
-
const transition: MailboxTransitionIntent = {
|
|
72
|
-
from: ["pending"],
|
|
73
|
-
wherePendingPath: null,
|
|
74
|
-
to: "synced",
|
|
75
|
-
set: { fullPath: "INBOX/Archive", pendingPath: null },
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const subtree: MailboxSubtreeTransitionIntent = {
|
|
79
|
-
from: ["synced", "failed"],
|
|
80
|
-
to: "pending",
|
|
81
|
-
rowSet: (row) => ({ pendingPath: row.fullPath }),
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
// The predicate names states, never a free-form column: a caller cannot smuggle
|
|
85
|
-
// an unrelated field into the WHERE clause and call it a lock.
|
|
86
|
-
const notAState: MailboxStatePredicate = {
|
|
87
|
-
from: ["synced"],
|
|
88
|
-
// @ts-expect-error `fullPath` is not part of a folder's mutation state
|
|
89
|
-
fullPath: "INBOX",
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
test("the folder-state doors are the two transitions", () => {
|
|
93
|
-
assert.equal(transition.to, "synced");
|
|
94
|
-
assert.equal(
|
|
95
|
-
subtree.rowSet({ fullPath: "Work" } as never).pendingPath,
|
|
96
|
-
"Work",
|
|
97
|
-
);
|
|
98
|
-
assert.equal(ordinary.messageCount, 12);
|
|
99
|
-
assert.deepEqual(
|
|
100
|
-
[syncStatus, pendingPath, createdWithTarget, notAState].length,
|
|
101
|
-
4,
|
|
102
|
-
"each field taken off an input, and the predicate's own shape, is pinned above",
|
|
103
|
-
);
|
|
104
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The two doors, proved by the compiler.
|
|
3
|
-
*
|
|
4
|
-
* `docs/architecture/imap-mutations.md` R3 says a placement is written only as
|
|
5
|
-
* a conditional write. A rule like that is worth exactly as much as the thing
|
|
6
|
-
* enforcing it, and a convention enforces nothing: the first four writers on
|
|
7
|
-
* this branch each reached for the general-purpose `update()` and nobody
|
|
8
|
-
* noticed, because nothing stopped them.
|
|
9
|
-
*
|
|
10
|
-
* So the placement fields are gone from every input type but the two that are
|
|
11
|
-
* allowed to carry them — `transitionPlacement`, which takes a predicate, and
|
|
12
|
-
* the settle in `updateUid`, which takes no input object at all and writes what
|
|
13
|
-
* the mail server confirmed. Every `@ts-expect-error` below is a door that is
|
|
14
|
-
* shut: delete the omission from `UpdateMessageInput` and these stop erroring,
|
|
15
|
-
* which fails the build.
|
|
16
|
-
*
|
|
17
|
-
* `@ts-expect-error` is the assertion here, so this file is checked rather than
|
|
18
|
-
* run. The one runtime case exists to keep the suite honest about having loaded
|
|
19
|
-
* it.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import assert from "node:assert/strict";
|
|
23
|
-
import { test } from "node:test";
|
|
24
|
-
import type {
|
|
25
|
-
PlacementPredicate,
|
|
26
|
-
PlacementTransitionInput,
|
|
27
|
-
UpdateMessageInput,
|
|
28
|
-
} from "./types.js";
|
|
29
|
-
|
|
30
|
-
// --- The general-purpose writer refuses every placement field ---------------
|
|
31
|
-
|
|
32
|
-
// @ts-expect-error `status` is a placement field: write it through a transition
|
|
33
|
-
const status: UpdateMessageInput = { status: "moving" };
|
|
34
|
-
|
|
35
|
-
// @ts-expect-error `syncStatus` is a placement field
|
|
36
|
-
const syncStatus: UpdateMessageInput = { syncStatus: "failed" };
|
|
37
|
-
|
|
38
|
-
// @ts-expect-error `abandonedMutation` is a placement field
|
|
39
|
-
const abandonedMutation: UpdateMessageInput = { abandonedMutation: "delete" };
|
|
40
|
-
|
|
41
|
-
// @ts-expect-error `mailboxId` is a placement field
|
|
42
|
-
const mailboxId: UpdateMessageInput = { mailboxId: "mbx-1" };
|
|
43
|
-
|
|
44
|
-
// @ts-expect-error `uid` is a placement field
|
|
45
|
-
const uid: UpdateMessageInput = { uid: 42 };
|
|
46
|
-
|
|
47
|
-
// @ts-expect-error `originalMailboxId` is a placement field
|
|
48
|
-
const originalMailboxId: UpdateMessageInput = { originalMailboxId: "mbx-1" };
|
|
49
|
-
|
|
50
|
-
// @ts-expect-error `originalUid` is a placement field
|
|
51
|
-
const originalUid: UpdateMessageInput = { originalUid: 42 };
|
|
52
|
-
|
|
53
|
-
// Everything else about a message still goes through it, unchanged.
|
|
54
|
-
const ordinary: UpdateMessageInput = {
|
|
55
|
-
bodyStorageKey: "s3://body",
|
|
56
|
-
category: "personal",
|
|
57
|
-
spamReport: { reportedAt: 0 },
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// --- The transition is the door, and it takes a predicate -------------------
|
|
61
|
-
|
|
62
|
-
const transition: {
|
|
63
|
-
expected: PlacementPredicate;
|
|
64
|
-
next: PlacementTransitionInput;
|
|
65
|
-
} = {
|
|
66
|
-
expected: { status: ["moving", "deleting"], mailboxId: "mbx-1", uid: 42 },
|
|
67
|
-
next: {
|
|
68
|
-
status: "active",
|
|
69
|
-
syncStatus: "abandoned",
|
|
70
|
-
abandonedMutation: "delete",
|
|
71
|
-
mailboxId: "mbx-1",
|
|
72
|
-
uid: 42,
|
|
73
|
-
originalMailboxId: null,
|
|
74
|
-
originalUid: null,
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// The predicate names states, never a free-form column: a caller cannot smuggle
|
|
79
|
-
// an unrelated field into the WHERE clause and call it a lock.
|
|
80
|
-
// @ts-expect-error `category` is not part of a placement
|
|
81
|
-
const notAPlacement: PlacementPredicate = { category: "personal" };
|
|
82
|
-
|
|
83
|
-
test("the placement doors are the transition and the settle", () => {
|
|
84
|
-
assert.equal(transition.next.status, "active");
|
|
85
|
-
assert.equal(ordinary.category, "personal");
|
|
86
|
-
assert.deepEqual(
|
|
87
|
-
[
|
|
88
|
-
status,
|
|
89
|
-
syncStatus,
|
|
90
|
-
abandonedMutation,
|
|
91
|
-
mailboxId,
|
|
92
|
-
uid,
|
|
93
|
-
originalMailboxId,
|
|
94
|
-
originalUid,
|
|
95
|
-
notAPlacement,
|
|
96
|
-
].length,
|
|
97
|
-
8,
|
|
98
|
-
"each of the seven fields, and the predicate's own shape, is pinned above",
|
|
99
|
-
);
|
|
100
|
-
});
|