package warningnotify import ( "context" "encoding/json" "html" "mime" "net" "net/mail" "net/smtp" "strings" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/utility" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" ) func sendWarningEmail(ctx context.Context, batch Batch) error { recipients := normalizeEmailRecipients(batch.Recipients) if len(recipients) == 0 { g.Log().Warningf( ctx, "WarningNotify email skipped, no valid recipients, business:%s, tenantId:%s, deptId:%d, category:%s, stage:%d, count:%d", batch.Business, batch.TenantID, batch.DeptID, batch.Category, batch.WarnStage, len(batch.Items), ) return nil } cfg, ok := loadWarningEmailConfig(ctx) if !ok || strings.TrimSpace(cfg.Host) == "" && strings.TrimSpace(cfg.Url) == "" { g.Log().Infof( ctx, "WarningNotify email payload prepared, business:%s, tenantId:%s, deptId:%d, category:%s, stage:%d, recipients:%s, count:%d, subject:%s, body:%s", batch.Business, batch.TenantID, batch.DeptID, batch.Category, batch.WarnStage, strings.Join(recipients, ","), len(batch.Items), batch.EmailSubject, batch.EmailBody, ) return nil } if strings.TrimSpace(cfg.Url) != "" { if err := callWarningEmailHTTP(ctx, cfg.UserCodeHttpSenderConfig, batch, recipients); err != nil { return gerror.Wrap(err, "send warning email failed") } g.Log().Infof( ctx, "WarningNotify email sent by http, business:%s, tenantId:%s, deptId:%d, category:%s, stage:%d, recipients:%s, count:%d, subject:%s", batch.Business, batch.TenantID, batch.DeptID, batch.Category, batch.WarnStage, strings.Join(recipients, ","), len(batch.Items), batch.EmailSubject, ) return nil } if cfg.Port <= 0 { cfg.Port = 25 } if strings.TrimSpace(cfg.From) == "" { cfg.From = cfg.Username } if strings.TrimSpace(cfg.From) == "" { return gerror.New("warning email sender is empty") } addr := net.JoinHostPort(cfg.Host, gconv.String(cfg.Port)) auth := smtp.Auth(nil) if strings.TrimSpace(cfg.Username) != "" { auth = smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host) } msg := buildWarningEmailMessage(cfg, batch, recipients) err := utility.SendSMTP(ctx, utility.SMTPOptions{ Address: addr, Host: cfg.Host, Auth: auth, From: cfg.From, To: recipients, Data: msg, SSL: cfg.SSL, Timeout: utility.ExternalRequestTimeout(cfg.Timeout), }) if err != nil { return gerror.Wrap(err, "send warning email failed") } g.Log().Infof( ctx, "WarningNotify email sent, business:%s, tenantId:%s, deptId:%d, category:%s, stage:%d, recipients:%s, count:%d, subject:%s", batch.Business, batch.TenantID, batch.DeptID, batch.Category, batch.WarnStage, strings.Join(recipients, ","), len(batch.Items), batch.EmailSubject, ) return nil } func loadWarningEmailConfig(ctx context.Context) (model.UserCodeEmailSenderConfig, bool) { var cfg model.UserCodeEmailSenderConfig for _, key := range []string{"warning.email", "register.email"} { v, err := g.Cfg().Get(ctx, key) if err != nil || v.IsNil() { continue } _ = v.Scan(&cfg) if strings.TrimSpace(cfg.Host) != "" || strings.TrimSpace(cfg.Url) != "" { return cfg, true } } return cfg, false } func normalizeEmailRecipients(values []string) []string { seen := make(map[string]struct{}, len(values)) recipients := make([]string, 0, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value == "" { continue } address, err := mail.ParseAddress(value) if err != nil || address == nil || strings.TrimSpace(address.Address) == "" { continue } email := strings.ToLower(strings.TrimSpace(address.Address)) if _, ok := seen[email]; ok { continue } seen[email] = struct{}{} recipients = append(recipients, email) } return recipients } func buildWarningEmailMessage(cfg model.UserCodeEmailSenderConfig, batch Batch, recipients []string) []byte { subject := strings.TrimSpace(batch.EmailSubject) if subject == "" { subject = buildEmailSubject(batch) } from := strings.TrimSpace(cfg.From) if cfg.FromName != "" { from = mime.QEncoding.Encode("UTF-8", cfg.FromName) + " <" + from + ">" } message := strings.Builder{} message.WriteString("From: " + from + "\r\n") message.WriteString("To: " + strings.Join(recipients, ", ") + "\r\n") message.WriteString("Subject: " + mime.QEncoding.Encode("UTF-8", subject) + "\r\n") message.WriteString("MIME-Version: 1.0\r\n") message.WriteString("Content-Type: text/html; charset=UTF-8\r\n") message.WriteString("\r\n") message.WriteString(warningEmailHTMLBody(batch.EmailBody)) return []byte(message.String()) } func warningEmailHTMLBody(body string) string { body = strings.TrimSpace(body) if body == "" { body = defaultPlatformContent } if strings.Contains(body, warningEmailHTMLSignature) { return warningEmailHTMLDocument(body) } escaped := html.EscapeString(body) escaped = strings.ReplaceAll(escaped, "\r\n", "\n") escaped = strings.ReplaceAll(escaped, "\n", "
\n") return warningEmailHTMLDocument(`
` + escaped + `
`) } func warningEmailHTMLDocument(body string) string { return `` + body + `` } func callWarningEmailHTTP(ctx context.Context, cfg model.UserCodeHttpSenderConfig, batch Batch, recipients []string) error { method := strings.ToUpper(strings.TrimSpace(cfg.Method)) if method == "" { method = "POST" } successStatus := cfg.SuccessStatus if successStatus <= 0 { successStatus = 200 } body := renderWarningEmailTemplate(cfg.BodyTemplate, batch, recipients) if strings.TrimSpace(body) == "" { bodyBytes, _ := json.Marshal(g.Map{ "subject": strings.TrimSpace(batch.EmailSubject), "title": strings.TrimSpace(batch.EmailSubject), "content": strings.TrimSpace(batch.EmailBody), "body": strings.TrimSpace(batch.EmailBody), "recipients": recipients, "business": batch.Business, "category": batch.Category, "stage": batch.WarnStage, }) body = string(bodyBytes) } headers := cfg.Headers if headers == nil { headers = map[string]string{} } if _, ok := headers["Content-Type"]; !ok { headers["Content-Type"] = "application/json" } resp, err := g.Client().Timeout(utility.ExternalRequestTimeout(cfg.Timeout)).SetHeaderMap(headers).DoRequest(ctx, method, cfg.Url, body) if err != nil { return gerror.Wrap(err, "send warning email http request failed") } defer resp.Close() if resp.StatusCode != successStatus { return gerror.Newf("send warning email http request failed, status code: %d", resp.StatusCode) } return nil } func renderWarningEmailTemplate(template string, batch Batch, recipients []string) string { template = strings.TrimSpace(template) if template == "" { return "" } replacer := strings.NewReplacer( "{subject}", batch.EmailSubject, "{{subject}}", batch.EmailSubject, "{title}", batch.EmailSubject, "{{title}}", batch.EmailSubject, "{content}", batch.EmailBody, "{{content}}", batch.EmailBody, "{body}", batch.EmailBody, "{{body}}", batch.EmailBody, "{recipients}", strings.Join(recipients, ","), "{{recipients}}", strings.Join(recipients, ","), "{deptName}", "", "{{deptName}}", "", "{category}", batch.Category, "{{category}}", batch.Category, "{stage}", stageText(batch.WarnStage), "{{stage}}", stageText(batch.WarnStage), "{business}", batch.Business, "{{business}}", batch.Business, ) return replacer.Replace(template) }