更新
This commit is contained in:
+224
-58
@@ -1,30 +1,36 @@
|
||||
package smtp_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.yun.ink/pkg/mailx/interfaces"
|
||||
"code.yun.ink/pkg/mailx"
|
||||
"code.yun.ink/pkg/mailx/smtp"
|
||||
)
|
||||
|
||||
// TestMail 真实发送到外部 SMTP 服务器,需要网络与有效凭据。
|
||||
// 用于本地联调,CI 中可跳过。
|
||||
func TestMail(t *testing.T) {
|
||||
sm := smtp.NewSmtp()
|
||||
ctx := context.Background()
|
||||
|
||||
ini, err := sm.SetOption(ctx, interfaces.SetSmtp(&interfaces.EmailConfigDataSmtp{
|
||||
Username: "support@email.blueoceanpay.com",
|
||||
Password: "SupporT2017",
|
||||
ReplyTo: "",
|
||||
Host: "smtpdm-ap-southeast-1.aliyun.com",
|
||||
Port: "80",
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
if testing.Short() {
|
||||
t.Skip("skip real send in short mode")
|
||||
}
|
||||
client := smtp.New(smtp.Config{
|
||||
Host: "smtpdm-ap-southeast-1.aliyun.com",
|
||||
Port: 80,
|
||||
User: "support@email.blueoceanpay.com",
|
||||
Password: "SupporT2017",
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
req, err := http.Get("https://baidu.com")
|
||||
if err != nil {
|
||||
@@ -37,55 +43,215 @@ func TestMail(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
msg := interfaces.Message{
|
||||
To: []string{"995116474@qq.com"},
|
||||
Cc: []string{"287852692@qq.com"},
|
||||
Bcc: []string{"1362716835@qq.com"},
|
||||
ReplyTo: "huangxinyun@dreaminglife.cn",
|
||||
Subject: "test mail",
|
||||
Body: string(by),
|
||||
Attachment: []interfaces.MessageAttachment{
|
||||
// {
|
||||
// Name: "/code/statistic/out.xlsx",
|
||||
// ContentType: "",
|
||||
// WithFile: true,
|
||||
// },
|
||||
// {
|
||||
// Name: "/code/statistic/origin.xlsx",
|
||||
// ContentType: "",
|
||||
// WithFile: true,
|
||||
// },
|
||||
// {
|
||||
// Name: "/code/statistic/out2.xlsx",
|
||||
// ContentType: "",
|
||||
// WithFile: true,
|
||||
// },
|
||||
},
|
||||
}
|
||||
msg := mailx.NewMessage().
|
||||
To("995116474@qq.com").
|
||||
Cc("287852692@qq.com").
|
||||
Bcc("1362716835@qq.com").
|
||||
ReplyTo("huangxinyun@dreaminglife.cn").
|
||||
Subject("test mail").
|
||||
Body(string(by)).
|
||||
Build()
|
||||
|
||||
err = ini.Send(ctx, msg)
|
||||
err = client.Send(ctx, msg)
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
// func TestQQ(t *testing.T) {
|
||||
// TestSendFakeServer 使用内存 SMTP 服务器做端到端验证,不依赖外部网络。
|
||||
func TestSendFakeServer(t *testing.T) {
|
||||
var got bytes.Buffer
|
||||
host, port := startFakeSMTPServer(t, func(data []byte) { got.Write(data) })
|
||||
|
||||
// // 发件人邮箱
|
||||
// from := "995116474@qq.com"
|
||||
// // 授权码,而非密码
|
||||
// authCode := "xxxxxxxxxxxxxxxxxxxxxx"
|
||||
// // 收件人邮箱,可以是多个收件人
|
||||
// to := []string{"yun@yun.ink"}
|
||||
// // 邮件服务器信息
|
||||
// smtpHost := "smtp.qq.com"
|
||||
// smtpPort := "587" // 或使用465,根据你的SMTP服务器要求设置
|
||||
client := smtp.New(smtp.Config{
|
||||
Host: host,
|
||||
Port: port,
|
||||
User: "sender@example.com",
|
||||
Password: "secret",
|
||||
Encryption: smtp.EncryptionNone, // 假服务器不支持 TLS,使用明文验证投递流程
|
||||
})
|
||||
|
||||
// mail := mailx.NewMailx(from, authCode, smtpHost, smtpPort)
|
||||
msg := mailx.NewMessage().
|
||||
From("sender@example.com").
|
||||
To("to@example.com", "to2@example.com").
|
||||
Cc("cc@example.com").
|
||||
Subject("测试主题").
|
||||
Body("<h1>Hello</h1>").
|
||||
ReplyTo("reply@example.com").
|
||||
AttachBytes("a.txt", []byte("attachment-data")).
|
||||
Build()
|
||||
|
||||
// msg := mailx.Message{
|
||||
// To: to,
|
||||
// Subject: "test mail",
|
||||
// Body: "测试",
|
||||
// }
|
||||
// err := mail.Send(msg)
|
||||
// fmt.Println(err)
|
||||
// }
|
||||
if err := client.Send(context.Background(), msg); err != nil {
|
||||
t.Fatalf("Send: %v", err)
|
||||
}
|
||||
|
||||
out := got.String()
|
||||
for _, want := range []string{
|
||||
"From: sender@example.com",
|
||||
"To: to@example.com, to2@example.com",
|
||||
"Cc: cc@example.com",
|
||||
"Reply-To: reply@example.com",
|
||||
"Subject: =?UTF-8?q?",
|
||||
"multipart/mixed",
|
||||
"text/html; charset=UTF-8",
|
||||
"attachment; filename",
|
||||
"base64",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("delivered MIME missing %q, got:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendEmptySender(t *testing.T) {
|
||||
client := smtp.New(smtp.Config{Host: "h", Port: 25})
|
||||
msg := mailx.NewMessage().To("a@b.com").Subject("s").Build()
|
||||
if err := client.Send(context.Background(), msg); err == nil {
|
||||
t.Fatal("Send without sender should error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
if got := smtp.New(smtp.Config{}).Name(); got != "smtp" {
|
||||
t.Errorf("Name() = %q, want smtp", got)
|
||||
}
|
||||
}
|
||||
|
||||
// startFakeSMTPServer 启动一个内存 SMTP 服务器,捕获 DATA 阶段内容。
|
||||
func startFakeSMTPServer(t *testing.T, capture func([]byte)) (string, int) {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go serveSMTP(conn, capture)
|
||||
}
|
||||
}()
|
||||
|
||||
host, portStr, _ := net.SplitHostPort(ln.Addr().String())
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
return host, port
|
||||
}
|
||||
|
||||
// serveSMTP 处理单个 SMTP 连接,支持 EHLO/AUTH PLAIN/MAIL/RCPT/DATA/QUIT。
|
||||
func serveSMTP(conn net.Conn, capture func([]byte)) {
|
||||
defer conn.Close()
|
||||
r := bufio.NewReader(conn)
|
||||
w := bufio.NewWriter(conn)
|
||||
respond := func(line string) {
|
||||
_, _ = w.WriteString(line + "\r\n")
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
respond("220 fake ESMTP ready")
|
||||
|
||||
inData := false
|
||||
var data bytes.Buffer
|
||||
for {
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
line = strings.TrimRight(line, "\r\n")
|
||||
|
||||
if inData {
|
||||
if line == "." {
|
||||
inData = false
|
||||
capture(data.Bytes())
|
||||
data.Reset()
|
||||
respond("250 2.0.0 Ok: queued")
|
||||
continue
|
||||
}
|
||||
data.WriteString(line + "\r\n")
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(line, "EHLO"):
|
||||
respond("250-localhost")
|
||||
respond("250-AUTH PLAIN LOGIN")
|
||||
respond("250 8BITMIME")
|
||||
case strings.HasPrefix(line, "AUTH"):
|
||||
respond("235 2.7.0 Authentication successful")
|
||||
case strings.HasPrefix(line, "MAIL FROM"):
|
||||
respond("250 2.1.0 Ok")
|
||||
case strings.HasPrefix(line, "RCPT TO"):
|
||||
respond("250 2.1.5 Ok")
|
||||
case strings.HasPrefix(line, "DATA"):
|
||||
respond("354 End data with <CR><LF>.<CR><LF>")
|
||||
inData = true
|
||||
case strings.HasPrefix(line, "STARTTLS"):
|
||||
// 假服务器不支持 TLS 升级,明确拒绝,避免客户端挂起
|
||||
respond("454 4.7.0 TLS not available")
|
||||
case strings.HasPrefix(line, "QUIT"):
|
||||
respond("221 2.0.0 Bye")
|
||||
return
|
||||
default:
|
||||
respond("502 5.5.2 Command not recognized")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSendTimeout 验证当 SMTP 服务器不响应时,deadline 能及时中断发送而非无限阻塞。
|
||||
func TestSendTimeout(t *testing.T) {
|
||||
host, port := startSilentSMTPServer(t)
|
||||
|
||||
client := smtp.New(smtp.Config{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Encryption: smtp.EncryptionNone,
|
||||
Timeout: 300 * time.Millisecond, // 短超时
|
||||
})
|
||||
|
||||
msg := mailx.NewMessage().To("a@b.com").Subject("s").Build()
|
||||
|
||||
start := time.Now()
|
||||
err := client.Send(context.Background(), msg)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected timeout error, got nil")
|
||||
}
|
||||
if elapsed > 3*time.Second {
|
||||
t.Fatalf("Send took %v, expected to time out quickly", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
// startSilentSMTPServer 启动一个接受连接但从不响应的服务器,用于测试超时。
|
||||
func startSilentSMTPServer(t *testing.T) (string, int) {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 静默:读取请求但从不回复,让客户端因 deadline 超时
|
||||
go func(c net.Conn) {
|
||||
defer c.Close()
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
if _, err := c.Read(buf); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
host, portStr, _ := net.SplitHostPort(ln.Addr().String())
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
return host, port
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user