@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
@@ -0,0 +1,351 @@
1
+ import { type IAddressRepository, type ICalendarStore, type ICalendarSuggestionRepository, type IEnvelopeRepository, type IFilterRepository, type IMailboxSpecialUseRepository, type IMessageRepository, type IThreadMessageRepository } from "@remit/data-ports";
2
+ import { type ParsedBody, type StorageService } from "@remit/storage-service";
3
+ import { type ParsedMail } from "mailparser";
4
+ import { type FilterConfig } from "./filters/pipeline.js";
5
+ import type { FlagQueueService } from "./flag-queue.js";
6
+ import { type FolderPlacement } from "./heuristics/classifyPlacement.js";
7
+ import type { PlacementMoveService } from "./placement-move.js";
8
+ import { type QuarantineService } from "./quarantine.js";
9
+ import type { SyncWarningService } from "./sync-warning.js";
10
+ import { type IImapConnection } from "./types.js";
11
+ /**
12
+ * Whether the body-sync hot path defers per-part S3 objects. Each MIME leaf is
13
+ * its own PutObject, so during bulk sync these per-part writes dominate S3 write
14
+ * count and cost. With deferral ON a synced message writes only `body.eml` +
15
+ * `parsed.json.gz` (2 writes); the per-part objects are materialized lazily on
16
+ * the first `contentUrl` read via {@link BodySyncService.ensureBodyPartsStored}.
17
+ *
18
+ * Default ON (cost savings). Set `DEFER_BODY_PARTS=false`/`0`/`off` to restore
19
+ * the original eager per-part writes — a safe, reversible escape hatch.
20
+ */
21
+ export declare const isBodyPartDeferralEnabled: () => boolean;
22
+ /**
23
+ * The snippet the list row shows, from whichever body part carries text.
24
+ * Exported for the classification backfill (issue #1197), which denormalizes
25
+ * the same fields the body-store path does, from the same bytes.
26
+ */
27
+ export declare const extractSnippet: (parsed: ParsedMail) => string;
28
+ export declare const toParsedBody: (parsed: ParsedMail) => ParsedBody;
29
+ export interface BodySyncLogger {
30
+ info(obj: Record<string, unknown>, msg: string): void;
31
+ debug?(obj: Record<string, unknown>, msg: string): void;
32
+ warn?(obj: Record<string, unknown>, msg: string): void;
33
+ error?(obj: Record<string, unknown>, msg: string): void;
34
+ }
35
+ export interface SyncBodiesResult {
36
+ syncedCount: number;
37
+ syncedMessageIds: string[];
38
+ skippedCount: number;
39
+ failedCount: number;
40
+ failedMessageIds: string[];
41
+ }
42
+ export interface FetchBodyResult {
43
+ text: string | null;
44
+ html: string | null;
45
+ storedAt: string;
46
+ }
47
+ export interface StoreBodyPartContentsResult {
48
+ stored: number;
49
+ }
50
+ export type ConnectionGetter = () => Promise<IImapConnection>;
51
+ export interface BodySyncFolder {
52
+ mailboxId: string;
53
+ fullPath: string;
54
+ placement: FolderPlacement;
55
+ }
56
+ export interface BodySyncRoundFolder extends BodySyncFolder {
57
+ uidValidity: number;
58
+ }
59
+ export interface PlacementConfig {
60
+ mailboxSpecialUseService: IMailboxSpecialUseRepository;
61
+ /**
62
+ * Local-first mover for a confident placement verdict (issue #1271). Same
63
+ * `moveMessage(accountConfigId, messageId, destinationMailboxId, accountId)`
64
+ * signature as the general-purpose `MessageMoveService` it replaced here,
65
+ * but backed by a pending-move marker + its own reconciler queue instead of
66
+ * the general MESSAGE_MOVE event (see `placement-move.ts`).
67
+ */
68
+ placementMoveService: PlacementMoveService;
69
+ }
70
+ /**
71
+ * What body sync needs to set a message aside (issue #72). The mailbox facts
72
+ * are supplied by the caller because they are properties of the round, not of
73
+ * any one message.
74
+ */
75
+ export interface QuarantineConfig {
76
+ quarantineService: QuarantineService;
77
+ mailboxId: string;
78
+ /**
79
+ * The mailbox's stored UIDVALIDITY. Taken from the mailbox row rather than
80
+ * from the open box because a round whose two disagree is paused by the
81
+ * cursor guard before it reaches any UID.
82
+ */
83
+ uidValidity: number;
84
+ /** Redeliveries of this batch so far, so the record says how hard we tried. */
85
+ attempts: number;
86
+ }
87
+ /**
88
+ * What body sync needs to auto-mark-read a message from an unsubscribed
89
+ * sender (issue #302, RFC 039 Decision 3). Reuses the same
90
+ * `FlagQueueService.markAsRead` a manual mark-as-read already goes through —
91
+ * local `\Seen` + `ThreadMessage.isRead` + a durable pending IMAP flag-push
92
+ * marker — so this fires the same round-trip, not a second primitive.
93
+ */
94
+ export interface UnsubscribeConfig {
95
+ flagQueueService: FlagQueueService;
96
+ }
97
+ /**
98
+ * What body sync needs to offer a mail's invitation as a card (issue #1033).
99
+ *
100
+ * The calendar store is here to provision the account's default collection on
101
+ * first use, through the one function every other caller provisions with. That
102
+ * is not bookkeeping: an invitation whose DTSTART names no zone is RFC 5545
103
+ * floating time, and reading it anywhere but where the user's calendar lives
104
+ * shows the meeting at the wrong hour. Without a collection there is no zone
105
+ * to read it in, and every such event silently lands in UTC.
106
+ *
107
+ * `filterService` is read to honour `dismiss{muteSender:true}`: a sender the
108
+ * user muted gets no card at all.
109
+ */
110
+ export interface CalendarSuggestionConfig {
111
+ calendarSuggestionService: ICalendarSuggestionRepository;
112
+ calendarStore: ICalendarStore;
113
+ filterService: Pick<IFilterRepository, "listByAccountAndState">;
114
+ }
115
+ export declare class BodySyncService {
116
+ private messageService;
117
+ private storageService;
118
+ private threadMessageService;
119
+ private addressService;
120
+ private envelopeService;
121
+ private readonly syncWarningService;
122
+ private readonly placementConfig?;
123
+ private readonly filterConfig?;
124
+ private readonly quarantineConfig?;
125
+ private readonly unsubscribeConfig?;
126
+ private readonly calendarConfig?;
127
+ private log;
128
+ private readonly filterPipeline?;
129
+ constructor(messageService: IMessageRepository, storageService: StorageService, threadMessageService: IThreadMessageRepository, addressService: IAddressRepository, envelopeService: IEnvelopeRepository, syncWarningService: SyncWarningService, logger?: BodySyncLogger, placementConfig?: PlacementConfig | undefined, filterConfig?: FilterConfig | undefined, quarantineConfig?: QuarantineConfig | undefined, unsubscribeConfig?: UnsubscribeConfig | undefined, calendarConfig?: CalendarSuggestionConfig | undefined);
130
+ syncBodies(messageIds: string[], accountId: string, accountConfigId: string, folder: BodySyncRoundFolder, getConnection: ConnectionGetter, force?: boolean): Promise<SyncBodiesResult>;
131
+ /**
132
+ * Set a message aside because its body will not parse (issue #72). The
133
+ * fingerprint comes from the BodyPart rows metadata sync already wrote: the
134
+ * body path streams raw bytes and has no FETCH result to read.
135
+ */
136
+ private recordQuarantine;
137
+ private buildResult;
138
+ /**
139
+ * Stream one message body straight to storage while teeing the bytes into a
140
+ * buffer for the parse-dependent steps (snippet, classification, parsed-body
141
+ * cache, per-part objects). The S3 upload never sees a whole-body concat —
142
+ * the storage service streams it — but mailparser still needs the full bytes,
143
+ * so we collect them in parallel. Later issues move parsing off the hot path.
144
+ *
145
+ * `isReStore` marks a re-fetch of a message whose row already carried
146
+ * `bodyStorageKey` (the `force` re-arm in {@link syncBodies}): the
147
+ * post-store steps then re-materialize the body artifacts without
148
+ * re-running any decision (issue #1011).
149
+ */
150
+ private storeStreamedBody;
151
+ /**
152
+ * The steps every synced body must go through regardless of which path
153
+ * fetched it first — the sync path ({@link storeStreamedBody}) and the
154
+ * read-path backfill ({@link fetchAndGetBody}) both call this instead of
155
+ * duplicating (and drifting on) their own subset. Before issue #1271,
156
+ * `fetchAndGetBody` skipped classification/placement entirely — a message
157
+ * materialized by a read got different treatment than one synced in bulk.
158
+ *
159
+ * Order matters for two independent reasons, both load-bearing:
160
+ * 1. The parsed-body cache must be durable before `bodyStorageKey` (the
161
+ * skip-guard signal) — see the comment on the write below.
162
+ * 2. A placement move — local mailboxId change, pending marker, IMAP-push
163
+ * enqueue (`PlacementMoveService.moveMessage`, issue #1271) — must
164
+ * complete, or fully fail, BEFORE `bodyStorageKey` is written. That
165
+ * failure is never swallowed: if it throws, `bodyStorageKey` is not yet
166
+ * durable, so a retry reprocesses the message from scratch (marker
167
+ * included) instead of the skip-guard stranding a `movedByRemit` flag
168
+ * with no marker behind it — the defect this issue fixes.
169
+ *
170
+ * Returns the parsed mail so callers that already need it (both do) don't
171
+ * pay for mailparser twice.
172
+ */
173
+ private applyPostStoreSteps;
174
+ /**
175
+ * Fetch a single message body, store it, and return the parsed content.
176
+ *
177
+ * If the body is already stored, retrieves it from storage.
178
+ * Otherwise fetches from IMAP, stores it, and returns the parsed content.
179
+ *
180
+ * @param messageId - The message ID to fetch
181
+ * @param accountId - The account ID (for storage path)
182
+ * @param accountConfigId - The account config ID (for thread updates)
183
+ * @param folder - The IMAP mailbox path and its folder placement
184
+ * @param getConnection - Function to get a (lazy) IMAP connection
185
+ * @returns Parsed text and HTML content
186
+ */
187
+ fetchAndGetBody(messageId: string, accountId: string, accountConfigId: string, folder: BodySyncFolder, getConnection: ConnectionGetter): Promise<FetchBodyResult>;
188
+ private fetchFromImap;
189
+ /**
190
+ * The server no longer holds the message at the row's UID. A row with our
191
+ * own move or delete in flight belongs to that mutation (409). A settled row
192
+ * is removed with its thread entries, but only while its placement still
193
+ * matches what was read (imap-mutations R3); losing that predicate is a 409
194
+ * like any other lost placement transition.
195
+ */
196
+ private reconcileGoneMessage;
197
+ /**
198
+ * Header classification, with the sender's `Address.flags.category`
199
+ * override (issue #299, RFC 039 Decision 3) substituted for the
200
+ * header-derived category when one is set. Shared with the classification
201
+ * backfill (issue #1197) as {@link classifyParsedMessage}, so the one-time
202
+ * pass and the body-store path can never classify the same bytes
203
+ * differently.
204
+ */
205
+ private classifyMessage;
206
+ private incrementInboundCount;
207
+ /**
208
+ * The per-sender signals {@link computePlacement} needs, from ONE `Address`
209
+ * fetch (RFC 039 Decision 3/3a, issue #300): the trust reads exactly as
210
+ * `deriveSenderTrust` always did — `vip → wellknown → unknown`, untouched by
211
+ * `blocked` — plus `blocked`/`autoArchive` off the same row. `trustSetAt` is
212
+ * the `setAt` of whichever flag produced the trust value, needed by
213
+ * {@link resolveSenderPlacement}'s tie-break; it stays local to placement and
214
+ * never reaches `deriveSenderTrust`'s own contract (the trust badge).
215
+ */
216
+ private deriveSenderPlacementSignals;
217
+ /**
218
+ * `flags.unsubscribed` (issue #302, RFC 039 Decision 3): "auto-mark-read
219
+ * until sender stops" — fires on every new message from that sender for as
220
+ * long as the flag stays set, per the flag's own doc comment; there is no
221
+ * separate expiry mechanism, and no caching of the decision beyond this
222
+ * per-message `Address` read. A no-op when body sync was built without an
223
+ * {@link UnsubscribeConfig} or the message carries no `From` address.
224
+ *
225
+ * Write-once per message (issue #499), like the `category` and placement
226
+ * derivations alongside it: read state is applied on the pass that first
227
+ * classifies a message and never re-decided. A user who marks such a message
228
+ * unread afterwards owns it — a re-entrant pass over an already-classified
229
+ * body would otherwise silently undo that.
230
+ */
231
+ private applyUnsubscribedAutoRead;
232
+ /**
233
+ * Offer what a message's `text/calendar` parts propose, as cards beside the
234
+ * message (issue #1033). Nothing here reaches a calendar: a suggestion is
235
+ * written `Pending` and waits for a person, and a `METHOD:CANCEL` is a card
236
+ * of its own rather than a withdrawal of an event the user added.
237
+ *
238
+ * Write-once per message, like the `category`, placement and read-state
239
+ * derivations alongside it (issues #499, #1011): a card is offered on the
240
+ * pass that first classifies a message — the same moment the account's
241
+ * filters run — and never again. The two re-entrant paths a forced body
242
+ * re-sync goes through (`fetchAndGetBody`'s `NoSuchKey` fallback,
243
+ * `syncBodies(..., force: true)`) reach this with `bodyStorageKey` already
244
+ * set, and re-offering there would resurrect a card the user dismissed.
245
+ *
246
+ * Isolated the same way placement and filters are: a card is auxiliary to
247
+ * storing the mail, which is already durable by the time this runs. A
248
+ * repository failure here is logged loudly with the messageId rather than
249
+ * failing a body store that otherwise succeeded.
250
+ */
251
+ private deriveCalendarSuggestions;
252
+ private deriveSenderUnsubscribed;
253
+ /**
254
+ * Evaluate the account's active filters against a synced message (RFC 034),
255
+ * BEFORE the single Message update — so a filter's `movedByRemit` flag joins
256
+ * that same UpdateItem. Returns the actions to apply (labels + at most one
257
+ * move); the caller applies them. A no-op returning no actions when no
258
+ * {@link FilterConfig} is wired.
259
+ *
260
+ * Isolation lives in {@link FilterPipeline.evaluate}: this read/decision phase
261
+ * never fails the body store (the #1246 placement precedent). The applies of
262
+ * the returned decision are the caller's and are deliberately unswallowed.
263
+ */
264
+ private evaluateFilters;
265
+ /**
266
+ * Resolve the placement decision for a message, BEFORE the single Message
267
+ * update — so both the `movedByRemit` flag and the audit verdict join that
268
+ * one UpdateItem instead of a second mutation. Drives both directions (junk → inbox rescue,
269
+ * inbox → junk demote) off the pure {@link classifyPlacement} verdict.
270
+ *
271
+ * Returns a {@link PlacementOutcome}: a `verdict` to persist whenever Remit
272
+ * decided to act (action != leave), confident and unsure alike, so the
273
+ * distribution is queryable on the message; a `move` to enqueue only for a
274
+ * confident verdict; and `placementDecidedAt` whenever a verdict — including
275
+ * "leave" — was genuinely computed (issue #383), so a re-entrant call short-
276
+ * circuits instead of re-deciding a placement the user may have since
277
+ * overridden by hand.
278
+ *
279
+ * Always logs a structured verdict line for confident, actionable verdicts so
280
+ * the real distribution is observable on a live mailbox.
281
+ *
282
+ * The verdict reads the just-computed classification rather than a persisted
283
+ * row, since those fields are not written until the single update. Placement
284
+ * is auxiliary to the body store — the primary artifact (body.eml) is already
285
+ * durable by the time this runs — so a failure here (e.g. a placement-repo
286
+ * query erroring on a schema-drifted DB) is caught and logged loudly with an
287
+ * alertable field instead of failing the surrounding message store. The empty
288
+ * {@link PlacementOutcome} means "no action", whether Remit genuinely decided
289
+ * to leave the message alone or placement itself failed; the alert log is
290
+ * what distinguishes the latter, and neither case marks the message decided.
291
+ */
292
+ private resolvePlacement;
293
+ private computePlacement;
294
+ /**
295
+ * `flags.autoArchive` (issue #300, RFC 039 Decision 3): file a message
296
+ * straight to Archive, skipping Inbox. Only reached from
297
+ * {@link computePlacement} when {@link classifyPlacement} had no confident
298
+ * junk/inbox verdict of its own — `blocked`/DKIM/DMARC always take priority
299
+ * over this filing preference — and only for the `leave` verdicts
300
+ * {@link autoArchiveApplies} admits.
301
+ *
302
+ * No {@link MessagePlacementVerdict} is recorded: `PlacementAction` (the
303
+ * audit enum) has only `MoveToInbox`/`MoveToJunk` — issue #300 is scoped to
304
+ * no TypeSpec change, so an archive move carries no audit verdict, same as
305
+ * a matched filter's move. The move itself reuses the same
306
+ * `placementMoveService.moveMessage` path as every other confident move.
307
+ *
308
+ * Idempotent the same way {@link classifyPlacement}'s own branches are: a
309
+ * message already sitting in Archive is left alone, not moved again.
310
+ */
311
+ private resolveAutoArchive;
312
+ /**
313
+ * Persist one S3 object per non-multipart leaf so the SPA can resolve
314
+ * `BodyPartResponse.contentUrl` (#298). Keys follow the layout
315
+ * `accounts/{accountConfigId}/{accountId}/messages/{messageId}/parts/{partPath}`
316
+ * so they line up with the URL shape `derive/contentUrl.ts` emits.
317
+ *
318
+ * The mapper is total (#395 PR B): every leaf gets a `BodyPartContentPair`,
319
+ * possibly with a zero-byte content for leaves that have no source bytes
320
+ * (genuinely empty parts, or pathological inputs the positional fallback
321
+ * couldn't pair). No try/catch is needed; the only failure surface here is
322
+ * an S3 write itself, which `pMap` surfaces directly.
323
+ *
324
+ * If `listBodyParts` returns an empty list (e.g. a legacy message synced
325
+ * before #133 populated BodyPart rows), this is a no-op.
326
+ */
327
+ private storeBodyPartContents;
328
+ /**
329
+ * Lazily materialize the per-part S3 objects for an already-synced message
330
+ * whose parts were deferred during bulk sync (DEFER_BODY_PARTS).
331
+ *
332
+ * Called from the read path (the API `describeMessage` handler) before the
333
+ * SPA fetches any `contentUrl`: the part bytes are served directly from S3 by
334
+ * CloudFront with no Lambda in the request path, so a missing object would
335
+ * surface to the SPA as a hard "body-missing" failure. Generating the parts
336
+ * here — re-parsing the stored `body.eml` with the same `mapBodyPartsToContent`
337
+ * logic body-sync uses — guarantees every `contentUrl` resolves, while bulk
338
+ * sync stays at 2 writes/message.
339
+ *
340
+ * Idempotent: skips any leaf already on S3, so repeat reads cost only HEAD
341
+ * checks, and a re-read after a partial failure fills the gaps. Requires the
342
+ * message body to be stored (`bodyStorageKey`); callers gate on that.
343
+ *
344
+ * Warm-open fast path: a single HEAD on the `.materialized` sentinel
345
+ * short-circuits the whole pass — no body.eml GET, no MIME re-parse, no
346
+ * per-leaf HEADs. The sentinel is written only after every leaf is confirmed
347
+ * stored, so its presence guarantees all `contentUrl`s resolve.
348
+ */
349
+ ensureBodyPartsStored(accountConfigId: string, accountId: string, messageId: string, bodyStorageKey: string): Promise<StoreBodyPartContentsResult>;
350
+ private storeParsedBodyCache;
351
+ }