Files
sub2api/backend/internal/service/email_service_smtp_test.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

384 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//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 服务器:
// 隐式 TLS465 语义)、明文+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")
}
}