713 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			713 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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 := `
 | ||
| <!DOCTYPE html>
 | ||
| <html>
 | ||
| <head>
 | ||
|     <meta charset="UTF-8">
 | ||
|     <title>发票开具成功通知</title>
 | ||
|     <style>
 | ||
|         @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
 | ||
|         
 | ||
|         * {
 | ||
|             margin: 0;
 | ||
|             padding: 0;
 | ||
|             box-sizing: border-box;
 | ||
|         }
 | ||
|         
 | ||
|         body {
 | ||
|             font-family: 'Inter', 'Microsoft YaHei', Arial, sans-serif;
 | ||
|             line-height: 1.6;
 | ||
|             color: #2d3748;
 | ||
|             background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
 | ||
|             min-height: 100vh;
 | ||
|             padding: 20px;
 | ||
|         }
 | ||
|         
 | ||
|         .container {
 | ||
|             max-width: 650px;
 | ||
|             margin: 0 auto;
 | ||
|             background: #ffffff;
 | ||
|             border-radius: 24px;
 | ||
|             box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.08);
 | ||
|             overflow: hidden;
 | ||
|             border: 1px solid #e2e8f0;
 | ||
|         }
 | ||
|         
 | ||
|         .header {
 | ||
|             background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | ||
|             color: white;
 | ||
|             padding: 50px 40px 40px;
 | ||
|             text-align: center;
 | ||
|             position: relative;
 | ||
|             overflow: hidden;
 | ||
|         }
 | ||
|         
 | ||
|         .header::before {
 | ||
|             content: '';
 | ||
|             position: absolute;
 | ||
|             top: -50%;
 | ||
|             left: -50%;
 | ||
|             width: 200%;
 | ||
|             height: 200%;
 | ||
|             background: radial-gradient(circle, rgba(255,255,255,0.08) 0%, transparent 70%);
 | ||
|             animation: float 6s ease-in-out infinite;
 | ||
|         }
 | ||
|         
 | ||
|         @keyframes float {
 | ||
|             0%, 100% { transform: translateY(0px) rotate(0deg); }
 | ||
|             50% { transform: translateY(-20px) rotate(180deg); }
 | ||
|         }
 | ||
|         
 | ||
|         .success-icon {
 | ||
|             font-size: 48px;
 | ||
|             margin-bottom: 16px;
 | ||
|             position: relative;
 | ||
|             z-index: 1;
 | ||
|             opacity: 0.9;
 | ||
|         }
 | ||
|         
 | ||
|         .header h1 {
 | ||
|             font-size: 24px;
 | ||
|             font-weight: 500;
 | ||
|             margin: 0;
 | ||
|             position: relative;
 | ||
|             z-index: 1;
 | ||
|             letter-spacing: 0.5px;
 | ||
|         }
 | ||
|         
 | ||
|         .content {
 | ||
|             padding: 0;
 | ||
|         }
 | ||
|         
 | ||
|         .greeting {
 | ||
|             padding: 40px 40px 20px;
 | ||
|             text-align: center;
 | ||
|             background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
 | ||
|         }
 | ||
|         
 | ||
|         .greeting p {
 | ||
|             font-size: 16px;
 | ||
|             color: #4a5568;
 | ||
|             margin-bottom: 8px;
 | ||
|             font-weight: 400;
 | ||
|         }
 | ||
|         
 | ||
|         .access-section {
 | ||
|             background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | ||
|             padding: 40px;
 | ||
|             text-align: center;
 | ||
|             position: relative;
 | ||
|             overflow: hidden;
 | ||
|             margin: 0 20px 30px;
 | ||
|             border-radius: 20px;
 | ||
|         }
 | ||
|         
 | ||
|         .access-section::before {
 | ||
|             content: '';
 | ||
|             position: absolute;
 | ||
|             top: -50%;
 | ||
|             left: -50%;
 | ||
|             width: 200%;
 | ||
|             height: 200%;
 | ||
|             background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
 | ||
|             animation: shimmer 8s ease-in-out infinite;
 | ||
|         }
 | ||
|         
 | ||
|         @keyframes shimmer {
 | ||
|             0%, 100% { transform: translateX(-100%) translateY(-100%) rotate(0deg); }
 | ||
|             50% { transform: translateX(100%) translateY(100%) rotate(180deg); }
 | ||
|         }
 | ||
|         
 | ||
|         .access-section h3 {
 | ||
|             color: white;
 | ||
|             font-size: 22px;
 | ||
|             font-weight: 600;
 | ||
|             margin-bottom: 12px;
 | ||
|             position: relative;
 | ||
|             z-index: 1;
 | ||
|         }
 | ||
|         
 | ||
|         .access-section p {
 | ||
|             color: rgba(255, 255, 255, 0.9);
 | ||
|             margin-bottom: 25px;
 | ||
|             position: relative;
 | ||
|             z-index: 1;
 | ||
|             font-size: 15px;
 | ||
|         }
 | ||
|         
 | ||
|         .access-btn {
 | ||
|             display: inline-block;
 | ||
|             background: rgba(255, 255, 255, 0.15);
 | ||
|             color: white;
 | ||
|             padding: 16px 32px;
 | ||
|             text-decoration: none;
 | ||
|             border-radius: 50px;
 | ||
|             font-weight: 600;
 | ||
|             font-size: 15px;
 | ||
|             border: 2px solid rgba(255, 255, 255, 0.2);
 | ||
|             transition: all 0.3s ease;
 | ||
|             position: relative;
 | ||
|             z-index: 1;
 | ||
|             backdrop-filter: blur(10px);
 | ||
|             letter-spacing: 0.3px;
 | ||
|         }
 | ||
|         
 | ||
|         .access-btn:hover {
 | ||
|             background: rgba(255, 255, 255, 0.25);
 | ||
|             transform: translateY(-3px);
 | ||
|             box-shadow: 0 15px 35px rgba(0, 0, 0, 0.15);
 | ||
|             border-color: rgba(255, 255, 255, 0.3);
 | ||
|         }
 | ||
|         
 | ||
|         .info-section {
 | ||
|             padding: 0 40px 40px;
 | ||
|         }
 | ||
|         
 | ||
|         .info-grid {
 | ||
|             display: grid;
 | ||
|             grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
 | ||
|             gap: 20px;
 | ||
|             margin: 30px 0;
 | ||
|         }
 | ||
|         
 | ||
|         .info-item {
 | ||
|             background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
 | ||
|             padding: 24px;
 | ||
|             border-radius: 16px;
 | ||
|             border: 1px solid #e2e8f0;
 | ||
|             position: relative;
 | ||
|             overflow: hidden;
 | ||
|             transition: all 0.3s ease;
 | ||
|             box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
 | ||
|         }
 | ||
|         
 | ||
|         .info-item:hover {
 | ||
|             transform: translateY(-2px);
 | ||
|             box-shadow: 0 12px 25px -5px rgba(102, 126, 234, 0.1);
 | ||
|             border-color: #cbd5e0;
 | ||
|         }
 | ||
|         
 | ||
|         .info-item::before {
 | ||
|             content: '';
 | ||
|             position: absolute;
 | ||
|             top: 0;
 | ||
|             left: 0;
 | ||
|             width: 4px;
 | ||
|             height: 100%;
 | ||
|             background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | ||
|             border-radius: 0 2px 2px 0;
 | ||
|         }
 | ||
|         
 | ||
|         .info-label {
 | ||
|             font-weight: 600;
 | ||
|             color: #718096;
 | ||
|             display: block;
 | ||
|             margin-bottom: 8px;
 | ||
|             font-size: 13px;
 | ||
|             text-transform: uppercase;
 | ||
|             letter-spacing: 0.8px;
 | ||
|         }
 | ||
|         
 | ||
|         .info-value {
 | ||
|             color: #2d3748;
 | ||
|             font-size: 16px;
 | ||
|             font-weight: 500;
 | ||
|             position: relative;
 | ||
|             z-index: 1;
 | ||
|         }
 | ||
|         
 | ||
|         .notes-section {
 | ||
|             background: linear-gradient(135deg, #f0fff4 0%, #ffffff 100%);
 | ||
|             padding: 30px;
 | ||
|             border-radius: 16px;
 | ||
|             margin: 30px 0;
 | ||
|             border: 1px solid #c6f6d5;
 | ||
|             position: relative;
 | ||
|         }
 | ||
|         
 | ||
|         .notes-section::before {
 | ||
|             content: '';
 | ||
|             position: absolute;
 | ||
|             top: 0;
 | ||
|             left: 0;
 | ||
|             width: 4px;
 | ||
|             height: 100%;
 | ||
|             background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
 | ||
|             border-radius: 0 2px 2px 0;
 | ||
|         }
 | ||
|         
 | ||
|         .notes-section h4 {
 | ||
|             color: #2f855a;
 | ||
|             font-size: 16px;
 | ||
|             font-weight: 600;
 | ||
|             margin-bottom: 16px;
 | ||
|             display: flex;
 | ||
|             align-items: center;
 | ||
|         }
 | ||
|         
 | ||
|         .notes-section h4::before {
 | ||
|             content: '📋';
 | ||
|             margin-right: 8px;
 | ||
|             font-size: 18px;
 | ||
|         }
 | ||
|         
 | ||
|         .notes-section ul {
 | ||
|             list-style: none;
 | ||
|             padding: 0;
 | ||
|         }
 | ||
|         
 | ||
|         .notes-section li {
 | ||
|             color: #4a5568;
 | ||
|             margin-bottom: 10px;
 | ||
|             padding-left: 24px;
 | ||
|             position: relative;
 | ||
|             font-size: 14px;
 | ||
|         }
 | ||
|         
 | ||
|         .notes-section li::before {
 | ||
|             content: '✓';
 | ||
|             color: #48bb78;
 | ||
|             font-weight: bold;
 | ||
|             position: absolute;
 | ||
|             left: 0;
 | ||
|             font-size: 16px;
 | ||
|         }
 | ||
|         
 | ||
|         .footer {
 | ||
|             background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
 | ||
|             color: rgba(255, 255, 255, 0.8);
 | ||
|             padding: 35px 40px;
 | ||
|             text-align: center;
 | ||
|             font-size: 14px;
 | ||
|         }
 | ||
|         
 | ||
|         .footer p {
 | ||
|             margin-bottom: 8px;
 | ||
|             line-height: 1.5;
 | ||
|         }
 | ||
|         
 | ||
|         .footer-divider {
 | ||
|             width: 60px;
 | ||
|             height: 2px;
 | ||
|             background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 | ||
|             margin: 20px auto;
 | ||
|             border-radius: 1px;
 | ||
|         }
 | ||
|         
 | ||
|         @media (max-width: 600px) {
 | ||
|             .container {
 | ||
|                 margin: 10px;
 | ||
|                 border-radius: 20px;
 | ||
|             }
 | ||
|             
 | ||
|             .header {
 | ||
|                 padding: 40px 30px 30px;
 | ||
|             }
 | ||
|             
 | ||
|             .greeting {
 | ||
|                 padding: 30px 30px 20px;
 | ||
|             }
 | ||
|             
 | ||
|             .access-section {
 | ||
|                 margin: 0 15px 25px;
 | ||
|                 padding: 30px 25px;
 | ||
|             }
 | ||
|             
 | ||
|             .info-section {
 | ||
|                 padding: 0 30px 30px;
 | ||
|             }
 | ||
|             
 | ||
|             .info-grid {
 | ||
|                 grid-template-columns: 1fr;
 | ||
|                 gap: 16px;
 | ||
|             }
 | ||
|             
 | ||
|             .footer {
 | ||
|                 padding: 30px 30px;
 | ||
|             }
 | ||
|         }
 | ||
|     </style>
 | ||
| </head>
 | ||
| <body>
 | ||
|     <div class="container">
 | ||
|         <div class="header">
 | ||
|             <div class="success-icon">✓</div>
 | ||
|             <h1>发票已开具完成</h1>
 | ||
|         </div>
 | ||
|         
 | ||
|         <div class="content">
 | ||
|             <div class="greeting">
 | ||
|                 <p>尊敬的用户,您好!</p>
 | ||
|                 <p>您的发票申请已审核通过,发票已成功开具。</p>
 | ||
|             </div>
 | ||
|             
 | ||
|             <div class="access-section">
 | ||
|                 <h3>📄 发票访问链接</h3>
 | ||
|                 <p>您的发票已准备就绪,请点击下方按钮访问查看页面</p>
 | ||
|                 <a href="{{.FileURL}}" class="access-btn" target="_blank">
 | ||
|                     🔗 访问发票页面
 | ||
|                 </a>
 | ||
|             </div>
 | ||
|             
 | ||
|             <div class="info-section">
 | ||
|                 <div class="info-grid">
 | ||
|                     <div class="info-item">
 | ||
|                         <span class="info-label">公司名称</span>
 | ||
|                         <span class="info-value">{{.CompanyName}}</span>
 | ||
|                     </div>
 | ||
|                     
 | ||
|                     <div class="info-item">
 | ||
|                         <span class="info-label">发票金额</span>
 | ||
|                         <span class="info-value">¥{{.Amount}}</span>
 | ||
|                     </div>
 | ||
|                     
 | ||
|                     <div class="info-item">
 | ||
|                         <span class="info-label">发票类型</span>
 | ||
|                         <span class="info-value">{{.InvoiceType}}</span>
 | ||
|                     </div>
 | ||
|                     
 | ||
|                     <div class="info-item">
 | ||
|                         <span class="info-label">开具时间</span>
 | ||
|                         <span class="info-value">{{.ApprovedAt}}</span>
 | ||
|                     </div>
 | ||
|                 </div>
 | ||
|                 
 | ||
|                 <div class="notes-section">
 | ||
|                     <h4>注意事项</h4>
 | ||
|                     <ul>
 | ||
|                         <li>访问页面后可在页面内下载发票文件</li>
 | ||
|                         <li>请妥善保管发票文件,建议打印存档</li>
 | ||
|                         <li>如有疑问,请回到我们平台进行下载</li>
 | ||
|                     </ul>
 | ||
|                 </div>
 | ||
|             </div>
 | ||
|         </div>
 | ||
|         
 | ||
|         <div class="footer">
 | ||
|             <p>此邮件由系统自动发送,请勿回复</p>
 | ||
|             <div class="footer-divider"></div>
 | ||
|             <p>天远数据 API 服务平台</p>
 | ||
|             <p>发送时间:{{.CurrentTime}}</p>
 | ||
|         </div>
 | ||
|     </div>
 | ||
| </body>
 | ||
| </html>`
 | ||
| 
 | ||
| 	// 解析模板
 | ||
| 	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
 | ||
| }
 |