diff --git a/services/mail/pod-mail/README.md b/services/mail/pod-mail/README.md index 246b9bd49c..e28c86effa 100644 --- a/services/mail/pod-mail/README.md +++ b/services/mail/pod-mail/README.md @@ -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. diff --git a/services/mail/pod-mail/src/__tests__/config.test.ts b/services/mail/pod-mail/src/__tests__/config.test.ts index 380fb8ec3e..6c9ce9271b 100644 --- a/services/mail/pod-mail/src/__tests__/config.test.ts +++ b/services/mail/pod-mail/src/__tests__/config.test.ts @@ -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') + }) }) diff --git a/services/mail/pod-mail/src/config.ts b/services/mail/pod-mail/src/config.ts index bea520967a..f0c7319d98 100644 --- a/services/mail/pod-mail/src/config.ts +++ b/services/mail/pod-mail/src/config.ts @@ -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 } } diff --git a/services/mail/pod-mail/src/transport.ts b/services/mail/pod-mail/src/transport.ts index 32f26f94c2..3433be3eee 100644 --- a/services/mail/pod-mail/src/transport.ts +++ b/services/mail/pod-mail/src/transport.ts @@ -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 }) }