2016-07-16 05:08:04 +03:00
// Copyright 2016 The Gogs Authors. All rights reserved.
2020-03-02 21:25:36 +03:00
// Copyright 2020 The Gitea Authors. All rights reserved.
2016-07-16 05:08:04 +03:00
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"fmt"
2020-11-14 19:53:43 +03:00
"net/mail"
2016-07-16 05:08:04 +03:00
"strings"
2020-03-02 21:25:36 +03:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
2016-07-16 05:08:04 +03:00
)
2016-11-26 03:03:06 +03:00
// EmailAddress is the list of all email addresses of a user. Can contain the
2016-07-16 05:08:04 +03:00
// primary email address, but is not obligatory.
type EmailAddress struct {
ID int64 ` xorm:"pk autoincr" `
UID int64 ` xorm:"INDEX NOT NULL" `
Email string ` xorm:"UNIQUE NOT NULL" `
IsActivated bool
IsPrimary bool ` xorm:"-" `
}
2020-11-21 00:45:55 +03:00
// ValidateEmail check if email is a allowed address
func ValidateEmail ( email string ) error {
if len ( email ) == 0 {
return nil
}
if _ , err := mail . ParseAddress ( email ) ; err != nil {
return ErrEmailInvalid { email }
}
// TODO: add an email allow/block list
return nil
}
2016-07-16 05:08:04 +03:00
// GetEmailAddresses returns all email addresses belongs to given user.
func GetEmailAddresses ( uid int64 ) ( [ ] * EmailAddress , error ) {
emails := make ( [ ] * EmailAddress , 0 , 5 )
2016-11-10 18:16:32 +03:00
if err := x .
Where ( "uid=?" , uid ) .
Find ( & emails ) ; err != nil {
2016-07-16 05:08:04 +03:00
return nil , err
}
u , err := GetUserByID ( uid )
if err != nil {
return nil , err
}
isPrimaryFound := false
for _ , email := range emails {
if email . Email == u . Email {
isPrimaryFound = true
email . IsPrimary = true
} else {
email . IsPrimary = false
}
}
2017-01-05 03:50:34 +03:00
// We always want the primary email address displayed, even if it's not in
// the email address table (yet).
2016-07-16 05:08:04 +03:00
if ! isPrimaryFound {
emails = append ( emails , & EmailAddress {
Email : u . Email ,
2020-03-02 21:25:36 +03:00
IsActivated : u . IsActive ,
2016-07-16 05:08:04 +03:00
IsPrimary : true ,
} )
}
return emails , nil
}
2020-03-02 21:25:36 +03:00
// GetEmailAddressByID gets a user's email address by ID
func GetEmailAddressByID ( uid , id int64 ) ( * EmailAddress , error ) {
// User ID is required for security reasons
2020-06-17 20:50:11 +03:00
email := & EmailAddress { UID : uid }
if has , err := x . ID ( id ) . Get ( email ) ; err != nil {
2020-03-02 21:25:36 +03:00
return nil , err
} else if ! has {
return nil , nil
}
return email , nil
}
func isEmailActive ( e Engine , email string , userID , emailID int64 ) ( bool , error ) {
if len ( email ) == 0 {
return true , nil
}
// Can't filter by boolean field unless it's explicit
cond := builder . NewCond ( )
cond = cond . And ( builder . Eq { "email" : email } , builder . Neq { "id" : emailID } )
if setting . Service . RegisterEmailConfirm {
// Inactive (unvalidated) addresses don't count as active if email validation is required
cond = cond . And ( builder . Eq { "is_activated" : true } )
}
em := EmailAddress { }
if has , err := e . Where ( cond ) . Get ( & em ) ; has || err != nil {
if has {
log . Info ( "isEmailActive('%s',%d,%d) found duplicate in email ID %d" , email , userID , emailID , em . ID )
}
return has , err
}
// Can't filter by boolean field unless it's explicit
cond = builder . NewCond ( )
cond = cond . And ( builder . Eq { "email" : email } , builder . Neq { "id" : userID } )
if setting . Service . RegisterEmailConfirm {
cond = cond . And ( builder . Eq { "is_active" : true } )
}
us := User { }
if has , err := e . Where ( cond ) . Get ( & us ) ; has || err != nil {
if has {
log . Info ( "isEmailActive('%s',%d,%d) found duplicate in user ID %d" , email , userID , emailID , us . ID )
}
return has , err
}
return false , nil
}
2016-07-16 05:08:04 +03:00
func isEmailUsed ( e Engine , email string ) ( bool , error ) {
if len ( email ) == 0 {
return true , nil
}
2020-06-17 20:50:11 +03:00
return e . Where ( "email=?" , email ) . Get ( & EmailAddress { } )
2016-07-16 05:08:04 +03:00
}
// IsEmailUsed returns true if the email has been used.
func IsEmailUsed ( email string ) ( bool , error ) {
return isEmailUsed ( x , email )
}
func addEmailAddress ( e Engine , email * EmailAddress ) error {
email . Email = strings . ToLower ( strings . TrimSpace ( email . Email ) )
used , err := isEmailUsed ( e , email . Email )
if err != nil {
return err
} else if used {
return ErrEmailAlreadyUsed { email . Email }
}
2020-11-21 00:45:55 +03:00
if err = ValidateEmail ( email . Email ) ; err != nil {
return err
2020-11-14 19:53:43 +03:00
}
2016-07-16 05:08:04 +03:00
_ , err = e . Insert ( email )
return err
}
2016-11-27 14:59:12 +03:00
// AddEmailAddress adds an email address to given user.
2016-07-16 05:08:04 +03:00
func AddEmailAddress ( email * EmailAddress ) error {
return addEmailAddress ( x , email )
}
2016-11-27 14:59:12 +03:00
// AddEmailAddresses adds an email address to given user.
2016-07-16 05:08:04 +03:00
func AddEmailAddresses ( emails [ ] * EmailAddress ) error {
if len ( emails ) == 0 {
return nil
}
// Check if any of them has been used
for i := range emails {
emails [ i ] . Email = strings . ToLower ( strings . TrimSpace ( emails [ i ] . Email ) )
used , err := IsEmailUsed ( emails [ i ] . Email )
if err != nil {
return err
} else if used {
return ErrEmailAlreadyUsed { emails [ i ] . Email }
}
2020-11-21 00:45:55 +03:00
if err = ValidateEmail ( emails [ i ] . Email ) ; err != nil {
return err
2020-11-14 19:53:43 +03:00
}
2016-07-16 05:08:04 +03:00
}
if _ , err := x . Insert ( emails ) ; err != nil {
return fmt . Errorf ( "Insert: %v" , err )
}
return nil
}
2016-11-27 14:59:12 +03:00
// Activate activates the email address to given user.
2016-07-16 05:08:04 +03:00
func ( email * EmailAddress ) Activate ( ) error {
2020-03-02 21:25:36 +03:00
sess := x . NewSession ( )
defer sess . Close ( )
if err := sess . Begin ( ) ; err != nil {
2016-07-16 05:08:04 +03:00
return err
}
2020-03-02 21:25:36 +03:00
if err := email . updateActivation ( sess , true ) ; err != nil {
2016-12-20 15:32:02 +03:00
return err
}
2020-03-02 21:25:36 +03:00
return sess . Commit ( )
}
2016-07-16 05:08:04 +03:00
2020-03-02 21:25:36 +03:00
func ( email * EmailAddress ) updateActivation ( e Engine , activate bool ) error {
user , err := getUserByID ( e , email . UID )
if err != nil {
2016-07-16 05:08:04 +03:00
return err
}
2020-03-02 21:25:36 +03:00
if user . Rands , err = GetUserSalt ( ) ; err != nil {
2016-07-16 05:08:04 +03:00
return err
2020-03-02 21:25:36 +03:00
}
email . IsActivated = activate
if _ , err := e . ID ( email . ID ) . Cols ( "is_activated" ) . Update ( email ) ; err != nil {
2016-07-16 05:08:04 +03:00
return err
}
2020-03-02 21:25:36 +03:00
return updateUserCols ( e , user , "rands" )
2016-07-16 05:08:04 +03:00
}
2016-11-27 14:59:12 +03:00
// DeleteEmailAddress deletes an email address of given user.
2016-07-16 05:08:04 +03:00
func DeleteEmailAddress ( email * EmailAddress ) ( err error ) {
2016-12-15 11:49:06 +03:00
var deleted int64
// ask to check UID
2021-03-14 21:52:12 +03:00
address := EmailAddress {
2016-12-15 11:49:06 +03:00
UID : email . UID ,
}
2016-07-16 05:08:04 +03:00
if email . ID > 0 {
2017-10-05 07:43:04 +03:00
deleted , err = x . ID ( email . ID ) . Delete ( & address )
2016-07-16 05:08:04 +03:00
} else {
2016-12-15 11:49:06 +03:00
deleted , err = x .
2016-11-10 18:16:32 +03:00
Where ( "email=?" , email . Email ) .
2016-12-15 11:49:06 +03:00
Delete ( & address )
2016-07-16 05:08:04 +03:00
}
2016-12-15 11:49:06 +03:00
if err != nil {
return err
} else if deleted != 1 {
2021-04-10 09:12:38 +03:00
return ErrEmailAddressNotExist { Email : email . Email }
2016-12-15 11:49:06 +03:00
}
return nil
2016-07-16 05:08:04 +03:00
}
2016-11-27 14:59:12 +03:00
// DeleteEmailAddresses deletes multiple email addresses
2016-07-16 05:08:04 +03:00
func DeleteEmailAddresses ( emails [ ] * EmailAddress ) ( err error ) {
for i := range emails {
if err = DeleteEmailAddress ( emails [ i ] ) ; err != nil {
return err
}
}
return nil
}
2016-11-27 14:59:12 +03:00
// MakeEmailPrimary sets primary email address of given user.
2016-07-16 05:08:04 +03:00
func MakeEmailPrimary ( email * EmailAddress ) error {
has , err := x . Get ( email )
if err != nil {
return err
} else if ! has {
return ErrEmailNotExist
}
if ! email . IsActivated {
return ErrEmailNotActivated
}
2020-06-17 20:50:11 +03:00
user := & User { }
has , err = x . ID ( email . UID ) . Get ( user )
2016-07-16 05:08:04 +03:00
if err != nil {
return err
} else if ! has {
2016-11-12 02:01:09 +03:00
return ErrUserNotExist { email . UID , "" , 0 }
2016-07-16 05:08:04 +03:00
}
// Make sure the former primary email doesn't disappear.
2020-02-21 16:08:04 +03:00
formerPrimaryEmail := & EmailAddress { UID : user . ID , Email : user . Email }
2016-07-16 05:08:04 +03:00
has , err = x . Get ( formerPrimaryEmail )
if err != nil {
return err
}
sess := x . NewSession ( )
2017-06-21 03:57:05 +03:00
defer sess . Close ( )
2016-07-16 05:08:04 +03:00
if err = sess . Begin ( ) ; err != nil {
return err
}
if ! has {
2016-07-23 20:08:22 +03:00
formerPrimaryEmail . UID = user . ID
2016-07-16 05:08:04 +03:00
formerPrimaryEmail . IsActivated = user . IsActive
if _ , err = sess . Insert ( formerPrimaryEmail ) ; err != nil {
return err
}
}
user . Email = email . Email
2017-10-05 07:43:04 +03:00
if _ , err = sess . ID ( user . ID ) . Cols ( "email" ) . Update ( user ) ; err != nil {
2016-07-16 05:08:04 +03:00
return err
}
return sess . Commit ( )
}
2020-03-02 21:25:36 +03:00
// SearchEmailOrderBy is used to sort the results from SearchEmails()
type SearchEmailOrderBy string
func ( s SearchEmailOrderBy ) String ( ) string {
return string ( s )
}
// Strings for sorting result
const (
SearchEmailOrderByEmail SearchEmailOrderBy = "emails.email ASC, is_primary DESC, sortid ASC"
SearchEmailOrderByEmailReverse SearchEmailOrderBy = "emails.email DESC, is_primary ASC, sortid DESC"
SearchEmailOrderByName SearchEmailOrderBy = "`user`.lower_name ASC, is_primary DESC, sortid ASC"
SearchEmailOrderByNameReverse SearchEmailOrderBy = "`user`.lower_name DESC, is_primary ASC, sortid DESC"
)
// SearchEmailOptions are options to search e-mail addresses for the admin panel
type SearchEmailOptions struct {
ListOptions
Keyword string
SortType SearchEmailOrderBy
IsPrimary util . OptionalBool
IsActivated util . OptionalBool
}
// SearchEmailResult is an e-mail address found in the user or email_address table
type SearchEmailResult struct {
UID int64
Email string
IsActivated bool
IsPrimary bool
// From User
Name string
FullName string
}
// SearchEmails takes options i.e. keyword and part of email name to search,
// it returns results in given range and number of total results.
func SearchEmails ( opts * SearchEmailOptions ) ( [ ] * SearchEmailResult , int64 , error ) {
// Unfortunately, UNION support for SQLite in xorm is currently broken, so we must
// build the SQL ourselves.
where := make ( [ ] string , 0 , 5 )
args := make ( [ ] interface { } , 0 , 5 )
emailsSQL := "(SELECT id as sortid, uid, email, is_activated, 0 as is_primary " +
"FROM email_address " +
"UNION ALL " +
"SELECT id as sortid, id AS uid, email, is_active AS is_activated, 1 as is_primary " +
"FROM `user` " +
"WHERE type = ?) AS emails"
args = append ( args , UserTypeIndividual )
if len ( opts . Keyword ) > 0 {
// Note: % can be injected in the Keyword parameter, but it won't do any harm.
where = append ( where , "(lower(`user`.full_name) LIKE ? OR `user`.lower_name LIKE ? OR emails.email LIKE ?)" )
likeStr := "%" + strings . ToLower ( opts . Keyword ) + "%"
args = append ( args , likeStr )
args = append ( args , likeStr )
args = append ( args , likeStr )
}
switch {
case opts . IsPrimary . IsTrue ( ) :
where = append ( where , "emails.is_primary = ?" )
args = append ( args , true )
case opts . IsPrimary . IsFalse ( ) :
where = append ( where , "emails.is_primary = ?" )
args = append ( args , false )
}
switch {
case opts . IsActivated . IsTrue ( ) :
where = append ( where , "emails.is_activated = ?" )
args = append ( args , true )
case opts . IsActivated . IsFalse ( ) :
where = append ( where , "emails.is_activated = ?" )
args = append ( args , false )
}
var whereStr string
if len ( where ) > 0 {
whereStr = "WHERE " + strings . Join ( where , " AND " )
}
joinSQL := "FROM " + emailsSQL + " INNER JOIN `user` ON `user`.id = emails.uid " + whereStr
count , err := x . SQL ( "SELECT count(*) " + joinSQL , args ... ) . Count ( )
if err != nil {
return nil , 0 , fmt . Errorf ( "Count: %v" , err )
}
orderby := opts . SortType . String ( )
if orderby == "" {
orderby = SearchEmailOrderByEmail . String ( )
}
querySQL := "SELECT emails.uid, emails.email, emails.is_activated, emails.is_primary, " +
"`user`.name, `user`.full_name " + joinSQL + " ORDER BY " + orderby
opts . setDefaultValues ( )
rows , err := x . SQL ( querySQL , args ... ) . Rows ( new ( SearchEmailResult ) )
if err != nil {
return nil , 0 , fmt . Errorf ( "Emails: %v" , err )
}
// Page manually because xorm can't handle Limit() with raw SQL
defer rows . Close ( )
emails := make ( [ ] * SearchEmailResult , 0 , opts . PageSize )
skip := ( opts . Page - 1 ) * opts . PageSize
for rows . Next ( ) {
var email SearchEmailResult
if err := rows . Scan ( & email ) ; err != nil {
return nil , 0 , err
}
if skip > 0 {
skip --
continue
}
emails = append ( emails , & email )
if len ( emails ) == opts . PageSize {
break
}
}
return emails , count , err
}
// ActivateUserEmail will change the activated state of an email address,
// either primary (in the user table) or secondary (in the email_address table)
func ActivateUserEmail ( userID int64 , email string , primary , activate bool ) ( err error ) {
sess := x . NewSession ( )
defer sess . Close ( )
if err = sess . Begin ( ) ; err != nil {
return err
}
if primary {
// Activate/deactivate a user's primary email address
user := User { ID : userID , Email : email }
if has , err := sess . Get ( & user ) ; err != nil {
return err
} else if ! has {
return fmt . Errorf ( "no such user: %d (%s)" , userID , email )
}
if user . IsActive == activate {
// Already in the desired state; no action
return nil
}
if activate {
if used , err := isEmailActive ( sess , email , userID , 0 ) ; err != nil {
return fmt . Errorf ( "isEmailActive(): %v" , err )
} else if used {
return ErrEmailAlreadyUsed { Email : email }
}
}
user . IsActive = activate
if user . Rands , err = GetUserSalt ( ) ; err != nil {
return fmt . Errorf ( "generate salt: %v" , err )
}
if err = updateUserCols ( sess , & user , "is_active" , "rands" ) ; err != nil {
return fmt . Errorf ( "updateUserCols(): %v" , err )
}
} else {
// Activate/deactivate a user's secondary email address
// First check if there's another user active with the same address
addr := EmailAddress { UID : userID , Email : email }
if has , err := sess . Get ( & addr ) ; err != nil {
return err
} else if ! has {
return fmt . Errorf ( "no such email: %d (%s)" , userID , email )
}
if addr . IsActivated == activate {
// Already in the desired state; no action
return nil
}
if activate {
if used , err := isEmailActive ( sess , email , 0 , addr . ID ) ; err != nil {
return fmt . Errorf ( "isEmailActive(): %v" , err )
} else if used {
return ErrEmailAlreadyUsed { Email : email }
}
}
if err = addr . updateActivation ( sess , activate ) ; err != nil {
return fmt . Errorf ( "updateActivation(): %v" , err )
}
}
return sess . Commit ( )
}