221 lines
6.6 KiB
Go
221 lines
6.6 KiB
Go
package services
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"net/smtp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// SMTPConfig 发送邮件所需参数(与 yz_system_email 字段对应)
|
|
type SMTPConfig struct {
|
|
FromAddress string
|
|
FromName string
|
|
Host string
|
|
Port uint
|
|
Password string
|
|
Encryption string // ssl / tls / none
|
|
Timeout uint // 秒
|
|
}
|
|
|
|
// SendTestEmailSMTP 发送一封简单测试邮件(纯文本 UTF-8)
|
|
func SendTestEmailSMTP(cfg SMTPConfig, to string) error {
|
|
to = strings.TrimSpace(to)
|
|
if to == "" {
|
|
return fmt.Errorf("收件人不能为空")
|
|
}
|
|
if cfg.Host == "" || cfg.FromAddress == "" {
|
|
return fmt.Errorf("SMTP 主机或发件人不能为空")
|
|
}
|
|
if cfg.Port == 0 {
|
|
cfg.Port = 465
|
|
}
|
|
timeout := cfg.Timeout
|
|
if timeout == 0 {
|
|
timeout = 30
|
|
}
|
|
d := net.Dialer{Timeout: time.Duration(timeout) * time.Second}
|
|
addr := net.JoinHostPort(cfg.Host, strconv.FormatUint(uint64(cfg.Port), 10))
|
|
enc := strings.ToLower(strings.TrimSpace(cfg.Encryption))
|
|
if enc == "" {
|
|
enc = "ssl"
|
|
}
|
|
|
|
var client *smtp.Client
|
|
var err error
|
|
|
|
switch enc {
|
|
case "ssl":
|
|
conn, derr := tls.DialWithDialer(&d, "tcp", addr, &tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12})
|
|
if derr != nil {
|
|
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
|
}
|
|
defer conn.Close()
|
|
client, err = smtp.NewClient(conn, cfg.Host)
|
|
if err != nil {
|
|
return fmt.Errorf("SMTP 握手失败: %w", err)
|
|
}
|
|
case "tls":
|
|
conn, derr := d.Dial("tcp", addr)
|
|
if derr != nil {
|
|
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
|
}
|
|
defer conn.Close()
|
|
client, err = smtp.NewClient(conn, cfg.Host)
|
|
if err != nil {
|
|
return fmt.Errorf("SMTP 握手失败: %w", err)
|
|
}
|
|
if ok, _ := client.Extension("STARTTLS"); ok {
|
|
if err = client.StartTLS(&tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12}); err != nil {
|
|
_ = client.Close()
|
|
return fmt.Errorf("STARTTLS 失败: %w", err)
|
|
}
|
|
}
|
|
case "none":
|
|
conn, derr := d.Dial("tcp", addr)
|
|
if derr != nil {
|
|
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
|
}
|
|
defer conn.Close()
|
|
client, err = smtp.NewClient(conn, cfg.Host)
|
|
if err != nil {
|
|
return fmt.Errorf("SMTP 握手失败: %w", err)
|
|
}
|
|
default:
|
|
return fmt.Errorf("不支持的加密方式: %s", cfg.Encryption)
|
|
}
|
|
defer func() { _ = client.Close() }()
|
|
|
|
auth := smtp.PlainAuth("", cfg.FromAddress, cfg.Password, cfg.Host)
|
|
if err = client.Auth(auth); err != nil {
|
|
return fmt.Errorf("SMTP 认证失败: %w", err)
|
|
}
|
|
if err = client.Mail(cfg.FromAddress); err != nil {
|
|
return fmt.Errorf("MAIL FROM 失败: %w", err)
|
|
}
|
|
if err = client.Rcpt(to); err != nil {
|
|
return fmt.Errorf("RCPT TO 失败: %w", err)
|
|
}
|
|
wc, err := client.Data()
|
|
if err != nil {
|
|
return fmt.Errorf("DATA 失败: %w", err)
|
|
}
|
|
fromName := strings.TrimSpace(cfg.FromName)
|
|
subject := "平台邮箱测试"
|
|
body := "这是一封来自管理后台「邮箱管理」的测试邮件。\r\nThis is a test email from the platform email settings.\r\n"
|
|
headers := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n",
|
|
formatFromHeader(fromName, cfg.FromAddress), to, subject)
|
|
if _, err = wc.Write([]byte(headers + body)); err != nil {
|
|
return fmt.Errorf("写入邮件内容失败: %w", err)
|
|
}
|
|
if err = wc.Close(); err != nil {
|
|
return fmt.Errorf("结束 DATA 失败: %w", err)
|
|
}
|
|
return client.Quit()
|
|
}
|
|
|
|
// SendHTMLEmailSMTP 发送一封 HTML 格式邮件
|
|
func SendHTMLEmailSMTP(cfg SMTPConfig, to string, subject string, htmlBody string) error {
|
|
to = strings.TrimSpace(to)
|
|
if to == "" {
|
|
return fmt.Errorf("收件人不能为空")
|
|
}
|
|
if cfg.Host == "" || cfg.FromAddress == "" {
|
|
return fmt.Errorf("SMTP 主机或发件人不能为空")
|
|
}
|
|
if cfg.Port == 0 {
|
|
cfg.Port = 465
|
|
}
|
|
timeout := cfg.Timeout
|
|
if timeout == 0 {
|
|
timeout = 30
|
|
}
|
|
d := net.Dialer{Timeout: time.Duration(timeout) * time.Second}
|
|
addr := net.JoinHostPort(cfg.Host, strconv.FormatUint(uint64(cfg.Port), 10))
|
|
enc := strings.ToLower(strings.TrimSpace(cfg.Encryption))
|
|
if enc == "" {
|
|
enc = "ssl"
|
|
}
|
|
|
|
var client *smtp.Client
|
|
var err error
|
|
|
|
switch enc {
|
|
case "ssl":
|
|
conn, derr := tls.DialWithDialer(&d, "tcp", addr, &tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12})
|
|
if derr != nil {
|
|
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
|
}
|
|
defer conn.Close()
|
|
client, err = smtp.NewClient(conn, cfg.Host)
|
|
if err != nil {
|
|
return fmt.Errorf("SMTP 握手失败: %w", err)
|
|
}
|
|
case "tls":
|
|
conn, derr := d.Dial("tcp", addr)
|
|
if derr != nil {
|
|
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
|
}
|
|
defer conn.Close()
|
|
client, err = smtp.NewClient(conn, cfg.Host)
|
|
if err != nil {
|
|
return fmt.Errorf("SMTP 握手失败: %w", err)
|
|
}
|
|
if ok, _ := client.Extension("STARTTLS"); ok {
|
|
if err = client.StartTLS(&tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12}); err != nil {
|
|
_ = client.Close()
|
|
return fmt.Errorf("STARTTLS 失败: %w", err)
|
|
}
|
|
}
|
|
case "none":
|
|
conn, derr := d.Dial("tcp", addr)
|
|
if derr != nil {
|
|
return fmt.Errorf("连接 SMTP 失败: %w", derr)
|
|
}
|
|
defer conn.Close()
|
|
client, err = smtp.NewClient(conn, cfg.Host)
|
|
if err != nil {
|
|
return fmt.Errorf("SMTP 握手失败: %w", err)
|
|
}
|
|
default:
|
|
return fmt.Errorf("不支持的加密方式: %s", cfg.Encryption)
|
|
}
|
|
defer func() { _ = client.Close() }()
|
|
|
|
auth := smtp.PlainAuth("", cfg.FromAddress, cfg.Password, cfg.Host)
|
|
if err = client.Auth(auth); err != nil {
|
|
return fmt.Errorf("SMTP 认证失败: %w", err)
|
|
}
|
|
if err = client.Mail(cfg.FromAddress); err != nil {
|
|
return fmt.Errorf("MAIL FROM 失败: %w", err)
|
|
}
|
|
if err = client.Rcpt(to); err != nil {
|
|
return fmt.Errorf("RCPT TO 失败: %w", err)
|
|
}
|
|
wc, err := client.Data()
|
|
if err != nil {
|
|
return fmt.Errorf("DATA 失败: %w", err)
|
|
}
|
|
fromName := strings.TrimSpace(cfg.FromName)
|
|
headers := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n",
|
|
formatFromHeader(fromName, cfg.FromAddress), to, subject)
|
|
if _, err = wc.Write([]byte(headers + htmlBody)); err != nil {
|
|
return fmt.Errorf("写入邮件内容失败: %w", err)
|
|
}
|
|
if err = wc.Close(); err != nil {
|
|
return fmt.Errorf("结束 DATA 失败: %w", err)
|
|
}
|
|
return client.Quit()
|
|
}
|
|
|
|
func formatFromHeader(name, addr string) string {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return addr
|
|
}
|
|
return fmt.Sprintf("%s <%s>", name, addr)
|
|
}
|