Files
mailx/smtp/smtp_test.go
T

258 lines
6.0 KiB
Go
Raw Normal View History

2024-11-20 19:42:07 +08:00
package smtp_test
import (
2026-08-15 01:38:05 +08:00
"bufio"
"bytes"
2024-11-20 19:42:07 +08:00
"context"
"fmt"
"io"
2026-08-15 01:38:05 +08:00
"net"
2024-11-20 19:42:07 +08:00
"net/http"
2026-08-15 01:38:05 +08:00
"strconv"
"strings"
2024-11-20 19:42:07 +08:00
"testing"
2026-08-15 01:38:05 +08:00
"time"
2024-11-20 19:42:07 +08:00
2026-08-15 01:38:05 +08:00
"code.yun.ink/pkg/mailx"
2024-11-20 19:42:07 +08:00
"code.yun.ink/pkg/mailx/smtp"
)
2026-08-15 01:38:05 +08:00
// TestMail 真实发送到外部 SMTP 服务器,需要网络与有效凭据。
// 用于本地联调,CI 中可跳过。
2024-11-20 19:42:07 +08:00
func TestMail(t *testing.T) {
2026-08-15 01:38:05 +08:00
if testing.Short() {
t.Skip("skip real send in short mode")
2024-11-20 19:42:07 +08:00
}
2026-08-15 01:38:05 +08:00
client := smtp.New(smtp.Config{
Host: "smtpdm-ap-southeast-1.aliyun.com",
Port: 80,
User: "support@email.blueoceanpay.com",
Password: "SupporT2017",
})
ctx := context.Background()
2024-11-20 19:42:07 +08:00
req, err := http.Get("https://baidu.com")
if err != nil {
t.Fatal(err)
}
defer req.Body.Close()
by, err := io.ReadAll(req.Body)
if err != nil {
t.Fatal(err)
}
2026-08-15 01:38:05 +08:00
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()
2024-11-20 19:42:07 +08:00
2026-08-15 01:38:05 +08:00
err = client.Send(ctx, msg)
2024-11-20 19:42:07 +08:00
fmt.Println(err)
}
2026-08-15 01:38:05 +08:00
// TestSendFakeServer 使用内存 SMTP 服务器做端到端验证,不依赖外部网络。
func TestSendFakeServer(t *testing.T) {
var got bytes.Buffer
host, port := startFakeSMTPServer(t, func(data []byte) { got.Write(data) })
2024-11-20 19:42:07 +08:00
2026-08-15 01:38:05 +08:00
client := smtp.New(smtp.Config{
Host: host,
Port: port,
User: "sender@example.com",
Password: "secret",
Encryption: smtp.EncryptionNone, // 假服务器不支持 TLS,使用明文验证投递流程
})
2024-11-20 19:42:07 +08:00
2026-08-15 01:38:05 +08:00
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()
2024-11-20 19:42:07 +08:00
2026-08-15 01:38:05 +08:00
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
}