127 lines
3.5 KiB
Go
127 lines
3.5 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()
|
||
}
|
||
|
||
func formatFromHeader(name, addr string) string {
|
||
name = strings.TrimSpace(name)
|
||
if name == "" {
|
||
return addr
|
||
}
|
||
return fmt.Sprintf("%s <%s>", name, addr)
|
||
}
|