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 }