feat: major update - MySQL, chat, wishlist, PWA, admin overhaul
This commit is contained in:
192
mengyastore-backend/internal/email/email.go
Normal file
192
mengyastore-backend/internal/email/email.go
Normal file
@@ -0,0 +1,192 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user