package email import ( "crypto/tls" "fmt" "net/smtp" "strings" "time" ) // Config holds SMTP sender configuration. type Config struct { SMTPHost string // e.g. smtp.qq.com SMTPPort string // e.g. 465 (SSL) or 587 (STARTTLS) From string // sender email address Password string // SMTP auth password / app password FromName string // display name, e.g. "萌芽小店" } // IsConfigured returns true if enough config is present to send mail. func (c *Config) IsConfigured() bool { return c.From != "" && c.Password != "" && c.SMTPHost != "" } // OrderNotifyData contains the data for an order notification email. type OrderNotifyData struct { ToEmail string ToName string ProductName string OrderID string Quantity int Codes []string // empty for manual delivery IsManual bool } // SendOrderNotify sends an order delivery notification email. // Returns nil if config is not ready or ToEmail is empty (silently skip). func SendOrderNotify(cfg Config, data OrderNotifyData) error { if !cfg.IsConfigured() || data.ToEmail == "" { return nil } if cfg.SMTPPort == "" { cfg.SMTPPort = "465" } if cfg.SMTPHost == "" { cfg.SMTPHost = "smtp.qq.com" } fromName := cfg.FromName if fromName == "" { fromName = "萌芽小店" } subject := "【萌芽小店】您的订单已发货" if data.IsManual { subject = "【萌芽小店】您的订单正在处理中" } body := buildBody(data) msg := buildMIMEMessage(cfg.From, fromName, data.ToEmail, subject, body) addr := fmt.Sprintf("%s:%s", cfg.SMTPHost, cfg.SMTPPort) auth := smtp.PlainAuth("", cfg.From, cfg.Password, cfg.SMTPHost) // QQ mail uses SSL on port 465; use TLS dial directly. if cfg.SMTPPort == "465" { return sendSSL(addr, cfg.SMTPHost, auth, cfg.From, data.ToEmail, msg) } return smtp.SendMail(addr, auth, cfg.From, []string{data.ToEmail}, []byte(msg)) } func buildBody(data OrderNotifyData) string { var sb strings.Builder now := time.Now().Format("2006 年 01 月 02 日 15:04:05") recipient := data.ToName if recipient == "" { recipient = "用户" } sb.WriteString("尊敬的 ") sb.WriteString(recipient) sb.WriteString(",\n\n") sb.WriteString(" 您好!感谢您在萌芽小店的支持与购买。\n\n") sb.WriteString("────────────────────────────────\n") sb.WriteString(" 订单信息\n") sb.WriteString("────────────────────────────────\n") sb.WriteString(fmt.Sprintf(" 商品名称:%s\n", data.ProductName)) sb.WriteString(fmt.Sprintf(" 订单编号:%s\n", data.OrderID)) sb.WriteString(fmt.Sprintf(" 购买数量:%d 件\n", data.Quantity)) sb.WriteString(fmt.Sprintf(" 通知时间:%s\n", now)) sb.WriteString("────────────────────────────────\n\n") if data.IsManual { sb.WriteString(" 您的订单已成功提交,目前正在等待人工审核与处理。\n") sb.WriteString(" 工作人员将尽快为您安排发货,请耐心等候。\n") sb.WriteString(" 发货完成后,我们将另行发送邮件通知。\n\n") } else { sb.WriteString(" 您的订单已完成自动发货,发货内容如下:\n\n") if len(data.Codes) > 0 { for i, code := range data.Codes { sb.WriteString(fmt.Sprintf(" [%d] %s\n", i+1, code)) } sb.WriteString("\n") } sb.WriteString(" 请妥善保管以上发货内容,切勿泄露给他人。\n\n") } sb.WriteString(" 如有任何疑问,请联系在线客服,我们将竭诚为您服务。\n\n") sb.WriteString("────────────────────────────────\n") sb.WriteString(" 此邮件由系统自动发送,请勿直接回复。\n") sb.WriteString("────────────────────────────────\n") return sb.String() } func buildMIMEMessage(from, fromName, to, subject, body string) string { encodedFromName := fmt.Sprintf("=?UTF-8?B?%s?=", encodeBase64(fromName)) encodedSubject := fmt.Sprintf("=?UTF-8?B?%s?=", encodeBase64(subject)) return fmt.Sprintf( "From: %s <%s>\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: base64\r\n\r\n%s", encodedFromName, from, to, encodedSubject, encodeBase64(body), ) } func encodeBase64(s string) string { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" b := []byte(s) var buf strings.Builder for i := 0; i < len(b); i += 3 { remaining := len(b) - i b0 := b[i] b1 := byte(0) b2 := byte(0) if remaining > 1 { b1 = b[i+1] } if remaining > 2 { b2 = b[i+2] } buf.WriteByte(chars[b0>>2]) buf.WriteByte(chars[((b0&0x03)<<4)|(b1>>4)]) if remaining > 1 { buf.WriteByte(chars[((b1&0x0f)<<2)|(b2>>6)]) } else { buf.WriteByte('=') } if remaining > 2 { buf.WriteByte(chars[b2&0x3f]) } else { buf.WriteByte('=') } } return buf.String() } func sendSSL(addr, host string, auth smtp.Auth, from, to string, msg string) error { tlsConfig := &tls.Config{ ServerName: host, MinVersion: tls.VersionTLS12, } conn, err := tls.Dial("tcp", addr, tlsConfig) if err != nil { return fmt.Errorf("tls dial: %w", err) } defer conn.Close() client, err := smtp.NewClient(conn, host) if err != nil { return fmt.Errorf("smtp new client: %w", err) } defer client.Quit() //nolint:errcheck if err = client.Auth(auth); err != nil { return fmt.Errorf("smtp auth: %w", err) } if err = client.Mail(from); err != nil { return fmt.Errorf("smtp MAIL FROM: %w", err) } if err = client.Rcpt(to); err != nil { return fmt.Errorf("smtp RCPT TO: %w", err) } w, err := client.Data() if err != nil { return fmt.Errorf("smtp DATA: %w", err) } if _, err = fmt.Fprint(w, msg); err != nil { return fmt.Errorf("smtp write body: %w", err) } return w.Close() }