{"record":{"id":"7e22376730343de8","repo":"immich-app/immich","slug":"failed-to-verify-smtp-configuration-7e2237","errorCode":null,"errorMessage":"Failed to verify SMTP configuration","messagePattern":"Failed to verify SMTP configuration","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/notification.service.ts","lineNumber":254,"sourceCode":"    await this.jobRepository.queue({ name: JobName.NotifyAlbumInvite, data: { id, recipientId: userId, senderName } });\n  }\n\n  @OnEvent({ name: 'SessionDelete' })\n  onSessionDelete({ sessionId }: ArgOf<'SessionDelete'>) {\n    // after the response is sent\n    setTimeout(() => this.websocketRepository.clientSend('on_session_delete', sessionId, sessionId), 500);\n  }\n\n  async sendTestEmail(id: string, dto: SystemConfigSmtpDto, tempTemplate?: string) {\n    const user = await this.userRepository.get(id, { withDeleted: false });\n    if (!user) {\n      throw new Error('User not found');\n    }\n\n    try {\n      await this.emailRepository.verifySmtp(dto.transport);\n    } catch (error) {\n      throw new BadRequestException('Failed to verify SMTP configuration', { cause: error });\n    }\n\n    const { server } = await this.getConfig({ withCache: false });\n    const { html, text } = await this.emailRepository.renderEmail({\n      template: EmailTemplate.TEST_EMAIL,\n      data: {\n        baseUrl: getExternalDomain(server),\n        displayName: user.name,\n      },\n      customTemplate: tempTemplate!,\n    });\n    const { messageId } = await this.emailRepository.sendEmail({\n      to: user.email,\n      subject: 'Test email from Immich',\n      html,\n      text,\n      from: dto.from,\n      replyTo: dto.replyTo || dto.from,","sourceCodeStart":236,"sourceCodeEnd":272,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/notification.service.ts#L236-L272","documentation":"Thrown by NotificationService.sendTestEmail when emailRepository.verifySmtp(dto.transport) rejects. It is a BadRequestException with the original SMTP error attached via { cause }, so NestJS responds with HTTP 400 and the cause is preserved in logs. The SMTP verification opens a real connection to the configured transport before any email is rendered or sent.","triggerScenarios":"POST /system/preferences/notifications/test with a SystemConfigSmtpDto whose transport.host/port/username/password/security are wrong, unreachable, or whose credentials are rejected by the SMTP server.","commonSituations":"Typo in host/port, firewall blocking outbound SMTP (port 25/465/587), wrong auth credentials, STARTTLS vs SSL mismatch, self-signed cert rejected, Gmail requiring an app password, or the SMTP host not resolvable in the container's DNS.","solutions":["Check the `cause` field on the exception: it carries the upstream nodemailer error (EAUTH, ECONNECTION, EENVELOPE, etc.).","Verify host, port, and security setting (e.g. 465 with 'tls', 587 with 'starttls') against the provider's docs.","Confirm outbound network access and DNS resolution from the immich-server container.","Use provider-specific credentials (e.g. a Gmail App Password, not the account password).","Test the same transport with swaks or nodemailer directly from the host to isolate immich vs SMTP server issues."],"exampleFix":"// before\ntry {\n  await this.emailRepository.verifySmtp(dto.transport);\n} catch (error) {\n  throw new BadRequestException('Failed to verify SMTP configuration', { cause: error });\n}\n\n// after (surface the upstream code so callers can branch)\ntry {\n  await this.emailRepository.verifySmtp(dto.transport);\n} catch (error) {\n  throw new BadRequestException(`Failed to verify SMTP configuration: ${error.code ?? error.message}`, { cause: error });\n}","handlingStrategy":"try-catch","validationCode":"// Pre-flight: open a throwaway SMTP connection before persisting the config.\ntry {\n  await emailRepository.verifySmtp(dto.transport);\n} catch (e) {\n  // report the upstream code (EAUTH, ECONNECTION, ...) to the user\n  return { ok: false, code: e.code, message: e.message };\n}","typeGuard":"import { isIPv4 } from 'node:net';\n\nconst isLikelyValidTransport = (t: SmtpTransport): boolean =>\n  !!t.host && (typeof t.port === 'number' && t.port > 0 && t.port < 65536);\n\nif (!isLikelyValidTransport(dto.transport)) { /* reject early */ }","tryCatchPattern":"try {\n  await notificationService.sendTestEmail(id, dto);\n} catch (e) {\n  if (e instanceof BadRequestException && /SMTP/.test(e.message)) {\n    const upstream = (e as any).cause; // nodemailer error\n    // surface upstream.code to the operator\n  }\n  throw e;\n}","preventionTips":["Test SMTP credentials with swaks/nodemailer from the same host before entering them in Immich.","Use provider app passwords for OAuth-enabled providers (Gmail, Microsoft).","Whitelist the immich-server container's outbound IP on your SMTP relay.","Read the `cause` field to distinguish auth (EAUTH) vs network (ECONNECTION) failures."],"tags":["smtp","notification","email","network","nestjs","configuration"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}