fix(email): test saved SMTP config and add TLS modes (#3388)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-11 13:30:00 +02:00
1 parent 45b78a13c2
commit cca8227ca6
66 files changed
+2464 -319

No files matched your search

@@ -41,6 +41,7 @@ import {
ServicesStep,
SignInMethodStep,
ThemeStep,
toSmtpTlsMode,
WelcomeStep,
} from '@app/features/app/components/setup/SetupWizardSteps';
import {resolveAppShellBranding} from '@app/features/app/state/AppShellBranding';
@@ -80,6 +81,7 @@ import {
TagStyles,
} from '@fluxer/constants/src/AccountIdentityConstants';
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
import type {SmtpTlsMode} from '@fluxer/constants/src/SmtpConstants';
import {type ThemeType, ThemeTypes} from '@fluxer/constants/src/UserConstants';
import type {InstanceConfigResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import type {MessageDescriptor} from '@lingui/core';
@@ -199,7 +201,7 @@ const DEFAULT_INTEGRATION_DRAFT: ServiceIntegrationDraft = {
smtpPort: '587',
smtpUsername: '',
smtpPassword: '',
smtpSecure: true,
smtpTlsMode: 'auto',
blueskyMode: 'later',
blueskyEnabled: true,
blueskyClientName: '',
@@ -345,7 +347,7 @@ function buildIntegrationsPatch(draft: ServiceIntegrationDraft, accountIdentity:
port: number;
username: string;
password: string;
secure: boolean;
tls_mode: SmtpTlsMode;
};
};
bluesky?: {
@@ -365,6 +367,7 @@ function buildIntegrationsPatch(draft: ServiceIntegrationDraft, accountIdentity:
integrations.youtube = {api_key: draft.youtubeApiKey.trim()};
}
if (draft.emailMode === 'configure' && accountIdentity === AccountIdentityModes.EMAIL) {
const smtpPort = parsePort(draft.smtpPort) ?? 587;
integrations.email = {
enabled: draft.emailEnabled,
provider: 'smtp',
@@ -372,10 +375,10 @@ function buildIntegrationsPatch(draft: ServiceIntegrationDraft, accountIdentity:
from_name: draft.emailFromName.trim(),
smtp: {
host: draft.smtpHost.trim(),
port: parsePort(draft.smtpPort) ?? 587,
port: smtpPort,
username: draft.smtpUsername.trim(),
password: draft.smtpPassword.trim(),
secure: draft.smtpSecure,
tls_mode: toSmtpTlsMode(draft.smtpTlsMode, smtpPort),
},
};
}
@@ -599,7 +602,7 @@ export const SelfHostedSetupWizardGate = observer(() => {
smtpHost: next.integrations.email.smtp.host ?? current.smtpHost,
smtpPort: next.integrations.email.smtp.port ? String(next.integrations.email.smtp.port) : current.smtpPort,
smtpUsername: next.integrations.email.smtp.username ?? current.smtpUsername,
smtpSecure: next.integrations.email.smtp.secure ?? current.smtpSecure,
smtpTlsMode: next.integrations.email.smtp.tls_mode ?? current.smtpTlsMode,
blueskyEnabled: next.integrations.bluesky.effective_enabled || next.integrations.bluesky.enabled !== false,
blueskyClientName: next.integrations.bluesky.client_name ?? current.blueskyClientName,
blueskyClientUri: next.integrations.bluesky.client_uri ?? current.blueskyClientUri,
@@ -864,7 +867,7 @@ export const SelfHostedSetupWizardGate = observer(() => {
port,
username: integrationDraft.smtpUsername.trim(),
password: integrationDraft.smtpPassword,
secure: integrationDraft.smtpSecure,
tls_mode: toSmtpTlsMode(integrationDraft.smtpTlsMode, port),
});
setSmtpTestResult(result.ok ? 'ok' : (result.error ?? 'failed'));
} catch (error) {
@@ -22,6 +22,7 @@ import {
type TagStyle,
TagStyles,
} from '@fluxer/constants/src/AccountIdentityConstants';
import {resolveSmtpTlsMode, type SmtpTlsMode, SmtpTlsModes} from '@fluxer/constants/src/SmtpConstants';
import type {ThemeType} from '@fluxer/constants/src/UserConstants';
import type {MessageDescriptor} from '@lingui/core';
import {msg} from '@lingui/core/macro';
@@ -1046,7 +1047,7 @@ export interface ServiceIntegrationDraft {
smtpPort: string;
smtpUsername: string;
smtpPassword: string;
smtpSecure: boolean;
smtpTlsMode: SmtpTlsModeChoice;
blueskyMode: IntegrationSetupMode;
blueskyEnabled: boolean;
blueskyClientName: string;
@@ -1123,9 +1124,49 @@ const SMTP_HOST_LABEL_DESCRIPTOR = msg({message: 'SMTP host', comment: 'Label fo
const SMTP_PORT_LABEL_DESCRIPTOR = msg({message: 'Port', comment: 'Label for SMTP port input.'});
const SMTP_USERNAME_LABEL_DESCRIPTOR = msg({message: 'Username', comment: 'Label for SMTP username input.'});
const SMTP_PASSWORD_LABEL_DESCRIPTOR = msg({message: 'Password', comment: 'Label for SMTP password input.'});
const SMTP_SECURE_LABEL_DESCRIPTOR = msg({
message: 'Use TLS',
comment: 'Label for SMTP TLS toggle.',
const SMTP_TLS_MODE_LABEL_DESCRIPTOR = msg({
message: 'Encryption',
comment: 'Label for the choice of how the SMTP connection is encrypted.',
});
const SMTP_TLS_MODE_AUTO_NAME_DESCRIPTOR = msg({
message: 'Automatic',
comment: 'SMTP encryption option that picks the mode from the port.',
});
const SMTP_TLS_MODE_AUTO_DESC_DESCRIPTOR = msg({
message: 'Implicit TLS on port 465, required STARTTLS on any other port.',
comment: 'Description of the automatic SMTP encryption option.',
});
const SMTP_TLS_MODE_IMPLICIT_NAME_DESCRIPTOR = msg({
message: 'Implicit TLS',
comment: 'SMTP encryption option where the connection starts encrypted.',
});
const SMTP_TLS_MODE_IMPLICIT_DESC_DESCRIPTOR = msg({
message: 'The connection is encrypted from the start. Usually port 465.',
comment: 'Description of the implicit TLS SMTP encryption option.',
});
const SMTP_TLS_MODE_STARTTLS_NAME_DESCRIPTOR = msg({
message: 'STARTTLS',
comment: 'SMTP encryption option where a plain connection must upgrade with STARTTLS.',
});
const SMTP_TLS_MODE_STARTTLS_DESC_DESCRIPTOR = msg({
message: 'The connection must upgrade to TLS before signing in. Usually port 587.',
comment: 'Description of the required STARTTLS SMTP encryption option.',
});
const SMTP_TLS_MODE_OPPORTUNISTIC_NAME_DESCRIPTOR = msg({
message: 'STARTTLS when offered',
comment: 'SMTP encryption option where STARTTLS is used only if the server offers it.',
});
const SMTP_TLS_MODE_OPPORTUNISTIC_DESC_DESCRIPTOR = msg({
message: 'The connection upgrades to TLS only if the server offers it.',
comment: 'Description of the opportunistic STARTTLS SMTP encryption option.',
});
const SMTP_TLS_MODE_NONE_NAME_DESCRIPTOR = msg({
message: 'None',
comment: 'SMTP encryption option that never uses TLS.',
});
const SMTP_TLS_MODE_NONE_DESC_DESCRIPTOR = msg({
message: 'The connection is never encrypted. Only for a mail server on a network you trust.',
comment: 'Description of the SMTP encryption option that never uses TLS.',
});
const SMTP_TEST_DESCRIPTOR = msg({
message: 'Test SMTP',
@@ -1172,6 +1213,44 @@ const BLUESKY_PRIVATE_KEY_LABEL_DESCRIPTOR = msg({
comment: 'Label for Bluesky OAuth private key input.',
});
export type SmtpTlsModeChoice = SmtpTlsMode | 'auto';
export function toSmtpTlsMode(choice: SmtpTlsModeChoice, port: number): SmtpTlsMode {
return choice === 'auto' ? resolveSmtpTlsMode(null, port) : choice;
}
function buildSmtpTlsModeOptions(
i18n: ReturnType<typeof useLingui>['i18n'],
): ReadonlyArray<RadioOption<SmtpTlsModeChoice>> {
return [
{
value: 'auto',
name: i18n._(SMTP_TLS_MODE_AUTO_NAME_DESCRIPTOR),
desc: i18n._(SMTP_TLS_MODE_AUTO_DESC_DESCRIPTOR),
},
{
value: SmtpTlsModes.IMPLICIT,
name: i18n._(SMTP_TLS_MODE_IMPLICIT_NAME_DESCRIPTOR),
desc: i18n._(SMTP_TLS_MODE_IMPLICIT_DESC_DESCRIPTOR),
},
{
value: SmtpTlsModes.STARTTLS,
name: i18n._(SMTP_TLS_MODE_STARTTLS_NAME_DESCRIPTOR),
desc: i18n._(SMTP_TLS_MODE_STARTTLS_DESC_DESCRIPTOR),
},
{
value: SmtpTlsModes.OPPORTUNISTIC,
name: i18n._(SMTP_TLS_MODE_OPPORTUNISTIC_NAME_DESCRIPTOR),
desc: i18n._(SMTP_TLS_MODE_OPPORTUNISTIC_DESC_DESCRIPTOR),
},
{
value: SmtpTlsModes.NONE,
name: i18n._(SMTP_TLS_MODE_NONE_NAME_DESCRIPTOR),
desc: i18n._(SMTP_TLS_MODE_NONE_DESC_DESCRIPTOR),
},
];
}
function buildIntegrationModeOptions(
i18n: ReturnType<typeof useLingui>['i18n'],
): ReadonlyArray<RadioOption<IntegrationSetupMode>> {
@@ -1490,12 +1569,19 @@ export const IntegrationStep = observer(
disabled={disabled}
data-flx="app.setup.setup-wizard-steps.integration-step.input.draft-change.password--2"
/>
<Switch
label={i18n._(SMTP_SECURE_LABEL_DESCRIPTOR)}
value={draft.smtpSecure}
onChange={(value) => onDraftChange({smtpSecure: value})}
<div
className={styles.fieldLabel}
data-flx="app.setup.setup-wizard-steps.integration-step.smtp-tls-mode-label"
>
{i18n._(SMTP_TLS_MODE_LABEL_DESCRIPTOR)}
</div>
<RadioGroup
options={buildSmtpTlsModeOptions(i18n)}
value={draft.smtpTlsMode}
onChange={(value) => onDraftChange({smtpTlsMode: value})}
disabled={disabled}
data-flx="app.setup.setup-wizard-steps.integration-step.switch.draft-change--2"
aria-label={i18n._(SMTP_TLS_MODE_LABEL_DESCRIPTOR)}
data-flx="app.setup.setup-wizard-steps.integration-step.radio-group.smtp-tls-mode"
/>
<div
className={styles.integrationActionRow}