sendEmailVerificationCode generates a code, atomically stores its hash (subject to cooldown), and emails the plain code. Returns nil on a successful send as well as on silent-skip paths (cooldown active, or PASSWORD_RESET for an unknown email) — both are intentionally indistinguishable to the caller
(ctx context.Context, workspaceName *string, email string, purpose storepb.EmailVerificationCodePurpose, subject, bodyFmt string)
| 1711 | // to know email delivery failed), `RequestPasswordReset` swallows it to avoid revealing that |
| 1712 | // the account exists. `bodyFmt` must contain one %s for the 6-digit code. |
| 1713 | func (s *AuthService) sendEmailVerificationCode(ctx context.Context, workspaceName *string, email string, purpose storepb.EmailVerificationCodePurpose, subject, bodyFmt string) error { |
| 1714 | // For password reset, only send to existing principals — no upsert, no email for unknown addresses. |
| 1715 | // Prevents arbitrary email spam. LOGIN intentionally skips this check (also covers signup). |
| 1716 | if purpose == storepb.EmailVerificationCodePurpose_PASSWORD_RESET { |
| 1717 | account, err := s.store.GetAccountByEmail(ctx, email) |
| 1718 | if err != nil { |
| 1719 | return errors.Wrap(err, "failed to look up account for password reset") |
| 1720 | } |
| 1721 | if account == nil || account.Type != storepb.PrincipalType_END_USER { |
| 1722 | return nil // silent: account doesn't exist |
| 1723 | } |
| 1724 | } |
| 1725 | |
| 1726 | workspaceID, err := parseOptionalWorkspace(workspaceName) |
| 1727 | if err != nil { |
| 1728 | return errors.Wrap(err, "failed to parse workspace id") |
| 1729 | } |
| 1730 | |
| 1731 | // Resolve the EMAIL setting FIRST — fail fast if misconfigured so we don't write a |
| 1732 | // verification row we can't actually deliver. |
| 1733 | emailSetting, err := resolvePreLoginEmailSetting(ctx, s.store, workspaceID) |
| 1734 | if err != nil { |
| 1735 | return err |
| 1736 | } |
| 1737 | if emailSetting == nil { |
| 1738 | return errors.Errorf("cannot found email config for workspace %v", workspaceID) |
| 1739 | } |
| 1740 | |
| 1741 | code, err := generateEmailCode() |
| 1742 | if err != nil { |
| 1743 | return errors.Wrap(err, "failed to generate code") |
| 1744 | } |
| 1745 | |
| 1746 | now := time.Now() |
| 1747 | sent, err := s.store.UpsertEmailVerificationCodeIfCooldownExpired(ctx, &store.EmailVerificationCodeMessage{ |
| 1748 | Email: email, |
| 1749 | Purpose: purpose, |
| 1750 | CodeHash: s.hashEmailCode(code), |
| 1751 | ExpiresAt: now.Add(emailCodeExpiry), |
| 1752 | LastSentAt: now, |
| 1753 | Workspace: workspaceID, |
| 1754 | }, emailCodeResendCooldown) |
| 1755 | if err != nil { |
| 1756 | return errors.Wrap(err, "failed to upsert verification code") |
| 1757 | } |
| 1758 | if !sent { |
| 1759 | return nil // cooldown active — silent skip |
| 1760 | } |
| 1761 | |
| 1762 | sender, err := mailer.NewSender(emailSetting) |
| 1763 | if err != nil { |
| 1764 | return errors.Wrap(err, "failed to create mail sender") |
| 1765 | } |
| 1766 | |
| 1767 | body := fmt.Sprintf(bodyFmt, code, int(emailCodeExpiry.Minutes())) |
| 1768 | if err := sender.Send(ctx, &mailer.SendRequest{ |
| 1769 | To: []string{email}, |
| 1770 | Subject: subject, |
no test coverage detected