@remit/mailbox-service 0.0.112 → 0.0.113

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 (270) hide show
  1. package/dist/account-credentials.d.ts +83 -0
  2. package/dist/account-credentials.js +104 -0
  3. package/dist/attribute-mapper.d.ts +36 -0
  4. package/dist/attribute-mapper.js +119 -0
  5. package/dist/body-parse.d.ts +28 -0
  6. package/{src/body-parse.ts → dist/body-parse.js} +18 -26
  7. package/dist/body-part-mapper.d.ts +92 -0
  8. package/dist/body-part-mapper.js +301 -0
  9. package/dist/body-sync-queue.d.ts +59 -0
  10. package/dist/body-sync-queue.js +56 -0
  11. package/dist/body-sync-terminal.d.ts +58 -0
  12. package/dist/body-sync-terminal.js +95 -0
  13. package/dist/body-sync.d.ts +351 -0
  14. package/dist/body-sync.js +1204 -0
  15. package/dist/calendar-parts.d.ts +23 -0
  16. package/dist/calendar-parts.js +18 -0
  17. package/dist/classification-backfill.d.ts +91 -0
  18. package/dist/classification-backfill.js +180 -0
  19. package/dist/classify-message.d.ts +31 -0
  20. package/dist/classify-message.js +89 -0
  21. package/dist/connection-factory.d.ts +54 -0
  22. package/dist/connection-factory.js +54 -0
  23. package/dist/connection-test.d.ts +41 -0
  24. package/dist/connection-test.js +152 -0
  25. package/dist/external-move.d.ts +94 -0
  26. package/dist/external-move.js +173 -0
  27. package/dist/filter-operations.d.ts +42 -0
  28. package/dist/filter-operations.js +211 -0
  29. package/dist/filters/filter-config.d.ts +14 -0
  30. package/dist/filters/filter-config.js +10 -0
  31. package/{src/filters/list-id.ts → dist/filters/list-id.d.ts} +2 -16
  32. package/dist/filters/list-id.js +26 -0
  33. package/dist/filters/match.d.ts +69 -0
  34. package/dist/filters/match.js +165 -0
  35. package/dist/filters/pipeline.d.ts +64 -0
  36. package/dist/filters/pipeline.js +100 -0
  37. package/{src/flag-push-terminal.ts → dist/flag-push-terminal.d.ts} +16 -72
  38. package/dist/flag-push-terminal.js +63 -0
  39. package/dist/flag-push.d.ts +69 -0
  40. package/dist/flag-push.js +111 -0
  41. package/dist/flag-queue.d.ts +175 -0
  42. package/dist/flag-queue.js +291 -0
  43. package/dist/folder-operations.d.ts +39 -0
  44. package/dist/folder-operations.js +112 -0
  45. package/dist/folder-placement.d.ts +13 -0
  46. package/dist/folder-placement.js +23 -0
  47. package/dist/heuristics/authenticationResults.d.ts +10 -0
  48. package/dist/heuristics/authenticationResults.js +187 -0
  49. package/dist/heuristics/classifyByHeaders.d.ts +82 -0
  50. package/dist/heuristics/classifyByHeaders.js +365 -0
  51. package/dist/heuristics/classifyPlacement.d.ts +67 -0
  52. package/dist/heuristics/classifyPlacement.js +153 -0
  53. package/dist/heuristics/machineSenders.d.ts +21 -0
  54. package/{src/heuristics/machineSenders.ts → dist/heuristics/machineSenders.js} +34 -40
  55. package/dist/heuristics/senderMismatch.d.ts +81 -0
  56. package/dist/heuristics/senderMismatch.js +192 -0
  57. package/dist/heuristics/socialDomains.d.ts +14 -0
  58. package/{src/heuristics/socialDomains.ts → dist/heuristics/socialDomains.js} +9 -9
  59. package/dist/heuristics/transactionalDomains.d.ts +18 -0
  60. package/{src/heuristics/transactionalDomains.ts → dist/heuristics/transactionalDomains.js} +8 -8
  61. package/dist/imapflow-connection.d.ts +377 -0
  62. package/dist/imapflow-connection.js +1147 -0
  63. package/dist/index.d.ts +63 -0
  64. package/dist/index.js +65 -0
  65. package/dist/list-id-backfill.d.ts +83 -0
  66. package/dist/list-id-backfill.js +132 -0
  67. package/dist/mailbox-cursor-rebuild.d.ts +74 -0
  68. package/dist/mailbox-cursor-rebuild.js +71 -0
  69. package/{src/mailbox-cursor.ts → dist/mailbox-cursor.d.ts} +20 -98
  70. package/dist/mailbox-cursor.js +102 -0
  71. package/dist/mailbox-intent.d.ts +41 -0
  72. package/{src/mailbox-intent.ts → dist/mailbox-intent.js} +38 -64
  73. package/dist/mailbox-management.d.ts +240 -0
  74. package/dist/mailbox-management.js +576 -0
  75. package/dist/mailbox-presence.d.ts +52 -0
  76. package/{src/mailbox-presence.ts → dist/mailbox-presence.js} +19 -30
  77. package/dist/mailbox-queue.d.ts +126 -0
  78. package/dist/mailbox-queue.js +214 -0
  79. package/dist/mailbox-sync.d.ts +93 -0
  80. package/dist/mailbox-sync.js +490 -0
  81. package/dist/mailbox-upstream.d.ts +38 -0
  82. package/{src/mailbox-upstream.ts → dist/mailbox-upstream.js} +37 -47
  83. package/dist/message-category.d.ts +51 -0
  84. package/{src/message-category.ts → dist/message-category.js} +28 -71
  85. package/dist/message-move.d.ts +285 -0
  86. package/dist/message-move.js +858 -0
  87. package/dist/message-parser.d.ts +14 -0
  88. package/dist/message-parser.js +17 -0
  89. package/{src/message-presence.ts → dist/message-presence.d.ts} +1 -13
  90. package/dist/message-presence.js +29 -0
  91. package/dist/message-read.d.ts +51 -0
  92. package/dist/message-read.js +31 -0
  93. package/dist/message-sync.d.ts +345 -0
  94. package/dist/message-sync.js +1358 -0
  95. package/dist/mime-walker.d.ts +102 -0
  96. package/dist/mime-walker.js +198 -0
  97. package/dist/organize-job.d.ts +20 -0
  98. package/dist/organize-job.js +37 -0
  99. package/dist/outbox-attachment-content.d.ts +37 -0
  100. package/dist/outbox-attachment-content.js +50 -0
  101. package/dist/outbox-attachment-filename.d.ts +19 -0
  102. package/dist/outbox-attachment-filename.js +64 -0
  103. package/dist/outbox-attachment.d.ts +148 -0
  104. package/dist/outbox-attachment.js +273 -0
  105. package/dist/outbox-queue.d.ts +73 -0
  106. package/dist/outbox-queue.js +284 -0
  107. package/{src/outbox-status.ts → dist/outbox-status.d.ts} +1 -6
  108. package/dist/outbox-status.js +18 -0
  109. package/dist/ownership.d.ts +24 -0
  110. package/dist/ownership.js +83 -0
  111. package/dist/placement-move-terminal.d.ts +67 -0
  112. package/dist/placement-move-terminal.js +88 -0
  113. package/dist/placement-move.d.ts +83 -0
  114. package/dist/placement-move.js +216 -0
  115. package/{src/placement-settled.ts → dist/placement-settled.d.ts} +13 -68
  116. package/dist/placement-settled.js +75 -0
  117. package/dist/quarantine.d.ts +95 -0
  118. package/dist/quarantine.js +131 -0
  119. package/dist/refusal.d.ts +31 -0
  120. package/dist/refusal.js +3 -0
  121. package/{src/restore-source-placement.ts → dist/restore-source-placement.d.ts} +29 -97
  122. package/dist/restore-source-placement.js +79 -0
  123. package/dist/search-text-backfill.d.ts +15 -0
  124. package/dist/search-text-backfill.js +33 -0
  125. package/dist/snippet.d.ts +61 -0
  126. package/dist/snippet.js +129 -0
  127. package/dist/spam-report.d.ts +86 -0
  128. package/dist/spam-report.js +241 -0
  129. package/dist/sync-warning.d.ts +21 -0
  130. package/dist/sync-warning.js +26 -0
  131. package/dist/sync-watermarks.d.ts +138 -0
  132. package/dist/sync-watermarks.js +205 -0
  133. package/dist/test-helpers/fake-imap.d.ts +151 -0
  134. package/dist/test-helpers/fake-imap.js +508 -0
  135. package/dist/test-helpers/folder-roles.d.ts +34 -0
  136. package/dist/test-helpers/folder-roles.js +47 -0
  137. package/dist/test-helpers/mime-tree-generator.d.ts +82 -0
  138. package/dist/test-helpers/mime-tree-generator.js +329 -0
  139. package/dist/test-helpers/test-store.d.ts +69 -0
  140. package/dist/test-helpers/test-store.js +193 -0
  141. package/dist/text/normalizer.d.ts +20 -0
  142. package/dist/text/normalizer.js +102 -0
  143. package/dist/thread-message-rows.d.ts +69 -0
  144. package/dist/thread-message-rows.js +40 -0
  145. package/dist/thread-root.d.ts +7 -0
  146. package/dist/thread-root.js +25 -0
  147. package/dist/types.d.ts +343 -0
  148. package/dist/types.js +19 -0
  149. package/package.json +86 -88
  150. package/src/account-credentials.ts +0 -210
  151. package/src/attribute-mapper.test.ts +0 -104
  152. package/src/attribute-mapper.ts +0 -148
  153. package/src/body-parse.test.ts +0 -55
  154. package/src/body-part-mapper.property.test.ts +0 -354
  155. package/src/body-part-mapper.ts +0 -410
  156. package/src/body-sync-queue.test.ts +0 -64
  157. package/src/body-sync-queue.ts +0 -131
  158. package/src/body-sync-terminal.test.ts +0 -148
  159. package/src/body-sync-terminal.ts +0 -174
  160. package/src/body-sync.test.ts +0 -925
  161. package/src/body-sync.ts +0 -1891
  162. package/src/calendar-parts.ts +0 -41
  163. package/src/classification-backfill.test.ts +0 -285
  164. package/src/classification-backfill.ts +0 -363
  165. package/src/classify-message.test.ts +0 -142
  166. package/src/classify-message.ts +0 -116
  167. package/src/connection-factory.ts +0 -102
  168. package/src/connection-test.ts +0 -208
  169. package/src/credentials.test.ts +0 -181
  170. package/src/external-move.test.ts +0 -262
  171. package/src/external-move.ts +0 -285
  172. package/src/filter-operations.test.ts +0 -319
  173. package/src/filter-operations.ts +0 -377
  174. package/src/filter-update.test.ts +0 -313
  175. package/src/filters/filter-config.test.ts +0 -87
  176. package/src/filters/filter-config.ts +0 -34
  177. package/src/filters/list-id.test.ts +0 -50
  178. package/src/filters/match.test.ts +0 -392
  179. package/src/filters/match.ts +0 -225
  180. package/src/filters/pipeline.test.ts +0 -199
  181. package/src/filters/pipeline.ts +0 -170
  182. package/src/flag-push.test.ts +0 -112
  183. package/src/flag-push.ts +0 -198
  184. package/src/flag-queue.test.ts +0 -197
  185. package/src/flag-queue.ts +0 -509
  186. package/src/folder-operations.test.ts +0 -273
  187. package/src/folder-operations.ts +0 -217
  188. package/src/folder-placement.ts +0 -56
  189. package/src/forwarded-message.test.ts +0 -122
  190. package/src/heuristics/authenticationResults.test.ts +0 -189
  191. package/src/heuristics/authenticationResults.ts +0 -212
  192. package/src/heuristics/classifyByHeaders.test.ts +0 -889
  193. package/src/heuristics/classifyByHeaders.ts +0 -467
  194. package/src/heuristics/classifyPlacement.test.ts +0 -366
  195. package/src/heuristics/classifyPlacement.ts +0 -211
  196. package/src/heuristics/classifyRealMail.test.ts +0 -242
  197. package/src/heuristics/machineSenders.test.ts +0 -101
  198. package/src/heuristics/senderMismatch.test.ts +0 -346
  199. package/src/heuristics/senderMismatch.ts +0 -259
  200. package/src/imapflow-connect-retry.test.ts +0 -97
  201. package/src/imapflow-connection.test.ts +0 -503
  202. package/src/imapflow-connection.ts +0 -1476
  203. package/src/index.ts +0 -397
  204. package/src/list-id-backfill.test.ts +0 -207
  205. package/src/list-id-backfill.ts +0 -274
  206. package/src/mailbox-cursor-rebuild.test.ts +0 -171
  207. package/src/mailbox-cursor-rebuild.ts +0 -146
  208. package/src/mailbox-cursor.test.ts +0 -141
  209. package/src/mailbox-delete-intent.test.ts +0 -119
  210. package/src/mailbox-management.test.ts +0 -243
  211. package/src/mailbox-management.ts +0 -838
  212. package/src/mailbox-presence.test.ts +0 -74
  213. package/src/mailbox-queue.ts +0 -397
  214. package/src/mailbox-rename-intent.test.ts +0 -220
  215. package/src/mailbox-rename-subtree.test.ts +0 -287
  216. package/src/mailbox-sync.test.ts +0 -286
  217. package/src/mailbox-sync.ts +0 -724
  218. package/src/message-move.test.ts +0 -726
  219. package/src/message-move.ts +0 -1518
  220. package/src/message-parser.ts +0 -26
  221. package/src/message-presence.test.ts +0 -50
  222. package/src/message-read.ts +0 -87
  223. package/src/message-save-retry.test.ts +0 -90
  224. package/src/message-sync-flags.test.ts +0 -167
  225. package/src/message-sync.test.ts +0 -762
  226. package/src/message-sync.ts +0 -2133
  227. package/src/mime-walker.test.ts +0 -311
  228. package/src/mime-walker.ts +0 -306
  229. package/src/organize-job.ts +0 -93
  230. package/src/outbox-attachment-content.test.ts +0 -153
  231. package/src/outbox-attachment-content.ts +0 -104
  232. package/src/outbox-attachment-filename.test.ts +0 -132
  233. package/src/outbox-attachment-filename.ts +0 -76
  234. package/src/outbox-attachment.test.ts +0 -626
  235. package/src/outbox-attachment.ts +0 -536
  236. package/src/outbox-failed-recovery.test.ts +0 -299
  237. package/src/outbox-queue.test.ts +0 -315
  238. package/src/outbox-queue.ts +0 -525
  239. package/src/ownership.ts +0 -140
  240. package/src/placement-move-terminal.ts +0 -145
  241. package/src/placement-move.ts +0 -363
  242. package/src/placement-settled.test.ts +0 -147
  243. package/src/placement.test.ts +0 -404
  244. package/src/quarantine.test.ts +0 -133
  245. package/src/quarantine.ts +0 -229
  246. package/src/refusal.ts +0 -29
  247. package/src/snippet.test.ts +0 -213
  248. package/src/snippet.ts +0 -152
  249. package/src/spam-report.test.ts +0 -329
  250. package/src/spam-report.ts +0 -357
  251. package/src/sync-warning.test.ts +0 -213
  252. package/src/sync-warning.ts +0 -56
  253. package/src/sync-watermarks.test.ts +0 -409
  254. package/src/sync-watermarks.ts +0 -319
  255. package/src/system-flag-wire-format.test.ts +0 -77
  256. package/src/test-helpers/fake-imap.ts +0 -690
  257. package/src/test-helpers/folder-roles.ts +0 -66
  258. package/src/test-helpers/mime-tree-generator.ts +0 -461
  259. package/src/test-helpers/test-queue.ts +0 -56
  260. package/src/test-helpers/test-store.ts +0 -299
  261. package/src/text/normalizer.test.ts +0 -223
  262. package/src/text/normalizer.ts +0 -140
  263. package/src/thread-message-rows.ts +0 -77
  264. package/src/thread-root.test.ts +0 -29
  265. package/src/thread-root.ts +0 -51
  266. package/src/types/mailparser-augment.d.ts +0 -23
  267. package/src/types/natural-porter-stemmer.d.ts +0 -50
  268. package/src/types/stopword.d.ts +0 -69
  269. package/src/types.ts +0 -371
  270. package/tsconfig.json +0 -8
package/src/quarantine.ts DELETED
@@ -1,229 +0,0 @@
1
- import type {
2
- IMailboxSpecialUseRepository,
3
- IQuarantineRepository,
4
- MessageData,
5
- MessageItem,
6
- QuarantineItem,
7
- QuarantineMimeNodeItem,
8
- } from "@remit/data-ports";
9
- import { quarantineMessageIdHash, ROOT_PART_PATH } from "@remit/data-ports/id";
10
- import { CanonicalMailboxRole } from "@remit/domain-enums";
11
-
12
- type CanonicalRole = NonNullable<QuarantineItem["mailboxRole"]>;
13
- type FailureStage = QuarantineItem["failureStage"];
14
- type FailureCode = QuarantineItem["failureCode"];
15
-
16
- const CANONICAL_ROLES = new Set<string>(Object.values(CanonicalMailboxRole));
17
-
18
- /**
19
- * The canonical role of the folder a quarantined message arrived in, read from
20
- * the server's own RFC 6154 SPECIAL-USE declaration — the same source the
21
- * appointment flow seeds from. A folder the server declares nothing about has
22
- * no role, which is the normal state for a plain folder and is why the field
23
- * is optional. `Important` is a SPECIAL-USE with no canonical role and drops
24
- * out here rather than being invented into one.
25
- */
26
- export const resolveMailboxRole = (
27
- mailboxPath: string,
28
- specialUse: readonly string[],
29
- ): CanonicalRole | undefined => {
30
- if (mailboxPath.toUpperCase() === "INBOX") return CanonicalMailboxRole.Inbox;
31
- const matched = specialUse.find((use) => CANONICAL_ROLES.has(use));
32
- return matched as CanonicalRole | undefined;
33
- };
34
-
35
- /** Everything about the quarantine that is the same for every uid in a round. */
36
- export interface QuarantineContext {
37
- accountId: string;
38
- accountConfigId: string;
39
- mailboxId: string;
40
- mailboxPath: string;
41
- uidValidity: number;
42
- /** Rounds tried before the message was set aside. */
43
- attempts: number;
44
- }
45
-
46
- /**
47
- * The repro fingerprint: what the message declared itself to be. Every field
48
- * is optional-by-absence because a message can fail before its shape was read,
49
- * and a required column would force the writer to invent a value or drop the
50
- * record.
51
- */
52
- export interface QuarantineMessageShape {
53
- contentType?: string;
54
- transferEncoding?: string;
55
- charset?: string;
56
- sizeBytes?: number;
57
- structure: QuarantineMimeNodeItem[];
58
- messageIdHash?: string;
59
- }
60
-
61
- export interface QuarantineFailure {
62
- stage: FailureStage;
63
- code: FailureCode;
64
- /** Parser error text. Stored, shown on screen, never published. */
65
- message: string;
66
- partPath?: string;
67
- }
68
-
69
- const EMPTY_SHAPE: QuarantineMessageShape = { structure: [] };
70
-
71
- /**
72
- * Message shape off the rows metadata sync already wrote. The body path has no
73
- * FETCH result to read — it streams raw bytes — but the MIME tree it needs was
74
- * walked into BodyPart rows when the message's metadata was synced, so the
75
- * fingerprint comes from there rather than from re-reading headers the parser
76
- * has just refused.
77
- */
78
- export const shapeFromMessageData = (
79
- message: Pick<MessageItem, "rfc822Size">,
80
- data: MessageData,
81
- messageIdHeader: string | undefined,
82
- ): QuarantineMessageShape => {
83
- const parts = [...data.bodyPart].sort((a, b) =>
84
- a.partPath.localeCompare(b.partPath, "en", { numeric: true }),
85
- );
86
- const root = parts.find((part) => part.partPath === ROOT_PART_PATH);
87
- const rootCharset = root
88
- ? data.bodyPartParameter.find(
89
- (param) =>
90
- param.bodyPartId === root.bodyPartId &&
91
- param.parameterName.toLowerCase() === "charset",
92
- )?.parameterValue
93
- : undefined;
94
-
95
- return {
96
- ...(root ? { contentType: `${root.mediaType}/${root.mediaSubtype}` } : {}),
97
- ...(root ? { transferEncoding: root.transferEncoding } : {}),
98
- ...(rootCharset ? { charset: rootCharset } : {}),
99
- ...(message.rfc822Size ? { sizeBytes: message.rfc822Size } : {}),
100
- structure: parts.map((part) => ({
101
- depth: partPathDepth(part.partPath),
102
- contentType: `${part.mediaType}/${part.mediaSubtype}`,
103
- })),
104
- ...hashOf(messageIdHeader),
105
- };
106
- };
107
-
108
- const partPathDepth = (partPath: string): number =>
109
- partPath === ROOT_PART_PATH ? 0 : partPath.split(".").length;
110
-
111
- const hashOf = (
112
- messageIdHeader: string | undefined,
113
- ): { messageIdHash?: string } => {
114
- const messageIdHash = quarantineMessageIdHash(messageIdHeader);
115
- return messageIdHash ? { messageIdHash } : {};
116
- };
117
-
118
- /**
119
- * The set of messages already set aside, held for the duration of one sync
120
- * round. A round loads it once and filters against it in memory: the list is
121
- * small by design — a growing one is a bug being reported, not a page to
122
- * paginate — and a lookup per message would put a query on the hot path for a
123
- * state that is almost always empty.
124
- */
125
- export class QuarantinedUids {
126
- private readonly keys: ReadonlySet<string>;
127
-
128
- constructor(entries: readonly QuarantineItem[]) {
129
- this.keys = new Set(
130
- entries.map((entry) =>
131
- uidKey(entry.mailboxId, entry.uidValidity, entry.uid),
132
- ),
133
- );
134
- }
135
-
136
- get size(): number {
137
- return this.keys.size;
138
- }
139
-
140
- has(mailboxId: string, uidValidity: number, uid: number): boolean {
141
- return this.keys.has(uidKey(mailboxId, uidValidity, uid));
142
- }
143
- }
144
-
145
- const uidKey = (mailboxId: string, uidValidity: number, uid: number): string =>
146
- `${mailboxId}:${uidValidity}:${uid}`;
147
-
148
- export interface QuarantineLogger {
149
- info(obj: Record<string, unknown>, msg: string): void;
150
- warn(obj: Record<string, unknown>, msg: string): void;
151
- }
152
-
153
- /**
154
- * Writes the record a message becomes when the sync path cannot apply it
155
- * (issue #72), and loads the set a round filters against.
156
- *
157
- * Only a message defect reaches this class. Deciding that is the caller's job
158
- * and it is made at exactly one kind of catch site — one narrow enough that
159
- * the error can only have come from the message itself. An S3, queue or
160
- * database failure propagates; writing one here would advance a cursor past
161
- * mail that is fine and tell the user it was unreadable.
162
- */
163
- export class QuarantineService {
164
- constructor(
165
- private readonly repository: IQuarantineRepository,
166
- private readonly mailboxSpecialUseService: IMailboxSpecialUseRepository,
167
- private readonly workerVersion: string,
168
- private readonly log: QuarantineLogger,
169
- ) {}
170
-
171
- async load(accountConfigId: string): Promise<QuarantinedUids> {
172
- return new QuarantinedUids(
173
- await this.repository.listByAccountConfigId(accountConfigId),
174
- );
175
- }
176
-
177
- /**
178
- * Set a message aside. Resolves only once the row is durable, because the
179
- * caller's next act is to let a cursor move past this uid — and a cursor
180
- * that moves past work no record survives is the silent loss this whole
181
- * feature exists to end.
182
- */
183
- async record(
184
- context: QuarantineContext,
185
- uid: number,
186
- failure: QuarantineFailure,
187
- shape: QuarantineMessageShape = EMPTY_SHAPE,
188
- ): Promise<void> {
189
- // Resolved here rather than per round: a round almost never writes a row,
190
- // so the lookup belongs on the write and not on the hot path.
191
- const specialUse = await this.mailboxSpecialUseService.listByMailboxId(
192
- context.mailboxId,
193
- );
194
- const mailboxRole = resolveMailboxRole(
195
- context.mailboxPath,
196
- specialUse.map((entry) => entry.specialUse),
197
- );
198
-
199
- await this.repository.upsert({
200
- accountConfigId: context.accountConfigId,
201
- accountId: context.accountId,
202
- mailboxId: context.mailboxId,
203
- uidValidity: context.uidValidity,
204
- uid,
205
- ...(mailboxRole ? { mailboxRole } : {}),
206
- mailboxPath: context.mailboxPath,
207
- quarantinedAt: Date.now(),
208
- attempts: context.attempts,
209
- failureStage: failure.stage,
210
- failureCode: failure.code,
211
- failureMessage: failure.message,
212
- ...(failure.partPath ? { failurePartPath: failure.partPath } : {}),
213
- workerVersion: this.workerVersion,
214
- ...shape,
215
- });
216
-
217
- this.log.warn(
218
- {
219
- mailboxId: context.mailboxId,
220
- mailboxPath: context.mailboxPath,
221
- uid,
222
- uidValidity: context.uidValidity,
223
- failureStage: failure.stage,
224
- failureCode: failure.code,
225
- },
226
- "Message quarantined; cursor may advance past it",
227
- );
228
- }
229
- }
package/src/refusal.ts DELETED
@@ -1,29 +0,0 @@
1
- import type { MailboxItem } from "@remit/data-ports";
2
-
3
- export type AccessMode = "read" | "act";
4
-
5
- export type Refusal =
6
- | { kind: "Invalid"; message: string }
7
- | { kind: "Forbidden"; message: string }
8
- | { kind: "NotFound"; message: string }
9
- | { kind: "Conflict"; message: string }
10
- | {
11
- kind: "NotSettled";
12
- message: string;
13
- mailboxId: string;
14
- syncStatus: MailboxItem["syncStatus"];
15
- };
16
-
17
- export interface Refused {
18
- ok: false;
19
- refusal: Refusal;
20
- }
21
-
22
- export type Refusable<T> = { ok: true; value: T } | Refused;
23
-
24
- export const accept = <T>(value: T): Refusable<T> => ({ ok: true, value });
25
-
26
- export const refuse = (refusal: Refusal): Refused => ({ ok: false, refusal });
27
-
28
- export const invalid = (message: string): Refused =>
29
- refuse({ kind: "Invalid", message });
@@ -1,213 +0,0 @@
1
- import assert from "node:assert";
2
- import { describe, it } from "node:test";
3
- import {
4
- extractSnippetFromEmail,
5
- generateSnippet,
6
- normalizeSubject,
7
- removeQuotedContent,
8
- } from "./snippet.js";
9
-
10
- describe("removeQuotedContent", () => {
11
- it("removes content after > quote marker", () => {
12
- const text = "Hello\nWorld\n> quoted line\nmore quoted";
13
- assert.strictEqual(removeQuotedContent(text), "Hello\nWorld");
14
- });
15
-
16
- it("removes content after 'On ... wrote:' pattern", () => {
17
- const text = "Hello\nOn Monday, John wrote:\nQuoted content";
18
- assert.strictEqual(removeQuotedContent(text), "Hello");
19
- });
20
-
21
- it("removes content after original message separator", () => {
22
- const text = "Hello\n--- Original Message ---\nQuoted content";
23
- assert.strictEqual(removeQuotedContent(text), "Hello");
24
- });
25
-
26
- it("removes content after Outlook underline separator", () => {
27
- const text = "Hello\n___\nQuoted content";
28
- assert.strictEqual(removeQuotedContent(text), "Hello");
29
- });
30
-
31
- it("removes content after Outlook forward header", () => {
32
- const text = "Hello\nFrom: John Sent: Today To: Jane\nQuoted content";
33
- assert.strictEqual(removeQuotedContent(text), "Hello");
34
- });
35
-
36
- it("returns full text when no quote markers", () => {
37
- const text = "Hello\nWorld\nNo quotes here";
38
- assert.strictEqual(removeQuotedContent(text), text);
39
- });
40
- });
41
-
42
- describe("normalizeSubject", () => {
43
- describe("prefix removal", () => {
44
- it("removes Re: prefix", () => {
45
- const result = normalizeSubject("Re: Meeting tomorrow");
46
- assert.ok(!result.includes("re"));
47
- assert.ok(result.includes("meet"));
48
- });
49
-
50
- it("removes Fwd: prefix", () => {
51
- const result = normalizeSubject("Fwd: Important document");
52
- assert.ok(!result.includes("fwd"));
53
- });
54
-
55
- it("removes numbered Re: prefix", () => {
56
- const result = normalizeSubject("Re[3]: Meeting tomorrow");
57
- assert.ok(!result.includes("re"));
58
- });
59
-
60
- it("removes multiple prefixes", () => {
61
- const result = normalizeSubject("Re: Fwd: Re: Original subject");
62
- assert.ok(!result.includes("re"));
63
- assert.ok(!result.includes("fwd"));
64
- });
65
-
66
- it("removes German Aw: prefix", () => {
67
- const result = normalizeSubject("Aw: Besprechung morgen");
68
- assert.ok(!result.includes("aw"));
69
- });
70
-
71
- it("removes Portuguese Res: prefix", () => {
72
- const result = normalizeSubject("Res: Reunião amanhã");
73
- assert.ok(!result.includes("res"));
74
- });
75
- });
76
-
77
- describe("language detection", () => {
78
- it("auto-detects English and uses English stemming", () => {
79
- const result = normalizeSubject(
80
- "Meeting tomorrow about running the project quickly",
81
- );
82
- // Should stem "running" to "run", "quickly" to "quick"
83
- assert.ok(result.includes("run") || result.includes("quick"));
84
- });
85
-
86
- it("auto-detects German and uses German processing", () => {
87
- const result = normalizeSubject(
88
- "Besprechung über die Verbindung und den Projektstand morgen",
89
- );
90
- // Should process with German stemmer
91
- assert.ok(result.length > 0);
92
- });
93
-
94
- it("auto-detects French and uses French processing", () => {
95
- const result = normalizeSubject(
96
- "Réunion demain matin pour discuter du projet",
97
- );
98
- // Should preserve French accented characters and process
99
- assert.ok(result.includes("réunion") || result.includes("reunion"));
100
- });
101
-
102
- it("uses provided language override", () => {
103
- const result = normalizeSubject("Meeting tomorrow", "en");
104
- assert.ok(result.includes("meet"));
105
- });
106
- });
107
-
108
- describe("normalization", () => {
109
- it("removes stopwords", () => {
110
- const result = normalizeSubject("The meeting is about the project");
111
- // "the" and "is" should be removed
112
- assert.ok(!result.split(" ").includes("the"));
113
- });
114
-
115
- it("handles Unicode characters", () => {
116
- const result = normalizeSubject("Réunion à Paris");
117
- // Should not strip accented characters
118
- assert.ok(result.includes("réunion") || result.includes("reunion"));
119
- assert.ok(result.includes("pari"));
120
- });
121
-
122
- it("deduplicates consecutive words", () => {
123
- const result = normalizeSubject("Meeting meeting meeting update");
124
- // After stemming, should dedupe
125
- const words = result.split(" ");
126
- for (let i = 1; i < words.length; i++) {
127
- assert.notStrictEqual(
128
- words[i],
129
- words[i - 1],
130
- "Should not have consecutive duplicates",
131
- );
132
- }
133
- });
134
- });
135
- });
136
-
137
- describe("generateSnippet", () => {
138
- it("generates snippet from text", () => {
139
- const text = "Hello, this is a test message.";
140
- assert.strictEqual(generateSnippet(text), text);
141
- });
142
-
143
- it("removes quoted content", () => {
144
- const text = "Hello\n> quoted line";
145
- assert.strictEqual(generateSnippet(text), "Hello");
146
- });
147
-
148
- it("normalizes whitespace", () => {
149
- const text = "Hello world\n\nmultiple spaces";
150
- assert.strictEqual(generateSnippet(text), "Hello world multiple spaces");
151
- });
152
-
153
- it("truncates long text at word boundary", () => {
154
- const text = "The quick brown fox jumps over the lazy dog repeatedly";
155
- const result = generateSnippet(text, 30);
156
- assert.ok(result.length <= 30);
157
- assert.ok(result.endsWith("..."));
158
- });
159
-
160
- it("truncates single long word", () => {
161
- const text = "supercalifragilisticexpialidocious";
162
- const result = generateSnippet(text, 20);
163
- assert.ok(result.endsWith("..."));
164
- // When no word boundary found, truncates at maxLength-1 + "..."
165
- // so result is at most maxLength + 2 (for the extra chars in "...")
166
- assert.ok(result.length <= 22);
167
- });
168
- });
169
-
170
- describe("extractSnippetFromEmail", () => {
171
- it("prefers plain text over HTML", () => {
172
- const result = extractSnippetFromEmail("Plain text", "<p>HTML</p>");
173
- assert.strictEqual(result, "Plain text");
174
- });
175
-
176
- it("falls back to HTML when no plain text", () => {
177
- const result = extractSnippetFromEmail(undefined, "<p>HTML content</p>");
178
- assert.ok(result.includes("HTML content"));
179
- });
180
-
181
- it("strips HTML tags", () => {
182
- const result = extractSnippetFromEmail(
183
- undefined,
184
- "<p><strong>Bold</strong> text</p>",
185
- );
186
- assert.ok(!result.includes("<"));
187
- assert.ok(!result.includes(">"));
188
- assert.ok(result.includes("Bold"));
189
- assert.ok(result.includes("text"));
190
- });
191
-
192
- it("removes script and style tags", () => {
193
- const html = "<style>body{}</style><script>alert(1)</script><p>Content</p>";
194
- const result = extractSnippetFromEmail(undefined, html);
195
- assert.ok(!result.includes("body"));
196
- assert.ok(!result.includes("alert"));
197
- assert.ok(result.includes("Content"));
198
- });
199
-
200
- it("decodes HTML entities", () => {
201
- const html = "<p>&amp; &lt; &gt; &quot; &nbsp;</p>";
202
- const result = extractSnippetFromEmail(undefined, html);
203
- assert.ok(result.includes("&"));
204
- assert.ok(result.includes("<"));
205
- assert.ok(result.includes(">"));
206
- assert.ok(result.includes('"'));
207
- });
208
-
209
- it("returns empty string when no content", () => {
210
- const result = extractSnippetFromEmail(undefined, undefined);
211
- assert.strictEqual(result, "");
212
- });
213
- });
package/src/snippet.ts DELETED
@@ -1,152 +0,0 @@
1
- /**
2
- * Snippet generation utilities for email threading.
3
- *
4
- * Extracts preview text from email bodies by removing quoted content
5
- * and normalizing whitespace.
6
- */
7
-
8
- import {
9
- createTextNormalizer,
10
- type SupportedLanguage,
11
- } from "./text/normalizer.js";
12
-
13
- /**
14
- * Remove quoted reply content from email body.
15
- *
16
- * Stops at common quote indicators:
17
- * - Lines starting with ">"
18
- * - "On ... wrote:" patterns
19
- * - "--- Original Message ---" separators
20
- * - Outlook-style underline separators
21
- */
22
- export const removeQuotedContent = (text: string): string => {
23
- const lines = text.split("\n");
24
- const result: string[] = [];
25
-
26
- for (const line of lines) {
27
- // Stop at quote indicators
28
- if (/^>/.test(line)) break;
29
- if (/^On .+ wrote:$/i.test(line)) break;
30
- if (/^-{3,}\s*Original Message\s*-{3,}$/i.test(line)) break;
31
- if (/^_{3,}$/.test(line)) break; // Outlook separator
32
- if (/^From:.*Sent:.*To:/i.test(line)) break; // Outlook forward header
33
-
34
- result.push(line);
35
- }
36
-
37
- return result.join("\n");
38
- };
39
-
40
- const normalizer = createTextNormalizer();
41
-
42
- /**
43
- * Normalize subject for thread grouping and search.
44
- *
45
- * Processing steps:
46
- * 1. Remove Re:/Fwd:/etc prefixes (multi-language support)
47
- * 2. Auto-detect language or use provided language
48
- * 3. Normalize with stemming and stopword removal
49
- * 4. Deduplicate consecutive words
50
- *
51
- * Handles common prefixes across languages:
52
- * - Re, Fwd, Fw (English)
53
- * - Aw, Sv, Vs (German, Swedish, Danish)
54
- * - Ref, Rif (Italian, Spanish)
55
- * - Odp (Polish)
56
- * - Ynt (Turkish)
57
- * - Antw (Dutch)
58
- * - Res (Portuguese)
59
- *
60
- * @param subject - The email subject line
61
- * @param language - Optional language override (auto-detected if not provided)
62
- */
63
- export const normalizeSubject = (
64
- subject: string,
65
- language?: SupportedLanguage,
66
- ): string => {
67
- const SUBJECT_PREFIX_PATTERN =
68
- /^(\s*(Re|Fwd|Fw|Aw|Sv|Vs|Ref|Rif|Odp|Ynt|Antw|Res)(\[\d+\])?:\s*)+/i;
69
-
70
- // Step 1: Remove prefixes
71
- const withoutPrefix = subject.replace(SUBJECT_PREFIX_PATTERN, "").trim();
72
-
73
- // Step 2: Auto-detect language if not provided
74
- const detectedLanguage = language ?? normalizer.detectLanguage(withoutPrefix);
75
-
76
- // Step 3: Normalize with stemming and stopword removal
77
- return normalizer.normalize(withoutPrefix, {
78
- language: detectedLanguage,
79
- stem: true,
80
- removeStopwords: true,
81
- });
82
- };
83
-
84
- /**
85
- * Generate a snippet from email text content.
86
- *
87
- * @param text - The text content (plain text or stripped HTML)
88
- * @param maxLength - Maximum snippet length (default 256)
89
- * @returns Truncated snippet with ellipsis if needed
90
- */
91
- export const generateSnippet = (text: string, maxLength = 256): string => {
92
- // Remove quoted content
93
- const unquoted = removeQuotedContent(text);
94
-
95
- // Normalize whitespace: collapse multiple spaces/newlines to single space
96
- const normalized = unquoted.replace(/\s+/g, " ").trim();
97
-
98
- if (normalized.length <= maxLength) {
99
- return normalized;
100
- }
101
-
102
- // Truncate at word boundary
103
- const truncated = normalized.slice(0, maxLength);
104
- const lastSpace = truncated.lastIndexOf(" ");
105
-
106
- // If no space found, just truncate
107
- if (lastSpace === -1) {
108
- return `${truncated.slice(0, maxLength - 1)}...`;
109
- }
110
-
111
- // Truncate at word boundary and add ellipsis
112
- return `${truncated.slice(0, lastSpace)}...`;
113
- };
114
-
115
- /**
116
- * Extract text content from parsed email for snippet generation.
117
- *
118
- * Prefers plain text over HTML. If only HTML is available,
119
- * the caller should strip HTML tags before calling generateSnippet.
120
- *
121
- * @param textContent - Plain text content (may be undefined)
122
- * @param htmlContent - HTML content (may be undefined)
123
- * @param maxLength - Maximum snippet length
124
- * @returns Generated snippet or empty string
125
- */
126
- export const extractSnippetFromEmail = (
127
- textContent: string | undefined,
128
- htmlContent: string | undefined,
129
- maxLength = 256,
130
- ): string => {
131
- // Prefer plain text
132
- if (textContent) {
133
- return generateSnippet(textContent, maxLength);
134
- }
135
-
136
- // Fall back to HTML (caller should strip tags)
137
- if (htmlContent) {
138
- // Basic HTML tag stripping - for more robust handling, use html-to-text
139
- const stripped = htmlContent
140
- .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
141
- .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
142
- .replace(/<[^>]+>/g, " ")
143
- .replace(/&nbsp;/g, " ")
144
- .replace(/&amp;/g, "&")
145
- .replace(/&lt;/g, "<")
146
- .replace(/&gt;/g, ">")
147
- .replace(/&quot;/g, '"');
148
- return generateSnippet(stripped, maxLength);
149
- }
150
-
151
- return "";
152
- };