UBERF-9712: Improve mail TLS settings and logs for self hosters (#8399)

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artyom Savchenko 2025-03-31 14:23:13 +07:00 committed by GitHub
parent f9d6ef0621
commit d994ed202d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 116 additions and 6 deletions

View File

@ -17,6 +17,12 @@ SMTP settings:
- `SMTP_PORT`: Port number of the SMTP server.
- `SMTP_USERNAME`: Username for authenticating with the SMTP server. Refer to your SMTP server documentation for the appropriate format.
- `SMTP_PASSWORD`: Password for authenticating with the SMTP server. Refer to your SMTP server documentation for the appropriate format.
- `SMTP_TLS_MODE` (Optional): TLS mode for SMTP connection. Default: 'upgrade'. Possible values:
- `secure`: Always use TLS (implicit TLS)
- `upgrade`: Start unencrypted, upgrade to TLS if supported (STARTTLS)
- `ignore`: Do not use TLS (not recommended for production use)
- `SMTP_DEBUG_LOG` (Optional): Enable debug logging for SMTP connection. Set to 'true' to enable. Default: false
- `SMTP_ALLOW_SELF_SIGNED` (Optional): Allow self-signed certificates for TLS connections. Set to 'true' to enable (not recommended for production use). Default: false
SES settings:
- `SES_ACCESS_KEY`: AWS SES access key for authentication.

View File

@ -45,7 +45,50 @@ describe('Config', () => {
Host: 'smtp.example.com',
Port: 587,
Username: 'user',
Password: undefined
Password: undefined,
TlsMode: 'upgrade',
DebugLog: false,
AllowSelfSigned: false
})
})
test('should properly configure TLS settings', () => {
process.env.PORT = '1025'
process.env.SMTP_HOST = 'smtp.example.com'
process.env.SMTP_PORT = '587'
process.env.SMTP_TLS_MODE = 'secure'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { default: config, getTlsSettings } = require('../config')
const tlsSettings = getTlsSettings(config.smtpConfig)
expect(tlsSettings).toEqual({
secure: true,
ignoreTLS: false
})
})
test('should handle debug and self-signed certificate settings', () => {
process.env.PORT = '1025'
process.env.SMTP_HOST = 'smtp.example.com'
process.env.SMTP_PORT = '587'
process.env.SMTP_DEBUG_LOG = 'true'
process.env.SMTP_ALLOW_SELF_SIGNED = 'true'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { default: config, getTlsSettings } = require('../config')
expect(config.smtpConfig).toMatchObject({
DebugLog: true,
AllowSelfSigned: true
})
const tlsSettings = getTlsSettings(config.smtpConfig)
expect(tlsSettings).toEqual({
secure: false,
ignoreTLS: false,
tls: {
rejectUnauthorized: false
}
})
})
@ -81,4 +124,13 @@ describe('Config', () => {
expect(() => require('../config')).toThrow('Both SMTP and SES configuration are specified, please specify only one')
})
test('should throw an error if invalid TLS mode is provided', () => {
process.env.PORT = '1025'
process.env.SMTP_HOST = 'smtp.example.com'
process.env.SMTP_PORT = '587'
process.env.SMTP_TLS_MODE = 'invalid'
expect(() => require('../config')).toThrow('Invalid SMTP_TLS_MODE value. Must be one of: secure, upgrade, ignore')
})
})

View File

@ -29,11 +29,41 @@ export interface SesConfig {
Region: string
}
export enum TlsOptions {
SECURE = 'secure', // Always use TLS (implicit TLS)
UPGRADE = 'upgrade', // Start unencrypted, upgrade to TLS if supported (STARTTLS)
IGNORE = 'ignore' // Do not use TLS (not recommended for production use)
}
export interface SmtpConfig {
Host: string
Port: number
Username: string | undefined
Password: string | undefined
TlsMode: TlsOptions
DebugLog?: boolean
AllowSelfSigned?: boolean
}
export interface TlsSettings {
secure: boolean
ignoreTLS: boolean
tls?: {
rejectUnauthorized: boolean
}
}
export function getTlsSettings (config: SmtpConfig): TlsSettings {
const tlsConfig: TlsSettings = {
secure: config.TlsMode === TlsOptions.SECURE || config.Port === 465,
ignoreTLS: config.TlsMode === TlsOptions.IGNORE
}
if (config.AllowSelfSigned === true) {
tlsConfig.tls = {
rejectUnauthorized: false
}
}
return tlsConfig
}
const envMap = {
@ -46,12 +76,25 @@ const envMap = {
SmtpHost: 'SMTP_HOST',
SmtpPort: 'SMTP_PORT',
SmtpUsername: 'SMTP_USERNAME',
SmtpPassword: 'SMTP_PASSWORD'
SmtpPassword: 'SMTP_PASSWORD',
SmtpTlsMode: 'SMTP_TLS_MODE', // TLS mode, see TlsOptions for possible values
SmtpDebugLog: 'SMTP_DEBUG_LOG', // Enable debug logging for SMTP
SmtpAllowSelfSigned: 'SMTP_ALLOW_SELF_SIGNED' // Allow self-signed certificates (not recommended for production use)
}
const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined)
const isEmpty = (str: string | undefined): boolean => str === undefined || str.trim().length === 0
const normalizeTlsMode = (mode: string | undefined): TlsOptions | undefined => {
if (mode === undefined || mode === '') return undefined
const normalized = mode.toLowerCase()
const value: TlsOptions | undefined = Object.values(TlsOptions).find((opt) => opt.toLowerCase() === normalized)
if (value === undefined) {
throw Error('Invalid SMTP_TLS_MODE value. Must be one of: secure, upgrade, ignore')
}
return value
}
const buildSesConfig = (): SesConfig => {
const accessKey = process.env[envMap.SesAccessKey]
const secretKey = process.env[envMap.SesSecretKey]
@ -78,10 +121,12 @@ const buildSmtpConfig = (): SmtpConfig => {
const port = parseNumber(process.env[envMap.SmtpPort])
const username = process.env[envMap.SmtpUsername]
const password = process.env[envMap.SmtpPassword]
const tlsMode = normalizeTlsMode(process.env[envMap.SmtpTlsMode])
const debugLog = process.env[envMap.SmtpDebugLog]?.toLowerCase() === 'true'
const allowSelfSigned = process.env[envMap.SmtpAllowSelfSigned]?.toLowerCase() === 'true'
if (isEmpty(host) || port === undefined) {
const missingKeys = [isEmpty(host) && 'SMTP_HOST', port === undefined && 'SMTP_PORT'].filter(Boolean)
throw Error(`Missing env variables for SMTP configuration: ${missingKeys.join(', ')}`)
}
@ -89,7 +134,10 @@ const buildSmtpConfig = (): SmtpConfig => {
Host: host as string,
Port: port,
Username: username,
Password: password
Password: password,
TlsMode: tlsMode ?? TlsOptions.UPGRADE,
DebugLog: debugLog,
AllowSelfSigned: allowSelfSigned
}
}

View File

@ -15,7 +15,7 @@
import nodemailer, { type Transporter } from 'nodemailer'
import aws from '@aws-sdk/client-ses'
import type { Config, SmtpConfig, SesConfig } from './config'
import { type Config, type SmtpConfig, type SesConfig, getTlsSettings } from './config'
function smtp (config: SmtpConfig): Transporter {
const auth =
@ -25,10 +25,14 @@ function smtp (config: SmtpConfig): Transporter {
pass: config.Password
}
: undefined
const tlsSettings = getTlsSettings(config)
return nodemailer.createTransport({
host: config.Host,
port: config.Port,
auth
auth,
logger: true,
debug: config.DebugLog,
...tlsSettings
})
}