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