@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.
Files changed (174) hide show
  1. package/dist/account-settings.d.ts +371 -0
  2. package/dist/account-settings.js +114 -0
  3. package/dist/conformance/account.d.ts +3 -0
  4. package/dist/conformance/account.js +118 -0
  5. package/dist/conformance/calendar-collection.d.ts +3 -0
  6. package/dist/conformance/calendar-collection.js +251 -0
  7. package/dist/conformance/calendar-event-index.d.ts +3 -0
  8. package/dist/conformance/calendar-event-index.js +114 -0
  9. package/dist/conformance/calendar-feed-token.d.ts +3 -0
  10. package/dist/conformance/calendar-feed-token.js +97 -0
  11. package/dist/conformance/calendar-object.d.ts +3 -0
  12. package/dist/conformance/calendar-object.js +156 -0
  13. package/dist/conformance/calendar-suggestion.d.ts +3 -0
  14. package/dist/conformance/calendar-suggestion.js +255 -0
  15. package/{src/conformance/harness.ts → dist/conformance/harness.d.ts} +4 -4
  16. package/dist/conformance/harness.js +1 -0
  17. package/dist/conformance/index.js +8 -0
  18. package/dist/conformance/label.d.ts +3 -0
  19. package/dist/conformance/label.js +92 -0
  20. package/dist/conformance/sender-signer-standing.d.ts +3 -0
  21. package/dist/conformance/sender-signer-standing.js +81 -0
  22. package/dist/display-name.d.ts +1 -0
  23. package/dist/display-name.js +26 -0
  24. package/dist/errors.d.ts +150 -0
  25. package/dist/errors.js +181 -0
  26. package/dist/filter-ttl.d.ts +7 -0
  27. package/{src/filter-ttl.ts → dist/filter-ttl.js} +8 -11
  28. package/dist/folder-role.d.ts +239 -0
  29. package/dist/folder-role.js +276 -0
  30. package/dist/id.d.ts +124 -0
  31. package/{src/id.ts → dist/id.js} +41 -127
  32. package/dist/index.d.ts +37 -0
  33. package/dist/index.js +5 -0
  34. package/dist/interfaces/account-config.d.ts +15 -0
  35. package/dist/interfaces/account-config.js +1 -0
  36. package/dist/interfaces/account-export-request.d.ts +10 -0
  37. package/dist/interfaces/account-export-request.js +1 -0
  38. package/dist/interfaces/account-setting.d.ts +8 -0
  39. package/dist/interfaces/account-setting.js +1 -0
  40. package/dist/interfaces/account.d.ts +29 -0
  41. package/dist/interfaces/account.js +1 -0
  42. package/dist/interfaces/address.d.ts +70 -0
  43. package/dist/interfaces/address.js +1 -0
  44. package/dist/interfaces/calendar-collection.d.ts +38 -0
  45. package/dist/interfaces/calendar-collection.js +1 -0
  46. package/dist/interfaces/calendar-event-index.d.ts +19 -0
  47. package/dist/interfaces/calendar-event-index.js +1 -0
  48. package/dist/interfaces/calendar-feed-token.d.ts +31 -0
  49. package/dist/interfaces/calendar-feed-token.js +1 -0
  50. package/dist/interfaces/calendar-object.d.ts +36 -0
  51. package/dist/interfaces/calendar-object.js +1 -0
  52. package/dist/interfaces/calendar-store.d.ts +93 -0
  53. package/dist/interfaces/calendar-store.js +1 -0
  54. package/dist/interfaces/calendar-suggestion.d.ts +62 -0
  55. package/dist/interfaces/calendar-suggestion.js +1 -0
  56. package/dist/interfaces/config-import.d.ts +8 -0
  57. package/dist/interfaces/config-import.js +1 -0
  58. package/dist/interfaces/envelope.d.ts +24 -0
  59. package/dist/interfaces/envelope.js +1 -0
  60. package/dist/interfaces/filter.d.ts +28 -0
  61. package/dist/interfaces/filter.js +1 -0
  62. package/dist/interfaces/label.d.ts +17 -0
  63. package/dist/interfaces/label.js +1 -0
  64. package/dist/interfaces/mailbox-lock.d.ts +12 -0
  65. package/dist/interfaces/mailbox-lock.js +1 -0
  66. package/dist/interfaces/mailbox-special-use.d.ts +54 -0
  67. package/dist/interfaces/mailbox-special-use.js +1 -0
  68. package/dist/interfaces/mailbox.d.ts +79 -0
  69. package/dist/interfaces/mailbox.js +1 -0
  70. package/dist/interfaces/message-flag-push.d.ts +9 -0
  71. package/dist/interfaces/message-flag-push.js +1 -0
  72. package/dist/interfaces/message-flag.d.ts +14 -0
  73. package/dist/interfaces/message-flag.js +1 -0
  74. package/dist/interfaces/message-label.d.ts +21 -0
  75. package/dist/interfaces/message-label.js +1 -0
  76. package/dist/interfaces/message-placement-move.d.ts +8 -0
  77. package/dist/interfaces/message-placement-move.js +1 -0
  78. package/dist/interfaces/message.d.ts +60 -0
  79. package/dist/interfaces/message.js +1 -0
  80. package/dist/interfaces/organize-job-request.d.ts +10 -0
  81. package/dist/interfaces/organize-job-request.js +1 -0
  82. package/dist/interfaces/outbox-attachment.d.ts +101 -0
  83. package/dist/interfaces/outbox-attachment.js +7 -0
  84. package/dist/interfaces/outbox-message.d.ts +70 -0
  85. package/dist/interfaces/outbox-message.js +20 -0
  86. package/dist/interfaces/quarantine.d.ts +20 -0
  87. package/dist/interfaces/quarantine.js +1 -0
  88. package/dist/interfaces/sender-signer-standing.d.ts +26 -0
  89. package/dist/interfaces/sender-signer-standing.js +1 -0
  90. package/dist/interfaces/sync-warning.d.ts +11 -0
  91. package/dist/interfaces/sync-warning.js +1 -0
  92. package/dist/interfaces/thread-message.d.ts +202 -0
  93. package/dist/interfaces/thread-message.js +1 -0
  94. package/dist/mailbox-name.d.ts +35 -0
  95. package/dist/mailbox-name.js +68 -0
  96. package/dist/message-parts.d.ts +22 -0
  97. package/dist/message-parts.js +18 -0
  98. package/dist/message-settlement.d.ts +35 -0
  99. package/{src/message-settlement.ts → dist/message-settlement.js} +5 -19
  100. package/dist/mutation-events.d.ts +19 -0
  101. package/{src/mutation-events.ts → dist/mutation-events.js} +2 -5
  102. package/dist/sender-mute.d.ts +26 -0
  103. package/{src/sender-mute.ts → dist/sender-mute.js} +12 -34
  104. package/dist/sync-warning-state.d.ts +3 -0
  105. package/dist/sync-warning-state.js +5 -0
  106. package/dist/types.d.ts +491 -0
  107. package/dist/types.js +1 -0
  108. package/dist/update-manifest.d.ts +24 -0
  109. package/dist/update-manifest.js +20 -0
  110. package/dist/virtual-copy.d.ts +8 -0
  111. package/{src/virtual-copy.ts → dist/virtual-copy.js} +10 -15
  112. package/dist/wellknown.d.ts +47 -0
  113. package/{src/wellknown.ts → dist/wellknown.js} +26 -49
  114. package/package.json +44 -79
  115. package/src/account-settings.ts +0 -194
  116. package/src/conformance/account.ts +0 -104
  117. package/src/conformance/calendar-collection.ts +0 -342
  118. package/src/conformance/calendar-event-index.ts +0 -203
  119. package/src/conformance/calendar-feed-token.ts +0 -148
  120. package/src/conformance/calendar-object.ts +0 -269
  121. package/src/conformance/calendar-suggestion.ts +0 -490
  122. package/src/conformance/label.ts +0 -136
  123. package/src/conformance/sender-signer-standing.ts +0 -117
  124. package/src/display-name.test.ts +0 -101
  125. package/src/display-name.ts +0 -40
  126. package/src/errors.ts +0 -244
  127. package/src/folder-role.test.ts +0 -422
  128. package/src/folder-role.ts +0 -447
  129. package/src/id.test.ts +0 -352
  130. package/src/index.ts +0 -170
  131. package/src/interfaces/account-config.ts +0 -25
  132. package/src/interfaces/account-export-request.ts +0 -21
  133. package/src/interfaces/account-setting.ts +0 -14
  134. package/src/interfaces/account.ts +0 -43
  135. package/src/interfaces/address.ts +0 -122
  136. package/src/interfaces/calendar-collection.ts +0 -60
  137. package/src/interfaces/calendar-event-index.ts +0 -34
  138. package/src/interfaces/calendar-feed-token.ts +0 -38
  139. package/src/interfaces/calendar-object.ts +0 -58
  140. package/src/interfaces/calendar-suggestion.ts +0 -93
  141. package/src/interfaces/calendar-unit-of-work.ts +0 -47
  142. package/src/interfaces/config-import.ts +0 -16
  143. package/src/interfaces/envelope.ts +0 -32
  144. package/src/interfaces/filter-anchor-transaction.ts +0 -23
  145. package/src/interfaces/filter-anchor.ts +0 -11
  146. package/src/interfaces/filter.ts +0 -41
  147. package/src/interfaces/label.ts +0 -30
  148. package/src/interfaces/mailbox-lock.ts +0 -32
  149. package/src/interfaces/mailbox-special-use.ts +0 -60
  150. package/src/interfaces/mailbox.ts +0 -115
  151. package/src/interfaces/message-flag-push.ts +0 -17
  152. package/src/interfaces/message-flag.ts +0 -15
  153. package/src/interfaces/message-label.ts +0 -25
  154. package/src/interfaces/message-placement-move.ts +0 -15
  155. package/src/interfaces/message.ts +0 -65
  156. package/src/interfaces/organize-job-request.ts +0 -19
  157. package/src/interfaces/outbox-attachment.ts +0 -132
  158. package/src/interfaces/outbox-message.ts +0 -106
  159. package/src/interfaces/quarantine.ts +0 -22
  160. package/src/interfaces/sender-signer-standing.ts +0 -37
  161. package/src/interfaces/thread-message.ts +0 -264
  162. package/src/interfaces/unit-of-work.ts +0 -28
  163. package/src/mailbox-name.test.ts +0 -106
  164. package/src/mailbox-name.ts +0 -90
  165. package/src/mailbox-state-doors.test.ts +0 -104
  166. package/src/message-placement-doors.test.ts +0 -100
  167. package/src/message-settlement.test.ts +0 -132
  168. package/src/sender-mute.test.ts +0 -76
  169. package/src/types.ts +0 -820
  170. package/src/update-manifest.test.ts +0 -130
  171. package/src/update-manifest.ts +0 -22
  172. package/src/virtual-copy.test.ts +0 -34
  173. package/tsconfig.json +0 -8
  174. /package/{src/conformance/index.ts → dist/conformance/index.d.ts} +0 -0
@@ -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
- message: MessageSettlementFields,
39
- ): MessageItem["abandonedMutation"] =>
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: MessageSettlementFields): boolean =>
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: MessageSettlementFields): boolean =>
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: unknown): boolean =>
22
- value === MUTATION_EVENT_SCHEMA_VERSION ||
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
- filter: Pick<
25
- FilterItem,
26
- "scope" | "literalClauses" | "actionLabelId" | "actionMailboxId"
27
- >,
28
- ): boolean =>
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
- filters: readonly Pick<
47
- FilterItem,
48
- "scope" | "literalClauses" | "actionLabelId" | "actionMailboxId"
49
- >[],
50
- sender: string,
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;