Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/.golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ linters:
- G304
- G306
- G404
- G402
severity: high
confidence: high
errcheck:
Expand Down
87 changes: 55 additions & 32 deletions backend/internal/handler/admin/setting_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
SMTPFrom: settings.SMTPFrom,
SMTPFromName: settings.SMTPFromName,
SMTPUseTLS: settings.SMTPUseTLS,
SMTPSkipTLSVerify: settings.SMTPSkipTLSVerify,
TurnstileEnabled: settings.TurnstileEnabled,
TurnstileSiteKey: settings.TurnstileSiteKey,
TurnstileSecretKeyConfigured: settings.TurnstileSecretKeyConfigured,
Expand Down Expand Up @@ -252,13 +253,14 @@ type UpdateSettingsRequest struct {
TotpEnabled bool `json:"totp_enabled"` // TOTP 双因素认证

// 邮件服务设置
SMTPHost string `json:"smtp_host"`
SMTPPort int `json:"smtp_port"`
SMTPUsername string `json:"smtp_username"`
SMTPPassword string `json:"smtp_password"`
SMTPFrom string `json:"smtp_from_email"`
SMTPFromName string `json:"smtp_from_name"`
SMTPUseTLS bool `json:"smtp_use_tls"`
SMTPHost string `json:"smtp_host"`
SMTPPort int `json:"smtp_port"`
SMTPUsername string `json:"smtp_username"`
SMTPPassword string `json:"smtp_password"`
SMTPFrom string `json:"smtp_from_email"`
SMTPFromName string `json:"smtp_from_name"`
SMTPUseTLS bool `json:"smtp_use_tls"`
SMTPSkipTLSVerify *bool `json:"smtp_skip_tls_verify"`

// Cloudflare Turnstile 设置
TurnstileEnabled bool `json:"turnstile_enabled"`
Expand Down Expand Up @@ -484,6 +486,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
req.SMTPFrom = previousSettings.SMTPFrom
req.SMTPFromName = previousSettings.SMTPFromName
req.SMTPUseTLS = previousSettings.SMTPUseTLS
req.SMTPSkipTLSVerify = &previousSettings.SMTPSkipTLSVerify
}

// Turnstile 参数验证
Expand Down Expand Up @@ -1044,6 +1047,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
SMTPFrom: req.SMTPFrom,
SMTPFromName: req.SMTPFromName,
SMTPUseTLS: req.SMTPUseTLS,
SMTPSkipTLSVerify: func() bool {
if req.SMTPSkipTLSVerify != nil {
return *req.SMTPSkipTLSVerify
}
return previousSettings.SMTPSkipTLSVerify
}(),
TurnstileEnabled: req.TurnstileEnabled,
TurnstileSiteKey: req.TurnstileSiteKey,
TurnstileSecretKey: req.TurnstileSecretKey,
Expand Down Expand Up @@ -1339,6 +1348,7 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
SMTPFrom: updatedSettings.SMTPFrom,
SMTPFromName: updatedSettings.SMTPFromName,
SMTPUseTLS: updatedSettings.SMTPUseTLS,
SMTPSkipTLSVerify: updatedSettings.SMTPSkipTLSVerify,
TurnstileEnabled: updatedSettings.TurnstileEnabled,
TurnstileSiteKey: updatedSettings.TurnstileSiteKey,
TurnstileSecretKeyConfigured: updatedSettings.TurnstileSecretKeyConfigured,
Expand Down Expand Up @@ -1534,6 +1544,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
if before.SMTPUseTLS != after.SMTPUseTLS {
changed = append(changed, "smtp_use_tls")
}
if before.SMTPSkipTLSVerify != after.SMTPSkipTLSVerify {
changed = append(changed, "smtp_skip_tls_verify")
}
if before.TurnstileEnabled != after.TurnstileEnabled {
changed = append(changed, "turnstile_enabled")
}
Expand Down Expand Up @@ -1997,11 +2010,12 @@ func equalNotifyEmailEntries(a, b []service.NotifyEmailEntry) bool {

// TestSMTPRequest 测试SMTP连接请求
type TestSMTPRequest struct {
SMTPHost string `json:"smtp_host"`
SMTPPort int `json:"smtp_port"`
SMTPUsername string `json:"smtp_username"`
SMTPPassword string `json:"smtp_password"`
SMTPUseTLS bool `json:"smtp_use_tls"`
SMTPHost string `json:"smtp_host"`
SMTPPort int `json:"smtp_port"`
SMTPUsername string `json:"smtp_username"`
SMTPPassword string `json:"smtp_password"`
SMTPUseTLS bool `json:"smtp_use_tls"`
SMTPSkipTLSVerify *bool `json:"smtp_skip_tls_verify"`
}

// TestSMTPConnection 测试SMTP连接
Expand Down Expand Up @@ -2034,6 +2048,9 @@ func (h *SettingHandler) TestSMTPConnection(c *gin.Context) {
if req.SMTPUsername == "" && savedConfig != nil {
req.SMTPUsername = savedConfig.Username
}
if req.SMTPSkipTLSVerify == nil && savedConfig != nil {
req.SMTPSkipTLSVerify = &savedConfig.SkipTLSVerify
}
password := strings.TrimSpace(req.SMTPPassword)
if password == "" && savedConfig != nil {
password = savedConfig.Password
Expand All @@ -2044,11 +2061,12 @@ func (h *SettingHandler) TestSMTPConnection(c *gin.Context) {
}

config := &service.SMTPConfig{
Host: req.SMTPHost,
Port: req.SMTPPort,
Username: req.SMTPUsername,
Password: password,
UseTLS: req.SMTPUseTLS,
Host: req.SMTPHost,
Port: req.SMTPPort,
Username: req.SMTPUsername,
Password: password,
UseTLS: req.SMTPUseTLS,
SkipTLSVerify: req.SMTPSkipTLSVerify != nil && *req.SMTPSkipTLSVerify,
}

err := h.emailService.TestSMTPConnectionWithConfig(config)
Expand All @@ -2062,14 +2080,15 @@ func (h *SettingHandler) TestSMTPConnection(c *gin.Context) {

// SendTestEmailRequest 发送测试邮件请求
type SendTestEmailRequest struct {
Email string `json:"email" binding:"required,email"`
SMTPHost string `json:"smtp_host"`
SMTPPort int `json:"smtp_port"`
SMTPUsername string `json:"smtp_username"`
SMTPPassword string `json:"smtp_password"`
SMTPFrom string `json:"smtp_from_email"`
SMTPFromName string `json:"smtp_from_name"`
SMTPUseTLS bool `json:"smtp_use_tls"`
Email string `json:"email" binding:"required,email"`
SMTPHost string `json:"smtp_host"`
SMTPPort int `json:"smtp_port"`
SMTPUsername string `json:"smtp_username"`
SMTPPassword string `json:"smtp_password"`
SMTPFrom string `json:"smtp_from_email"`
SMTPFromName string `json:"smtp_from_name"`
SMTPUseTLS bool `json:"smtp_use_tls"`
SMTPSkipTLSVerify *bool `json:"smtp_skip_tls_verify"`
}

// SendTestEmail 发送测试邮件
Expand Down Expand Up @@ -2104,6 +2123,9 @@ func (h *SettingHandler) SendTestEmail(c *gin.Context) {
if req.SMTPUsername == "" && savedConfig != nil {
req.SMTPUsername = savedConfig.Username
}
if req.SMTPSkipTLSVerify == nil && savedConfig != nil {
req.SMTPSkipTLSVerify = &savedConfig.SkipTLSVerify
}
password := strings.TrimSpace(req.SMTPPassword)
if password == "" && savedConfig != nil {
password = savedConfig.Password
Expand All @@ -2120,13 +2142,14 @@ func (h *SettingHandler) SendTestEmail(c *gin.Context) {
}

config := &service.SMTPConfig{
Host: req.SMTPHost,
Port: req.SMTPPort,
Username: req.SMTPUsername,
Password: password,
From: req.SMTPFrom,
FromName: req.SMTPFromName,
UseTLS: req.SMTPUseTLS,
Host: req.SMTPHost,
Port: req.SMTPPort,
Username: req.SMTPUsername,
Password: password,
From: req.SMTPFrom,
FromName: req.SMTPFromName,
UseTLS: req.SMTPUseTLS,
SkipTLSVerify: req.SMTPSkipTLSVerify != nil && *req.SMTPSkipTLSVerify,
}

siteName := h.settingService.GetSiteName(c.Request.Context())
Expand Down
1 change: 1 addition & 0 deletions backend/internal/handler/dto/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type SystemSettings struct {
SMTPFrom string `json:"smtp_from_email"`
SMTPFromName string `json:"smtp_from_name"`
SMTPUseTLS bool `json:"smtp_use_tls"`
SMTPSkipTLSVerify bool `json:"smtp_skip_tls_verify"`

TurnstileEnabled bool `json:"turnstile_enabled"`
TurnstileSiteKey string `json:"turnstile_site_key"`
Expand Down
16 changes: 9 additions & 7 deletions backend/internal/server/api_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,13 +572,14 @@ func TestAPIContracts(t *testing.T) {
service.SettingKeyRegistrationEmailSuffixWhitelist: "[]",
service.SettingKeyPromoCodeEnabled: "true",

service.SettingKeySMTPHost: "smtp.example.com",
service.SettingKeySMTPPort: "587",
service.SettingKeySMTPUsername: "user",
service.SettingKeySMTPPassword: "secret",
service.SettingKeySMTPFrom: "no-reply@example.com",
service.SettingKeySMTPFromName: "Sub2API",
service.SettingKeySMTPUseTLS: "true",
service.SettingKeySMTPHost: "smtp.example.com",
service.SettingKeySMTPPort: "587",
service.SettingKeySMTPUsername: "user",
service.SettingKeySMTPPassword: "secret",
service.SettingKeySMTPFrom: "no-reply@example.com",
service.SettingKeySMTPFromName: "Sub2API",
service.SettingKeySMTPUseTLS: "true",
service.SettingKeySMTPSkipTLSVerify: "false",

service.SettingKeyTurnstileEnabled: "true",
service.SettingKeyTurnstileSiteKey: "site-key",
Expand Down Expand Up @@ -651,6 +652,7 @@ func TestAPIContracts(t *testing.T) {
"smtp_from_email": "no-reply@example.com",
"smtp_from_name": "Sub2API",
"smtp_use_tls": true,
"smtp_skip_tls_verify": false,
"turnstile_enabled": true,
"turnstile_site_key": "site-key",
"turnstile_secret_key_configured": true,
Expand Down
15 changes: 8 additions & 7 deletions backend/internal/service/domain_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,14 @@ const (
SettingKeyInvitationCodeEnabled = "invitation_code_enabled" // 是否启用邀请码注册

// 邮件服务设置
SettingKeySMTPHost = "smtp_host" // SMTP服务器地址
SettingKeySMTPPort = "smtp_port" // SMTP端口
SettingKeySMTPUsername = "smtp_username" // SMTP用户名
SettingKeySMTPPassword = "smtp_password" // SMTP密码(加密存储)
SettingKeySMTPFrom = "smtp_from" // 发件人地址
SettingKeySMTPFromName = "smtp_from_name" // 发件人名称
SettingKeySMTPUseTLS = "smtp_use_tls" // 是否使用TLS
SettingKeySMTPHost = "smtp_host" // SMTP服务器地址
SettingKeySMTPPort = "smtp_port" // SMTP端口
SettingKeySMTPUsername = "smtp_username" // SMTP用户名
SettingKeySMTPPassword = "smtp_password" // SMTP密码(加密存储)
SettingKeySMTPFrom = "smtp_from" // 发件人地址
SettingKeySMTPFromName = "smtp_from_name" // 发件人名称
SettingKeySMTPUseTLS = "smtp_use_tls" // 是否使用TLS
SettingKeySMTPSkipTLSVerify = "smtp_skip_tls_verify" // 是否跳过 TLS 证书信任校验

// Cloudflare Turnstile 设置
SettingKeyTurnstileEnabled = "turnstile_enabled" // 是否启用 Turnstile 验证
Expand Down
52 changes: 31 additions & 21 deletions backend/internal/service/email_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,14 @@ const (

// SMTPConfig SMTP配置
type SMTPConfig struct {
Host string
Port int
Username string
Password string
From string
FromName string
UseTLS bool
Host string
Port int
Username string
Password string
From string
FromName string
UseTLS bool
SkipTLSVerify bool
}

// EmailService 邮件服务
Expand All @@ -116,6 +117,7 @@ func (s *EmailService) GetSMTPConfig(ctx context.Context) (*SMTPConfig, error) {
SettingKeySMTPFrom,
SettingKeySMTPFromName,
SettingKeySMTPUseTLS,
SettingKeySMTPSkipTLSVerify,
}

settings, err := s.settingRepo.GetMultiple(ctx, keys)
Expand All @@ -136,15 +138,17 @@ func (s *EmailService) GetSMTPConfig(ctx context.Context) (*SMTPConfig, error) {
}

useTLS := settings[SettingKeySMTPUseTLS] == "true"
skipTLSVerify := settings[SettingKeySMTPSkipTLSVerify] == "true"

return &SMTPConfig{
Host: host,
Port: port,
Username: strings.TrimSpace(settings[SettingKeySMTPUsername]),
Password: strings.TrimSpace(settings[SettingKeySMTPPassword]),
From: strings.TrimSpace(settings[SettingKeySMTPFrom]),
FromName: strings.TrimSpace(settings[SettingKeySMTPFromName]),
UseTLS: useTLS,
Host: host,
Port: port,
Username: strings.TrimSpace(settings[SettingKeySMTPUsername]),
Password: strings.TrimSpace(settings[SettingKeySMTPPassword]),
From: strings.TrimSpace(settings[SettingKeySMTPFrom]),
FromName: strings.TrimSpace(settings[SettingKeySMTPFromName]),
UseTLS: useTLS,
SkipTLSVerify: skipTLSVerify,
}, nil
}

Expand Down Expand Up @@ -178,14 +182,14 @@ func (s *EmailService) SendEmailWithConfig(config *SMTPConfig, to, subject, body
auth := smtp.PlainAuth("", config.Username, config.Password, config.Host)

if config.UseTLS {
return s.sendMailTLS(addr, auth, config.From, to, []byte(msg), config.Host)
return s.sendMailTLS(addr, auth, config.From, to, []byte(msg), config.Host, config.SkipTLSVerify)
}

return s.sendMailPlain(addr, auth, config.From, to, []byte(msg), config.Host)
return s.sendMailPlain(addr, auth, config.From, to, []byte(msg), config.Host, config.SkipTLSVerify)
}

// sendMailPlain sends mail without TLS using a dialer with timeout.
func (s *EmailService) sendMailPlain(addr string, auth smtp.Auth, from, to string, msg []byte, host string) error {
func (s *EmailService) sendMailPlain(addr string, auth smtp.Auth, from, to string, msg []byte, host string, skipTLSVerify bool) error {
dialer := &net.Dialer{Timeout: smtpDialTimeout}
conn, err := dialer.Dial("tcp", addr)
if err != nil {
Expand All @@ -203,7 +207,11 @@ func (s *EmailService) sendMailPlain(addr string, auth smtp.Auth, from, to strin
// Opportunistic STARTTLS: upgrade to encrypted connection if the server supports it.
// This mirrors the behavior of smtp.SendMail which we replaced for timeout support.
if ok, _ := client.Extension("STARTTLS"); ok {
if err = client.StartTLS(&tls.Config{ServerName: host, MinVersion: tls.VersionTLS12}); err != nil {
if err = client.StartTLS(&tls.Config{
ServerName: host,
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: skipTLSVerify,
}); err != nil {
return fmt.Errorf("starttls: %w", err)
}
}
Expand Down Expand Up @@ -232,9 +240,10 @@ func (s *EmailService) sendMailPlain(addr string, auth smtp.Auth, from, to strin
}

// sendMailTLS 使用TLS发送邮件
func (s *EmailService) sendMailTLS(addr string, auth smtp.Auth, from, to string, msg []byte, host string) error {
func (s *EmailService) sendMailTLS(addr string, auth smtp.Auth, from, to string, msg []byte, host string, skipTLSVerify bool) error {
tlsConfig := &tls.Config{
ServerName: host,
ServerName: host,
InsecureSkipVerify: skipTLSVerify,
// 强制 TLS 1.2+,避免协议降级导致的弱加密风险。
MinVersion: tls.VersionTLS12,
}
Expand Down Expand Up @@ -420,7 +429,8 @@ func (s *EmailService) TestSMTPConnectionWithConfig(config *SMTPConfig) error {

if config.UseTLS {
tlsConfig := &tls.Config{
ServerName: config.Host,
ServerName: config.Host,
InsecureSkipVerify: config.SkipTLSVerify,
// 与发送逻辑一致,显式要求 TLS 1.2+。
MinVersion: tls.VersionTLS12,
}
Expand Down
3 changes: 3 additions & 0 deletions backend/internal/service/setting_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,7 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting
updates[SettingKeySMTPFrom] = settings.SMTPFrom
updates[SettingKeySMTPFromName] = settings.SMTPFromName
updates[SettingKeySMTPUseTLS] = strconv.FormatBool(settings.SMTPUseTLS)
updates[SettingKeySMTPSkipTLSVerify] = strconv.FormatBool(settings.SMTPSkipTLSVerify)

// Cloudflare Turnstile 设置(只有非空才更新密钥)
updates[SettingKeyTurnstileEnabled] = strconv.FormatBool(settings.TurnstileEnabled)
Expand Down Expand Up @@ -1614,6 +1615,7 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
SettingKeyForceEmailOnThirdPartySignup: "false",
SettingKeySMTPPort: "587",
SettingKeySMTPUseTLS: "false",
SettingKeySMTPSkipTLSVerify: "false",
// Model fallback defaults
SettingKeyEnableModelFallback: "false",
SettingKeyFallbackModelAnthropic: "claude-3-5-sonnet-20241022",
Expand Down Expand Up @@ -1663,6 +1665,7 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
SMTPFrom: settings[SettingKeySMTPFrom],
SMTPFromName: settings[SettingKeySMTPFromName],
SMTPUseTLS: settings[SettingKeySMTPUseTLS] == "true",
SMTPSkipTLSVerify: settings[SettingKeySMTPSkipTLSVerify] == "true",
SMTPPasswordConfigured: settings[SettingKeySMTPPassword] != "",
TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true",
TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey],
Expand Down
13 changes: 13 additions & 0 deletions backend/internal/service/setting_service_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,16 @@ func TestSettingService_UpdateSettings_RejectsInvalidPaymentVisibleMethodSource(
require.Equal(t, "INVALID_PAYMENT_VISIBLE_METHOD_SOURCE", infraerrors.Reason(err))
require.Nil(t, repo.updates)
}

func TestSettingService_UpdateSettings_SMTPFlags(t *testing.T) {
repo := &settingUpdateRepoStub{}
svc := NewSettingService(repo, &config.Config{})

err := svc.UpdateSettings(context.Background(), &SystemSettings{
SMTPUseTLS: true,
SMTPSkipTLSVerify: true,
})
require.NoError(t, err)
require.Equal(t, "true", repo.updates[SettingKeySMTPUseTLS])
require.Equal(t, "true", repo.updates[SettingKeySMTPSkipTLSVerify])
}
1 change: 1 addition & 0 deletions backend/internal/service/settings_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type SystemSettings struct {
SMTPFrom string
SMTPFromName string
SMTPUseTLS bool
SMTPSkipTLSVerify bool

TurnstileEnabled bool
TurnstileSiteKey string
Expand Down
Loading
Loading