feat(mail): SMTP diagnosis and admin email test for Microsoft 365
Mail failures were only logged as a bare error, so a broken Microsoft 365 setup showed up as "no emails arrive" with no cause. Now: - lib/smtp-diagnose.ts maps SMTP errors to a cause with German instructions: security defaults, SMTP AUTH disabled for tenant or mailbox, MFA/conditional access, SendAsDenied (SMTP_FROM differs from the mailbox), wrong credentials, basic-auth retirement, network, TLS. The password is redacted from any echoed server message. - /admin/e-mail-test verifies login and sends a real test email to the signed-in admin only (no free recipient, rate limited, audit logged) and shows the server message, the cause and the settings in use. - The transport now requires STARTTLS on port 587 so credentials are never sent unencrypted, strips a trailing newline from secrets (a common kubectl/base64 mistake) and logs the diagnosed cause. Verified against smtp.office365.com with a fake account: STARTTLS succeeds and "535 5.7.3" is classified as wrong credentials. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
cef2248a45
commit
302a18baa6
10 files changed
+355
-12
No files matched your search
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { explainSmtpError } from "./smtp-diagnose";
|
||||
|
||||
function smtpError(message: string, extra: Record<string, unknown> = {}): Error {
|
||||
return Object.assign(new Error(message), extra);
|
||||
}
|
||||
|
||||
describe("explainSmtpError", () => {
|
||||
it("recognises security defaults blocking basic auth", () => {
|
||||
const result = explainSmtpError(
|
||||
smtpError("Invalid login: 535 5.7.139 Authentication unsuccessful, user is locked by your organization's security defaults policy.", { code: "EAUTH", responseCode: 535 })
|
||||
);
|
||||
expect(result.cause).toBe("security-defaults");
|
||||
expect(result.hint).toContain("Sicherheitsstandards");
|
||||
});
|
||||
|
||||
it("recognises SMTP AUTH disabled for the tenant or mailbox", () => {
|
||||
expect(explainSmtpError(smtpError("535 5.7.139 Authentication unsuccessful, SmtpClientAuthentication is disabled for the Tenant.")).cause).toBe(
|
||||
"smtp-auth-disabled"
|
||||
);
|
||||
expect(explainSmtpError(smtpError("535 5.7.139 Authentication unsuccessful, SmtpClientAuthentication is disabled for the Mailbox.")).cause).toBe(
|
||||
"smtp-auth-disabled"
|
||||
);
|
||||
});
|
||||
|
||||
it("recognises MFA or conditional access rejections", () => {
|
||||
expect(
|
||||
explainSmtpError(smtpError("535 5.7.139 Authentication unsuccessful, the request did not meet the criteria to be authenticated successfully.")).cause
|
||||
).toBe("mfa-or-conditional-access");
|
||||
});
|
||||
|
||||
it("recognises wrong credentials", () => {
|
||||
expect(explainSmtpError(smtpError("Invalid login: 535 5.7.3 Authentication unsuccessful", { code: "EAUTH" })).cause).toBe("wrong-credentials");
|
||||
});
|
||||
|
||||
it("recognises the basic auth retirement", () => {
|
||||
expect(explainSmtpError(smtpError("550 5.7.30 Basic authentication is not supported for Client Submission.")).cause).toBe("basic-auth-retired");
|
||||
});
|
||||
|
||||
it("recognises a sender address the mailbox may not use", () => {
|
||||
expect(
|
||||
explainSmtpError(smtpError("Message failed: 554 5.2.252 SendAsDenied; [email protected] not allowed to send as [email protected]")).cause
|
||||
).toBe("send-as-denied");
|
||||
expect(explainSmtpError(smtpError("550 5.7.60 SMTP; Client does not have permissions to send as this sender")).cause).toBe("send-as-denied");
|
||||
});
|
||||
|
||||
it("recognises network problems", () => {
|
||||
expect(explainSmtpError(smtpError("connect ETIMEDOUT 52.97.1.1:587", { code: "ETIMEDOUT" })).cause).toBe("network");
|
||||
expect(explainSmtpError(smtpError("getaddrinfo ENOTFOUND smtp.office356.com", { code: "EDNS" })).cause).toBe("network");
|
||||
});
|
||||
|
||||
it("falls back to unknown and never throws on odd input", () => {
|
||||
expect(explainSmtpError("kaputt").cause).toBe("unknown");
|
||||
expect(explainSmtpError(null).cause).toBe("unknown");
|
||||
});
|
||||
|
||||
it("never echoes a password that appears in the error", () => {
|
||||
const result = explainSmtpError(smtpError("535 5.7.3 Authentication unsuccessful"), "geheim123");
|
||||
expect(JSON.stringify(result)).not.toContain("geheim123");
|
||||
const leaking = explainSmtpError(smtpError("AUTH PLAIN failed for secret geheim123"), "geheim123");
|
||||
expect(leaking.detail).not.toContain("geheim123");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user