统一邮件协议加密方式与端口联动
This commit is contained in:
parent
0975e26762
commit
b7a4614e5b
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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...)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -25,7 +25,13 @@
|
||||
placeholder="请选择"
|
||||
@update:model-value="update"
|
||||
>
|
||||
<el-option v-for="option in selectOptions" :key="String(option.value)" :label="option.label" :value="option.value" />
|
||||
<el-option
|
||||
v-for="option in selectOptions"
|
||||
:key="String(option.value)"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
:disabled="option.disabled === true"
|
||||
/>
|
||||
</el-select>
|
||||
<el-switch
|
||||
v-else-if="field.type === 'switch'"
|
||||
|
||||
@ -6,6 +6,17 @@ const loginOptions = options([
|
||||
const port = (path, label, extra = {}) => 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] = '邮件服务监听端口不能重复'
|
||||
|
||||
@ -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'])
|
||||
})
|
||||
|
||||
@ -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 } } } }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user