| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- //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", "<p>body</p>")
- 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", "<p>body</p>")
- 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")
- }
- }
|