11// Copyright 2016 The Gogs Authors. All rights reserved.
2+ // Copyright 2020 The Gitea Authors. All rights reserved.
23// Use of this source code is governed by a MIT-style
34// license that can be found in the LICENSE file.
45
@@ -8,6 +9,12 @@ import (
89 "errors"
910 "fmt"
1011 "strings"
12+
13+ "code.gitea.io/gitea/modules/log"
14+ "code.gitea.io/gitea/modules/setting"
15+ "code.gitea.io/gitea/modules/util"
16+
17+ "xorm.io/builder"
1118)
1219
1320var (
@@ -54,13 +61,66 @@ func GetEmailAddresses(uid int64) ([]*EmailAddress, error) {
5461 if ! isPrimaryFound {
5562 emails = append (emails , & EmailAddress {
5663 Email : u .Email ,
57- IsActivated : true ,
64+ IsActivated : u . IsActive ,
5865 IsPrimary : true ,
5966 })
6067 }
6168 return emails , nil
6269}
6370
71+ // GetEmailAddressByID gets a user's email address by ID
72+ func GetEmailAddressByID (uid , id int64 ) (* EmailAddress , error ) {
73+ // User ID is required for security reasons
74+ email := & EmailAddress {ID : id , UID : uid }
75+ if has , err := x .Get (email ); err != nil {
76+ return nil , err
77+ } else if ! has {
78+ return nil , nil
79+ }
80+ return email , nil
81+ }
82+
83+ func isEmailActive (e Engine , email string , userID , emailID int64 ) (bool , error ) {
84+ if len (email ) == 0 {
85+ return true , nil
86+ }
87+
88+ // Can't filter by boolean field unless it's explicit
89+ cond := builder .NewCond ()
90+ cond = cond .And (builder.Eq {"email" : email }, builder.Neq {"id" : emailID })
91+ if setting .Service .RegisterEmailConfirm {
92+ // Inactive (unvalidated) addresses don't count as active if email validation is required
93+ cond = cond .And (builder.Eq {"is_activated" : true })
94+ }
95+
96+ em := EmailAddress {}
97+
98+ if has , err := e .Where (cond ).Get (& em ); has || err != nil {
99+ if has {
100+ log .Info ("isEmailActive('%s',%d,%d) found duplicate in email ID %d" , email , userID , emailID , em .ID )
101+ }
102+ return has , err
103+ }
104+
105+ // Can't filter by boolean field unless it's explicit
106+ cond = builder .NewCond ()
107+ cond = cond .And (builder.Eq {"email" : email }, builder.Neq {"id" : userID })
108+ if setting .Service .RegisterEmailConfirm {
109+ cond = cond .And (builder.Eq {"is_active" : true })
110+ }
111+
112+ us := User {}
113+
114+ if has , err := e .Where (cond ).Get (& us ); has || err != nil {
115+ if has {
116+ log .Info ("isEmailActive('%s',%d,%d) found duplicate in user ID %d" , email , userID , emailID , us .ID )
117+ }
118+ return has , err
119+ }
120+
121+ return false , nil
122+ }
123+
64124func isEmailUsed (e Engine , email string ) (bool , error ) {
65125 if len (email ) == 0 {
66126 return true , nil
@@ -118,31 +178,30 @@ func AddEmailAddresses(emails []*EmailAddress) error {
118178
119179// Activate activates the email address to given user.
120180func (email * EmailAddress ) Activate () error {
121- user , err := GetUserByID (email .UID )
122- if err != nil {
181+ sess := x .NewSession ()
182+ defer sess .Close ()
183+ if err := sess .Begin (); err != nil {
123184 return err
124185 }
125- if user . Rands , err = GetUserSalt ( ); err != nil {
186+ if err := email . updateActivation ( sess , true ); err != nil {
126187 return err
127188 }
189+ return sess .Commit ()
190+ }
128191
129- sess := x . NewSession ()
130- defer sess . Close ( )
131- if err = sess . Begin (); err != nil {
192+ func ( email * EmailAddress ) updateActivation ( e Engine , activate bool ) error {
193+ user , err := getUserByID ( e , email . UID )
194+ if err != nil {
132195 return err
133196 }
134-
135- email .IsActivated = true
136- if _ , err := sess .
137- ID (email .ID ).
138- Cols ("is_activated" ).
139- Update (email ); err != nil {
197+ if user .Rands , err = GetUserSalt (); err != nil {
140198 return err
141- } else if err = updateUserCols (sess , user , "rands" ); err != nil {
199+ }
200+ email .IsActivated = activate
201+ if _ , err := e .ID (email .ID ).Cols ("is_activated" ).Update (email ); err != nil {
142202 return err
143203 }
144-
145- return sess .Commit ()
204+ return updateUserCols (e , user , "rands" )
146205}
147206
148207// DeleteEmailAddress deletes an email address of given user.
@@ -228,3 +287,199 @@ func MakeEmailPrimary(email *EmailAddress) error {
228287
229288 return sess .Commit ()
230289}
290+
291+ // SearchEmailOrderBy is used to sort the results from SearchEmails()
292+ type SearchEmailOrderBy string
293+
294+ func (s SearchEmailOrderBy ) String () string {
295+ return string (s )
296+ }
297+
298+ // Strings for sorting result
299+ const (
300+ SearchEmailOrderByEmail SearchEmailOrderBy = "emails.email ASC, is_primary DESC, sortid ASC"
301+ SearchEmailOrderByEmailReverse SearchEmailOrderBy = "emails.email DESC, is_primary ASC, sortid DESC"
302+ SearchEmailOrderByName SearchEmailOrderBy = "`user`.lower_name ASC, is_primary DESC, sortid ASC"
303+ SearchEmailOrderByNameReverse SearchEmailOrderBy = "`user`.lower_name DESC, is_primary ASC, sortid DESC"
304+ )
305+
306+ // SearchEmailOptions are options to search e-mail addresses for the admin panel
307+ type SearchEmailOptions struct {
308+ Page int
309+ PageSize int // Can be smaller than or equal to setting.UI.ExplorePagingNum
310+ Keyword string
311+ SortType SearchEmailOrderBy
312+ IsPrimary util.OptionalBool
313+ IsActivated util.OptionalBool
314+ }
315+
316+ // SearchEmailResult is an e-mail address found in the user or email_address table
317+ type SearchEmailResult struct {
318+ UID int64
319+ Email string
320+ IsActivated bool
321+ IsPrimary bool
322+ // From User
323+ Name string
324+ FullName string
325+ }
326+
327+ // SearchEmails takes options i.e. keyword and part of email name to search,
328+ // it returns results in given range and number of total results.
329+ func SearchEmails (opts * SearchEmailOptions ) ([]* SearchEmailResult , int64 , error ) {
330+ // Unfortunately, UNION support for SQLite in xorm is currently broken, so we must
331+ // build the SQL ourselves.
332+ where := make ([]string , 0 , 5 )
333+ args := make ([]interface {}, 0 , 5 )
334+
335+ emailsSQL := "(SELECT id as sortid, uid, email, is_activated, 0 as is_primary " +
336+ "FROM email_address " +
337+ "UNION ALL " +
338+ "SELECT id as sortid, id AS uid, email, is_active AS is_activated, 1 as is_primary " +
339+ "FROM `user` " +
340+ "WHERE type = ?) AS emails"
341+ args = append (args , UserTypeIndividual )
342+
343+ if len (opts .Keyword ) > 0 {
344+ // Note: % can be injected in the Keyword parameter, but it won't do any harm.
345+ where = append (where , "(lower(`user`.full_name) LIKE ? OR `user`.lower_name LIKE ? OR emails.email LIKE ?)" )
346+ likeStr := "%" + strings .ToLower (opts .Keyword ) + "%"
347+ args = append (args , likeStr )
348+ args = append (args , likeStr )
349+ args = append (args , likeStr )
350+ }
351+
352+ switch {
353+ case opts .IsPrimary .IsTrue ():
354+ where = append (where , "emails.is_primary = ?" )
355+ args = append (args , true )
356+ case opts .IsPrimary .IsFalse ():
357+ where = append (where , "emails.is_primary = ?" )
358+ args = append (args , false )
359+ }
360+
361+ switch {
362+ case opts .IsActivated .IsTrue ():
363+ where = append (where , "emails.is_activated = ?" )
364+ args = append (args , true )
365+ case opts .IsActivated .IsFalse ():
366+ where = append (where , "emails.is_activated = ?" )
367+ args = append (args , false )
368+ }
369+
370+ var whereStr string
371+ if len (where ) > 0 {
372+ whereStr = "WHERE " + strings .Join (where , " AND " )
373+ }
374+
375+ joinSQL := "FROM " + emailsSQL + " INNER JOIN `user` ON `user`.id = emails.uid " + whereStr
376+
377+ count , err := x .SQL ("SELECT count(*) " + joinSQL , args ... ).Count ()
378+ if err != nil {
379+ return nil , 0 , fmt .Errorf ("Count: %v" , err )
380+ }
381+
382+ orderby := opts .SortType .String ()
383+ if orderby == "" {
384+ orderby = SearchEmailOrderByEmail .String ()
385+ }
386+
387+ querySQL := "SELECT emails.uid, emails.email, emails.is_activated, emails.is_primary, " +
388+ "`user`.name, `user`.full_name " + joinSQL + " ORDER BY " + orderby
389+
390+ if opts .PageSize == 0 || opts .PageSize > setting .UI .ExplorePagingNum {
391+ opts .PageSize = setting .UI .ExplorePagingNum
392+ }
393+ if opts .Page <= 0 {
394+ opts .Page = 1
395+ }
396+
397+ rows , err := x .SQL (querySQL , args ... ).Rows (new (SearchEmailResult ))
398+ if err != nil {
399+ return nil , 0 , fmt .Errorf ("Emails: %v" , err )
400+ }
401+
402+ // Page manually because xorm can't handle Limit() with raw SQL
403+ defer rows .Close ()
404+
405+ emails := make ([]* SearchEmailResult , 0 , opts .PageSize )
406+ skip := (opts .Page - 1 ) * opts .PageSize
407+
408+ for rows .Next () {
409+ var email SearchEmailResult
410+ if err := rows .Scan (& email ); err != nil {
411+ return nil , 0 , err
412+ }
413+ if skip > 0 {
414+ skip --
415+ continue
416+ }
417+ emails = append (emails , & email )
418+ if len (emails ) == opts .PageSize {
419+ break
420+ }
421+ }
422+
423+ return emails , count , err
424+ }
425+
426+ // ActivateUserEmail will change the activated state of an email address,
427+ // either primary (in the user table) or secondary (in the email_address table)
428+ func ActivateUserEmail (userID int64 , email string , primary , activate bool ) (err error ) {
429+ sess := x .NewSession ()
430+ defer sess .Close ()
431+ if err = sess .Begin (); err != nil {
432+ return err
433+ }
434+ if primary {
435+ // Activate/deactivate a user's primary email address
436+ user := User {ID : userID , Email : email }
437+ if has , err := sess .Get (& user ); err != nil {
438+ return err
439+ } else if ! has {
440+ return fmt .Errorf ("no such user: %d (%s)" , userID , email )
441+ }
442+ if user .IsActive == activate {
443+ // Already in the desired state; no action
444+ return nil
445+ }
446+ if activate {
447+ if used , err := isEmailActive (sess , email , userID , 0 ); err != nil {
448+ return fmt .Errorf ("isEmailActive(): %v" , err )
449+ } else if used {
450+ return ErrEmailAlreadyUsed {Email : email }
451+ }
452+ }
453+ user .IsActive = activate
454+ if user .Rands , err = GetUserSalt (); err != nil {
455+ return fmt .Errorf ("generate salt: %v" , err )
456+ }
457+ if err = updateUserCols (sess , & user , "is_active" , "rands" ); err != nil {
458+ return fmt .Errorf ("updateUserCols(): %v" , err )
459+ }
460+ } else {
461+ // Activate/deactivate a user's secondary email address
462+ // First check if there's another user active with the same address
463+ addr := EmailAddress {UID : userID , Email : email }
464+ if has , err := sess .Get (& addr ); err != nil {
465+ return err
466+ } else if ! has {
467+ return fmt .Errorf ("no such email: %d (%s)" , userID , email )
468+ }
469+ if addr .IsActivated == activate {
470+ // Already in the desired state; no action
471+ return nil
472+ }
473+ if activate {
474+ if used , err := isEmailActive (sess , email , 0 , addr .ID ); err != nil {
475+ return fmt .Errorf ("isEmailActive(): %v" , err )
476+ } else if used {
477+ return ErrEmailAlreadyUsed {Email : email }
478+ }
479+ }
480+ if err = addr .updateActivation (sess , activate ); err != nil {
481+ return fmt .Errorf ("updateActivation(): %v" , err )
482+ }
483+ }
484+ return sess .Commit ()
485+ }
0 commit comments