diff --git a/backend/config/mail_listeners.go b/backend/config/mail_listeners.go
index 44ce787..19b7b60 100644
--- a/backend/config/mail_listeners.go
+++ b/backend/config/mail_listeners.go
@@ -1,10 +1,17 @@
package config
-// 邮件监听与发信中继相互独立。零值表示沿用兼容默认值,不表示关闭服务。
+const (
+ MailSecurityBoth = "both"
+ MailSecuritySTARTTLS = "starttls"
+ MailSecurityImplicitTLS = "implicit_tls"
+)
+
+// 邮件监听与发信中继相互独立。未设置加密模式的旧配置继续同时开放两套监听。
type SMTPListenerConfig struct {
- Port int `json:"port"`
- STARTTLSPort int `json:"starttlsPort"`
- TLSPort int `json:"tlsPort"`
+ Port int `json:"port"`
+ STARTTLSPort int `json:"starttlsPort"`
+ TLSPort int `json:"tlsPort"`
+ Security string `json:"security,omitempty"`
}
type IMAPListenerConfig struct {
@@ -16,6 +23,32 @@ type POP3ListenerConfig struct {
TLSPort int `json:"tlsPort"`
}
+func ResolveMailSecurity(listener SMTPListenerConfig) string {
+ switch NormalizeMailSecurity(listener.Security) {
+ case MailSecuritySTARTTLS:
+ return MailSecuritySTARTTLS
+ case MailSecurityImplicitTLS:
+ return MailSecurityImplicitTLS
+ case "", MailSecurityBoth:
+ return MailSecurityBoth
+ default:
+ return listener.Security
+ }
+}
+
+// NormalizeMailSecurity 保留空值及未知值,只将旧版 TLS 名称规范为当前名称。
+func NormalizeMailSecurity(value string) string {
+ if value == "tls" {
+ return MailSecurityImplicitTLS
+ }
+ return value
+}
+
+// PublicMailSecurity 保留旧配置的空值,只规范公开视图中的旧版名称。
+func PublicMailSecurity(listener SMTPListenerConfig) string {
+ return NormalizeMailSecurity(listener.Security)
+}
+
// ResolveMailPorts 是公开配置视图、冲突校验及真实监听的共同端口来源。
func ResolveMailPorts(smtp SMTPSectionConfig, email EmailSectionConfig) MailPorts {
portOr := func(port, fallback int) int {
@@ -24,7 +57,7 @@ func ResolveMailPorts(smtp SMTPSectionConfig, email EmailSectionConfig) MailPort
}
return fallback
}
- return MailPorts{
+ ports := MailPorts{
SMTP: portOr(smtp.Listener.Port, portOr(email.Policy.SMTPPort, 25)),
SMTPSTARTTLS: portOr(smtp.Listener.STARTTLSPort, 587),
SMTPTLS: portOr(smtp.Listener.TLSPort, 465),
@@ -33,9 +66,34 @@ func ResolveMailPorts(smtp SMTPSectionConfig, email EmailSectionConfig) MailPort
POP3: portOr(email.POP3.Port, portOr(email.Policy.POP3Port, 110)),
POP3TLS: portOr(email.POP3.TLSPort, 995),
}
+ switch ResolveMailSecurity(smtp.Listener) {
+ case MailSecuritySTARTTLS:
+ ports.SMTPTLS = 0
+ ports.IMAP = portOr(email.IMAP.TLSPort, 143)
+ ports.POP3TLS = 0
+ case MailSecurityImplicitTLS:
+ ports.SMTPSTARTTLS = 0
+ ports.IMAP = portOr(email.IMAP.TLSPort, 993)
+ ports.POP3 = 0
+ }
+ return ports
}
func validateMailListeners(smtp SMTPSectionConfig, email EmailSectionConfig) error {
+ if smtp.Listener.Security == MailSecurityBoth {
+ return ErrInvalidRuntimeConfig
+ }
+ security := ResolveMailSecurity(smtp.Listener)
+ if security != MailSecurityBoth && security != MailSecuritySTARTTLS && security != MailSecurityImplicitTLS {
+ return ErrInvalidRuntimeConfig
+ }
+ relayConfigured := smtpRelayConfigured(smtp.Relay)
+ if relayConfigured && security != MailSecurityBoth {
+ relaySecurity := NormalizeMailSecurity(smtp.Relay.Security)
+ if relaySecurity != security {
+ return ErrInvalidSMTP
+ }
+ }
for _, port := range []int{
smtp.Listener.Port, smtp.Listener.STARTTLSPort, smtp.Listener.TLSPort,
email.IMAP.TLSPort, email.POP3.Port, email.POP3.TLSPort,
diff --git a/backend/config/mail_listeners_test.go b/backend/config/mail_listeners_test.go
index 2ca14af..1f50d04 100644
--- a/backend/config/mail_listeners_test.go
+++ b/backend/config/mail_listeners_test.go
@@ -146,3 +146,67 @@ func TestOldPartitionPayloadsKeepExistingListenerDefaults(t *testing.T) {
t.Fatalf("legacy listeners changed: %+v", got)
}
}
+
+func TestUnifiedMailSecurityResolvesOnlySelectedClientListeners(t *testing.T) {
+ for _, test := range []struct {
+ security string
+ want MailPorts
+ }{
+ {MailSecuritySTARTTLS, MailPorts{SMTP: 25, SMTPSTARTTLS: 587, IMAP: 143, POP3: 110}},
+ {MailSecurityImplicitTLS, MailPorts{SMTP: 25, SMTPTLS: 465, IMAP: 993, POP3TLS: 995}},
+ } {
+ smtp := SMTPSectionConfig{Listener: SMTPListenerConfig{Security: test.security}}
+ if got := ResolveMailPorts(smtp, EmailSectionConfig{}); got != test.want {
+ t.Fatalf("%s ports = %+v, want %+v", test.security, got, test.want)
+ }
+ }
+}
+
+func TestLegacyMailSecurityKeepsExistingListeners(t *testing.T) {
+ smtp := SMTPSectionConfig{Listener: SMTPListenerConfig{Port: 2525, STARTTLSPort: 2587, TLSPort: 2465}}
+ email := EmailSectionConfig{IMAP: IMAPListenerConfig{TLSPort: 2993}, POP3: POP3ListenerConfig{Port: 2110, TLSPort: 2995}}
+ want := MailPorts{SMTP: 2525, SMTPSTARTTLS: 2587, SMTPTLS: 2465, IMAP: 2993, POP3: 2110, POP3TLS: 2995}
+ if got := ResolveMailPorts(smtp, email); got != want {
+ t.Fatalf("legacy ports = %+v, want %+v", got, want)
+ }
+ if got := ResolveMailSecurity(smtp.Listener); got != MailSecurityBoth {
+ t.Fatalf("legacy security = %q, want %q", got, MailSecurityBoth)
+ }
+ if got := PublicMailSecurity(smtp.Listener); got != "" {
+ t.Fatalf("legacy public security = %q, want empty", got)
+ }
+ if got := PublicMailSecurity(SMTPListenerConfig{Security: "tls"}); got != MailSecurityImplicitTLS {
+ t.Fatalf("legacy TLS public security = %q, want %q", got, MailSecurityImplicitTLS)
+ }
+ if got := NormalizeMailSecurity("tls"); got != MailSecurityImplicitTLS {
+ t.Fatalf("legacy relay security = %q, want %q", got, MailSecurityImplicitTLS)
+ }
+}
+
+func TestMailListenerValidationAcceptsMatchingLegacyTLSNames(t *testing.T) {
+ runtime := validRuntimeConfig()
+ runtime.SMTP.Listener.Security = "tls"
+ runtime.SMTP.Relay.Security = "tls"
+ if err := ValidateRuntimeConfig(runtime, RuntimeValidationOptions{HasSuperAdmin: true}); err != nil {
+ t.Fatalf("matching legacy TLS validation = %v", err)
+ }
+}
+
+func TestMailListenerValidationRejectsUnknownSecurity(t *testing.T) {
+ runtime := validRuntimeConfig()
+ runtime.SMTP.Listener.Security = "unknown"
+ if err := ValidateRuntimeConfig(runtime, RuntimeValidationOptions{HasSuperAdmin: true}); !errors.Is(err, ErrInvalidRuntimeConfig) {
+ t.Fatalf("validation = %v, want invalid runtime configuration", err)
+ }
+}
+
+func TestMailListenerValidationAllowsLegacyEmptySecurityButRejectsExplicitBoth(t *testing.T) {
+ runtime := validRuntimeConfig()
+ if err := ValidateRuntimeConfig(runtime, RuntimeValidationOptions{HasSuperAdmin: true}); err != nil {
+ t.Fatalf("legacy empty security validation = %v", err)
+ }
+ runtime.SMTP.Listener.Security = MailSecurityBoth
+ if err := ValidateRuntimeConfig(runtime, RuntimeValidationOptions{HasSuperAdmin: true}); !errors.Is(err, ErrInvalidRuntimeConfig) {
+ t.Fatalf("explicit both validation = %v, want invalid runtime configuration", err)
+ }
+}
diff --git a/backend/config/runtime_access.go b/backend/config/runtime_access.go
index 705ac32..f76afa3 100644
--- a/backend/config/runtime_access.go
+++ b/backend/config/runtime_access.go
@@ -289,6 +289,15 @@ func CurrentMailPorts() MailPorts {
// 旧版 SMTPPort 属于发信中继,不能用作本机监听端口。
return ResolveMailPorts(SMTPSectionConfig{}, EmailSectionConfig{})
}
+
+func CurrentMailSecurity() string {
+ if manager := CurrentManager(); manager != nil {
+ if runtime := manager.Snapshot(); runtime.Generation != 0 {
+ return ResolveMailSecurity(runtime.SMTP.Listener)
+ }
+ }
+ return MailSecurityBoth
+}
func cloneHTTPConfig(source HTTPConfig) HTTPConfig {
clone := source
clone.TrustedProxyCIDRs = append([]string(nil), source.TrustedProxyCIDRs...)
diff --git a/backend/config/validator.go b/backend/config/validator.go
index 6a87d0b..3bb2d0e 100644
--- a/backend/config/validator.go
+++ b/backend/config/validator.go
@@ -149,8 +149,7 @@ func validateSMTP(smtp SMTPConfig) error {
return ErrInvalidSMTP
}
}
- relayConfigured := smtp.RelayHost != "" || smtp.RelayPort != 0 || smtp.Username != "" ||
- smtp.Password.Configured() || smtp.Security != "" || smtp.From != ""
+ relayConfigured := smtpRelayConfigured(smtp)
if !relayConfigured {
return nil
}
@@ -167,6 +166,12 @@ func validateSMTP(smtp SMTPConfig) error {
return nil
}
+// smtpRelayConfigured 只把实际连接或发件身份字段视为启用中继。
+// 推荐端口、统一加密方式和默认发件域名单独存在时,中继仍保持可选状态。
+func smtpRelayConfigured(smtp SMTPConfig) bool {
+ return smtp.RelayHost != "" || smtp.Username != "" || smtp.Password.Configured() || smtp.From != ""
+}
+
func validateSSL(ssl SSLConfig) error {
if !isControlledRelativePath(ssl.DefaultDirectory) {
return ErrInvalidSSL
diff --git a/backend/config/validator_security_test.go b/backend/config/validator_security_test.go
index dfc3704..2eb769a 100644
--- a/backend/config/validator_security_test.go
+++ b/backend/config/validator_security_test.go
@@ -183,6 +183,15 @@ func TestValidateSMTPKeepsLoginVerificationIndependentFromRelay(t *testing.T) {
},
valid: true,
},
+ {
+ name: "unified defaults without relay host",
+ smtp: SMTPConfig{
+ RelayPort: 587,
+ Security: "starttls",
+ FromDomain: "example.com",
+ },
+ valid: true,
+ },
{
name: "partially configured relay",
smtp: SMTPConfig{
diff --git a/backend/controllers/config.go b/backend/controllers/config.go
index cb02f80..662fada 100644
--- a/backend/controllers/config.go
+++ b/backend/controllers/config.go
@@ -449,12 +449,12 @@ func publicConfigSection(runtime config.RuntimeConfig, section config.Section) (
ports := config.ResolveMailPorts(runtime.SMTP, runtime.Email)
return map[string]any{
"meta": runtime.SMTP.Meta,
- "listener": config.SMTPListenerConfig{Port: ports.SMTP, STARTTLSPort: ports.SMTPSTARTTLS, TLSPort: ports.SMTPTLS},
+ "listener": config.SMTPListenerConfig{Port: ports.SMTP, STARTTLSPort: ports.SMTPSTARTTLS, TLSPort: ports.SMTPTLS, Security: config.PublicMailSecurity(runtime.SMTP.Listener)},
"relay": map[string]any{
"relayHost": runtime.SMTP.Relay.RelayHost,
"relayPort": runtime.SMTP.Relay.RelayPort,
"username": runtime.SMTP.Relay.Username,
- "security": runtime.SMTP.Relay.Security,
+ "security": config.NormalizeMailSecurity(runtime.SMTP.Relay.Security),
"from": runtime.SMTP.Relay.From,
"fromDomain": runtime.SMTP.Relay.FromDomain,
"verificationEnabled": runtime.SMTP.Relay.VerificationEnabled,
@@ -467,12 +467,12 @@ func publicConfigSection(runtime config.RuntimeConfig, section config.Section) (
ports := config.ResolveMailPorts(runtime.SMTP, runtime.Email)
return map[string]any{
"meta": runtime.SMTP.Meta,
- "listener": config.SMTPListenerConfig{Port: ports.SMTP, STARTTLSPort: ports.SMTPSTARTTLS, TLSPort: ports.SMTPTLS},
+ "listener": config.SMTPListenerConfig{Port: ports.SMTP, STARTTLSPort: ports.SMTPSTARTTLS, TLSPort: ports.SMTPTLS, Security: config.PublicMailSecurity(runtime.SMTP.Listener)},
"relay": map[string]any{
"relayHost": runtime.SMTP.Relay.RelayHost,
"relayPort": runtime.SMTP.Relay.RelayPort,
"username": runtime.SMTP.Relay.Username,
- "security": runtime.SMTP.Relay.Security,
+ "security": config.NormalizeMailSecurity(runtime.SMTP.Relay.Security),
"from": runtime.SMTP.Relay.From,
"fromDomain": runtime.SMTP.Relay.FromDomain,
"verificationEnabled": runtime.SMTP.Relay.VerificationEnabled,
diff --git a/backend/controllers/config_mail_ports_test.go b/backend/controllers/config_mail_ports_test.go
index df04c82..d5a3cc0 100644
--- a/backend/controllers/config_mail_ports_test.go
+++ b/backend/controllers/config_mail_ports_test.go
@@ -44,6 +44,9 @@ func TestPublicMailPortsMatchListenersWithoutChangingSnapshot(t *testing.T) {
if mail.Listener.Port != want.SMTP || mail.Listener.STARTTLSPort != want.SMTPSTARTTLS || mail.Listener.TLSPort != want.SMTPTLS || mail.IMAP.TLSPort != want.IMAP || mail.POP3.Port != want.POP3 || mail.POP3.TLSPort != want.POP3TLS {
t.Fatal("combined public mail ports differ from listener configuration")
}
+ if mail.Listener.Security != runtime.SMTP.Listener.Security {
+ t.Fatal("public mail view changed legacy security state")
+ }
smtpPublic, err := publicConfigSection(runtime, config.SectionSMTP)
if err != nil {
t.Fatal(err)
@@ -71,3 +74,36 @@ func TestPublicMailPortsMatchListenersWithoutChangingSnapshot(t *testing.T) {
}
}
}
+
+func TestPublicMailSecurityNormalizesLegacyTLSNames(t *testing.T) {
+ runtime := config.RuntimeConfig{
+ SMTP: config.SMTPSectionConfig{
+ Listener: config.SMTPListenerConfig{Security: "tls"},
+ Relay: config.SMTPConfig{Security: "tls"},
+ },
+ }
+
+ for _, section := range []config.Section{config.SectionSMTP, config.SectionMail} {
+ public, err := publicConfigSection(runtime, section)
+ if err != nil {
+ t.Fatal(err)
+ }
+ data, err := json.Marshal(public)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var value struct {
+ Listener config.SMTPListenerConfig `json:"listener"`
+ Relay config.SMTPConfig `json:"relay"`
+ }
+ if err := json.Unmarshal(data, &value); err != nil {
+ t.Fatal(err)
+ }
+ if value.Listener.Security != config.MailSecurityImplicitTLS {
+ t.Fatalf("%s listener security = %q, want %q", section, value.Listener.Security, config.MailSecurityImplicitTLS)
+ }
+ if value.Relay.Security != config.MailSecurityImplicitTLS {
+ t.Fatalf("%s relay security = %q, want %q", section, value.Relay.Security, config.MailSecurityImplicitTLS)
+ }
+ }
+}
diff --git a/backend/listen/imap_server/imap_server.go b/backend/listen/imap_server/imap_server.go
index 19620e6..456735c 100644
--- a/backend/listen/imap_server/imap_server.go
+++ b/backend/listen/imap_server/imap_server.go
@@ -9,9 +9,16 @@ import (
log "github.com/sirupsen/logrus"
)
+var instance *imapserver.Server
var instanceTLS *imapserver.Server
func Stop() {
+ if instance != nil {
+ if err := instance.Close(); err != nil {
+ log.WithError(err).Warn("close IMAP STARTTLS server failed")
+ }
+ instance = nil
+ }
if instanceTLS != nil {
if err := instanceTLS.Close(); err != nil {
log.WithError(err).Warn("close IMAP server failed")
@@ -20,6 +27,21 @@ func Stop() {
}
}
+// Start 启动普通 IMAP 监听,认证前必须通过 STARTTLS 升级连接。
+func Start() error {
+ server, err := newIMAPServer()
+ if err != nil {
+ return err
+ }
+ instance = server
+ imapPort := config.CurrentMailPorts().IMAP
+ log.Infof("IMAP With STARTTLS Server Start On Port :%d", imapPort)
+ if err := instance.ListenAndServe(fmt.Sprintf(":%d", imapPort)); err != nil {
+ return fmt.Errorf("serve IMAP STARTTLS: %w", err)
+ }
+ return nil
+}
+
// StarTLS 启动隐式 TLS 监听,不提供明文认证入口。
func StarTLS() error {
server, err := newIMAPServer()
diff --git a/backend/res_init/init.go b/backend/res_init/init.go
index 878ed37..78c7a2a 100644
--- a/backend/res_init/init.go
+++ b/backend/res_init/init.go
@@ -194,10 +194,15 @@ func Init(serverVersion string) error {
}()
}
hooks.Init(serverVersion)
- // smtp server start
+ mailSecurity := config.CurrentMailSecurity()
+ // SMTP 25 用于服务器间投递,始终保留并支持机会性 STARTTLS。
startService(serviceErrors, "SMTP", smtp_server.Start)
- startService(serviceErrors, "SMTP TLS", smtp_server.StartWithTLS)
- startService(serviceErrors, "SMTP STARTTLS", smtp_server.StartWithTLSNew)
+ if mailSecurity == config.MailSecurityBoth || mailSecurity == config.MailSecurityImplicitTLS {
+ startService(serviceErrors, "SMTP TLS", smtp_server.StartWithTLS)
+ }
+ if mailSecurity == config.MailSecurityBoth || mailSecurity == config.MailSecuritySTARTTLS {
+ startService(serviceErrors, "SMTP STARTTLS", smtp_server.StartWithTLSNew)
+ }
// http server start
startService(serviceErrors, "HTTPS", http_server.HttpsStart)
startService(serviceErrors, "HTTP", http_server.HttpStart)
@@ -211,11 +216,17 @@ func Init(serverVersion string) error {
// 更新 SSL 配置。
ssl.Update(false)
- // pop3 server start
- startService(serviceErrors, "POP3", pop3_server.Start)
- startService(serviceErrors, "POP3 TLS", pop3_server.StartWithTls)
- // imap server start
- startService(serviceErrors, "IMAP TLS", imap_server.StarTLS)
+ if mailSecurity == config.MailSecurityBoth || mailSecurity == config.MailSecuritySTARTTLS {
+ startService(serviceErrors, "POP3 STLS", pop3_server.Start)
+ }
+ if mailSecurity == config.MailSecurityBoth || mailSecurity == config.MailSecurityImplicitTLS {
+ startService(serviceErrors, "POP3 TLS", pop3_server.StartWithTls)
+ }
+ if mailSecurity == config.MailSecuritySTARTTLS {
+ startService(serviceErrors, "IMAP STARTTLS", imap_server.Start)
+ } else {
+ startService(serviceErrors, "IMAP TLS", imap_server.StarTLS)
+ }
log.Info("邮箱服务已启动")
restart.SetCurrent(restart.NewSignalTrigger(signal.RestartChan))
diff --git a/frontend/src/views/components/ConfigSettings/ConfigField/ConfigField.vue b/frontend/src/views/components/ConfigSettings/ConfigField/ConfigField.vue
index a602f3d..320840d 100644
--- a/frontend/src/views/components/ConfigSettings/ConfigField/ConfigField.vue
+++ b/frontend/src/views/components/ConfigSettings/ConfigField/ConfigField.vue
@@ -25,7 +25,13 @@
placeholder="请选择"
@update:model-value="update"
>
-
+
field(path, label, 'number', { min: 0, max: 65535, integer: true, ...extra })
const group = (id, title, description, fields) => ({ id, title, description, fields })
const smtpPorts = new Map([['starttls', 587], ['implicit_tls', 465], ['tls', 465], ['plain', 25]])
+const normalizedMailSecurity = value => value === 'tls' ? 'implicit_tls' : value
+const mailSecurity = config => {
+ const value = normalizedMailSecurity(config.listener?.security)
+ return ['starttls', 'implicit_tls', 'both'].includes(value) ? value : 'both'
+}
+const startTLSMail = config => mailSecurity(config) === 'starttls'
+const implicitTLSMail = config => mailSecurity(config) === 'implicit_tls'
+const relayConfigured = config => !!(
+ config.relay?.relayHost || config.relay?.username || config.relay?.password ||
+ config.relay?.from
+)
export function readFormConfig(text) {
try {
@@ -32,7 +43,7 @@ export function getRecommendedPort(config, path) {
case 'http.httpsPort':
return 443
case 'relay.relayPort':
- return smtpPorts.get(config.relay?.security)
+ return smtpPorts.get(mailSecurity(config))
case 'listener.port':
return 25
case 'listener.starttlsPort':
@@ -40,7 +51,7 @@ export function getRecommendedPort(config, path) {
case 'listener.tlsPort':
return 465
case 'imap.tlsPort':
- return 993
+ return startTLSMail(config) ? 143 : 993
case 'pop3.port':
return 110
case 'pop3.tlsPort':
@@ -51,20 +62,6 @@ export function getRecommendedPort(config, path) {
}
const validPort = value => Number.isInteger(value) && value > 0 && value <= 65535
-const localSMTPHost = value => {
- if (typeof value !== 'string') return false
- const host = value.trim().toLowerCase().replace(/\.$/, '')
- if (host === 'localhost') return true
- if (/^127(?:\.(?:0|[1-9]\d{0,2})){3}$/.test(host)) {
- return host.split('.').every(part => Number(part) <= 255)
- }
- try {
- return new URL(`http://[${host}]`).hostname === '[::1]'
- } catch {
- return false
- }
-}
-
const networkDatabase = config => config.database?.type !== 'sqlite'
const uploadedCertificate = config => ['managed_upload', 'custom'].includes(config.tls?.mode)
const certificateManagement = config => {
@@ -117,23 +114,24 @@ const baseGroups = [
const mailGroups = [
group('listener', '邮件服务监听', '', [
- port('listener.port', 'SMTP 收信端口', { min: 1, hint: '默认 25,用于接收其他邮件服务器投递,支持 STARTTLS;不是发信中继端口。' }),
- port('listener.starttlsPort', 'SMTP 提交端口(STARTTLS)', { min: 1, hint: '默认 587,客户端升级 TLS 后进行认证。' }),
- port('listener.tlsPort', 'SMTP 隐式 TLS 端口', { min: 1, hint: '默认 465,客户端建立连接时直接使用 TLS。' }),
- port('imap.tlsPort', 'IMAP 隐式 TLS 端口', { min: 1, required: true, hint: '默认 993。客户端建立连接时直接使用 TLS。' }),
- port('pop3.port', 'POP3 端口(STLS)', { min: 1, required: true, hint: '默认 110,客户端使用 STLS 升级加密后认证。' }),
- port('pop3.tlsPort', 'POP3 隐式 TLS 端口', { min: 1, required: true, hint: '默认 995,客户端建立连接时直接使用 TLS。' }),
- field('relay.security', 'SMTP 中继加密方式', 'select', {
+ field('listener.security', '统一加密方式', 'select', {
wide: true,
- options: options([
- ['', '尚未配置'],
- ['starttls', 'STARTTLS(推荐端口 587)'],
- ['implicit_tls', '隐式 TLS(推荐端口 465)'],
- ['tls', 'TLS 兼容模式(推荐端口 465)'],
- ['plain', '不加密(仅限本机,端口 25)']
- ]),
- hint: '仅控制下方外部 SMTP 中继连接,不改变本机 SMTP、IMAP 或 POP3 监听方式。切换后会自动匹配推荐中继端口,仍可手动填写服务商指定的端口。'
- })
+ fallback: 'both',
+ options: [
+ { value: 'both', label: '旧配置兼容状态(请选择统一加密方式)', disabled: true },
+ ...options([
+ ['starttls', 'STARTTLS / STLS'],
+ ['implicit_tls', '隐式 TLS']
+ ])
+ ],
+ hint: '切换后 SMTP 提交、IMAP、POP3 和已配置的 SMTP 中继会统一使用同一种加密方式,并自动匹配推荐端口。SMTP 25 收信端口始终保留。'
+ }),
+ port('listener.port', 'SMTP 收信端口', { min: 1, hint: '默认 25,用于接收其他邮件服务器投递,支持 STARTTLS;不是发信中继端口。' }),
+ port('listener.starttlsPort', 'SMTP 提交端口(STARTTLS)', { min: 1, show: config => !implicitTLSMail(config), hint: '默认 587,客户端升级 TLS 后进行认证。' }),
+ port('listener.tlsPort', 'SMTP 提交端口(隐式 TLS)', { min: 1, show: config => !startTLSMail(config), hint: '默认 465,客户端建立连接时直接使用 TLS。' }),
+ port('imap.tlsPort', 'IMAP 端口', { min: 1, required: true, dynamicLabel: config => startTLSMail(config) ? 'IMAP 端口(STARTTLS)' : 'IMAP 端口(隐式 TLS)', hint: 'STARTTLS 默认使用 143,隐式 TLS 默认使用 993。' }),
+ port('pop3.port', 'POP3 端口(STLS)', { min: 1, required: true, show: config => !implicitTLSMail(config), hint: '默认 110,客户端使用 STLS 升级加密后认证。' }),
+ port('pop3.tlsPort', 'POP3 端口(隐式 TLS)', { min: 1, required: true, show: config => !startTLSMail(config), hint: '默认 995,客户端建立连接时直接使用 TLS。' })
]),
group('relay', 'SMTP 中继连接(可选)', '', [
field('relay.relayHost', 'SMTP 服务器', 'text', { placeholder: 'smtp.example.com' }),
@@ -212,7 +210,11 @@ export function getFieldGroups(section, config) {
...item,
fields: item.fields
.filter(input => !input.show || input.show(config))
- .map(input => ({ ...input, recommendedPort: getRecommendedPort(config, input.path) }))
+ .map(input => ({
+ ...input,
+ label: input.dynamicLabel ? input.dynamicLabel(config) : input.label,
+ recommendedPort: getRecommendedPort(config, input.path)
+ }))
}))
.filter(item => item.fields.length > 0)
}
@@ -252,9 +254,23 @@ export function updateFormConfig(text, section, path, value) {
}
parent[path.at(-1)] = value ?? (input.type === 'number' ? null : '')
if (previousValue !== value) {
- if (key === 'relay.security') {
- const recommended = getRecommendedPort(config, ['relay', 'relayPort'])
- if (recommended !== undefined) config.relay.relayPort = recommended
+ if (key === 'listener.security' && ['starttls', 'implicit_tls'].includes(value)) {
+ const startTLS = value === 'starttls'
+ config.listener.port = 25
+ config.listener.starttlsPort = startTLS ? 587 : 0
+ config.listener.tlsPort = startTLS ? 0 : 465
+ config.imap ||= {}
+ config.imap.tlsPort = startTLS ? 143 : 993
+ config.pop3 ||= {}
+ config.pop3.port = startTLS ? 110 : 0
+ config.pop3.tlsPort = startTLS ? 0 : 995
+ config.relay ||= {}
+ config.relay.security = value
+ config.relay.relayPort = smtpPorts.get(value)
+ }
+ if (key.startsWith('relay.') && key !== 'relay.security' && relayConfigured(config) && ['starttls', 'implicit_tls'].includes(mailSecurity(config))) {
+ config.relay.security = mailSecurity(config)
+ if (!validPort(config.relay.relayPort)) config.relay.relayPort = smtpPorts.get(config.relay.security)
}
if (key === 'http.tlsMode' && ['direct', 'proxy'].includes(value)) {
if (!validPort(config.http.port)) {
@@ -311,22 +327,24 @@ export function validateFormConfig(section, config) {
}
if (section === 'mail') {
const relay = config.relay || {}
- const relayConfigured = !!(
- relay.relayHost || relay.relayPort || relay.username || relay.password || relay.security || relay.from
- )
- if (relayConfigured) {
+ const configured = relayConfigured(config)
+ if (!['starttls', 'implicit_tls'].includes(mailSecurity(config))) {
+ errors['listener.security'] = '请选择 STARTTLS / STLS 或隐式 TLS'
+ }
+ if (configured) {
if (!relay.relayHost) errors['relay.relayHost'] = '请填写 SMTP 服务器'
- if (!['starttls', 'implicit_tls', 'tls', 'plain'].includes(relay.security)) errors['relay.security'] = '请选择连接加密方式'
+ const relaySecurity = normalizedMailSecurity(relay.security)
+ if (!['starttls', 'implicit_tls'].includes(relaySecurity)) errors['listener.security'] = '请先选择统一加密方式'
+ if (['starttls', 'implicit_tls'].includes(mailSecurity(config)) && relaySecurity !== mailSecurity(config)) errors['listener.security'] = 'SMTP 中继加密方式必须与邮件服务保持一致'
if (!validPort(relay.relayPort)) errors['relay.relayPort'] = '请填写 1~65535 的 SMTP 端口'
if (!relay.from) errors['relay.from'] = '请填写发件邮箱'
if (!relay.fromDomain) errors['relay.fromDomain'] = '请填写发件域名'
}
- if (relay.security === 'plain' && !localSMTPHost(relay.relayHost)) {
- errors['relay.relayHost'] = '不加密连接仅限本机地址;远程服务器请选择 STARTTLS 或隐式 TLS'
- }
}
if (section === 'mail') {
- const paths = ['listener.port', 'listener.starttlsPort', 'listener.tlsPort', 'imap.tlsPort', 'pop3.port', 'pop3.tlsPort']
+ const paths = getFieldGroups('mail', config).flatMap(group => group.fields)
+ .map(field => fieldKey(field))
+ .filter(path => /^(listener\.(port|starttlsPort|tlsPort)|imap\.tlsPort|pop3\.(port|tlsPort))$/.test(path))
for (const path of paths) {
const value = getValue(config, path.split('.'))
if (value > 0 && paths.some(other => other !== path && getValue(config, other.split('.')) === value)) errors[path] = '邮件服务监听端口不能重复'
diff --git a/test/frontend/config-form.test.mjs b/test/frontend/config-form.test.mjs
index 7bf1369..c478599 100644
--- a/test/frontend/config-form.test.mjs
+++ b/test/frontend/config-form.test.mjs
@@ -49,7 +49,7 @@ test('配置与安全策略页面使用分组表单,控件和样式能编译
}
})
-test('邮件服务页面统一编辑六个监听端口,中继和策略字段保持在同一配置分区', () => {
+test('邮件服务页面使用统一加密方式管理监听端口,中继和策略字段保持在同一配置分区', () => {
const config = { listener: { port: 25, starttlsPort: 587, tlsPort: 465 }, relay: { relayPort: 465 }, imap: { tlsPort: 993 }, pop3: { port: 110, tlsPort: 995 }, policy: { smtpPort: 25, imapPort: 143, pop3Port: 110 } }
const text = JSON.stringify(config)
const groups = getFieldGroups('mail', config)
@@ -57,7 +57,7 @@ test('邮件服务页面统一编辑六个监听端口,中继和策略字段
const relay = groups.find(group => group.id === 'relay')
const keys = groups.flatMap(group => group.fields.map(input => input.path.join('.')))
assert.deepEqual(keys.filter(key => /^(listener|imap|pop3)\./.test(key)), [
- 'listener.port', 'listener.starttlsPort', 'listener.tlsPort',
+ 'listener.security', 'listener.port', 'listener.starttlsPort', 'listener.tlsPort',
'imap.tlsPort', 'pop3.port', 'pop3.tlsPort'
])
assert.ok(keys.includes('relay.relayHost'))
@@ -67,9 +67,10 @@ test('邮件服务页面统一编辑六个监听端口,中继和策略字段
assert.ok(!keys.includes('policy.smtpPort'))
assert.ok(!keys.includes('policy.imapPort'))
assert.ok(!keys.includes('policy.pop3Port'))
- assert.equal(listener.fields.at(-1).path.join('.'), 'relay.security')
- assert.equal(listener.fields.at(-1).label, 'SMTP 中继加密方式')
- assert.equal(listener.fields.at(-1).wide, true)
+ assert.equal(listener.fields[0].path.join('.'), 'listener.security')
+ assert.equal(listener.fields[0].label, '统一加密方式')
+ assert.equal(listener.fields[0].wide, true)
+ assert.equal(listener.fields[0].options.find(option => option.value === 'both').disabled, true)
assert.ok(!relay.fields.some(field => field.path.join('.') === 'relay.security'))
assert.ok(relay.fields.some(field => field.path.join('.') === 'relay.relayPort'))
const edited = readFormConfig(updateFormConfig(text, 'mail', ['listener', 'tlsPort'], 2465))
@@ -240,8 +241,8 @@ test('开关、数字、空数字和列表保持真实类型,代理 HTTPS 正
}
assert.ok(validateFormConfig('mail', { imap: { tlsPort: 993 }, pop3: { port: 993, tlsPort: 995 } })['pop3.port'])
assert.deepEqual(validateFormConfig('mail', {
- listener: { port: 25, starttlsPort: 587, tlsPort: 465 },
- imap: { tlsPort: 993 }, pop3: { port: 110, tlsPort: 995 }, policy: { maxMessageBytes: 0 }
+ listener: { port: 25, starttlsPort: 587, tlsPort: 0, security: 'starttls' },
+ imap: { tlsPort: 143 }, pop3: { port: 110, tlsPort: 0 }, policy: { maxMessageBytes: 0 }
}), {})
})
@@ -306,31 +307,47 @@ test('证书管理方式只展示三种业务模式并同步底层托管配置',
assert.ok(getFieldGroups('ssl', updated).flatMap(group => group.fields).some(field => field.path.join('.') === 'tls.certificateRef'))
})
-test('SMTP 加密方式自动匹配端口,手动端口和读取过程不改变原配置', () => {
- for (const [security, expectedPort] of [['starttls', 587], ['implicit_tls', 465], ['tls', 465], ['plain', 25]]) {
- const config = { generation: 9, relay: { relayHost: 'localhost', relayPort: 2525, future: true } }
+test('统一加密方式同步切换 SMTP、IMAP、POP3 与已配置中继', () => {
+ for (const [security, expected] of [
+ ['starttls', { relay: 587, smtpStartTLS: 587, smtpTLS: 0, imap: 143, pop3: 110, pop3TLS: 0 }],
+ ['implicit_tls', { relay: 465, smtpStartTLS: 0, smtpTLS: 465, imap: 993, pop3: 0, pop3TLS: 995 }]
+ ]) {
+ const config = { generation: 9, listener: { port: 2525, starttlsPort: 2587, tlsPort: 2465 }, imap: { tlsPort: 2993 }, pop3: { port: 2110, tlsPort: 2995 }, relay: { relayHost: 'smtp.example.com', relayPort: 2525, from: 'sender@example.com', fromDomain: 'example.com', future: true } }
let text = JSON.stringify(config)
getFieldGroups('mail', config)
assert.equal(JSON.stringify(config), text)
- text = updateFormConfig(text, 'mail', ['relay', 'security'], security)
- assert.deepEqual(readFormConfig(text), { ...config, relay: { ...config.relay, security, relayPort: expectedPort } })
- const fields = getFieldGroups('mail', readFormConfig(text)).flatMap(group => group.fields)
- assert.equal(fields.find(field => field.path.at(-1) === 'relayPort').recommendedPort, expectedPort)
+ text = updateFormConfig(text, 'mail', ['listener', 'security'], security)
+ const updated = readFormConfig(text)
+ assert.deepEqual(updated.listener, { security, port: 25, starttlsPort: expected.smtpStartTLS, tlsPort: expected.smtpTLS })
+ assert.equal(updated.imap.tlsPort, expected.imap)
+ assert.deepEqual(updated.pop3, { port: expected.pop3, tlsPort: expected.pop3TLS })
+ assert.deepEqual(updated.relay, { ...config.relay, security, relayPort: expected.relay })
+ const fields = getFieldGroups('mail', updated).flatMap(group => group.fields)
+ assert.equal(fields.find(field => field.path.at(-1) === 'relayPort').recommendedPort, expected.relay)
+ assert.equal(fields.find(field => field.path.join('.') === 'imap.tlsPort').label, security === 'starttls' ? 'IMAP 端口(STARTTLS)' : 'IMAP 端口(隐式 TLS)')
+ assert.equal(fields.some(field => field.path.join('.') === 'listener.starttlsPort'), security === 'starttls')
+ assert.equal(fields.some(field => field.path.join('.') === 'listener.tlsPort'), security === 'implicit_tls')
+ assert.equal(fields.some(field => field.path.join('.') === 'pop3.port'), security === 'starttls')
+ assert.equal(fields.some(field => field.path.join('.') === 'pop3.tlsPort'), security === 'implicit_tls')
text = updateFormConfig(text, 'mail', ['relay', 'relayPort'], 2526)
assert.equal(readFormConfig(text).relay.security, security)
- text = updateFormConfig(text, 'mail', ['relay', 'security'], security)
- assert.equal(readFormConfig(text).relay.relayPort, 2526)
- }
- const fields = getFieldGroups('mail', {}).flatMap(group => group.fields)
- assert.ok(fields.findIndex(field => field.path.at(-1) === 'security') < fields.findIndex(field => field.path.at(-1) === 'relayPort'))
- const unused = JSON.stringify({ relay: { relayPort: 2525, security: 'starttls' } })
- assert.equal(readFormConfig(updateFormConfig(unused, 'mail', ['relay', 'security'], '')).relay.relayPort, 2525)
- for (const security of ['constructor', '__proto__', 'future']) {
- const updated = readFormConfig(updateFormConfig(unused, 'mail', ['relay', 'security'], security))
- assert.equal(updated.relay.relayPort, 2525)
- const fields = getFieldGroups('mail', updated).flatMap(group => group.fields)
- assert.equal(fields.find(field => field.path.at(-1) === 'relayPort').recommendedPort, undefined)
}
+ const untouched = { listener: {}, relay: {} }
+ const text = JSON.stringify(untouched)
+ getFieldGroups('mail', untouched)
+ assert.equal(JSON.stringify(untouched), text)
+ const configured = readFormConfig(updateFormConfig(JSON.stringify({ listener: { security: 'starttls' }, relay: {} }), 'mail', ['relay', 'relayHost'], 'smtp.example.com'))
+ assert.equal(configured.relay.security, 'starttls')
+ assert.equal(configured.relay.relayPort, 587)
+ assert.deepEqual(validateFormConfig('mail', {
+ listener: { security: 'tls', port: 25, starttlsPort: 0, tlsPort: 465 },
+ relay: { relayHost: 'smtp.example.com', relayPort: 465, security: 'tls', from: 'sender@example.com', fromDomain: 'example.com' },
+ imap: { tlsPort: 993 }, pop3: { port: 0, tlsPort: 995 }
+ }), {})
+ const defaultsOnly = readFormConfig(updateFormConfig(JSON.stringify({ listener: {}, relay: { fromDomain: 'example.com' } }), 'mail', ['listener', 'security'], 'starttls'))
+ assert.deepEqual(defaultsOnly.relay, { fromDomain: 'example.com', security: 'starttls', relayPort: 587 })
+ assert.deepEqual(validateFormConfig('mail', defaultsOnly), {})
+ assert.ok(validateFormConfig('mail', { listener: {}, relay: {}, imap: { tlsPort: 993 }, pop3: { port: 110, tlsPort: 995 } })['listener.security'])
})
test('内置 HTTPS 自动补全端口,反代禁用内置监听,已有自定义 HTTP 端口保留', () => {
@@ -352,7 +369,7 @@ test('内置 HTTPS 自动补全端口,反代禁用内置监听,已有自定
assert.equal(readFormConfig(updateFormConfig(custom, 'base', ['http', 'tlsMode'], 'direct')).http.httpsPort, 8443)
})
-test('加密配置校验拦截监听冲突、远程明文和已启用但未选择加密的中继', () => {
+test('加密配置校验拦截监听冲突和未统一的中继加密方式', () => {
const listeners = {
listener: { port: 25, starttlsPort: 587, tlsPort: 465 },
imap: { tlsPort: 993 },
@@ -361,14 +378,10 @@ test('加密配置校验拦截监听冲突、远程明文和已启用但未选
for (const port of [80, 0]) {
assert.ok(validateFormConfig('base', { http: { port, httpsEnabled: true, httpsPort: 80 } })['http.httpsPort'])
}
- for (const relayHost of ['localhost', 'LOCALHOST', '127.0.0.1', '127.1.2.3', '::1', '0:0:0:0:0:0:0:1']) {
- assert.deepEqual(validateFormConfig('mail', { ...listeners, relay: { relayHost, security: 'plain', relayPort: 25, from: 'local@example.com', fromDomain: 'example.com' } }), {})
- }
- for (const relayHost of ['smtp.example.com', '127.0.0.999', '127.1', '127.0.0.1.evil.com', '192.168.1.1']) {
- assert.ok(validateFormConfig('mail', { ...listeners, relay: { relayHost, security: 'plain', relayPort: 25, from: 'sender@example.com', fromDomain: 'example.com' } })['relay.relayHost'])
- }
- const errors = validateFormConfig('mail', { ...listeners, relay: { relayHost: 'smtp.example.com', security: 'starttls', relayPort: 0, from: 'sender@example.com', fromDomain: 'example.com' } })
+ const unified = { ...listeners, listener: { ...listeners.listener, security: 'starttls', tlsPort: 0 }, imap: { tlsPort: 143 }, pop3: { port: 110, tlsPort: 0 } }
+ const errors = validateFormConfig('mail', { ...unified, relay: { relayHost: 'smtp.example.com', security: 'starttls', relayPort: 0, from: 'sender@example.com', fromDomain: 'example.com' } })
assert.ok(errors['relay.relayPort'])
- assert.deepEqual(validateFormConfig('mail', { ...listeners, relay: {} }), {})
- assert.deepEqual(validateFormConfig('mail', { ...listeners, relay: { security: 'starttls', relayPort: 2525, relayHost: 'smtp.example.com', from: 'sender@example.com', fromDomain: 'example.com' } }), {})
+ assert.deepEqual(validateFormConfig('mail', { ...unified, relay: {} }), {})
+ assert.deepEqual(validateFormConfig('mail', { ...unified, relay: { security: 'starttls', relayPort: 2525, relayHost: 'smtp.example.com', from: 'sender@example.com', fromDomain: 'example.com' } }), {})
+ assert.ok(validateFormConfig('mail', { ...unified, relay: { security: 'implicit_tls', relayPort: 465, relayHost: 'smtp.example.com', from: 'sender@example.com', fromDomain: 'example.com' } })['listener.security'])
})
diff --git a/test/frontend/config-settings.test.mjs b/test/frontend/config-settings.test.mjs
index 4e6efd5..3664303 100644
--- a/test/frontend/config-settings.test.mjs
+++ b/test/frontend/config-settings.test.mjs
@@ -77,12 +77,12 @@ for (const failedRequest of ['/status', '/pending']) {
let fail = false
let saves = 0
const active = {
- listener: { port: 25, starttlsPort: 587, tlsPort: 465 },
+ listener: { port: 25, starttlsPort: 587, tlsPort: 0, security: 'starttls' },
relay: {
relayHost: 'old.example.com', relayPort: 587, security: 'starttls',
from: 'sender@example.com', fromDomain: 'example.com'
},
- imap: { tlsPort: 993 }, pop3: { port: 110, tlsPort: 995 }, policy: {}
+ imap: { tlsPort: 143 }, pop3: { port: 110, tlsPort: 0 }, policy: {}
}
const { page } = fixture(() => {}, 'mail', { http: {
get: async path => {
@@ -271,9 +271,9 @@ for (const key of ['mail', 'security_policy']) {
let pending = null
let active = {
meta: { schema: 1, generation: 1 },
- listener: { port: 25, starttlsPort: 587, tlsPort: 465 },
+ listener: { port: 25, starttlsPort: 587, tlsPort: 0, security: 'starttls' },
relay: { verificationEnabled: true, loginPolicy: 'new_device' },
- imap: { tlsPort: 993 }, pop3: { port: 110, tlsPort: 995 },
+ imap: { tlsPort: 143 }, pop3: { port: 110, tlsPort: 0 },
policy: { maxMessageBytes: 1234, spamFilterLevel: 1, externalRecoveryEnabled: true }
}
const { page } = fixture(() => {}, key, { http: {
@@ -451,10 +451,10 @@ test('保存并应用一次点击按候选协议连续完成保存和应用', as
const requests = []
let pending = null
const active = {
- listener: { port: 25, starttlsPort: 587, tlsPort: 465 },
+ listener: { port: 25, starttlsPort: 587, tlsPort: 0, security: 'starttls' },
relay: { relayHost: '', relayPort: 0, security: '', from: '', fromDomain: '' },
- imap: { tlsPort: 993 },
- pop3: { port: 110, tlsPort: 995 },
+ imap: { tlsPort: 143 },
+ pop3: { port: 110, tlsPort: 0 },
policy: { maxMessageBytes: 10 << 20, spamFilterLevel: 2 }
}
const { page } = fixture(async path => ({
@@ -494,9 +494,9 @@ test('最近认证过期时在当前页确认密码并自动重试保存和应
let saveAttempts = 0
let prompts = 0
const active = {
- listener: { port: 25, starttlsPort: 587, tlsPort: 465 },
+ listener: { port: 25, starttlsPort: 587, tlsPort: 0, security: 'starttls' },
relay: { relayHost: '', relayPort: 0, security: '', from: '', fromDomain: '' },
- imap: { tlsPort: 993 }, pop3: { port: 110, tlsPort: 995 }, policy: {}
+ imap: { tlsPort: 143 }, pop3: { port: 110, tlsPort: 0 }, policy: {}
}
const recentAuthError = Object.assign(new Error('需要最近认证'), { apiErrorCode: 'RECENT_AUTH_REQUIRED' })
const { page } = fixture(async () => ({ errorNo: 0, data: active }), 'mail', {
@@ -539,8 +539,8 @@ test('最近认证过期时在当前页确认密码并自动重试保存和应
test('取消最近身份确认时保留配置输入且不重试请求', async () => {
let saves = 0
const active = {
- listener: { port: 25, starttlsPort: 587, tlsPort: 465 },
- relay: {}, imap: { tlsPort: 993 }, pop3: { port: 110, tlsPort: 995 }, policy: {}
+ listener: { port: 25, starttlsPort: 587, tlsPort: 0, security: 'starttls' },
+ relay: {}, imap: { tlsPort: 143 }, pop3: { port: 110, tlsPort: 0 }, policy: {}
}
const { page, messages } = fixture(async path => ({
errorNo: 0,
@@ -654,12 +654,12 @@ test('邮件页保持验证、保存、应用端点和代次协议,中继密
const requests = []
let pending = null
const active = {
- listener: { port: 25, starttlsPort: 587, tlsPort: 465 },
+ listener: { port: 25, starttlsPort: 587, tlsPort: 0, security: 'starttls' },
relay: {
relayHost: 'smtp.example.com', relayPort: 587, security: 'starttls',
from: 'sender@example.com', fromDomain: 'example.com'
},
- imap: { tlsPort: 993 }, pop3: { port: 110, tlsPort: 995 }, policy: {}
+ imap: { tlsPort: 143 }, pop3: { port: 110, tlsPort: 0 }, policy: {}
}
const { page } = fixture(async path => ({ errorNo: 0, data: path.endsWith('/status') ? { activeGeneration: 8 } : active }), 'mail', {
http: {
@@ -729,12 +729,12 @@ test('可视化编辑按权限和工作状态保护,编辑会清除过期确
test('前端无效字段阻止网络请求,修正字段后同步清除错误', async () => {
const requests = []
const config = {
- listener: { port: 25, starttlsPort: 587, tlsPort: 465 },
+ listener: { port: 25, starttlsPort: 587, tlsPort: 0, security: 'starttls' },
relay: {
relayHost: 'smtp.example.com', relayPort: 587, security: 'starttls',
from: 'sender@example.com', fromDomain: 'example.com'
},
- imap: { tlsPort: 993 }, pop3: { port: 110, tlsPort: 995 }, policy: {}
+ imap: { tlsPort: 143 }, pop3: { port: 110, tlsPort: 0 }, policy: {}
}
const { page } = fixture(async () => ({ errorNo: 0, data: config }), 'mail', {
http: { post: async path => { requests.push(path); return { errorNo: 0, data: { valid: true } } } }
@@ -789,12 +789,12 @@ test('SQLite 不显示或发送密码,网络数据库和中继密码留空保
}
let request
const mail = {
- listener: { port: 25, starttlsPort: 587, tlsPort: 465 },
+ listener: { port: 25, starttlsPort: 587, tlsPort: 0, security: 'starttls' },
relay: {
relayHost: 'smtp.example.com', relayPort: 587, security: 'starttls',
from: 'sender@example.com', fromDomain: 'example.com'
},
- imap: { tlsPort: 993 }, pop3: { port: 110, tlsPort: 995 }, policy: {}
+ imap: { tlsPort: 143 }, pop3: { port: 110, tlsPort: 0 }, policy: {}
}
const { page } = fixture(async () => ({ errorNo: 0, data: mail }), 'mail', {
http: { post: async (path, body) => { request = body; return { errorNo: 0, data: { valid: true } } } }