修复部分功能
This commit is contained in:
+150
-129
@@ -26,10 +26,18 @@ type POP3Server struct {
|
||||
|
||||
// NewPOP3Server creates a new POP3 server instance.
|
||||
func NewPOP3Server(cfg config.POP3Config, stores *store.Stores) *POP3Server {
|
||||
return &POP3Server{
|
||||
stores: stores,
|
||||
cfg: cfg,
|
||||
return &POP3Server{stores: stores, cfg: cfg}
|
||||
}
|
||||
|
||||
func (s *POP3Server) tlsConfig() (*tls.Config, error) {
|
||||
if s.cfg.TLSCert == "" || s.cfg.TLSKey == "" {
|
||||
return nil, fmt.Errorf("POP3 TLS certificate or key not configured")
|
||||
}
|
||||
cert, err := tls.LoadX509KeyPair(s.cfg.TLSCert, s.cfg.TLSKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load POP3 TLS certificate failed: %w", err)
|
||||
}
|
||||
return &tls.Config{Certificates: []tls.Certificate{cert}}, nil
|
||||
}
|
||||
|
||||
// Start starts the POP3 server on the configured plain-text port.
|
||||
@@ -48,7 +56,6 @@ func (s *POP3Server) Start() error {
|
||||
for {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
// Listener closed
|
||||
return
|
||||
}
|
||||
s.wg.Add(1)
|
||||
@@ -64,16 +71,12 @@ func (s *POP3Server) Start() error {
|
||||
|
||||
// StartTLS starts the POP3 server on the configured TLS port.
|
||||
func (s *POP3Server) StartTLS() error {
|
||||
if s.cfg.TLSCert == "" || s.cfg.TLSKey == "" {
|
||||
return fmt.Errorf("POP3 TLS certificate or key not configured")
|
||||
}
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(s.cfg.TLSCert, s.cfg.TLSKey)
|
||||
tlsConfig, err := s.tlsConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load POP3 TLS certificate failed: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
listener, err := tls.Listen("tcp", s.cfg.TLSAddr, &tls.Config{Certificates: []tls.Certificate{cert}})
|
||||
listener, err := tls.Listen("tcp", s.cfg.TLSAddr, tlsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("POP3 TLS listen failed: %w", err)
|
||||
}
|
||||
@@ -102,25 +105,20 @@ func (s *POP3Server) StartTLS() error {
|
||||
// handleConn handles a single POP3 client connection.
|
||||
func (s *POP3Server) handleConn(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
// Set read/write deadlines
|
||||
conn.SetDeadline(time.Now().Add(10 * time.Minute))
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
// Session state
|
||||
var user *db.User
|
||||
var messages []pop3Message
|
||||
var deleted map[int]bool
|
||||
tlsActive := false
|
||||
|
||||
// Send greeting
|
||||
sendResponse(conn, "+OK MailGo POP3 server ready")
|
||||
|
||||
// Main command loop
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return // connection closed or error
|
||||
return
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
@@ -128,7 +126,6 @@ func (s *POP3Server) handleConn(conn net.Conn) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse command
|
||||
parts := strings.SplitN(line, " ", 2)
|
||||
cmd := strings.ToUpper(parts[0])
|
||||
arg := ""
|
||||
@@ -136,6 +133,12 @@ func (s *POP3Server) handleConn(conn net.Conn) {
|
||||
arg = strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
authenticated := user != nil && user.ID != 0
|
||||
if !authenticated && requiresAuth(cmd) {
|
||||
sendResponse(conn, "-ERR authentication required")
|
||||
continue
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case "USER":
|
||||
user, messages, deleted = s.handleUSER(conn, arg, user)
|
||||
@@ -148,21 +151,40 @@ func (s *POP3Server) handleConn(conn net.Conn) {
|
||||
case "RETR":
|
||||
s.handleRETR(conn, arg, messages, deleted)
|
||||
case "DELE":
|
||||
s.handleDELE(conn, arg, deleted)
|
||||
s.handleDELE(conn, arg, messages, deleted)
|
||||
case "NOOP":
|
||||
sendResponse(conn, "+OK")
|
||||
case "RSET":
|
||||
deleted = make(map[int]bool)
|
||||
sendResponse(conn, "+OK")
|
||||
case "QUIT":
|
||||
// In UPDATE state, actually delete marked messages
|
||||
s.expungeDeleted(messages, deleted, user)
|
||||
sendResponse(conn, "+OK MailGo POP3 server signing off")
|
||||
return
|
||||
case "CAPA":
|
||||
sendResponse(conn, "+OK Capability list follows")
|
||||
sendResponse(conn, "USER")
|
||||
sendResponse(conn, ".")
|
||||
s.handleCAPA(conn, tlsActive)
|
||||
case "STLS":
|
||||
if authenticated {
|
||||
sendResponse(conn, "-ERR STLS not allowed after authentication")
|
||||
continue
|
||||
}
|
||||
if tlsActive {
|
||||
sendResponse(conn, "-ERR TLS already active")
|
||||
continue
|
||||
}
|
||||
tlsConfig, err := s.tlsConfig()
|
||||
if err != nil {
|
||||
sendResponse(conn, "-ERR TLS not available")
|
||||
continue
|
||||
}
|
||||
sendResponse(conn, "+OK Begin TLS negotiation")
|
||||
tlsConn := tls.Server(conn, tlsConfig)
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
return
|
||||
}
|
||||
conn = tlsConn
|
||||
reader = bufio.NewReader(conn)
|
||||
tlsActive = true
|
||||
case "TOP":
|
||||
s.handleTOP(conn, arg, messages, deleted)
|
||||
case "UIDL":
|
||||
@@ -173,6 +195,27 @@ func (s *POP3Server) handleConn(conn net.Conn) {
|
||||
}
|
||||
}
|
||||
|
||||
func requiresAuth(cmd string) bool {
|
||||
switch cmd {
|
||||
case "STAT", "LIST", "RETR", "DELE", "RSET", "TOP", "UIDL":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *POP3Server) handleCAPA(conn net.Conn, tlsActive bool) {
|
||||
sendResponse(conn, "+OK Capability list follows")
|
||||
sendResponse(conn, "USER")
|
||||
sendResponse(conn, "TOP")
|
||||
sendResponse(conn, "UIDL")
|
||||
sendResponse(conn, "RESP-CODES")
|
||||
if !tlsActive && s.cfg.TLSCert != "" && s.cfg.TLSKey != "" {
|
||||
sendResponse(conn, "STLS")
|
||||
}
|
||||
sendResponse(conn, ".")
|
||||
}
|
||||
|
||||
// pop3Message holds a message and its computed size for POP3.
|
||||
type pop3Message struct {
|
||||
id uint
|
||||
@@ -198,7 +241,7 @@ func (s *POP3Server) loadMessages(user *db.User) []pop3Message {
|
||||
msgs = append(msgs, pop3Message{
|
||||
id: dbMsgs[i].ID,
|
||||
raw: raw,
|
||||
size: len(raw),
|
||||
size: len(normalizePOP3Data(raw)),
|
||||
message: &dbMsgs[i],
|
||||
})
|
||||
}
|
||||
@@ -212,12 +255,9 @@ func (s *POP3Server) handleUSER(conn net.Conn, username string, currentUser *db.
|
||||
return currentUser, nil, nil
|
||||
}
|
||||
|
||||
// Look up the user by email
|
||||
user, err := s.stores.Users.GetByEmail(username)
|
||||
if err != nil {
|
||||
// Don't reveal whether user exists yet (standard POP3 behavior)
|
||||
sendResponse(conn, "+OK")
|
||||
// Store the attempted username for PASS to use
|
||||
return &db.User{Username: username}, nil, nil
|
||||
}
|
||||
|
||||
@@ -232,23 +272,14 @@ func (s *POP3Server) handlePASS(conn net.Conn, password string, user *db.User) (
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// Authenticate
|
||||
authUser, err := s.stores.Users.Authenticate(user.Username, password)
|
||||
if err != nil {
|
||||
// If username was an email, try using it directly
|
||||
if strings.Contains(user.Username, "@") {
|
||||
authUser, err = s.stores.Users.Authenticate(user.Username, password)
|
||||
}
|
||||
if err != nil {
|
||||
sendResponse(conn, "-ERR authentication failed")
|
||||
return nil, nil, nil
|
||||
}
|
||||
sendResponse(conn, "-ERR authentication failed")
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// Load messages
|
||||
messages := s.loadMessages(authUser)
|
||||
deleted := make(map[int]bool)
|
||||
|
||||
sendResponse(conn, fmt.Sprintf("+OK authenticated, %d messages", len(messages)))
|
||||
return authUser, messages, deleted
|
||||
}
|
||||
@@ -269,7 +300,6 @@ func (s *POP3Server) handleSTAT(conn net.Conn, messages []pop3Message, deleted m
|
||||
// handleLIST processes the LIST command (with optional message number).
|
||||
func (s *POP3Server) handleLIST(conn net.Conn, arg string, messages []pop3Message, deleted map[int]bool) {
|
||||
if arg == "" {
|
||||
// List all messages
|
||||
sendResponse(conn, "+OK message list follows")
|
||||
for i, msg := range messages {
|
||||
if !deleted[i+1] {
|
||||
@@ -277,19 +307,19 @@ func (s *POP3Server) handleLIST(conn net.Conn, arg string, messages []pop3Messag
|
||||
}
|
||||
}
|
||||
sendResponse(conn, ".")
|
||||
} else {
|
||||
// List specific message
|
||||
num, err := strconv.Atoi(arg)
|
||||
if err != nil || num < 1 || num > len(messages) {
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
if deleted[num] {
|
||||
sendResponse(conn, "-ERR message deleted")
|
||||
return
|
||||
}
|
||||
sendResponse(conn, fmt.Sprintf("+OK %d %d", num, messages[num-1].size))
|
||||
return
|
||||
}
|
||||
|
||||
num, err := strconv.Atoi(arg)
|
||||
if err != nil || num < 1 || num > len(messages) {
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
if deleted[num] {
|
||||
sendResponse(conn, "-ERR message deleted")
|
||||
return
|
||||
}
|
||||
sendResponse(conn, fmt.Sprintf("+OK %d %d", num, messages[num-1].size))
|
||||
}
|
||||
|
||||
// handleRETR processes the RETR command.
|
||||
@@ -306,29 +336,17 @@ func (s *POP3Server) handleRETR(conn net.Conn, arg string, messages []pop3Messag
|
||||
|
||||
msg := messages[num-1]
|
||||
sendResponse(conn, fmt.Sprintf("+OK %d octets", msg.size))
|
||||
|
||||
// Send the raw message, dot-stuffing any lines that start with "."
|
||||
scanner := bufio.NewScanner(strings.NewReader(msg.raw))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, ".") {
|
||||
conn.Write([]byte("."))
|
||||
}
|
||||
conn.Write([]byte(line + "\r\n"))
|
||||
}
|
||||
writeDotStuffed(conn, msg.raw, nil)
|
||||
conn.Write([]byte(".\r\n"))
|
||||
}
|
||||
|
||||
// handleDELE processes the DELE command.
|
||||
func (s *POP3Server) handleDELE(conn net.Conn, arg string, deleted map[int]bool) {
|
||||
func (s *POP3Server) handleDELE(conn net.Conn, arg string, messages []pop3Message, deleted map[int]bool) {
|
||||
num, err := strconv.Atoi(arg)
|
||||
if err != nil || num < 1 {
|
||||
sendResponse(conn, "-ERR invalid message number")
|
||||
if err != nil || num < 1 || num > len(messages) {
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
if deleted == nil {
|
||||
deleted = make(map[int]bool)
|
||||
}
|
||||
if deleted[num] {
|
||||
sendResponse(conn, "-ERR message already deleted")
|
||||
return
|
||||
@@ -339,7 +357,6 @@ func (s *POP3Server) handleDELE(conn net.Conn, arg string, deleted map[int]bool)
|
||||
|
||||
// handleTOP processes the TOP command (headers + first N lines of body).
|
||||
func (s *POP3Server) handleTOP(conn net.Conn, arg string, messages []pop3Message, deleted map[int]bool) {
|
||||
// Format: TOP msgNum [n]
|
||||
parts := strings.Fields(arg)
|
||||
if len(parts) < 1 {
|
||||
sendResponse(conn, "-ERR syntax error")
|
||||
@@ -351,7 +368,7 @@ func (s *POP3Server) handleTOP(conn net.Conn, arg string, messages []pop3Message
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
if deleted != nil && deleted[num] {
|
||||
if deleted[num] {
|
||||
sendResponse(conn, "-ERR message deleted")
|
||||
return
|
||||
}
|
||||
@@ -362,79 +379,42 @@ func (s *POP3Server) handleTOP(conn net.Conn, arg string, messages []pop3Message
|
||||
}
|
||||
|
||||
msg := messages[num-1]
|
||||
header, body := splitHeaderBody(msg.raw)
|
||||
sendResponse(conn, "+OK top of message follows")
|
||||
|
||||
// Find blank line separating headers from body
|
||||
headerEnd := strings.Index(msg.raw, "\r\n\r\n")
|
||||
if headerEnd == -1 {
|
||||
headerEnd = strings.Index(msg.raw, "\n\n")
|
||||
if headerEnd == -1 {
|
||||
// No body, just send everything
|
||||
sendResponse(conn, msg.raw)
|
||||
sendResponse(conn, ".")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Send headers
|
||||
header := msg.raw[:headerEnd]
|
||||
scanner := bufio.NewScanner(strings.NewReader(header))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, ".") {
|
||||
conn.Write([]byte("."))
|
||||
}
|
||||
conn.Write([]byte(line + "\r\n"))
|
||||
}
|
||||
writeDotStuffed(conn, header, nil)
|
||||
conn.Write([]byte("\r\n"))
|
||||
|
||||
// Send up to N lines of body
|
||||
if nLines > 0 {
|
||||
body := msg.raw[headerEnd:]
|
||||
scanner := bufio.NewScanner(strings.NewReader(body))
|
||||
lineCount := 0
|
||||
for scanner.Scan() && lineCount < nLines {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, ".") {
|
||||
conn.Write([]byte("."))
|
||||
}
|
||||
conn.Write([]byte(line + "\r\n"))
|
||||
lineCount++
|
||||
}
|
||||
}
|
||||
|
||||
writeDotStuffed(conn, body, &nLines)
|
||||
conn.Write([]byte(".\r\n"))
|
||||
}
|
||||
|
||||
// handleUIDL processes the UIDL command.
|
||||
func (s *POP3Server) handleUIDL(conn net.Conn, arg string, messages []pop3Message, deleted map[int]bool) {
|
||||
if arg == "" {
|
||||
// List all UIDs
|
||||
sendResponse(conn, "+OK UIDL list follows")
|
||||
for i, msg := range messages {
|
||||
if deleted == nil || !deleted[i+1] {
|
||||
if !deleted[i+1] {
|
||||
sendResponse(conn, fmt.Sprintf("%d %d", i+1, msg.id))
|
||||
}
|
||||
}
|
||||
sendResponse(conn, ".")
|
||||
} else {
|
||||
// Specific message UID
|
||||
num, err := strconv.Atoi(arg)
|
||||
if err != nil || num < 1 || num > len(messages) {
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
if deleted != nil && deleted[num] {
|
||||
sendResponse(conn, "-ERR message deleted")
|
||||
return
|
||||
}
|
||||
sendResponse(conn, fmt.Sprintf("+OK %d %d", num, messages[num-1].id))
|
||||
return
|
||||
}
|
||||
|
||||
num, err := strconv.Atoi(arg)
|
||||
if err != nil || num < 1 || num > len(messages) {
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
if deleted[num] {
|
||||
sendResponse(conn, "-ERR message deleted")
|
||||
return
|
||||
}
|
||||
sendResponse(conn, fmt.Sprintf("+OK %d %d", num, messages[num-1].id))
|
||||
}
|
||||
|
||||
// expungeDeleted actually deletes messages that were marked for deletion.
|
||||
func (s *POP3Server) expungeDeleted(messages []pop3Message, deleted map[int]bool, user *db.User) {
|
||||
if deleted == nil || user == nil {
|
||||
if deleted == nil || user == nil || user.ID == 0 {
|
||||
return
|
||||
}
|
||||
for seqNum, msgDeleted := range deleted {
|
||||
@@ -451,11 +431,53 @@ func sendResponse(conn net.Conn, line string) {
|
||||
conn.Write([]byte(line + "\r\n"))
|
||||
}
|
||||
|
||||
// buildRawMessage reconstructs a raw RFC822 message from a db.Message.
|
||||
// This is a local copy to avoid importing from the imap_server package.
|
||||
func buildRawMessage(msg *db.Message) []byte {
|
||||
var buf strings.Builder
|
||||
func normalizePOP3Data(raw string) string {
|
||||
var b strings.Builder
|
||||
writeDotStuffed(&b, raw, nil)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type stringWriter interface {
|
||||
Write([]byte) (int, error)
|
||||
}
|
||||
|
||||
func writeDotStuffed(w stringWriter, raw string, maxLines *int) {
|
||||
raw = strings.ReplaceAll(raw, "\r\n", "\n")
|
||||
raw = strings.ReplaceAll(raw, "\r", "\n")
|
||||
lines := strings.Split(raw, "\n")
|
||||
written := 0
|
||||
for i, line := range lines {
|
||||
if maxLines != nil && written >= *maxLines {
|
||||
break
|
||||
}
|
||||
if i == len(lines)-1 && line == "" {
|
||||
break
|
||||
}
|
||||
if strings.HasPrefix(line, ".") {
|
||||
w.Write([]byte("."))
|
||||
}
|
||||
w.Write([]byte(line + "\r\n"))
|
||||
written++
|
||||
}
|
||||
}
|
||||
|
||||
func splitHeaderBody(raw string) (string, string) {
|
||||
if idx := strings.Index(raw, "\r\n\r\n"); idx >= 0 {
|
||||
return raw[:idx], raw[idx+4:]
|
||||
}
|
||||
if idx := strings.Index(raw, "\n\n"); idx >= 0 {
|
||||
return raw[:idx], raw[idx+2:]
|
||||
}
|
||||
return raw, ""
|
||||
}
|
||||
|
||||
// buildRawMessage reconstructs a raw RFC822 message from a db.Message.
|
||||
func buildRawMessage(msg *db.Message) []byte {
|
||||
if msg.RawData != "" {
|
||||
return []byte(msg.RawData)
|
||||
}
|
||||
|
||||
var buf strings.Builder
|
||||
buf.WriteString(fmt.Sprintf("From: %s\r\n", msg.FromAddr))
|
||||
buf.WriteString(fmt.Sprintf("To: %s\r\n", msg.ToAddr))
|
||||
if msg.CcAddr != "" {
|
||||
@@ -490,6 +512,5 @@ func buildRawMessage(msg *db.Message) []byte {
|
||||
} else {
|
||||
buf.WriteString("Content-Type: text/plain; charset=utf-8\r\n\r\n")
|
||||
}
|
||||
|
||||
return []byte(buf.String())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user