email.go (5372B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package user 19 20 import ( 21 "context" 22 "errors" 23 "fmt" 24 "time" 25 26 "github.com/google/uuid" 27 "github.com/superseriousbusiness/gotosocial/internal/config" 28 "github.com/superseriousbusiness/gotosocial/internal/db" 29 "github.com/superseriousbusiness/gotosocial/internal/email" 30 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 31 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 32 "github.com/superseriousbusiness/gotosocial/internal/uris" 33 ) 34 35 var oneWeek = 168 * time.Hour 36 37 // EmailSendConfirmation sends an email address confirmation request email to the given user. 38 func (p *Processor) EmailSendConfirmation(ctx context.Context, user *gtsmodel.User, username string) error { 39 if user.UnconfirmedEmail == "" || user.UnconfirmedEmail == user.Email { 40 // user has already confirmed this email address, so there's nothing to do 41 return nil 42 } 43 44 // We need a token and a link for the user to click on. 45 // We'll use a uuid as our token since it's basically impossible to guess. 46 // From the uuid package we use (which uses crypto/rand under the hood): 47 // Randomly generated UUIDs have 122 random bits. One's annual risk of being 48 // hit by a meteorite is estimated to be one chance in 17 billion, that 49 // means the probability is about 0.00000000006 (6 × 10−11), 50 // equivalent to the odds of creating a few tens of trillions of UUIDs in a 51 // year and having one duplicate. 52 confirmationToken := uuid.NewString() 53 confirmationLink := uris.GenerateURIForEmailConfirm(confirmationToken) 54 55 // pull our instance entry from the database so we can greet the user nicely in the email 56 instance := >smodel.Instance{} 57 host := config.GetHost() 58 if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "domain", Value: host}}, instance); err != nil { 59 return fmt.Errorf("SendConfirmEmail: error getting instance: %s", err) 60 } 61 62 // assemble the email contents and send the email 63 confirmData := email.ConfirmData{ 64 Username: username, 65 InstanceURL: instance.URI, 66 InstanceName: instance.Title, 67 ConfirmLink: confirmationLink, 68 } 69 if err := p.emailSender.SendConfirmEmail(user.UnconfirmedEmail, confirmData); err != nil { 70 return fmt.Errorf("SendConfirmEmail: error sending to email address %s belonging to user %s: %s", user.UnconfirmedEmail, username, err) 71 } 72 73 // email sent, now we need to update the user entry with the token we just sent them 74 updatingColumns := []string{"confirmation_sent_at", "confirmation_token", "last_emailed_at", "updated_at"} 75 user.ConfirmationSentAt = time.Now() 76 user.ConfirmationToken = confirmationToken 77 user.LastEmailedAt = time.Now() 78 user.UpdatedAt = time.Now() 79 80 if err := p.state.DB.UpdateByID(ctx, user, user.ID, updatingColumns...); err != nil { 81 return fmt.Errorf("SendConfirmEmail: error updating user entry after email sent: %s", err) 82 } 83 84 return nil 85 } 86 87 // EmailConfirm processes an email confirmation request, usually initiated as a result of clicking on a link 88 // in a 'confirm your email address' type email. 89 func (p *Processor) EmailConfirm(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) { 90 if token == "" { 91 return nil, gtserror.NewErrorNotFound(errors.New("no token provided")) 92 } 93 94 user, err := p.state.DB.GetUserByConfirmationToken(ctx, token) 95 if err != nil { 96 if err == db.ErrNoEntries { 97 return nil, gtserror.NewErrorNotFound(err) 98 } 99 return nil, gtserror.NewErrorInternalError(err) 100 } 101 102 if user.Account == nil { 103 a, err := p.state.DB.GetAccountByID(ctx, user.AccountID) 104 if err != nil { 105 return nil, gtserror.NewErrorNotFound(err) 106 } 107 user.Account = a 108 } 109 110 if !user.Account.SuspendedAt.IsZero() { 111 return nil, gtserror.NewErrorForbidden(fmt.Errorf("ConfirmEmail: account %s is suspended", user.AccountID)) 112 } 113 114 if user.UnconfirmedEmail == "" || user.UnconfirmedEmail == user.Email { 115 // no pending email confirmations so just return OK 116 return user, nil 117 } 118 119 if user.ConfirmationSentAt.Before(time.Now().Add(-oneWeek)) { 120 return nil, gtserror.NewErrorForbidden(errors.New("ConfirmEmail: confirmation token expired")) 121 } 122 123 // mark the user's email address as confirmed + remove the unconfirmed address and the token 124 updatingColumns := []string{"email", "unconfirmed_email", "confirmed_at", "confirmation_token", "updated_at"} 125 user.Email = user.UnconfirmedEmail 126 user.UnconfirmedEmail = "" 127 user.ConfirmedAt = time.Now() 128 user.ConfirmationToken = "" 129 user.UpdatedAt = time.Now() 130 131 if err := p.state.DB.UpdateByID(ctx, user, user.ID, updatingColumns...); err != nil { 132 return nil, gtserror.NewErrorInternalError(err) 133 } 134 135 return user, nil 136 }