@remit/smtp-worker 1.0.7 → 1.0.8
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
|
@@ -226,10 +226,20 @@ const attach = async (
|
|
|
226
226
|
});
|
|
227
227
|
};
|
|
228
228
|
|
|
229
|
-
const
|
|
230
|
-
(
|
|
229
|
+
const closedPort = (): Promise<number> =>
|
|
230
|
+
new Promise((resolve) => {
|
|
231
|
+
const server = net.createServer();
|
|
232
|
+
server.listen(0, "127.0.0.1", () => {
|
|
233
|
+
const address = server.address();
|
|
234
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
235
|
+
server.close(() => resolve(port));
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const socketTo =
|
|
240
|
+
(port: number): SendMessageContext["getSocket"] =>
|
|
231
241
|
(_request, callback) => {
|
|
232
|
-
callback(
|
|
242
|
+
callback(null, { connection: net.connect(port, "127.0.0.1") });
|
|
233
243
|
};
|
|
234
244
|
|
|
235
245
|
const seedAddress = async (w: World, email: string): Promise<string> => {
|
|
@@ -675,7 +685,7 @@ describe("handleSendMessage retry-budget exhaustion (issue #951)", () => {
|
|
|
675
685
|
|
|
676
686
|
await handleSendMessage(
|
|
677
687
|
a.event,
|
|
678
|
-
{ ...w.ctx, getSocket:
|
|
688
|
+
{ ...w.ctx, getSocket: socketTo(await closedPort()), maxAttempts: 1 },
|
|
679
689
|
1,
|
|
680
690
|
);
|
|
681
691
|
|
|
@@ -685,24 +695,39 @@ describe("handleSendMessage retry-budget exhaustion (issue #951)", () => {
|
|
|
685
695
|
assert.deepEqual(w.failures, ["network"]);
|
|
686
696
|
});
|
|
687
697
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
const a = await arrange(w);
|
|
698
|
+
it("retries a connection the server drops before DATA, then settles it `failed`", async () => {
|
|
699
|
+
w.close();
|
|
700
|
+
w = await openWorld({ dropBeforeGreeting: true });
|
|
701
|
+
const a = await arrange(w);
|
|
693
702
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
703
|
+
await assert.rejects(
|
|
704
|
+
() => handleSendMessage(a.event, w.ctx, MAX_ATTEMPTS - 1),
|
|
705
|
+
SmtpConnectionError,
|
|
706
|
+
);
|
|
707
|
+
await handleSendMessage(a.event, w.ctx, MAX_ATTEMPTS);
|
|
699
708
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
709
|
+
const row = await outboxRow(w, a);
|
|
710
|
+
assert.equal(row.status, OutboxMessageStatus.failed);
|
|
711
|
+
assert.equal(w.smtp.received.length, 0);
|
|
712
|
+
assert.deepEqual(w.failures, ["network"]);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it("settles a connection the server closes after DATA at `unfiled`, sending once", async () => {
|
|
716
|
+
w.close();
|
|
717
|
+
w = await openWorld({ dropAfterData: true });
|
|
718
|
+
const a = await arrange(w);
|
|
719
|
+
|
|
720
|
+
await handleSendMessage(a.event, w.ctx, 1);
|
|
721
|
+
|
|
722
|
+
const row = await outboxRow(w, a);
|
|
723
|
+
assert.equal(row.status, OutboxMessageStatus.unfiled);
|
|
724
|
+
assert.match(String(row.lastError), /may already have been delivered/);
|
|
725
|
+
assert.equal(
|
|
726
|
+
w.smtp.received.filter((chunk) => chunk.includes("\r\n.\r\n")).length,
|
|
727
|
+
1,
|
|
728
|
+
);
|
|
729
|
+
assert.deepEqual(w.failures, ["network"]);
|
|
730
|
+
});
|
|
706
731
|
|
|
707
732
|
it("settles an exhausted transient SMTP refusal at `failed` instead of leaving it `queued` forever", async () => {
|
|
708
733
|
w.close();
|
|
@@ -148,35 +148,6 @@ const REAUTH_REQUIRED =
|
|
|
148
148
|
const UNFILED_CONNECTION_LOST =
|
|
149
149
|
"The connection to the outgoing server dropped during the send, so this message may already have been delivered. Check with the recipient before sending it again.";
|
|
150
150
|
|
|
151
|
-
/**
|
|
152
|
-
* The connection failures that prove nothing was submitted: no session ever
|
|
153
|
-
* opened, so the server holds no copy and `failed` is safe — Retry sends the
|
|
154
|
-
* only copy there is.
|
|
155
|
-
*
|
|
156
|
-
* `ECONNRESET` and `ETIMEDOUT` classify as `network` too and are deliberately
|
|
157
|
-
* absent. Either can land after DATA, with the message already queued on the
|
|
158
|
-
* server, and a `failed` row invites a Retry that delivers it twice.
|
|
159
|
-
*/
|
|
160
|
-
const NEVER_SUBMITTED_CODES: ReadonlySet<string> = new Set([
|
|
161
|
-
"ECONNREFUSED",
|
|
162
|
-
"ENOTFOUND",
|
|
163
|
-
"EHOSTUNREACH",
|
|
164
|
-
]);
|
|
165
|
-
|
|
166
|
-
const errorCode = (cause: unknown): string =>
|
|
167
|
-
cause instanceof Error && "code" in cause && typeof cause.code === "string"
|
|
168
|
-
? cause.code
|
|
169
|
-
: "";
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Whether the message can be re-sent without risking a second copy. An auth
|
|
173
|
-
* rejection is decided before the envelope; a connection failure carries the
|
|
174
|
-
* code that says how far it got. Anything else counts as possibly delivered,
|
|
175
|
-
* which is the answer that cannot produce a duplicate.
|
|
176
|
-
*/
|
|
177
|
-
const neverSubmitted = (err: SmtpConnectionError): boolean =>
|
|
178
|
-
err.kind === "auth" || NEVER_SUBMITTED_CODES.has(errorCode(err.cause));
|
|
179
|
-
|
|
180
151
|
const SENDABLE_STATUSES: ReadonlySet<OutboxMessageItem["status"]> = new Set([
|
|
181
152
|
OutboxMessageStatus.draft,
|
|
182
153
|
OutboxMessageStatus.queued,
|
|
@@ -408,27 +379,18 @@ export const handleSendMessage = async (
|
|
|
408
379
|
await settleBlockedOnReauth(OutboxMessageStatus.sending);
|
|
409
380
|
return; // ACK — do not retry
|
|
410
381
|
}
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
// here instead of dead-lettering with the row stuck at `sending`
|
|
414
|
-
// (issue #951). Where it settles is the double-send question:
|
|
415
|
-
// `neverSubmitted` says the message cannot be on the server, so
|
|
416
|
-
// `failed` offers the Retry the user needs; anything else settles
|
|
417
|
-
// `unfiled`, the state that says a copy may be out there and which
|
|
418
|
-
// Retry is not offered on. Wait-or-reconcile
|
|
419
|
-
// (docs/architecture/imap-mutations.md R2): neither applies — a
|
|
420
|
-
// submission leaves no server-side handle to reconcile against, so the
|
|
421
|
-
// row settles on what the failure itself proves.
|
|
382
|
+
// Once the whole message is written the server may hold a copy, so a
|
|
383
|
+
// redelivery or a Retry could deliver it twice.
|
|
422
384
|
if (err instanceof SmtpConnectionError) {
|
|
423
|
-
if (receiveCount < ctx.maxAttempts) {
|
|
385
|
+
if (!err.afterData && receiveCount < ctx.maxAttempts) {
|
|
424
386
|
throw err;
|
|
425
387
|
}
|
|
426
|
-
const settled =
|
|
427
|
-
? {
|
|
428
|
-
: {
|
|
388
|
+
const settled = err.afterData
|
|
389
|
+
? {
|
|
429
390
|
status: OutboxMessageStatus.unfiled,
|
|
430
391
|
lastError: `${UNFILED_CONNECTION_LOST} (${err.message})`,
|
|
431
|
-
}
|
|
392
|
+
}
|
|
393
|
+
: { status: OutboxMessageStatus.failed, lastError: err.message };
|
|
432
394
|
await ports.outboxMessage.update(
|
|
433
395
|
accountConfigId,
|
|
434
396
|
outboxMessageId,
|
|
@@ -444,7 +406,7 @@ export const handleSendMessage = async (
|
|
|
444
406
|
receiveCount,
|
|
445
407
|
status: settled.status,
|
|
446
408
|
},
|
|
447
|
-
"SMTP
|
|
409
|
+
"SMTP connection failed; settling the row",
|
|
448
410
|
);
|
|
449
411
|
return; // ACK — settled terminal, no more retries
|
|
450
412
|
}
|