2025-11-28 23:30:49 +08:00
|
|
|
|
package email
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"crypto/tls"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
|
|
|
|
"carrotskin/pkg/config"
|
2026-02-24 02:18:15 +08:00
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
|
"go.uber.org/zap"
|
2026-02-24 02:18:15 +08:00
|
|
|
|
gomail "gopkg.in/mail.v2"
|
2025-11-28 23:30:49 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Service 邮件服务
|
|
|
|
|
|
type Service struct {
|
|
|
|
|
|
cfg config.EmailConfig
|
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewService 创建邮件服务
|
|
|
|
|
|
func NewService(cfg config.EmailConfig, logger *zap.Logger) *Service {
|
|
|
|
|
|
return &Service{
|
|
|
|
|
|
cfg: cfg,
|
|
|
|
|
|
logger: logger,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SendVerificationCode 发送验证码邮件
|
|
|
|
|
|
func (s *Service) SendVerificationCode(to, code, purpose string) error {
|
|
|
|
|
|
if !s.cfg.Enabled {
|
|
|
|
|
|
s.logger.Warn("邮件服务未启用,跳过发送", zap.String("to", to))
|
|
|
|
|
|
return fmt.Errorf("邮件服务未启用")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
subject := s.getSubject(purpose)
|
|
|
|
|
|
body := s.getBody(code, purpose)
|
|
|
|
|
|
|
2026-02-24 02:18:15 +08:00
|
|
|
|
return s.send(to, subject, body)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SendResetPassword 发送重置密码邮件
|
|
|
|
|
|
func (s *Service) SendResetPassword(to, code string) error {
|
|
|
|
|
|
return s.SendVerificationCode(to, code, "reset_password")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SendEmailVerification 发送邮箱验证邮件
|
|
|
|
|
|
func (s *Service) SendEmailVerification(to, code string) error {
|
|
|
|
|
|
return s.SendVerificationCode(to, code, "email_verification")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SendChangeEmail 发送更换邮箱验证码
|
|
|
|
|
|
func (s *Service) SendChangeEmail(to, code string) error {
|
|
|
|
|
|
return s.SendVerificationCode(to, code, "change_email")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// send 发送邮件
|
2026-02-24 02:18:15 +08:00
|
|
|
|
func (s *Service) send(to, subject, body string) error {
|
|
|
|
|
|
m := gomail.NewMessage()
|
|
|
|
|
|
m.SetHeader("From", fmt.Sprintf("%s <%s>", s.cfg.FromName, s.cfg.Username))
|
|
|
|
|
|
m.SetHeader("To", to)
|
|
|
|
|
|
m.SetHeader("Subject", subject)
|
|
|
|
|
|
m.SetBody("text/html", body)
|
|
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
|
var err error
|
|
|
|
|
|
if s.cfg.SMTPPort == 465 {
|
|
|
|
|
|
// 使用SSL/TLS连接(适用于465端口)
|
|
|
|
|
|
tlsConfig := &tls.Config{
|
|
|
|
|
|
ServerName: s.cfg.SMTPHost,
|
|
|
|
|
|
InsecureSkipVerify: false, // 生产环境建议设置为false
|
|
|
|
|
|
}
|
2026-02-24 02:18:15 +08:00
|
|
|
|
d := &gomail.Dialer{
|
|
|
|
|
|
Host: s.cfg.SMTPHost,
|
|
|
|
|
|
Port: s.cfg.SMTPPort,
|
|
|
|
|
|
Username: s.cfg.Username,
|
|
|
|
|
|
Password: s.cfg.Password,
|
|
|
|
|
|
TLSConfig: tlsConfig,
|
|
|
|
|
|
}
|
|
|
|
|
|
err = d.DialAndSend(m)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 使用STARTTLS连接(适用于587端口等)
|
2026-02-24 02:18:15 +08:00
|
|
|
|
d := &gomail.Dialer{
|
|
|
|
|
|
Host: s.cfg.SMTPHost,
|
|
|
|
|
|
Port: s.cfg.SMTPPort,
|
|
|
|
|
|
Username: s.cfg.Username,
|
|
|
|
|
|
Password: s.cfg.Password,
|
|
|
|
|
|
}
|
|
|
|
|
|
err = d.DialAndSend(m)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
2026-02-24 02:18:15 +08:00
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
s.logger.Error("发送邮件失败",
|
2026-02-24 02:18:15 +08:00
|
|
|
|
zap.String("to", to),
|
2025-11-28 23:30:49 +08:00
|
|
|
|
zap.String("subject", subject),
|
|
|
|
|
|
zap.String("smtp_host", s.cfg.SMTPHost),
|
|
|
|
|
|
zap.Int("smtp_port", s.cfg.SMTPPort),
|
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
)
|
|
|
|
|
|
return fmt.Errorf("发送邮件失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
s.logger.Info("邮件发送成功",
|
2026-02-24 02:18:15 +08:00
|
|
|
|
zap.String("to", to),
|
2025-11-28 23:30:49 +08:00
|
|
|
|
zap.String("subject", subject),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getSubject 获取邮件主题
|
|
|
|
|
|
func (s *Service) getSubject(purpose string) string {
|
|
|
|
|
|
switch purpose {
|
|
|
|
|
|
case "email_verification":
|
|
|
|
|
|
return "【CarrotSkin】邮箱验证"
|
|
|
|
|
|
case "reset_password":
|
|
|
|
|
|
return "【CarrotSkin】重置密码"
|
|
|
|
|
|
case "change_email":
|
|
|
|
|
|
return "【CarrotSkin】更换邮箱验证"
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "【CarrotSkin】验证码"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// getBody 获取邮件正文
|
|
|
|
|
|
func (s *Service) getBody(code, purpose string) string {
|
|
|
|
|
|
var message string
|
|
|
|
|
|
switch purpose {
|
|
|
|
|
|
case "email_verification":
|
|
|
|
|
|
message = "感谢注册CarrotSkin!请使用以下验证码完成邮箱验证:"
|
|
|
|
|
|
case "reset_password":
|
|
|
|
|
|
message = "您正在重置密码,请使用以下验证码:"
|
|
|
|
|
|
case "change_email":
|
|
|
|
|
|
message = "您正在更换邮箱,请使用以下验证码验证新邮箱:"
|
|
|
|
|
|
default:
|
|
|
|
|
|
message = "您的验证码为:"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return fmt.Sprintf(`
|
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html>
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<title>验证码</title>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f4f4f4;">
|
|
|
|
|
|
<div style="max-width: 600px; margin: 20px auto; background-color: #ffffff; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
|
|
|
|
|
<div style="text-align: center; padding-bottom: 20px;">
|
|
|
|
|
|
<h1 style="color: #ff6b35; margin: 0;">CarrotSkin</h1>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="padding: 20px 0; border-top: 2px solid #ff6b35; border-bottom: 2px solid #ff6b35;">
|
|
|
|
|
|
<p style="font-size: 16px; color: #333; margin: 0 0 20px 0;">%s</p>
|
|
|
|
|
|
<div style="background-color: #f9f9f9; padding: 20px; text-align: center; border-radius: 4px; margin: 20px 0;">
|
|
|
|
|
|
<span style="font-size: 32px; font-weight: bold; color: #ff6b35; letter-spacing: 5px;">%s</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p style="font-size: 14px; color: #666; margin: 20px 0 0 0;">验证码有效期为10分钟,请及时使用。</p>
|
|
|
|
|
|
<p style="font-size: 14px; color: #666; margin: 10px 0 0 0;">如果这不是您的操作,请忽略此邮件。</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="text-align: center; padding-top: 20px;">
|
|
|
|
|
|
<p style="font-size: 12px; color: #999; margin: 0;">© 2025 CarrotSkin. All rights reserved.</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|
|
|
|
|
|
`, message, code)
|
|
|
|
|
|
}
|