//go:build unit package service import ( "bufio" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "math/big" "net" "strings" "sync" "sync/atomic" "testing" "time" ) // newSMTPTestCert 生成 127.0.0.1/localhost 的自签证书及其信任池。 func newSMTPTestCert(t *testing.T) (tls.Certificate, *x509.CertPool) { t.Helper() priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("generate key: %v", err) } template := x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "127.0.0.1"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, IsCA: true, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, DNSNames: []string{"localhost"}, } der, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) if err != nil { t.Fatalf("create certificate: %v", err) } leaf, err := x509.ParseCertificate(der) if err != nil { t.Fatalf("parse certificate: %v", err) } pool := x509.NewCertPool() pool.AddCert(leaf) return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: priv}, pool } // fakeSMTPServer 是覆盖三种连接形态的最小 SMTP 服务器: // 隐式 TLS(465 语义)、明文+STARTTLS(587 语义)、纯明文。 type fakeSMTPServer struct { listener net.Listener tlsConfig *tls.Config advertiseStartTLS bool mu sync.Mutex commands []string conns atomic.Int64 wg sync.WaitGroup } func startFakeSMTPServer(t *testing.T, implicitTLS, advertiseStartTLS bool) (*fakeSMTPServer, int) { t.Helper() cert, pool := newSMTPTestCert(t) prevPool := smtpTestRootCAs smtpTestRootCAs = pool t.Cleanup(func() { smtpTestRootCAs = prevPool }) listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("listen: %v", err) } srv := &fakeSMTPServer{ listener: listener, tlsConfig: &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12}, advertiseStartTLS: advertiseStartTLS, } if implicitTLS { srv.listener = tls.NewListener(listener, srv.tlsConfig) } t.Cleanup(func() { _ = srv.listener.Close() srv.wg.Wait() }) srv.wg.Add(1) go func() { defer srv.wg.Done() for { conn, err := srv.listener.Accept() if err != nil { return } srv.conns.Add(1) srv.wg.Add(1) go func() { defer srv.wg.Done() defer func() { _ = conn.Close() }() _ = conn.SetDeadline(time.Now().Add(10 * time.Second)) srv.serve(conn, srv.advertiseStartTLS) }() } }() port := listener.Addr().(*net.TCPAddr).Port return srv, port } func (srv *fakeSMTPServer) record(cmd string) { srv.mu.Lock() defer srv.mu.Unlock() srv.commands = append(srv.commands, cmd) } func (srv *fakeSMTPServer) sawCommand(prefix string) bool { srv.mu.Lock() defer srv.mu.Unlock() for _, cmd := range srv.commands { if strings.HasPrefix(strings.ToUpper(cmd), prefix) { return true } } return false } func (srv *fakeSMTPServer) serve(conn net.Conn, allowStartTLS bool) { reader := bufio.NewReader(conn) writer := bufio.NewWriter(conn) writeLine := func(line string) bool { if _, err := writer.WriteString(line + "\r\n"); err != nil { return false } return writer.Flush() == nil } if !writeLine("220 fake.test ESMTP ready") { return } for { line, err := reader.ReadString('\n') if err != nil { return } cmd := strings.TrimSpace(line) srv.record(cmd) upper := strings.ToUpper(cmd) switch { case strings.HasPrefix(upper, "EHLO"), strings.HasPrefix(upper, "HELO"): ok := writeLine("250-fake.test") if allowStartTLS { ok = ok && writeLine("250-STARTTLS") } if !(ok && writeLine("250-AUTH PLAIN LOGIN") && writeLine("250 8BITMIME")) { return } case upper == "STARTTLS" && allowStartTLS: if !writeLine("220 2.0.0 ready to start TLS") { return } tlsConn := tls.Server(conn, srv.tlsConfig) if err := tlsConn.Handshake(); err != nil { return } srv.serveUpgraded(tlsConn) return case strings.HasPrefix(upper, "AUTH"): if !writeLine("235 2.7.0 authentication successful") { return } case strings.HasPrefix(upper, "MAIL"), strings.HasPrefix(upper, "RCPT"): if !writeLine("250 ok") { return } case upper == "DATA": if !writeLine("354 go ahead") { return } for { dataLine, err := reader.ReadString('\n') if err != nil { return } if strings.TrimRight(dataLine, "\r\n") == "." { break } } if !writeLine("250 message accepted") { return } case upper == "QUIT": _ = writeLine("221 bye") return default: if !writeLine("250 ok") { return } } } } // serveUpgraded 复用命令循环处理 STARTTLS 升级后的会话(升级后不再提供 STARTTLS)。 func (srv *fakeSMTPServer) serveUpgraded(conn net.Conn) { reader := bufio.NewReader(conn) writer := bufio.NewWriter(conn) // net/smtp 在 StartTLS 成功后会重新发送 EHLO,直接进入命令循环即可。 srv.serveCommands(reader, writer) } func (srv *fakeSMTPServer) serveCommands(reader *bufio.Reader, writer *bufio.Writer) { writeLine := func(line string) bool { if _, err := writer.WriteString(line + "\r\n"); err != nil { return false } return writer.Flush() == nil } for { line, err := reader.ReadString('\n') if err != nil { return } cmd := strings.TrimSpace(line) srv.record(cmd) upper := strings.ToUpper(cmd) switch { case strings.HasPrefix(upper, "EHLO"), strings.HasPrefix(upper, "HELO"): if !(writeLine("250-fake.test") && writeLine("250-AUTH PLAIN LOGIN") && writeLine("250 8BITMIME")) { return } case strings.HasPrefix(upper, "AUTH"): if !writeLine("235 2.7.0 authentication successful") { return } case strings.HasPrefix(upper, "MAIL"), strings.HasPrefix(upper, "RCPT"): if !writeLine("250 ok") { return } case upper == "DATA": if !writeLine("354 go ahead") { return } for { dataLine, err := reader.ReadString('\n') if err != nil { return } if strings.TrimRight(dataLine, "\r\n") == "." { break } } if !writeLine("250 message accepted") { return } case upper == "QUIT": _ = writeLine("221 bye") return default: if !writeLine("250 ok") { return } } } } func smtpTestConfig(port int, useTLS bool) *SMTPConfig { return &SMTPConfig{ Host: "127.0.0.1", Port: port, Username: "user", Password: "pass", From: "noreply@example.com", FromName: "Test", UseTLS: useTLS, } } // 465 语义:UseTLS=true + 隐式 TLS 服务器,原有路径保持可用。 func TestSMTPConnectionImplicitTLS(t *testing.T) { srv, port := startFakeSMTPServer(t, true, false) svc := &EmailService{} if err := svc.TestSMTPConnectionWithConfig(smtpTestConfig(port, true)); err != nil { t.Fatalf("expected implicit TLS connection to succeed, got: %v", err) } if !srv.sawCommand("EHLO") { t.Fatal("expected server to receive EHLO") } } // 587 语义(#1470/#1488 核心场景):UseTLS=true + 明文问候的 STARTTLS 服务器, // 隐式 TLS 失败后必须自动降级为强制 STARTTLS 并成功。 func TestSMTPConnectionStartTLSFallbackWhenTLSEnabled(t *testing.T) { srv, port := startFakeSMTPServer(t, false, true) svc := &EmailService{} if err := svc.TestSMTPConnectionWithConfig(smtpTestConfig(port, true)); err != nil { t.Fatalf("expected STARTTLS fallback to succeed, got: %v", err) } if !srv.sawCommand("STARTTLS") { t.Fatal("expected server to receive STARTTLS command") } if got := srv.conns.Load(); got < 2 { t.Fatalf("expected implicit TLS attempt before STARTTLS fallback (>=2 connections), got %d", got) } } // UseTLS=true 但服务器不支持 STARTTLS:必须报错,且绝不能把凭据发到明文连接上。 func TestSMTPConnectionMandatoryStartTLSRefusesPlaintext(t *testing.T) { srv, port := startFakeSMTPServer(t, false, false) svc := &EmailService{} err := svc.TestSMTPConnectionWithConfig(smtpTestConfig(port, true)) if err == nil { t.Fatal("expected error when server does not support STARTTLS") } if !strings.Contains(err.Error(), "STARTTLS") { t.Fatalf("expected STARTTLS-related error, got: %v", err) } if srv.sawCommand("AUTH") { t.Fatal("credentials must not be sent over plaintext when TLS is required") } } // UseTLS=false + 服务器支持 STARTTLS:测试连接与发送路径一致,机会式升级后认证成功。 // 这是 #1488 评论"测试连接不成功,发送测试邮件实际上能发"的回归用例。 func TestSMTPConnectionOpportunisticStartTLSWhenTLSDisabled(t *testing.T) { srv, port := startFakeSMTPServer(t, false, true) svc := &EmailService{} if err := svc.TestSMTPConnectionWithConfig(smtpTestConfig(port, false)); err != nil { t.Fatalf("expected opportunistic STARTTLS test connection to succeed, got: %v", err) } if !srv.sawCommand("STARTTLS") { t.Fatal("expected test connection to upgrade via STARTTLS like the send path") } } // UseTLS=false + 服务器不支持 STARTTLS:保持明文直连(既有行为不回归)。 func TestSMTPConnectionPlainWhenNoStartTLS(t *testing.T) { srv, port := startFakeSMTPServer(t, false, false) svc := &EmailService{} if err := svc.TestSMTPConnectionWithConfig(smtpTestConfig(port, false)); err != nil { t.Fatalf("expected plain connection to succeed, got: %v", err) } if srv.sawCommand("STARTTLS") { t.Fatal("did not expect STARTTLS command when server does not advertise it") } } // 发送路径全流程:UseTLS=true + STARTTLS 服务器(587 语义)完整走完 MAIL/RCPT/DATA。 func TestSendEmailWithConfigStartTLSFallback(t *testing.T) { srv, port := startFakeSMTPServer(t, false, true) svc := &EmailService{} err := svc.SendEmailWithConfig(smtpTestConfig(port, true), "rcpt@example.com", "subject", "

body

") if err != nil { t.Fatalf("expected send via STARTTLS fallback to succeed, got: %v", err) } if !srv.sawCommand("STARTTLS") { t.Fatal("expected send path to upgrade via STARTTLS") } if !srv.sawCommand("DATA") { t.Fatal("expected send path to reach DATA") } } // 发送路径全流程:UseTLS=true + 隐式 TLS 服务器(465 语义)保持既有行为。 func TestSendEmailWithConfigImplicitTLS(t *testing.T) { srv, port := startFakeSMTPServer(t, true, false) svc := &EmailService{} err := svc.SendEmailWithConfig(smtpTestConfig(port, true), "rcpt@example.com", "subject", "

body

") if err != nil { t.Fatalf("expected send via implicit TLS to succeed, got: %v", err) } if !srv.sawCommand("DATA") { t.Fatal("expected send path to reach DATA") } }