mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	Restrict email address validation (#17688)
This didn't follow the RFC but it's a subset of that. I think we should narrow the allowed chars at first and discuss more possibility in future PRs.
This commit is contained in:
		@@ -10,6 +10,7 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/mail"
 | 
						"net/mail"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models/db"
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
@@ -22,7 +23,22 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ErrEmailNotActivated e-mail address has not been activated error
 | 
					// ErrEmailNotActivated e-mail address has not been activated error
 | 
				
			||||||
var ErrEmailNotActivated = errors.New("E-mail address has not been activated")
 | 
					var ErrEmailNotActivated = errors.New("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
 | 
					// ErrEmailInvalid represents an error where the email address does not comply with RFC 5322
 | 
				
			||||||
type ErrEmailInvalid struct {
 | 
					type ErrEmailInvalid struct {
 | 
				
			||||||
@@ -106,12 +122,24 @@ func (email *EmailAddress) BeforeInsert() {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 allowed address
 | 
					// ValidateEmail check if email is a allowed address
 | 
				
			||||||
func ValidateEmail(email string) error {
 | 
					func ValidateEmail(email string) error {
 | 
				
			||||||
	if len(email) == 0 {
 | 
						if len(email) == 0 {
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !emailRegexp.MatchString(email) {
 | 
				
			||||||
 | 
							return ErrEmailCharIsNotSupported{email}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !(email[0] >= 'a' && email[0] <= 'z') &&
 | 
				
			||||||
 | 
							!(email[0] >= 'A' && email[0] <= 'Z') &&
 | 
				
			||||||
 | 
							!(email[0] >= '0' && email[0] <= '9') {
 | 
				
			||||||
 | 
							return ErrEmailInvalid{email}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if _, err := mail.ParseAddress(email); err != nil {
 | 
						if _, err := mail.ParseAddress(email); err != nil {
 | 
				
			||||||
		return ErrEmailInvalid{email}
 | 
							return ErrEmailInvalid{email}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -252,3 +252,58 @@ func TestListEmails(t *testing.T) {
 | 
				
			|||||||
	assert.Len(t, emails, 5)
 | 
						assert.Len(t, emails, 5)
 | 
				
			||||||
	assert.Greater(t, count, int64(len(emails)))
 | 
						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`:            ErrEmailCharIsNotSupported{`first;last@iana.org`},
 | 
				
			||||||
 | 
							".233@qq.com":                    ErrEmailInvalid{".233@qq.com"},
 | 
				
			||||||
 | 
							"!233@qq.com":                    ErrEmailInvalid{"!233@qq.com"},
 | 
				
			||||||
 | 
							"#233@qq.com":                    ErrEmailInvalid{"#233@qq.com"},
 | 
				
			||||||
 | 
							"$233@qq.com":                    ErrEmailInvalid{"$233@qq.com"},
 | 
				
			||||||
 | 
							"%233@qq.com":                    ErrEmailInvalid{"%233@qq.com"},
 | 
				
			||||||
 | 
							"&233@qq.com":                    ErrEmailInvalid{"&233@qq.com"},
 | 
				
			||||||
 | 
							"'233@qq.com":                    ErrEmailInvalid{"'233@qq.com"},
 | 
				
			||||||
 | 
							"*233@qq.com":                    ErrEmailInvalid{"*233@qq.com"},
 | 
				
			||||||
 | 
							"+233@qq.com":                    ErrEmailInvalid{"+233@qq.com"},
 | 
				
			||||||
 | 
							"/233@qq.com":                    ErrEmailInvalid{"/233@qq.com"},
 | 
				
			||||||
 | 
							"=233@qq.com":                    ErrEmailInvalid{"=233@qq.com"},
 | 
				
			||||||
 | 
							"?233@qq.com":                    ErrEmailInvalid{"?233@qq.com"},
 | 
				
			||||||
 | 
							"^233@qq.com":                    ErrEmailInvalid{"^233@qq.com"},
 | 
				
			||||||
 | 
							"`233@qq.com":                    ErrEmailInvalid{"`233@qq.com"},
 | 
				
			||||||
 | 
							"{233@qq.com":                    ErrEmailInvalid{"{233@qq.com"},
 | 
				
			||||||
 | 
							"|233@qq.com":                    ErrEmailInvalid{"|233@qq.com"},
 | 
				
			||||||
 | 
							"}233@qq.com":                    ErrEmailInvalid{"}233@qq.com"},
 | 
				
			||||||
 | 
							"~233@qq.com":                    ErrEmailInvalid{"~233@qq.com"},
 | 
				
			||||||
 | 
							";233@qq.com":                    ErrEmailCharIsNotSupported{";233@qq.com"},
 | 
				
			||||||
 | 
							"Foo <foo@bar.com>":              ErrEmailCharIsNotSupported{"Foo <foo@bar.com>"},
 | 
				
			||||||
 | 
							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))
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -644,6 +644,15 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e
 | 
				
			|||||||
		u.Visibility = overwriteDefault[0].Visibility
 | 
							u.Visibility = overwriteDefault[0].Visibility
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// validate data
 | 
				
			||||||
 | 
						if err := validateUser(u); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := ValidateEmail(u.Email); err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx, committer, err := db.TxContext()
 | 
						ctx, committer, err := db.TxContext()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
@@ -652,11 +661,6 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	sess := db.GetEngine(ctx)
 | 
						sess := db.GetEngine(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// validate data
 | 
					 | 
				
			||||||
	if err := validateUser(u); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	isExist, err := isUserExist(sess, 0, u.Name)
 | 
						isExist, err := isUserExist(sess, 0, u.Name)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -232,7 +232,7 @@ func TestCreateUserInvalidEmail(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	err := CreateUser(user)
 | 
						err := CreateUser(user)
 | 
				
			||||||
	assert.Error(t, err)
 | 
						assert.Error(t, err)
 | 
				
			||||||
	assert.True(t, IsErrEmailInvalid(err))
 | 
						assert.True(t, IsErrEmailCharIsNotSupported(err))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestCreateUserEmailAlreadyUsed(t *testing.T) {
 | 
					func TestCreateUserEmailAlreadyUsed(t *testing.T) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -119,6 +119,7 @@ func CreateUser(ctx *context.APIContext) {
 | 
				
			|||||||
			user_model.IsErrEmailAlreadyUsed(err) ||
 | 
								user_model.IsErrEmailAlreadyUsed(err) ||
 | 
				
			||||||
			db.IsErrNameReserved(err) ||
 | 
								db.IsErrNameReserved(err) ||
 | 
				
			||||||
			db.IsErrNameCharsNotAllowed(err) ||
 | 
								db.IsErrNameCharsNotAllowed(err) ||
 | 
				
			||||||
 | 
								user_model.IsErrEmailCharIsNotSupported(err) ||
 | 
				
			||||||
			user_model.IsErrEmailInvalid(err) ||
 | 
								user_model.IsErrEmailInvalid(err) ||
 | 
				
			||||||
			db.IsErrNamePatternNotAllowed(err) {
 | 
								db.IsErrNamePatternNotAllowed(err) {
 | 
				
			||||||
			ctx.Error(http.StatusUnprocessableEntity, "", err)
 | 
								ctx.Error(http.StatusUnprocessableEntity, "", err)
 | 
				
			||||||
@@ -265,7 +266,9 @@ func EditUser(ctx *context.APIContext) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := user_model.UpdateUser(u, emailChanged); err != nil {
 | 
						if err := user_model.UpdateUser(u, emailChanged); err != nil {
 | 
				
			||||||
		if user_model.IsErrEmailAlreadyUsed(err) || user_model.IsErrEmailInvalid(err) {
 | 
							if user_model.IsErrEmailAlreadyUsed(err) ||
 | 
				
			||||||
 | 
								user_model.IsErrEmailCharIsNotSupported(err) ||
 | 
				
			||||||
 | 
								user_model.IsErrEmailInvalid(err) {
 | 
				
			||||||
			ctx.Error(http.StatusUnprocessableEntity, "", err)
 | 
								ctx.Error(http.StatusUnprocessableEntity, "", err)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
 | 
								ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -80,7 +80,8 @@ func AddEmail(ctx *context.APIContext) {
 | 
				
			|||||||
	if err := user_model.AddEmailAddresses(emails); err != nil {
 | 
						if err := user_model.AddEmailAddresses(emails); err != nil {
 | 
				
			||||||
		if user_model.IsErrEmailAlreadyUsed(err) {
 | 
							if user_model.IsErrEmailAlreadyUsed(err) {
 | 
				
			||||||
			ctx.Error(http.StatusUnprocessableEntity, "", "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email)
 | 
								ctx.Error(http.StatusUnprocessableEntity, "", "Email address has been used: "+err.(user_model.ErrEmailAlreadyUsed).Email)
 | 
				
			||||||
		} else if user_model.IsErrEmailInvalid(err) {
 | 
							} else if user_model.IsErrEmailCharIsNotSupported(err) ||
 | 
				
			||||||
 | 
								user_model.IsErrEmailInvalid(err) {
 | 
				
			||||||
			errMsg := fmt.Sprintf("Email address %s invalid", err.(user_model.ErrEmailInvalid).Email)
 | 
								errMsg := fmt.Sprintf("Email address %s invalid", err.(user_model.ErrEmailInvalid).Email)
 | 
				
			||||||
			ctx.Error(http.StatusUnprocessableEntity, "", errMsg)
 | 
								ctx.Error(http.StatusUnprocessableEntity, "", errMsg)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -171,6 +171,9 @@ func NewUserPost(ctx *context.Context) {
 | 
				
			|||||||
		case user_model.IsErrEmailAlreadyUsed(err):
 | 
							case user_model.IsErrEmailAlreadyUsed(err):
 | 
				
			||||||
			ctx.Data["Err_Email"] = true
 | 
								ctx.Data["Err_Email"] = true
 | 
				
			||||||
			ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form)
 | 
								ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form)
 | 
				
			||||||
 | 
							case user_model.IsErrEmailCharIsNotSupported(err):
 | 
				
			||||||
 | 
								ctx.Data["Err_Email"] = true
 | 
				
			||||||
 | 
								ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
 | 
				
			||||||
		case user_model.IsErrEmailInvalid(err):
 | 
							case user_model.IsErrEmailInvalid(err):
 | 
				
			||||||
			ctx.Data["Err_Email"] = true
 | 
								ctx.Data["Err_Email"] = true
 | 
				
			||||||
			ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
 | 
								ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
 | 
				
			||||||
@@ -386,7 +389,8 @@ func EditUserPost(ctx *context.Context) {
 | 
				
			|||||||
		if user_model.IsErrEmailAlreadyUsed(err) {
 | 
							if user_model.IsErrEmailAlreadyUsed(err) {
 | 
				
			||||||
			ctx.Data["Err_Email"] = true
 | 
								ctx.Data["Err_Email"] = true
 | 
				
			||||||
			ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
 | 
								ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
 | 
				
			||||||
		} else if user_model.IsErrEmailInvalid(err) {
 | 
							} else if user_model.IsErrEmailCharIsNotSupported(err) ||
 | 
				
			||||||
 | 
								user_model.IsErrEmailInvalid(err) {
 | 
				
			||||||
			ctx.Data["Err_Email"] = true
 | 
								ctx.Data["Err_Email"] = true
 | 
				
			||||||
			ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
 | 
								ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -573,6 +573,9 @@ func createUserInContext(ctx *context.Context, tpl base.TplName, form interface{
 | 
				
			|||||||
		case user_model.IsErrEmailAlreadyUsed(err):
 | 
							case user_model.IsErrEmailAlreadyUsed(err):
 | 
				
			||||||
			ctx.Data["Err_Email"] = true
 | 
								ctx.Data["Err_Email"] = true
 | 
				
			||||||
			ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, form)
 | 
								ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, form)
 | 
				
			||||||
 | 
							case user_model.IsErrEmailCharIsNotSupported(err):
 | 
				
			||||||
 | 
								ctx.Data["Err_Email"] = true
 | 
				
			||||||
 | 
								ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
 | 
				
			||||||
		case user_model.IsErrEmailInvalid(err):
 | 
							case user_model.IsErrEmailInvalid(err):
 | 
				
			||||||
			ctx.Data["Err_Email"] = true
 | 
								ctx.Data["Err_Email"] = true
 | 
				
			||||||
			ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
 | 
								ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -188,7 +188,8 @@ func EmailPost(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
 | 
								ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		} else if user_model.IsErrEmailInvalid(err) {
 | 
							} else if user_model.IsErrEmailCharIsNotSupported(err) ||
 | 
				
			||||||
 | 
								user_model.IsErrEmailInvalid(err) {
 | 
				
			||||||
			loadAccountData(ctx)
 | 
								loadAccountData(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form)
 | 
								ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsAccount, &form)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user