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 | |||
|  | } |