Your verification code is:
This code will expire in 15 minutes.
If you did not request this code, please ignore this email.
package service import ( "context" "crypto/rand" "crypto/tls" "fmt" "math/big" "net/smtp" "strconv" "time" infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" ) var ( ErrEmailNotConfigured = infraerrors.ServiceUnavailable("EMAIL_NOT_CONFIGURED", "email service not configured") ErrInvalidVerifyCode = infraerrors.BadRequest("INVALID_VERIFY_CODE", "invalid or expired verification code") ErrVerifyCodeTooFrequent = infraerrors.TooManyRequests("VERIFY_CODE_TOO_FREQUENT", "please wait before requesting a new code") ErrVerifyCodeMaxAttempts = infraerrors.TooManyRequests("VERIFY_CODE_MAX_ATTEMPTS", "too many failed attempts, please request a new code") ) // EmailCache defines cache operations for email service type EmailCache interface { GetVerificationCode(ctx context.Context, email string) (*VerificationCodeData, error) SetVerificationCode(ctx context.Context, email string, data *VerificationCodeData, ttl time.Duration) error DeleteVerificationCode(ctx context.Context, email string) error } // VerificationCodeData represents verification code data type VerificationCodeData struct { Code string Attempts int CreatedAt time.Time } const ( verifyCodeTTL = 15 * time.Minute verifyCodeCooldown = 1 * time.Minute maxVerifyCodeAttempts = 5 ) // SMTPConfig SMTP配置 type SMTPConfig struct { Host string Port int Username string Password string From string FromName string UseTLS bool } // EmailService 邮件服务 type EmailService struct { settingRepo SettingRepository cache EmailCache } // NewEmailService 创建邮件服务实例 func NewEmailService(settingRepo SettingRepository, cache EmailCache) *EmailService { return &EmailService{ settingRepo: settingRepo, cache: cache, } } // GetSMTPConfig 从数据库获取SMTP配置 func (s *EmailService) GetSMTPConfig(ctx context.Context) (*SMTPConfig, error) { keys := []string{ SettingKeySMTPHost, SettingKeySMTPPort, SettingKeySMTPUsername, SettingKeySMTPPassword, SettingKeySMTPFrom, SettingKeySMTPFromName, SettingKeySMTPUseTLS, } settings, err := s.settingRepo.GetMultiple(ctx, keys) if err != nil { return nil, fmt.Errorf("get smtp settings: %w", err) } host := settings[SettingKeySMTPHost] if host == "" { return nil, ErrEmailNotConfigured } port := 587 // 默认端口 if portStr := settings[SettingKeySMTPPort]; portStr != "" { if p, err := strconv.Atoi(portStr); err == nil { port = p } } useTLS := settings[SettingKeySMTPUseTLS] == "true" return &SMTPConfig{ Host: host, Port: port, Username: settings[SettingKeySMTPUsername], Password: settings[SettingKeySMTPPassword], From: settings[SettingKeySMTPFrom], FromName: settings[SettingKeySMTPFromName], UseTLS: useTLS, }, nil } // SendEmail 发送邮件(使用数据库中保存的配置) func (s *EmailService) SendEmail(ctx context.Context, to, subject, body string) error { config, err := s.GetSMTPConfig(ctx) if err != nil { return err } return s.SendEmailWithConfig(config, to, subject, body) } // SendEmailWithConfig 使用指定配置发送邮件 func (s *EmailService) SendEmailWithConfig(config *SMTPConfig, to, subject, body string) error { from := config.From if config.FromName != "" { from = fmt.Sprintf("%s <%s>", config.FromName, config.From) } msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n%s", from, to, subject, body) addr := fmt.Sprintf("%s:%d", config.Host, config.Port) auth := smtp.PlainAuth("", config.Username, config.Password, config.Host) if config.UseTLS { return s.sendMailTLS(addr, auth, config.From, to, []byte(msg), config.Host) } return smtp.SendMail(addr, auth, config.From, []string{to}, []byte(msg)) } // sendMailTLS 使用TLS发送邮件 func (s *EmailService) sendMailTLS(addr string, auth smtp.Auth, from, to string, msg []byte, host string) error { tlsConfig := &tls.Config{ ServerName: host, // 强制 TLS 1.2+,避免协议降级导致的弱加密风险。 MinVersion: tls.VersionTLS12, } conn, err := tls.Dial("tcp", addr, tlsConfig) if err != nil { return fmt.Errorf("tls dial: %w", err) } defer func() { _ = conn.Close() }() client, err := smtp.NewClient(conn, host) if err != nil { return fmt.Errorf("new smtp client: %w", err) } defer func() { _ = client.Close() }() if err = client.Auth(auth); err != nil { return fmt.Errorf("smtp auth: %w", err) } if err = client.Mail(from); err != nil { return fmt.Errorf("smtp mail: %w", err) } if err = client.Rcpt(to); err != nil { return fmt.Errorf("smtp rcpt: %w", err) } w, err := client.Data() if err != nil { return fmt.Errorf("smtp data: %w", err) } _, err = w.Write(msg) if err != nil { return fmt.Errorf("write msg: %w", err) } err = w.Close() if err != nil { return fmt.Errorf("close writer: %w", err) } // Email is sent successfully after w.Close(), ignore Quit errors // Some SMTP servers return non-standard responses on QUIT _ = client.Quit() return nil } // GenerateVerifyCode 生成6位数字验证码 func (s *EmailService) GenerateVerifyCode() (string, error) { const digits = "0123456789" code := make([]byte, 6) for i := range code { num, err := rand.Int(rand.Reader, big.NewInt(int64(len(digits)))) if err != nil { return "", err } code[i] = digits[num.Int64()] } return string(code), nil } // SendVerifyCode 发送验证码邮件 func (s *EmailService) SendVerifyCode(ctx context.Context, email, siteName string) error { // 检查是否在冷却期内 existing, err := s.cache.GetVerificationCode(ctx, email) if err == nil && existing != nil { if time.Since(existing.CreatedAt) < verifyCodeCooldown { return ErrVerifyCodeTooFrequent } } // 生成验证码 code, err := s.GenerateVerifyCode() if err != nil { return fmt.Errorf("generate code: %w", err) } // 保存验证码到 Redis data := &VerificationCodeData{ Code: code, Attempts: 0, CreatedAt: time.Now(), } if err := s.cache.SetVerificationCode(ctx, email, data, verifyCodeTTL); err != nil { return fmt.Errorf("save verify code: %w", err) } // 构建邮件内容 subject := fmt.Sprintf("[%s] Email Verification Code", siteName) body := s.buildVerifyCodeEmailBody(code, siteName) // 发送邮件 if err := s.SendEmail(ctx, email, subject, body); err != nil { return fmt.Errorf("send email: %w", err) } return nil } // VerifyCode 验证验证码 func (s *EmailService) VerifyCode(ctx context.Context, email, code string) error { data, err := s.cache.GetVerificationCode(ctx, email) if err != nil || data == nil { return ErrInvalidVerifyCode } // 检查是否已达到最大尝试次数 if data.Attempts >= maxVerifyCodeAttempts { return ErrVerifyCodeMaxAttempts } // 验证码不匹配 if data.Code != code { data.Attempts++ _ = s.cache.SetVerificationCode(ctx, email, data, verifyCodeTTL) if data.Attempts >= maxVerifyCodeAttempts { return ErrVerifyCodeMaxAttempts } return ErrInvalidVerifyCode } // 验证成功,删除验证码 _ = s.cache.DeleteVerificationCode(ctx, email) return nil } // buildVerifyCodeEmailBody 构建验证码邮件HTML内容 func (s *EmailService) buildVerifyCodeEmailBody(code, siteName string) string { return fmt.Sprintf(`
Your verification code is:
This code will expire in 15 minutes.
If you did not request this code, please ignore this email.