258 lines
6.0 KiB
Go
258 lines
6.0 KiB
Go
package smtp_test
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"code.yun.ink/pkg/mailx"
|
|
"code.yun.ink/pkg/mailx/smtp"
|
|
)
|
|
|
|
// TestMail 真实发送到外部 SMTP 服务器,需要网络与有效凭据。
|
|
// 用于本地联调,CI 中可跳过。
|
|
func TestMail(t *testing.T) {
|
|
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 {
|
|
t.Fatal(err)
|
|
}
|
|
defer req.Body.Close()
|
|
|
|
by, err := io.ReadAll(req.Body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
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 = client.Send(ctx, msg)
|
|
fmt.Println(err)
|
|
}
|
|
|
|
// TestSendFakeServer 使用内存 SMTP 服务器做端到端验证,不依赖外部网络。
|
|
func TestSendFakeServer(t *testing.T) {
|
|
var got bytes.Buffer
|
|
host, port := startFakeSMTPServer(t, func(data []byte) { got.Write(data) })
|
|
|
|
client := smtp.New(smtp.Config{
|
|
Host: host,
|
|
Port: port,
|
|
User: "sender@example.com",
|
|
Password: "secret",
|
|
Encryption: smtp.EncryptionNone, // 假服务器不支持 TLS,使用明文验证投递流程
|
|
})
|
|
|
|
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()
|
|
|
|
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
|
|
}
|