package logic import ( "bytes" "fmt" "fusenapi/utils/check" "fusenapi/utils/fstpl" "html/template" "log" "net/smtp" "sync" "time" "github.com/zeromicro/go-zero/core/logx" ) var EmailTaskResendTime = time.Second * 30 var TimeLimit *check.TimeLimit[string] var EmailManager *EmailSender var tpls *template.Template func init() { var err error tpls = fstpl.AutoParseTplFiles() if err != nil { log.Fatal(err) } TimeLimit = check.NewTimeLimit[string](EmailTaskResendTime) // Initialize the email manager EmailManager = &EmailSender{ EmailTasks: make(chan *EmailFormat, 10), Auth: smtp.PlainAuth( "fusen support", "support@fusenpack.com", "wfbjpdgvaozjvwah", "smtp.gmail.com", ), FromEmail: "support@fusenpack.com", emailSending: make(map[string]*EmailTask, 10), ResendTimeLimit: EmailTaskResendTime, semaphore: make(chan struct{}, 100), // Initialize semaphore with a capacity of 10 } // Start processing email tasks go EmailManager.ProcessEmailTasks() // Start clearing expired tasks go EmailManager.ClearExpiredTasks() } type EmailFormat struct { TemplateName string // 模板名字 UniqueKey string // 用于处理唯一的任务,重发都会被利用到 TargetEmail string // 发送的目标email CompanyName string // fs公司名 ConfirmationLink string // fs确认连接 SenderName string // 发送人 SenderTitle string // 发送标题 Extend map[string]string // 扩展参数 } // 验证邮件格式是否符合要求 func (eformat *EmailFormat) Vaild() error { // 1. 检查模板名称 if tpls.Lookup(eformat.TemplateName) == nil { return fmt.Errorf("%s template name is not found", eformat.TemplateName) } // 2. 检查公司名称 if eformat.CompanyName == "" { return fmt.Errorf("company name cannot be empty") } // 3. 检查确认链接 if eformat.ConfirmationLink == "" { return fmt.Errorf("confirmation link cannot be empty") } // 4. 检查发送人名称 if eformat.SenderName == "" { return fmt.Errorf("sender name cannot be empty") } // 5. 检查发送人头衔 if eformat.SenderTitle == "" { return fmt.Errorf("sender title cannot be empty") } // 6. 检查目标邮箱 if eformat.TargetEmail == "" { return fmt.Errorf("target email cannot be empty") } // 7. 检查唯一键 if eformat.UniqueKey == "" { return fmt.Errorf("unique key cannot be empty") } // 所有校验通过 return nil } // EmailSender type EmailSender struct { lock sync.Mutex EmailTasks chan *EmailFormat Auth smtp.Auth FromEmail string ResendTimeLimit time.Duration emailSending map[string]*EmailTask semaphore chan struct{} } // EmailTask type EmailTask struct { Email *EmailFormat // email SendTime time.Time // 处理的任务时间 } // ProcessEmailTasks 是 EmailSender 结构体的方法,用于处理邮件任务。 func (m *EmailSender) ProcessEmailTasks() { for { // 从 EmailTasks 通道中接收邮件任务 emailformat, ok := <-m.EmailTasks if !ok { log.Println("Email task channel closed") break } // 验证邮件格式是否合法 err := emailformat.Vaild() if err != nil { logx.Error(err) continue } // 加锁,避免并发修改 emailSending 字典 m.lock.Lock() // 检查 emailSending 字典中是否已存在相同的任务 _, isSending := m.emailSending[emailformat.UniqueKey] if isSending { m.lock.Unlock() continue } // 将邮件任务添加到 emailSending 字典中 m.emailSending[emailformat.UniqueKey] = &EmailTask{ Email: emailformat, SendTime: time.Now().UTC(), } m.lock.Unlock() // 获取一个信号量,限制同时发送的邮件任务数量 m.semaphore <- struct{}{} go func() { defer func() { <-m.semaphore }() // 释放一个信号量 // 渲染邮件模板内容 content := RenderEmailTemplate( emailformat.TemplateName, emailformat.CompanyName, emailformat.ConfirmationLink, emailformat.SenderName, emailformat.SenderTitle, emailformat.Extend, ) // 发送邮件 err := smtp.SendMail("smtp.gmail.com:587", m.Auth, m.FromEmail, []string{emailformat.TargetEmail}, content) if err != nil { log.Printf("Failed to send email to %s: %v\n", emailformat, err) // 重新发送邮件 m.Resend(emailformat.UniqueKey, content) } }() } } // Resend 是 EmailSender 结构体的方法,用于重发邮件。 func (m *EmailSender) Resend(uniqueKey string, content []byte) { // 等待一段时间后重发邮件,避免频繁重发 time.Sleep(m.ResendTimeLimit) m.lock.Lock() defer m.lock.Unlock() // 检查邮件任务是否仍存在并且未成功发送 if task, ok := m.emailSending[uniqueKey]; ok && task.SendTime.Add(m.ResendTimeLimit).After(time.Now().UTC()) { err := smtp.SendMail(task.Email.TargetEmail, m.Auth, m.FromEmail, []string{task.Email.TargetEmail}, content) if err != nil { log.Printf("Failed to resend email to %s: %v\n", task.Email.TargetEmail, err) } else { // 从 emailSending 字典中删除已成功发送的邮件任务 delete(m.emailSending, uniqueKey) } } } // ClearExpiredTasks 是 EmailSender 结构体的方法,用于清除过期的邮件任务。 func (m *EmailSender) ClearExpiredTasks() { // 每分钟触发一次清除操作 ticker := time.NewTicker(time.Minute) defer ticker.Stop() for { <-ticker.C m.lock.Lock() // 遍历 emailSending 字典,删除发送时间超过一定限制的过期任务 for email, task := range m.emailSending { if task.SendTime.Add(m.ResendTimeLimit).Before(time.Now().UTC()) { delete(m.emailSending, email) } } m.lock.Unlock() } } // RenderEmailTemplate 是一个渲染邮件模板的函数,根据给定的参数生成邮件内容。 // 参数: // - templateName: 邮件模板的名称 // - companyName: 公司名称 // - confirmationLink: 确认链接 // - senderName: 发件人姓名 // - senderTitle: 发件人职务 // - extend: 扩展字段,包含其他自定义键值对的映射 // 返回值: // - []byte: 渲染后的邮件内容(以字节切片形式返回) func RenderEmailTemplate(templateName, companyName, confirmationLink, senderName, senderTitle string, extend map[string]string) []byte { // 创建一个包含邮件模板所需数据的映射 data := map[string]string{ "CompanyName": companyName, "ConfirmationLink": confirmationLink, "SenderName": senderName, "SenderTitle": senderTitle, } // 将扩展字段中的键值对添加到数据映射中 for k, v := range extend { data[k] = v } // 创建一个字节缓冲区,用于存储渲染后的邮件内容 var result bytes.Buffer // 使用给定的数据映射执行邮件模板渲染 err := tpls.ExecuteTemplate(&result, templateName, data) if err != nil { log.Fatal(err) } // 将渲染后的邮件内容转换为字节切片并返回 return result.Bytes() }