mirror of
https://github.com/axllent/mailpit.git
synced 2025-05-19 22:23:25 +02:00
Fix: POP3 end of file reached error (#315)
* Changed POP3 size output to show compatible size * Setting POP3 10 minutes timeout according to RFC1939 * fixed issue with unauthorized commands access, refactor * readded package description * fixes error strings should not be capitalized (ST1005)go-staticcheck
This commit is contained in:
parent
ce7dcce61c
commit
a32237e14f
@ -72,5 +72,5 @@ func getSafeArg(args []string, nr int) (string, error) {
|
||||
if nr < len(args) {
|
||||
return args[nr], nil
|
||||
}
|
||||
return "", errors.New("Out of range")
|
||||
return "", errors.New("-ERR out of range")
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -22,15 +23,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// UNAUTHORIZED state
|
||||
UNAUTHORIZED = 1
|
||||
// TRANSACTION state
|
||||
TRANSACTION = 2
|
||||
// UPDATE state
|
||||
UPDATE = 3
|
||||
AUTHORIZATION = 1
|
||||
TRANSACTION = 2
|
||||
UPDATE = 3
|
||||
)
|
||||
|
||||
// Run will start the pop3 server if enabled
|
||||
// Run will start the POP3 server if enabled
|
||||
func Run() {
|
||||
if auth.POP3Credentials == nil || config.POP3Listen == "" {
|
||||
// POP3 server is disabled without authentication
|
||||
@ -68,6 +66,7 @@ func Run() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[pop3] accept error: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
@ -84,8 +83,9 @@ type message struct {
|
||||
func handleClient(conn net.Conn) {
|
||||
var (
|
||||
user = ""
|
||||
state = 1
|
||||
toDelete = []string{}
|
||||
state = AUTHORIZATION // Start with AUTHORIZATION state
|
||||
toDelete []string // Track messages marked for deletion
|
||||
messages []message
|
||||
)
|
||||
|
||||
defer func() {
|
||||
@ -94,7 +94,7 @@ func handleClient(conn net.Conn) {
|
||||
_ = storage.DeleteMessages([]string{id})
|
||||
}
|
||||
if len(toDelete) > 0 {
|
||||
// update web UI to remove deleted messages
|
||||
// Update web UI to remove deleted messages
|
||||
websockets.Broadcast("prune", nil)
|
||||
}
|
||||
}
|
||||
@ -106,23 +106,17 @@ func handleClient(conn net.Conn) {
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
messages := []message{}
|
||||
|
||||
// State
|
||||
// 1 = Unauthorized
|
||||
// 2 = Transaction mode
|
||||
// 3 = update mode
|
||||
|
||||
logger.Log().Debugf("[pop3] connection opened by %s", conn.RemoteAddr().String())
|
||||
|
||||
// First welcome the new connection
|
||||
sendResponse(conn, "+OK Mailpit POP3 server")
|
||||
|
||||
// Set 10 minutes timeout according to RFC1939
|
||||
timeoutDuration := 600 * time.Second
|
||||
|
||||
for {
|
||||
// POP3 server enforced a timeout of 30 seconds
|
||||
if err := conn.SetDeadline(time.Now().Add(timeoutDuration)); err != nil {
|
||||
// Set read deadline
|
||||
if err := conn.SetReadDeadline(time.Now().Add(timeoutDuration)); err != nil {
|
||||
logger.Log().Errorf("[pop3] %s", err.Error())
|
||||
return
|
||||
}
|
||||
@ -130,7 +124,11 @@ func handleClient(conn net.Conn) {
|
||||
// Reads a line from the client
|
||||
rawLine, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[pop3] %s", err.Error())
|
||||
if err == io.EOF {
|
||||
logger.Log().Debugf("[pop3] client disconnected: %s", conn.RemoteAddr().String())
|
||||
} else {
|
||||
logger.Log().Errorf("[pop3] read error: %s", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -139,7 +137,8 @@ func handleClient(conn net.Conn) {
|
||||
|
||||
logger.Log().Debugf("[pop3] received: %s (%s)", strings.TrimSpace(rawLine), conn.RemoteAddr().String())
|
||||
|
||||
if cmd == "CAPA" {
|
||||
switch cmd {
|
||||
case "CAPA":
|
||||
// List our capabilities per RFC2449
|
||||
sendResponse(conn, "+OK Capability list follows")
|
||||
sendResponse(conn, "TOP")
|
||||
@ -147,178 +146,159 @@ func handleClient(conn net.Conn) {
|
||||
sendResponse(conn, "UIDL")
|
||||
sendResponse(conn, "IMPLEMENTATION Mailpit")
|
||||
sendResponse(conn, ".")
|
||||
continue
|
||||
} else if cmd == "USER" && state == UNAUTHORIZED {
|
||||
if len(args) != 1 {
|
||||
sendResponse(conn, "-ERR must supply a user")
|
||||
return
|
||||
}
|
||||
// always true - stash for PASS
|
||||
sendResponse(conn, "+OK")
|
||||
user = args[0]
|
||||
|
||||
} else if cmd == "PASS" && state == UNAUTHORIZED {
|
||||
if len(args) != 1 {
|
||||
sendResponse(conn, "-ERR must supply a password")
|
||||
return
|
||||
}
|
||||
|
||||
pass := args[0]
|
||||
if authUser(user, pass) {
|
||||
sendResponse(conn, "+OK signed in")
|
||||
messages, err = getMessages()
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[pop3] %s", err.Error())
|
||||
case "USER":
|
||||
if state == AUTHORIZATION {
|
||||
if len(args) != 1 {
|
||||
sendResponse(conn, "-ERR must supply a user")
|
||||
return
|
||||
}
|
||||
state = 2
|
||||
sendResponse(conn, "+OK")
|
||||
user = args[0]
|
||||
} else {
|
||||
sendResponse(conn, "-ERR invalid password")
|
||||
logger.Log().Warnf("[pop3] failed login: %s", user)
|
||||
sendResponse(conn, "-ERR user already specified")
|
||||
}
|
||||
case "PASS":
|
||||
if state == AUTHORIZATION {
|
||||
if len(args) != 1 {
|
||||
sendResponse(conn, "-ERR must supply a password")
|
||||
return
|
||||
}
|
||||
|
||||
} else if cmd == "STAT" && state == TRANSACTION {
|
||||
totalSize := float64(0)
|
||||
for _, m := range messages {
|
||||
totalSize = totalSize + m.Size
|
||||
pass := args[0]
|
||||
if authUser(user, pass) {
|
||||
sendResponse(conn, "+OK signed in")
|
||||
var err error
|
||||
messages, err = getMessages()
|
||||
if err != nil {
|
||||
logger.Log().Errorf("[pop3] %s", err.Error())
|
||||
}
|
||||
state = TRANSACTION
|
||||
} else {
|
||||
sendResponse(conn, "-ERR invalid password")
|
||||
logger.Log().Warnf("[pop3] failed login: %s", user)
|
||||
}
|
||||
} else {
|
||||
sendResponse(conn, "-ERR user not specified")
|
||||
}
|
||||
|
||||
sendResponse(conn, fmt.Sprintf("+OK %d %d", len(messages), int64(totalSize)))
|
||||
|
||||
} else if cmd == "LIST" && state == TRANSACTION {
|
||||
totalSize := float64(0)
|
||||
for _, m := range messages {
|
||||
totalSize = totalSize + m.Size
|
||||
case "STAT", "LIST", "UIDL", "RETR", "TOP", "NOOP", "DELE":
|
||||
if state == TRANSACTION {
|
||||
handleTransactionCommand(conn, cmd, args, messages, &toDelete)
|
||||
} else {
|
||||
sendResponse(conn, "-ERR user not authenticated")
|
||||
}
|
||||
sendData(conn, fmt.Sprintf("+OK %d messages (%d octets)", len(messages), int64(totalSize)))
|
||||
|
||||
// print all sizes
|
||||
for row, m := range messages {
|
||||
sendData(conn, fmt.Sprintf("%d %d", row+1, int64(m.Size))) // Convert Size to int64 when printing
|
||||
}
|
||||
// end
|
||||
sendData(conn, ".")
|
||||
|
||||
} else if cmd == "UIDL" && state == TRANSACTION {
|
||||
totalSize := float64(0)
|
||||
for _, m := range messages {
|
||||
totalSize = totalSize + m.Size
|
||||
}
|
||||
|
||||
sendData(conn, "+OK unique-id listing follows")
|
||||
|
||||
// print all message IDS
|
||||
for row, m := range messages {
|
||||
sendData(conn, fmt.Sprintf("%d %s", row+1, m.ID))
|
||||
}
|
||||
// end
|
||||
sendData(conn, ".")
|
||||
|
||||
} else if cmd == "RETR" && state == TRANSACTION {
|
||||
if len(args) != 1 {
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
|
||||
nr, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
|
||||
if nr < 1 || nr > len(messages) {
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
|
||||
m := messages[nr-1]
|
||||
raw, err := storage.GetMessageRaw(m.ID)
|
||||
if err != nil {
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
|
||||
size := len(raw)
|
||||
sendData(conn, fmt.Sprintf("+OK %d octets", size))
|
||||
|
||||
// When all lines of the response have been sent, a
|
||||
// final line is sent, consisting of a termination octet (decimal code
|
||||
// 046, ".") and a CRLF pair. If any line of the multi-line response
|
||||
// begins with the termination octet, the line is "byte-stuffed" by
|
||||
// pre-pending the termination octet to that line of the response.
|
||||
// @see: https://www.ietf.org/rfc/rfc1939.txt
|
||||
sendData(conn, strings.Replace(string(raw), "\n.", "\n..", -1))
|
||||
sendData(conn, ".")
|
||||
|
||||
} else if cmd == "TOP" && state == TRANSACTION {
|
||||
arg, err := getSafeArg(args, 0)
|
||||
if err != nil {
|
||||
sendResponse(conn, "-ERR TOP requires two arguments")
|
||||
return
|
||||
}
|
||||
nr, err := strconv.Atoi(arg)
|
||||
if err != nil {
|
||||
sendResponse(conn, "-ERR TOP requires two arguments")
|
||||
return
|
||||
}
|
||||
|
||||
if nr < 1 || nr > len(messages) {
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
arg2, err := getSafeArg(args, 1)
|
||||
if err != nil {
|
||||
sendResponse(conn, "-ERR TOP requires two arguments")
|
||||
return
|
||||
}
|
||||
|
||||
lines, err := strconv.Atoi(arg2)
|
||||
if err != nil {
|
||||
sendResponse(conn, "-ERR TOP requires two arguments")
|
||||
return
|
||||
}
|
||||
|
||||
m := messages[nr-1]
|
||||
headers, body, err := getTop(m.ID, lines)
|
||||
if err != nil {
|
||||
sendResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
sendData(conn, "+OK Top of message follows")
|
||||
sendData(conn, headers+"\r\n")
|
||||
sendData(conn, body)
|
||||
sendData(conn, ".")
|
||||
|
||||
} else if cmd == "NOOP" && state == TRANSACTION {
|
||||
sendData(conn, "+OK")
|
||||
} else if cmd == "DELE" && state == TRANSACTION {
|
||||
arg, _ := getSafeArg(args, 0)
|
||||
nr, err := strconv.Atoi(arg)
|
||||
if err != nil {
|
||||
logger.Log().Warnf("[pop3] -ERR invalid DELETE integer: %s", arg)
|
||||
sendResponse(conn, "-ERR invalid integer")
|
||||
return
|
||||
}
|
||||
|
||||
if nr < 1 || nr > len(messages) {
|
||||
logger.Log().Warnf("[pop3] -ERR no such message")
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
toDelete = append(toDelete, messages[nr-1].ID)
|
||||
|
||||
sendResponse(conn, "+OK")
|
||||
|
||||
} else if cmd == "RSET" && state == TRANSACTION {
|
||||
toDelete = []string{}
|
||||
sendData(conn, "+OK")
|
||||
|
||||
} else if cmd == "QUIT" {
|
||||
case "QUIT":
|
||||
sendResponse(conn, "+OK Goodbye")
|
||||
state = UPDATE
|
||||
return
|
||||
} else {
|
||||
logger.Log().Warnf("[pop3] -ERR %s not implemented", cmd)
|
||||
sendResponse(conn, fmt.Sprintf("-ERR %s not implemented", cmd))
|
||||
default:
|
||||
sendResponse(conn, "-ERR unknown command")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleTransactionCommand(conn net.Conn, cmd string, args []string, messages []message, toDelete *[]string) {
|
||||
switch cmd {
|
||||
case "STAT":
|
||||
totalSize := float64(0)
|
||||
for _, m := range messages {
|
||||
totalSize += m.Size
|
||||
}
|
||||
sendResponse(conn, fmt.Sprintf("+OK %d %d", len(messages), int64(totalSize)))
|
||||
case "LIST":
|
||||
totalSize := float64(0)
|
||||
for _, m := range messages {
|
||||
totalSize += m.Size
|
||||
}
|
||||
sendResponse(conn, fmt.Sprintf("+OK %d messages (%d octets)", len(messages), int64(totalSize)))
|
||||
|
||||
for row, m := range messages {
|
||||
sendResponse(conn, fmt.Sprintf("%d %d", row+1, int64(m.Size))) // Convert Size to int64 when printing
|
||||
}
|
||||
sendResponse(conn, ".")
|
||||
case "UIDL":
|
||||
sendResponse(conn, "+OK unique-id listing follows")
|
||||
for row, m := range messages {
|
||||
sendResponse(conn, fmt.Sprintf("%d %s", row+1, m.ID))
|
||||
}
|
||||
sendResponse(conn, ".")
|
||||
case "RETR":
|
||||
if len(args) != 1 {
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
|
||||
nr, err := strconv.Atoi(args[0])
|
||||
if err != nil || nr < 1 || nr > len(messages) {
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
|
||||
m := messages[nr-1]
|
||||
raw, err := storage.GetMessageRaw(m.ID)
|
||||
if err != nil {
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
size := len(raw)
|
||||
sendResponse(conn, fmt.Sprintf("+OK %d octets", size))
|
||||
|
||||
// When all lines of the response have been sent, a
|
||||
// final line is sent, consisting of a termination octet (decimal code
|
||||
// 046, ".") and a CRLF pair. If any line of the multi-line response
|
||||
// begins with the termination octet, the line is "byte-stuffed" by
|
||||
// pre-pending the termination octet to that line of the response.
|
||||
// @see: https://www.ietf.org/rfc/rfc1939.txt
|
||||
sendData(conn, strings.Replace(string(raw), "\n.", "\n..", -1))
|
||||
sendResponse(conn, ".")
|
||||
case "TOP":
|
||||
arg, err := getSafeArg(args, 0)
|
||||
if err != nil {
|
||||
sendResponse(conn, "-ERR TOP requires two arguments")
|
||||
return
|
||||
}
|
||||
nr, err := strconv.Atoi(arg)
|
||||
if err != nil || nr < 1 || nr > len(messages) {
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
|
||||
arg2, err := getSafeArg(args, 1)
|
||||
if err != nil {
|
||||
sendResponse(conn, "-ERR TOP requires two arguments")
|
||||
return
|
||||
}
|
||||
|
||||
lines, err := strconv.Atoi(arg2)
|
||||
if err != nil {
|
||||
sendResponse(conn, "-ERR TOP requires two arguments")
|
||||
return
|
||||
}
|
||||
|
||||
m := messages[nr-1]
|
||||
headers, body, err := getTop(m.ID, lines)
|
||||
if err != nil {
|
||||
sendResponse(conn, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
sendResponse(conn, "+OK Top of message follows")
|
||||
sendData(conn, headers+"\r\n")
|
||||
sendData(conn, body)
|
||||
sendResponse(conn, ".")
|
||||
case "NOOP":
|
||||
sendResponse(conn, "+OK")
|
||||
case "DELE":
|
||||
arg, _ := getSafeArg(args, 0)
|
||||
nr, err := strconv.Atoi(arg)
|
||||
if err != nil || nr < 1 || nr > len(messages) {
|
||||
sendResponse(conn, "-ERR no such message")
|
||||
return
|
||||
}
|
||||
|
||||
m := messages[nr-1]
|
||||
*toDelete = append(*toDelete, m.ID)
|
||||
sendResponse(conn, "+OK message marked for deletion")
|
||||
default:
|
||||
sendResponse(conn, "-ERR unknown command")
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user