@remit/imap-worker 0.0.72 → 0.0.73

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.
@@ -1,11 +1,15 @@
1
1
  import { getClient } from "@remit/backend/client";
2
- import { MessageStatus, MessageSyncStatus } from "@remit/domain-enums";
2
+ import { isNotFoundError } from "@remit/data-ports/errors";
3
+ import {
4
+ MessageMutation,
5
+ MessageStatus,
6
+ MessageSyncStatus,
7
+ } from "@remit/domain-enums";
3
8
  import type { Logger } from "@remit/logger-lambda";
4
9
  import {
5
10
  guardConnectionCursor,
6
11
  type IImapConnection,
7
12
  isCursorRebuildNeeded,
8
- isPlacementUnsettled,
9
13
  MailboxCursorPausedError,
10
14
  reconcileStaleMessage,
11
15
  } from "@remit/mailbox-service";
@@ -14,7 +18,6 @@ import { isAccountDeleted } from "../account-check.js";
14
18
  import { createConnectionScopeWithCredentials } from "../connection-scope.js";
15
19
  import { emitEvent } from "../emit.js";
16
20
  import type { MessageCopyEvent } from "../events.js";
17
- import { isNotFoundError } from "../is-not-found.js";
18
21
  import { withOAuthLifecycle } from "../with-oauth-lifecycle.js";
19
22
  import { buildLifecycleDeps } from "../with-oauth-lifecycle-deps.js";
20
23
  import {
@@ -147,7 +150,12 @@ export const handleMessageCopy = async (
147
150
  // This copy already settled — `updateUid` cleared `status: moving` when the
148
151
  // server confirmed it. A redelivery reaching here would COPY the message a
149
152
  // second time, and COPY has no source-side effect to make that a no-op.
150
- if (!isPlacementUnsettled(copyRow)) {
153
+ //
154
+ // `moving` specifically, not the shared unsettled predicate: the question is
155
+ // whether THIS copy is still outstanding, and a `deleting` row is somebody
156
+ // else's work — reading it as unsettled is what would let the second COPY
157
+ // through.
158
+ if (copyRow.status !== MessageStatus.moving) {
151
159
  log.info(
152
160
  { accountId, newMessageId, uid: copyRow.uid, status: copyRow.status },
153
161
  "Skipping MESSAGE_COPY: the copy already settled against confirmed IMAP state",
@@ -206,17 +214,15 @@ export const handleMessageCopy = async (
206
214
  };
207
215
 
208
216
  const settleCopied = async (newUid: number): Promise<void> => {
217
+ // `updateUid` writes the confirmed uid, `active` and `synced` in one
218
+ // statement, so the second write this used to make said nothing the
219
+ // first had not — and said it without a predicate.
209
220
  await messageService.updateUid(
210
221
  newMessageId,
211
222
  newUid,
212
223
  destinationMailboxId,
213
224
  );
214
225
 
215
- await messageService.update(newMessageId, {
216
- status: MessageStatus.active,
217
- syncStatus: MessageSyncStatus.synced,
218
- });
219
-
220
226
  const threadMessage = await threadMessageService.findByMessageId(
221
227
  account.accountConfigId,
222
228
  newMessageId,
@@ -293,10 +299,20 @@ export const handleMessageCopy = async (
293
299
  // that turns out to be there. Silence buys the safer half of one pair
294
300
  // and the worse half of the other.
295
301
  const settleBroken = async (reason: string): Promise<void> => {
296
- await messageService.update(newMessageId, {
297
- status: MessageStatus.deleted,
298
- syncStatus: MessageSyncStatus.failed,
299
- });
302
+ // A transition, not a plain update (imap-mutations R3): the copy row
303
+ // is only this handler's to settle while its own placement is still
304
+ // unsettled. The row it leaves is `deleted`, so no listing carries it
305
+ // and the marker is read by nothing — it is written anyway, because a
306
+ // give-up that does not name itself is the gap #1229 came from.
307
+ await messageService.transitionPlacement(
308
+ newMessageId,
309
+ { status: MessageStatus.moving },
310
+ {
311
+ status: MessageStatus.deleted,
312
+ syncStatus: MessageSyncStatus.abandoned,
313
+ abandonedMutation: MessageMutation.copy,
314
+ },
315
+ );
300
316
  log.error(
301
317
  {
302
318
  alert: "message_copy_unconfirmed",
@@ -495,10 +511,15 @@ export const handleMessageCopy = async (
495
511
  { sourceMessageId, uid },
496
512
  "Source message not found on IMAP, marking copy as failed",
497
513
  );
498
- await messageService.update(newMessageId, {
499
- status: MessageStatus.deleted,
500
- syncStatus: MessageSyncStatus.failed,
501
- });
514
+ await messageService.transitionPlacement(
515
+ newMessageId,
516
+ { status: MessageStatus.moving },
517
+ {
518
+ status: MessageStatus.deleted,
519
+ syncStatus: MessageSyncStatus.abandoned,
520
+ abandonedMutation: MessageMutation.copy,
521
+ },
522
+ );
502
523
  return;
503
524
  }
504
525
 
@@ -506,9 +527,11 @@ export const handleMessageCopy = async (
506
527
  // Transient copy failure — expected (connections drop). Queue
507
528
  // redelivery retries, and the probe above keeps the retry from
508
529
  // copying the message twice.
509
- await messageService.update(newMessageId, {
510
- syncStatus: MessageSyncStatus.failed,
511
- });
530
+ await messageService.transitionPlacement(
531
+ newMessageId,
532
+ { status: MessageStatus.moving },
533
+ { syncStatus: MessageSyncStatus.failed },
534
+ );
512
535
  throw error;
513
536
  }
514
537
 
@@ -2,14 +2,14 @@ import type {
2
2
  IMessageRepository,
3
3
  IThreadMessageRepository,
4
4
  } from "@remit/data-ports";
5
- import { MessageSyncStatus } from "@remit/domain-enums";
5
+ import { MessageMutation, MessageSyncStatus } from "@remit/domain-enums";
6
6
  import {
7
7
  type IImapConnection,
8
8
  isMessageGoneFromOpenMailbox,
9
9
  reconcileStaleMessage,
10
+ restoreSourcePlacement,
10
11
  type StaleMessageReconcileDeps,
11
12
  } from "@remit/mailbox-service";
12
- import { restoreSourcePlacement } from "./restore-source-placement.js";
13
13
 
14
14
  export interface MessageDeleteTerminalLogger {
15
15
  info(obj: Record<string, unknown>, msg: string): void;
@@ -18,7 +18,7 @@ export interface MessageDeleteTerminalLogger {
18
18
 
19
19
  export interface ResolveExhaustedMessageDeleteDeps
20
20
  extends StaleMessageReconcileDeps {
21
- messageService: Pick<IMessageRepository, "delete" | "updateForMove">;
21
+ messageService: Pick<IMessageRepository, "delete" | "transitionPlacement">;
22
22
  threadMessageService: Pick<
23
23
  IThreadMessageRepository,
24
24
  "findAllByMessageId" | "deleteMany" | "update"
@@ -60,7 +60,7 @@ export interface ResolveExhaustedMessageDeleteResult {
60
60
  * effect, but it keeps failing: broken code or a broken account, not a
61
61
  * transient blip. The row is put back where the server has just said the
62
62
  * message is: `mailboxId` and `uid` return to the delete's source pair, and
63
- * the thread rows lose the deletion mark the optimistic `updateForMove`
63
+ * the thread rows lose the deletion mark the optimistic transition
64
64
  * wrote (issue #1098).
65
65
  *
66
66
  * Settling `status` to `active` while leaving `mailboxId` on Trash is what
@@ -132,9 +132,13 @@ export const resolveExhaustedMessageDeleteFailure = async (
132
132
  sourceMailboxId,
133
133
  uid,
134
134
  // The probe above just confirmed the pair, so the row is a faithful
135
- // projection of the source again. The delete's own failure is the alert,
136
- // not a `failed` left on a row nothing is still trying to delete.
137
- syncStatus: MessageSyncStatus.synced,
135
+ // projection of the source again — and the delete the user asked for did
136
+ // not happen, with nothing left to try it. `abandoned` is the value that
137
+ // says so without claiming a retry is coming (imap-mutations R3); the
138
+ // alert stays for the operator, and the mutation names itself so the
139
+ // reading pane offers the right way out (issue #1229).
140
+ syncStatus: MessageSyncStatus.abandoned,
141
+ abandonedMutation: MessageMutation.delete,
138
142
  });
139
143
 
140
144
  return { outcome: "broken" };