v1.0.0
This commit is contained in:
712
internal/infrastructure/external/email/qq_email_service.go
vendored
Normal file
712
internal/infrastructure/external/email/qq_email_service.go
vendored
Normal file
@@ -0,0 +1,712 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user