fix(user): unify email validation for registration and settings (#39304)

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Abhay Pratap Singh
2026-09-16 17:34:41 +00:00
committed by GitHub
co-authored by silverwind wxiaoguang
parent c6c671e113
commit 7ebb2caa9e
21 changed files with 138 additions and 209 deletions
+24 -74
View File
@@ -7,7 +7,6 @@ package user
import (
"context"
"fmt"
"net/mail"
"strings"
"time"
@@ -22,45 +21,6 @@ import (
"xorm.io/builder"
)
// ErrEmailCharIsNotSupported e-mail address contains unsupported character
type ErrEmailCharIsNotSupported struct {
Email string
}
// IsErrEmailCharIsNotSupported checks if an error is an ErrEmailCharIsNotSupported
func IsErrEmailCharIsNotSupported(err error) bool {
_, ok := err.(ErrEmailCharIsNotSupported)
return ok
}
func (err ErrEmailCharIsNotSupported) Error() string {
return fmt.Sprintf("e-mail address contains unsupported character [email: %s]", err.Email)
}
func (err ErrEmailCharIsNotSupported) Unwrap() error {
return util.ErrInvalidArgument
}
// ErrEmailInvalid represents an error where the email address does not comply with RFC 5322
// or has a leading '-' character
type ErrEmailInvalid struct {
Email string
}
// IsErrEmailInvalid checks if an error is an ErrEmailInvalid
func IsErrEmailInvalid(err error) bool {
_, ok := err.(ErrEmailInvalid)
return ok
}
func (err ErrEmailInvalid) Error() string {
return fmt.Sprintf("e-mail invalid [email: %s]", err.Email)
}
func (err ErrEmailInvalid) Unwrap() error {
return util.ErrInvalidArgument
}
// ErrEmailAlreadyUsed represents a "EmailAlreadyUsed" kind of error.
type ErrEmailAlreadyUsed struct {
Email string
@@ -147,18 +107,34 @@ func InsertEmailAddress(ctx context.Context, email *EmailAddress) (*EmailAddress
return email, nil
}
type ErrEmailInvalid string
func (err ErrEmailInvalid) Error() string {
return string(err)
}
func (err ErrEmailInvalid) Unwrap() error {
return util.ErrInvalidArgument
}
// ValidateEmail check if email is a valid & allowed address
func ValidateEmail(email string) error {
if err := validateEmailBasic(email); err != nil {
return err
if !validation.IsEmailAddressValid(email) {
return ErrEmailInvalid("email address is invalid: " + email)
}
return validateEmailDomain(email)
if !IsEmailDomainAllowed(email) {
return ErrEmailInvalid("email domain is not allowed: " + email)
}
return nil
}
// ValidateEmailForAdmin check if email is a valid address when admins manually add or edit users
func ValidateEmailForAdmin(email string) error {
return validateEmailBasic(email)
// In this case we do not need to check the email domain
if !validation.IsEmailAddressValid(email) {
return ErrEmailInvalid("email address is invalid: " + email)
}
return nil
}
func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) {
@@ -491,37 +467,11 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
})
}
// validateEmailBasic checks whether the email complies with the rules
func validateEmailBasic(email string) error {
if len(email) == 0 {
return ErrEmailInvalid{email}
}
if !globalVars().emailRegexp.MatchString(email) {
return ErrEmailCharIsNotSupported{email}
}
if email[0] == '-' {
return ErrEmailInvalid{email}
}
if _, err := mail.ParseAddress(email); err != nil {
return ErrEmailInvalid{email}
}
return nil
}
// validateEmailDomain checks whether the email domain is allowed or blocked
func validateEmailDomain(email string) error {
if !IsEmailDomainAllowed(email) {
return ErrEmailInvalid{email}
}
return nil
}
func IsEmailDomainAllowed(email string) bool {
localPart, _, _ := strings.CutLast(email, "@")
if strings.ContainsAny(localPart, "%!") && (len(setting.Service.EmailDomainAllowList) > 0 || len(setting.Service.EmailDomainBlockList) > 0) {
return false // percent-hack and bang-path local parts can route mail to a domain other than the listed one
}
if len(setting.Service.EmailDomainAllowList) == 0 {
return !validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email)
}
+15 -51
View File
@@ -150,58 +150,22 @@ func TestListEmails(t *testing.T) {
}
func TestEmailAddressValidate(t *testing.T) {
kases := map[string]error{
"abc@gmail.com": nil,
"132@hotmail.com": nil,
"1-3-2@test.org": nil,
"1.3.2@test.org": nil,
"a_123@test.org.cn": nil,
`first.last@iana.org`: nil,
`first!last@iana.org`: nil,
`first#last@iana.org`: nil,
`first$last@iana.org`: nil,
`first%last@iana.org`: nil,
`first&last@iana.org`: nil,
`first'last@iana.org`: nil,
`first*last@iana.org`: nil,
`first+last@iana.org`: nil,
`first/last@iana.org`: nil,
`first=last@iana.org`: nil,
`first?last@iana.org`: nil,
`first^last@iana.org`: nil,
"first`last@iana.org": nil,
`first{last@iana.org`: nil,
`first|last@iana.org`: nil,
`first}last@iana.org`: nil,
`first~last@iana.org`: nil,
`first;last@iana.org`: user_model.ErrEmailCharIsNotSupported{`first;last@iana.org`},
".233@qq.com": user_model.ErrEmailInvalid{".233@qq.com"},
"!233@qq.com": nil,
"#233@qq.com": nil,
"$233@qq.com": nil,
"%233@qq.com": nil,
"&233@qq.com": nil,
"'233@qq.com": nil,
"*233@qq.com": nil,
"+233@qq.com": nil,
"-233@qq.com": user_model.ErrEmailInvalid{"-233@qq.com"},
"/233@qq.com": nil,
"=233@qq.com": nil,
"?233@qq.com": nil,
"^233@qq.com": nil,
"_233@qq.com": nil,
"`233@qq.com": nil,
"{233@qq.com": nil,
"|233@qq.com": nil,
"}233@qq.com": nil,
"~233@qq.com": nil,
";233@qq.com": user_model.ErrEmailCharIsNotSupported{";233@qq.com"},
"Foo <foo@bar.com>": user_model.ErrEmailCharIsNotSupported{"Foo <foo@bar.com>"},
string([]byte{0xE2, 0x84, 0xAA}): user_model.ErrEmailCharIsNotSupported{string([]byte{0xE2, 0x84, 0xAA})},
cases := map[string]bool{
"": false,
"root@localhost": true,
"user@[192.168.1.2]": true,
"@a": false,
"abc@gmail.com": true,
"abc@gmail.com\n": false,
"Foo <foo@bar.com>": false,
"abc@gmail.com (x)": false,
"jürgen@example.com": false,
"a@foo_bar.com": false,
}
for kase, err := range kases {
t.Run(kase, func(t *testing.T) {
assert.Equal(t, err, user_model.ValidateEmail(kase))
for tc, isValid := range cases {
t.Run(tc, func(t *testing.T) {
err := user_model.ValidateEmail(tc)
assert.Equal(t, err == nil, isValid)
})
}
}
-2
View File
@@ -553,7 +553,6 @@ type globalVarsStruct struct {
transformDiacritics transform.Transformer
replaceCharsHyphenRE *regexp.Regexp
emailToReplacer *strings.Replacer
emailRegexp *regexp.Regexp
systemUserNewFuncs map[int64]func() *User
systemUserNameIdMap map[string]int64
}
@@ -577,7 +576,6 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct {
":", "",
";", "",
),
emailRegexp: regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"),
}
userFuncs := []func() *User{NewGhostUser, NewActionsUser, NewDeployKeyUser, NewCliUser, NewAuthSourceUser}
-15
View File
@@ -297,21 +297,6 @@ func TestDisplayName(t *testing.T) {
}
}
func TestCreateUserInvalidEmail(t *testing.T) {
user := &user_model.User{
Name: "GiteaBot",
Email: "GiteaBot@gitea.io\r\n",
Passwd: ";p['////..-++']",
IsAdmin: false,
Theme: setting.UI.DefaultTheme,
MustChangePassword: false,
}
err := user_model.CreateUser(t.Context(), user, &user_model.Meta{})
assert.Error(t, err)
assert.True(t, user_model.IsErrEmailCharIsNotSupported(err))
}
func TestCreateUserEmailAlreadyUsed(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())