From e0baf27cc33ce12967db11b13592daa2d1a42802 Mon Sep 17 00:00:00 2001 From: smail Date: Sun, 20 Sep 2026 02:48:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DPOP3=E6=9C=AA=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E9=80=80=E5=87=BA=E6=97=A0=E5=93=8D=E5=BA=94=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=E5=85=85=E8=BF=9E=E6=8E=A5=E5=85=B3=E9=97=AD=E5=9B=9E?= =?UTF-8?q?=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/gopop/pop3.go | 6 +- backend/internal/gopop/pop3_security_test.go | 65 +++++++++++++++++++- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/backend/internal/gopop/pop3.go b/backend/internal/gopop/pop3.go index ffe7ac6..829145c 100644 --- a/backend/internal/gopop/pop3.go +++ b/backend/internal/gopop/pop3.go @@ -329,11 +329,11 @@ func (s *Server) handleClient(conn net.Conn) { if session.Status == TRANSACTION { if err := s.Action.Quit(session); err != nil { fmt.Fprintf(conn, "-ERR mailbox update failed %s", eol) - } else { - fmt.Fprintf(conn, "+OK Bye %s", eol) + return } - return } + fmt.Fprintf(conn, "+OK Bye %s", eol) + return case "NOOP": fmt.Fprintf(conn, "+OK%s", eol) case "STLS": diff --git a/backend/internal/gopop/pop3_security_test.go b/backend/internal/gopop/pop3_security_test.go index cbf1ace..b9c8731 100644 --- a/backend/internal/gopop/pop3_security_test.go +++ b/backend/internal/gopop/pop3_security_test.go @@ -10,6 +10,7 @@ import ( "crypto/x509/pkix" "errors" "fmt" + "io" "math/big" "net" "strings" @@ -19,6 +20,7 @@ import ( type securityTestAction struct { quitErr error + quitCalls int passErrors []error } @@ -59,7 +61,10 @@ func (a *securityTestAction) Top(session *Session, id int64, n int) (string, err return "", nil } func (a *securityTestAction) Noop(session *Session) error { return nil } -func (a *securityTestAction) Quit(session *Session) error { return a.quitErr } +func (a *securityTestAction) Quit(session *Session) error { + a.quitCalls++ + return a.quitErr +} func (a *securityTestAction) Capa(session *Session) ([]string, error) { return nil, nil } @@ -83,7 +88,8 @@ func TestQuitReportsMailboxUpdateResult(t *testing.T) { if err := clientConn.SetDeadline(time.Now().Add(2 * time.Second)); err != nil { t.Fatal(err) } - server := &Server{Domain: "test", Action: &securityTestAction{quitErr: test.quitErr}} + action := &securityTestAction{quitErr: test.quitErr} + server := &Server{Domain: "test", Action: action} done := make(chan struct{}) go func() { server.handleClient(serverConn) @@ -104,6 +110,61 @@ func TestQuitReportsMailboxUpdateResult(t *testing.T) { case <-time.After(2 * time.Second): t.Fatal("server did not close the connection after QUIT") } + if action.quitCalls != 1 { + t.Fatalf("邮箱更新次数错误:%d", action.quitCalls) + } + }) + } +} + +func TestQuitBeforeAuthenticationClosesWithoutMailboxUpdate(t *testing.T) { + tests := []struct { + name string + commands []string + passErr error + }{ + {name: "initial"}, + {name: "after_user", commands: []string{"USER user"}}, + {name: "failed_password", commands: []string{"USER user", "PASS incorrect"}, passErr: errors.New("invalid password")}, + {name: "temporary_failure", commands: []string{"USER user", "PASS password"}, passErr: ErrAuthenticationTemporary}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + serverConn, clientConn := net.Pipe() + t.Cleanup(func() { _ = clientConn.Close() }) + if err := clientConn.SetDeadline(time.Now().Add(2 * time.Second)); err != nil { + t.Fatal(err) + } + action := &securityTestAction{passErrors: []error{test.passErr}} + server := &Server{Domain: "test", Action: action} + done := make(chan struct{}) + go func() { + server.handleClient(serverConn) + close(done) + }() + reader := bufio.NewReader(clientConn) + readResponseLine(t, reader, "+OK") + for _, command := range test.commands { + writeCommand(t, clientConn, command) + if strings.HasPrefix(command, "PASS ") { + readResponseLine(t, reader, "-ERR") + } else { + readResponseLine(t, reader, "+OK") + } + } + writeCommand(t, clientConn, "QUIT") + readResponseLine(t, reader, "+OK Bye") + if _, err := reader.ReadByte(); !errors.Is(err, io.EOF) { + t.Fatalf("退出后连接未关闭:%v", err) + } + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("未认证连接退出超时") + } + if action.quitCalls != 0 { + t.Fatalf("未认证退出不应提交邮箱更新:%d", action.quitCalls) + } }) } }