Files
backend/pkg/email/email.go

163 lines
4.8 KiB
Go
Raw Permalink Normal View History

package email
import (
"crypto/tls"
"fmt"
"net/smtp"
"net/textproto"
"carrotskin/pkg/config"
"github.com/jordan-wright/email"
"go.uber.org/zap"
)
// 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)
return s.send([]string{to}, subject, body)
}
// 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 发送邮件
func (s *Service) send(to []string, subject, body string) error {
e := email.NewEmail()
e.From = fmt.Sprintf("%s <%s>", s.cfg.FromName, s.cfg.Username)
e.To = to
e.Subject = subject
e.HTML = []byte(body)
e.Headers = textproto.MIMEHeader{}
// SMTP认证
auth := smtp.PlainAuth("", s.cfg.Username, s.cfg.Password, s.cfg.SMTPHost)
// 发送邮件
addr := fmt.Sprintf("%s:%d", s.cfg.SMTPHost, s.cfg.SMTPPort)
// 判断端口决定发送方式
// 465端口使用SSL/TLS隐式TLS
// 587端口使用STARTTLS显式TLS
var err error
if s.cfg.SMTPPort == 465 {
// 使用SSL/TLS连接适用于465端口
tlsConfig := &tls.Config{
ServerName: s.cfg.SMTPHost,
InsecureSkipVerify: false, // 生产环境建议设置为false
}
err = e.SendWithTLS(addr, auth, tlsConfig)
} else {
// 使用STARTTLS连接适用于587端口等
err = e.Send(addr, auth)
}
if err != nil {
s.logger.Error("发送邮件失败",
zap.Strings("to", to),
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("邮件发送成功",
zap.Strings("to", to),
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)
}