diff --git a/models/user/email_address.go b/models/user/email_address.go index 8c6f24e57b..b14ff7886c 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -7,8 +7,6 @@ package user import ( "context" "fmt" - "net/mail" - "regexp" "strings" "time" @@ -18,53 +16,10 @@ import ( "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/validation" "xorm.io/builder" ) -// ErrEmailNotActivated e-mail address has not been activated error -var ErrEmailNotActivated = util.NewInvalidArgumentErrorf("e-mail address has not been activated") - -// 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 @@ -156,22 +111,6 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error { return err } -var 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])?)*$") - -// ValidateEmail check if email is a valid & allowed address -func ValidateEmail(email string) error { - if err := validateEmailBasic(email); err != nil { - return err - } - return validateEmailDomain(email) -} - -// 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 -} - func GetEmailAddressByEmail(ctx context.Context, email string) (*EmailAddress, error) { ea := &EmailAddress{} if has, err := db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(ea); err != nil { @@ -462,41 +401,3 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate return committer.Commit() } - -// validateEmailBasic checks whether the email complies with the rules -func validateEmailBasic(email string) error { - if len(email) == 0 { - return ErrEmailInvalid{email} - } - - if !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 { - if len(setting.Service.EmailDomainAllowList) == 0 { - return !validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) - } - - return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) -} diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go index b918f21018..b00b91c5f2 100644 --- a/models/user/email_address_test.go +++ b/models/user/email_address_test.go @@ -130,63 +130,6 @@ func TestListEmails(t *testing.T) { assert.Greater(t, count, int64(len(emails))) } -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 ": user_model.ErrEmailCharIsNotSupported{"Foo "}, - string([]byte{0xE2, 0x84, 0xAA}): user_model.ErrEmailCharIsNotSupported{string([]byte{0xE2, 0x84, 0xAA})}, - } - for kase, err := range kases { - t.Run(kase, func(t *testing.T) { - assert.EqualValues(t, err, user_model.ValidateEmail(kase)) - }) - } -} - func TestGetActivatedEmailAddresses(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/user/user.go b/models/user/user.go index c538d56ed1..43bab4f3e9 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -717,11 +717,11 @@ func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefa } if createdByAdmin { - if err := ValidateEmailForAdmin(u.Email); err != nil { + if err := validation.ValidateEmailForAdmin(u.Email); err != nil { return err } } else { - if err := ValidateEmail(u.Email); err != nil { + if err := validation.ValidateEmail(u.Email); err != nil { return err } } @@ -885,7 +885,7 @@ func (u User) Validate() []string { if err := ValidateUser(&u); err != nil { result = append(result, err.Error()) } - if err := ValidateEmail(u.Email); err != nil { + if err := validation.ValidateEmail(u.Email); err != nil { result = append(result, err.Error()) } return result diff --git a/models/user/user_test.go b/models/user/user_test.go index 8f4350f776..082c21063c 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -320,7 +321,7 @@ func TestCreateUserInvalidEmail(t *testing.T) { err := user_model.CreateUser(db.DefaultContext, user) require.Error(t, err) - assert.True(t, user_model.IsErrEmailCharIsNotSupported(err)) + assert.True(t, validation.IsErrEmailCharIsNotSupported(err)) } func TestCreateUserEmailAlreadyUsed(t *testing.T) { diff --git a/modules/structs/admin_user.go b/modules/structs/admin_user.go index 5b7df127b4..8a4d066d72 100644 --- a/modules/structs/admin_user.go +++ b/modules/structs/admin_user.go @@ -15,7 +15,7 @@ type CreateUserOption struct { FullName string `json:"full_name" binding:"MaxSize(100)"` // required: true // swagger:strfmt email - Email string `json:"email" binding:"Required;Email;MaxSize(254)"` + Email string `json:"email" binding:"Required;EmailForAdmin;MaxSize(254)"` Password string `json:"password" binding:"MaxSize(255)"` MustChangePassword *bool `json:"must_change_password"` SendNotify bool `json:"send_notify"` diff --git a/modules/structs/user_email.go b/modules/structs/user_email.go index 9319667e8f..485d0de1af 100644 --- a/modules/structs/user_email.go +++ b/modules/structs/user_email.go @@ -7,7 +7,7 @@ package structs // Email an email address belonging to a user type Email struct { // swagger:strfmt email - Email string `json:"email"` + Email string `json:"email" binding:"EmailWithAllowedDomain"` Verified bool `json:"verified"` Primary bool `json:"primary"` UserID int64 `json:"user_id"` diff --git a/modules/validation/binding.go b/modules/validation/binding.go index cb0a5063e5..fe434500d5 100644 --- a/modules/validation/binding.go +++ b/modules/validation/binding.go @@ -26,6 +26,8 @@ const ( ErrUsername = "UsernameError" // ErrInvalidGroupTeamMap is returned when a group team mapping is invalid ErrInvalidGroupTeamMap = "InvalidGroupTeamMap" + // ErrEmail is returned when an email address is invalid + ErrEmail = "Email" ) // AddBindingRules adds additional binding rules @@ -38,6 +40,7 @@ func AddBindingRules() { addGlobOrRegexPatternRule() addUsernamePatternRule() addValidGroupTeamMapRule() + addEmailBindingRules() } func addGitRefNameBindingRule() { @@ -185,6 +188,34 @@ func addValidGroupTeamMapRule() { }) } +func addEmailBindingRules() { + binding.AddRule(&binding.Rule{ + IsMatch: func(rule string) bool { + return strings.HasPrefix(rule, "EmailWithAllowedDomain") + }, + IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) { + if err := ValidateEmail(fmt.Sprintf("%v", val)); err != nil { + errs.Add([]string{name}, ErrEmail, err.Error()) + return false, errs + } + return true, errs + }, + }) + + binding.AddRule(&binding.Rule{ + IsMatch: func(rule string) bool { + return strings.HasPrefix(rule, "EmailForAdmin") + }, + IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) { + if err := ValidateEmailForAdmin(fmt.Sprintf("%v", val)); err != nil { + errs.Add([]string{name}, ErrEmail, err.Error()) + return false, errs + } + return true, errs + }, + }) +} + func portOnly(hostport string) string { colon := strings.IndexByte(hostport, ':') if colon == -1 { diff --git a/modules/validation/email.go b/modules/validation/email.go new file mode 100644 index 0000000000..bef816586f --- /dev/null +++ b/modules/validation/email.go @@ -0,0 +1,131 @@ +// Copyright 2016 The Gogs Authors. All rights reserved. +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package validation + +import ( + "fmt" + "net/mail" + "regexp" + "strings" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + + "github.com/gobwas/glob" +) + +// ErrEmailNotActivated e-mail address has not been activated error +var ErrEmailNotActivated = util.NewInvalidArgumentErrorf("e-mail address has not been activated") + +// 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) +} + +// 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 +} + +var 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])?)*$") + +// check if email is a valid address with allowed domain +func ValidateEmail(email string) error { + if err := validateEmailBasic(email); err != nil { + return err + } + return validateEmailDomain(email) +} + +// 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 +} + +// validateEmailBasic checks whether the email complies with the rules +func validateEmailBasic(email string) error { + if len(email) == 0 { + return ErrEmailInvalid{email} + } + + if !emailRegexp.MatchString(email) { + return ErrEmailCharIsNotSupported{email} + } + + if email[0] == '-' { + return ErrEmailInvalid{email} + } + + if _, err := mail.ParseAddress(email); err != nil { + return ErrEmailInvalid{email} + } + + return nil +} + +func validateEmailDomain(email string) error { + if !IsEmailDomainAllowed(email) { + return ErrEmailInvalid{email} + } + + return nil +} + +func IsEmailDomainAllowed(email string) bool { + if len(setting.Service.EmailDomainAllowList) == 0 { + return !isEmailDomainListed(setting.Service.EmailDomainBlockList, email) + } + + return isEmailDomainListed(setting.Service.EmailDomainAllowList, email) +} + +// isEmailDomainListed checks whether the domain of an email address +// matches a list of domains +func isEmailDomainListed(globs []glob.Glob, email string) bool { + if len(globs) == 0 { + return false + } + + n := strings.LastIndex(email, "@") + if n <= 0 { + return false + } + + domain := strings.ToLower(email[n+1:]) + + for _, g := range globs { + if g.Match(domain) { + return true + } + } + + return false +} diff --git a/modules/validation/email_test.go b/modules/validation/email_test.go new file mode 100644 index 0000000000..e5125a9357 --- /dev/null +++ b/modules/validation/email_test.go @@ -0,0 +1,67 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package validation + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +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`: ErrEmailCharIsNotSupported{`first;last@iana.org`}, + ".233@qq.com": 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": 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": ErrEmailCharIsNotSupported{";233@qq.com"}, + "Foo ": ErrEmailCharIsNotSupported{"Foo "}, + string([]byte{0xE2, 0x84, 0xAA}): ErrEmailCharIsNotSupported{string([]byte{0xE2, 0x84, 0xAA})}, + } + for kase, err := range kases { + t.Run(kase, func(t *testing.T) { + assert.EqualValues(t, err, ValidateEmail(kase)) + }) + } +} diff --git a/modules/validation/helpers.go b/modules/validation/helpers.go index 567ad867fe..2f88fcbc60 100644 --- a/modules/validation/helpers.go +++ b/modules/validation/helpers.go @@ -10,8 +10,6 @@ import ( "strings" "code.gitea.io/gitea/modules/setting" - - "github.com/gobwas/glob" ) var externalTrackerRegex = regexp.MustCompile(`({?)(?:user|repo|index)+?(}?)`) @@ -50,29 +48,6 @@ func IsValidSiteURL(uri string) bool { return false } -// IsEmailDomainListed checks whether the domain of an email address -// matches a list of domains -func IsEmailDomainListed(globs []glob.Glob, email string) bool { - if len(globs) == 0 { - return false - } - - n := strings.LastIndex(email, "@") - if n <= 0 { - return false - } - - domain := strings.ToLower(email[n+1:]) - - for _, g := range globs { - if g.Match(domain) { - return true - } - } - - return false -} - // IsAPIURL checks if URL is current Gitea instance API URL func IsAPIURL(uri string) bool { return strings.HasPrefix(strings.ToLower(uri), strings.ToLower(setting.AppURL+"api")) diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index 8fa71a81bd..bc0735b2cd 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -143,6 +143,8 @@ func Validate(errs binding.Errors, data map[string]any, f any, l translation.Loc } case validation.ErrInvalidGroupTeamMap: data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message) + case validation.ErrEmail: + data["ErrorMsg"] = trName + l.TrString("form.email_error") default: msg := errs[0].Classification if msg != "" && errs[0].Message != "" { diff --git a/routers/api/v1/activitypub/repository_test.go b/routers/api/v1/activitypub/repository_test.go index acd588d99b..1e5af6acac 100644 --- a/routers/api/v1/activitypub/repository_test.go +++ b/routers/api/v1/activitypub/repository_test.go @@ -6,22 +6,22 @@ package activitypub import ( "testing" - "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/validation" ) func Test_UserEmailValidate(t *testing.T) { sut := "ab@cd.ef" - if err := user.ValidateEmail(sut); err != nil { + if err := validation.ValidateEmail(sut); err != nil { t.Errorf("sut should be valid, %v, %v", sut, err) } sut = "83ce13c8-af0b-4112-8327-55a54e54e664@code.cartoon-aa.xyz" - if err := user.ValidateEmail(sut); err != nil { + if err := validation.ValidateEmail(sut); err != nil { t.Errorf("sut should be valid, %v, %v", sut, err) } sut = "1" - if err := user.ValidateEmail(sut); err == nil { + if err := validation.ValidateEmail(sut); err == nil { t.Errorf("sut should not be valid, %v", sut) } } diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 9ea210ee4e..b17200381a 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/api/v1/utils" @@ -138,8 +139,8 @@ func CreateUser(ctx *context.APIContext) { user_model.IsErrEmailAlreadyUsed(err) || db.IsErrNameReserved(err) || db.IsErrNameCharsNotAllowed(err) || - user_model.IsErrEmailCharIsNotSupported(err) || - user_model.IsErrEmailInvalid(err) || + validation.IsErrEmailCharIsNotSupported(err) || + validation.IsErrEmailInvalid(err) || db.IsErrNamePatternNotAllowed(err) { ctx.Error(http.StatusUnprocessableEntity, "", err) } else { @@ -148,7 +149,7 @@ func CreateUser(ctx *context.APIContext) { return } - if !user_model.IsEmailDomainAllowed(u.Email) { + if !validation.IsEmailDomainAllowed(u.Email) { ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", u.Email)) } @@ -224,7 +225,7 @@ func EditUser(ctx *context.APIContext) { if form.Email != nil { if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil { switch { - case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err): + case validation.IsErrEmailCharIsNotSupported(err), validation.IsErrEmailInvalid(err): ctx.Error(http.StatusBadRequest, "EmailInvalid", err) case user_model.IsErrEmailAlreadyUsed(err): ctx.Error(http.StatusBadRequest, "EmailUsed", err) @@ -234,7 +235,7 @@ func EditUser(ctx *context.APIContext) { return } - if !user_model.IsEmailDomainAllowed(*form.Email) { + if !validation.IsEmailDomainAllowed(*form.Email) { ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email)) } } diff --git a/routers/api/v1/user/email.go b/routers/api/v1/user/email.go index 33aa851a80..1e9d0984df 100644 --- a/routers/api/v1/user/email.go +++ b/routers/api/v1/user/email.go @@ -9,6 +9,7 @@ import ( user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" @@ -66,12 +67,12 @@ func AddEmail(ctx *context.APIContext) { if err := user_service.AddEmailAddresses(ctx, ctx.Doer, form.Emails); err != nil { if user_model.IsErrEmailAlreadyUsed(err) { ctx.Error(http.StatusUnprocessableEntity, "", "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email) - } else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) { + } else if validation.IsErrEmailCharIsNotSupported(err) || validation.IsErrEmailInvalid(err) { email := "" - if typedError, ok := err.(user_model.ErrEmailInvalid); ok { + if typedError, ok := err.(validation.ErrEmailInvalid); ok { email = typedError.Email } - if typedError, ok := err.(user_model.ErrEmailCharIsNotSupported); ok { + if typedError, ok := err.(validation.ErrEmailCharIsNotSupported); ok { email = typedError.Email } diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index 25fef5fa2e..fb4a1ffa61 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/explore" user_setting "code.gitea.io/gitea/routers/web/user/setting" @@ -185,7 +186,7 @@ func NewUserPost(ctx *context.Context) { case user_model.IsErrEmailAlreadyUsed(err): ctx.Data["Err_Email"] = true ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form) - case user_model.IsErrEmailInvalid(err), user_model.IsErrEmailCharIsNotSupported(err): + case validation.IsErrEmailInvalid(err), validation.IsErrEmailCharIsNotSupported(err): ctx.Data["Err_Email"] = true ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form) case db.IsErrNameReserved(err): @@ -203,7 +204,7 @@ func NewUserPost(ctx *context.Context) { return } - if !user_model.IsEmailDomainAllowed(u.Email) { + if !validation.IsEmailDomainAllowed(u.Email) { ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", u.Email)) } @@ -414,7 +415,7 @@ func EditUserPost(ctx *context.Context) { if form.Email != "" { if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, u, form.Email); err != nil { switch { - case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err): + case validation.IsErrEmailCharIsNotSupported(err), validation.IsErrEmailInvalid(err): ctx.Data["Err_Email"] = true ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form) case user_model.IsErrEmailAlreadyUsed(err): @@ -425,7 +426,7 @@ func EditUserPost(ctx *context.Context) { } return } - if !user_model.IsEmailDomainAllowed(form.Email) { + if !validation.IsEmailDomainAllowed(form.Email) { ctx.Flash.Warning(ctx.Tr("form.email_domain_is_not_allowed", form.Email)) } } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index bb20309030..1f5ebef7d7 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web/middleware" auth_service "code.gitea.io/gitea/services/auth" @@ -575,10 +576,10 @@ func createUserInContext(ctx *context.Context, tpl base.TplName, form any, u *us case user_model.IsErrEmailAlreadyUsed(err): ctx.Data["Err_Email"] = true ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, form) - case user_model.IsErrEmailCharIsNotSupported(err): + case validation.IsErrEmailCharIsNotSupported(err): ctx.Data["Err_Email"] = true ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form) - case user_model.IsErrEmailInvalid(err): + case validation.IsErrEmailInvalid(err): ctx.Data["Err_Email"] = true ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form) case db.IsErrNameReserved(err): diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 45c36743e8..df9de4af98 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" @@ -131,7 +132,7 @@ func TeamsAction(ctx *context.Context) { u, err = user_model.GetUserByName(ctx, uname) if err != nil { if user_model.IsErrUserNotExist(err) { - if setting.MailService != nil && user_model.ValidateEmail(uname) == nil { + if setting.MailService != nil && validation.ValidateEmail(uname) == nil { if err := org_service.CreateTeamInvite(ctx, ctx.Doer, ctx.Org.Team, uname); err != nil { if org_model.IsErrTeamInviteAlreadyExist(err) { ctx.Flash.Error(ctx.Tr("form.duplicate_invite_to_team")) diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go index 3a2527cdc0..7e4d45e991 100644 --- a/routers/web/user/setting/account.go +++ b/routers/web/user/setting/account.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth/source/db" @@ -205,7 +206,7 @@ func EmailPost(ctx *context.Context) { loadAccountData(ctx) ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form) - } else if user_model.IsErrEmailCharIsNotSupported(err) || user_model.IsErrEmailInvalid(err) { + } else if validation.IsErrEmailCharIsNotSupported(err) || validation.IsErrEmailInvalid(err) { loadAccountData(ctx) ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form) diff --git a/services/auth/source/pam/source_authenticate.go b/services/auth/source/pam/source_authenticate.go index addd1bd2c9..0df0b2bca1 100644 --- a/services/auth/source/pam/source_authenticate.go +++ b/services/auth/source/pam/source_authenticate.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/auth/pam" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/validation" "github.com/google/uuid" ) @@ -39,13 +40,13 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u if idx > -1 { username = pamLogin[:idx] } - if user_model.ValidateEmail(email) != nil { + if validation.ValidateEmail(email) != nil { if source.EmailDomain != "" { email = fmt.Sprintf("%s@%s", username, source.EmailDomain) } else { email = fmt.Sprintf("%s@%s", username, setting.Service.NoReplyAddress) } - if user_model.ValidateEmail(email) != nil { + if validation.ValidateEmail(email) != nil { email = uuid.New().String() + "@localhost" } } diff --git a/services/doctor/breaking.go b/services/doctor/breaking.go index 77e3d4e8ef..683ec97389 100644 --- a/services/doctor/breaking.go +++ b/services/doctor/breaking.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/validation" "xorm.io/builder" ) @@ -31,7 +32,7 @@ func iterateUserAccounts(ctx context.Context, each func(*user.User) error) error func checkUserEmail(ctx context.Context, logger log.Logger, _ bool) error { // We could use quirky SQL to get all users that start without a [a-zA-Z0-9], but that would mean // DB provider-specific SQL and only works _now_. So instead we iterate through all user accounts - // and use the user.ValidateEmail function to be future-proof. + // and use the validation.ValidateEmail function to be future-proof. var invalidUserCount int64 if err := iterateUserAccounts(ctx, func(u *user.User) error { // Only check for users, skip @@ -39,7 +40,7 @@ func checkUserEmail(ctx context.Context, logger log.Logger, _ bool) error { return nil } - if err := user.ValidateEmail(u.Email); err != nil { + if err := validation.ValidateEmail(u.Email); err != nil { invalidUserCount++ logger.Warn("User[id=%d name=%q] have not a valid e-mail: %v", u.ID, u.Name, err) } diff --git a/services/forms/admin.go b/services/forms/admin.go index 7d46904440..d71fc076fc 100644 --- a/services/forms/admin.go +++ b/services/forms/admin.go @@ -18,7 +18,7 @@ type AdminCreateUserForm struct { LoginType string `binding:"Required"` LoginName string UserName string `binding:"Required;Username;MaxSize(40)"` - Email string `binding:"Required;Email;MaxSize(254)"` + Email string `binding:"Required;EmailForAdmin;MaxSize(254)"` Password string `binding:"MaxSize(255)"` SendNotify bool MustChangePassword bool @@ -37,7 +37,7 @@ type AdminEditUserForm struct { UserName string `binding:"Username;MaxSize(40)"` LoginName string FullName string `binding:"MaxSize(100)"` - Email string `binding:"Required;Email;MaxSize(254)"` + Email string `binding:"Required;EmailForAdmin;MaxSize(254)"` Password string `binding:"MaxSize(255)"` Website string `binding:"ValidUrl;MaxSize(255)"` Location string `binding:"MaxSize(50)"` diff --git a/services/forms/user_form.go b/services/forms/user_form.go index cc93b27e2a..67c8f00715 100644 --- a/services/forms/user_form.go +++ b/services/forms/user_form.go @@ -10,9 +10,9 @@ import ( "strings" auth_model "code.gitea.io/gitea/models/auth" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/services/context" @@ -110,7 +110,7 @@ func (f *RegisterForm) Validate(req *http.Request, errs binding.Errors) binding. // domains in the whitelist or if it doesn't match any of // domains in the blocklist, if any such list is not empty. func (f *RegisterForm) IsEmailDomainAllowed() bool { - return user_model.IsEmailDomainAllowed(f.Email) + return validation.IsEmailDomainAllowed(f.Email) } // MustChangePasswordForm form for updating your password after account creation @@ -258,7 +258,7 @@ const ( type AvatarForm struct { Source string Avatar *multipart.FileHeader - Gravatar string `binding:"OmitEmpty;Email;MaxSize(254)"` + Gravatar string `binding:"OmitEmpty;EmailWithAllowedDomain;MaxSize(254)"` Federavatar bool } @@ -270,7 +270,7 @@ func (f *AvatarForm) Validate(req *http.Request, errs binding.Errors) binding.Er // AddEmailForm form for adding new email type AddEmailForm struct { - Email string `binding:"Required;Email;MaxSize(254)"` + Email string `binding:"Required;EmailWithAllowedDomain;MaxSize(254)"` } // Validate validates the fields diff --git a/services/forms/user_form_auth_openid.go b/services/forms/user_form_auth_openid.go index ca1c77e320..595bfecad9 100644 --- a/services/forms/user_form_auth_openid.go +++ b/services/forms/user_form_auth_openid.go @@ -27,7 +27,7 @@ func (f *SignInOpenIDForm) Validate(req *http.Request, errs binding.Errors) bind // SignUpOpenIDForm form for signin up with OpenID type SignUpOpenIDForm struct { UserName string `binding:"Required;Username;MaxSize(40)"` - Email string `binding:"Required;Email;MaxSize(254)"` + Email string `binding:"Required;EmailWithAllowedDomain;MaxSize(254)"` } // Validate validates the fields diff --git a/services/user/email.go b/services/user/email.go index e8725267f4..31404aadaa 100644 --- a/services/user/email.go +++ b/services/user/email.go @@ -12,6 +12,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/services/mailer" ) @@ -21,7 +22,7 @@ func AdminAddOrSetPrimaryEmailAddress(ctx context.Context, u *user_model.User, e return nil } - if err := user_model.ValidateEmailForAdmin(emailStr); err != nil { + if err := validation.ValidateEmailForAdmin(emailStr); err != nil { return err } @@ -74,7 +75,7 @@ func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailSt return nil } - if err := user_model.ValidateEmail(emailStr); err != nil { + if err := validation.ValidateEmail(emailStr); err != nil { return err } @@ -119,7 +120,7 @@ func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailSt func AddEmailAddresses(ctx context.Context, u *user_model.User, emails []string) error { for _, emailStr := range emails { - if err := user_model.ValidateEmail(emailStr); err != nil { + if err := validation.ValidateEmail(emailStr); err != nil { return err }