From b64f101ab91cc9aab9c44982d985c21b2389a966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Tue, 28 Jul 2026 10:41:00 +0200 Subject: [PATCH] Drop resend verification email cooldown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resend only runs after an explicit form submit, so a per-address cooldown is unnecessary overhead compared with forgot-password. Signed-off-by: Émile Ré --- packages/emails/emails.go | 4 ++-- pkg/coredata/email.go | 33 --------------------------------- pkg/iam/account_service.go | 19 ------------------- 3 files changed, 2 insertions(+), 54 deletions(-) diff --git a/packages/emails/emails.go b/packages/emails/emails.go index 302b6ba95..1077897cf 100644 --- a/packages/emails/emails.go +++ b/packages/emails/emails.go @@ -100,7 +100,7 @@ func NewPresenter(baseURL string, fullName string) *Presenter { } const ( - SubjectConfirmEmail = "Confirm your email address" + subjectConfirmEmail = "Confirm your email address" subjectPasswordReset = "Reset your password" subjectInvitation = "Invitation to join %s on Probo" subjectDocumentApproval = "Action Required – Please review and approve %s compliance documents" @@ -185,7 +185,7 @@ func (p *Presenter) RenderConfirmEmail(ctx context.Context, confirmationURLPath textBody, htmlBody, err = renderEmail(confirmEmailTextTemplate, confirmEmailHTMLTemplate, data) - return SubjectConfirmEmail, textBody, htmlBody, err + return subjectConfirmEmail, textBody, htmlBody, err } func (p *Presenter) RenderPasswordReset(ctx context.Context, resetPasswordURLPath string, resetPasswordToken string) (subject string, textBody string, htmlBody *string, err error) { diff --git a/pkg/coredata/email.go b/pkg/coredata/email.go index 7471c9b17..f6341dceb 100644 --- a/pkg/coredata/email.go +++ b/pkg/coredata/email.go @@ -319,39 +319,6 @@ WHERE id = @id return err } -// ExistsByRecipientSubjectCreatedAfter reports whether an email with the given -// recipient and subject was created at or after createdAfter. -func ExistsByRecipientSubjectCreatedAfter( - ctx context.Context, - conn pg.Querier, - recipientEmail mail.Addr, - subject string, - createdAfter time.Time, -) (bool, error) { - q := ` -SELECT EXISTS ( - SELECT 1 - FROM emails - WHERE recipient_email = @recipient_email - AND subject = @subject - AND created_at >= @created_after -) -` - - args := pgx.StrictNamedArgs{ - "recipient_email": recipientEmail.String(), - "subject": subject, - "created_after": createdAfter, - } - - var exists bool - if err := conn.QueryRow(ctx, q, args).Scan(&exists); err != nil { - return false, fmt.Errorf("cannot check recent email: %w", err) - } - - return exists, nil -} - func ResetStaleProcessingEmails( ctx context.Context, conn pg.Querier, diff --git a/pkg/iam/account_service.go b/pkg/iam/account_service.go index 8fb6520c2..2ccd08cd6 100644 --- a/pkg/iam/account_service.go +++ b/pkg/iam/account_service.go @@ -76,11 +76,6 @@ var SupportedIdentityLocales = []string{ const ( TokenTypeEmailConfirmation = "email_confirmation" - - // emailConfirmationResendCooldown is the minimum time between confirmation - // emails for the same address. Resend requests inside this window succeed - // without enqueueing another message (anti-enumeration + anti-abuse). - emailConfirmationResendCooldown = time.Minute ) func NewAccountService(svc *Service) *AccountService { @@ -248,20 +243,6 @@ func (s AccountService) ResendVerificationEmail(ctx context.Context, email mail. return nil // Don't leak information about already-verified identities } - recent, err := coredata.ExistsByRecipientSubjectCreatedAfter( - ctx, - tx, - identity.EmailAddress, - emails.SubjectConfirmEmail, - time.Now().Add(-emailConfirmationResendCooldown), - ) - if err != nil { - return fmt.Errorf("cannot check recent confirmation email: %w", err) - } - if recent { - return nil // Cooldown: avoid flooding the recipient / mail queue - } - confirmationToken, err := statelesstoken.NewToken( s.tokenSecret, TokenTypeEmailConfirmation,