From bf2d7b6e2d953b94ba9e50484235218c28540d66 Mon Sep 17 00:00:00 2001 From: smail Date: Sun, 6 Sep 2026 05:38:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DPOP3=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E5=92=8C=E5=88=A0=E9=99=A4=E8=A1=8C=E4=B8=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/gopop/pop3.go | 8 +- backend/internal/gopop/session.go | 16 +- backend/listen/pop3_server/action.go | 209 ++++++++++++++++++--------- 3 files changed, 157 insertions(+), 76 deletions(-) diff --git a/backend/internal/gopop/pop3.go b/backend/internal/gopop/pop3.go index fd05019..2755aa1 100644 --- a/backend/internal/gopop/pop3.go +++ b/backend/internal/gopop/pop3.go @@ -234,6 +234,8 @@ func (s *Server) handleClient(conn net.Conn) { infos, err := s.Action.List(session, getSafeArg(args, 0)) if err != nil { fmt.Fprintf(conn, "-ERR %s %s", err.Error(), eol) + } else if len(args) > 0 && len(infos) == 1 { + fmt.Fprintf(conn, "+OK %d %d%s", infos[0].Id, infos[0].Size, eol) } else { ret := "+OK" + eol for _, info := range infos { @@ -248,6 +250,8 @@ func (s *Server) handleClient(conn net.Conn) { infos, err := s.Action.Uidl(session, getSafeArg(args, 0)) if err != nil { fmt.Fprintf(conn, "-ERR %s %s", err.Error(), eol) + } else if len(args) > 0 && len(infos) == 1 { + fmt.Fprintf(conn, "+OK %d %s%s", infos[0].Id, infos[0].UnionId, eol) } else { ret := fmt.Sprintf("+OK%s", eol) for _, info := range infos { @@ -261,7 +265,7 @@ func (s *Server) handleClient(conn net.Conn) { if session.Status == TRANSACTION { id, err1 := strconv.ParseInt(getSafeArg(args, 0), 10, 64) line, err2 := strconv.Atoi(getSafeArg(args, 1)) - if err1 != nil || err2 != nil { + if err1 != nil || err2 != nil || line < 0 { fmt.Fprintf(conn, "-ERR %s %s", "params error", eol) } else { res, err := s.Action.Top(session, id, line) @@ -306,7 +310,7 @@ func (s *Server) handleClient(conn net.Conn) { } } } - case "REST": + case "RSET", "REST": if session.Status == TRANSACTION { err := s.Action.Rest(session) if err != nil { diff --git a/backend/internal/gopop/session.go b/backend/internal/gopop/session.go index b832049..b88ccce 100644 --- a/backend/internal/gopop/session.go +++ b/backend/internal/gopop/session.go @@ -15,11 +15,13 @@ const ( ) type Session struct { - Status Status - User string - DeleteIds []int64 - Ctx context.Context - Conn net.Conn - InTls bool - AliveTime time.Time + Status Status + User string + DeleteIds []int64 + MessageIDs []int64 + MessageMap map[int64]int64 + Ctx context.Context + Conn net.Conn + InTls bool + AliveTime time.Time } diff --git a/backend/listen/pop3_server/action.go b/backend/listen/pop3_server/action.go index de4b717..985cc1a 100644 --- a/backend/listen/pop3_server/action.go +++ b/backend/listen/pop3_server/action.go @@ -5,6 +5,7 @@ import ( "git.pcux.cn/xuans/Smail/consts" "git.pcux.cn/xuans/Smail/dto" "git.pcux.cn/xuans/Smail/dto/parsemail" + "git.pcux.cn/xuans/Smail/dto/response" "git.pcux.cn/xuans/Smail/internal/gopop" "git.pcux.cn/xuans/Smail/services/access" "git.pcux.cn/xuans/Smail/services/auth" @@ -17,6 +18,7 @@ import ( "git.pcux.cn/xuans/Smail/utils/id" log "github.com/sirupsen/logrus" "github.com/spf13/cast" + "strconv" "strings" ) @@ -129,52 +131,41 @@ func (a action) Apop(session *gopop.Session, username, digest string) error { } func (a action) Stat(session *gopop.Session) (msgNum, msgSize int64, err error) { log.WithContext(session.Ctx).Debugf("POP3 CMD: STAT") - - num, size, err := list.Stat(session.Ctx.(*context.Context)) + emailList, err := a.visibleMessages(session) if err != nil { return 0, 0, err } - log.WithContext(session.Ctx).Debugf("POP3 STAT RETURT : %d,%d", num, size) - return num, size, nil + for _, info := range emailList { + msgNum++ + msgSize += int64(info.Size) + } + log.WithContext(session.Ctx).Debugf("POP3 STAT RETURT : %d,%d", msgNum, msgSize) + return msgNum, msgSize, nil } // Uidl 查询某封邮件的唯一标志符 func (a action) Uidl(session *gopop.Session, msg string) ([]gopop.UidlItem, error) { log.WithContext(session.Ctx).Debugf("POP3 CMD: UIDL ,Args:%s", msg) - reqId := cast.ToInt64(msg) - if reqId > 0 { - info, err := detail.GetEmailDetail(session.Ctx.(*context.Context), cast.ToInt(reqId), false) - if err != nil || info == nil { - return nil, errors2.New("no such message") + if msg != "" { + number, err := parseMessageNumber(msg) + if err != nil { + return nil, err } - log.WithContext(session.Ctx).Debugf("Uidl \n %+v", reqId) - return []gopop.UidlItem{ - { - Id: reqId, - UnionId: msg, - }, - }, nil + id, err := a.resolveMessageID(session, int64(number)) + if err != nil { + return nil, err + } + return []gopop.UidlItem{{Id: int64(number), UnionId: cast.ToString(id)}}, nil } - var res []listItem - - emailList, _, err := list.GetEmailList(session.Ctx.(*context.Context), dto.SearchTag{Type: consts.EmailTypeReceive, Status: -1, GroupId: -1}, "", true, 0, 99999) + emailList, err := a.visibleMessages(session) if err != nil { return nil, err } - for _, info := range emailList { - res = append(res, listItem{ - Id: cast.ToInt64(info.Id), - Size: cast.ToInt64(info.Size), - }) - } - ret := []gopop.UidlItem{} - for _, re := range res { - ret = append(ret, gopop.UidlItem{ - Id: re.Id, - UnionId: cast.ToString(re.Id), - }) + ret := make([]gopop.UidlItem, 0, len(emailList)) + for number, info := range emailList { + ret = append(ret, gopop.UidlItem{Id: int64(number + 1), UnionId: cast.ToString(info.Id)}) } log.WithContext(session.Ctx).Debugf("Uidl \n %+v", ret) @@ -189,49 +180,41 @@ type listItem struct { // List 邮件列表 func (a action) List(session *gopop.Session, msg string) ([]gopop.MailInfo, error) { log.WithContext(session.Ctx).Debugf("POP3 CMD: LIST ,Args:%s", msg) - var res []listItem - var listId int + requestedNumber := 0 if msg != "" { - listId = cast.ToInt(msg) - if listId == 0 { - return nil, errors.New("params error") + var err error + requestedNumber, err = parseMessageNumber(msg) + if err != nil { + return nil, err } } - - if listId != 0 { - info, err := detail.GetEmailDetail(session.Ctx.(*context.Context), listId, false) - if err != nil { - return nil, err - } - item := listItem{ - Id: cast.ToInt64(info.Id), - Size: cast.ToInt64(info.Size), - } - if item.Size == 0 { - item.Size = 9999 - } - res = append(res, item) - } else { - emailList, _, err := list.GetEmailList(session.Ctx.(*context.Context), dto.SearchTag{Type: consts.EmailTypeReceive, Status: -1, GroupId: -1}, "", true, 0, 99999) + emailList, err := a.visibleMessages(session) + if err != nil { + return nil, err + } + if requestedNumber > 0 { + id, err := a.resolveMessageID(session, int64(requestedNumber)) if err != nil { return nil, err } + filtered := make([]*response.EmailResponseData, 0, 1) for _, info := range emailList { - item := listItem{ - Id: cast.ToInt64(info.Id), - Size: cast.ToInt64(info.Size), + if cast.ToInt64(info.Id) == id { + filtered = append(filtered, info) + break } - if item.Size == 0 { - item.Size = 9999 - } - res = append(res, item) } + emailList = filtered } - ret := []gopop.MailInfo{} - for _, re := range res { + ret := make([]gopop.MailInfo, 0, len(emailList)) + for number, info := range emailList { + size := int64(info.Size) + if size == 0 { + size = 9999 + } ret = append(ret, gopop.MailInfo{ - Id: re.Id, - Size: re.Size, + Id: int64(number + 1), + Size: size, }) } @@ -242,7 +225,11 @@ func (a action) List(session *gopop.Session, msg string) ([]gopop.MailInfo, erro // Retr 获取邮件详情 func (a action) Retr(session *gopop.Session, id int64) (string, int64, error) { log.WithContext(session.Ctx).Debugf("POP3 CMD: RETR ,Args:%d", id) - email, err := detail.GetEmailDetail(session.Ctx.(*context.Context), cast.ToInt(id), false) + actualID, err := a.resolveMessageID(session, id) + if err != nil { + return "", 0, err + } + email, err := detail.GetEmailDetail(session.Ctx.(*context.Context), cast.ToInt(actualID), false) if err != nil { log.WithContext(session.Ctx.(*context.Context)).Errorf("%+v", err) return "", 0, errors.New("server error") @@ -266,8 +253,16 @@ func (a action) Retr(session *gopop.Session, id int64) (string, int64, error) { // Delete 删除邮件 func (a action) Delete(session *gopop.Session, id int64) error { log.WithContext(session.Ctx).Debugf("POP3 CMD: DELE ,Args:%d", id) - - session.DeleteIds = append(session.DeleteIds, id) + actualID, err := a.resolveMessageID(session, id) + if err != nil { + return err + } + for _, deletedID := range session.DeleteIds { + if deletedID == actualID { + return errors2.New("message already marked for deletion") + } + } + session.DeleteIds = append(session.DeleteIds, actualID) session.DeleteIds = array.Unique(session.DeleteIds) return nil } @@ -280,7 +275,14 @@ func (a action) Rest(session *gopop.Session) error { func (a action) Top(session *gopop.Session, id int64, n int) (string, error) { log.WithContext(session.Ctx).Debugf("POP3 CMD: TOP %d %d", id, n) - email, err := detail.GetEmailDetail(session.Ctx.(*context.Context), cast.ToInt(id), false) + if n < 0 { + return "", errors2.New("invalid line count") + } + actualID, err := a.resolveMessageID(session, id) + if err != nil { + return "", err + } + email, err := detail.GetEmailDetail(session.Ctx.(*context.Context), cast.ToInt(actualID), false) if err != nil { log.WithContext(session.Ctx.(*context.Context)).Errorf("%+v", err) return "", errors2.New("password error") @@ -314,6 +316,79 @@ func (a action) Top(session *gopop.Session, id int64, n int) (string, error) { } +func parseMessageNumber(value string) (int, error) { + number, err := strconv.Atoi(strings.TrimSpace(value)) + if err != nil || number <= 0 { + return 0, errors2.New("no such message") + } + return number, nil +} + +func (a action) ensureMessageMap(session *gopop.Session) ([]*response.EmailResponseData, error) { + if session.MessageMap != nil { + return nil, nil + } + emailList, _, err := list.GetEmailList(session.Ctx.(*context.Context), dto.SearchTag{Type: consts.EmailTypeReceive, Status: -1, GroupId: -1}, "", true, 0, 99999) + if err != nil { + return nil, err + } + session.MessageMap = make(map[int64]int64, len(emailList)) + session.MessageIDs = make([]int64, 0, len(emailList)) + for number, info := range emailList { + id := cast.ToInt64(info.Id) + session.MessageMap[int64(number+1)] = id + session.MessageIDs = append(session.MessageIDs, id) + } + return emailList, nil +} + +func (a action) resolveMessageID(session *gopop.Session, number int64) (int64, error) { + if number <= 0 { + return 0, errors2.New("no such message") + } + if _, err := a.ensureMessageMap(session); err != nil { + return 0, err + } + id, ok := session.MessageMap[number] + if !ok { + return 0, errors2.New("no such message") + } + for _, deletedID := range session.DeleteIds { + if deletedID == id { + return 0, errors2.New("message already marked for deletion") + } + } + return id, nil +} + +func (a action) visibleMessages(session *gopop.Session) ([]*response.EmailResponseData, error) { + emailList, err := a.ensureMessageMap(session) + if err != nil { + return nil, err + } + if emailList == nil { + emailList, _, err = list.GetEmailList(session.Ctx.(*context.Context), dto.SearchTag{Type: consts.EmailTypeReceive, Status: -1, GroupId: -1}, "", true, 0, 99999) + if err != nil { + return nil, err + } + } + result := make([]*response.EmailResponseData, 0, len(emailList)) + for _, info := range emailList { + id := cast.ToInt64(info.Id) + marked := false + for _, deletedID := range session.DeleteIds { + if deletedID == id { + marked = true + break + } + } + if !marked { + result = append(result, info) + } + } + return result, nil +} + func (a action) Noop(session *gopop.Session) error { log.WithContext(session.Ctx).Debugf("POP3 CMD: NOOP ") return nil