From a0f23dcefe809cfbf33c9e416907a4411f507fff Mon Sep 17 00:00:00 2001 From: Iain Sproat <68657+iainsproat@users.noreply.github.com> Date: Thu, 11 Sep 2025 17:06:36 +0100 Subject: [PATCH] fix(server/email): emails configuration is secure by default but can be overridden (#5417) --- docker-compose-speckle.yml | 1 + packages/server/.env.example | 1 + .../modules/emails/utils/transporter.ts | 27 ++++++++++++---- .../modules/shared/helpers/envHelper.ts | 32 ++++++++++++++++--- .../speckle-server/templates/_helpers.tpl | 10 +++--- utils/helm/speckle-server/values.schema.json | 5 +++ utils/helm/speckle-server/values.yaml | 3 ++ 7 files changed, 63 insertions(+), 16 deletions(-) diff --git a/docker-compose-speckle.yml b/docker-compose-speckle.yml index 657a09be4..8dff4b04c 100644 --- a/docker-compose-speckle.yml +++ b/docker-compose-speckle.yml @@ -94,6 +94,7 @@ services: S3_REGION: '' # optional, defaults to 'us-east-1' FILE_SIZE_LIMIT_MB: 1000 EMAIL_FROM: 'no-reply@example.org' + EMAIL_SECURE: 'false' # If connecting to maildev server, do not use TLS FRONTEND_ORIGIN: 'http://127.0.0.1' ONBOARDING_STREAM_URL: 'https://latest.speckle.systems/projects/843d07eb10' diff --git a/packages/server/.env.example b/packages/server/.env.example index 1afdcc369..01584c39f 100644 --- a/packages/server/.env.example +++ b/packages/server/.env.example @@ -75,6 +75,7 @@ EMAIL=true EMAIL_HOST="127.0.0.1" EMAIL_FROM="no-reply@example.org" EMAIL_PORT="1025" +EMAIL_SECURE="false" # EMAIL_HOST="-> FILL IN <-" # EMAIL_PORT="-> FILL IN <-" diff --git a/packages/server/modules/emails/utils/transporter.ts b/packages/server/modules/emails/utils/transporter.ts index 8fdbfc733..805c9ad5e 100644 --- a/packages/server/modules/emails/utils/transporter.ts +++ b/packages/server/modules/emails/utils/transporter.ts @@ -1,6 +1,14 @@ import { emailLogger as logger } from '@/observability/logging' import { MisconfiguredEnvironmentError } from '@/modules/shared/errors' -import { isEmailEnabled, isTestEnv } from '@/modules/shared/helpers/envHelper' +import { + getEmailHost, + getEmailPassword, + getEmailPort, + getEmailUsername, + isEmailEnabled, + isSecureEmailEnabled, + isTestEnv +} from '@/modules/shared/helpers/envHelper' import type { Transporter } from 'nodemailer' import { createTransport } from 'nodemailer' @@ -11,18 +19,23 @@ const createJsonEchoTransporter = () => createTransport({ jsonTransport: true }) const initSmtpTransporter = async () => { try { const smtpTransporter = createTransport({ - host: process.env.EMAIL_HOST || '127.0.0.1', - port: parseInt(process.env.EMAIL_PORT || '587'), - secure: process.env.EMAIL_SECURE === 'true', + host: getEmailHost(), + port: getEmailPort(), + secure: isSecureEmailEnabled(), auth: { - user: process.env.EMAIL_USERNAME, - pass: process.env.EMAIL_PASSWORD + user: getEmailUsername(), + pass: getEmailPassword() }, pool: true, maxConnections: 20, maxMessages: Infinity }) - await smtpTransporter.verify() + const transporterVerified = await smtpTransporter.verify() + if (!transporterVerified) { + logger.error( + '📧 Email provider is likely misconfigured as validation failed, check config variables' + ) + } return smtpTransporter } catch (e) { logger.error(e, '📧 Email provider is misconfigured, check config variables.') diff --git a/packages/server/modules/shared/helpers/envHelper.ts b/packages/server/modules/shared/helpers/envHelper.ts index 22c6fbedc..b86595f34 100644 --- a/packages/server/modules/shared/helpers/envHelper.ts +++ b/packages/server/modules/shared/helpers/envHelper.ts @@ -7,14 +7,16 @@ import { ensureError } from '@speckle/shared' export function getStringFromEnv( envVarKey: string, options?: Partial<{ + default?: string /** * If set to true, wont throw if the env var is not set */ - unsafe: boolean + unsafe?: boolean }> ): string { const envVar = process.env[envVarKey] if (!envVar) { + if (options?.default !== undefined) return options.default if (options?.unsafe) return '' throw new MisconfiguredEnvironmentError(`${envVarKey} env var not configured`) } @@ -334,10 +336,6 @@ export function getOnboardingStreamCacheBustNumber() { return parseInt(val) || 1 } -export function getEmailFromAddress() { - return getStringFromEnv('EMAIL_FROM') -} - export function getMaximumProjectModelsPerPage() { return getIntFromEnv('MAX_PROJECT_MODELS_PER_PAGE', '500') } @@ -373,6 +371,30 @@ export function isEmailEnabled() { return getBooleanFromEnv('EMAIL') } +export function getEmailFromAddress() { + return getStringFromEnv('EMAIL_FROM') +} + +export function getEmailHost() { + return getStringFromEnv('EMAIL_HOST', { default: '127.0.0.1' }) +} + +export function getEmailPort() { + return getIntFromEnv('EMAIL_PORT', '587') +} + +export function isSecureEmailEnabled() { + return getBooleanFromEnv('EMAIL_SECURE', true) // default to secure +} + +export function getEmailUsername() { + return getStringFromEnv('EMAIL_USERNAME', { unsafe: true }) // can be empty +} + +export function getEmailPassword() { + return getStringFromEnv('EMAIL_PASSWORD', { unsafe: true }) // can be empty +} + export const getFileImporterQueuePostgresUrl = () => process.env['FILEIMPORT_QUEUE_POSTGRES_URL'] ?? null diff --git a/utils/helm/speckle-server/templates/_helpers.tpl b/utils/helm/speckle-server/templates/_helpers.tpl index d3967949a..097b3af1b 100644 --- a/utils/helm/speckle-server/templates/_helpers.tpl +++ b/utils/helm/speckle-server/templates/_helpers.tpl @@ -971,18 +971,20 @@ Generate the environment variables for Speckle server and Speckle objects deploy - name: EMAIL value: "true" - name: EMAIL_HOST - value: "{{ .Values.server.email.host }}" + value: {{ .Values.server.email.host | quote }} - name: EMAIL_PORT - value: "{{ .Values.server.email.port }}" + value: {{ .Values.server.email.port | quote }} - name: EMAIL_USERNAME - value: "{{ .Values.server.email.username }}" + value: {{ .Values.server.email.username | quote }} - name: EMAIL_PASSWORD valueFrom: secretKeyRef: name: {{ default .Values.secretName .Values.server.email.password.secretName }} key: {{ default "email_password" .Values.server.email.password.secretKey }} - name: EMAIL_FROM - value: "{{ .Values.server.email.from }}" + value: {{ .Values.server.email.from | quote }} +- name: EMAIL_SECURE + value: {{ .Values.server.email.secure | quote }} - name: EMAIL_VERIFICATION_TIMEOUT_MINUTES value: {{ .Values.server.email.verificationTimeoutMinutes | quote }} {{- end }} diff --git a/utils/helm/speckle-server/values.schema.json b/utils/helm/speckle-server/values.schema.json index 4388fae3b..bb25fc0aa 100644 --- a/utils/helm/speckle-server/values.schema.json +++ b/utils/helm/speckle-server/values.schema.json @@ -1307,6 +1307,11 @@ } } }, + "secure": { + "type": "boolean", + "description": "If true, will use TLS when connecting to the email server", + "default": true + }, "networkPolicy": { "type": "object", "properties": { diff --git a/utils/helm/speckle-server/values.yaml b/utils/helm/speckle-server/values.yaml index 50d1cc587..7cb0c8935 100644 --- a/utils/helm/speckle-server/values.yaml +++ b/utils/helm/speckle-server/values.yaml @@ -800,6 +800,9 @@ server: ## @param server.email.password.secretKey The key within the Kubernetes Secret holding the email password as its value. ## secretKey: '' + ## @param server.email.secure If true, will use TLS when connecting to the email server + ## + secure: true ## @extra server.email.networkPolicy If networkPolicy is enabled for Speckle server, this provides the Network Policy with the necessary details to allow egress connections to the email server ## networkPolicy: