fix: SMTP-Fehler nur bei Empfaengerablehnung endgueltig

Bisher setzte jeder SMTP-5xx die Mail sofort auf FAILED, auch Anmelde-
und Richtlinienfehler (EAUTH, EMESSAGE, 552). Ein kaputtes SMTP-Secret
haette alle Buchungs- und Zahlungsmails verworfen, ohne Rueckweg
(dedupeKey verhindert erneutes Einreihen).

Endgueltig ist jetzt nur EENVELOPE mit 550, 551, 553, 556 oder 5xx mit
Status 5.1.x. Alles andere bleibt PENDING und wird bis
MAX_EMAIL_ATTEMPTS wiederholt.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-12 11:20:34 +02:00
1 parent e6fa383934
commit 34ccbc3b9a
2 files changed
+48 -7

No files matched your search

+36 -2
View File
@@ -13,8 +13,8 @@ vi.mock("nodemailer", async (importOriginal) => {
return { ...actual, default: { ...actual.default, createTransport } };
});
function smtpError(message: string, responseCode?: number) {
return Object.assign(new Error(message), { code: "EENVELOPE", responseCode });
function smtpError(message: string, responseCode?: number, code = "EENVELOPE") {
return Object.assign(new Error(message), { code, responseCode });
}
async function queueMail(to = "[email protected]") {
@@ -79,4 +79,38 @@ describe("Postausgang: Fehler beim SMTP-Versand", () => {
expect(await db.emailMessage.findUniqueOrThrow({ where: { id } })).toMatchObject({ status: "PENDING", attempts: 1 });
}
});
test("Server- und Absenderfehler (Anmeldung, Richtlinie) bleiben PENDING statt FAILED", async () => {
const auth = await queueMail("[email protected]");
const policy = await queueMail("[email protected]");
sendMail
.mockRejectedValueOnce(smtpError("535 5.7.8 Authentication failed", 535, "EAUTH"))
.mockRejectedValueOnce(smtpError("554 5.7.1 Message rejected as spam", 554, "EMESSAGE"));
vi.spyOn(console, "error").mockImplementation(() => {});
expect(await flushEmails(db)).toEqual({ sent: 0, failed: 2 });
for (const id of [auth.id, policy.id]) {
expect(await db.emailMessage.findUniqueOrThrow({ where: { id } })).toMatchObject({ status: "PENDING", attempts: 1 });
}
});
test("EENVELOPE mit 550/551/553/556 oder 5.1.x ist endgueltig, 552 und 4xx nicht", async () => {
const cases: [number, string, boolean][] = [
[550, "550 5.1.1 User unknown", true],
[551, "551 User not local", true],
[553, "553 Mailbox name not allowed", true],
[554, "554 5.1.2 Bad destination system address", true],
[552, "552 Mailbox full", false],
[451, "451 4.1.1 Try later", false],
];
vi.spyOn(console, "error").mockImplementation(() => {});
for (const [index, [code, response, permanent]] of cases.entries()) {
await resetDb();
const mail = await queueMail(`r${index}@example.org`);
sendMail.mockRejectedValueOnce(Object.assign(smtpError(response, code), { response }));
await flushEmails(db);
const after = await db.emailMessage.findUniqueOrThrow({ where: { id: mail.id } });
expect(after.status, response).toBe(permanent ? "FAILED" : "PENDING");
}
});
});