Files
Wabenhain/src/lib/smtp-diagnose.test.ts
T
Nick SzaunigandClaude Opus 5.5 11910dd6dd
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m25s
refactor(mail): send via the SSO app registration, drop SMTP basic auth
Mail now uses the same Entra ID app registration as "Sign in with
Microsoft" (ENTRA_TENANT_ID, ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET), so
only one enterprise application is needed. The app additionally gets
the SMTP.SendAsApp permission; Exchange still limits it to the mailbox
granted via Add-MailboxPermission.

Password (basic auth) sending is removed entirely: SMTP_HOST, SMTP_PORT,
SMTP_PASSWORD and the separate SMTP_OAUTH_* settings are gone. The
transport is fixed to smtp.office365.com:587 with required STARTTLS.
SMTP_USER (sending mailbox) and SMTP_FROM remain.

- lib/entra.ts exposes readEntraAppCredentials(), shared by SSO and mail
- diagnosis drops basic-auth-only causes; hints name the ENTRA_* values
  and warn that an expired secret breaks sign-in and mail together
- admin e-mail test, .env.example, docker-compose, k8s manifests,
  preflight and README updated

Verified against Entra ID with fake values: token error classified,
client secret never exposed, missing ENTRA_CLIENT_SECRET reported.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 19:23:05 +02:00

74 lines
3.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { explainSmtpError } from "./smtp-diagnose";
import { Ms365TokenError } from "./ms365-oauth";
function smtpError(message: string, extra: Record<string, unknown> = {}): Error {
return Object.assign(new Error(message), extra);
}
describe("explainSmtpError", () => {
it("explains a client secret ID used instead of its value", () => {
const result = explainSmtpError(new Ms365TokenError("Token-Abruf abgelehnt: AADSTS7000215: Invalid client secret provided.", "7000215"));
expect(result.cause).toBe("oauth-token");
expect(result.hint).toContain("ENTRA_CLIENT_SECRET");
expect(result.hint).toContain("Wert");
});
it("explains an expired client secret and that SSO is affected too", () => {
const result = explainSmtpError(new Ms365TokenError("AADSTS7000222: The provided client secret keys are expired.", "7000222"));
expect(result.cause).toBe("oauth-token");
expect(result.hint).toContain("abgelaufen");
expect(result.hint).toContain("Mit Microsoft anmelden");
});
it("explains an unknown app or tenant", () => {
expect(explainSmtpError(new Ms365TokenError("AADSTS700016: Application not found", "700016")).hint).toContain("ENTRA_CLIENT_ID");
expect(explainSmtpError(new Ms365TokenError("AADSTS90002: Tenant not found", "90002")).hint).toContain("ENTRA_TENANT_ID");
});
it("falls back to a general token hint for other AADSTS codes", () => {
const result = explainSmtpError(new Ms365TokenError("AADSTS1234: something", "1234"));
expect(result.cause).toBe("oauth-token");
expect(result.hint).toContain("ENTRA_TENANT_ID");
});
it("maps a rejected XOAUTH2 login to missing mailbox access", () => {
const result = explainSmtpError(smtpError("Invalid login: 535 5.7.3 Authentication unsuccessful", { code: "EAUTH" }));
expect(result.cause).toBe("oauth-mailbox-access");
expect(result.hint).toContain("SMTP.SendAsApp");
expect(result.hint).toContain("Add-MailboxPermission");
});
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 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.office365.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("redacts secrets from the echoed server message", () => {
const result = explainSmtpError(smtpError("AUTH XOAUTH2 eyJtoken failed secret-xyz"), { secrets: ["eyJtoken", "secret-xyz"] });
expect(result.detail).not.toContain("eyJtoken");
expect(result.detail).not.toContain("secret-xyz");
});
});