新增中继启用控制、列表展示、编辑和清除逻辑,保留安全配置边界。
This commit is contained in:
parent
a71d57e848
commit
3cca3438b8
@ -84,8 +84,7 @@ func validateMailListeners(smtp SMTPSectionConfig, email EmailSectionConfig) err
|
||||
if security != MailSecurityBoth && security != MailSecuritySTARTTLS && security != MailSecurityImplicitTLS {
|
||||
return ErrInvalidRuntimeConfig
|
||||
}
|
||||
relayConfigured := smtpRelayConfigured(smtp.Relay)
|
||||
if relayConfigured && security != MailSecurityBoth {
|
||||
if smtpRelayEnabled(smtp.Relay) && security != MailSecurityBoth {
|
||||
relaySecurity := NormalizeMailSecurity(smtp.Relay.Security)
|
||||
if relaySecurity != security {
|
||||
return ErrInvalidSMTP
|
||||
|
||||
@ -870,6 +870,7 @@ func applySectionPayload(current RuntimeConfig, section Section, payload json.Ra
|
||||
return RuntimeConfig{}, errors.New("configuration payload is empty")
|
||||
}
|
||||
candidate := cloneRuntimeConfig(current)
|
||||
clearRelay := false
|
||||
switch section {
|
||||
case SectionRoot:
|
||||
var value struct {
|
||||
@ -895,7 +896,15 @@ func applySectionPayload(current RuntimeConfig, section Section, payload json.Ra
|
||||
if value.SMTPSectionConfig.Relay.Password.Configured() {
|
||||
return RuntimeConfig{}, ErrInvalidSecretChange
|
||||
}
|
||||
value.SMTPSectionConfig.Relay.Password = current.SMTP.Relay.Password
|
||||
clearRelay = value.SMTPSectionConfig.Relay.Clear
|
||||
if !value.SMTPSectionConfig.Relay.relayEnabledSet {
|
||||
value.SMTPSectionConfig.Relay.RelayEnabled = current.SMTP.Relay.RelayEnabled
|
||||
}
|
||||
if clearRelay {
|
||||
clearSMTPRelay(&value.SMTPSectionConfig.Relay)
|
||||
} else {
|
||||
value.SMTPSectionConfig.Relay.Password = current.SMTP.Relay.Password
|
||||
}
|
||||
if smtpRelayConfigured(value.SMTPSectionConfig.Relay) && value.SMTPSectionConfig.Relay.Security == "" {
|
||||
if ResolveMailSecurity(value.SMTPSectionConfig.Listener) == MailSecurityImplicitTLS {
|
||||
value.SMTPSectionConfig.Relay.Security = MailSecurityImplicitTLS
|
||||
@ -923,7 +932,15 @@ func applySectionPayload(current RuntimeConfig, section Section, payload json.Ra
|
||||
if value.MailSectionConfig.Relay.Password.Configured() {
|
||||
return RuntimeConfig{}, ErrInvalidSecretChange
|
||||
}
|
||||
value.MailSectionConfig.Relay.Password = current.SMTP.Relay.Password
|
||||
clearRelay = value.MailSectionConfig.Relay.Clear
|
||||
if !value.MailSectionConfig.Relay.relayEnabledSet {
|
||||
value.MailSectionConfig.Relay.RelayEnabled = current.SMTP.Relay.RelayEnabled
|
||||
}
|
||||
if clearRelay {
|
||||
clearSMTPRelay(&value.MailSectionConfig.Relay)
|
||||
} else {
|
||||
value.MailSectionConfig.Relay.Password = current.SMTP.Relay.Password
|
||||
}
|
||||
if smtpRelayConfigured(value.Relay) && value.Relay.Security == "" {
|
||||
if ResolveMailSecurity(value.Listener) == MailSecurityImplicitTLS {
|
||||
value.Relay.Security = MailSecurityImplicitTLS
|
||||
@ -980,10 +997,29 @@ func applySectionPayload(current RuntimeConfig, section Section, payload json.Ra
|
||||
if err := applySecretChanges(&candidate, section, changes, vault); err != nil {
|
||||
return RuntimeConfig{}, err
|
||||
}
|
||||
if clearRelay {
|
||||
clearSMTPRelay(&candidate.SMTP.Relay)
|
||||
}
|
||||
candidate.SetupState = candidate.Root.SetupState
|
||||
return candidate, nil
|
||||
}
|
||||
|
||||
func clearSMTPRelay(relay *SMTPConfig) {
|
||||
if relay == nil {
|
||||
return
|
||||
}
|
||||
relay.RelayEnabled = false
|
||||
relay.RelayHost = ""
|
||||
relay.RelayPort = 0
|
||||
relay.Username = ""
|
||||
relay.Password = StoredSecret{}
|
||||
relay.Security = ""
|
||||
relay.From = ""
|
||||
relay.FromDomain = ""
|
||||
relay.Clear = false
|
||||
relay.relayEnabledSet = true
|
||||
}
|
||||
|
||||
func setCandidateGeneration(candidate *RuntimeConfig, generation uint64) {
|
||||
if candidate == nil {
|
||||
return
|
||||
|
||||
@ -763,6 +763,7 @@ func validLegacyDatabasePassword(value string) bool {
|
||||
|
||||
func legacySMTPConfig(root string, vault *credential.Vault, legacy legacyMigrationConfig) (SMTPConfig, []string, error) {
|
||||
relay := SMTPConfig{
|
||||
RelayEnabled: legacy.SMTPHost != "" || legacy.SMTPPort != 0 || legacy.SMTPUsername != "" || legacy.SMTPFrom != "" || legacy.SMTPPassword != "",
|
||||
RelayHost: legacy.SMTPHost,
|
||||
RelayPort: legacy.SMTPPort,
|
||||
Username: legacy.SMTPUsername,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SetupState string
|
||||
|
||||
@ -82,6 +85,7 @@ type DatabaseConfig struct {
|
||||
}
|
||||
|
||||
type SMTPConfig struct {
|
||||
RelayEnabled bool `json:"enabled"`
|
||||
RelayHost string `json:"relayHost"`
|
||||
RelayPort int `json:"relayPort"`
|
||||
Username string `json:"username"`
|
||||
@ -91,6 +95,42 @@ type SMTPConfig struct {
|
||||
FromDomain string `json:"fromDomain"`
|
||||
VerificationEnabled bool `json:"verificationEnabled"`
|
||||
LoginPolicy string `json:"loginPolicy"`
|
||||
Clear bool `json:"clear,omitempty"`
|
||||
relayEnabledSet bool `json:"-"`
|
||||
}
|
||||
|
||||
func (s *SMTPConfig) UnmarshalJSON(data []byte) error {
|
||||
type smtpConfigAlias SMTPConfig
|
||||
var value smtpConfigAlias
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &fields); err != nil {
|
||||
return err
|
||||
}
|
||||
*s = SMTPConfig(value)
|
||||
_, enabledProvided := fields["enabled"]
|
||||
configured := smtpRelayFieldsConfigured(*s)
|
||||
s.relayEnabledSet = enabledProvided && (s.RelayEnabled || configured)
|
||||
if !enabledProvided {
|
||||
s.RelayEnabled = configured
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SMTPConfig) MarshalJSON() ([]byte, error) {
|
||||
type smtpConfigAlias SMTPConfig
|
||||
data, err := json.Marshal(smtpConfigAlias(s))
|
||||
if err != nil || s.RelayEnabled || s.relayEnabledSet {
|
||||
return data, err
|
||||
}
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &fields); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delete(fields, "enabled")
|
||||
return json.Marshal(fields)
|
||||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
|
||||
@ -288,6 +288,7 @@ func CurrentSMTPRuntimeSettings() (SMTPRuntimeSettings, error) {
|
||||
return SMTPRuntimeSettings{}, fmt.Errorf("runtime configuration is unavailable")
|
||||
}
|
||||
settings.Relay = SMTPConfig{
|
||||
RelayEnabled: cfg.SMTPHost != "" || cfg.SMTPPort != 0 || cfg.SMTPUsername != "" || cfg.SMTPFrom != "" || cfg.SMTPPassword != "",
|
||||
RelayHost: cfg.SMTPHost,
|
||||
RelayPort: cfg.SMTPPort,
|
||||
Username: cfg.SMTPUsername,
|
||||
|
||||
@ -152,8 +152,7 @@ func validateSMTP(smtp SMTPConfig) error {
|
||||
return ErrInvalidSMTP
|
||||
}
|
||||
}
|
||||
relayConfigured := smtpRelayConfigured(smtp)
|
||||
if !relayConfigured {
|
||||
if !smtpRelayEnabled(smtp) {
|
||||
return nil
|
||||
}
|
||||
if smtp.RelayHost == "" || smtp.RelayPort == 0 || smtp.Security == "" || smtp.From == "" || smtp.FromDomain == "" {
|
||||
@ -169,12 +168,22 @@ func validateSMTP(smtp SMTPConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// smtpRelayConfigured 只把实际连接或发件身份字段视为启用中继。
|
||||
// 推荐端口、统一加密方式和默认发件域名单独存在时,中继仍保持可选状态。
|
||||
func smtpRelayConfigured(smtp SMTPConfig) bool {
|
||||
func smtpRelayFieldsConfigured(smtp SMTPConfig) bool {
|
||||
return smtp.RelayHost != "" || smtp.Username != "" || smtp.Password.Configured() || smtp.From != ""
|
||||
}
|
||||
|
||||
func smtpRelayConfigured(smtp SMTPConfig) bool {
|
||||
return smtpRelayFieldsConfigured(smtp) || smtp.RelayEnabled
|
||||
}
|
||||
|
||||
func smtpRelayEnabled(smtp SMTPConfig) bool {
|
||||
return smtp.RelayEnabled || (!smtp.relayEnabledSet && smtpRelayFieldsConfigured(smtp))
|
||||
}
|
||||
|
||||
func (smtp SMTPConfig) RelayActive() bool {
|
||||
return smtpRelayEnabled(smtp)
|
||||
}
|
||||
|
||||
func validateSSL(ssl SSLConfig) error {
|
||||
if err := validateDNSProvider(ssl); err != nil {
|
||||
return err
|
||||
|
||||
@ -456,6 +456,7 @@ func publicConfigSection(runtime config.RuntimeConfig, section config.Section) (
|
||||
"meta": runtime.SMTP.Meta,
|
||||
"listener": config.SMTPListenerConfig{Port: ports.SMTP, STARTTLSPort: ports.SMTPSTARTTLS, TLSPort: ports.SMTPTLS, Security: config.PublicMailSecurity(runtime.SMTP.Listener)},
|
||||
"relay": map[string]any{
|
||||
"enabled": runtime.SMTP.Relay.RelayEnabled,
|
||||
"relayHost": runtime.SMTP.Relay.RelayHost,
|
||||
"relayPort": runtime.SMTP.Relay.RelayPort,
|
||||
"username": runtime.SMTP.Relay.Username,
|
||||
@ -474,6 +475,7 @@ func publicConfigSection(runtime config.RuntimeConfig, section config.Section) (
|
||||
"meta": runtime.SMTP.Meta,
|
||||
"listener": config.SMTPListenerConfig{Port: ports.SMTP, STARTTLSPort: ports.SMTPSTARTTLS, TLSPort: ports.SMTPTLS, Security: config.PublicMailSecurity(runtime.SMTP.Listener)},
|
||||
"relay": map[string]any{
|
||||
"enabled": runtime.SMTP.Relay.RelayEnabled,
|
||||
"relayHost": runtime.SMTP.Relay.RelayHost,
|
||||
"relayPort": runtime.SMTP.Relay.RelayPort,
|
||||
"username": runtime.SMTP.Relay.Username,
|
||||
|
||||
@ -81,11 +81,7 @@ func smtpRelayComplete(settings config.SMTPRuntimeSettings) bool {
|
||||
}
|
||||
|
||||
func smtpRelayRequested(settings config.SMTPRuntimeSettings) bool {
|
||||
relay := settings.Relay
|
||||
return strings.TrimSpace(relay.RelayHost) != "" ||
|
||||
strings.TrimSpace(relay.Username) != "" ||
|
||||
strings.TrimSpace(settings.Password) != "" ||
|
||||
strings.TrimSpace(relay.From) != ""
|
||||
return settings.Relay.RelayActive()
|
||||
}
|
||||
|
||||
func verificationSMTPSettings(settings config.SMTPRuntimeSettings) (verification.SMTPSettings, error) {
|
||||
|
||||
@ -15,8 +15,9 @@ export const relaySecurityForMail = config => mailSecurity(config) === 'implicit
|
||||
const startTLSMail = config => mailSecurity(config) === 'starttls'
|
||||
const implicitTLSMail = config => mailSecurity(config) === 'implicit_tls'
|
||||
export const relayConfigured = config => !!(
|
||||
config.relay?.relayHost || config.relay?.username || config.relay?.password ||
|
||||
config.relay?.from
|
||||
config.relay?.enabled || config.relay?.relayHost || config.relay?.relayPort ||
|
||||
config.relay?.username || config.relay?.password || config.relay?.security ||
|
||||
config.relay?.from || config.relay?.fromDomain
|
||||
)
|
||||
|
||||
export function readFormConfig(text) {
|
||||
@ -141,6 +142,7 @@ const mailGroups = [
|
||||
port('pop3.tlsPort', 'POP3 端口(隐式 TLS)', { min: 1, required: true, show: config => !startTLSMail(config), hint: '默认 995,客户端建立连接时直接使用 TLS。' })
|
||||
]),
|
||||
group('relay', 'SMTP 中继连接(可选)', '', [
|
||||
field('relay.enabled', '启用 SMTP 中继', 'switch', { hint: '关闭后保留配置,但系统不会使用该中继发信。' }),
|
||||
field('relay.relayHost', 'SMTP 服务器', 'text', { placeholder: 'smtp.example.com' }),
|
||||
port('relay.relayPort', 'SMTP 发信中继端口', { hint: '外部 SMTP 服务器端口,与本机监听无关;选择加密方式后自动填写。' }),
|
||||
field('relay.username', '认证用户名', 'text', { placeholder: '需要认证时填写' }),
|
||||
@ -281,14 +283,16 @@ export function updateFormConfig(text, section, path, value) {
|
||||
if (!Array.isArray(path) || path.some(key => ['__proto__', 'prototype', 'constructor'].includes(key))) return text
|
||||
const key = path.join('.')
|
||||
const input = getFieldGroups(section, config).flatMap(item => item.fields).find(item => fieldKey(item) === key)
|
||||
if (!input || input.locked) return text
|
||||
const isRelayClear = section === 'mail' && key === 'relay.clear'
|
||||
if ((!input && !isRelayClear) || input?.locked) return text
|
||||
const previousValue = getValue(config, path)
|
||||
let parent = config
|
||||
for (const part of path.slice(0, -1)) {
|
||||
if (!parent[part] || typeof parent[part] !== 'object') parent[part] = {}
|
||||
parent = parent[part]
|
||||
}
|
||||
parent[path.at(-1)] = value ?? (input.type === 'number' ? null : '')
|
||||
parent[path.at(-1)] = value ?? (input?.type === 'number' ? null : '')
|
||||
if (key.startsWith('relay.') && key !== 'relay.clear' && config.relay) config.relay.clear = false
|
||||
if (previousValue !== value) {
|
||||
if (key === 'listener.security' && ['both', 'starttls', 'implicit_tls'].includes(value)) {
|
||||
const startTLS = value === 'starttls'
|
||||
@ -305,7 +309,7 @@ export function updateFormConfig(text, section, path, value) {
|
||||
config.relay.security = relaySecurityForMail(config)
|
||||
config.relay.relayPort = smtpPorts.get(config.relay.security)
|
||||
}
|
||||
if (key.startsWith('relay.') && key !== 'relay.security' && relayConfigured(config) && ['both', 'starttls', 'implicit_tls'].includes(mailSecurity(config))) {
|
||||
if (key.startsWith('relay.') && key !== 'relay.security' && key !== 'relay.enabled' && relayConfigured(config) && ['both', 'starttls', 'implicit_tls'].includes(mailSecurity(config))) {
|
||||
config.relay.security = relaySecurityForMail(config)
|
||||
if (!validPort(config.relay.relayPort)) config.relay.relayPort = smtpPorts.get(config.relay.security)
|
||||
}
|
||||
@ -365,10 +369,11 @@ export function validateFormConfig(section, config) {
|
||||
if (section === 'mail') {
|
||||
const relay = config.relay || {}
|
||||
const configured = relayConfigured(config)
|
||||
const enabled = relay.enabled === true || (relay.enabled === undefined && configured)
|
||||
if (!['both', 'starttls', 'implicit_tls'].includes(mailSecurity(config))) {
|
||||
errors['listener.security'] = '请选择全部启用、STARTTLS / STLS 或隐式 TLS'
|
||||
}
|
||||
if (configured) {
|
||||
if (enabled && !relay.clear) {
|
||||
if (!relay.relayHost) errors['relay.relayHost'] = '请填写 SMTP 服务器'
|
||||
const relaySecurity = normalizedMailSecurity(relay.security)
|
||||
if (!['starttls', 'implicit_tls'].includes(relaySecurity)) errors['listener.security'] = '请先选择统一加密方式'
|
||||
|
||||
@ -8,11 +8,19 @@
|
||||
:data-group="group.id"
|
||||
:aria-label="group.title"
|
||||
>
|
||||
<div class="form-group-heading">
|
||||
<div v-if="!(section === 'mail' && group.id === 'relay')" class="form-group-heading">
|
||||
<h4>{{ group.title }}</h4>
|
||||
<span v-if="group.fields.some(field => errors[fieldKey(field)])" class="section-error">待修正</span>
|
||||
</div>
|
||||
<div class="form-grid" :class="{ 'site-grid': section === 'base' && group.id === 'site' }">
|
||||
<RelayConfig
|
||||
v-if="section === 'mail' && group.id === 'relay'"
|
||||
:config="config"
|
||||
:fields="group.fields"
|
||||
:errors="errors"
|
||||
:disabled="disabled"
|
||||
@change="(path, value) => emit('change', path, value)"
|
||||
/>
|
||||
<div v-else class="form-grid" :class="{ 'site-grid': section === 'base' && group.id === 'site' }">
|
||||
<ConfigField
|
||||
v-for="field in group.fields"
|
||||
:key="fieldKey(field)"
|
||||
@ -31,6 +39,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { fieldKey, getFieldGroups, getValue } from './ConfigForm.js'
|
||||
import ConfigField from '../ConfigField/ConfigField.vue'
|
||||
import RelayConfig from '../RelayConfig/RelayConfig.vue'
|
||||
|
||||
const props = defineProps({
|
||||
section: { type: String, required: true },
|
||||
|
||||
@ -555,7 +555,7 @@ export function usePage(pageKey = 'base') {
|
||||
secretChanges['database.password'] = { operation: 'replace', value: { plaintext: value } }
|
||||
}
|
||||
const relayPassword = formConfig.value?.relay?.password
|
||||
if (relayPassword && selectedSection.value === 'mail') {
|
||||
if (relayPassword && selectedSection.value === 'mail' && !formConfig.value?.relay?.clear) {
|
||||
secretChanges['relay.password'] = { operation: 'replace', value: { plaintext: relayPassword } }
|
||||
}
|
||||
if (selectedSection.value === 'ssl' && formConfig.value?.tls?.dnsProvider === 'alidns') {
|
||||
|
||||
@ -0,0 +1,103 @@
|
||||
.relay-config {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.relay-list-heading,
|
||||
.relay-item-heading,
|
||||
.relay-editor-heading,
|
||||
.relay-item-actions,
|
||||
.relay-editor-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.relay-list-heading h5,
|
||||
.relay-editor-heading h5 {
|
||||
margin: 0;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.relay-list-heading p,
|
||||
.relay-editor-heading span,
|
||||
.relay-subtitle {
|
||||
margin: 5px 0 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.relay-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.relay-item {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.relay-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px 20px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
.relay-summary div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.relay-summary dt {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.relay-summary dd {
|
||||
margin: 4px 0 0;
|
||||
overflow: hidden;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.relay-editor {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
}
|
||||
|
||||
.relay-fields {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.relay-alert {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.relay-summary {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.relay-list-heading,
|
||||
.relay-item-heading,
|
||||
.relay-editor-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.relay-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="relay-config">
|
||||
<div class="relay-list-heading">
|
||||
<div>
|
||||
<h5>已配置的 SMTP 中继</h5>
|
||||
<p>中继配置独立于本机邮件服务;关闭开关只停止使用,不会删除已填写的信息。</p>
|
||||
</div>
|
||||
<el-button v-if="hasConfigured && !editing" type="primary" plain :disabled="disabled" @click="editing = true">
|
||||
编辑中继
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="relay.clear"
|
||||
title="已标记清除,保存后将删除服务器、端口、账号、密码、发件邮箱和发件域名。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="relay-alert"
|
||||
/>
|
||||
|
||||
<div v-if="hasConfigured && !editing" class="relay-list" role="list" aria-label="SMTP 中继列表">
|
||||
<article class="relay-item" role="listitem">
|
||||
<div class="relay-item-heading">
|
||||
<div>
|
||||
<strong>默认 SMTP 中继</strong>
|
||||
<span class="relay-subtitle">单个默认发信中继配置</span>
|
||||
</div>
|
||||
<el-tag :type="relay.enabled ? 'success' : 'info'" effect="plain">
|
||||
{{ relay.enabled ? '已启用' : '已关闭' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<dl class="relay-summary">
|
||||
<div><dt>SMTP 服务器</dt><dd>{{ relay.relayHost || '未填写' }}</dd></div>
|
||||
<div><dt>端口</dt><dd>{{ relay.relayPort || '未填写' }}</dd></div>
|
||||
<div><dt>认证账号</dt><dd>{{ relay.username || '未填写' }}</dd></div>
|
||||
<div><dt>发件邮箱</dt><dd>{{ relay.from || '未填写' }}</dd></div>
|
||||
<div><dt>密码</dt><dd>{{ passwordConfigured ? '已配置(不显示明文)' : '未配置' }}</dd></div>
|
||||
<div><dt>发件域名</dt><dd>{{ relay.fromDomain || '未填写' }}</dd></div>
|
||||
</dl>
|
||||
<div class="relay-item-actions">
|
||||
<el-button type="primary" plain :disabled="disabled" @click="editing = true">编辑</el-button>
|
||||
<el-button type="danger" plain :disabled="disabled" @click="clearRelay">清除中继</el-button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<el-empty v-else-if="!editing" description="暂无已配置的 SMTP 中继" />
|
||||
|
||||
<div v-if="editing" class="relay-editor">
|
||||
<div class="relay-editor-heading">
|
||||
<h5>{{ hasConfigured ? '编辑 SMTP 中继' : '添加 SMTP 中继' }}</h5>
|
||||
<span>密码留空表示保持原值,清除操作需要单独点击“清除中继”。</span>
|
||||
</div>
|
||||
<div class="form-grid relay-fields">
|
||||
<ConfigField
|
||||
v-for="field in fields"
|
||||
:key="fieldKey(field)"
|
||||
:field="field"
|
||||
:value="getValue(config, field.path)"
|
||||
:error="errors[fieldKey(field)]"
|
||||
:disabled="disabled"
|
||||
@change="(path, value) => emit('change', path, value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="relay-editor-actions">
|
||||
<el-button v-if="hasConfigured" :disabled="disabled" @click="editing = false">取消编辑</el-button>
|
||||
<el-button v-if="hasConfigured" type="danger" plain :disabled="disabled" @click="clearRelay">清除中继</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import ConfigField from '../ConfigField/ConfigField.vue'
|
||||
import { fieldKey, getValue, relayConfigured } from '../ConfigForm/ConfigForm.js'
|
||||
|
||||
const props = defineProps({
|
||||
config: { type: Object, required: true },
|
||||
fields: { type: Array, required: true },
|
||||
errors: { type: Object, default: () => ({}) },
|
||||
disabled: Boolean
|
||||
})
|
||||
const emit = defineEmits(['change'])
|
||||
const relay = computed(() => props.config.relay || {})
|
||||
const hasConfigured = computed(() => relayConfigured(props.config))
|
||||
const passwordConfigured = computed(() => !!props.config.relayPasswordStatus?.configured)
|
||||
const editing = ref(!hasConfigured.value)
|
||||
|
||||
const clearRelay = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'清除后将删除已保存的 SMTP 中继信息和密码,关闭中继开关不会产生此影响。确认清除吗?',
|
||||
'确认清除 SMTP 中继',
|
||||
{ type: 'warning', confirmButtonText: '确认清除', cancelButtonText: '取消' }
|
||||
)
|
||||
emit('change', ['relay', 'clear'], true)
|
||||
editing.value = false
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') throw error
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped src="./RelayConfig.css"></style>
|
||||
Loading…
Reference in New Issue
Block a user