fix: support SMTP STARTTLS mode and NTLM auth (#5426)

* fix: support SMTP STARTTLS mode and NTLM auth

Add explicit SMTP STARTTLS configuration for 587-style connections and keep SSL/TLS as the implicit TLS mode.

Prefer PLAIN when advertised, keep LOGIN compatibility, and add NTLM as a fallback for Exchange SMTP servers that require it after STARTTLS.

* fix: respect explicit SMTP encryption mode

* fix: preserve SMTP TLS compatibility
This commit is contained in:
Benson Yan
2026-06-24 12:45:39 +08:00
committed by GitHub
parent 9fc9c8f1e3
commit 2f23a66733
27 changed files with 959 additions and 52 deletions
@@ -30,6 +30,8 @@ import {
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Switch } from '@/components/ui/switch'
import {
SettingsForm,
@@ -57,6 +59,8 @@ const createEmailSchema = (t: (key: string) => string) =>
}, t('Enter a valid email or leave blank')),
SMTPToken: z.string(),
SMTPSSLEnabled: z.boolean(),
SMTPStartTLSEnabled: z.boolean(),
SMTPInsecureSkipVerify: z.boolean(),
SMTPForceAuthLogin: z.boolean(),
})
@@ -66,6 +70,17 @@ type EmailSettingsSectionProps = {
defaultValues: EmailFormValues
}
type SmtpSecurityMode = 'none' | 'ssl_tls' | 'starttls'
function getSmtpSecurityMode(values: {
SMTPSSLEnabled: boolean
SMTPStartTLSEnabled: boolean
}): SmtpSecurityMode {
if (values.SMTPSSLEnabled) return 'ssl_tls'
if (values.SMTPStartTLSEnabled) return 'starttls'
return 'none'
}
export function EmailSettingsSection({
defaultValues,
}: EmailSettingsSectionProps) {
@@ -81,13 +96,16 @@ export function EmailSettingsSection({
useResetForm(form, defaultValues)
const onSubmit = async (values: EmailFormValues) => {
const securityMode = getSmtpSecurityMode(values)
const sanitized = {
SMTPServer: values.SMTPServer.trim(),
SMTPPort: values.SMTPPort.trim(),
SMTPAccount: values.SMTPAccount.trim(),
SMTPFrom: values.SMTPFrom.trim(),
SMTPToken: values.SMTPToken.trim(),
SMTPSSLEnabled: values.SMTPSSLEnabled,
SMTPSSLEnabled: securityMode === 'ssl_tls',
SMTPStartTLSEnabled: securityMode === 'starttls',
SMTPInsecureSkipVerify: values.SMTPInsecureSkipVerify,
SMTPForceAuthLogin: values.SMTPForceAuthLogin,
}
@@ -98,6 +116,8 @@ export function EmailSettingsSection({
SMTPFrom: defaultValues.SMTPFrom.trim(),
SMTPToken: defaultValues.SMTPToken.trim(),
SMTPSSLEnabled: defaultValues.SMTPSSLEnabled,
SMTPStartTLSEnabled: defaultValues.SMTPStartTLSEnabled,
SMTPInsecureSkipVerify: defaultValues.SMTPInsecureSkipVerify,
SMTPForceAuthLogin: defaultValues.SMTPForceAuthLogin,
}
@@ -130,6 +150,20 @@ export function EmailSettingsSection({
})
}
if (sanitized.SMTPStartTLSEnabled !== initial.SMTPStartTLSEnabled) {
updates.push({
key: 'SMTPStartTLSEnabled',
value: sanitized.SMTPStartTLSEnabled,
})
}
if (sanitized.SMTPInsecureSkipVerify !== initial.SMTPInsecureSkipVerify) {
updates.push({
key: 'SMTPInsecureSkipVerify',
value: sanitized.SMTPInsecureSkipVerify,
})
}
if (sanitized.SMTPForceAuthLogin !== initial.SMTPForceAuthLogin) {
updates.push({
key: 'SMTPForceAuthLogin',
@@ -197,15 +231,78 @@ export function EmailSettingsSection({
)}
/>
<FormItem>
<FormLabel>{t('SMTP encryption')}</FormLabel>
<FormControl>
<RadioGroup
value={getSmtpSecurityMode({
SMTPSSLEnabled: form.watch('SMTPSSLEnabled'),
SMTPStartTLSEnabled: form.watch('SMTPStartTLSEnabled'),
})}
onValueChange={(value) => {
const mode = value as SmtpSecurityMode
form.setValue('SMTPSSLEnabled', mode === 'ssl_tls', {
shouldDirty: true,
})
form.setValue('SMTPStartTLSEnabled', mode === 'starttls', {
shouldDirty: true,
})
}}
className='gap-3'
>
<div className='flex items-center gap-2'>
<RadioGroupItem value='none' id='smtp-security-none' />
<Label
htmlFor='smtp-security-none'
className='cursor-pointer font-normal'
>
{t('No encryption')}
</Label>
</div>
<div className='flex items-center gap-2'>
<RadioGroupItem
value='ssl_tls'
id='smtp-security-ssl-tls'
/>
<Label
htmlFor='smtp-security-ssl-tls'
className='cursor-pointer font-normal'
>
{t('SSL/TLS')}
</Label>
</div>
<div className='flex items-center gap-2'>
<RadioGroupItem
value='starttls'
id='smtp-security-starttls'
/>
<Label
htmlFor='smtp-security-starttls'
className='cursor-pointer font-normal'
>
{t('STARTTLS')}
</Label>
</div>
</RadioGroup>
</FormControl>
<FormDescription>
{t('Choose one SMTP transport security mode')}
</FormDescription>
</FormItem>
<FormField
control={form.control}
name='SMTPSSLEnabled'
name='SMTPInsecureSkipVerify'
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{t('Enable SSL/TLS')}</FormLabel>
<FormLabel>
{t('Skip SMTP TLS certificate verification')}
</FormLabel>
<FormDescription>
{t('Use secure connection when sending emails')}
{t(
'Allow self-signed or hostname-mismatched SMTP certificates'
)}
</FormDescription>
</SettingsSwitchContent>
<FormControl>
@@ -36,6 +36,8 @@ const defaultOperationsSettings: OperationsSettings = {
SMTPFrom: '',
SMTPToken: '',
SMTPSSLEnabled: false,
SMTPStartTLSEnabled: false,
SMTPInsecureSkipVerify: false,
SMTPForceAuthLogin: false,
WorkerUrl: '',
WorkerValidKey: '',
@@ -71,6 +71,8 @@ const OPERATIONS_SECTIONS = [
SMTPFrom: settings.SMTPFrom,
SMTPToken: settings.SMTPToken,
SMTPSSLEnabled: settings.SMTPSSLEnabled,
SMTPStartTLSEnabled: settings.SMTPStartTLSEnabled,
SMTPInsecureSkipVerify: settings.SMTPInsecureSkipVerify,
SMTPForceAuthLogin: settings.SMTPForceAuthLogin,
}}
/>
+2
View File
@@ -334,6 +334,8 @@ export type OperationsSettings = {
SMTPFrom: string
SMTPToken: string
SMTPSSLEnabled: boolean
SMTPStartTLSEnabled: boolean
SMTPInsecureSkipVerify: boolean
SMTPForceAuthLogin: boolean
WorkerUrl: string
WorkerValidKey: string