immich-app/immich · error · BadRequestException

Failed to verify SMTP configuration

Error message

Failed to verify SMTP configuration

What it means

Thrown by NotificationAdminService.sendTestEmail when the SMTP transport supplied in the test-email request fails verifySmtp, meaning Immich could not establish/authenticate an SMTP session with the provided configuration. The original transport error is attached as the exception cause. The test email is aborted before rendering.

Solutions

  1. Read the `cause` of the error for the underlying SMTP failure (auth failed, connection refused, TLS error) and fix that specific issue
  2. Confirm host, port, and security mode match your provider (e.g. 465 = TLS, 587 = STARTTLS)
  3. Test credentials by logging into the mail provider; use an app password if 2FA is enabled
  4. Verify the Immich server container can reach the SMTP host (network/firewall/DNS)

Example fix

// before
{ "transport": { "host": "smtp.gmail.com", "port": 587, "secure": true } } // secure:true on 587 fails handshake
// after
{ "transport": { "host": "smtp.gmail.com", "port": 587, "secure": false } } // STARTTLS on submission port
Defensive patterns

Strategy: try-catch

Validate before calling

const net = require('net');
const reachable = await new Promise(res => {
  const s = net.connect(transport.port, transport.host);
  s.once('connect', () => { s.end(); res(true); });
  s.once('error', () => res(false));
  s.setTimeout(5000, () => { s.destroy(); res(false); });
});
if (!reachable) throw new Error(`Cannot reach SMTP host ${transport.host}:${transport.port}`);

Type guard

function hasValidSmtpTransport(t) {
  return !!t && typeof t.host === 'string' && t.host.length > 0 && Number.isInteger(t.port) && t.port > 0;
}

Try / catch

try {
  await api.notificationsApi.sendTestEmail(transport);
} catch (e) {
  if (String(e.message).includes('Failed to verify SMTP configuration')) {
    console.error('SMTP verify failed:', e.cause ?? e.response?.data); // inspect underlying cause
  } else throw e;
}

Prevention

When it happens

Trigger: Calling the admin test-email endpoint (POST /notifications/admin/test-email) with dto.transport whose host/port/credentials are wrong, the server is unreachable, TLS settings mismatch, or authentication fails during verifySmtp.

Common situations: Typo in SMTP host or port; using port 465 with STARTTLS (or 587 with implicit TLS) so the handshake fails; wrong username/password or OAuth settings; corporate firewall blocking outbound SMTP; mail provider requiring an app password.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/5d6cb3ddbc75574c. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/notification-admin.service.ts:34

      level: dto.level ?? NotificationLevel.Info,
      title: dto.title,
      description: dto.description,
      data: dto.data,
    });

    return mapNotification(item);
  }

  async sendTestEmail(id: string, dto: SystemConfigSmtpDto, tempTemplate?: string) {
    const user = await this.userRepository.get(id, { withDeleted: false });
    if (!user) {
      throw new Error('User not found');
    }

    try {
      await this.emailRepository.verifySmtp(dto.transport);
    } catch (error) {
      throw new BadRequestException('Failed to verify SMTP configuration', { cause: error });
    }

    const { server } = await this.getConfig({ withCache: false });
    const { html, text } = await this.emailRepository.renderEmail({
      template: EmailTemplate.TEST_EMAIL,
      data: {
        baseUrl: getExternalDomain(server),
        displayName: user.name,
      },
      customTemplate: tempTemplate!,
    });
    const { messageId } = await this.emailRepository.sendEmail({
      to: user.email,
      subject: 'Test email from Immich',
      html,
      text,
      from: dto.from,
      replyTo: dto.replyTo || dto.from,

View on GitHub (pinned to e55ac299a4)