@remit/imap-worker 0.0.47 → 0.0.48

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/imap-worker",
3
- "version": "0.0.47",
3
+ "version": "0.0.48",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -125,6 +125,27 @@ const event: AppendSentMessageEvent = {
125
125
  const called = (method: string): Call[] =>
126
126
  h.calls.filter((c) => c.method === method);
127
127
 
128
+ type BackendClient = Awaited<ReturnType<AppendSentMessageDeps["getClient"]>>;
129
+
130
+ const depsWithFailingDelete = (): AppendSentMessageDeps => {
131
+ const base = deps();
132
+ return {
133
+ ...base,
134
+ getClient: async (): Promise<BackendClient> => {
135
+ const client = await base.getClient();
136
+ return {
137
+ ...client,
138
+ outboxMessage: {
139
+ ...client.outboxMessage,
140
+ delete: async (): Promise<void> => {
141
+ throw new Error("storage down");
142
+ },
143
+ },
144
+ };
145
+ },
146
+ };
147
+ };
148
+
128
149
  describe("handleAppendSentMessage", () => {
129
150
  beforeEach(() => {
130
151
  h = fresh();
@@ -268,19 +289,8 @@ describe("handleAppendSentMessage", () => {
268
289
  });
269
290
 
270
291
  it("leaves the row alone when the APPEND landed but the delete did not", async () => {
271
- const failingDeps = deps();
272
- const client = await failingDeps.getClient();
273
- (
274
- client as unknown as { outboxMessage: { delete: () => Promise<void> } }
275
- ).outboxMessage.delete = async () => {
276
- throw new Error("storage down");
277
- };
278
-
279
292
  await assert.rejects(
280
- handleAppendSentMessage(event, noopLog, APPEND_SENT_MAX_ATTEMPTS, {
281
- ...failingDeps,
282
- getClient: async () => client,
283
- } as AppendSentMessageDeps),
293
+ handleAppendSentMessage(event, noopLog, 1, depsWithFailingDelete()),
284
294
  /storage down/,
285
295
  );
286
296
 
@@ -288,4 +298,18 @@ describe("handleAppendSentMessage", () => {
288
298
  // would say the opposite.
289
299
  assert.equal(called("outboxMessage.update").length, 0);
290
300
  });
301
+
302
+ it("stops redelivering a landed APPEND at the budget instead of filing another copy", async () => {
303
+ // A redelivery starts from the top and appends again, so retrying past the
304
+ // budget files one copy per attempt in the user's Sent folder (#830).
305
+ await handleAppendSentMessage(
306
+ event,
307
+ noopLog,
308
+ APPEND_SENT_MAX_ATTEMPTS,
309
+ depsWithFailingDelete(),
310
+ );
311
+
312
+ assert.equal(called("connection.append").length, 1);
313
+ assert.equal(called("outboxMessage.update").length, 0);
314
+ });
291
315
  });
@@ -193,16 +193,27 @@ export const handleAppendSentMessage = async (
193
193
  // attempt to pick up. At the budget the record would dead-letter, and a
194
194
  // dead-lettered APPEND is exactly how a delivered message goes missing.
195
195
  //
196
- // Once the APPEND itself has landed the copy is in Sent whatever else
197
- // failed, so that case keeps the plain retry semantics.
198
- if (appended || receiveCount < APPEND_SENT_MAX_ATTEMPTS) throw error;
196
+ // The budget binds whether or not the APPEND landed. A redelivery
197
+ // starts from the top and appends again, so an unbudgeted retry files
198
+ // one copy per attempt in the user's Sent folder.
199
+ if (receiveCount < APPEND_SENT_MAX_ATTEMPTS) throw error;
199
200
  return error;
200
201
  },
201
202
  );
202
203
 
203
- // The APPEND landed: the copy is in Sent even if the row delete that follows
204
- // it failed, so there is nothing to settle and a retry may still run.
205
- if (appended) return;
204
+ // The APPEND landed: the copy is in Sent whatever failed after it, so there
205
+ // is nothing to settle as unfiled. A row that outlives its delete holds
206
+ // `sent`, which every view hides, until the migrator's boot-time stranded-row
207
+ // repair settles it — the next container start, not sooner (#824).
208
+ if (appended) {
209
+ if (failure) {
210
+ log.error(
211
+ { accountId, outboxMessageId, reason: String(failure) },
212
+ "Sent message was filed but its outbox row survived its delete and stays hidden until the boot-time repair",
213
+ );
214
+ }
215
+ return;
216
+ }
206
217
 
207
218
  // A terminal auth failure returns here without throwing — withOAuthLifecycle
208
219
  // flips the account to reauth_required and ACKs the record, which without