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