email_service_smtp_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. //go:build unit
  2. package service
  3. import (
  4. "bufio"
  5. "crypto/ecdsa"
  6. "crypto/elliptic"
  7. "crypto/rand"
  8. "crypto/tls"
  9. "crypto/x509"
  10. "crypto/x509/pkix"
  11. "math/big"
  12. "net"
  13. "strings"
  14. "sync"
  15. "sync/atomic"
  16. "testing"
  17. "time"
  18. )
  19. // newSMTPTestCert 生成 127.0.0.1/localhost 的自签证书及其信任池。
  20. func newSMTPTestCert(t *testing.T) (tls.Certificate, *x509.CertPool) {
  21. t.Helper()
  22. priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
  23. if err != nil {
  24. t.Fatalf("generate key: %v", err)
  25. }
  26. template := x509.Certificate{
  27. SerialNumber: big.NewInt(1),
  28. Subject: pkix.Name{CommonName: "127.0.0.1"},
  29. NotBefore: time.Now().Add(-time.Hour),
  30. NotAfter: time.Now().Add(time.Hour),
  31. KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
  32. ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
  33. BasicConstraintsValid: true,
  34. IsCA: true,
  35. IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
  36. DNSNames: []string{"localhost"},
  37. }
  38. der, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
  39. if err != nil {
  40. t.Fatalf("create certificate: %v", err)
  41. }
  42. leaf, err := x509.ParseCertificate(der)
  43. if err != nil {
  44. t.Fatalf("parse certificate: %v", err)
  45. }
  46. pool := x509.NewCertPool()
  47. pool.AddCert(leaf)
  48. return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: priv}, pool
  49. }
  50. // fakeSMTPServer 是覆盖三种连接形态的最小 SMTP 服务器:
  51. // 隐式 TLS(465 语义)、明文+STARTTLS(587 语义)、纯明文。
  52. type fakeSMTPServer struct {
  53. listener net.Listener
  54. tlsConfig *tls.Config
  55. advertiseStartTLS bool
  56. mu sync.Mutex
  57. commands []string
  58. conns atomic.Int64
  59. wg sync.WaitGroup
  60. }
  61. func startFakeSMTPServer(t *testing.T, implicitTLS, advertiseStartTLS bool) (*fakeSMTPServer, int) {
  62. t.Helper()
  63. cert, pool := newSMTPTestCert(t)
  64. prevPool := smtpTestRootCAs
  65. smtpTestRootCAs = pool
  66. t.Cleanup(func() { smtpTestRootCAs = prevPool })
  67. listener, err := net.Listen("tcp", "127.0.0.1:0")
  68. if err != nil {
  69. t.Fatalf("listen: %v", err)
  70. }
  71. srv := &fakeSMTPServer{
  72. listener: listener,
  73. tlsConfig: &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12},
  74. advertiseStartTLS: advertiseStartTLS,
  75. }
  76. if implicitTLS {
  77. srv.listener = tls.NewListener(listener, srv.tlsConfig)
  78. }
  79. t.Cleanup(func() {
  80. _ = srv.listener.Close()
  81. srv.wg.Wait()
  82. })
  83. srv.wg.Add(1)
  84. go func() {
  85. defer srv.wg.Done()
  86. for {
  87. conn, err := srv.listener.Accept()
  88. if err != nil {
  89. return
  90. }
  91. srv.conns.Add(1)
  92. srv.wg.Add(1)
  93. go func() {
  94. defer srv.wg.Done()
  95. defer func() { _ = conn.Close() }()
  96. _ = conn.SetDeadline(time.Now().Add(10 * time.Second))
  97. srv.serve(conn, srv.advertiseStartTLS)
  98. }()
  99. }
  100. }()
  101. port := listener.Addr().(*net.TCPAddr).Port
  102. return srv, port
  103. }
  104. func (srv *fakeSMTPServer) record(cmd string) {
  105. srv.mu.Lock()
  106. defer srv.mu.Unlock()
  107. srv.commands = append(srv.commands, cmd)
  108. }
  109. func (srv *fakeSMTPServer) sawCommand(prefix string) bool {
  110. srv.mu.Lock()
  111. defer srv.mu.Unlock()
  112. for _, cmd := range srv.commands {
  113. if strings.HasPrefix(strings.ToUpper(cmd), prefix) {
  114. return true
  115. }
  116. }
  117. return false
  118. }
  119. func (srv *fakeSMTPServer) serve(conn net.Conn, allowStartTLS bool) {
  120. reader := bufio.NewReader(conn)
  121. writer := bufio.NewWriter(conn)
  122. writeLine := func(line string) bool {
  123. if _, err := writer.WriteString(line + "\r\n"); err != nil {
  124. return false
  125. }
  126. return writer.Flush() == nil
  127. }
  128. if !writeLine("220 fake.test ESMTP ready") {
  129. return
  130. }
  131. for {
  132. line, err := reader.ReadString('\n')
  133. if err != nil {
  134. return
  135. }
  136. cmd := strings.TrimSpace(line)
  137. srv.record(cmd)
  138. upper := strings.ToUpper(cmd)
  139. switch {
  140. case strings.HasPrefix(upper, "EHLO"), strings.HasPrefix(upper, "HELO"):
  141. ok := writeLine("250-fake.test")
  142. if allowStartTLS {
  143. ok = ok && writeLine("250-STARTTLS")
  144. }
  145. if !(ok && writeLine("250-AUTH PLAIN LOGIN") && writeLine("250 8BITMIME")) {
  146. return
  147. }
  148. case upper == "STARTTLS" && allowStartTLS:
  149. if !writeLine("220 2.0.0 ready to start TLS") {
  150. return
  151. }
  152. tlsConn := tls.Server(conn, srv.tlsConfig)
  153. if err := tlsConn.Handshake(); err != nil {
  154. return
  155. }
  156. srv.serveUpgraded(tlsConn)
  157. return
  158. case strings.HasPrefix(upper, "AUTH"):
  159. if !writeLine("235 2.7.0 authentication successful") {
  160. return
  161. }
  162. case strings.HasPrefix(upper, "MAIL"), strings.HasPrefix(upper, "RCPT"):
  163. if !writeLine("250 ok") {
  164. return
  165. }
  166. case upper == "DATA":
  167. if !writeLine("354 go ahead") {
  168. return
  169. }
  170. for {
  171. dataLine, err := reader.ReadString('\n')
  172. if err != nil {
  173. return
  174. }
  175. if strings.TrimRight(dataLine, "\r\n") == "." {
  176. break
  177. }
  178. }
  179. if !writeLine("250 message accepted") {
  180. return
  181. }
  182. case upper == "QUIT":
  183. _ = writeLine("221 bye")
  184. return
  185. default:
  186. if !writeLine("250 ok") {
  187. return
  188. }
  189. }
  190. }
  191. }
  192. // serveUpgraded 复用命令循环处理 STARTTLS 升级后的会话(升级后不再提供 STARTTLS)。
  193. func (srv *fakeSMTPServer) serveUpgraded(conn net.Conn) {
  194. reader := bufio.NewReader(conn)
  195. writer := bufio.NewWriter(conn)
  196. // net/smtp 在 StartTLS 成功后会重新发送 EHLO,直接进入命令循环即可。
  197. srv.serveCommands(reader, writer)
  198. }
  199. func (srv *fakeSMTPServer) serveCommands(reader *bufio.Reader, writer *bufio.Writer) {
  200. writeLine := func(line string) bool {
  201. if _, err := writer.WriteString(line + "\r\n"); err != nil {
  202. return false
  203. }
  204. return writer.Flush() == nil
  205. }
  206. for {
  207. line, err := reader.ReadString('\n')
  208. if err != nil {
  209. return
  210. }
  211. cmd := strings.TrimSpace(line)
  212. srv.record(cmd)
  213. upper := strings.ToUpper(cmd)
  214. switch {
  215. case strings.HasPrefix(upper, "EHLO"), strings.HasPrefix(upper, "HELO"):
  216. if !(writeLine("250-fake.test") && writeLine("250-AUTH PLAIN LOGIN") && writeLine("250 8BITMIME")) {
  217. return
  218. }
  219. case strings.HasPrefix(upper, "AUTH"):
  220. if !writeLine("235 2.7.0 authentication successful") {
  221. return
  222. }
  223. case strings.HasPrefix(upper, "MAIL"), strings.HasPrefix(upper, "RCPT"):
  224. if !writeLine("250 ok") {
  225. return
  226. }
  227. case upper == "DATA":
  228. if !writeLine("354 go ahead") {
  229. return
  230. }
  231. for {
  232. dataLine, err := reader.ReadString('\n')
  233. if err != nil {
  234. return
  235. }
  236. if strings.TrimRight(dataLine, "\r\n") == "." {
  237. break
  238. }
  239. }
  240. if !writeLine("250 message accepted") {
  241. return
  242. }
  243. case upper == "QUIT":
  244. _ = writeLine("221 bye")
  245. return
  246. default:
  247. if !writeLine("250 ok") {
  248. return
  249. }
  250. }
  251. }
  252. }
  253. func smtpTestConfig(port int, useTLS bool) *SMTPConfig {
  254. return &SMTPConfig{
  255. Host: "127.0.0.1",
  256. Port: port,
  257. Username: "user",
  258. Password: "pass",
  259. From: "noreply@example.com",
  260. FromName: "Test",
  261. UseTLS: useTLS,
  262. }
  263. }
  264. // 465 语义:UseTLS=true + 隐式 TLS 服务器,原有路径保持可用。
  265. func TestSMTPConnectionImplicitTLS(t *testing.T) {
  266. srv, port := startFakeSMTPServer(t, true, false)
  267. svc := &EmailService{}
  268. if err := svc.TestSMTPConnectionWithConfig(smtpTestConfig(port, true)); err != nil {
  269. t.Fatalf("expected implicit TLS connection to succeed, got: %v", err)
  270. }
  271. if !srv.sawCommand("EHLO") {
  272. t.Fatal("expected server to receive EHLO")
  273. }
  274. }
  275. // 587 语义(#1470/#1488 核心场景):UseTLS=true + 明文问候的 STARTTLS 服务器,
  276. // 隐式 TLS 失败后必须自动降级为强制 STARTTLS 并成功。
  277. func TestSMTPConnectionStartTLSFallbackWhenTLSEnabled(t *testing.T) {
  278. srv, port := startFakeSMTPServer(t, false, true)
  279. svc := &EmailService{}
  280. if err := svc.TestSMTPConnectionWithConfig(smtpTestConfig(port, true)); err != nil {
  281. t.Fatalf("expected STARTTLS fallback to succeed, got: %v", err)
  282. }
  283. if !srv.sawCommand("STARTTLS") {
  284. t.Fatal("expected server to receive STARTTLS command")
  285. }
  286. if got := srv.conns.Load(); got < 2 {
  287. t.Fatalf("expected implicit TLS attempt before STARTTLS fallback (>=2 connections), got %d", got)
  288. }
  289. }
  290. // UseTLS=true 但服务器不支持 STARTTLS:必须报错,且绝不能把凭据发到明文连接上。
  291. func TestSMTPConnectionMandatoryStartTLSRefusesPlaintext(t *testing.T) {
  292. srv, port := startFakeSMTPServer(t, false, false)
  293. svc := &EmailService{}
  294. err := svc.TestSMTPConnectionWithConfig(smtpTestConfig(port, true))
  295. if err == nil {
  296. t.Fatal("expected error when server does not support STARTTLS")
  297. }
  298. if !strings.Contains(err.Error(), "STARTTLS") {
  299. t.Fatalf("expected STARTTLS-related error, got: %v", err)
  300. }
  301. if srv.sawCommand("AUTH") {
  302. t.Fatal("credentials must not be sent over plaintext when TLS is required")
  303. }
  304. }
  305. // UseTLS=false + 服务器支持 STARTTLS:测试连接与发送路径一致,机会式升级后认证成功。
  306. // 这是 #1488 评论"测试连接不成功,发送测试邮件实际上能发"的回归用例。
  307. func TestSMTPConnectionOpportunisticStartTLSWhenTLSDisabled(t *testing.T) {
  308. srv, port := startFakeSMTPServer(t, false, true)
  309. svc := &EmailService{}
  310. if err := svc.TestSMTPConnectionWithConfig(smtpTestConfig(port, false)); err != nil {
  311. t.Fatalf("expected opportunistic STARTTLS test connection to succeed, got: %v", err)
  312. }
  313. if !srv.sawCommand("STARTTLS") {
  314. t.Fatal("expected test connection to upgrade via STARTTLS like the send path")
  315. }
  316. }
  317. // UseTLS=false + 服务器不支持 STARTTLS:保持明文直连(既有行为不回归)。
  318. func TestSMTPConnectionPlainWhenNoStartTLS(t *testing.T) {
  319. srv, port := startFakeSMTPServer(t, false, false)
  320. svc := &EmailService{}
  321. if err := svc.TestSMTPConnectionWithConfig(smtpTestConfig(port, false)); err != nil {
  322. t.Fatalf("expected plain connection to succeed, got: %v", err)
  323. }
  324. if srv.sawCommand("STARTTLS") {
  325. t.Fatal("did not expect STARTTLS command when server does not advertise it")
  326. }
  327. }
  328. // 发送路径全流程:UseTLS=true + STARTTLS 服务器(587 语义)完整走完 MAIL/RCPT/DATA。
  329. func TestSendEmailWithConfigStartTLSFallback(t *testing.T) {
  330. srv, port := startFakeSMTPServer(t, false, true)
  331. svc := &EmailService{}
  332. err := svc.SendEmailWithConfig(smtpTestConfig(port, true), "rcpt@example.com", "subject", "<p>body</p>")
  333. if err != nil {
  334. t.Fatalf("expected send via STARTTLS fallback to succeed, got: %v", err)
  335. }
  336. if !srv.sawCommand("STARTTLS") {
  337. t.Fatal("expected send path to upgrade via STARTTLS")
  338. }
  339. if !srv.sawCommand("DATA") {
  340. t.Fatal("expected send path to reach DATA")
  341. }
  342. }
  343. // 发送路径全流程:UseTLS=true + 隐式 TLS 服务器(465 语义)保持既有行为。
  344. func TestSendEmailWithConfigImplicitTLS(t *testing.T) {
  345. srv, port := startFakeSMTPServer(t, true, false)
  346. svc := &EmailService{}
  347. err := svc.SendEmailWithConfig(smtpTestConfig(port, true), "rcpt@example.com", "subject", "<p>body</p>")
  348. if err != nil {
  349. t.Fatalf("expected send via implicit TLS to succeed, got: %v", err)
  350. }
  351. if !srv.sawCommand("DATA") {
  352. t.Fatal("expected send path to reach DATA")
  353. }
  354. }