Jelajahi Sumber

feature/v2.8.0

jinnrry 1 tahun lalu
induk
melakukan
ba33c0d4f8

+ 3 - 3
server/db/init.go

@@ -58,9 +58,9 @@ func Init(version string) error {
 		Instance.Update(&v)
 	}
 
-	//if config.Instance.LogLevel == "debug" {
-	//	Instance.ShowSQL(true)
-	//}
+	if config.Instance.LogLevel == "debug" {
+		Instance.ShowSQL(true)
+	}
 
 	return nil
 }

+ 1 - 1
server/dto/parsemail/email_test.go

@@ -68,7 +68,7 @@ func TestEmail_builder(t *testing.T) {
 	e := Email{
 		From:    buildUser("i@test.com"),
 		To:      buildUsers([]string{"to@test.com"}),
-		Subject: "Title",
+		Subject: "Title中文",
 		HTML:    []byte("Html"),
 		Text:    []byte("Text"),
 		Attachments: []*Attachment{

+ 92 - 68
server/listen/imap_server/action.go

@@ -2,11 +2,11 @@ package imap_server
 
 import (
 	"database/sql"
-	errors2 "errors"
 	"fmt"
 	"github.com/Jinnrry/pmail/db"
 	"github.com/Jinnrry/pmail/models"
 	"github.com/Jinnrry/pmail/services/group"
+	"github.com/Jinnrry/pmail/utils/array"
 	"github.com/Jinnrry/pmail/utils/context"
 	"github.com/Jinnrry/pmail/utils/errors"
 	"github.com/Jinnrry/pmail/utils/goimap"
@@ -50,43 +50,51 @@ func PushMsgByIDLE(ctx *context.Context, account string, unionId string) error {
 
 type action struct{}
 
-func (a action) Create(session *goimap.Session, path string) error {
+func (a action) Create(session *goimap.Session, path string) goimap.CommandResponse {
 	log.Infof("%s,%s", "Create", path)
-	return nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) Delete(session *goimap.Session, path string) error {
+func (a action) Delete(session *goimap.Session, path string) goimap.CommandResponse {
 	log.Infof("%s,%s", "Delete", path)
-	return nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) Rename(session *goimap.Session, oldPath, newPath string) error {
+func (a action) Rename(session *goimap.Session, oldPath, newPath string) goimap.CommandResponse {
 	log.Infof("%s,%s,%s", "Rename", oldPath, newPath)
-	return nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) List(session *goimap.Session, basePath, template string) ([]string, error) {
+func (a action) List(session *goimap.Session, basePath, template string) goimap.CommandResponse {
 	log.Infof("%s,%s,%s", "List", basePath, template)
 	var ret []string
 	if basePath == "" && template == "" {
-		ret = append(ret, `* LIST (\NoSelect \HasChildren) "/" "[PMail]`)
-		return ret, nil
+		ret = append(ret, `* LIST (\NoSelect \HasChildren) "/" "[PMail]"`)
+		return goimap.CommandResponse{
+			Type:    goimap.SUCCESS,
+			Data:    ret,
+			Message: "Success",
+		}
 	}
 
 	ret = group.MatchGroup(session.Ctx.(*context.Context), basePath, template)
 
-	return ret, nil
+	return goimap.CommandResponse{
+		Type:    goimap.SUCCESS,
+		Data:    ret,
+		Message: "Success",
+	}
 }
 
-func (a action) Append(session *goimap.Session, item string) error {
+func (a action) Append(session *goimap.Session, item string) goimap.CommandResponse {
 	log.Infof("%s,%s", "Append", item)
-	return nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) Select(session *goimap.Session, path string) ([]string, error) {
+func (a action) Select(session *goimap.Session, path string) goimap.CommandResponse {
 	log.Infof("%s,%s", "Select", path)
 	paths := strings.Split(path, "/")
-	session.CurrentPath = paths[len(paths)-1]
+	session.CurrentPath = strings.Trim(paths[len(paths)-1], `"`)
 	_, data := group.GetGroupStatus(session.Ctx.(*context.Context), session.CurrentPath, []string{"MESSAGES", "UNSEEN", "UIDNEXT", "UIDVALIDITY"})
 	ret := []string{}
 	allNum := data["MESSAGES"]
@@ -100,85 +108,98 @@ func (a action) Select(session *goimap.Session, path string) ([]string, error) {
 	ret = append(ret, fmt.Sprintf("* OK [UIDNEXT %d] Predicted next UID", nextID))
 	ret = append(ret, `* FLAGS (\Answered \Flagged \Deleted \Draft \Seen)`)
 	ret = append(ret, `* OK [PERMANENTFLAGS (\* \Answered \Flagged \Deleted \Draft \Seen)] Permanent flags`)
-	ret = append(ret, `$$NUM OK [READ-WRITE] SELECT complete`)
-
-	return ret, nil
-}
 
-func (a action) Fetch(session *goimap.Session, mailIds, dataNames string) (string, error) {
-	log.Infof("%s,%s,%s", "Fetch", mailIds, dataNames)
-	return "", nil
+	return goimap.CommandResponse{
+		Type:    goimap.SUCCESS,
+		Data:    ret,
+		Message: "OK [READ-WRITE] SELECT complete",
+	}
 }
 
-func (a action) Store(session *goimap.Session, mailId, flags string) error {
+func (a action) Store(session *goimap.Session, mailId, flags string) goimap.CommandResponse {
 	log.Infof("%s,%s,%s", "Store", mailId, flags)
-	return nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) Close(session *goimap.Session) error {
+func (a action) Close(session *goimap.Session) goimap.CommandResponse {
 	log.Infof("%s", "Close")
-	return nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) Expunge(session *goimap.Session) error {
+func (a action) Expunge(session *goimap.Session) goimap.CommandResponse {
 	log.Infof("%s", "Expunge")
-	return nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) Examine(session *goimap.Session, path string) error {
+func (a action) Examine(session *goimap.Session, path string) goimap.CommandResponse {
 	log.Infof("%s,%s", "Examine", path)
-	return nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) Subscribe(session *goimap.Session, path string) error {
+func (a action) Subscribe(session *goimap.Session, path string) goimap.CommandResponse {
 	log.Infof("%s,%s", "Subscribe", path)
-	return nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) UnSubscribe(session *goimap.Session, path string) error {
+func (a action) UnSubscribe(session *goimap.Session, path string) goimap.CommandResponse {
 	log.Infof("%s,%s", "UnSubscribe", path)
-	return nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) LSub(session *goimap.Session, path, mailbox string) ([]string, error) {
+func (a action) LSub(session *goimap.Session, path, mailbox string) goimap.CommandResponse {
 	log.Infof("%s,%s,%s", "LSub", path, mailbox)
-	return nil, nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) Status(session *goimap.Session, mailbox string, category []string) (string, error) {
+func (a action) Status(session *goimap.Session, mailbox string, category []string) goimap.CommandResponse {
 	log.Infof("%s,%s,%+v", "Status", mailbox, category)
+
+	category = array.Intersect([]string{"MESSAGES", "UIDNEXT", "UIDVALIDITY", "UNSEEN"}, category)
+	if len(category) == 0 {
+		category = []string{"MESSAGES", "UIDNEXT", "UIDVALIDITY", "UNSEEN"}
+	}
+
 	ret, _ := group.GetGroupStatus(session.Ctx.(*context.Context), mailbox, category)
-	return fmt.Sprintf(`* STATUS "%s" %s`, mailbox, ret), nil
+	return goimap.CommandResponse{
+		Type:    goimap.SUCCESS,
+		Data:    []string{fmt.Sprintf(`* STATUS "%s" %s`, mailbox, ret)},
+		Message: "STATUS completed",
+	}
+
 }
 
-func (a action) Check(session *goimap.Session) error {
+func (a action) Check(session *goimap.Session) goimap.CommandResponse {
 	log.Infof("%s", "Check")
-	return nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) Search(session *goimap.Session, keyword, criteria string) (string, error) {
+func (a action) Search(session *goimap.Session, keyword, criteria string) goimap.CommandResponse {
 	log.Infof("%s,%s,%s", "Search", keyword, criteria)
-	return "", nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) Copy(session *goimap.Session, mailId, mailBoxName string) error {
+func (a action) Copy(session *goimap.Session, mailId, mailBoxName string) goimap.CommandResponse {
 	log.Infof("%s,%s,%s", "Copy", mailId, mailBoxName)
-	return nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) CapaBility(session *goimap.Session) ([]string, error) {
+func (a action) CapaBility(session *goimap.Session) goimap.CommandResponse {
 	log.Infof("%s", "CapaBility")
-	return []string{
-		"CAPABILITY",
-		"IMAP4rev1",
-		"UNSELECT",
-		"IDLE",
-		"AUTH=PLAIN",
-		"AUTH=LOGIN",
-	}, nil
+
+	return goimap.CommandResponse{
+		Type: goimap.SUCCESS,
+		Data: []string{
+			"CAPABILITY",
+			"IMAP4rev1",
+			"UNSELECT",
+			"IDLE",
+			"AUTH=LOGIN",
+		},
+	}
+
 }
 
-func (a action) IDLE(session *goimap.Session) error {
+func (a action) IDLE(session *goimap.Session) goimap.CommandResponse {
 	pools, ok := idlePool.Load(session.Account)
 	if !ok {
 		idlePool.Store(session.Account, []*goimap.Session{
@@ -193,21 +214,21 @@ func (a action) IDLE(session *goimap.Session) error {
 			idlePool.Store(session.Account, sPools)
 		}
 	}
-	return nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) Unselect(session *goimap.Session) error {
+func (a action) Unselect(session *goimap.Session) goimap.CommandResponse {
 	log.Infof("%s", "Unselect")
 	session.CurrentPath = ""
-	return nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) Noop(session *goimap.Session) error {
+func (a action) Noop(session *goimap.Session) goimap.CommandResponse {
 	log.Infof("%s", "Noop")
-	return nil
+	return goimap.CommandResponse{}
 }
 
-func (a action) Login(session *goimap.Session, username, pwd string) error {
+func (a action) Login(session *goimap.Session, username, pwd string) goimap.CommandResponse {
 	log.WithContext(session.Ctx).Infof("%s,%s,%s", "Login", username, pwd)
 
 	if strings.Contains(username, "@") {
@@ -237,21 +258,24 @@ func (a action) Login(session *goimap.Session, username, pwd string) error {
 		session.Ctx.(*context.Context).UserName = user.Name
 		session.Ctx.(*context.Context).UserAccount = user.Account
 
-		return nil
+		return goimap.CommandResponse{}
 	}
 
-	return errors2.New("password error")
+	return goimap.CommandResponse{
+		Type:    goimap.NO,
+		Message: "[AUTHENTICATIONFAILED] Invalid credentials (Failure)",
+	}
 }
 
-func (a action) Logout(session *goimap.Session) error {
+func (a action) Logout(session *goimap.Session) goimap.CommandResponse {
 	session.Status = goimap.UNAUTHORIZED
-	if session.Conn != nil {
-		_ = session.Conn.Close()
+
+	return goimap.CommandResponse{
+		Type: goimap.SUCCESS,
 	}
-	return nil
 }
 
-func (a action) Custom(session *goimap.Session, cmd string, args string) ([]string, error) {
+func (a action) Custom(session *goimap.Session, cmd string, args string) goimap.CommandResponse {
 	log.Infof("Custom  %s,%+v", cmd, args)
-	return nil, nil
+	return goimap.CommandResponse{}
 }

+ 195 - 0
server/listen/imap_server/action_fetch.go

@@ -0,0 +1,195 @@
+package imap_server
+
+import (
+	"fmt"
+	"github.com/Jinnrry/pmail/dto/parsemail"
+	"github.com/Jinnrry/pmail/dto/response"
+	"github.com/Jinnrry/pmail/services/detail"
+	"github.com/Jinnrry/pmail/services/list"
+	"github.com/Jinnrry/pmail/utils/context"
+	"github.com/Jinnrry/pmail/utils/goimap"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/cast"
+	"strings"
+)
+
+func (a action) Fetch(session *goimap.Session, mailIds, commands string, uid bool) goimap.CommandResponse {
+	log.Infof("%s,%s,%s", "Fetch", mailIds, commands)
+	if session.CurrentPath == "" {
+		return goimap.CommandResponse{
+			Type:    goimap.BAD,
+			Message: "Please Select Mailbox!",
+		}
+	}
+
+	offset := 0
+	limit := 0
+
+	if strings.Contains(mailIds, ":") {
+		args := strings.Split(mailIds, ":")
+		offset = cast.ToInt(args[0])
+		limit = cast.ToInt(args[1])
+	} else {
+		offset = cast.ToInt(mailIds)
+		limit = 1
+	}
+	if offset > 0 {
+		offset -= 1
+	}
+	emailList := list.GetEmailListByGroup(session.Ctx.(*context.Context), session.CurrentPath, offset, limit)
+	ret := goimap.CommandResponse{}
+
+	commandArg := splitCommand(commands, uid)
+
+	for i, email := range emailList {
+		buildResponse(session.Ctx.(*context.Context), offset+i+1, email, commandArg, &ret)
+	}
+
+	ret.Message = "FETCH Completed"
+
+	return ret
+}
+
+func buildResponse(ctx *context.Context, no int, email *response.EmailResponseData, commands []string, ret *goimap.CommandResponse) {
+	retStr := ""
+	for _, command := range commands {
+		switch command {
+		case "INTERNALDATE":
+			if retStr != "" {
+				retStr += " "
+			}
+			retStr += fmt.Sprintf(`INTERNALDATE "%s"`, email.CreateTime.Format("2-Jan-2006 15:04:05 -0700"))
+		case "UID":
+			if retStr != "" {
+				retStr += " "
+			}
+			retStr += fmt.Sprintf(`UID %d`, no)
+		case "RFC822.SIZE":
+			if retStr != "" {
+				retStr += " "
+			}
+			retStr += fmt.Sprintf(`RFC822.SIZE %d`, email.Size)
+		case "FLAGS":
+			if retStr != "" {
+				retStr += " "
+			}
+			if email.IsRead == 1 {
+				retStr += `FLAGS (\Seen)`
+			} else {
+				retStr += `FLAGS ()`
+			}
+		default:
+			if strings.HasPrefix(command, "BODY") {
+				if retStr != "" {
+					retStr += " "
+				}
+
+				retStr += strings.Replace(command, ".PEEK", "", 1) + buildBody(ctx, command, email)
+			}
+		}
+	}
+	ret.Data = append(ret.Data, fmt.Sprintf("* %d FETCH (%s)", no, retStr))
+}
+
+type item struct {
+	content string
+	name    string
+}
+
+func buildBody(ctx *context.Context, command string, email *response.EmailResponseData) string {
+	if !strings.HasPrefix(command, "BODY.PEEK") && email.IsRead == 0 {
+		detail.MakeRead(ctx, email.Id)
+	}
+	ret := ""
+	fields := []string{}
+	if strings.Contains(command, "HEADER.FIELDS") {
+		args := strings.Split(command, "(")
+		data := strings.Split(args[1], ")")
+		fields = strings.Split(data[0], " ")
+	}
+	emailContent := parsemail.NewEmailFromModel(email.Email).BuildBytes(ctx, false)
+	headerMap := map[string]*item{}
+	var key string
+	var isContent bool
+	content := ""
+
+	for _, line := range strings.Split(string(emailContent), "\r\n") {
+		if line == "" {
+			isContent = true
+		}
+		if isContent {
+			content += line + "\r\n"
+		} else {
+			if !strings.HasPrefix(line, " ") {
+				args := strings.SplitN(line, ":", 2)
+				key = strings.ToTitle(args[0])
+				headerMap[key] = &item{strings.TrimSpace(args[1]), args[0]}
+			} else {
+				headerMap[key].content += fmt.Sprintf("\r\n%s", line)
+			}
+		}
+
+	}
+
+	if len(fields) == 0 {
+		for _, v := range headerMap {
+			ret += fmt.Sprintf("%s: %s\r\n", v.name, v.content)
+		}
+		ret += content
+	} else {
+		for _, field := range fields {
+			field = strings.Trim(field, `" `)
+
+			key := strings.ToTitle(field)
+
+			if headerMap[key] != nil {
+				ret += fmt.Sprintf("%s: %s\r\n", headerMap[key].name, headerMap[key].content)
+			}
+		}
+	}
+
+	size := len([]byte(ret)) + 2
+
+	return fmt.Sprintf(" {%d}\r\n%s\r\n", size, ret)
+}
+
+func splitCommand(commands string, uid bool) []string {
+	var ret []string
+	if uid {
+		ret = append(ret, "UID")
+	}
+
+	commands = strings.Trim(commands, "() ")
+
+	for i := 0; i < 1000; i++ {
+		if commands == "" {
+			break
+		}
+		if !strings.HasPrefix(commands, "BODY") {
+			args := strings.SplitN(commands, " ", 2)
+			if len(args) >= 2 {
+				commands = strings.TrimSpace(args[1])
+			} else {
+				commands = ""
+			}
+			ret = append(ret, args[0])
+		} else {
+			item := ""
+			if strings.HasPrefix(commands, "BODY.PEEK") {
+				commands = strings.TrimPrefix(commands, "BODY.PEEK")
+				item += "BODY.PEEK"
+			} else if strings.HasPrefix(commands, "BODY") {
+				commands = strings.TrimPrefix(commands, "BODY")
+				item += "BODY"
+			}
+			if commands[0] == '[' {
+				args := strings.SplitN(commands, "]", 2)
+				item += args[0] + "]"
+				ret = append(ret, item)
+				commands = strings.TrimSpace(args[1])
+			}
+		}
+	}
+
+	return ret
+}

+ 42 - 0
server/listen/imap_server/action_fetch_test.go

@@ -0,0 +1,42 @@
+package imap_server
+
+import (
+	"reflect"
+	"testing"
+)
+
+func Test_splitCommand(t *testing.T) {
+	type args struct {
+		commands string
+	}
+	tests := []struct {
+		name string
+		args args
+		want []string
+	}{
+		{
+			name: "normal",
+			args: args{
+				commands: `(UID ENVELOPE FLAGS INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS ("date" "subject" "from" "to" "cc" "message-id" "in-reply-to" "references" "content-type" "x-priority" "x-uniform-type-identifier" "x-universally-unique-identifier" "list-id" "list-unsubscribe" "bimi-indicator" "bimi-location" "x-bimi-indicator-hash" "authentication-results" "dkim-signature")])`,
+			},
+			want: []string{
+				"UID", "ENVELOPE", "FLAGS", "INTERNALDATE", "RFC822.SIZE",
+				"BODY.PEEK[HEADER.FIELDS (\"date\" \"subject\" \"from\" \"to\" \"cc\" \"message-id\" \"in-reply-to\" \"references\" \"content-type\" \"x-priority\" \"x-uniform-type-identifier\" \"x-universally-unique-identifier\" \"list-id\" \"list-unsubscribe\" \"bimi-indicator\" \"bimi-location\" \"x-bimi-indicator-hash\" \"authentication-results\" \"dkim-signature\")]",
+			},
+		},
+		{
+			name: "fetch",
+			args: args{
+				commands: "(FLAGS UID)",
+			},
+			want: []string{"FLAGS", "UID"},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := splitCommand(tt.args.commands, false); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("splitCommand() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}

+ 0 - 61
server/listen/imap_server/action_test.go

@@ -1,61 +0,0 @@
-package imap_server
-
-import (
-	"github.com/Jinnrry/pmail/config"
-	"github.com/Jinnrry/pmail/db"
-	"github.com/Jinnrry/pmail/models"
-	"github.com/Jinnrry/pmail/utils/context"
-	"github.com/Jinnrry/pmail/utils/goimap"
-	"github.com/Jinnrry/pmail/utils/id"
-	"testing"
-)
-
-func Test_action_List(t *testing.T) {
-	config.Init()
-	db.Init("")
-	db.Instance.ShowSQL(true)
-
-	_, err := db.Instance.Exec("DELETE FROM `group`")
-
-	groupData1 := models.Group{
-		ID:     1,
-		Name:   "第一层",
-		UserId: 1,
-	}
-	groupData2 := models.Group{
-		ID:       2,
-		Name:     "第二层",
-		UserId:   1,
-		ParentId: 1,
-	}
-	db.Instance.Insert(&groupData1)
-	db.Instance.Insert(&groupData2)
-
-	session := goimap.Session{
-		Account: "admin",
-	}
-	tc := &context.Context{}
-	tc.UserID = 1
-	tc.SetValue(context.LogID, id.GenLogID())
-	session.Ctx = tc
-
-	ret, err := action{}.List(&session, "", "")
-	if err != nil {
-		t.Error(err)
-	}
-	if len(ret) == 1 && ret[0] == `* LIST (\NoSelect \HasChildren) "/" "[PMail]` {
-		t.Logf("%s", ret[0])
-	} else {
-		t.Errorf("%s", ret)
-	}
-
-	ret, err = action{}.List(&session, "", "*")
-	if err != nil {
-		t.Error(err)
-	}
-	if len(ret) == 8 && ret[7] == `* LIST (\HasNoChildren) "/" "&eyxOAFxC-/&eyxOjFxC-"` {
-		t.Logf("%s", ret[0])
-	} else {
-		t.Errorf("%s", ret)
-	}
-}

+ 234 - 16
server/listen/imap_server/imap_server_test.go

@@ -2,9 +2,9 @@ package imap_server
 
 import (
 	"crypto/tls"
-	"fmt"
 	"github.com/Jinnrry/pmail/config"
 	"github.com/Jinnrry/pmail/db"
+	"github.com/emersion/go-imap/v2"
 	"github.com/emersion/go-imap/v2/imapclient"
 	"github.com/emersion/go-message/charset"
 	"mime"
@@ -12,7 +12,10 @@ import (
 	"time"
 )
 
-func TestStarTLS(t *testing.T) {
+var clientUnLogin *imapclient.Client
+var clientLogin *imapclient.Client
+
+func TestMain(m *testing.M) {
 	config.Init()
 	db.Init("")
 	go StarTLS()
@@ -25,26 +28,241 @@ func TestStarTLS(t *testing.T) {
 		},
 	}
 
-	client, err := imapclient.DialTLS("127.0.0.1:993", options)
+	var err error
+	clientUnLogin, err = imapclient.DialTLS("127.0.0.1:993", options)
+	if err != nil {
+		panic(err)
+	}
+
+	clientLogin, err = imapclient.DialTLS("127.0.0.1:993", options)
+	if err != nil {
+		panic(err)
+	}
+
+	err = clientLogin.Login("testCase", "testCase").Wait()
 	if err != nil {
-		t.Fatal(err)
+		panic(err)
 	}
 
-	fmt.Println("_________")
+	m.Run()
+}
+
+func TestCapability(t *testing.T) {
 
-	res, err := client.Capability().Wait() // wait forever!
+	res, err := clientUnLogin.Capability().Wait()
 	if err != nil {
-		t.Fatal(err)
+		t.Error(err)
+	}
+	if _, ok := res["IMAP4rev1"]; !ok {
+		t.Error("Capability Error")
 	}
-	fmt.Println("Response:", res)
 
-	time.Sleep(10 * time.Second)
 }
 
-/*
-Here Is My Server Input&Output Log:
-Output:	* OK [CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN AUTH=LOGIN] PMail Server ready
-Input:	T1 CAPABILITY
-Output:	* CAPABILITY IMAP4rev1 UNSELECT IDLE AUTH=PLAIN AUTH=LOGIN
-Output:	T1 OK success
-*/
+func TestLogin(t *testing.T) {
+	err := clientUnLogin.Login("testCase", "testCaseasdfsadf").Wait()
+	sErr := err.(*imap.Error)
+	if sErr.Code != "AUTHENTICATIONFAILED" {
+		t.Error("Login Error")
+	}
+}
+
+func TestCreate(t *testing.T) {
+
+}
+func TestDelete(t *testing.T) {
+
+}
+func TestRename(t *testing.T) {
+
+}
+func TestList(t *testing.T) {
+	res, err := clientUnLogin.List("", "", &imap.ListOptions{}).Collect()
+
+	if err == nil {
+		t.Logf("%+v", res)
+		t.Error("List Unlogin error")
+	}
+
+	res, err = clientLogin.List("", "", &imap.ListOptions{}).Collect()
+	if err != nil {
+		t.Error(err)
+	}
+	if len(res) == 0 {
+		t.Error("List Error")
+	}
+
+	res, err = clientLogin.List("", "*", &imap.ListOptions{}).Collect()
+	if err != nil {
+		t.Error(err)
+	}
+	if len(res) == 0 {
+		t.Error("List Error")
+	}
+
+}
+func TestAppend(t *testing.T) {
+
+}
+func TestSelect(t *testing.T) {
+	res, err := clientUnLogin.Select("INBOX", &imap.SelectOptions{}).Wait()
+	if err == nil {
+		t.Logf("%+v", res)
+		t.Error("Select Unlogin error")
+	}
+
+	res, err = clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait()
+	if err != nil {
+		t.Logf("%+v", res)
+		t.Error("Select error")
+	}
+
+	if res == nil || res.NumMessages == 0 {
+		t.Error("Select Error")
+	}
+
+	res, err = clientLogin.Select("Deleted Messages", &imap.SelectOptions{}).Wait()
+	if err != nil {
+		t.Logf("%+v", res)
+		t.Error("Select error")
+	}
+
+	if res == nil || res.NumMessages == 0 {
+		t.Error("Select Error")
+	}
+
+}
+
+func TestStatus(t *testing.T) {
+	res, err := clientUnLogin.Status("INBOX", &imap.StatusOptions{}).Wait()
+	if err == nil {
+		t.Logf("%+v", res)
+		t.Error("Select Unlogin error")
+	}
+
+	res, err = clientLogin.Status("INBOX", &imap.StatusOptions{}).Wait()
+	if err != nil {
+		t.Logf("%+v", res)
+		t.Error("Select error")
+	}
+
+}
+
+func TestFetch(t *testing.T) {
+	res2, err := clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait()
+	if err != nil {
+		t.Logf("%+v", res2)
+		t.Error("Fetch error")
+	}
+
+	res, err := clientLogin.Fetch(imap.SeqSetNum(1), &imap.FetchOptions{
+		Envelope:     true,
+		Flags:        true,
+		InternalDate: true,
+		RFC822Size:   true,
+		UID:          true,
+		BodySection: []*imap.FetchItemBodySection{
+			{
+				Specifier: imap.PartSpecifierText,
+				Peek:      true,
+			},
+		},
+	}).Collect()
+	if err != nil {
+		t.Logf("%+v", res)
+		t.Error("Fetch error")
+	}
+
+	res, err = clientLogin.Fetch(imap.SeqSetNum(1, 2, 3, 4, 5, 6, 7, 8, 9), &imap.FetchOptions{
+		Flags: true,
+		UID:   true,
+	}).Collect()
+	if err != nil {
+		t.Logf("%+v", res)
+		t.Error("Fetch error")
+	}
+
+	res, err = clientLogin.Fetch(imap.SeqSetNum(1), &imap.FetchOptions{
+		Envelope:     true,
+		Flags:        true,
+		InternalDate: true,
+		RFC822Size:   true,
+		UID:          true,
+		BodySection: []*imap.FetchItemBodySection{
+			{
+				Specifier:    imap.PartSpecifierHeader,
+				HeaderFields: []string{"subject"},
+				Peek:         true,
+			},
+		},
+	}).Collect()
+	if err != nil {
+		t.Logf("%+v", res)
+		t.Error("Fetch error")
+	}
+
+	res, err = clientLogin.Fetch(imap.SeqSetNum(1), &imap.FetchOptions{
+		UID: true,
+		BodySection: []*imap.FetchItemBodySection{
+			{
+				Specifier: imap.PartSpecifierHeader,
+				Peek:      true,
+			},
+		},
+	}).Collect()
+	if err != nil {
+		t.Logf("%+v", res)
+		t.Error("Fetch error")
+	}
+}
+func TestStore(t *testing.T) {
+
+}
+func TestClose(t *testing.T) {
+
+}
+func TestExpunge(t *testing.T) {
+
+}
+func TestExamine(t *testing.T) {
+
+}
+func TestSubscribe(t *testing.T) {
+
+}
+func TestUnSubscribe(t *testing.T) {
+
+}
+func TestLSub(t *testing.T) {
+
+}
+
+func TestCheck(t *testing.T) {
+
+}
+func TestSearch(t *testing.T) {
+
+}
+func TestCopy(t *testing.T) {
+
+}
+
+func TestNoop(t *testing.T) {
+	err := clientLogin.Noop().Wait()
+	if err != nil {
+		t.Error(err)
+	}
+}
+func TestIDLE(t *testing.T) {
+
+}
+func TestUnselect(t *testing.T) {
+
+}
+
+func TestLogout(t *testing.T) {
+	err := clientLogin.Logout().Wait()
+	if err != nil {
+		t.Error(err)
+	}
+}

+ 6 - 3
server/main_test.go

@@ -4,10 +4,10 @@ import (
 	"encoding/json"
 	"flag"
 	"fmt"
+	"github.com/Jinnrry/pmail/config"
 	"github.com/Jinnrry/pmail/db"
 	"github.com/Jinnrry/pmail/dto/response"
 	"github.com/Jinnrry/pmail/models"
-	"github.com/Jinnrry/pmail/services/setup"
 	"github.com/Jinnrry/pmail/signal"
 	"github.com/Jinnrry/pmail/utils/array"
 	"github.com/spf13/cast"
@@ -54,22 +54,25 @@ func TestMaster(t *testing.T) {
 	t.Run("testPwdSet", testPwdSet)
 	t.Run("testDomainSet", testDomainSet)
 	t.Run("testDNSSet", testDNSSet)
-	cfg, err := setup.ReadConfig()
+	cfg, err := config.ReadConfig()
 	if err != nil {
 		t.Fatal(err)
 	}
 	cfg.HttpsEnabled = 2
 	cfg.HttpPort = TestPort
-	err = setup.WriteConfig(cfg)
+	cfg.LogLevel = "debug"
+	err = config.WriteConfig(cfg)
 	if err != nil {
 		t.Fatal(err)
 	}
 	t.Run("testSSLSet", testSSLSet)
+	t.Logf("Stop 8 Second for wating restart")
 	time.Sleep(8 * time.Second)
 	t.Run("testLogin", testLogin)           // 登录管理员账号
 	t.Run("testCreateUser", testCreateUser) // 创建3个测试用户
 	t.Run("testEditUser", testEditUser)     // 编辑user2,封禁user3
 	t.Run("testSendEmail", testSendEmail)
+	t.Logf("Stop 8 Second for wating sending")
 	time.Sleep(8 * time.Second)
 	t.Run("testEmailList", testEmailList)
 	t.Run("testGetDetail", testGetEmailDetail)

+ 1 - 1
server/models/user_email.go

@@ -6,7 +6,7 @@ type UserEmail struct {
 	EmailID int  `xorm:"email_id not null index('idx_eid') index comment('信件id')"`
 	IsRead  int8 `xorm:"is_read tinyint(1) comment('是否已读')" json:"is_read"`
 	GroupId int  `xorm:"group_id int notnull default(0) comment('分组id')'" json:"group_id"`
-	Status  int8 `xorm:"status tinyint(4) notnull default(0) comment('0未发送或收件,1已发送,2发送失败,3删除')" json:"status"` // 0未发送或收件,1已发送,2发送失败 3删除
+	Status  int8 `xorm:"status tinyint(4) notnull default(0) comment('0未发送或收件,1已发送,2发送失败,3删除')" json:"status"` // 0未发送或收件,1已发送,2发送失败 3删除 4草稿箱(Drafts)  5骚扰邮件(Junk)
 }
 
 func (p UserEmail) TableName() string {

+ 9 - 0
server/services/detail/detail.go

@@ -56,3 +56,12 @@ func GetEmailDetail(ctx *context.Context, id int, markRead bool) (*response.Emai
 
 	return &email, nil
 }
+
+func MakeRead(ctx *context.Context, emailId int) {
+	ue := models.UserEmail{
+		UserID:  ctx.UserID,
+		IsRead:  1,
+		EmailID: emailId,
+	}
+	db.Instance.Where("email_id = ? and user_id=?", emailId, ctx.UserID).Cols("is_read").Update(&ue)
+}

+ 16 - 1
server/services/group/group.go

@@ -218,10 +218,16 @@ func GetGroupStatus(ctx *context.Context, groupName string, params []string) (st
 			value = models.GroupNameToCode[groupName]
 		case "UNSEEN":
 			value = getGroupNum(ctx, groupName, true)
+		default:
+			continue
 		}
 		retMap[param] = value
 		ret += fmt.Sprintf("%s %d", param, value)
 	}
+	if ret == "" {
+		return "", retMap
+	}
+
 	return fmt.Sprintf("(%s)", ret), retMap
 
 }
@@ -241,8 +247,12 @@ func getGroupNum(ctx *context.Context, groupName string, mustUnread bool) int {
 		} else {
 			db.Instance.Table("user_email").Select("count(1)").Where("user_id=? and status=1", ctx.UserID).Get(&count)
 		}
-
 	case "Drafts":
+		if mustUnread {
+			db.Instance.Table("user_email").Select("count(1)").Where("user_id=? and status=4 and is_read=0", ctx.UserID).Get(&count)
+		} else {
+			db.Instance.Table("user_email").Select("count(1)").Where("user_id=? and status=4", ctx.UserID).Get(&count)
+		}
 	case "Deleted Messages":
 		if mustUnread {
 			db.Instance.Table("user_email").Select("count(1)").Where("user_id=? and status=3 and is_read=0", ctx.UserID).Get(&count)
@@ -250,6 +260,11 @@ func getGroupNum(ctx *context.Context, groupName string, mustUnread bool) int {
 			db.Instance.Table("user_email").Select("count(1)").Where("user_id=? and status=3", ctx.UserID).Get(&count)
 		}
 	case "Junk":
+		if mustUnread {
+			db.Instance.Table("user_email").Select("count(1)").Where("user_id=? and status=5 and is_read=0", ctx.UserID).Get(&count)
+		} else {
+			db.Instance.Table("user_email").Select("count(1)").Where("user_id=? and status=5", ctx.UserID).Get(&count)
+		}
 	}
 	return count
 }

+ 48 - 0
server/services/list/list.go

@@ -5,9 +5,12 @@ import (
 	"github.com/Jinnrry/pmail/db"
 	"github.com/Jinnrry/pmail/dto"
 	"github.com/Jinnrry/pmail/dto/response"
+	"github.com/Jinnrry/pmail/models"
 	"github.com/Jinnrry/pmail/utils/context"
 	log "github.com/sirupsen/logrus"
+	"strings"
 )
+import . "xorm.io/builder"
 
 func GetEmailList(ctx *context.Context, tagInfo dto.SearchTag, keyword string, pop3List bool, offset, limit int) (emailList []*response.EmailResponseData, total int64) {
 	return getList(ctx, tagInfo, keyword, pop3List, offset, limit)
@@ -94,3 +97,48 @@ func Stat(ctx *context.Context) (int64, int64) {
 	}
 	return ret.Total, ret.Size
 }
+
+func GetEmailListByGroup(ctx *context.Context, groupName string, offset, limit int) []*response.EmailResponseData {
+	if limit == 0 {
+		limit = 1
+	}
+
+	var ret []*response.EmailResponseData
+	var ue []*models.UserEmail
+	switch groupName {
+	case "INBOX":
+		db.Instance.Table("user_email").Select("email_id,is_read").Where("user_id=? and status=0", ctx.UserID).Limit(limit, offset).Find(&ue)
+	case "Sent Messages":
+		db.Instance.Table("user_email").Select("email_id,is_read").Where("user_id=? and status=1", ctx.UserID).Limit(limit, offset).Find(&ue)
+	case "Drafts":
+		db.Instance.Table("user_email").Select("email_id,is_read").Where("user_id=? and status=4", ctx.UserID).Limit(limit, offset).Find(&ue)
+	case "Deleted Messages":
+		db.Instance.Table("user_email").Select("email_id,is_read").Where("user_id=? and status=3", ctx.UserID).Limit(limit, offset).Find(&ue)
+	case "Junk":
+		db.Instance.Table("user_email").Select("email_id,is_read").Where("user_id=? and status=5", ctx.UserID).Limit(limit, offset).Find(&ue)
+	default:
+		groupNames := strings.Split(groupName, "/")
+		groupName = groupNames[len(groupNames)-1]
+
+		var group models.Group
+		db.Instance.Table("group").Where("user_id=? and name=?", ctx.UserID, groupName).Get(&group)
+		if group.ID == 0 {
+			return ret
+		}
+		db.Instance.Table("user_email").Select("email_id,is_read").Where("user_id=? and group_id = ?", ctx.UserID, group.ID).Limit(limit, offset).Find(&ue)
+	}
+
+	ueMap := map[int]*models.UserEmail{}
+	var emailIds []int
+	for _, email := range ue {
+		ueMap[email.EmailID] = email
+		emailIds = append(emailIds, email.EmailID)
+	}
+
+	_ = db.Instance.Table("email").Select("*").Where(Eq{"id": emailIds}).Find(&ret)
+	for i, data := range ret {
+		ret[i].IsRead = ueMap[data.Id].IsRead
+	}
+
+	return ret
+}

+ 25 - 25
server/utils/goimap/action.go

@@ -1,12 +1,12 @@
 package goimap
 
 type Action interface {
-	Create(session *Session, path string) error                         // 创建邮箱
-	Delete(session *Session, path string) error                         // 删除邮箱
-	Rename(session *Session, oldPath, newPath string) error             // 重命名邮箱
-	List(session *Session, basePath, template string) ([]string, error) // 浏览邮箱
-	Append(session *Session, item string) error                         // 上传邮件
-	Select(session *Session, path string) ([]string, error)             // 选择邮箱
+	Create(session *Session, path string) CommandResponse             // 创建邮箱
+	Delete(session *Session, path string) CommandResponse             // 删除邮箱
+	Rename(session *Session, oldPath, newPath string) CommandResponse // 重命名邮箱
+	List(session *Session, basePath, template string) CommandResponse // 浏览邮箱
+	Append(session *Session, item string) CommandResponse             // 上传邮件
+	Select(session *Session, path string) CommandResponse             // 选择邮箱
 	/*
 		读取邮件的文本信息,且仅用于显示的目的。
 			ALL:只返回按照一定格式的邮件摘要,包括邮件标志、RFC822.SIZE、自身的时间和信封信息。IMAP客户机能够将标准邮件解析成这些信息并显示出来。
@@ -29,14 +29,14 @@ type Action interface {
 			BODY[MIME]:返回邮件的[MIME-IMB]的头信息,在正常情况下跟BODY[HEADER]没有区别。
 			BODY[TEXT]:返回整个邮件体,这里的邮件体并不包括邮件头。
 			**/
-	Fetch(session *Session, mailIds, dataNames string) (string, error)
-	Store(session *Session, mailId, flags string) error            // STORE 命令用于修改指定邮件的属性,包括给邮件打上已读标记、删除标记
-	Close(session *Session) error                                  // 关闭文件夹
-	Expunge(session *Session) error                                // 删除已经标记为删除的邮件,释放服务器上的存储空间
-	Examine(session *Session, path string) error                   // 只读方式打开邮箱
-	Subscribe(session *Session, path string) error                 // 活动邮箱列表中增加一个邮箱
-	UnSubscribe(session *Session, path string) error               // 活动邮箱列表中去掉一个邮箱
-	LSub(session *Session, path, mailbox string) ([]string, error) // 显示那些使用SUBSCRIBE命令设置为活动邮箱的文件
+	Fetch(session *Session, mailIds, dataNames string, uid bool) CommandResponse
+	Store(session *Session, mailId, flags string) CommandResponse // STORE 命令用于修改指定邮件的属性,包括给邮件打上已读标记、删除标记
+	Close(session *Session) CommandResponse                       // 关闭文件夹
+	Expunge(session *Session) CommandResponse                     // 删除已经标记为删除的邮件,释放服务器上的存储空间
+	Examine(session *Session, path string) CommandResponse        // 只读方式打开邮箱
+	Subscribe(session *Session, path string) CommandResponse      // 活动邮箱列表中增加一个邮箱
+	UnSubscribe(session *Session, path string) CommandResponse    // 活动邮箱列表中去掉一个邮箱
+	LSub(session *Session, path, mailbox string) CommandResponse  // 显示那些使用SUBSCRIBE命令设置为活动邮箱的文件
 	/*
 		@category:
 			MESSAGES	邮箱中的邮件总数
@@ -45,15 +45,15 @@ type Action interface {
 			UIDVALIDITY	邮箱的UID有效性标志
 			UNSEEN	邮箱中没有被标志为\UNSEEN的邮件数
 	*/
-	Status(session *Session, mailbox string, category []string) (string, error) // 查询邮箱的当前状态
-	Check(session *Session) error                                               // sync数据
-	Search(session *Session, keyword, criteria string) (string, error)          // 命令可以根据搜索条件在处于活动状态的邮箱中搜索邮件,然后显示匹配的邮件编号
-	Copy(session *Session, mailId, mailBoxName string) error                    // 把邮件从一个邮箱复制到另一个邮箱
-	CapaBility(session *Session) ([]string, error)                              // 返回IMAP服务器支持的功能列表
-	Noop(session *Session) error                                                // 什么都不做,连接保活
-	Login(session *Session, username, password string) error                    // 登录
-	Logout(session *Session) error                                              // 注销登录
-	IDLE(session *Session) error                                                // 进入IDLE状态
-	Unselect(session *Session) error                                            // 取消邮箱选择
-	Custom(session *Session, cmd string, args string) ([]string, error)
+	Status(session *Session, mailbox string, category []string) CommandResponse // 查询邮箱的当前状态
+	Check(session *Session) CommandResponse                                     // sync数据
+	Search(session *Session, keyword, criteria string) CommandResponse          // 命令可以根据搜索条件在处于活动状态的邮箱中搜索邮件,然后显示匹配的邮件编号
+	Copy(session *Session, mailId, mailBoxName string) CommandResponse          // 把邮件从一个邮箱复制到另一个邮箱
+	CapaBility(session *Session) CommandResponse                                // 返回IMAP服务器支持的功能列表
+	Noop(session *Session) CommandResponse                                      // 什么都不做,连接保活
+	Login(session *Session, username, password string) CommandResponse          // 登录
+	Logout(session *Session) CommandResponse                                    // 注销登录
+	IDLE(session *Session) CommandResponse                                      // 进入IDLE状态
+	Unselect(session *Session) CommandResponse                                  // 取消邮箱选择
+	Custom(session *Session, cmd string, args string) CommandResponse
 }

+ 15 - 0
server/utils/goimap/dto.go

@@ -0,0 +1,15 @@
+package goimap
+
+type CommandResponseType uint8
+
+const (
+	SUCCESS CommandResponseType = 0
+	BAD     CommandResponseType = 1
+	NO      CommandResponseType = 2
+)
+
+type CommandResponse struct {
+	Type    CommandResponseType
+	Message string
+	Data    []string
+}

+ 591 - 330
server/utils/goimap/imap.go

@@ -3,6 +3,7 @@ package goimap
 import (
 	"bufio"
 	"crypto/tls"
+	"encoding/base64"
 	"errors"
 	"fmt"
 	log "github.com/sirupsen/logrus"
@@ -122,6 +123,557 @@ func (s *Server) Stop() {
 	s.stop <- true
 }
 
+func (s *Server) authenticate(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if args == "LOGIN" {
+		write(conn, "+ VXNlciBOYW1lAA=="+eol, "")
+		line, err2 := reader.ReadString('\n')
+		if err2 != nil {
+			if conn != nil {
+				_ = conn.Close()
+			}
+			session.Conn = nil
+			session.IN_IDLE = false
+			return
+		}
+		account, err := base64.StdEncoding.DecodeString(line)
+		if err != nil {
+			showBad(conn, "Data Error.", nub)
+			return
+		}
+		write(conn, "+ UGFzc3dvcmQA"+eol, "")
+		line, err = reader.ReadString('\n')
+		if err2 != nil {
+			if conn != nil {
+				_ = conn.Close()
+			}
+			session.Conn = nil
+			session.IN_IDLE = false
+			return
+		}
+		password, err := base64.StdEncoding.DecodeString(line)
+		res := s.Action.Login(session, string(account), string(password))
+		if res.Type == SUCCESS {
+			showSucc(conn, res.Message, nub)
+		} else if res.Type == BAD {
+			showBad(conn, res.Message, nub)
+		} else {
+			showNo(conn, res.Message, nub)
+		}
+	} else {
+		showBad(conn, "Unsupported AUTHENTICATE mechanism.", nub)
+	}
+}
+
+func (s *Server) capability(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	res := s.Action.CapaBility(session)
+	if res.Type == BAD {
+		write(conn, fmt.Sprintf("* BAD %s%s", res.Message, eol), nub)
+	} else {
+		ret := "*"
+		for _, command := range res.Data {
+			ret += " " + command
+		}
+		ret += eol
+		write(conn, ret, nub)
+		showSucc(conn, res.Message, nub)
+	}
+}
+
+func (s *Server) create(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	if args == "" {
+		paramsErr(conn, "CREATE", nub)
+		return
+	}
+	res := s.Action.Create(session, args)
+	showSucc(conn, res.Message, nub)
+}
+
+func (s *Server) delete(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	if args == "" {
+		paramsErr(conn, "DELETE", nub)
+		return
+	}
+	res := s.Action.Delete(session, args)
+	if res.Type == SUCCESS {
+		showSucc(conn, res.Message, nub)
+	} else if res.Type == BAD {
+		showBad(conn, res.Message, nub)
+	} else {
+		showNo(conn, res.Message, nub)
+	}
+}
+
+func (s *Server) rename(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	if args == "" {
+		paramsErr(conn, "RENAME", nub)
+	} else {
+		dt := strings.Split(args, " ")
+		res := s.Action.Rename(session, dt[0], dt[1])
+		if res.Type == SUCCESS {
+			showSucc(conn, res.Message, nub)
+		} else if res.Type == BAD {
+			showBad(conn, res.Message, nub)
+		} else {
+			showNo(conn, res.Message, nub)
+		}
+	}
+}
+
+func (s *Server) list(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	if args == "" {
+		paramsErr(conn, "LIST", nub)
+	} else {
+		dt := strings.Split(args, " ")
+		dt[0] = strings.Trim(dt[0], `"`)
+		dt[1] = strings.Trim(dt[1], `"`)
+		res := s.Action.List(session, dt[0], dt[1])
+		if res.Type == SUCCESS {
+			showSuccWithData(conn, res.Data, res.Message, nub)
+		} else if res.Type == BAD {
+			showBad(conn, res.Message, nub)
+		} else {
+			showNo(conn, res.Message, nub)
+		}
+	}
+}
+
+func (s *Server) append(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	log.Debugf("Append: %+v", args)
+}
+
+func (s *Server) cselect(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	res := s.Action.Select(session, args)
+	if res.Type == SUCCESS {
+		showSuccWithData(conn, res.Data, res.Message, nub)
+	} else if res.Type == BAD {
+		showBad(conn, res.Message, nub)
+	} else {
+		showNo(conn, res.Message, nub)
+	}
+}
+
+func (s *Server) fetch(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader, uid bool) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	if args == "" {
+		paramsErr(conn, "FETCH", nub)
+	} else {
+		dt := strings.SplitN(args, " ", 2)
+		res := s.Action.Fetch(session, dt[0], dt[1], uid)
+		if res.Type == SUCCESS {
+			showSuccWithData(conn, res.Data, res.Message, nub)
+		} else if res.Type == BAD {
+			showBad(conn, res.Message, nub)
+		} else {
+			showNo(conn, res.Message, nub)
+		}
+	}
+}
+
+func (s *Server) store(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	if args == "" {
+		paramsErr(conn, "RENAME", nub)
+	} else {
+		dt := strings.Split(args, " ")
+		res := s.Action.Store(session, dt[0], dt[1])
+		if res.Type == SUCCESS {
+			showSucc(conn, res.Message, nub)
+		} else if res.Type == BAD {
+			showBad(conn, res.Message, nub)
+		} else {
+			showNo(conn, res.Message, nub)
+		}
+	}
+}
+
+func (s *Server) cclose(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	res := s.Action.Close(session)
+	if res.Type == SUCCESS {
+		showSucc(conn, res.Message, nub)
+	} else if res.Type == BAD {
+		showBad(conn, res.Message, nub)
+	} else {
+		showNo(conn, res.Message, nub)
+	}
+}
+
+func (s *Server) expunge(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	res := s.Action.Expunge(session)
+	if res.Type == SUCCESS {
+		showSucc(conn, res.Message, nub)
+	} else if res.Type == BAD {
+		showBad(conn, res.Message, nub)
+	} else {
+		showNo(conn, res.Message, nub)
+	}
+}
+
+func (s *Server) examine(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	if args == "" {
+		paramsErr(conn, "EXAMINE", nub)
+	}
+	res := s.Action.Examine(session, args)
+	if res.Type == SUCCESS {
+		showSucc(conn, res.Message, nub)
+	} else if res.Type == BAD {
+		showBad(conn, res.Message, nub)
+	} else {
+		showNo(conn, res.Message, nub)
+	}
+}
+
+func (s *Server) unsubscribe(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	if args == "" {
+		paramsErr(conn, "UNSUBSCRIBE", nub)
+	} else {
+		res := s.Action.UnSubscribe(session, args)
+		if res.Type == SUCCESS {
+			showSucc(conn, res.Message, nub)
+		} else if res.Type == BAD {
+			showBad(conn, res.Message, nub)
+		} else {
+			showNo(conn, res.Message, nub)
+		}
+	}
+}
+
+func (s *Server) lsub(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	if args == "" {
+		paramsErr(conn, "LSUB", nub)
+	} else {
+		dt := strings.Split(args, " ")
+		res := s.Action.LSub(session, dt[0], dt[1])
+		if res.Type == SUCCESS {
+			showSucc(conn, res.Message, nub)
+		} else if res.Type == BAD {
+			showBad(conn, res.Message, nub)
+		} else {
+			showNo(conn, res.Message, nub)
+		}
+	}
+}
+
+func (s *Server) status(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	if args == "" {
+		paramsErr(conn, "STATUS", nub)
+	} else {
+		var mailBox string
+		var params []string
+		if strings.HasPrefix(args, `"`) {
+			dt := strings.Split(args, `"`)
+			if len(dt) >= 3 {
+				mailBox = dt[1]
+			}
+			dt[2] = strings.Trim(dt[2], "() ")
+			params = strings.Split(dt[2], " ")
+		} else {
+			dt := strings.SplitN(args, " ", 2)
+			dt[0] = strings.ReplaceAll(dt[0], `"`, "")
+			dt[1] = strings.Trim(dt[1], "()")
+			mailBox = dt[0]
+			params = strings.Split(dt[1], " ")
+		}
+
+		res := s.Action.Status(session, mailBox, params)
+		if res.Type == SUCCESS {
+			showSuccWithData(conn, res.Data, res.Message, nub)
+		} else if res.Type == BAD {
+			showBad(conn, res.Message, nub)
+		} else {
+			showNo(conn, res.Message, nub)
+		}
+	}
+}
+
+func (s *Server) check(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	res := s.Action.Check(session)
+	if res.Type == SUCCESS {
+		showSucc(conn, res.Message, nub)
+	} else if res.Type == BAD {
+		showBad(conn, res.Message, nub)
+	} else {
+		showNo(conn, res.Message, nub)
+	}
+}
+
+func (s *Server) search(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	if args == "" {
+		paramsErr(conn, "SEARCH", nub)
+	} else {
+		dt := strings.SplitN(args, " ", 2)
+		res := s.Action.Search(session, dt[0], dt[1])
+		if res.Type == SUCCESS {
+			showSucc(conn, res.Message, nub)
+		} else if res.Type == BAD {
+			showBad(conn, res.Message, nub)
+		} else {
+			showNo(conn, res.Message, nub)
+		}
+	}
+}
+
+func (s *Server) copy(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	if args == "" {
+		paramsErr(conn, "COPY", nub)
+	} else {
+		dt := strings.SplitN(args, " ", 2)
+		res := s.Action.Copy(session, dt[0], dt[1])
+		if res.Type == SUCCESS {
+			showSucc(conn, res.Message, nub)
+		} else if res.Type == BAD {
+			showBad(conn, res.Message, nub)
+		} else {
+			showNo(conn, res.Message, nub)
+		}
+	}
+}
+
+func (s *Server) noop(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	res := s.Action.Noop(session)
+	if res.Type == SUCCESS {
+		showSucc(conn, res.Message, nub)
+	} else if res.Type == BAD {
+		showBad(conn, res.Message, nub)
+	} else {
+		showNo(conn, res.Message, nub)
+	}
+}
+
+func (s *Server) login(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if args == "" {
+		paramsErr(conn, "LOGIN", nub)
+	} else {
+		dt := strings.SplitN(args, " ", 2)
+		res := s.Action.Login(session, strings.Trim(dt[0], `"`), strings.Trim(dt[1], `"`))
+		if res.Type == SUCCESS {
+			showSucc(conn, res.Message, nub)
+		} else if res.Type == BAD {
+			showBad(conn, res.Message, nub)
+		} else {
+			showNo(conn, res.Message, nub)
+		}
+	}
+}
+
+func (s *Server) logout(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	res := s.Action.Logout(session)
+	write(conn, "* BYE PMail Server logging out"+eol, nub)
+	if res.Type == SUCCESS {
+		showSucc(conn, res.Message, nub)
+	} else if res.Type == BAD {
+		showBad(conn, res.Message, nub)
+	} else {
+		showNo(conn, res.Message, nub)
+	}
+	if conn != nil {
+		_ = conn.Close()
+	}
+}
+
+func (s *Server) unselect(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	res := s.Action.Unselect(session)
+	if res.Type == SUCCESS {
+		showSucc(conn, res.Message, nub)
+	} else if res.Type == BAD {
+		showBad(conn, res.Message, nub)
+	} else {
+		showNo(conn, res.Message, nub)
+	}
+}
+
+func (s *Server) subscribe(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	if args == "" {
+		paramsErr(conn, "SUBSCRIBE", nub)
+	} else {
+		res := s.Action.Subscribe(session, args)
+		if res.Type == SUCCESS {
+			showSucc(conn, res.Message, nub)
+		} else if res.Type == BAD {
+			showBad(conn, res.Message, nub)
+		} else {
+			showNo(conn, res.Message, nub)
+		}
+	}
+}
+
+func (s *Server) idle(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	if session.Status != AUTHORIZED {
+		showBad(conn, "Need Login", nub)
+		return
+	}
+	session.IN_IDLE = true
+	res := s.Action.IDLE(session)
+	if res.Type == SUCCESS {
+		write(conn, "+ idling"+eol, nub)
+	} else if res.Type == BAD {
+		showBad(conn, res.Message, nub)
+	} else {
+		showNo(conn, res.Message, nub)
+	}
+}
+
+func (s *Server) custom(session *Session, cmd string, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+	res := s.Action.Custom(session, cmd, args)
+	if res.Type == BAD {
+		write(conn, fmt.Sprintf("* BAD %s %s", res.Message, eol), nub)
+	} else if res.Type == NO {
+		showNo(conn, res.Message, nub)
+	} else {
+		if len(res.Data) == 0 {
+			showSucc(conn, res.Message, nub)
+		} else {
+			ret := ""
+			for _, re := range res.Data {
+				ret += fmt.Sprintf("%s%s", re, eol)
+			}
+			ret += "." + eol
+			write(conn, fmt.Sprintf(ret), nub)
+		}
+	}
+}
+
+func (s *Server) doCommand(session *Session, rawLine string, conn net.Conn, reader *bufio.Reader) {
+	nub, cmd, args := getCommand(rawLine)
+	log.Debugf("Imap Input:\t %s", rawLine)
+	if cmd != "IDLE" {
+		session.IN_IDLE = false
+	}
+
+	switch cmd {
+	case "":
+		if conn != nil {
+			conn.Close()
+			conn = nil
+		}
+		break
+
+	case "AUTHENTICATE":
+		s.authenticate(session, args, nub, conn, reader)
+	case "CAPABILITY":
+		s.capability(session, rawLine, nub, conn, reader)
+	case "CREATE":
+		s.create(session, args, nub, conn, reader)
+	case "DELETE":
+		s.delete(session, args, nub, conn, reader)
+	case "RENAME":
+		s.rename(session, args, nub, conn, reader)
+	case "LIST":
+		s.list(session, args, nub, conn, reader)
+	case "APPEND":
+		s.append(session, args, nub, conn, reader)
+	case "SELECT":
+		s.cselect(session, args, nub, conn, reader)
+	case "FETCH":
+		s.fetch(session, args, nub, conn, reader, false)
+	case "UID FETCH":
+		s.fetch(session, args, nub, conn, reader, true)
+	case "STORE":
+		s.store(session, args, nub, conn, reader)
+	case "CLOSE":
+		s.cclose(session, args, nub, conn, reader)
+	case "EXPUNGE":
+		s.expunge(session, args, nub, conn, reader)
+	case "EXAMINE":
+		s.examine(session, args, nub, conn, reader)
+	case "SUBSCRIBE":
+		s.subscribe(session, args, nub, conn, reader)
+	case "UNSUBSCRIBE":
+		s.unsubscribe(session, args, nub, conn, reader)
+	case "LSUB":
+		s.lsub(session, args, nub, conn, reader)
+	case "STATUS":
+		s.status(session, args, nub, conn, reader)
+	case "CHECK":
+		s.check(session, args, nub, conn, reader)
+	case "SEARCH":
+		s.search(session, args, nub, conn, reader)
+	case "COPY":
+		s.copy(session, args, nub, conn, reader)
+	case "NOOP":
+		s.noop(session, args, nub, conn, reader)
+	case "LOGIN":
+		s.login(session, args, nub, conn, reader)
+	case "LOGOUT":
+		s.logout(session, args, nub, conn, reader)
+	case "UNSELECT":
+		s.unselect(session, args, nub, conn, reader)
+	case "IDLE":
+		s.idle(session, args, nub, conn, reader)
+	default:
+		s.custom(session, cmd, args, nub, conn, reader)
+	}
+}
+
 func (s *Server) handleClient(conn net.Conn) {
 	slog.Debug("Imap conn")
 
@@ -159,7 +711,7 @@ func (s *Server) handleClient(conn net.Conn) {
 	}
 
 	reader := bufio.NewReader(conn)
-	write(conn, `* OK [CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN AUTH=LOGIN] PMail Server ready`, "")
+	write(conn, fmt.Sprintf(`* OK [CAPABILITY IMAP4 IMAP4rev1 AUTH=LOGIN] PMail Server ready%s`, eol), "")
 
 	for {
 		rawLine, err := reader.ReadString('\n')
@@ -173,316 +725,7 @@ func (s *Server) handleClient(conn net.Conn) {
 		}
 		session.AliveTime = time.Now()
 
-		nub, cmd, args := getCommand(rawLine)
-		log.Debugf("Imap Input:\t %s", rawLine)
-		if cmd != "IDLE" {
-			session.IN_IDLE = false
-		}
-
-		switch cmd {
-		case "":
-			if conn != nil {
-				conn.Close()
-				conn = nil
-			}
-			break
-
-		case "CAPABILITY":
-			commands, err := s.Action.CapaBility(session)
-			if err != nil {
-				write(conn, fmt.Sprintf("* BAD %s%s", err.Error(), eol), nub)
-			} else {
-				ret := "*"
-				for _, command := range commands {
-					ret += " " + command
-				}
-				write(conn, ret, nub)
-				showSucc(conn, nub)
-			}
-
-		case "CREATE":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			if args == "" {
-				paramsErr(conn, "CREATE", nub)
-				break
-			}
-			err := s.Action.Create(session, args)
-			output(conn, nub, err)
-		case "DELETE":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			if args == "" {
-				paramsErr(conn, "DELETE", nub)
-				break
-			}
-			err := s.Action.Delete(session, args)
-			output(conn, nub, err)
-		case "RENAME":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			if args == "" {
-				paramsErr(conn, "RENAME", nub)
-			} else {
-				dt := strings.Split(args, " ")
-				err := s.Action.Rename(session, dt[0], dt[1])
-				output(conn, nub, err)
-			}
-		case "LIST":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			if args == "" {
-				paramsErr(conn, "LIST", nub)
-			} else {
-				dt := strings.Split(args, " ")
-				dt[0] = strings.ReplaceAll(dt[0], `"`, "")
-				rets, err := s.Action.List(session, dt[0], dt[1])
-				if err != nil {
-					showBad(conn, err, nub)
-				} else {
-					ret := ""
-					for _, str := range rets {
-						ret += str + eol
-					}
-					write(conn, ret, nub)
-					showSucc(conn, nub)
-				}
-			}
-		case "APPEND":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			log.Debugf("Append: %+v", args)
-		case "SELECT":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			ret, err := s.Action.Select(session, args)
-			args = strings.ReplaceAll(args, `"`, "")
-			if err != nil {
-				showBad(conn, err, nub)
-			} else {
-				for _, s2 := range ret {
-					write(conn, s2, nub)
-				}
-			}
-
-		case "FETCH":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			if args == "" {
-				paramsErr(conn, "RENAME", nub)
-			} else {
-				dt := strings.Split(args, " ")
-				ret, err := s.Action.Fetch(session, dt[0], dt[1])
-				if err != nil {
-					showBad(conn, err, nub)
-				} else {
-					write(conn, ret, nub)
-					showSucc(conn, ret)
-				}
-			}
-		case "STORE":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			if args == "" {
-				paramsErr(conn, "RENAME", nub)
-			} else {
-				dt := strings.Split(args, " ")
-				err := s.Action.Store(session, dt[0], dt[1])
-				output(conn, nub, err)
-			}
-		case "CLOSE":
-			err := s.Action.Close(session)
-			output(conn, nub, err)
-		case "EXPUNGE":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			err := s.Action.Expunge(session)
-			output(conn, nub, err)
-		case "EXAMINE":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			if args == "" {
-				paramsErr(conn, "EXAMINE", nub)
-			}
-			err := s.Action.Examine(session, args)
-			output(conn, nub, err)
-		case "SUBSCRIBE":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			if args == "" {
-				paramsErr(conn, "SUBSCRIBE", nub)
-			} else {
-				err := s.Action.Subscribe(session, args)
-				output(conn, nub, err)
-			}
-		case "UNSUBSCRIBE":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			if args == "" {
-				paramsErr(conn, "UNSUBSCRIBE", nub)
-			} else {
-				err := s.Action.UnSubscribe(session, args)
-				output(conn, nub, err)
-			}
-		case "LSUB":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			if args == "" {
-				paramsErr(conn, "LSUB", nub)
-			} else {
-				dt := strings.Split(args, " ")
-				rets, err := s.Action.LSub(session, dt[0], dt[1])
-				if err != nil {
-					showBad(conn, err, nub)
-				} else {
-					ret := ""
-					for _, str := range rets {
-						ret += str + eol
-					}
-					write(conn, ret, nub)
-					showSucc(conn, nub)
-				}
-			}
-		case "STATUS":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			if args == "" {
-				paramsErr(conn, "STATUS", nub)
-			} else {
-				dt := strings.SplitN(args, " ", 2)
-				dt[0] = strings.ReplaceAll(dt[0], `"`, "")
-				dt[1] = strings.Trim(dt[1], "()")
-				params := strings.Split(dt[1], " ")
-
-				ret, err := s.Action.Status(session, dt[0], params)
-				if err != nil {
-					showBad(conn, err, nub)
-				} else {
-					write(conn, ret, nub)
-					showSucc(conn, nub)
-				}
-			}
-		case "CHECK":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			err := s.Action.Check(session)
-			output(conn, nub, err)
-		case "SEARCH":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			if args == "" {
-				paramsErr(conn, "SEARCH", nub)
-			} else {
-				dt := strings.SplitN(args, " ", 2)
-				ret, err := s.Action.Search(session, dt[0], dt[1])
-				if err != nil {
-					showBad(conn, err, nub)
-				} else {
-					write(conn, ret, nub)
-					showSucc(conn, nub)
-				}
-			}
-		case "COPY":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			if args == "" {
-				paramsErr(conn, "COPY", nub)
-			} else {
-				dt := strings.SplitN(args, " ", 2)
-				err := s.Action.Copy(session, dt[0], dt[1])
-				output(conn, nub, err)
-			}
-
-		case "NOOP":
-			err := s.Action.Noop(session)
-			output(conn, nub, err)
-		case "LOGIN":
-			if args == "" {
-				paramsErr(conn, "LOGIN", nub)
-			} else {
-				dt := strings.SplitN(args, " ", 2)
-				err := s.Action.Login(session, dt[0], dt[1])
-				output(conn, nub, err)
-			}
-		case "LOGOUT":
-			err := s.Action.Logout(session)
-			write(conn, "* BYE PMail Server logging out", nub)
-			output(conn, nub, err)
-			if conn != nil {
-				_ = conn.Close()
-			}
-		case "UNSELECT":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			err := s.Action.Unselect(session)
-			output(conn, nub, err)
-		case "IDLE":
-			if session.Status != AUTHORIZED {
-				showBad(conn, errors.New("Need Login"), nub)
-				break
-			}
-			session.IN_IDLE = true
-			err := s.Action.IDLE(session)
-			if err != nil {
-				write(conn, fmt.Sprintf("+ idling%s", eol), nub)
-			} else {
-				showBad(conn, err, nub)
-			}
-		default:
-			rets, err := s.Action.Custom(session, cmd, args)
-			if err != nil {
-				write(conn, fmt.Sprintf("* BAD %s %s", err.Error(), eol), nub)
-			} else {
-				if len(rets) == 0 {
-					write(conn, fmt.Sprintf("%s OK %s", nub, eol), nub)
-				} else if len(rets) == 1 {
-					write(conn, fmt.Sprintf("%s OK %s%s", nub, rets[0], eol), nub)
-				} else {
-					ret := fmt.Sprintf("%s OK %s", nub, eol)
-					for _, re := range rets {
-						ret += fmt.Sprintf("%s%s", re, eol)
-					}
-					ret += "." + eol
-					write(conn, fmt.Sprintf(ret), nub)
-				}
-			}
-		}
+		s.doCommand(session, rawLine, conn, reader)
 
 	}
 }
@@ -495,15 +738,18 @@ func getCommand(line string) (string, string, string) {
 		return "", "", ""
 	}
 
-	for i, s := range cmd {
-		cmd[i] = s
-	}
-
 	if len(cmd) == 3 {
-		return strings.ToTitle(cmd[0]), strings.ToTitle(cmd[1]), cmd[2]
+		if strings.ToTitle(cmd[1]) == "UID" {
+			args := strings.SplitN(cmd[2], " ", 2)
+			if len(args) >= 2 {
+				return cmd[0], strings.ToTitle(cmd[1]) + " " + strings.ToTitle(args[0]), args[1]
+			}
+		}
+
+		return cmd[0], strings.ToTitle(cmd[1]), cmd[2]
 	}
 
-	return strings.ToTitle(cmd[0]), strings.ToTitle(cmd[1]), ""
+	return cmd[0], strings.ToTitle(cmd[1]), ""
 }
 
 func getSafeArg(args []string, nr int) string {
@@ -513,24 +759,37 @@ func getSafeArg(args []string, nr int) string {
 	return ""
 }
 
-func showSucc(w io.Writer, nub string) {
-	write(w, fmt.Sprintf("%s OK success %s", nub, eol), nub)
+func showSucc(w io.Writer, msg, nub string) {
+	if msg == "" {
+		write(w, fmt.Sprintf("%s OK success %s", nub, eol), nub)
+	} else {
+		write(w, fmt.Sprintf("%s %s %s", nub, msg, eol), nub)
+	}
+}
+
+func showSuccWithData(w io.Writer, data []string, msg string, nub string) {
+	content := ""
+	for _, datum := range data {
+		content += fmt.Sprintf("%s%s", datum, eol)
+	}
+	content += fmt.Sprintf("%s OK %s%s", nub, msg, eol)
+	write(w, content, nub)
 }
 
-func showBad(w io.Writer, err error, nub string) {
-	if err == nil {
-		write(w, fmt.Sprintf("* BAD %s", eol), nub)
+func showBad(w io.Writer, err string, nub string) {
+	if nub == "" {
+		nub = "*"
+	}
+
+	if err == "" {
+		write(w, fmt.Sprintf("%s BAD %s", nub, eol), nub)
 		return
 	}
-	write(w, fmt.Sprintf("* BAD %s%s", err.Error(), eol), nub)
+	write(w, fmt.Sprintf("%s BAD %s%s", nub, err, eol), nub)
 }
 
-func output(w io.Writer, nub string, err error) {
-	if err != nil {
-		showBad(w, err, nub)
-	} else {
-		showSucc(w, nub)
-	}
+func showNo(w io.Writer, msg string, nub string) {
+	write(w, fmt.Sprintf("%s NO %s%s", nub, msg, eol), nub)
 }
 
 func paramsErr(w io.Writer, commend string, nub string) {
@@ -538,7 +797,9 @@ func paramsErr(w io.Writer, commend string, nub string) {
 }
 
 func write(w io.Writer, content string, nub string) {
-	content = strings.ReplaceAll(content, "$$NUM", nub)
+	if !strings.HasSuffix(content, eol) {
+		log.Errorf("Error:返回结尾错误  %s", content)
+	}
 	log.Debugf("Imap Out:\t |%s", content)
 	fmt.Fprintf(w, content)
 }

+ 117 - 0
server/utils/goimap/imap_test.go

@@ -1,8 +1,11 @@
 package goimap
 
 import (
+	"net"
+	"net/netip"
 	"reflect"
 	"testing"
+	"time"
 )
 
 func Test_paramsErr(t *testing.T) {
@@ -27,6 +30,13 @@ func Test_getCommand(t *testing.T) {
 			"STATUS",
 			`"Deleted Messages" (MESSAGES UIDNEXT UIDVALIDITY UNSEEN)`,
 		},
+		{
+			"LOGIN命令测试",
+			args{`a LOGIN admin 666666`},
+			"a",
+			"LOGIN",
+			`admin 666666`,
+		},
 		{
 			"SELECT命令测试",
 			args{`9.79 SELECT INBOX`},
@@ -41,6 +51,13 @@ func Test_getCommand(t *testing.T) {
 			"CAPABILITY",
 			``,
 		},
+		{
+			"DELETE命令测试",
+			args{`3.183 SELECT "Deleted Messages"`},
+			"3.183",
+			"SELECT",
+			`"Deleted Messages"`,
+		},
 		{
 			"异常命令测试",
 			args{`GET/HTTP/1.0`},
@@ -48,6 +65,27 @@ func Test_getCommand(t *testing.T) {
 			"",
 			``,
 		},
+		{
+			"FETCH命令测试",
+			args{`4.189 FETCH 7:38 (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (date subject from to cc message-id in-reply-to references content-type x-priority x-uniform-type-identifier x-universally-unique-identifier list-id list-unsubscribe bimi-indicator bimi-location x-bimi-indicator-hash authentication-results dkim-signature)])`},
+			"4.189",
+			"FETCH",
+			`7:38 (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (date subject from to cc message-id in-reply-to references content-type x-priority x-uniform-type-identifier x-universally-unique-identifier list-id list-unsubscribe bimi-indicator bimi-location x-bimi-indicator-hash authentication-results dkim-signature)])`,
+		},
+		{
+			"FETCH命令测试2",
+			args{`4.167 FETCH 1:41 (FLAGS UID)`},
+			"4.167",
+			"FETCH",
+			`1:41 (FLAGS UID)`,
+		},
+		{
+			"UID FETCH命令测试",
+			args{`4.200 UID FETCH 5 BODY.PEEK[HEADER]`},
+			"4.200",
+			"UID FETCH",
+			`5 BODY.PEEK[HEADER]`,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -64,3 +102,82 @@ func Test_getCommand(t *testing.T) {
 		})
 	}
 }
+
+type mockConn struct{}
+
+func (m mockConn) Read(b []byte) (n int, err error) {
+	return 0, err
+}
+
+func (m mockConn) Write(b []byte) (n int, err error) {
+	return 0, err
+}
+
+func (m mockConn) Close() error {
+	return nil
+}
+
+func (m mockConn) LocalAddr() net.Addr {
+	return net.TCPAddrFromAddrPort(netip.AddrPort{})
+}
+
+func (m mockConn) RemoteAddr() net.Addr {
+	return net.TCPAddrFromAddrPort(netip.AddrPort{})
+}
+
+func (m mockConn) SetDeadline(t time.Time) error {
+	return nil
+}
+
+func (m mockConn) SetReadDeadline(t time.Time) error {
+	return nil
+}
+
+func (m mockConn) SetWriteDeadline(t time.Time) error {
+	return nil
+}
+
+//
+//func TestServer_doCommand(t *testing.T) {
+//	type args struct {
+//		session *Session
+//		rawLine string
+//		conn    net.Conn
+//		reader  *bufio.Reader
+//	}
+//	tests := []struct {
+//		name string
+//		args args
+//	}{
+//		{
+//			name: "StatusTest",
+//			args: args{
+//				session: &Session{
+//					Status: AUTHORIZED,
+//
+//				},
+//				rawLine: `9.33 STATUS "Sent Messages" (MESSAGES UIDNEXT UIDVALIDITY UNSEEN)`,
+//				conn:    &mockConn{},
+//				reader:  &bufio.Reader{},
+//			},
+//		},
+//		{
+//			name: "StatusTest2",
+//			args: args{
+//				session: &Session{
+//					Status: AUTHORIZED,
+//				},
+//				rawLine: `9.33 STATUS INBOX (MESSAGES UIDNEXT UIDVALIDITY UNSEEN)`,
+//				conn:    &mockConn{},
+//				reader:  &bufio.Reader{},
+//			},
+//		},
+//	}
+//	for _, tt := range tests {
+//		t.Run(tt.name, func(t *testing.T) {
+//			s := &Server{
+//			}
+//			s.doCommand(tt.args.session, tt.args.rawLine, tt.args.conn, tt.args.reader)
+//		})
+//	}
+//}