package email
import (
"context"
"crypto/tls"
"fmt"
"html/template"
"net"
"net/smtp"
"strings"
"time"
"go.uber.org/zap"
"tyapi-server/internal/config"
)
// QQEmailService QQ邮箱服务
type QQEmailService struct {
config config.EmailConfig
logger *zap.Logger
}
// EmailData 邮件数据
type EmailData struct {
To string `json:"to"`
Subject string `json:"subject"`
Content string `json:"content"`
Data map[string]interface{} `json:"data"`
}
// InvoiceEmailData 发票邮件数据
type InvoiceEmailData struct {
CompanyName string `json:"company_name"`
Amount string `json:"amount"`
InvoiceType string `json:"invoice_type"`
FileURL string `json:"file_url"`
FileName string `json:"file_name"`
ReceivingEmail string `json:"receiving_email"`
ApprovedAt string `json:"approved_at"`
}
// NewQQEmailService 创建QQ邮箱服务
func NewQQEmailService(config config.EmailConfig, logger *zap.Logger) *QQEmailService {
return &QQEmailService{
config: config,
logger: logger,
}
}
// SendEmail 发送邮件
func (s *QQEmailService) SendEmail(ctx context.Context, data *EmailData) error {
s.logger.Info("开始发送邮件",
zap.String("to", data.To),
zap.String("subject", data.Subject),
)
// 构建邮件内容
message := s.buildEmailMessage(data)
// 发送邮件
err := s.sendSMTP(data.To, data.Subject, message)
if err != nil {
s.logger.Error("发送邮件失败",
zap.String("to", data.To),
zap.String("subject", data.Subject),
zap.Error(err),
)
return fmt.Errorf("发送邮件失败: %w", err)
}
s.logger.Info("邮件发送成功",
zap.String("to", data.To),
zap.String("subject", data.Subject),
)
return nil
}
// SendInvoiceEmail 发送发票邮件
func (s *QQEmailService) SendInvoiceEmail(ctx context.Context, data *InvoiceEmailData) error {
s.logger.Info("开始发送发票邮件",
zap.String("to", data.ReceivingEmail),
zap.String("company_name", data.CompanyName),
zap.String("amount", data.Amount),
)
// 构建邮件内容
subject := "您的发票已开具成功"
content := s.buildInvoiceEmailContent(data)
emailData := &EmailData{
To: data.ReceivingEmail,
Subject: subject,
Content: content,
Data: map[string]interface{}{
"company_name": data.CompanyName,
"amount": data.Amount,
"invoice_type": data.InvoiceType,
"file_url": data.FileURL,
"file_name": data.FileName,
"approved_at": data.ApprovedAt,
},
}
return s.SendEmail(ctx, emailData)
}
// buildEmailMessage 构建邮件消息
func (s *QQEmailService) buildEmailMessage(data *EmailData) string {
headers := make(map[string]string)
headers["From"] = s.config.FromEmail
headers["To"] = data.To
headers["Subject"] = data.Subject
headers["MIME-Version"] = "1.0"
headers["Content-Type"] = "text/html; charset=UTF-8"
var message strings.Builder
for key, value := range headers {
message.WriteString(fmt.Sprintf("%s: %s\r\n", key, value))
}
message.WriteString("\r\n")
message.WriteString(data.Content)
return message.String()
}
// buildInvoiceEmailContent 构建发票邮件内容
func (s *QQEmailService) buildInvoiceEmailContent(data *InvoiceEmailData) string {
htmlTemplate := `
发票开具成功通知
尊敬的用户,您好!
您的发票申请已审核通过,发票已成功开具。
📄 发票访问链接
您的发票已准备就绪,请点击下方按钮访问查看页面
🔗 访问发票页面
公司名称
{{.CompanyName}}
发票金额
¥{{.Amount}}
发票类型
{{.InvoiceType}}
开具时间
{{.ApprovedAt}}
注意事项
- 访问页面后可在页面内下载发票文件
- 请妥善保管发票文件,建议打印存档
- 如有疑问,请回到我们平台进行下载
`
// 解析模板
tmpl, err := template.New("invoice_email").Parse(htmlTemplate)
if err != nil {
s.logger.Error("解析邮件模板失败", zap.Error(err))
return s.buildSimpleInvoiceEmail(data)
}
// 准备模板数据
templateData := struct {
CompanyName string
Amount string
InvoiceType string
FileURL string
FileName string
ApprovedAt string
CurrentTime string
Domain string
}{
CompanyName: data.CompanyName,
Amount: data.Amount,
InvoiceType: data.InvoiceType,
FileURL: data.FileURL,
FileName: data.FileName,
ApprovedAt: data.ApprovedAt,
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
Domain: s.config.Domain,
}
// 执行模板
var content strings.Builder
err = tmpl.Execute(&content, templateData)
if err != nil {
s.logger.Error("执行邮件模板失败", zap.Error(err))
return s.buildSimpleInvoiceEmail(data)
}
return content.String()
}
// buildSimpleInvoiceEmail 构建简单的发票邮件内容(备用方案)
func (s *QQEmailService) buildSimpleInvoiceEmail(data *InvoiceEmailData) string {
return fmt.Sprintf(`
发票开具成功通知
尊敬的用户,您好!
您的发票申请已审核通过,发票已成功开具。
发票信息:
- 公司名称:%s
- 发票金额:¥%s
- 发票类型:%s
- 开具时间:%s
发票文件下载链接:%s
文件名:%s
如有疑问,请访问控制台查看详细信息:https://%s
天远数据 API 服务平台
%s
`, data.CompanyName, data.Amount, data.InvoiceType, data.ApprovedAt, data.FileURL, data.FileName, s.config.Domain, time.Now().Format("2006-01-02 15:04:05"))
}
// sendSMTP 通过SMTP发送邮件
func (s *QQEmailService) sendSMTP(to, subject, message string) error {
// 构建认证信息
auth := smtp.PlainAuth("", s.config.Username, s.config.Password, s.config.Host)
// 构建收件人列表
toList := []string{to}
// 发送邮件
if s.config.UseSSL {
// QQ邮箱587端口使用STARTTLS,465端口使用直接SSL
if s.config.Port == 587 {
// 使用STARTTLS (587端口)
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", s.config.Host, s.config.Port))
if err != nil {
return fmt.Errorf("连接SMTP服务器失败: %w", err)
}
defer conn.Close()
client, err := smtp.NewClient(conn, s.config.Host)
if err != nil {
return fmt.Errorf("创建SMTP客户端失败: %w", err)
}
defer client.Close()
// 启用STARTTLS
if err = client.StartTLS(&tls.Config{
ServerName: s.config.Host,
InsecureSkipVerify: false,
}); err != nil {
return fmt.Errorf("启用STARTTLS失败: %w", err)
}
// 认证
if err = client.Auth(auth); err != nil {
return fmt.Errorf("SMTP认证失败: %w", err)
}
// 设置发件人
if err = client.Mail(s.config.FromEmail); err != nil {
return fmt.Errorf("设置发件人失败: %w", err)
}
// 设置收件人
for _, recipient := range toList {
if err = client.Rcpt(recipient); err != nil {
return fmt.Errorf("设置收件人失败: %w", err)
}
}
// 发送邮件内容
writer, err := client.Data()
if err != nil {
return fmt.Errorf("准备发送邮件内容失败: %w", err)
}
defer writer.Close()
_, err = writer.Write([]byte(message))
if err != nil {
return fmt.Errorf("发送邮件内容失败: %w", err)
}
} else {
// 使用直接SSL连接 (465端口)
tlsConfig := &tls.Config{
ServerName: s.config.Host,
InsecureSkipVerify: false,
}
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", s.config.Host, s.config.Port), tlsConfig)
if err != nil {
return fmt.Errorf("连接SMTP服务器失败: %w", err)
}
defer conn.Close()
client, err := smtp.NewClient(conn, s.config.Host)
if err != nil {
return fmt.Errorf("创建SMTP客户端失败: %w", err)
}
defer client.Close()
// 认证
if err = client.Auth(auth); err != nil {
return fmt.Errorf("SMTP认证失败: %w", err)
}
// 设置发件人
if err = client.Mail(s.config.FromEmail); err != nil {
return fmt.Errorf("设置发件人失败: %w", err)
}
// 设置收件人
for _, recipient := range toList {
if err = client.Rcpt(recipient); err != nil {
return fmt.Errorf("设置收件人失败: %w", err)
}
}
// 发送邮件内容
writer, err := client.Data()
if err != nil {
return fmt.Errorf("准备发送邮件内容失败: %w", err)
}
defer writer.Close()
_, err = writer.Write([]byte(message))
if err != nil {
return fmt.Errorf("发送邮件内容失败: %w", err)
}
}
} else {
// 使用普通连接
err := smtp.SendMail(
fmt.Sprintf("%s:%d", s.config.Host, s.config.Port),
auth,
s.config.FromEmail,
toList,
[]byte(message),
)
if err != nil {
return fmt.Errorf("发送邮件失败: %w", err)
}
}
return nil
}