193 lines
5.9 KiB
Go
193 lines
5.9 KiB
Go
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()
|
||
}
|