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:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-26 17:48:40 +02:00
1 parent cef2248a45
commit 302a18baa6
10 files changed
+355 -12

No files matched your search

+13
View File
@@ -1231,6 +1231,18 @@ export const en: Dictionary = {
sectionVisibility: "Visibility",
create: "Create",
},
adminEmailTestPage: {
title: "Test email delivery",
intro: "Connects to the mail server, signs in with SMTP_USER and sends a test email to your own address. If something fails, you see the mail server message and what to do (the explanation is in German).",
button: "Send test email",
running: "Testing …",
success: (to) => `Delivery works. The test email is on its way to ${to}.`,
notConfigured: "SMTP is not set up: SMTP_HOST, SMTP_USER or SMTP_PASSWORD is missing from the Kubernetes secret. Without them, emails are only written to the server log.",
failureHeading: "Delivery failed",
causeLabel: "What to do",
detailLabel: "Mail server message",
configLabel: "Settings used",
},
adminWithdrawalsPage: {
title: "Withdrawals",
intro: "Notices submitted via the online withdrawal function (§ 356a BGB). The refund is due no later than 14 days after receipt; you may withhold it until the goods are back or proof of return has been provided.",
@@ -1470,6 +1482,7 @@ export const en: Dictionary = {
navVouchers: "Vouchers",
navReviews: "Reviews",
navAuditLog: "Audit log",
navEmailTest: "Email test",
navContent: "Content",
navAriaLabel: "Admin navigation",
mobileNavAriaLabel: "Mobile admin navigation",