5cfa7ca460
There are multiple entries in the users table with the same email address. This is because in the past users were able to register multiple times if the email was not verified. This is no longer the case. If a user tries to register with an unverified email already in the DB, we send a verification email instead of creating another entry. However, since these old entries in the table with duplicate emails were never cleaned up, the email reminder chore will send out email verification reminders to them. A single person will get one separate email per entry in the DB with their email and where status = 0. Since the multiple entries with the same email problem was solved a while ago, just add a constraint to GetUnverifiedNeedingReminder to only select users created after a cutoff. Once the DB is migrated to remove the duplicate emails, we can remove the cutoff. github issue: https://github.com/storj/storj/issues/4853 Change-Id: I07d77d43109bcacc8909df61d4fb49165a99527c
156 lines
5.0 KiB
Go
156 lines
5.0 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package emailreminders
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spacemonkeygo/monkit/v3"
|
|
"go.uber.org/zap"
|
|
|
|
"storj.io/common/sync2"
|
|
"storj.io/storj/private/post"
|
|
"storj.io/storj/satellite/console"
|
|
"storj.io/storj/satellite/console/consoleauth"
|
|
"storj.io/storj/satellite/console/consoleweb/consoleapi"
|
|
"storj.io/storj/satellite/console/consoleweb/consoleql"
|
|
"storj.io/storj/satellite/mailservice"
|
|
)
|
|
|
|
var mon = monkit.Package()
|
|
|
|
// Config contains configurations for email reminders.
|
|
type Config struct {
|
|
FirstVerificationReminder time.Duration `help:"amount of time before sending first reminder to users who need to verify their email" default:"24h"`
|
|
SecondVerificationReminder time.Duration `help:"amount of time before sending second reminder to users who need to verify their email" default:"120h"`
|
|
ChoreInterval time.Duration `help:"how often to send reminders to users who need to verify their email" default:"24h"`
|
|
Enable bool `help:"enable sending emails reminding users to verify their email" releaseDefault:"false" devDefault:"true"`
|
|
}
|
|
|
|
// Chore checks whether any emails need to be re-sent.
|
|
//
|
|
// architecture: Chore
|
|
type Chore struct {
|
|
log *zap.Logger
|
|
Loop *sync2.Cycle
|
|
|
|
tokens *consoleauth.Service
|
|
usersDB console.Users
|
|
mailService *mailservice.Service
|
|
config Config
|
|
address string
|
|
useBlockingSend bool
|
|
}
|
|
|
|
// NewChore instantiates Chore.
|
|
func NewChore(log *zap.Logger, tokens *consoleauth.Service, usersDB console.Users, mailservice *mailservice.Service, config Config, address string) *Chore {
|
|
if !strings.HasSuffix(address, "/") {
|
|
address += "/"
|
|
}
|
|
return &Chore{
|
|
log: log,
|
|
Loop: sync2.NewCycle(config.ChoreInterval),
|
|
tokens: tokens,
|
|
usersDB: usersDB,
|
|
config: config,
|
|
mailService: mailservice,
|
|
address: address,
|
|
useBlockingSend: false,
|
|
}
|
|
}
|
|
|
|
// Run starts the chore.
|
|
func (chore *Chore) Run(ctx context.Context) (err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
return chore.Loop.Run(ctx, func(ctx context.Context) (err error) {
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
now := time.Now()
|
|
|
|
// cutoff to avoid emailing users multiple times due to email duplicates in the DB.
|
|
// TODO: remove cutoff once duplicates are removed.
|
|
cutoff := now.Add(30 * (-24 * time.Hour))
|
|
|
|
users, err := chore.usersDB.GetUnverifiedNeedingReminder(ctx, now.Add(-chore.config.FirstVerificationReminder), now.Add(-chore.config.SecondVerificationReminder), cutoff)
|
|
if err != nil {
|
|
chore.log.Error("error getting users in need of reminder", zap.Error(err))
|
|
return nil
|
|
}
|
|
mon.IntVal("unverified_needing_reminder").Observe(int64(len(users)))
|
|
|
|
for _, u := range users {
|
|
|
|
token, err := chore.tokens.CreateToken(ctx, u.ID, u.Email)
|
|
|
|
if err != nil {
|
|
chore.log.Error("error generating activation token", zap.Error(err))
|
|
return nil
|
|
}
|
|
authController := consoleapi.NewAuth(zap.L(), nil, nil, nil, nil, nil, chore.address, "", "", "")
|
|
|
|
link := authController.ActivateAccountURL + "?token=" + token
|
|
userName := u.ShortName
|
|
if u.ShortName == "" {
|
|
userName = u.FullName
|
|
}
|
|
|
|
// blocking send allows us to verify that links are clicked in tests.
|
|
if chore.useBlockingSend {
|
|
err = chore.mailService.SendRendered(
|
|
ctx,
|
|
[]post.Address{{Address: u.Email, Name: userName}},
|
|
&consoleql.AccountActivationEmail{
|
|
ActivationLink: link,
|
|
Origin: authController.ExternalAddress,
|
|
UserName: userName,
|
|
},
|
|
)
|
|
if err != nil {
|
|
chore.log.Error("error sending email reminder", zap.Error(err))
|
|
continue
|
|
}
|
|
} else {
|
|
chore.mailService.SendRenderedAsync(
|
|
ctx,
|
|
[]post.Address{{Address: u.Email, Name: userName}},
|
|
&consoleql.AccountActivationEmail{
|
|
ActivationLink: link,
|
|
Origin: authController.ExternalAddress,
|
|
UserName: userName,
|
|
},
|
|
)
|
|
}
|
|
if err = chore.usersDB.UpdateVerificationReminders(ctx, u.ID); err != nil {
|
|
chore.log.Error("error updating user's last email verifcation reminder", zap.Error(err))
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Close closes chore.
|
|
func (chore *Chore) Close() error {
|
|
chore.Loop.Close()
|
|
return nil
|
|
}
|
|
|
|
// TestSetLinkAddress allows the email link address to be reconfigured.
|
|
// The address points to the satellite web server's external address.
|
|
// In the test environment the external address is not set by a config.
|
|
// It is an internal address, and we don't know what the port is until after it
|
|
// has been assigned. With this method, we get the address from the api in testplanet
|
|
// and assign it here.
|
|
func (chore *Chore) TestSetLinkAddress(address string) {
|
|
chore.address = address
|
|
}
|
|
|
|
// TestUseBlockingSend allows us to set the chore to use a blocking send method.
|
|
// Using a blocking send method allows us to test that links are clicked without
|
|
// potential race conditions.
|
|
func (chore *Chore) TestUseBlockingSend() {
|
|
chore.useBlockingSend = true
|
|
}
|