immich-app/immich · error · Error

Invalid SMTP configuration

Error message

Invalid SMTP configuration

What it means

Thrown by NotificationService.onConfigValidate when the server config change event detects a modified notifications.smtp section and the new SMTP transport fails verifySmtp. The config validation event therefore fails with 'Invalid SMTP configuration', with the underlying SMTP error attached as cause, preventing the config change from being accepted.

Solutions

  1. Inspect the cause/log line 'Failed to validate SMTP configuration' for the underlying SMTP error and fix that (host, port, credentials, TLS)
  2. Re-enter credentials carefully; use an app password where required
  3. Match security mode to port (465 implicit TLS, 587 STARTTLS)
  4. Confirm outbound SMTP connectivity from the server host/container, then resubmit the config

Example fix

// before
notifications.smtp.transport = { host: 'mail.example.com', port: 465, secure: false } // plaintext on implicit-TLS port
// after
notifications.smtp.transport = { host: 'mail.example.com', port: 465, secure: true }
Defensive patterns

Strategy: validation

Validate before calling

// Before saving system config, verify the new SMTP transport yourself
import nodemailer from 'nodemailer';
const t = nodemailer.createTransport(newTransport);
await t.verify(); // throws with the same underlying error verifySmtp would
await api.systemConfigApi.updateConfig({ notifications: { smtp: { ...smtp, transport: newTransport } } });

Type guard

function smtpTransportChanged(oldCfg, newCfg) {
  return !!newCfg?.notifications?.smtp?.transport &&
    JSON.stringify(oldCfg?.notifications?.smtp?.transport) !== JSON.stringify(newCfg.notifications.smtp.transport);
}

Try / catch

try {
  await api.systemConfigApi.updateConfig(newConfig);
} catch (e) {
  if (String(e.message).includes('Invalid SMTP configuration')) {
    console.error('Config rejected; SMTP cause:', e.cause ?? e.response?.data); // correct transport then resubmit
  } else throw e;
}

Prevention

When it happens

Trigger: Saving system settings (PUT /system-config) where newConfig.notifications.smtp differs from the old config and the new transport cannot connect/authenticate (bad host, port, credentials, TLS mode).

Common situations: Admin edits SMTP settings in the Immich admin UI and mistypes the host or password; switching between TLS/STARTTLS ports; migrating to a new mail provider without updating credentials; firewall changes after an infra move.

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/2c8c062506606a65. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/notification.service.ts:137

    this.websocketRepository.clientBroadcast('AppRestartV1', {
      isMaintenanceMode: state.isMaintenanceMode,
    });

    this.websocketRepository.serverSend('AppRestart', state);
  }

  @OnEvent({ name: 'ConfigValidate', priority: -100 })
  async onConfigValidate({ oldConfig, newConfig }: ArgOf<'ConfigValidate'>) {
    try {
      if (
        newConfig.notifications.smtp.enabled &&
        !isEqualObject(oldConfig.notifications.smtp, newConfig.notifications.smtp)
      ) {
        await this.emailRepository.verifySmtp(newConfig.notifications.smtp.transport);
      }
    } catch (error: Error | any) {
      this.logger.error(`Failed to validate SMTP configuration: ${error}`, error?.stack);
      throw new Error('Invalid SMTP configuration', { cause: error });
    }
  }

  @OnEvent({ name: 'AssetHide' })
  onAssetHide({ assetId, userId }: ArgOf<'AssetHide'>) {
    this.websocketRepository.clientSend('on_asset_hidden', userId, assetId);
  }

  @OnEvent({ name: 'AssetShow' })
  async onAssetShow({ assetId }: ArgOf<'AssetShow'>) {
    await this.jobRepository.queue({ name: JobName.AssetGenerateThumbnails, data: { id: assetId, notify: true } });
  }

  @OnEvent({ name: 'AssetTrash' })
  onAssetTrash({ assetId, userId }: ArgOf<'AssetTrash'>) {
    this.websocketRepository.clientSend('on_asset_trash', userId, [assetId]);
  }

View on GitHub (pinned to e55ac299a4)