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,193 @@ 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+ ListOptions
309+ Keyword string
310+ SortType SearchEmailOrderBy
311+ IsPrimary util.OptionalBool
312+ IsActivated util.OptionalBool
313+ }
314+
315+ // SearchEmailResult is an e-mail address found in the user or email_address table
316+ type SearchEmailResult struct {
317+ UID int64
318+ Email string
319+ IsActivated bool
320+ IsPrimary bool
321+ // From User
322+ Name string
323+ FullName string
324+ }
325+
326+ // SearchEmails takes options i.e. keyword and part of email name to search,
327+ // it returns results in given range and number of total results.
328+ func SearchEmails (opts * SearchEmailOptions ) ([]* SearchEmailResult , int64 , error ) {
329+ // Unfortunately, UNION support for SQLite in xorm is currently broken, so we must
330+ // build the SQL ourselves.
331+ where := make ([]string , 0 , 5 )
332+ args := make ([]interface {}, 0 , 5 )
333+
334+ emailsSQL := "(SELECT id as sortid, uid, email, is_activated, 0 as is_primary " +
335+ "FROM email_address " +
336+ "UNION ALL " +
337+ "SELECT id as sortid, id AS uid, email, is_active AS is_activated, 1 as is_primary " +
338+ "FROM `user` " +
339+ "WHERE type = ?) AS emails"
340+ args = append (args , UserTypeIndividual )
341+
342+ if len (opts .Keyword ) > 0 {
343+ // Note: % can be injected in the Keyword parameter, but it won't do any harm.
344+ where = append (where , "(lower(`user`.full_name) LIKE ? OR `user`.lower_name LIKE ? OR emails.email LIKE ?)" )
345+ likeStr := "%" + strings .ToLower (opts .Keyword ) + "%"
346+ args = append (args , likeStr )
347+ args = append (args , likeStr )
348+ args = append (args , likeStr )
349+ }
350+
351+ switch {
352+ case opts .IsPrimary .IsTrue ():
353+ where = append (where , "emails.is_primary = ?" )
354+ args = append (args , true )
355+ case opts .IsPrimary .IsFalse ():
356+ where = append (where , "emails.is_primary = ?" )
357+ args = append (args , false )
358+ }
359+
360+ switch {
361+ case opts .IsActivated .IsTrue ():
362+ where = append (where , "emails.is_activated = ?" )
363+ args = append (args , true )
364+ case opts .IsActivated .IsFalse ():
365+ where = append (where , "emails.is_activated = ?" )
366+ args = append (args , false )
367+ }
368+
369+ var whereStr string
370+ if len (where ) > 0 {
371+ whereStr = "WHERE " + strings .Join (where , " AND " )
372+ }
373+
374+ joinSQL := "FROM " + emailsSQL + " INNER JOIN `user` ON `user`.id = emails.uid " + whereStr
375+
376+ count , err := x .SQL ("SELECT count(*) " + joinSQL , args ... ).Count ()
377+ if err != nil {
378+ return nil , 0 , fmt .Errorf ("Count: %v" , err )
379+ }
380+
381+ orderby := opts .SortType .String ()
382+ if orderby == "" {
383+ orderby = SearchEmailOrderByEmail .String ()
384+ }
385+
386+ querySQL := "SELECT emails.uid, emails.email, emails.is_activated, emails.is_primary, " +
387+ "`user`.name, `user`.full_name " + joinSQL + " ORDER BY " + orderby
388+
389+ opts .setDefaultValues ()
390+
391+ rows , err := x .SQL (querySQL , args ... ).Rows (new (SearchEmailResult ))
392+ if err != nil {
393+ return nil , 0 , fmt .Errorf ("Emails: %v" , err )
394+ }
395+
396+ // Page manually because xorm can't handle Limit() with raw SQL
397+ defer rows .Close ()
398+
399+ emails := make ([]* SearchEmailResult , 0 , opts .PageSize )
400+ skip := (opts .Page - 1 ) * opts .PageSize
401+
402+ for rows .Next () {
403+ var email SearchEmailResult
404+ if err := rows .Scan (& email ); err != nil {
405+ return nil , 0 , err
406+ }
407+ if skip > 0 {
408+ skip --
409+ continue
410+ }
411+ emails = append (emails , & email )
412+ if len (emails ) == opts .PageSize {
413+ break
414+ }
415+ }
416+
417+ return emails , count , err
418+ }
419+
420+ // ActivateUserEmail will change the activated state of an email address,
421+ // either primary (in the user table) or secondary (in the email_address table)
422+ func ActivateUserEmail (userID int64 , email string , primary , activate bool ) (err error ) {
423+ sess := x .NewSession ()
424+ defer sess .Close ()
425+ if err = sess .Begin (); err != nil {
426+ return err
427+ }
428+ if primary {
429+ // Activate/deactivate a user's primary email address
430+ user := User {ID : userID , Email : email }
431+ if has , err := sess .Get (& user ); err != nil {
432+ return err
433+ } else if ! has {
434+ return fmt .Errorf ("no such user: %d (%s)" , userID , email )
435+ }
436+ if user .IsActive == activate {
437+ // Already in the desired state; no action
438+ return nil
439+ }
440+ if activate {
441+ if used , err := isEmailActive (sess , email , userID , 0 ); err != nil {
442+ return fmt .Errorf ("isEmailActive(): %v" , err )
443+ } else if used {
444+ return ErrEmailAlreadyUsed {Email : email }
445+ }
446+ }
447+ user .IsActive = activate
448+ if user .Rands , err = GetUserSalt (); err != nil {
449+ return fmt .Errorf ("generate salt: %v" , err )
450+ }
451+ if err = updateUserCols (sess , & user , "is_active" , "rands" ); err != nil {
452+ return fmt .Errorf ("updateUserCols(): %v" , err )
453+ }
454+ } else {
455+ // Activate/deactivate a user's secondary email address
456+ // First check if there's another user active with the same address
457+ addr := EmailAddress {UID : userID , Email : email }
458+ if has , err := sess .Get (& addr ); err != nil {
459+ return err
460+ } else if ! has {
461+ return fmt .Errorf ("no such email: %d (%s)" , userID , email )
462+ }
463+ if addr .IsActivated == activate {
464+ // Already in the desired state; no action
465+ return nil
466+ }
467+ if activate {
468+ if used , err := isEmailActive (sess , email , 0 , addr .ID ); err != nil {
469+ return fmt .Errorf ("isEmailActive(): %v" , err )
470+ } else if used {
471+ return ErrEmailAlreadyUsed {Email : email }
472+ }
473+ }
474+ if err = addr .updateActivation (sess , activate ); err != nil {
475+ return fmt .Errorf ("updateActivation(): %v" , err )
476+ }
477+ }
478+ return sess .Commit ()
479+ }
0 commit comments