163 lines
4.8 KiB
Go
163 lines
4.8 KiB
Go
|
|
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)
|
|||
|
|
}
|