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