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 }