@remit/imap-worker 0.0.89 → 0.0.91

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 (171) hide show
  1. package/dist/account-check.d.ts +27 -0
  2. package/dist/account-check.js +70 -0
  3. package/dist/body-sync-gate.d.ts +12 -0
  4. package/dist/body-sync-gate.js +53 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +193 -0
  7. package/dist/config-env.d.ts +5 -0
  8. package/dist/config-env.js +37 -0
  9. package/dist/connection-pool.d.ts +22 -0
  10. package/dist/connection-pool.js +97 -0
  11. package/dist/connection-scope.d.ts +8 -0
  12. package/dist/connection-scope.js +10 -0
  13. package/dist/context.d.ts +46 -0
  14. package/dist/context.js +15 -0
  15. package/dist/e2e-processor-shim.d.ts +2 -0
  16. package/dist/e2e-processor-shim.js +181 -0
  17. package/dist/emit.d.ts +15 -0
  18. package/dist/emit.js +76 -0
  19. package/dist/events.d.ts +203 -0
  20. package/dist/events.js +1 -0
  21. package/dist/failure-kind.d.ts +10 -0
  22. package/{src/failure-kind.ts → dist/failure-kind.js} +6 -6
  23. package/dist/handlers/append-sent-message.d.ts +3 -0
  24. package/dist/handlers/append-sent-message.js +152 -0
  25. package/dist/handlers/delete-account-objects.d.ts +3 -0
  26. package/dist/handlers/delete-account-objects.js +35 -0
  27. package/dist/handlers/empty-trash.d.ts +14 -0
  28. package/dist/handlers/empty-trash.js +217 -0
  29. package/dist/handlers/flag-push.d.ts +38 -0
  30. package/dist/handlers/flag-push.js +195 -0
  31. package/dist/handlers/imported-folder.d.ts +4 -0
  32. package/dist/handlers/imported-folder.js +23 -0
  33. package/dist/handlers/mailbox-management.d.ts +6 -0
  34. package/dist/handlers/mailbox-management.js +294 -0
  35. package/dist/handlers/mailbox-sync-order.d.ts +21 -0
  36. package/dist/handlers/mailbox-sync-order.js +50 -0
  37. package/dist/handlers/message-copy.d.ts +40 -0
  38. package/dist/handlers/message-copy.js +371 -0
  39. package/dist/handlers/message-delete-terminal.d.ts +60 -0
  40. package/dist/handlers/message-delete-terminal.js +77 -0
  41. package/dist/handlers/message-delete.d.ts +57 -0
  42. package/dist/handlers/message-delete.js +520 -0
  43. package/{src/handlers/message-move-terminal.ts → dist/handlers/message-move-terminal.d.ts} +18 -105
  44. package/dist/handlers/message-move-terminal.js +94 -0
  45. package/dist/handlers/message-move.d.ts +146 -0
  46. package/dist/handlers/message-move.js +490 -0
  47. package/dist/handlers/placement-move-push.d.ts +62 -0
  48. package/dist/handlers/placement-move-push.js +307 -0
  49. package/dist/handlers/probe-message-id.d.ts +2 -0
  50. package/dist/handlers/probe-message-id.js +6 -0
  51. package/dist/handlers/sync-mailboxes.d.ts +45 -0
  52. package/dist/handlers/sync-mailboxes.js +233 -0
  53. package/dist/handlers/sync-message-body.d.ts +23 -0
  54. package/dist/handlers/sync-message-body.js +182 -0
  55. package/dist/handlers/sync-messages.d.ts +29 -0
  56. package/dist/handlers/sync-messages.js +285 -0
  57. package/dist/index.d.ts +9 -0
  58. package/dist/index.js +67 -0
  59. package/dist/lambda-context.d.ts +8 -0
  60. package/dist/lambda-context.js +33 -0
  61. package/dist/mailbox-row.d.ts +9 -0
  62. package/dist/mailbox-row.js +6 -0
  63. package/dist/poller.d.ts +1 -0
  64. package/dist/poller.js +51 -0
  65. package/dist/process.d.ts +6 -0
  66. package/dist/process.js +3 -0
  67. package/dist/processor.d.ts +3 -0
  68. package/dist/processor.js +44 -0
  69. package/dist/queues.d.ts +10 -0
  70. package/dist/queues.js +1 -0
  71. package/dist/s3-account-objects.d.ts +3 -0
  72. package/dist/s3-account-objects.js +26 -0
  73. package/dist/scheduler/attachment-sweep.d.ts +11 -0
  74. package/dist/scheduler/attachment-sweep.js +23 -0
  75. package/dist/scheduler/calendar-subscriptions.d.ts +26 -0
  76. package/dist/scheduler/calendar-subscriptions.js +35 -0
  77. package/dist/scheduler/config.d.ts +27 -0
  78. package/{src/scheduler/config.ts → dist/scheduler/config.js} +7 -26
  79. package/{src/scheduler/decide-due.ts → dist/scheduler/decide-due.d.ts} +3 -12
  80. package/dist/scheduler/decide-due.js +17 -0
  81. package/dist/scheduler/handler.d.ts +21 -0
  82. package/{src/scheduler/handler.ts → dist/scheduler/handler.js} +24 -36
  83. package/{src/scheduler/loop.ts → dist/scheduler/loop.d.ts} +7 -23
  84. package/dist/scheduler/loop.js +8 -0
  85. package/dist/scheduler/run-tick.d.ts +62 -0
  86. package/dist/scheduler/run-tick.js +164 -0
  87. package/dist/scheduler/runner.d.ts +2 -0
  88. package/{src/scheduler/runner.ts → dist/scheduler/runner.js} +58 -84
  89. package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
  90. package/dist/scheduler/stranded-placement-redrive.js +172 -0
  91. package/dist/sqs-queues.d.ts +4 -0
  92. package/dist/sqs-queues.js +22 -0
  93. package/dist/with-oauth-lifecycle-deps.d.ts +5 -0
  94. package/dist/with-oauth-lifecycle-deps.js +17 -0
  95. package/dist/with-oauth-lifecycle.d.ts +56 -0
  96. package/dist/with-oauth-lifecycle.js +92 -0
  97. package/{src/worker-version.ts → dist/worker-version.d.ts} +1 -2
  98. package/dist/worker-version.js +12 -0
  99. package/package.json +75 -69
  100. package/src/account-check.test.ts +0 -149
  101. package/src/account-check.ts +0 -101
  102. package/src/body-sync-gate.test.ts +0 -185
  103. package/src/body-sync-gate.ts +0 -86
  104. package/src/cli.ts +0 -224
  105. package/src/connection-scope.test.ts +0 -266
  106. package/src/connection-scope.ts +0 -314
  107. package/src/credential-less-sync-fence.test.ts +0 -152
  108. package/src/e2e-processor-shim.ts +0 -232
  109. package/src/emit.test.ts +0 -127
  110. package/src/emit.ts +0 -117
  111. package/src/events.ts +0 -252
  112. package/src/failure-kind.test.ts +0 -42
  113. package/src/handlers/append-sent-message.sqlite.test.ts +0 -176
  114. package/src/handlers/append-sent-message.ts +0 -264
  115. package/src/handlers/delete-account-objects.test.ts +0 -128
  116. package/src/handlers/delete-account-objects.ts +0 -94
  117. package/src/handlers/empty-trash.sqlite.test.ts +0 -209
  118. package/src/handlers/empty-trash.ts +0 -352
  119. package/src/handlers/flag-push.ts +0 -285
  120. package/src/handlers/imported-folder.test.ts +0 -63
  121. package/src/handlers/imported-folder.ts +0 -38
  122. package/src/handlers/mailbox-management.sqlite.test.ts +0 -265
  123. package/src/handlers/mailbox-management.ts +0 -486
  124. package/src/handlers/mailbox-sync-order.test.ts +0 -93
  125. package/src/handlers/mailbox-sync-order.ts +0 -65
  126. package/src/handlers/message-copy.sqlite.test.ts +0 -298
  127. package/src/handlers/message-copy.ts +0 -566
  128. package/src/handlers/message-delete-terminal.ts +0 -147
  129. package/src/handlers/message-delete.sqlite.test.ts +0 -291
  130. package/src/handlers/message-delete.ts +0 -885
  131. package/src/handlers/message-move.sqlite.test.ts +0 -271
  132. package/src/handlers/message-move.test.ts +0 -216
  133. package/src/handlers/message-move.ts +0 -752
  134. package/src/handlers/placement-move-push.test.ts +0 -137
  135. package/src/handlers/placement-move-push.ts +0 -475
  136. package/src/handlers/placement-push.sqlite.test.ts +0 -298
  137. package/src/handlers/probe-message-id.sqlite.test.ts +0 -192
  138. package/src/handlers/probe-message-id.ts +0 -15
  139. package/src/handlers/sync-mailboxes.test.ts +0 -183
  140. package/src/handlers/sync-mailboxes.ts +0 -421
  141. package/src/handlers/sync-message-body.sqlite.test.ts +0 -314
  142. package/src/handlers/sync-message-body.test.ts +0 -150
  143. package/src/handlers/sync-message-body.ts +0 -365
  144. package/src/handlers/sync-messages.sqlite.test.ts +0 -183
  145. package/src/handlers/sync-messages.ts +0 -525
  146. package/src/index.test.ts +0 -22
  147. package/src/index.ts +0 -86
  148. package/src/mail-sync-selection.sqlite.test.ts +0 -234
  149. package/src/mailbox-row.ts +0 -20
  150. package/src/pending-sync.sqlite.test.ts +0 -166
  151. package/src/poller.ts +0 -55
  152. package/src/processor.ts +0 -60
  153. package/src/queues.ts +0 -49
  154. package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -75
  155. package/src/scheduler/attachment-sweep.ts +0 -46
  156. package/src/scheduler/calendar-subscriptions.sqlite.test.ts +0 -147
  157. package/src/scheduler/calendar-subscriptions.ts +0 -103
  158. package/src/scheduler/config.test.ts +0 -40
  159. package/src/scheduler/decide-due.test.ts +0 -44
  160. package/src/scheduler/half-deleted-accounts.sqlite.test.ts +0 -196
  161. package/src/scheduler/loop.test.ts +0 -143
  162. package/src/scheduler/run-tick.sqlite.test.ts +0 -191
  163. package/src/scheduler/run-tick.ts +0 -281
  164. package/src/scheduler/stranded-placement-redrive.ts +0 -265
  165. package/src/stranded-placement.sqlite.test.ts +0 -794
  166. package/src/test-helpers/test-world.ts +0 -427
  167. package/src/thread-join.sqlite.test.ts +0 -106
  168. package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
  169. package/src/with-oauth-lifecycle-deps.ts +0 -71
  170. package/src/with-oauth-lifecycle.ts +0 -158
  171. package/tsconfig.json +0 -8
@@ -0,0 +1,94 @@
1
+ import { MessageMutation, MessageSyncStatus } from "@remit/domain-enums";
2
+ import { isMessageGoneFromOpenMailbox, restoreSourcePlacement, } from "@remit/mailbox-service";
3
+ /**
4
+ * Resolve a MESSAGE_MOVE failure that has exhausted the message queue's
5
+ * redelivery budget into exactly one of two terminal outcomes, mirroring
6
+ * `resolveExhaustedPlacementMoveFailure` and `resolveExhaustedFlagPushFailure`
7
+ * for the same failure taxonomy (issue #655) — no third, softer outcome.
8
+ *
9
+ * The move's pending state is the Message row itself (`status: moving`,
10
+ * `originalMailboxId`/`originalUid`, `mailboxId` already pointing at the
11
+ * destination), not a separate marker, so the outcomes act on that row.
12
+ *
13
+ * 1. RECONCILED (expected) — the message no longer exists at the move's source
14
+ * on IMAP, confirmed by {@link isMessageGoneFromOpenMailbox} rather than by
15
+ * a FETCH coming back empty. Either the MOVE did execute server-side and
16
+ * the connection dropped before the tagged OK was read, or a foreign client
17
+ * moved or expunged the message; from here those are indistinguishable and
18
+ * have the same answer. The stale Message/ThreadMessage rows are deleted
19
+ * via `deleteIfPlacement` and the caller resyncs both folders, so
20
+ * whichever folder actually holds the message re-projects it with the
21
+ * server's own UID. Metric only, no alarm — routine.
22
+ * 2. BROKEN — the message is still at the source, so the move never took
23
+ * effect, but it keeps failing: broken code or a broken account, not a
24
+ * transient blip. The row is put back where the server has just said the
25
+ * message is, as `resolveExhaustedMessageDeleteFailure` does (#1098). Logged
26
+ * with an `alert`-shaped entry for an operator alarm; never re-thrown
27
+ * (terminal — the caller acks either way, since retrying a
28
+ * permanently-broken move can never succeed).
29
+ *
30
+ * Leaving the row `moving` is what this replaces. BROKEN is terminal, so
31
+ * nothing was ever going to come back and settle it, and only `updateUid`
32
+ * clears `moving` — the row kept a `mailboxId` naming the destination and a
33
+ * `uid` belonging to the source, which `bindsForeignUid` refuses to act on,
34
+ * leaving the message undeletable and unmovable for good (issue #1005).
35
+ * Restoring the pair is not the revert-on-ambiguity PR #652 was pulled for:
36
+ * the presence probe above has just answered, so this writes a placement the
37
+ * server confirmed, and a MOVE that lands after all is re-projected by the
38
+ * resync the caller runs.
39
+ *
40
+ * A server that cannot be reached at exhaustion time never reaches either
41
+ * verdict: the probe throws and the record dead-letters with the row untouched.
42
+ * Absence is only ever concluded from an answer the server gave.
43
+ *
44
+ * An operator reading `message_move_failed` should know one case where the
45
+ * message is not actually at the source: a message another client expunged
46
+ * mid-session can answer an empty FETCH while the server still lists its UID
47
+ * in SEARCH, until it is allowed to send the untagged EXPUNGE. That message
48
+ * lands in BROKEN, and BROKEN is terminal — the row is bound to a source the
49
+ * message has already left and the alert stands until someone clears it. The
50
+ * reverse mistake discards the row for live mail, so the cost is paid
51
+ * deliberately.
52
+ */
53
+ export const resolveExhaustedMessageMoveFailure = async (deps, input) => {
54
+ const { accountId, accountConfigId, messageId, sourceMailboxId, uid, sourceMailboxPath, getConnection, } = input;
55
+ const connection = await getConnection();
56
+ await connection.openBox(sourceMailboxPath, true);
57
+ if (await isMessageGoneFromOpenMailbox(connection, uid)) {
58
+ const reconciled = await deps.messageService.deleteIfPlacement(messageId, input.placement);
59
+ deps.log.info({
60
+ metric: "message_move_stale_row_reconciled",
61
+ accountId,
62
+ accountConfigId,
63
+ messageId,
64
+ uid,
65
+ sourceMailboxPath,
66
+ reconciled,
67
+ }, "Message no longer at its move source after retry exhaustion (move landed server-side, or an external delete or move); stale row reconciled");
68
+ return { outcome: "reconciled" };
69
+ }
70
+ deps.log.error({
71
+ alert: "message_move_failed",
72
+ accountId,
73
+ accountConfigId,
74
+ messageId,
75
+ uid,
76
+ sourceMailboxId,
77
+ sourceMailboxPath,
78
+ }, "Message move could not be pushed to IMAP after retry exhaustion; the message is still at its source — row moved back to the source folder");
79
+ await restoreSourcePlacement(deps, {
80
+ accountConfigId,
81
+ messageId,
82
+ sourceMailboxId,
83
+ uid,
84
+ // The probe above just confirmed the pair, so the row is a faithful
85
+ // projection of the source again — and the move the user asked for did
86
+ // not happen, with nothing left to try it. `abandoned` is the value that
87
+ // says so without claiming a retry is coming (imap-mutations R3); the
88
+ // alert stays for the operator, and the mutation names itself so the
89
+ // reading pane offers the right way out (issue #1229).
90
+ syncStatus: MessageSyncStatus.abandoned,
91
+ abandonedMutation: MessageMutation.move,
92
+ });
93
+ return { outcome: "broken" };
94
+ };
@@ -0,0 +1,146 @@
1
+ import type { ThreadMessageItem } from "@remit/data-ports";
2
+ import { type IImapConnection } from "@remit/mailbox-service";
3
+ import type { ProcessContext } from "../context.js";
4
+ import type { MessageMoveEvent, SyncMessagesEvent } from "../events.js";
5
+ type EmitSyncMessages = (event: Omit<SyncMessagesEvent, "eventId" | "timestamp">) => Promise<unknown>;
6
+ /**
7
+ * Re-read both folders' counts from IMAP after a move by enqueuing the existing
8
+ * per-folder SYNC_MESSAGES sync. Counts are a projection of IMAP, never mutated
9
+ * locally — the move shifted a message between source and destination, so both
10
+ * folders must refresh through the one-way pipeline.
11
+ */
12
+ export declare const emitMoveResync: (emit: EmitSyncMessages, params: {
13
+ accountId: string;
14
+ sourceMailboxId: string;
15
+ destinationMailboxId: string;
16
+ }) => Promise<void>;
17
+ /**
18
+ * The one-folder half of {@link emitMoveResync}, for a mutation that has no
19
+ * destination — a permanent delete removed local rows the server still holds,
20
+ * and only a resync of that folder puts them back.
21
+ */
22
+ export declare const emitMailboxResync: (emit: EmitSyncMessages, params: {
23
+ accountId: string;
24
+ mailboxId: string;
25
+ }) => Promise<void>;
26
+ /**
27
+ * SEARCH a mailbox for copies of a message identified by its RFC822 Message-ID
28
+ * header and answer the HIGHEST matching uid. Read-only (EXAMINE, not SELECT) —
29
+ * this is a verification probe, never a write. `null` when nothing matched.
30
+ *
31
+ * The highest uid is the freshly delivered copy only when one exists; deciding
32
+ * that it does is the caller's job, not this function's. Every probe in this
33
+ * package wants the highest: uids ascend with arrival inside a UIDVALIDITY, so a
34
+ * copy that has just landed outranks every copy of the same Message-ID the
35
+ * folder already held. Where nothing was delivered, the highest is simply the
36
+ * newest pre-existing copy, and a caller that has not ruled that case out binds
37
+ * itself to unrelated mail.
38
+ * One Message-ID can have several server copies in one account (a sieve
39
+ * `fileinto` + `keep`, a resend, a repeated COPY — `deriveCopyMessageId` reuses
40
+ * the row, so the second copy is a second server COPY), while `deriveMessageId`
41
+ * is folder-independent and gives them one local row. Taking the lowest bound
42
+ * the fresh copy's row to an older copy's uid and orphaned the fresh one, which
43
+ * a later delete or Empty Trash then expunged by the wrong uid (issue #1122).
44
+ *
45
+ * The maximum is computed rather than read off the tail: RFC 3501 leaves the
46
+ * order of a SEARCH response unspecified, so "last returned" is not "highest".
47
+ */
48
+ export declare const searchMailboxForHighestMessageIdUid: (connection: IImapConnection, mailboxPath: string, messageIdHeader: string) => Promise<number | null>;
49
+ /**
50
+ * Which side of a paused mutation the server actually holds the message on.
51
+ * `gone` is an answer both folders gave; `unprobeable` is the row that carries
52
+ * no Message-ID header, where the server was never asked and its silence says
53
+ * nothing.
54
+ */
55
+ export type PausedPlacement = {
56
+ kind: "at-source";
57
+ } | {
58
+ kind: "at-destination";
59
+ uid: number;
60
+ } | {
61
+ kind: "gone";
62
+ } | {
63
+ kind: "unprobeable";
64
+ };
65
+ /**
66
+ * Ask the server, by Message-ID, which folder holds a message whose mutation a
67
+ * paused cursor interrupted (issue #1203).
68
+ *
69
+ * Reached only on a redelivery, where "the command never left" has stopped
70
+ * being provable: the earlier attempt's tagged OK can be lost with the
71
+ * connection, so restoring the source pair on that assumption would write a
72
+ * settled placement naming a folder the server has already moved the mail out
73
+ * of. `message-copy.ts` asks the same question for the same reason.
74
+ *
75
+ * The SOURCE is asked first and a hit ends it, the gate `confirmTrashMoveUid`
76
+ * documents (#1122): where the mutation never ran, every hit at the destination
77
+ * is an older copy of the same Message-ID — a sieve `fileinto` + `keep`, a
78
+ * multi-label store, a resend — that `deriveMessageId` folds into this one local
79
+ * row, and binding to it hands a later delete somebody else's uid.
80
+ *
81
+ * The source's silence, however, is not the mirror image of its hit. Every
82
+ * caller here reaches this on a paused cursor, which means the source's
83
+ * UIDVALIDITY has moved and the folder may have been recreated holding nothing
84
+ * it held before — so "not at the source" is an answer about the folder, not
85
+ * about our mutation, and `at-destination` is a sighting rather than a
86
+ * confirmation. Callers settle it as unproven; see `settlePausedMove`.
87
+ *
88
+ * Identity, not position, is what makes this askable at all: a paused cursor
89
+ * means the source's UIDVALIDITY has moved, so every stored uid for it names
90
+ * something else, but a SEARCH by header is independent of the axis and answers
91
+ * the same question the cursor rebuild itself matches rows by. For that reason
92
+ * the caller passes the UNGUARDED connection — a `guardConnectionCursor` wrap
93
+ * refuses to open the mailbox at all, and it is bound to the source's snapshot
94
+ * anyway, so opening the destination through it would trip that mailbox too.
95
+ */
96
+ export declare const probePausedPlacement: (connection: IImapConnection, input: {
97
+ messageIdHeader: string | undefined;
98
+ sourceMailboxPath: string;
99
+ destinationMailboxPath: string | undefined;
100
+ }) => Promise<PausedPlacement>;
101
+ /**
102
+ * Resync the affected folders only once the IMAP move has resolved. A move that
103
+ * fails (or is retried) must not refresh counts off a move that didn't happen,
104
+ * so the resync is sequenced strictly after `performMove`.
105
+ */
106
+ export declare const moveThenResync: (performMove: () => Promise<void>, resync: () => Promise<void>) => Promise<void>;
107
+ /**
108
+ * Build the `set` and `composites` payload for the ThreadMessage update on a
109
+ * MESSAGE_MOVE.
110
+ *
111
+ * The CURRENT row state goes in `composites`; the NEW values go in `set`.
112
+ * ElectroDB uses `composites` to run the conditional check on the existing row
113
+ * AND to compute the previous sort-key values needed to recompute the new ones.
114
+ * Passing the NEW values in `composites` makes the conditional check fail with
115
+ * ConditionalCheckFailedException, which ElectroDB wraps as NotFoundError, and
116
+ * the caller silently drops the update. Same root cause as PR #186 fixed for
117
+ * `flag-queue.ts`.
118
+ */
119
+ export declare const buildThreadMessageMoveUpdate: (threadMessage: Pick<ThreadMessageItem, "sentDate" | "mailboxId" | "isRead" | "isDeleted" | "hasStars" | "hasAttachment">, newUid: number, destinationMailboxId: string) => {
120
+ set: {
121
+ uid: number;
122
+ mailboxId: string;
123
+ isDeleted: boolean;
124
+ };
125
+ composites: {
126
+ sentDate: number;
127
+ mailboxId: string;
128
+ isRead: boolean;
129
+ isDeleted: boolean;
130
+ hasStars: boolean;
131
+ hasAttachment: boolean;
132
+ };
133
+ };
134
+ /**
135
+ * Handle MESSAGE_MOVE events.
136
+ * Executes IMAP MOVE command and updates local state with new UID.
137
+ *
138
+ * A failing move retries on SQS redelivery until `receiveCount` reaches
139
+ * the `config.attempts.messageMove` budget, at which point
140
+ * {@link resolveExhaustedMessageMoveFailure} asks IMAP where the message
141
+ * actually is and settles the row into one terminal outcome (issue #655).
142
+ * Before that, `syncStatus: failed` marked every attempt and nothing ever
143
+ * settled the row, so an exhausted move sat `moving`/`failed` forever.
144
+ */
145
+ export declare const handleMessageMove: (event: MessageMoveEvent, ctx: ProcessContext, receiveCount: number) => Promise<void>;
146
+ export {};