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("

Hello

"). 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 .") 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 }