fix(07): SMTP test warns when connection passes but no auth configured
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 01:29:53 +02:00
parent 853095faef
commit 4de87a8ea2
5 changed files with 22 additions and 13 deletions
+1 -2
View File
@@ -73,7 +73,6 @@ export class SettingsController {
throw new BadRequestException('No tenant context'); throw new BadRequestException('No tenant context');
} }
const success = await this.settingsService.testSmtpConfig(tenantId, dto); return this.settingsService.testSmtpConfig(tenantId, dto);
return { success };
} }
} }
+15 -8
View File
@@ -122,14 +122,14 @@ export class SettingsService {
* *
* T-07-16: Returns only a boolean — no credentials or transport details in the response. * T-07-16: Returns only a boolean — no credentials or transport details in the response.
*/ */
async testSmtpConfig(tenantId: string, dto: SmtpConfigDto): Promise<boolean> { async testSmtpConfig(
tenantId: string,
dto: SmtpConfigDto,
): Promise<{ success: boolean; warning?: string }> {
let password: string | undefined = dto.password; let password: string | undefined = dto.password;
let username: string | undefined = dto.username; let username: string | undefined = dto.username;
// Fall back to stored credentials for anything not provided in the DTO. // Fall back to stored credentials (form never pre-fills password — T-07-17)
// 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.
if (!password || !username) { if (!password || !username) {
const stored = await this.getDecryptedSmtpConfig(tenantId); const stored = await this.getDecryptedSmtpConfig(tenantId);
if (stored) { if (stored) {
@@ -138,6 +138,8 @@ export class SettingsService {
} }
} }
const hasAuth = !!(username && password);
try { try {
const transport = nodemailer.createTransport({ const transport = nodemailer.createTransport({
host: dto.host, host: dto.host,
@@ -153,13 +155,18 @@ export class SettingsService {
}); });
await transport.verify(); 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) { } catch (error) {
// T-07-16: Log only generic message, never credentials
this.logger.warn( this.logger.warn(
`SMTP connection test failed for tenant ${tenantId}: ${(error as Error).message}`, `SMTP connection test failed for tenant ${tenantId}: ${(error as Error).message}`,
); );
return false; return { success: false };
} }
} }
@@ -115,7 +115,9 @@ export function SmtpSettingsForm() {
setTestFeedback({ type: 'loading', message: t('smtp.testTesting') }); setTestFeedback({ type: 'loading', message: t('smtp.testTesting') });
try { try {
const result = await testSmtp(buildPayload()); 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') }); setTestFeedback({ type: 'success', message: t('smtp.testSuccess') });
} else { } else {
setTestFeedback({ type: 'error', message: t('smtp.testFailed') }); setTestFeedback({ type: 'error', message: t('smtp.testFailed') });
+1 -1
View File
@@ -77,7 +77,7 @@ export async function saveSmtp(payload: SaveSmtpPayload): Promise<SmtpConfig> {
*/ */
export async function testSmtp( export async function testSmtp(
payload: SaveSmtpPayload, payload: SaveSmtpPayload,
): Promise<{ success: boolean }> { ): Promise<{ success: boolean; warning?: string }> {
const res = await fetch(`${API_URL}/settings/smtp/test`, { const res = await fetch(`${API_URL}/settings/smtp/test`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
+2 -1
View File
@@ -135,7 +135,8 @@
"hidePassword": "Passwort verbergen", "hidePassword": "Passwort verbergen",
"testTesting": "Verbindung wird getestet...", "testTesting": "Verbindung wird getestet...",
"testSuccess": "Verbindung erfolgreich", "testSuccess": "Verbindung erfolgreich",
"testFailed": "Verbindung fehlgeschlagen" "testFailed": "Verbindung fehlgeschlagen",
"testNoAuth": "Server erreichbar, aber keine Zugangsdaten konfiguriert — E-Mail-Versand wird fehlschlagen"
} }
}, },
"widgets": { "widgets": {