package infra import ( "crypto/tls" "fmt" "net" "net/smtp" "strings" ) // SMTPMailConfig SMTP 发信参数 type SMTPMailConfig struct { Host string Port int User string Password string FromEmail string FromName string } // SendSMTPMail 通过 SMTP 发送纯文本邮件(支持 465 SSL / 587 STARTTLS) func SendSMTPMail(cfg SMTPMailConfig, to, subject, body string) error { if cfg.Host == "" || cfg.FromEmail == "" { return fmt.Errorf("SMTP 未配置完整") } to = strings.TrimSpace(to) if to == "" { return fmt.Errorf("收件人邮箱为空") } port := cfg.Port if port <= 0 { port = 465 } from := cfg.FromEmail fromHeader := from if cfg.FromName != "" { fromHeader = fmt.Sprintf("%s <%s>", cfg.FromName, from) } msg := buildPlainTextMessage(fromHeader, to, subject, body) addr := fmt.Sprintf("%s:%d", cfg.Host, port) if port == 465 { return sendSMTPSImplicitTLS(addr, cfg, from, to, msg) } return sendSMTPStartTLS(addr, cfg, from, to, msg) } func buildPlainTextMessage(from, to, subject, body string) []byte { var b strings.Builder b.WriteString("From: " + from + "\r\n") b.WriteString("To: " + to + "\r\n") b.WriteString("Subject: " + subject + "\r\n") b.WriteString("MIME-Version: 1.0\r\n") b.WriteString("Content-Type: text/plain; charset=UTF-8\r\n") b.WriteString("\r\n") b.WriteString(body) return []byte(b.String()) } func sendSMTPSImplicitTLS(addr string, cfg SMTPMailConfig, from, to string, msg []byte) error { tlsCfg := &tls.Config{ServerName: cfg.Host} conn, err := tls.Dial("tcp", addr, tlsCfg) if err != nil { return fmt.Errorf("连接 SMTP 失败: %w", err) } defer conn.Close() client, err := smtp.NewClient(conn, cfg.Host) if err != nil { return fmt.Errorf("创建 SMTP 客户端失败: %w", err) } defer client.Close() if cfg.User != "" { if err := client.Auth(smtp.PlainAuth("", cfg.User, cfg.Password, cfg.Host)); err != nil { return fmt.Errorf("SMTP 认证失败: %w", err) } } if err := client.Mail(from); err != nil { return fmt.Errorf("设置发件人失败: %w", err) } if err := client.Rcpt(to); err != nil { return fmt.Errorf("设置收件人失败: %w", err) } w, err := client.Data() if err != nil { return fmt.Errorf("打开邮件正文失败: %w", err) } if _, err := w.Write(msg); err != nil { return fmt.Errorf("写入邮件正文失败: %w", err) } if err := w.Close(); err != nil { return fmt.Errorf("关闭邮件正文失败: %w", err) } return client.Quit() } func sendSMTPStartTLS(addr string, cfg SMTPMailConfig, from, to string, msg []byte) error { conn, err := net.Dial("tcp", addr) if err != nil { return fmt.Errorf("连接 SMTP 失败: %w", err) } defer conn.Close() host := cfg.Host client, err := smtp.NewClient(conn, host) if err != nil { return fmt.Errorf("创建 SMTP 客户端失败: %w", err) } defer client.Close() if ok, _ := client.Extension("STARTTLS"); ok { if err := client.StartTLS(&tls.Config{ServerName: host}); err != nil { return fmt.Errorf("STARTTLS 失败: %w", err) } } if cfg.User != "" { if err := client.Auth(smtp.PlainAuth("", cfg.User, cfg.Password, host)); err != nil { return fmt.Errorf("SMTP 认证失败: %w", err) } } if err := client.Mail(from); err != nil { return fmt.Errorf("设置发件人失败: %w", err) } if err := client.Rcpt(to); err != nil { return fmt.Errorf("设置收件人失败: %w", err) } w, err := client.Data() if err != nil { return fmt.Errorf("打开邮件正文失败: %w", err) } if _, err := w.Write(msg); err != nil { return fmt.Errorf("写入邮件正文失败: %w", err) } if err := w.Close(); err != nil { return fmt.Errorf("关闭邮件正文失败: %w", err) } return client.Quit() }