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
- Inspect the cause/log line 'Failed to validate SMTP configuration' for the underlying SMTP error and fix that (host, port, credentials, TLS)
- Re-enter credentials carefully; use an app password where required
- Match security mode to port (465 implicit TLS, 587 STARTTLS)
- 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
- Run an SMTP verify (e.g. nodemailer transport.verify()) before saving config changes
- Keep the previous working transport handy to roll back if validation fails
- Test connectivity from the server host, not your workstation
- Document port/security-mode pairings to avoid TLS/STARTTLS mismatches
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
- Failed to verify SMTP configuration
- Failed to verify SMTP configuration
- Email is not available
- Email is not available
- Email is not available
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)