@remit/data-ports 0.0.83 → 0.0.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -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
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { CreateThreadMessageInput, ResultList, SearchOptions, ThreadMessageFieldTerm, ThreadMessageItem, UpdateThreadMessageInput } from "../types.js";
|
|
2
|
+
export interface IThreadMessageRepository {
|
|
3
|
+
create(input: CreateThreadMessageInput): Promise<ThreadMessageItem>;
|
|
4
|
+
get(accountConfigId: string, threadMessageId: string): Promise<ThreadMessageItem>;
|
|
5
|
+
get(accountConfigId: string, threadMessageIds: string[]): Promise<ThreadMessageItem[]>;
|
|
6
|
+
update(accountConfigId: string, threadMessageId: string, input: UpdateThreadMessageInput, options?: {
|
|
7
|
+
composites?: {
|
|
8
|
+
sentDate?: number;
|
|
9
|
+
mailboxId?: string;
|
|
10
|
+
isRead?: boolean;
|
|
11
|
+
isDeleted?: boolean;
|
|
12
|
+
hasStars?: boolean;
|
|
13
|
+
hasAttachment?: boolean;
|
|
14
|
+
};
|
|
15
|
+
}): Promise<ThreadMessageItem>;
|
|
16
|
+
delete(accountConfigId: string, threadMessageId: string): Promise<void>;
|
|
17
|
+
deleteMany(keys: Array<{
|
|
18
|
+
accountConfigId: string;
|
|
19
|
+
threadMessageId: string;
|
|
20
|
+
}>): Promise<void>;
|
|
21
|
+
listByAccount(accountConfigId: string, options?: {
|
|
22
|
+
limit?: number;
|
|
23
|
+
continuationToken?: string;
|
|
24
|
+
}): Promise<ResultList<ThreadMessageItem>>;
|
|
25
|
+
listByDate(accountConfigId: string, options?: {
|
|
26
|
+
order?: "asc" | "desc";
|
|
27
|
+
limit?: number;
|
|
28
|
+
continuationToken?: string;
|
|
29
|
+
inboxMailboxIds?: Set<string>;
|
|
30
|
+
excludeDeleted?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Row criteria applied inside the query, so a page is a page of
|
|
33
|
+
* matches however rare the criterion is. Filtering the rows a page
|
|
34
|
+
* returned instead is what makes a listing look empty whenever the
|
|
35
|
+
* matching mail sits below the newest page (#308).
|
|
36
|
+
*/
|
|
37
|
+
search?: SearchOptions;
|
|
38
|
+
}): Promise<ResultList<ThreadMessageItem>>;
|
|
39
|
+
/**
|
|
40
|
+
* Search rows for a config across every mailbox in `mailboxIds` — the
|
|
41
|
+
* cross-account, cross-folder counterpart of `searchByMailbox`. A row whose
|
|
42
|
+
* From matches any `query` term ranks above one that only mentions the
|
|
43
|
+
* terms, and each tier runs in date order.
|
|
44
|
+
*
|
|
45
|
+
* The caller supplies the mailbox scope, so a search reaching Archive, Sent, Spam and
|
|
46
|
+
* custom folders is a matter of which set is passed. An empty or absent
|
|
47
|
+
* `mailboxIds` means no narrowing (every mailbox of the config), which is why
|
|
48
|
+
* handlers pass an explicitly built set rather than relying on the default.
|
|
49
|
+
*
|
|
50
|
+
* Matching follows `searchByMailbox`: `query` splits on whitespace and every
|
|
51
|
+
* term must match subject OR From, case-insensitively. `limit` is a page size
|
|
52
|
+
* over MATCHES, clamped server-side, so a short page means the matches ran
|
|
53
|
+
* out — never that a read window was exhausted.
|
|
54
|
+
*
|
|
55
|
+
* Rows are per mailbox, not per conversation: the same mail filed in two
|
|
56
|
+
* folders is two rows sharing a `threadId`.
|
|
57
|
+
*/
|
|
58
|
+
searchByDate(accountConfigId: string, search: SearchOptions, options?: {
|
|
59
|
+
order?: "asc" | "desc";
|
|
60
|
+
limit?: number;
|
|
61
|
+
continuationToken?: string;
|
|
62
|
+
mailboxIds?: Set<string>;
|
|
63
|
+
excludeDeleted?: boolean;
|
|
64
|
+
}): Promise<ResultList<ThreadMessageItem>>;
|
|
65
|
+
/**
|
|
66
|
+
* Rows of a config whose sender, subject or `List-Id` column matches a set
|
|
67
|
+
* of substring terms, newest first, across every mailbox — the store-side
|
|
68
|
+
* narrowing a rule back-apply reads with.
|
|
69
|
+
*
|
|
70
|
+
* The terms are evaluated in the query, so `limit` is a page size over
|
|
71
|
+
* MATCHES and the scan reaches the whole config however old the matching
|
|
72
|
+
* mail is. Paging a date-ordered listing and filtering the rows it returned
|
|
73
|
+
* is what makes a rule for a quiet sender report a handful of matches on a
|
|
74
|
+
* mailbox holding thousands (#459).
|
|
75
|
+
*
|
|
76
|
+
* `operator` combines the terms — `"and"` requires every one, `"or"` any.
|
|
77
|
+
* An empty `terms` narrows nothing under either operator: the whole config,
|
|
78
|
+
* newest first.
|
|
79
|
+
*
|
|
80
|
+
* A term an implementation cannot evaluate faithfully is IGNORED, never
|
|
81
|
+
* approximated: the result may be wider than the terms asked for and never
|
|
82
|
+
* narrower, so the caller's own matcher stays the judge.
|
|
83
|
+
*
|
|
84
|
+
* Matching follows `searchByMailbox`: accent- and case-insensitive
|
|
85
|
+
* substring over the same folded columns.
|
|
86
|
+
*
|
|
87
|
+
* Rows are per mailbox, not per conversation: the same mail filed in two
|
|
88
|
+
* folders is two rows sharing a `threadId`. `mailboxIds` narrows the rows
|
|
89
|
+
* to those folders; omitting it reads every mailbox.
|
|
90
|
+
*/
|
|
91
|
+
listByFieldTerms(accountConfigId: string, terms: readonly ThreadMessageFieldTerm[], options?: {
|
|
92
|
+
operator?: "and" | "or";
|
|
93
|
+
order?: "asc" | "desc";
|
|
94
|
+
limit?: number;
|
|
95
|
+
continuationToken?: string;
|
|
96
|
+
excludeDeleted?: boolean;
|
|
97
|
+
mailboxIds?: readonly string[];
|
|
98
|
+
}): Promise<ResultList<ThreadMessageItem>>;
|
|
99
|
+
/**
|
|
100
|
+
* List starred rows for a config, newest first, across every mailbox.
|
|
101
|
+
*
|
|
102
|
+
* Backed by the `byStarred` index (pk = accountConfigId, sk = hasStars +
|
|
103
|
+
* sentDate). Starredness is read from `hasStars` — the boolean of record —
|
|
104
|
+
* never from the presentation-only `star` colour. `mailboxIds` narrows the
|
|
105
|
+
* result to a caller-supplied set (used to drop muted mailboxes and the
|
|
106
|
+
* folders a star never surfaces from); omitting it returns every mailbox.
|
|
107
|
+
*
|
|
108
|
+
* Rows are per mailbox, not per conversation: the same mail filed in two
|
|
109
|
+
* folders is two rows sharing a `threadId`. Callers that render one row per
|
|
110
|
+
* conversation must collapse by `threadId` across the pages they have
|
|
111
|
+
* accumulated — this cannot be done inside a single page.
|
|
112
|
+
*/
|
|
113
|
+
listByStarred(accountConfigId: string, options?: {
|
|
114
|
+
order?: "asc" | "desc";
|
|
115
|
+
limit?: number;
|
|
116
|
+
continuationToken?: string;
|
|
117
|
+
mailboxIds?: Set<string>;
|
|
118
|
+
excludeDeleted?: boolean;
|
|
119
|
+
/** Row criteria applied inside the query; see `listByDate`. */
|
|
120
|
+
search?: SearchOptions;
|
|
121
|
+
}): Promise<ResultList<ThreadMessageItem>>;
|
|
122
|
+
/**
|
|
123
|
+
* COUNT of matching CONVERSATIONS over the same predicate and the same
|
|
124
|
+
* mailbox scope the three cross-account listing modes read with — the scoped
|
|
125
|
+
* counterpart of `countByMailbox`.
|
|
126
|
+
*
|
|
127
|
+
* A page size bounds the rows a response carries and has no bearing on how
|
|
128
|
+
* much matches, so a count returns no rows and is answered in full — except
|
|
129
|
+
* that a count with search text stops at 1000, where walking the matches
|
|
130
|
+
* further buys a number nobody reads.
|
|
131
|
+
*
|
|
132
|
+
* Distinct on `threadId`, unlike `countByMailbox`, because the cross-account
|
|
133
|
+
* listing is collapsed by thread before it is rendered: a row is per mailbox,
|
|
134
|
+
* so one message reachable through a real folder and a virtual copy of it is
|
|
135
|
+
* several rows, and two matching messages of one conversation are two more.
|
|
136
|
+
*/
|
|
137
|
+
countThreadsInScope(accountConfigId: string, search: SearchOptions, options?: {
|
|
138
|
+
mailboxIds?: Set<string>;
|
|
139
|
+
excludeDeleted?: boolean;
|
|
140
|
+
}): Promise<number>;
|
|
141
|
+
/**
|
|
142
|
+
* Every message of a thread, across all mailboxes of the account. A
|
|
143
|
+
* conversation spans INBOX, Sent and any folder its messages were filed
|
|
144
|
+
* in, so this listing is deliberately not scoped to a mailbox (#46).
|
|
145
|
+
*
|
|
146
|
+
* Ordered by `sentDate` with `threadMessageId` breaking ties, ascending
|
|
147
|
+
* unless `order` says otherwise — a conversation reads in the order it
|
|
148
|
+
* happened (#81). Every implementation sorts in the query, so the order
|
|
149
|
+
* holds across pages and not merely within one page.
|
|
150
|
+
*/
|
|
151
|
+
listByThread(threadId: string, accountConfigId: string, options?: {
|
|
152
|
+
order?: "asc" | "desc";
|
|
153
|
+
limit?: number;
|
|
154
|
+
continuationToken?: string;
|
|
155
|
+
excludeDeleted?: boolean;
|
|
156
|
+
}): Promise<ResultList<ThreadMessageItem>>;
|
|
157
|
+
findByMessageId(accountConfigId: string, messageId: string): Promise<ThreadMessageItem | null>;
|
|
158
|
+
findAllByMessageId(accountConfigId: string, messageId: string): Promise<ThreadMessageItem[]>;
|
|
159
|
+
rekeyThread(accountConfigId: string, fromThreadId: string, toThreadId: string): Promise<{
|
|
160
|
+
moved: number;
|
|
161
|
+
dropped: number;
|
|
162
|
+
}>;
|
|
163
|
+
getByMessageId(accountConfigId: string, messageId: string): Promise<ThreadMessageItem>;
|
|
164
|
+
listByMailbox(accountConfigId: string, mailboxId: string, options?: {
|
|
165
|
+
order?: "asc" | "desc";
|
|
166
|
+
limit?: number;
|
|
167
|
+
continuationToken?: string;
|
|
168
|
+
attributes?: string[];
|
|
169
|
+
excludeDeleted?: boolean;
|
|
170
|
+
}): Promise<ResultList<ThreadMessageItem>>;
|
|
171
|
+
countByThread(accountConfigId: string, threadId: string): Promise<number>;
|
|
172
|
+
searchByMailbox(accountConfigId: string, mailboxId: string, search: SearchOptions, options?: {
|
|
173
|
+
order?: "asc" | "desc";
|
|
174
|
+
count?: number;
|
|
175
|
+
continuationToken?: string;
|
|
176
|
+
excludeDeleted?: boolean;
|
|
177
|
+
}): Promise<ResultList<ThreadMessageItem>>;
|
|
178
|
+
searchByMailboxWindow(accountConfigId: string, mailboxId: string, search: SearchOptions, options?: {
|
|
179
|
+
order?: "asc" | "desc";
|
|
180
|
+
limit?: number;
|
|
181
|
+
continuationToken?: string;
|
|
182
|
+
attributes?: string[];
|
|
183
|
+
excludeDeleted?: boolean;
|
|
184
|
+
}): Promise<ResultList<ThreadMessageItem>>;
|
|
185
|
+
/**
|
|
186
|
+
* COUNT of matching MESSAGES in one mailbox, under the same predicate
|
|
187
|
+
* `searchByMailboxWindow` reads with — the per-mailbox counterpart of
|
|
188
|
+
* `countThreadsInScope`, and not distinct on `threadId` because the listing
|
|
189
|
+
* it answers for renders a row per message.
|
|
190
|
+
*
|
|
191
|
+
* The number is over the whole mailbox. No page size bounds it and no cursor
|
|
192
|
+
* narrows it, so it does not shrink as the caller pages and does not change
|
|
193
|
+
* when the page size does (#305). A count with search text stops at 1000,
|
|
194
|
+
* as `countThreadsInScope` does.
|
|
195
|
+
*/
|
|
196
|
+
countByMailbox(accountConfigId: string, mailboxId: string, search: SearchOptions, options?: {
|
|
197
|
+
excludeDeleted?: boolean;
|
|
198
|
+
order?: "asc" | "desc";
|
|
199
|
+
}): Promise<number>;
|
|
200
|
+
listAllByAccount(accountConfigId: string): Promise<ThreadMessageItem[]>;
|
|
201
|
+
deleteAllByAccount(accountConfigId: string): Promise<number>;
|
|
202
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface MailboxPath {
|
|
2
|
+
fullPath: string;
|
|
3
|
+
hierarchyDelimiter: string;
|
|
4
|
+
}
|
|
5
|
+
export interface MailboxNameCandidate extends MailboxPath {
|
|
6
|
+
mailboxId: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The folder's own name: the last segment of its path under the delimiter its
|
|
10
|
+
* own server reports (`INBOX/Spam` → `Spam`, `INBOX.Projects.Q3` → `Q3`).
|
|
11
|
+
*/
|
|
12
|
+
export declare const mailboxLeafName: (mailbox: MailboxPath) => string;
|
|
13
|
+
/**
|
|
14
|
+
* Where a path under a renamed folder lands, or `undefined` when the rename did
|
|
15
|
+
* not move it. IMAP RENAME moves the whole subtree in one command, so a path
|
|
16
|
+
* inside the renamed branch moves exactly as far as its prefix does, and a path
|
|
17
|
+
* merely starting with the same characters (`Workshop` beside `Work`) does not
|
|
18
|
+
* move at all.
|
|
19
|
+
*
|
|
20
|
+
* Sliced rather than replaced. `String.prototype.replace` with a string pattern
|
|
21
|
+
* expands `$&`, `` $` ``, `$'` and `$$` in the replacement, so a rename to a
|
|
22
|
+
* folder whose name contains one of those wrote a different path locally from
|
|
23
|
+
* the one the RENAME carried to the server — the row and the server diverge and
|
|
24
|
+
* the next sweep inserts a second row for the real path.
|
|
25
|
+
*/
|
|
26
|
+
export declare const rebaseMailboxPath: (recorded: string, oldPath: string, newPath: string, delimiter: string) => string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* A special-use folder by conventional name, for servers that advertise no
|
|
29
|
+
* special-use flag for it. Matches the folder's own leaf segment against its
|
|
30
|
+
* mailbox's own hierarchy delimiter, so it resolves at any depth under any
|
|
31
|
+
* prefix (`INBOX/Spam`, `Mail.Junk E-mail`, `[Gmail]/Spam`) without knowing
|
|
32
|
+
* which prefixes a server uses. `names` is ordered best-first and must be
|
|
33
|
+
* lower case.
|
|
34
|
+
*/
|
|
35
|
+
export declare const resolveMailboxByLeafName: <T extends MailboxNameCandidate>(mailboxes: readonly T[], names: readonly string[]) => T | null;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// A flat mailbox namespace reports no delimiter at all — ImapFlow gives `""`
|
|
2
|
+
// for a NIL LIST delimiter — and splitting on "" returns single characters, so
|
|
3
|
+
// the path is its own leaf.
|
|
4
|
+
const segments = (mailbox) => mailbox.hierarchyDelimiter.length === 0
|
|
5
|
+
? [mailbox.fullPath]
|
|
6
|
+
: mailbox.fullPath.split(mailbox.hierarchyDelimiter);
|
|
7
|
+
/**
|
|
8
|
+
* The folder's own name: the last segment of its path under the delimiter its
|
|
9
|
+
* own server reports (`INBOX/Spam` → `Spam`, `INBOX.Projects.Q3` → `Q3`).
|
|
10
|
+
*/
|
|
11
|
+
export const mailboxLeafName = (mailbox) => {
|
|
12
|
+
const parts = segments(mailbox);
|
|
13
|
+
return parts[parts.length - 1] || mailbox.fullPath;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Where a path under a renamed folder lands, or `undefined` when the rename did
|
|
17
|
+
* not move it. IMAP RENAME moves the whole subtree in one command, so a path
|
|
18
|
+
* inside the renamed branch moves exactly as far as its prefix does, and a path
|
|
19
|
+
* merely starting with the same characters (`Workshop` beside `Work`) does not
|
|
20
|
+
* move at all.
|
|
21
|
+
*
|
|
22
|
+
* Sliced rather than replaced. `String.prototype.replace` with a string pattern
|
|
23
|
+
* expands `$&`, `` $` ``, `$'` and `$$` in the replacement, so a rename to a
|
|
24
|
+
* folder whose name contains one of those wrote a different path locally from
|
|
25
|
+
* the one the RENAME carried to the server — the row and the server diverge and
|
|
26
|
+
* the next sweep inserts a second row for the real path.
|
|
27
|
+
*/
|
|
28
|
+
export const rebaseMailboxPath = (recorded, oldPath, newPath, delimiter) => {
|
|
29
|
+
if (recorded === oldPath)
|
|
30
|
+
return newPath;
|
|
31
|
+
if (delimiter.length === 0)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (!recorded.startsWith(`${oldPath}${delimiter}`))
|
|
34
|
+
return undefined;
|
|
35
|
+
return `${newPath}${recorded.slice(oldPath.length)}`;
|
|
36
|
+
};
|
|
37
|
+
const rank = (mailbox, names) => {
|
|
38
|
+
const parts = segments(mailbox);
|
|
39
|
+
const nameIndex = names.indexOf(mailboxLeafName(mailbox).toLowerCase());
|
|
40
|
+
if (nameIndex < 0)
|
|
41
|
+
return null;
|
|
42
|
+
// Depth outranks the name: a "Spam" buried under Trash or an archive must
|
|
43
|
+
// never beat the account's real "Junk" one level up.
|
|
44
|
+
return parts.length * names.length + nameIndex;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* A special-use folder by conventional name, for servers that advertise no
|
|
48
|
+
* special-use flag for it. Matches the folder's own leaf segment against its
|
|
49
|
+
* mailbox's own hierarchy delimiter, so it resolves at any depth under any
|
|
50
|
+
* prefix (`INBOX/Spam`, `Mail.Junk E-mail`, `[Gmail]/Spam`) without knowing
|
|
51
|
+
* which prefixes a server uses. `names` is ordered best-first and must be
|
|
52
|
+
* lower case.
|
|
53
|
+
*/
|
|
54
|
+
export const resolveMailboxByLeafName = (mailboxes, names) => {
|
|
55
|
+
let best = null;
|
|
56
|
+
for (const mailbox of mailboxes) {
|
|
57
|
+
const candidate = rank(mailbox, names);
|
|
58
|
+
if (candidate === null)
|
|
59
|
+
continue;
|
|
60
|
+
if (!best ||
|
|
61
|
+
candidate < best.rank ||
|
|
62
|
+
(candidate === best.rank &&
|
|
63
|
+
mailbox.fullPath.localeCompare(best.mailbox.fullPath) < 0)) {
|
|
64
|
+
best = { mailbox, rank: candidate };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return best?.mailbox ?? null;
|
|
68
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which body parts belong to which message.
|
|
3
|
+
*
|
|
4
|
+
* A message/rfc822 (or message/global) part is a forwarded message carried
|
|
5
|
+
* whole. Its own parts are stored under its path — `<partPath>.0` is its root —
|
|
6
|
+
* and they belong to the forwarded message: the carrying message neither
|
|
7
|
+
* renders them as its body nor lists them as its attachments.
|
|
8
|
+
*/
|
|
9
|
+
export interface MessagePart {
|
|
10
|
+
partPath: string;
|
|
11
|
+
mediaType: string;
|
|
12
|
+
mediaSubtype: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const isEncapsulatedMessage: (part: Pick<MessagePart, "mediaType" | "mediaSubtype">) => boolean;
|
|
15
|
+
/** Whether `part` sits inside the forwarded message at `messagePath`. */
|
|
16
|
+
export declare const isWithinMessage: (part: Pick<MessagePart, "partPath">, messagePath: string) => boolean;
|
|
17
|
+
/** The forwarded messages a message carries itself, not those nested in them. */
|
|
18
|
+
export declare const outermostMessages: <T extends MessagePart>(parts: readonly T[]) => T[];
|
|
19
|
+
/** The parts that belong to the message itself, not to a message it carries. */
|
|
20
|
+
export declare const ownParts: <T extends MessagePart>(parts: readonly T[]) => T[];
|
|
21
|
+
/** Every part of the forwarded message at `messagePath`, nested ones included. */
|
|
22
|
+
export declare const encapsulatedParts: <T extends MessagePart>(parts: readonly T[], messagePath: string) => T[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const ENCAPSULATED_MESSAGE_SUBTYPES = new Set([
|
|
2
|
+
"rfc822",
|
|
3
|
+
"global",
|
|
4
|
+
]);
|
|
5
|
+
export const isEncapsulatedMessage = (part) => part.mediaType.toUpperCase() === "MESSAGE" &&
|
|
6
|
+
ENCAPSULATED_MESSAGE_SUBTYPES.has(part.mediaSubtype.toLowerCase());
|
|
7
|
+
/** Whether `part` sits inside the forwarded message at `messagePath`. */
|
|
8
|
+
export const isWithinMessage = (part, messagePath) => part.partPath.startsWith(`${messagePath}.`);
|
|
9
|
+
/** The forwarded messages a message carries itself, not those nested in them. */
|
|
10
|
+
export const outermostMessages = (parts) => parts.filter((part) => isEncapsulatedMessage(part) &&
|
|
11
|
+
!parts.some((outer) => isEncapsulatedMessage(outer) && isWithinMessage(part, outer.partPath)));
|
|
12
|
+
/** The parts that belong to the message itself, not to a message it carries. */
|
|
13
|
+
export const ownParts = (parts) => {
|
|
14
|
+
const messages = parts.filter(isEncapsulatedMessage);
|
|
15
|
+
return parts.filter((part) => !messages.some((message) => isWithinMessage(part, message.partPath)));
|
|
16
|
+
};
|
|
17
|
+
/** Every part of the forwarded message at `messagePath`, nested ones included. */
|
|
18
|
+
export const encapsulatedParts = (parts, messagePath) => parts.filter((part) => isWithinMessage(part, messagePath));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { MessageItem } from "./types.js";
|
|
2
|
+
export type MessageSettlementFields = Pick<MessageItem, "status" | "syncStatus" | "abandonedMutation">;
|
|
3
|
+
/**
|
|
4
|
+
* Which mutation gave up on this row, and `none` where none did.
|
|
5
|
+
*
|
|
6
|
+
* `syncStatus: abandoned` is the gate and the only gate: it is the give-up
|
|
7
|
+
* value of the placement state model (docs/architecture/imap-mutations.md R3),
|
|
8
|
+
* written only after the row has been put back on a placement the mail server
|
|
9
|
+
* confirmed. Every settle overwrites it, and the settle in `updateUid` clears
|
|
10
|
+
* `abandonedMutation` alongside it. A transient attempt writes
|
|
11
|
+
* `failed` and re-throws for redelivery, so `failed` never reaches here.
|
|
12
|
+
*
|
|
13
|
+
* `abandonedMutation` behind that gate says WHICH mutation, which the row
|
|
14
|
+
* otherwise cannot say: the hand-back sets `status` back to `active`, and
|
|
15
|
+
* `status` was the field naming the mutation that was outstanding. Reading the
|
|
16
|
+
* pair without it is how a move that handed back came to be reported to the
|
|
17
|
+
* user as a failed delete, under a "Delete again" button (issue #1229).
|
|
18
|
+
*
|
|
19
|
+
* Outside the gate the field is `none` and says nothing, exactly as
|
|
20
|
+
* `originalUid` says nothing once a placement has settled. Reading it
|
|
21
|
+
* unconditionally would resurrect a give-up a later mutation has settled.
|
|
22
|
+
*
|
|
23
|
+
* Two give-ups this deliberately does not name. `flag-push` never writes a
|
|
24
|
+
* placement at all — its give-up lives on its own marker row and in an
|
|
25
|
+
* operator alert. `placement-move-push` does restore the row when its push is
|
|
26
|
+
* exhausted, and drops its marker with it, but leaves this field at `none`:
|
|
27
|
+
* Remit's own classification move is not a mutation the user asked for, so a
|
|
28
|
+
* per-message treatment for it would report a failure against an intent nobody
|
|
29
|
+
* formed.
|
|
30
|
+
*/
|
|
31
|
+
export declare const abandonedMutationOf: (message: MessageSettlementFields) => MessageItem["abandonedMutation"];
|
|
32
|
+
/** A delete Remit refused to run or gave up on, having removed it locally first. */
|
|
33
|
+
export declare const hasAbandonedDelete: (message: MessageSettlementFields) => boolean;
|
|
34
|
+
/** A move Remit gave up on, having already pointed the row at the destination. */
|
|
35
|
+
export declare const hasAbandonedMove: (message: MessageSettlementFields) => boolean;
|
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import { MessageMutation, MessageSyncStatus } from "@remit/domain-enums";
|
|
2
|
-
import type { MessageItem } from "./types.js";
|
|
3
|
-
|
|
4
|
-
export type MessageSettlementFields = Pick<
|
|
5
|
-
MessageItem,
|
|
6
|
-
"status" | "syncStatus" | "abandonedMutation"
|
|
7
|
-
>;
|
|
8
|
-
|
|
9
2
|
/**
|
|
10
3
|
* Which mutation gave up on this row, and `none` where none did.
|
|
11
4
|
*
|
|
@@ -34,17 +27,10 @@ export type MessageSettlementFields = Pick<
|
|
|
34
27
|
* per-message treatment for it would report a failure against an intent nobody
|
|
35
28
|
* formed.
|
|
36
29
|
*/
|
|
37
|
-
export const abandonedMutationOf = (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
message.syncStatus === MessageSyncStatus.abandoned
|
|
41
|
-
? message.abandonedMutation
|
|
42
|
-
: MessageMutation.none;
|
|
43
|
-
|
|
30
|
+
export const abandonedMutationOf = (message) => message.syncStatus === MessageSyncStatus.abandoned
|
|
31
|
+
? message.abandonedMutation
|
|
32
|
+
: MessageMutation.none;
|
|
44
33
|
/** A delete Remit refused to run or gave up on, having removed it locally first. */
|
|
45
|
-
export const hasAbandonedDelete = (message
|
|
46
|
-
abandonedMutationOf(message) === MessageMutation.delete;
|
|
47
|
-
|
|
34
|
+
export const hasAbandonedDelete = (message) => abandonedMutationOf(message) === MessageMutation.delete;
|
|
48
35
|
/** A move Remit gave up on, having already pointed the row at the destination. */
|
|
49
|
-
export const hasAbandonedMove = (message
|
|
50
|
-
abandonedMutationOf(message) === MessageMutation.move;
|
|
36
|
+
export const hasAbandonedMove = (message) => abandonedMutationOf(message) === MessageMutation.move;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract version the mail-destroying queue events are minted under.
|
|
3
|
+
*
|
|
4
|
+
* It lives here because the producer (`MessageMoveService`) and the consumer
|
|
5
|
+
* (the imap-worker's handlers) sit in packages that cannot import each other,
|
|
6
|
+
* and a version the two sides disagree about is worse than no version at all.
|
|
7
|
+
* Bump it whenever a handler starts relying on a field an older producer never
|
|
8
|
+
* set.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MUTATION_EVENT_SCHEMA_VERSION = 2;
|
|
11
|
+
export declare const PATHLESS_MUTATION_EVENT_SCHEMA_VERSION = 3;
|
|
12
|
+
/**
|
|
13
|
+
* Queue payloads are `JSON.parse`d and cast with no validation, so the declared
|
|
14
|
+
* type is a promise the queue cannot keep. A handler that cannot vouch for an
|
|
15
|
+
* event's shape abandons it — treating a missing field as "skip the check"
|
|
16
|
+
* makes the unverified expunge the default for every event we cannot vouch for,
|
|
17
|
+
* including any future producer that forgets the field.
|
|
18
|
+
*/
|
|
19
|
+
export declare const isAcceptedSchemaVersion: (value: unknown) => boolean;
|
|
@@ -8,9 +8,7 @@
|
|
|
8
8
|
* set.
|
|
9
9
|
*/
|
|
10
10
|
export const MUTATION_EVENT_SCHEMA_VERSION = 2;
|
|
11
|
-
|
|
12
11
|
export const PATHLESS_MUTATION_EVENT_SCHEMA_VERSION = 3;
|
|
13
|
-
|
|
14
12
|
/**
|
|
15
13
|
* Queue payloads are `JSON.parse`d and cast with no validation, so the declared
|
|
16
14
|
* type is a promise the queue cannot keep. A handler that cannot vouch for an
|
|
@@ -18,6 +16,5 @@ export const PATHLESS_MUTATION_EVENT_SCHEMA_VERSION = 3;
|
|
|
18
16
|
* makes the unverified expunge the default for every event we cannot vouch for,
|
|
19
17
|
* including any future producer that forgets the field.
|
|
20
18
|
*/
|
|
21
|
-
export const isAcceptedSchemaVersion = (value
|
|
22
|
-
|
|
23
|
-
value === PATHLESS_MUTATION_EVENT_SCHEMA_VERSION;
|
|
19
|
+
export const isAcceptedSchemaVersion = (value) => value === MUTATION_EVENT_SCHEMA_VERSION ||
|
|
20
|
+
value === PATHLESS_MUTATION_EVENT_SCHEMA_VERSION;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FilterItem } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The `Filter` model's sentinel for "this rule takes no such action" — the
|
|
4
|
+
* value `actionLabelId` and `actionMailboxId` hold when a filter applies no
|
|
5
|
+
* label and moves nothing (RFC 034 Decision 3.1).
|
|
6
|
+
*/
|
|
7
|
+
export declare const FILTER_NO_ACTION = "None";
|
|
8
|
+
/**
|
|
9
|
+
* A standing rule that names senders and does nothing else.
|
|
10
|
+
*
|
|
11
|
+
* That combination has exactly one meaning. The index-time filter pipeline
|
|
12
|
+
* skips a filter with no label and no move, so such a rule changes nothing
|
|
13
|
+
* about where mail lands or how it is tagged; the only thing it can express is
|
|
14
|
+
* the user saying "not from this sender". `dismiss{muteSender:true}` writes
|
|
15
|
+
* one, and the calendar-suggestion producer reads it.
|
|
16
|
+
*
|
|
17
|
+
* Structural, never name-matching: the rule is recognised by its shape, so a
|
|
18
|
+
* rename in the settings UI cannot quietly un-mute a sender.
|
|
19
|
+
*/
|
|
20
|
+
export declare const isSenderMuteFilter: (filter: Pick<FilterItem, "scope" | "literalClauses" | "actionLabelId" | "actionMailboxId">) => boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether one of these rules mutes an address. Compared case-insensitively and
|
|
23
|
+
* whole: a mail address is one value, and a substring match would let a rule
|
|
24
|
+
* about `ada@example.com` silence `not-ada@example.com.evil.test`.
|
|
25
|
+
*/
|
|
26
|
+
export declare const isSenderMuted: (filters: readonly Pick<FilterItem, "scope" | "literalClauses" | "actionLabelId" | "actionMailboxId">[], sender: string) => boolean;
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { FilterClauseField, FilterScope } from "@remit/domain-enums";
|
|
2
|
-
import type { FilterItem } from "./types.js";
|
|
3
|
-
|
|
4
2
|
/**
|
|
5
3
|
* The `Filter` model's sentinel for "this rule takes no such action" — the
|
|
6
4
|
* value `actionLabelId` and `actionMailboxId` hold when a filter applies no
|
|
7
5
|
* label and moves nothing (RFC 034 Decision 3.1).
|
|
8
6
|
*/
|
|
9
7
|
export const FILTER_NO_ACTION = "None";
|
|
10
|
-
|
|
11
8
|
/**
|
|
12
9
|
* A standing rule that names senders and does nothing else.
|
|
13
10
|
*
|
|
@@ -20,40 +17,21 @@ export const FILTER_NO_ACTION = "None";
|
|
|
20
17
|
* Structural, never name-matching: the rule is recognised by its shape, so a
|
|
21
18
|
* rename in the settings UI cannot quietly un-mute a sender.
|
|
22
19
|
*/
|
|
23
|
-
export const isSenderMuteFilter = (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
)
|
|
29
|
-
filter.scope === FilterScope.Standing &&
|
|
30
|
-
filter.actionLabelId === FILTER_NO_ACTION &&
|
|
31
|
-
filter.actionMailboxId === FILTER_NO_ACTION &&
|
|
32
|
-
filter.literalClauses.length > 0 &&
|
|
33
|
-
filter.literalClauses.every(
|
|
34
|
-
(clause) =>
|
|
35
|
-
clause.field === FilterClauseField.From && clause.value.trim() !== "",
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
const fold = (value: string): string => value.trim().toLowerCase();
|
|
39
|
-
|
|
20
|
+
export const isSenderMuteFilter = (filter) => filter.scope === FilterScope.Standing &&
|
|
21
|
+
filter.actionLabelId === FILTER_NO_ACTION &&
|
|
22
|
+
filter.actionMailboxId === FILTER_NO_ACTION &&
|
|
23
|
+
filter.literalClauses.length > 0 &&
|
|
24
|
+
filter.literalClauses.every((clause) => clause.field === FilterClauseField.From && clause.value.trim() !== "");
|
|
25
|
+
const fold = (value) => value.trim().toLowerCase();
|
|
40
26
|
/**
|
|
41
27
|
* Whether one of these rules mutes an address. Compared case-insensitively and
|
|
42
28
|
* whole: a mail address is one value, and a substring match would let a rule
|
|
43
29
|
* about `ada@example.com` silence `not-ada@example.com.evil.test`.
|
|
44
30
|
*/
|
|
45
|
-
export const isSenderMuted = (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
): boolean => {
|
|
52
|
-
const wanted = fold(sender);
|
|
53
|
-
if (wanted === "") return false;
|
|
54
|
-
return filters.some(
|
|
55
|
-
(filter) =>
|
|
56
|
-
isSenderMuteFilter(filter) &&
|
|
57
|
-
filter.literalClauses.some((clause) => fold(clause.value) === wanted),
|
|
58
|
-
);
|
|
31
|
+
export const isSenderMuted = (filters, sender) => {
|
|
32
|
+
const wanted = fold(sender);
|
|
33
|
+
if (wanted === "")
|
|
34
|
+
return false;
|
|
35
|
+
return filters.some((filter) => isSenderMuteFilter(filter) &&
|
|
36
|
+
filter.literalClauses.some((clause) => fold(clause.value) === wanted));
|
|
59
37
|
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { SyncWarningStage, SyncWarningState } from "@remit/domain-enums";
|
|
2
|
+
export declare const SYNC_WARNING_SURFACE_ATTEMPTS = 3;
|
|
3
|
+
export declare const syncWarningState: (stage: (typeof SyncWarningStage)[keyof typeof SyncWarningStage], attempts: number) => (typeof SyncWarningState)[keyof typeof SyncWarningState];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { SyncWarningStage, SyncWarningState } from "@remit/domain-enums";
|
|
2
|
+
export const SYNC_WARNING_SURFACE_ATTEMPTS = 3;
|
|
3
|
+
export const syncWarningState = (stage, attempts) => stage === SyncWarningStage.Body && attempts >= SYNC_WARNING_SURFACE_ATTEMPTS
|
|
4
|
+
? SyncWarningState.GaveUp
|
|
5
|
+
: SyncWarningState.Retrying;
|