Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m25s
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]>
74 lines
3.6 KiB
TypeScript
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");
|
|
});
|
|
});
|