From 4de87a8ea2664b3805eac36a6b598909644e6c75 Mon Sep 17 00:00:00 2001 From: Schalli Date: Sun, 28 Jun 2026 01:29:53 +0200 Subject: [PATCH] fix(07): SMTP test warns when connection passes but no auth configured Server reachable without credentials (port 25 open relay) returns { success: true, warning: 'no_auth' } instead of green success. Frontend shows red warning: server reachable but emails will fail. Co-Authored-By: Claude Sonnet 4.6 --- apps/api/src/settings/settings.controller.ts | 3 +-- apps/api/src/settings/settings.service.ts | 23 ++++++++++++------- .../settings/smtp-settings-form.tsx | 4 +++- apps/web/src/lib/settings-api.ts | 2 +- apps/web/src/messages/de.json | 3 ++- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/apps/api/src/settings/settings.controller.ts b/apps/api/src/settings/settings.controller.ts index 9f4ff35..bb2f7e2 100644 --- a/apps/api/src/settings/settings.controller.ts +++ b/apps/api/src/settings/settings.controller.ts @@ -73,7 +73,6 @@ export class SettingsController { throw new BadRequestException('No tenant context'); } - const success = await this.settingsService.testSmtpConfig(tenantId, dto); - return { success }; + return this.settingsService.testSmtpConfig(tenantId, dto); } } diff --git a/apps/api/src/settings/settings.service.ts b/apps/api/src/settings/settings.service.ts index de2234f..048b5d9 100644 --- a/apps/api/src/settings/settings.service.ts +++ b/apps/api/src/settings/settings.service.ts @@ -122,14 +122,14 @@ export class SettingsService { * * T-07-16: Returns only a boolean — no credentials or transport details in the response. */ - async testSmtpConfig(tenantId: string, dto: SmtpConfigDto): Promise { + async testSmtpConfig( + tenantId: string, + dto: SmtpConfigDto, + ): Promise<{ success: boolean; warning?: string }> { let password: string | undefined = dto.password; let username: string | undefined = dto.username; - // Fall back to stored credentials for anything not provided in the DTO. - // The form never pre-fills the password field (T-07-17), so we always - // need to load it from storage. Username may also be absent if the form - // field was cleared, so load it too. + // Fall back to stored credentials (form never pre-fills password — T-07-17) if (!password || !username) { const stored = await this.getDecryptedSmtpConfig(tenantId); if (stored) { @@ -138,6 +138,8 @@ export class SettingsService { } } + const hasAuth = !!(username && password); + try { const transport = nodemailer.createTransport({ host: dto.host, @@ -153,13 +155,18 @@ export class SettingsService { }); await transport.verify(); - return true; + + // T-07-16: connection passed but warn when no auth — server reachable but + // unauthenticated relay will likely be rejected when actually sending. + if (!hasAuth) { + return { success: true, warning: 'no_auth' }; + } + return { success: true }; } catch (error) { - // T-07-16: Log only generic message, never credentials this.logger.warn( `SMTP connection test failed for tenant ${tenantId}: ${(error as Error).message}`, ); - return false; + return { success: false }; } } diff --git a/apps/web/src/components/settings/smtp-settings-form.tsx b/apps/web/src/components/settings/smtp-settings-form.tsx index adda3ac..b52fe7a 100644 --- a/apps/web/src/components/settings/smtp-settings-form.tsx +++ b/apps/web/src/components/settings/smtp-settings-form.tsx @@ -115,7 +115,9 @@ export function SmtpSettingsForm() { setTestFeedback({ type: 'loading', message: t('smtp.testTesting') }); try { const result = await testSmtp(buildPayload()); - if (result.success) { + if (result.success && result.warning === 'no_auth') { + setTestFeedback({ type: 'error', message: t('smtp.testNoAuth') }); + } else if (result.success) { setTestFeedback({ type: 'success', message: t('smtp.testSuccess') }); } else { setTestFeedback({ type: 'error', message: t('smtp.testFailed') }); diff --git a/apps/web/src/lib/settings-api.ts b/apps/web/src/lib/settings-api.ts index 5056aa1..4f50783 100644 --- a/apps/web/src/lib/settings-api.ts +++ b/apps/web/src/lib/settings-api.ts @@ -77,7 +77,7 @@ export async function saveSmtp(payload: SaveSmtpPayload): Promise { */ export async function testSmtp( payload: SaveSmtpPayload, -): Promise<{ success: boolean }> { +): Promise<{ success: boolean; warning?: string }> { const res = await fetch(`${API_URL}/settings/smtp/test`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index 7bddf5a..012bf17 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -135,7 +135,8 @@ "hidePassword": "Passwort verbergen", "testTesting": "Verbindung wird getestet...", "testSuccess": "Verbindung erfolgreich", - "testFailed": "Verbindung fehlgeschlagen" + "testFailed": "Verbindung fehlgeschlagen", + "testNoAuth": "Server erreichbar, aber keine Zugangsdaten konfiguriert — E-Mail-Versand wird fehlschlagen" } }, "widgets": {