From f81cdf8768763a6bb33c6c5646614e4a4a9f1770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 13 Sep 2026 14:50:33 +0200 Subject: [PATCH] fix(notifications): name TLS, protocol and envelope faults in email tests A STARTTLS failure (port 465 set up as 587) and a protocol error now read as connection_failed, and an envelope or message refused without a reply code as upstream_rejected. They used to fall through to the 500 for an unnamed failure. Refs #947 --- .../senders/__tests__/email.test.ts | 40 +++++++++++++++++++ src/lib/notifications/senders/email.ts | 11 ++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/lib/notifications/senders/__tests__/email.test.ts b/src/lib/notifications/senders/__tests__/email.test.ts index 83f12cec3..b785b1130 100644 --- a/src/lib/notifications/senders/__tests__/email.test.ts +++ b/src/lib/notifications/senders/__tests__/email.test.ts @@ -237,3 +237,43 @@ describe("sendViaEmail — the failure names itself for the test button", () => expect(result.smtpCode).toBeUndefined(); }); }); + +describe("sendViaEmail — transport and envelope faults are named too", () => { + it.each([ + ["a TLS handshake failure", "ETLS", undefined, "connection_failed"], + ["a protocol error", "EPROTOCOL", undefined, "connection_failed"], + [ + "an envelope refused without a code", + "EENVELOPE", + undefined, + "upstream_rejected", + ], + [ + "a message refused without a code", + "EMESSAGE", + undefined, + "upstream_rejected", + ], + [ + "an envelope refused with a temporary code", + "EENVELOPE", + 451, + "upstream_error", + ], + ])("%s", async (_label, code, responseCode, failureCode) => { + loadEmailConfigMock.mockReturnValue(transport); + sendMailMock.mockRejectedValue( + Object.assign(new Error(String(code)), { + code, + ...(responseCode !== undefined ? { responseCode } : {}), + }), + ); + + const result = await sendViaEmail( + { recipient: "you@example.com" }, + payload(), + ); + + expect(result.failureCode).toBe(failureCode); + }); +}); diff --git a/src/lib/notifications/senders/email.ts b/src/lib/notifications/senders/email.ts index 2b518638d..22011e45e 100644 --- a/src/lib/notifications/senders/email.ts +++ b/src/lib/notifications/senders/email.ts @@ -106,13 +106,20 @@ function classifySmtpError(err: unknown): { : code === "ECONNECTION" || code === "ESOCKET" || code === "EDNS" || - code === "ECONNREFUSED" + code === "ECONNREFUSED" || + // STARTTLS or TLS handshake failure (typically port 465 set up + // as 587 or the reverse) and a server that does not speak SMTP. + code === "ETLS" || + code === "EPROTOCOL" ? "connection_failed" : smtpCode !== undefined && smtpCode >= 500 ? "upstream_rejected" : smtpCode !== undefined && smtpCode >= 400 ? "upstream_error" - : undefined; + : // An envelope or message refused without a reply code. + code === "EENVELOPE" || code === "EMESSAGE" + ? "upstream_rejected" + : undefined; const extras = { ...(failureCode ? { failureCode } : {}), ...(smtpCode !== undefined ? { smtpCode } : {}),