code.gitea.io/gitea@v1.21.7/models/user/email_address.go (about)

     1  // Copyright 2016 The Gogs Authors. All rights reserved.
     2  // Copyright 2020 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package user
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"net/mail"
    11  	"regexp"
    12  	"strings"
    13  
    14  	"code.gitea.io/gitea/models/db"
    15  	"code.gitea.io/gitea/modules/base"
    16  	"code.gitea.io/gitea/modules/log"
    17  	"code.gitea.io/gitea/modules/setting"
    18  	"code.gitea.io/gitea/modules/util"
    19  	"code.gitea.io/gitea/modules/validation"
    20  
    21  	"xorm.io/builder"
    22  )
    23  
    24  // ErrEmailNotActivated e-mail address has not been activated error
    25  var ErrEmailNotActivated = util.NewInvalidArgumentErrorf("e-mail address has not been activated")
    26  
    27  // ErrEmailCharIsNotSupported e-mail address contains unsupported character
    28  type ErrEmailCharIsNotSupported struct {
    29  	Email string
    30  }
    31  
    32  // IsErrEmailCharIsNotSupported checks if an error is an ErrEmailCharIsNotSupported
    33  func IsErrEmailCharIsNotSupported(err error) bool {
    34  	_, ok := err.(ErrEmailCharIsNotSupported)
    35  	return ok
    36  }
    37  
    38  func (err ErrEmailCharIsNotSupported) Error() string {
    39  	return fmt.Sprintf("e-mail address contains unsupported character [email: %s]", err.Email)
    40  }
    41  
    42  func (err ErrEmailCharIsNotSupported) Unwrap() error {
    43  	return util.ErrInvalidArgument
    44  }
    45  
    46  // ErrEmailInvalid represents an error where the email address does not comply with RFC 5322
    47  // or has a leading '-' character
    48  type ErrEmailInvalid struct {
    49  	Email string
    50  }
    51  
    52  // IsErrEmailInvalid checks if an error is an ErrEmailInvalid
    53  func IsErrEmailInvalid(err error) bool {
    54  	_, ok := err.(ErrEmailInvalid)
    55  	return ok
    56  }
    57  
    58  func (err ErrEmailInvalid) Error() string {
    59  	return fmt.Sprintf("e-mail invalid [email: %s]", err.Email)
    60  }
    61  
    62  func (err ErrEmailInvalid) Unwrap() error {
    63  	return util.ErrInvalidArgument
    64  }
    65  
    66  // ErrEmailAlreadyUsed represents a "EmailAlreadyUsed" kind of error.
    67  type ErrEmailAlreadyUsed struct {
    68  	Email string
    69  }
    70  
    71  // IsErrEmailAlreadyUsed checks if an error is a ErrEmailAlreadyUsed.
    72  func IsErrEmailAlreadyUsed(err error) bool {
    73  	_, ok := err.(ErrEmailAlreadyUsed)
    74  	return ok
    75  }
    76  
    77  func (err ErrEmailAlreadyUsed) Error() string {
    78  	return fmt.Sprintf("e-mail already in use [email: %s]", err.Email)
    79  }
    80  
    81  func (err ErrEmailAlreadyUsed) Unwrap() error {
    82  	return util.ErrAlreadyExist
    83  }
    84  
    85  // ErrEmailAddressNotExist email address not exist
    86  type ErrEmailAddressNotExist struct {
    87  	Email string
    88  }
    89  
    90  // IsErrEmailAddressNotExist checks if an error is an ErrEmailAddressNotExist
    91  func IsErrEmailAddressNotExist(err error) bool {
    92  	_, ok := err.(ErrEmailAddressNotExist)
    93  	return ok
    94  }
    95  
    96  func (err ErrEmailAddressNotExist) Error() string {
    97  	return fmt.Sprintf("Email address does not exist [email: %s]", err.Email)
    98  }
    99  
   100  func (err ErrEmailAddressNotExist) Unwrap() error {
   101  	return util.ErrNotExist
   102  }
   103  
   104  // ErrPrimaryEmailCannotDelete primary email address cannot be deleted
   105  type ErrPrimaryEmailCannotDelete struct {
   106  	Email string
   107  }
   108  
   109  // IsErrPrimaryEmailCannotDelete checks if an error is an ErrPrimaryEmailCannotDelete
   110  func IsErrPrimaryEmailCannotDelete(err error) bool {
   111  	_, ok := err.(ErrPrimaryEmailCannotDelete)
   112  	return ok
   113  }
   114  
   115  func (err ErrPrimaryEmailCannotDelete) Error() string {
   116  	return fmt.Sprintf("Primary email address cannot be deleted [email: %s]", err.Email)
   117  }
   118  
   119  func (err ErrPrimaryEmailCannotDelete) Unwrap() error {
   120  	return util.ErrInvalidArgument
   121  }
   122  
   123  // EmailAddress is the list of all email addresses of a user. It also contains the
   124  // primary email address which is saved in user table.
   125  type EmailAddress struct {
   126  	ID          int64  `xorm:"pk autoincr"`
   127  	UID         int64  `xorm:"INDEX NOT NULL"`
   128  	Email       string `xorm:"UNIQUE NOT NULL"`
   129  	LowerEmail  string `xorm:"UNIQUE NOT NULL"`
   130  	IsActivated bool
   131  	IsPrimary   bool `xorm:"DEFAULT(false) NOT NULL"`
   132  }
   133  
   134  func init() {
   135  	db.RegisterModel(new(EmailAddress))
   136  }
   137  
   138  // BeforeInsert will be invoked by XORM before inserting a record
   139  func (email *EmailAddress) BeforeInsert() {
   140  	if email.LowerEmail == "" {
   141  		email.LowerEmail = strings.ToLower(email.Email)
   142  	}
   143  }
   144  
   145  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])?)*$")
   146  
   147  // ValidateEmail check if email is a allowed address
   148  func ValidateEmail(email string) error {
   149  	if len(email) == 0 {
   150  		return nil
   151  	}
   152  
   153  	if !emailRegexp.MatchString(email) {
   154  		return ErrEmailCharIsNotSupported{email}
   155  	}
   156  
   157  	if email[0] == '-' {
   158  		return ErrEmailInvalid{email}
   159  	}
   160  
   161  	if _, err := mail.ParseAddress(email); err != nil {
   162  		return ErrEmailInvalid{email}
   163  	}
   164  
   165  	// if there is no allow list, then check email against block list
   166  	if len(setting.Service.EmailDomainAllowList) == 0 &&
   167  		validation.IsEmailDomainListed(setting.Service.EmailDomainBlockList, email) {
   168  		return ErrEmailInvalid{email}
   169  	}
   170  
   171  	// if there is an allow list, then check email against allow list
   172  	if len(setting.Service.EmailDomainAllowList) > 0 &&
   173  		!validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email) {
   174  		return ErrEmailInvalid{email}
   175  	}
   176  
   177  	return nil
   178  }
   179  
   180  // GetEmailAddresses returns all email addresses belongs to given user.
   181  func GetEmailAddresses(ctx context.Context, uid int64) ([]*EmailAddress, error) {
   182  	emails := make([]*EmailAddress, 0, 5)
   183  	if err := db.GetEngine(ctx).
   184  		Where("uid=?", uid).
   185  		Asc("id").
   186  		Find(&emails); err != nil {
   187  		return nil, err
   188  	}
   189  	return emails, nil
   190  }
   191  
   192  // GetEmailAddressByID gets a user's email address by ID
   193  func GetEmailAddressByID(ctx context.Context, uid, id int64) (*EmailAddress, error) {
   194  	// User ID is required for security reasons
   195  	email := &EmailAddress{UID: uid}
   196  	if has, err := db.GetEngine(ctx).ID(id).Get(email); err != nil {
   197  		return nil, err
   198  	} else if !has {
   199  		return nil, nil
   200  	}
   201  	return email, nil
   202  }
   203  
   204  // IsEmailActive check if email is activated with a different emailID
   205  func IsEmailActive(ctx context.Context, email string, excludeEmailID int64) (bool, error) {
   206  	if len(email) == 0 {
   207  		return true, nil
   208  	}
   209  
   210  	// Can't filter by boolean field unless it's explicit
   211  	cond := builder.NewCond()
   212  	cond = cond.And(builder.Eq{"lower_email": strings.ToLower(email)}, builder.Neq{"id": excludeEmailID})
   213  	if setting.Service.RegisterEmailConfirm {
   214  		// Inactive (unvalidated) addresses don't count as active if email validation is required
   215  		cond = cond.And(builder.Eq{"is_activated": true})
   216  	}
   217  
   218  	var em EmailAddress
   219  	if has, err := db.GetEngine(ctx).Where(cond).Get(&em); has || err != nil {
   220  		if has {
   221  			log.Info("isEmailActive(%q, %d) found duplicate in email ID %d", email, excludeEmailID, em.ID)
   222  		}
   223  		return has, err
   224  	}
   225  
   226  	return false, nil
   227  }
   228  
   229  // IsEmailUsed returns true if the email has been used.
   230  func IsEmailUsed(ctx context.Context, email string) (bool, error) {
   231  	if len(email) == 0 {
   232  		return true, nil
   233  	}
   234  
   235  	return db.GetEngine(ctx).Where("lower_email=?", strings.ToLower(email)).Get(&EmailAddress{})
   236  }
   237  
   238  // AddEmailAddress adds an email address to given user.
   239  func AddEmailAddress(ctx context.Context, email *EmailAddress) error {
   240  	email.Email = strings.TrimSpace(email.Email)
   241  	used, err := IsEmailUsed(ctx, email.Email)
   242  	if err != nil {
   243  		return err
   244  	} else if used {
   245  		return ErrEmailAlreadyUsed{email.Email}
   246  	}
   247  
   248  	if err = ValidateEmail(email.Email); err != nil {
   249  		return err
   250  	}
   251  
   252  	return db.Insert(ctx, email)
   253  }
   254  
   255  // AddEmailAddresses adds an email address to given user.
   256  func AddEmailAddresses(ctx context.Context, emails []*EmailAddress) error {
   257  	if len(emails) == 0 {
   258  		return nil
   259  	}
   260  
   261  	// Check if any of them has been used
   262  	for i := range emails {
   263  		emails[i].Email = strings.TrimSpace(emails[i].Email)
   264  		used, err := IsEmailUsed(ctx, emails[i].Email)
   265  		if err != nil {
   266  			return err
   267  		} else if used {
   268  			return ErrEmailAlreadyUsed{emails[i].Email}
   269  		}
   270  		if err = ValidateEmail(emails[i].Email); err != nil {
   271  			return err
   272  		}
   273  	}
   274  
   275  	if err := db.Insert(ctx, emails); err != nil {
   276  		return fmt.Errorf("Insert: %w", err)
   277  	}
   278  
   279  	return nil
   280  }
   281  
   282  // DeleteEmailAddress deletes an email address of given user.
   283  func DeleteEmailAddress(ctx context.Context, email *EmailAddress) (err error) {
   284  	if email.IsPrimary {
   285  		return ErrPrimaryEmailCannotDelete{Email: email.Email}
   286  	}
   287  
   288  	var deleted int64
   289  	// ask to check UID
   290  	address := EmailAddress{
   291  		UID: email.UID,
   292  	}
   293  	if email.ID > 0 {
   294  		deleted, err = db.GetEngine(ctx).ID(email.ID).Delete(&address)
   295  	} else {
   296  		if email.Email != "" && email.LowerEmail == "" {
   297  			email.LowerEmail = strings.ToLower(email.Email)
   298  		}
   299  		deleted, err = db.GetEngine(ctx).
   300  			Where("lower_email=?", email.LowerEmail).
   301  			Delete(&address)
   302  	}
   303  
   304  	if err != nil {
   305  		return err
   306  	} else if deleted != 1 {
   307  		return ErrEmailAddressNotExist{Email: email.Email}
   308  	}
   309  	return nil
   310  }
   311  
   312  // DeleteEmailAddresses deletes multiple email addresses
   313  func DeleteEmailAddresses(ctx context.Context, emails []*EmailAddress) (err error) {
   314  	for i := range emails {
   315  		if err = DeleteEmailAddress(ctx, emails[i]); err != nil {
   316  			return err
   317  		}
   318  	}
   319  
   320  	return nil
   321  }
   322  
   323  // DeleteInactiveEmailAddresses deletes inactive email addresses
   324  func DeleteInactiveEmailAddresses(ctx context.Context) error {
   325  	_, err := db.GetEngine(ctx).
   326  		Where("is_activated = ?", false).
   327  		Delete(new(EmailAddress))
   328  	return err
   329  }
   330  
   331  // ActivateEmail activates the email address to given user.
   332  func ActivateEmail(ctx context.Context, email *EmailAddress) error {
   333  	ctx, committer, err := db.TxContext(ctx)
   334  	if err != nil {
   335  		return err
   336  	}
   337  	defer committer.Close()
   338  	if err := updateActivation(ctx, email, true); err != nil {
   339  		return err
   340  	}
   341  	return committer.Commit()
   342  }
   343  
   344  func updateActivation(ctx context.Context, email *EmailAddress, activate bool) error {
   345  	user, err := GetUserByID(ctx, email.UID)
   346  	if err != nil {
   347  		return err
   348  	}
   349  	if user.Rands, err = GetUserSalt(); err != nil {
   350  		return err
   351  	}
   352  	email.IsActivated = activate
   353  	if _, err := db.GetEngine(ctx).ID(email.ID).Cols("is_activated").Update(email); err != nil {
   354  		return err
   355  	}
   356  	return UpdateUserCols(ctx, user, "rands")
   357  }
   358  
   359  // MakeEmailPrimary sets primary email address of given user.
   360  func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
   361  	has, err := db.GetEngine(ctx).Get(email)
   362  	if err != nil {
   363  		return err
   364  	} else if !has {
   365  		return ErrEmailAddressNotExist{Email: email.Email}
   366  	}
   367  
   368  	if !email.IsActivated {
   369  		return ErrEmailNotActivated
   370  	}
   371  
   372  	user := &User{}
   373  	has, err = db.GetEngine(ctx).ID(email.UID).Get(user)
   374  	if err != nil {
   375  		return err
   376  	} else if !has {
   377  		return ErrUserNotExist{
   378  			UID:   email.UID,
   379  			Name:  "",
   380  			KeyID: 0,
   381  		}
   382  	}
   383  
   384  	ctx, committer, err := db.TxContext(ctx)
   385  	if err != nil {
   386  		return err
   387  	}
   388  	defer committer.Close()
   389  	sess := db.GetEngine(ctx)
   390  
   391  	// 1. Update user table
   392  	user.Email = email.Email
   393  	if _, err = sess.ID(user.ID).Cols("email").Update(user); err != nil {
   394  		return err
   395  	}
   396  
   397  	// 2. Update old primary email
   398  	if _, err = sess.Where("uid=? AND is_primary=?", email.UID, true).Cols("is_primary").Update(&EmailAddress{
   399  		IsPrimary: false,
   400  	}); err != nil {
   401  		return err
   402  	}
   403  
   404  	// 3. update new primary email
   405  	email.IsPrimary = true
   406  	if _, err = sess.ID(email.ID).Cols("is_primary").Update(email); err != nil {
   407  		return err
   408  	}
   409  
   410  	return committer.Commit()
   411  }
   412  
   413  // VerifyActiveEmailCode verifies active email code when active account
   414  func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress {
   415  	minutes := setting.Service.ActiveCodeLives
   416  
   417  	if user := GetVerifyUser(ctx, code); user != nil {
   418  		// time limit code
   419  		prefix := code[:base.TimeLimitCodeLength]
   420  		data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands)
   421  
   422  		if base.VerifyTimeLimitCode(data, minutes, prefix) {
   423  			emailAddress := &EmailAddress{UID: user.ID, Email: email}
   424  			if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
   425  				return emailAddress
   426  			}
   427  		}
   428  	}
   429  	return nil
   430  }
   431  
   432  // SearchEmailOrderBy is used to sort the results from SearchEmails()
   433  type SearchEmailOrderBy string
   434  
   435  func (s SearchEmailOrderBy) String() string {
   436  	return string(s)
   437  }
   438  
   439  // Strings for sorting result
   440  const (
   441  	SearchEmailOrderByEmail        SearchEmailOrderBy = "email_address.lower_email ASC, email_address.is_primary DESC, email_address.id ASC"
   442  	SearchEmailOrderByEmailReverse SearchEmailOrderBy = "email_address.lower_email DESC, email_address.is_primary ASC, email_address.id DESC"
   443  	SearchEmailOrderByName         SearchEmailOrderBy = "`user`.lower_name ASC, email_address.is_primary DESC, email_address.id ASC"
   444  	SearchEmailOrderByNameReverse  SearchEmailOrderBy = "`user`.lower_name DESC, email_address.is_primary ASC, email_address.id DESC"
   445  )
   446  
   447  // SearchEmailOptions are options to search e-mail addresses for the admin panel
   448  type SearchEmailOptions struct {
   449  	db.ListOptions
   450  	Keyword     string
   451  	SortType    SearchEmailOrderBy
   452  	IsPrimary   util.OptionalBool
   453  	IsActivated util.OptionalBool
   454  }
   455  
   456  // SearchEmailResult is an e-mail address found in the user or email_address table
   457  type SearchEmailResult struct {
   458  	UID         int64
   459  	Email       string
   460  	IsActivated bool
   461  	IsPrimary   bool
   462  	// From User
   463  	Name     string
   464  	FullName string
   465  }
   466  
   467  // SearchEmails takes options i.e. keyword and part of email name to search,
   468  // it returns results in given range and number of total results.
   469  func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmailResult, int64, error) {
   470  	var cond builder.Cond = builder.Eq{"`user`.`type`": UserTypeIndividual}
   471  	if len(opts.Keyword) > 0 {
   472  		likeStr := "%" + strings.ToLower(opts.Keyword) + "%"
   473  		cond = cond.And(builder.Or(
   474  			builder.Like{"lower(`user`.full_name)", likeStr},
   475  			builder.Like{"`user`.lower_name", likeStr},
   476  			builder.Like{"email_address.lower_email", likeStr},
   477  		))
   478  	}
   479  
   480  	switch {
   481  	case opts.IsPrimary.IsTrue():
   482  		cond = cond.And(builder.Eq{"email_address.is_primary": true})
   483  	case opts.IsPrimary.IsFalse():
   484  		cond = cond.And(builder.Eq{"email_address.is_primary": false})
   485  	}
   486  
   487  	switch {
   488  	case opts.IsActivated.IsTrue():
   489  		cond = cond.And(builder.Eq{"email_address.is_activated": true})
   490  	case opts.IsActivated.IsFalse():
   491  		cond = cond.And(builder.Eq{"email_address.is_activated": false})
   492  	}
   493  
   494  	count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.ID = email_address.uid").
   495  		Where(cond).Count(new(EmailAddress))
   496  	if err != nil {
   497  		return nil, 0, fmt.Errorf("Count: %w", err)
   498  	}
   499  
   500  	orderby := opts.SortType.String()
   501  	if orderby == "" {
   502  		orderby = SearchEmailOrderByEmail.String()
   503  	}
   504  
   505  	opts.SetDefaultValues()
   506  
   507  	emails := make([]*SearchEmailResult, 0, opts.PageSize)
   508  	err = db.GetEngine(ctx).Table("email_address").
   509  		Select("email_address.*, `user`.name, `user`.full_name").
   510  		Join("INNER", "`user`", "`user`.ID = email_address.uid").
   511  		Where(cond).
   512  		OrderBy(orderby).
   513  		Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).
   514  		Find(&emails)
   515  
   516  	return emails, count, err
   517  }
   518  
   519  // ActivateUserEmail will change the activated state of an email address,
   520  // either primary or secondary (all in the email_address table)
   521  func ActivateUserEmail(ctx context.Context, userID int64, email string, activate bool) (err error) {
   522  	ctx, committer, err := db.TxContext(ctx)
   523  	if err != nil {
   524  		return err
   525  	}
   526  	defer committer.Close()
   527  
   528  	// Activate/deactivate a user's secondary email address
   529  	// First check if there's another user active with the same address
   530  	addr := EmailAddress{UID: userID, LowerEmail: strings.ToLower(email)}
   531  	if has, err := db.GetByBean(ctx, &addr); err != nil {
   532  		return err
   533  	} else if !has {
   534  		return fmt.Errorf("no such email: %d (%s)", userID, email)
   535  	}
   536  	if addr.IsActivated == activate {
   537  		// Already in the desired state; no action
   538  		return nil
   539  	}
   540  	if activate {
   541  		if used, err := IsEmailActive(ctx, email, addr.ID); err != nil {
   542  			return fmt.Errorf("unable to check isEmailActive() for %s: %w", email, err)
   543  		} else if used {
   544  			return ErrEmailAlreadyUsed{Email: email}
   545  		}
   546  	}
   547  	if err = updateActivation(ctx, &addr, activate); err != nil {
   548  		return fmt.Errorf("unable to updateActivation() for %d:%s: %w", addr.ID, addr.Email, err)
   549  	}
   550  
   551  	// Activate/deactivate a user's primary email address and account
   552  	if addr.IsPrimary {
   553  		user := User{ID: userID, Email: email}
   554  		if has, err := db.GetByBean(ctx, &user); err != nil {
   555  			return err
   556  		} else if !has {
   557  			return fmt.Errorf("no user with ID: %d and Email: %s", userID, email)
   558  		}
   559  		// The user's activation state should be synchronized with the primary email
   560  		if user.IsActive != activate {
   561  			user.IsActive = activate
   562  			if user.Rands, err = GetUserSalt(); err != nil {
   563  				return fmt.Errorf("unable to generate salt: %w", err)
   564  			}
   565  			if err = UpdateUserCols(ctx, &user, "is_active", "rands"); err != nil {
   566  				return fmt.Errorf("unable to updateUserCols() for user ID: %d: %w", userID, err)
   567  			}
   568  		}
   569  	}
   570  
   571  	return committer.Commit()
   572  }