fix(*): 邮箱与系统公告 - 合并 PR #417 并修复迁移冲突和提醒关闭逻辑

保留历史数据库迁移谱系与公告按需加载,复用邮件配置快照校验注册域名,补充白名单及公告回归测试。
This commit is contained in:
ddCat
2026-09-05 16:39:45 +08:00
18 changed files with 560 additions and 79 deletions

View File

@@ -28,41 +28,56 @@ const emailSettingKey = "email"
const registrationEmailPurpose = "registration"
const registrationCodeTTL = 10 * time.Minute
var defaultRegistrationEmailDomains = []string{
"gmail.com",
"163.com",
"126.com",
"qq.com",
"outlook.com",
"hotmail.com",
"icloud.com",
"yahoo.com",
"foxmail.com",
}
type EmailSettingRequest struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Encryption string `json:"encryption"`
FromEmail string `json:"fromEmail"`
FromName string `json:"fromName"`
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Encryption string `json:"encryption"`
FromEmail string `json:"fromEmail"`
FromName string `json:"fromName"`
RegistrationAllowedDomains []string `json:"registrationAllowedDomains"`
}
type PublicEmailSetting struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Encryption string `json:"encryption"`
FromEmail string `json:"fromEmail"`
FromName string `json:"fromName"`
FromNameInherited bool `json:"fromNameInherited"`
HasPassword bool `json:"hasPassword"`
UpdatedBy string `json:"updatedBy"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Encryption string `json:"encryption"`
FromEmail string `json:"fromEmail"`
FromName string `json:"fromName"`
FromNameInherited bool `json:"fromNameInherited"`
HasPassword bool `json:"hasPassword"`
RegistrationAllowedDomains []string `json:"registrationAllowedDomains"`
UpdatedBy string `json:"updatedBy"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type emailSettingValue struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Encryption string `json:"encryption"`
FromEmail string `json:"fromEmail"`
FromName string `json:"fromName"`
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Encryption string `json:"encryption"`
FromEmail string `json:"fromEmail"`
FromName string `json:"fromName"`
RegistrationAllowedDomains []string `json:"registrationAllowedDomains"`
}
func (s *Service) AdminEmailSetting(actor *model.User) (*PublicEmailSetting, error) {
@@ -84,7 +99,15 @@ func (s *Service) UpdateEmailSetting(actor *model.User, req EmailSettingRequest)
if err != nil {
return nil, err
}
next := normalizeEmailSetting(emailSettingValue{Enabled: req.Enabled, Host: req.Host, Port: req.Port, Username: req.Username, Password: req.Password, Encryption: req.Encryption, FromEmail: req.FromEmail, FromName: req.FromName})
allowedDomains := req.RegistrationAllowedDomains
if allowedDomains == nil {
allowedDomains = current.RegistrationAllowedDomains
}
next := normalizeEmailSetting(emailSettingValue{
Enabled: req.Enabled, Host: req.Host, Port: req.Port, Username: req.Username, Password: req.Password,
Encryption: req.Encryption, FromEmail: req.FromEmail, FromName: req.FromName,
RegistrationAllowedDomains: allowedDomains,
})
if next.Password == "" {
next.Password = current.Password
}
@@ -146,6 +169,9 @@ func (s *Service) SendRegistrationEmailCode(rawEmail string) error {
if err != nil {
return err
}
if err := validateRegistrationEmailDomain(email, setting.RegistrationAllowedDomains); err != nil {
return err
}
if !setting.Enabled {
return Forbidden("平台尚未启用注册邮件,请联系管理员")
}
@@ -249,6 +275,9 @@ func (s *Service) readEmailSetting() (*model.SystemSetting, emailSettingValue, e
}
func validateEmailSetting(value emailSettingValue) error {
if err := validateRegistrationEmailDomains(value.RegistrationAllowedDomains); err != nil {
return err
}
if !value.Enabled {
return nil
}
@@ -273,6 +302,10 @@ func normalizeEmailSetting(value emailSettingValue) emailSettingValue {
value.Password = strings.TrimSpace(value.Password)
value.FromEmail = normalizeEmail(value.FromEmail)
value.FromName = strings.TrimSpace(value.FromName)
if value.RegistrationAllowedDomains == nil {
value.RegistrationAllowedDomains = append([]string(nil), defaultRegistrationEmailDomains...)
}
value.RegistrationAllowedDomains = normalizeEmailDomains(value.RegistrationAllowedDomains)
if value.Port == 0 {
value.Port = 587
}
@@ -287,7 +320,11 @@ func normalizeEmailSetting(value emailSettingValue) emailSettingValue {
func (s *Service) publicEmailSetting(setting *model.SystemSetting, value emailSettingValue) *PublicEmailSetting {
inherited := value.FromName == "" || value.FromName == defaultAppearanceBrandName
value = resolveEmailSender(value, s.appearanceBrandName())
result := &PublicEmailSetting{Enabled: value.Enabled, Host: value.Host, Port: value.Port, Username: value.Username, Encryption: value.Encryption, FromEmail: value.FromEmail, FromName: value.FromName, FromNameInherited: inherited, HasPassword: value.Password != ""}
result := &PublicEmailSetting{
Enabled: value.Enabled, Host: value.Host, Port: value.Port, Username: value.Username, Encryption: value.Encryption,
FromEmail: value.FromEmail, FromName: value.FromName, FromNameInherited: inherited, HasPassword: value.Password != "",
RegistrationAllowedDomains: value.RegistrationAllowedDomains,
}
if setting != nil {
result.UpdatedBy = setting.UpdatedBy
result.CreatedAt = setting.CreatedAt
@@ -296,6 +333,75 @@ func (s *Service) publicEmailSetting(setting *model.SystemSetting, value emailSe
return result
}
func normalizeEmailDomains(values []string) []string {
result := make([]string, 0, len(values))
seen := make(map[string]bool, len(values))
for _, raw := range values {
value := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(raw), "@")))
value = strings.TrimSuffix(value, ".")
if value == "" || seen[value] {
continue
}
seen[value] = true
result = append(result, value)
}
return result
}
func validateRegistrationEmailDomains(allowed []string) error {
if len(allowed) > 100 {
return BadAuthRequest("电子邮件域名白名单最多填写 100 项")
}
for _, domain := range allowed {
if !validEmailDomain(domain) {
return BadAuthRequest("电子邮件域名格式不正确:" + domain)
}
}
return nil
}
func validEmailDomain(value string) bool {
if len(value) > 253 || !strings.Contains(value, ".") || strings.HasPrefix(value, ".") || strings.HasSuffix(value, ".") {
return false
}
for _, label := range strings.Split(value, ".") {
if label == "" || len(label) > 63 || strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
return false
}
for _, char := range label {
if (char < 'a' || char > 'z') && (char < '0' || char > '9') && char != '-' {
return false
}
}
}
return true
}
func (s *Service) validateRegistrationEmailDomain(email string) error {
_, setting, err := s.readEmailSetting()
if err != nil {
return err
}
return validateRegistrationEmailDomain(email, setting.RegistrationAllowedDomains)
}
func validateRegistrationEmailDomain(email string, allowedDomains []string) error {
parts := strings.Split(email, "@")
if len(parts) != 2 {
return BadAuthRequest("邮箱格式不正确")
}
domain := strings.ToLower(parts[1])
if len(allowedDomains) == 0 {
return nil
}
for _, allowed := range allowedDomains {
if domain == allowed {
return nil
}
}
return BadAuthRequest("该邮箱域名不在管理员设置的白名单内")
}
func resolveEmailSender(value emailSettingValue, brandName string) emailSettingValue {
if value.FromName == "" || value.FromName == defaultAppearanceBrandName {
value.FromName = brandName