Jelajahi Sumber

feature/v2.8.0

支持Imap协议
升级所有依赖
修复部分bug
jinnrry 1 tahun lalu
induk
melakukan
cf3bab6c9f
47 mengubah file dengan 1913 tambahan dan 843 penghapusan
  1. 8 11
      .github/workflows/unitTest.yml
  2. 3 3
      Makefile
  3. 5 0
      server/config/config.go
  4. 6 0
      server/consts/consts.go
  5. 2 2
      server/controllers/email/delete.go
  6. 1 8
      server/controllers/group.go
  7. 2 1
      server/db/init.go
  8. 27 0
      server/dto/parsemail/email.go
  9. 7 0
      server/dto/response/email.go
  10. 16 16
      server/go.mod
  11. 44 42
      server/go.sum
  12. 0 281
      server/listen/imap_server/action.go
  13. 0 195
      server/listen/imap_server/action_fetch.go
  14. 0 42
      server/listen/imap_server/action_fetch_test.go
  15. 34 19
      server/listen/imap_server/imap_server.go
  16. 175 2
      server/listen/imap_server/imap_server_test.go
  17. 75 0
      server/listen/imap_server/server.go
  18. 126 0
      server/listen/imap_server/session_copy.go
  19. 25 0
      server/listen/imap_server/session_create.go
  20. 29 0
      server/listen/imap_server/session_delete.go
  21. 33 0
      server/listen/imap_server/session_expunge.go
  22. 113 0
      server/listen/imap_server/session_fetch.go
  23. 52 0
      server/listen/imap_server/session_idle.go
  24. 106 0
      server/listen/imap_server/session_list.go
  25. 46 0
      server/listen/imap_server/session_login.go
  26. 78 0
      server/listen/imap_server/session_move.go
  27. 16 0
      server/listen/imap_server/session_namespace.go
  28. 20 0
      server/listen/imap_server/session_poll.go
  29. 34 0
      server/listen/imap_server/session_rename.go
  30. 46 0
      server/listen/imap_server/session_search.go
  31. 32 0
      server/listen/imap_server/session_select.go
  32. 40 0
      server/listen/imap_server/session_status.go
  33. 47 0
      server/listen/imap_server/session_store.go
  34. 8 1
      server/listen/pop3_server/action.go
  35. 11 5
      server/listen/smtp_server/read_content.go
  36. 46 1
      server/main_test.go
  37. 1 0
      server/models/group.go
  38. 9 6
      server/models/user_email.go
  39. 42 2
      server/services/del_email/del_email.go
  40. 53 1
      server/services/detail/detail.go
  41. 108 33
      server/services/group/group.go
  42. 122 12
      server/services/list/list.go
  43. 17 17
      server/utils/goimap/action.go
  44. 168 143
      server/utils/goimap/imap.go
  45. 9 0
      server/utils/goimap/imap_test.go
  46. 16 0
      server/utils/version/version.go
  47. 55 0
      server/utils/version/version_test.go

+ 8 - 11
.github/workflows/unitTest.yml

@@ -39,6 +39,9 @@ jobs:
       COMMIT: ${{ github.workflow_sha }}
       EVENT: ${{ github.event_name}}
     steps:
+      - name: Setup Node.js environment
+        run: apt update && apt install -y nodejs npm
+
       - name: Checkout
         uses: actions/checkout@v4
 
@@ -50,9 +53,6 @@ jobs:
           token: ${{ secrets.GITHUB_TOKEN }}
           issue-number: ${{ github.event.pull_request.number }}
           labels: 'Auto: Test Failed'
-
-      - name: Setup Node.js environment
-        run: apt update && apt install -y nodejs npm
         
       - name: Install Dependencies
         run: npm install --global yarn
@@ -60,15 +60,12 @@ jobs:
       - name: FE build
         run: make build_fe
 
+      - name: Run Test Mysql
+        run: make test_mysql
+
       - name: Run Test
         run: make test
 
-      - uses: actions/upload-artifact@v4
-        with:
-          name: dbfile
-          path: server/config/pmail_temp.db
-
-
-      - name: Run Test Mysql
-        run: make test_mysql
+#      - name: Run postgres
+#        run: make test_postgres
 

+ 3 - 3
Makefile

@@ -52,10 +52,10 @@ package: clean
 	cp README.md output/
 
 test:
-	export setup_port=17888 && cd server && export PMail_ROOT=$(CURDIR)/server/ && go test -v ./...
+	export setup_port=17888 && cd server && export PMail_ROOT=$(CURDIR)/server/ && go test -v -p 1 ./...
 
 test_mysql:
-	export setup_port=17888 && cd server && go test -args "mysql" -v ./...
+	export setup_port=17888 && cd server && export PMail_ROOT=$(CURDIR)/server/ && go test -args "mysql" -v -p 1 ./...
 
 test_postgres:
-	export setup_port=17888 && cd server && go test -args "postgres" -v ./...
+	export setup_port=17888 && cd server && export PMail_ROOT=$(CURDIR)/server/ && go test -args "postgres" -v -p 1 ./...

+ 5 - 0
server/config/config.go

@@ -222,6 +222,7 @@ func createNewPrivateKey() *ecdsa.PrivateKey {
 
 func WriteConfig(cfg *Config) error {
 	bytes, _ := json.Marshal(cfg)
+	_ = os.MkdirAll(ROOT_PATH+"/config/", 0755)
 	err := os.WriteFile(ROOT_PATH+"./config/config.json", bytes, 0666)
 	if err != nil {
 		return errors.Wrap(err)
@@ -237,19 +238,23 @@ func ReadConfig() (*Config, error) {
 	}
 	if !file.PathExist(ROOT_PATH + "./config/config.json") {
 		bytes, _ := json.Marshal(configData)
+		_ = os.MkdirAll(ROOT_PATH+"/config/", 0755)
 		err := os.WriteFile(ROOT_PATH+"./config/config.json", bytes, 0666)
 		if err != nil {
+			log.Errorf("Write Config Error:%s", err.Error())
 			return nil, errors.Wrap(err)
 		}
 	} else {
 		cfgData, err := os.ReadFile(ROOT_PATH + "./config/config.json")
 		if err != nil {
+			log.Errorf("Read Config Error:%s", err.Error())
 			return nil, errors.Wrap(err)
 		}
 
 		err = json.Unmarshal(cfgData, &configData)
 		configData.fixPath()
 		if err != nil {
+			log.Errorf("Read Config Unmarshal Error:%s", err.Error())
 			return nil, errors.Wrap(err)
 		}
 	}

+ 6 - 0
server/consts/consts.go

@@ -17,4 +17,10 @@ const (
 
 	//EmailStatusDel 3删除
 	EmailStatusDel int8 = 3
+
+	// EmailStatusDrafts 草稿箱
+	EmailStatusDrafts int8 = 4
+
+	// EmailStatusJunk 骚扰邮件
+	EmailStatusJunk int8 = 5
 )

+ 2 - 2
server/controllers/email/delete.go

@@ -11,8 +11,8 @@ import (
 )
 
 type emailDeleteRequest struct {
-	IDs       []int64 `json:"ids"`
-	ForcedDel bool    `json:"forcedDel"`
+	IDs       []int `json:"ids"`
+	ForcedDel bool  `json:"forcedDel"`
 }
 
 func EmailDelete(ctx *context.Context, w http.ResponseWriter, req *http.Request) {

+ 1 - 8
server/controllers/group.go

@@ -2,11 +2,9 @@ package controllers
 
 import (
 	"encoding/json"
-	"github.com/Jinnrry/pmail/db"
 	"github.com/Jinnrry/pmail/dto"
 	"github.com/Jinnrry/pmail/dto/response"
 	"github.com/Jinnrry/pmail/i18n"
-	"github.com/Jinnrry/pmail/models"
 	"github.com/Jinnrry/pmail/services/group"
 	"github.com/Jinnrry/pmail/utils/array"
 	"github.com/Jinnrry/pmail/utils/context"
@@ -67,13 +65,8 @@ func AddGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
 		log.WithContext(ctx).Errorf("%+v", err)
 	}
 
-	var newGroup models.Group = models.Group{
-		Name:     reqData.Name,
-		ParentId: reqData.ParentId,
-		UserId:   ctx.UserID,
-	}
+	newGroup, err := group.CreateGroup(ctx, reqData.Name, reqData.ParentId)
 
-	_, err = db.Instance.Insert(&newGroup)
 	if err != nil {
 		response.NewErrorResponse(response.ServerError, "DBError", err.Error()).FPrint(w)
 		return

+ 2 - 1
server/db/init.go

@@ -36,6 +36,7 @@ func Init(version string) error {
 		return errors.New("Database Type Error!")
 	}
 	if err != nil {
+		log.Errorf("DB init Error! %s", err.Error())
 		return errors.Wrap(err)
 	}
 
@@ -53,7 +54,7 @@ func Init(version string) error {
 		panic(err)
 	}
 
-	if version != "" && v.Info != version {
+	if version != "" && v.Info != version && version != "test" {
 		v.Info = version
 		Instance.Update(&v)
 	}

+ 27 - 0
server/dto/parsemail/email.go

@@ -13,6 +13,7 @@ import (
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cast"
 	"io"
+	"mime"
 	"net/textproto"
 	"regexp"
 	"strings"
@@ -24,6 +25,13 @@ type User struct {
 	Name         string `json:"Name"`
 }
 
+func (u User) Build() string {
+	if u.Name != "" {
+		return fmt.Sprintf("\"%s\" <%s>", mime.QEncoding.Encode("utf-8", u.Name), u.EmailAddress)
+	}
+	return fmt.Sprintf("<%s>", u.EmailAddress)
+}
+
 func (u User) GetDomainAccount() (string, string) {
 	infos := strings.Split(u.EmailAddress, "@")
 	if len(infos) >= 2 {
@@ -60,6 +68,25 @@ type Email struct {
 	Size        int
 }
 
+func users2String(users []*User) string {
+	ret := ""
+	for _, user := range users {
+		if ret != "" {
+			ret += ", "
+		}
+		ret += user.Build()
+	}
+	return ret
+}
+
+func (e *Email) BuildTo2String() string {
+	return users2String(e.To)
+}
+
+func (e *Email) BuildCc2String() string {
+	return users2String(e.Cc)
+}
+
 func NewEmailFromModel(d models.Email) *Email {
 
 	var To []*User

+ 7 - 0
server/dto/response/email.go

@@ -5,4 +5,11 @@ import "github.com/Jinnrry/pmail/models"
 type EmailResponseData struct {
 	models.Email `xorm:"extends"`
 	IsRead       int8 `json:"is_read"`
+	SerialNumber int  `json:"serial_number"`
+	UeId         int  `json:"ue_id"`
+}
+
+type UserEmailUIDData struct {
+	models.UserEmail `xorm:"extends"`
+	SerialNumber     int `json:"serial_number"`
 }

+ 16 - 16
server/go.mod

@@ -9,19 +9,20 @@ require (
 	github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885
 	github.com/alexedwards/scs/v2 v2.8.0
 	github.com/dlclark/regexp2 v1.11.4
+	github.com/emersion/go-imap/v2 v2.0.0-beta.4
 	github.com/emersion/go-message v0.18.1
 	github.com/emersion/go-msgauth v0.6.8
 	github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43
 	github.com/emersion/go-smtp v0.21.3
-	github.com/go-acme/lego/v4 v4.18.0
+	github.com/go-acme/lego/v4 v4.21.0
 	github.com/go-sql-driver/mysql v1.8.1
 	github.com/lib/pq v1.10.9
 	github.com/mileusna/spf v0.9.5
 	github.com/sirupsen/logrus v1.9.3
-	github.com/spf13/cast v1.7.0
-	golang.org/x/crypto v0.27.0
-	golang.org/x/text v0.18.0
-	modernc.org/sqlite v1.33.1
+	github.com/spf13/cast v1.7.1
+	golang.org/x/crypto v0.31.0
+	golang.org/x/text v0.21.0
+	modernc.org/sqlite v1.34.4
 	xorm.io/builder v0.3.13
 	xorm.io/xorm v1.3.9
 )
@@ -30,9 +31,8 @@ require (
 	filippo.io/edwards25519 v1.1.0 // indirect
 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
-	github.com/emersion/go-imap/v2 v2.0.0-beta.4 // indirect
 	github.com/go-jose/go-jose/v4 v4.0.4 // indirect
-	github.com/goccy/go-json v0.10.3 // indirect
+	github.com/goccy/go-json v0.10.4 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
@@ -48,16 +48,16 @@ require (
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/rogpeppe/go-internal v1.12.0 // indirect
 	github.com/syndtr/goleveldb v1.0.0 // indirect
-	golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
-	golang.org/x/mod v0.21.0 // indirect
-	golang.org/x/net v0.29.0 // indirect
-	golang.org/x/sync v0.8.0 // indirect
-	golang.org/x/sys v0.25.0 // indirect
-	golang.org/x/tools v0.25.0 // indirect
+	golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect
+	golang.org/x/mod v0.22.0 // indirect
+	golang.org/x/net v0.33.0 // indirect
+	golang.org/x/sync v0.10.0 // indirect
+	golang.org/x/sys v0.28.0 // indirect
+	golang.org/x/tools v0.28.0 // indirect
 	modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
-	modernc.org/libc v1.61.0 // indirect
-	modernc.org/mathutil v1.6.0 // indirect
+	modernc.org/libc v1.61.6 // indirect
+	modernc.org/mathutil v1.7.1 // indirect
 	modernc.org/memory v1.8.0 // indirect
-	modernc.org/strutil v1.2.0 // indirect
+	modernc.org/strutil v1.2.1 // indirect
 	modernc.org/token v1.1.0 // indirect
 )

+ 44 - 42
server/go.sum

@@ -15,8 +15,9 @@ github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gv
 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
 github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -37,18 +38,18 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
-github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
-github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-github.com/go-acme/lego/v4 v4.18.0 h1:2hH8KcdRBSb+p5o9VZIm61GAOXYALgILUCSs1Q+OYsk=
-github.com/go-acme/lego/v4 v4.18.0/go.mod h1:Blkg3izvXpl3zxk7WKngIuwR2I/hvYVP3vRnvgBp7m8=
+github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
+github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/go-acme/lego/v4 v4.21.0 h1:arEW+8o5p7VI8Bk1kr/PDlgD1DrxtTH1gJ4b7mehL8o=
+github.com/go-acme/lego/v4 v4.21.0/go.mod h1:HrSWzm3Ckj45Ie3i+p1zKVobbQoMOaGu9m4up0dUeDI=
 github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
 github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
 github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
 github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
-github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
-github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
+github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
 github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -112,22 +113,23 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo=
 github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
-github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
 github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -137,16 +139,16 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
-golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
-golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
-golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
+golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU=
+golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
-golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
+golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -157,16 +159,16 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
 golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
-golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
-golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -188,8 +190,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
-golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -199,16 +201,16 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
-golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
-golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
+golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
+golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -230,30 +232,30 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
-modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
-modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
-modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0=
+modernc.org/cc/v4 v4.24.2 h1:uektamHbSXU7egelXcyVpMaaAsrRH4/+uMKUQAQUdOw=
+modernc.org/cc/v4 v4.24.2/go.mod h1:T1lKJZhXIi2VSqGBiB4LIbKs9NsKTbUXj4IDrmGqtTI=
+modernc.org/ccgo/v4 v4.23.5 h1:6uAwu8u3pnla3l/+UVUrDDO1HIGxHTYmFH6w+X9nsyw=
+modernc.org/ccgo/v4 v4.23.5/go.mod h1:FogrWfBdzqLWm1ku6cfr4IzEFouq2fSAPf6aSAHdAJQ=
 modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
 modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
-modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
-modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
+modernc.org/gc/v2 v2.6.0 h1:Tiw3pezQj7PfV8k4Dzyu/vhRHR2e92kOXtTFU8pbCl4=
+modernc.org/gc/v2 v2.6.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
 modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbPMxoTpgAJeyePh0SmO8M=
 modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
-modernc.org/libc v1.61.0 h1:eGFcvWpqlnoGwzZeZe3PWJkkKbM/3SUGyk1DVZQ0TpE=
-modernc.org/libc v1.61.0/go.mod h1:DvxVX89wtGTu+r72MLGhygpfi3aUGgZRdAYGCAVVud0=
-modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
-modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/libc v1.61.6 h1:L2jW0wxHPCyHK0YSHaGaVlY0WxjpG/TTVdg6gRJOPqw=
+modernc.org/libc v1.61.6/go.mod h1:G+DzuaCcReUYYg4nNSfigIfTDCENdj9EByglvaRx53A=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
 modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
 modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
 modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
 modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
 modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
 modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
-modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
-modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
-modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
-modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
+modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
 modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
 modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
 xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=

+ 0 - 281
server/listen/imap_server/action.go

@@ -1,281 +0,0 @@
-package imap_server
-
-import (
-	"database/sql"
-	"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"
-	"github.com/Jinnrry/pmail/utils/id"
-	"github.com/Jinnrry/pmail/utils/password"
-	log "github.com/sirupsen/logrus"
-	"strings"
-	"sync"
-)
-
-var idlePool sync.Map
-
-func init() {
-	idlePool = sync.Map{}
-}
-
-// PushMsgByIDLE 向IMAP客户端通知新邮件消息
-func PushMsgByIDLE(ctx *context.Context, account string, unionId string) error {
-	sess, ok := idlePool.Load(account)
-	if ok {
-		sSessions, ok2 := sess.([]*goimap.Session)
-		if !ok2 {
-			idlePool.Delete(account)
-			return nil
-		}
-		newPool := []*goimap.Session{}
-		for _, sSession := range sSessions {
-			if sSession.IN_IDLE && sSession.Conn != nil {
-				fmt.Fprintf(sSession.Conn, fmt.Sprintf("* %s EXISTS", unionId))
-				newPool = append(newPool, sSession)
-			}
-		}
-		if len(newPool) == 0 {
-			idlePool.Delete(account)
-		} else {
-			idlePool.Store(account, newPool)
-		}
-	}
-	return nil
-}
-
-type action struct{}
-
-func (a action) Create(session *goimap.Session, path string) goimap.CommandResponse {
-	log.Infof("%s,%s", "Create", path)
-	return goimap.CommandResponse{}
-}
-
-func (a action) Delete(session *goimap.Session, path string) goimap.CommandResponse {
-	log.Infof("%s,%s", "Delete", path)
-	return goimap.CommandResponse{}
-}
-
-func (a action) Rename(session *goimap.Session, oldPath, newPath string) goimap.CommandResponse {
-	log.Infof("%s,%s,%s", "Rename", oldPath, newPath)
-	return goimap.CommandResponse{}
-}
-
-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 goimap.CommandResponse{
-			Type:    goimap.SUCCESS,
-			Data:    ret,
-			Message: "Success",
-		}
-	}
-
-	ret = group.MatchGroup(session.Ctx.(*context.Context), basePath, template)
-
-	return goimap.CommandResponse{
-		Type:    goimap.SUCCESS,
-		Data:    ret,
-		Message: "Success",
-	}
-}
-
-func (a action) Append(session *goimap.Session, item string) goimap.CommandResponse {
-	log.Infof("%s,%s", "Append", item)
-	return goimap.CommandResponse{}
-}
-
-func (a action) Select(session *goimap.Session, path string) goimap.CommandResponse {
-	log.Infof("%s,%s", "Select", path)
-	paths := strings.Split(path, "/")
-	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"]
-	ret = append(ret, fmt.Sprintf("* %d EXISTS", allNum))
-	ret = append(ret, fmt.Sprintf("* 0 RECENT"))
-	unRead := data["UNSEEN"]
-	ret = append(ret, fmt.Sprintf("* OK [UNSEEN %d]", unRead))
-	unionID := data["UIDVALIDITY"]
-	ret = append(ret, fmt.Sprintf("* OK [UIDVALIDITY %d] UID validity status", unionID))
-	nextID := data["UIDNEXT"]
-	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`)
-
-	return goimap.CommandResponse{
-		Type:    goimap.SUCCESS,
-		Data:    ret,
-		Message: "OK [READ-WRITE] SELECT complete",
-	}
-}
-
-func (a action) Store(session *goimap.Session, mailId, flags string) goimap.CommandResponse {
-	log.Infof("%s,%s,%s", "Store", mailId, flags)
-	return goimap.CommandResponse{}
-}
-
-func (a action) Close(session *goimap.Session) goimap.CommandResponse {
-	log.Infof("%s", "Close")
-	return goimap.CommandResponse{}
-}
-
-func (a action) Expunge(session *goimap.Session) goimap.CommandResponse {
-	log.Infof("%s", "Expunge")
-	return goimap.CommandResponse{}
-}
-
-func (a action) Examine(session *goimap.Session, path string) goimap.CommandResponse {
-	log.Infof("%s,%s", "Examine", path)
-	return goimap.CommandResponse{}
-}
-
-func (a action) Subscribe(session *goimap.Session, path string) goimap.CommandResponse {
-	log.Infof("%s,%s", "Subscribe", path)
-	return goimap.CommandResponse{}
-}
-
-func (a action) UnSubscribe(session *goimap.Session, path string) goimap.CommandResponse {
-	log.Infof("%s,%s", "UnSubscribe", path)
-	return goimap.CommandResponse{}
-}
-
-func (a action) LSub(session *goimap.Session, path, mailbox string) goimap.CommandResponse {
-	log.Infof("%s,%s,%s", "LSub", path, mailbox)
-	return goimap.CommandResponse{}
-}
-
-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 goimap.CommandResponse{
-		Type:    goimap.SUCCESS,
-		Data:    []string{fmt.Sprintf(`* STATUS "%s" %s`, mailbox, ret)},
-		Message: "STATUS completed",
-	}
-
-}
-
-func (a action) Check(session *goimap.Session) goimap.CommandResponse {
-	log.Infof("%s", "Check")
-	return goimap.CommandResponse{}
-}
-
-func (a action) Search(session *goimap.Session, keyword, criteria string) goimap.CommandResponse {
-	log.Infof("%s,%s,%s", "Search", keyword, criteria)
-	return goimap.CommandResponse{}
-}
-
-func (a action) Copy(session *goimap.Session, mailId, mailBoxName string) goimap.CommandResponse {
-	log.Infof("%s,%s,%s", "Copy", mailId, mailBoxName)
-	return goimap.CommandResponse{}
-}
-
-func (a action) CapaBility(session *goimap.Session) goimap.CommandResponse {
-	log.Infof("%s", "CapaBility")
-
-	return goimap.CommandResponse{
-		Type: goimap.SUCCESS,
-		Data: []string{
-			"CAPABILITY",
-			"IMAP4rev1",
-			"UNSELECT",
-			"IDLE",
-			"AUTH=LOGIN",
-		},
-	}
-
-}
-
-func (a action) IDLE(session *goimap.Session) goimap.CommandResponse {
-	pools, ok := idlePool.Load(session.Account)
-	if !ok {
-		idlePool.Store(session.Account, []*goimap.Session{
-			session,
-		})
-	} else {
-		sPools, ok := pools.([]*goimap.Session)
-		if !ok {
-			idlePool.Delete(session.Account)
-		} else {
-			sPools = append(sPools, session)
-			idlePool.Store(session.Account, sPools)
-		}
-	}
-	return goimap.CommandResponse{}
-}
-
-func (a action) Unselect(session *goimap.Session) goimap.CommandResponse {
-	log.Infof("%s", "Unselect")
-	session.CurrentPath = ""
-	return goimap.CommandResponse{}
-}
-
-func (a action) Noop(session *goimap.Session) goimap.CommandResponse {
-	log.Infof("%s", "Noop")
-	return goimap.CommandResponse{}
-}
-
-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, "@") {
-		datas := strings.Split(username, "@")
-		username = datas[0]
-	}
-
-	if session.Ctx == nil {
-		tc := &context.Context{}
-		tc.SetValue(context.LogID, id.GenLogID())
-		session.Ctx = tc
-	}
-
-	var user models.User
-
-	encodePwd := password.Encode(pwd)
-
-	_, err := db.Instance.Where("account =? and password =? and disabled = 0", username, encodePwd).Get(&user)
-	if err != nil && !errors.Is(err, sql.ErrNoRows) {
-		log.WithContext(session.Ctx.(*context.Context)).Errorf("%+v", err)
-	}
-
-	if user.ID > 0 {
-		session.Status = goimap.AUTHORIZED
-
-		session.Ctx.(*context.Context).UserID = user.ID
-		session.Ctx.(*context.Context).UserName = user.Name
-		session.Ctx.(*context.Context).UserAccount = user.Account
-
-		return goimap.CommandResponse{}
-	}
-
-	return goimap.CommandResponse{
-		Type:    goimap.NO,
-		Message: "[AUTHENTICATIONFAILED] Invalid credentials (Failure)",
-	}
-}
-
-func (a action) Logout(session *goimap.Session) goimap.CommandResponse {
-	session.Status = goimap.UNAUTHORIZED
-
-	return goimap.CommandResponse{
-		Type: goimap.SUCCESS,
-	}
-}
-
-func (a action) Custom(session *goimap.Session, cmd string, args string) goimap.CommandResponse {
-	log.Infof("Custom  %s,%+v", cmd, args)
-	return goimap.CommandResponse{}
-}

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

@@ -1,195 +0,0 @@
-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
-}

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

@@ -1,42 +0,0 @@
-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)
-			}
-		})
-	}
-}

+ 34 - 19
server/listen/imap_server/imap_server.go

@@ -1,40 +1,55 @@
 package imap_server
 
 import (
-	"crypto/rand"
 	"crypto/tls"
 	"github.com/Jinnrry/pmail/config"
-	"github.com/Jinnrry/pmail/utils/goimap"
+	"github.com/emersion/go-imap/v2"
+	"github.com/emersion/go-imap/v2/imapserver"
 	log "github.com/sirupsen/logrus"
-	"time"
+	"os"
 )
 
-var instanceTLS *goimap.Server
+var instanceTLS *imapserver.Server
+
+func Stop() {
+	if instanceTLS != nil {
+		instanceTLS.Close()
+		instanceTLS = nil
+	}
+}
 
 // StarTLS 启动TLS端口监听,不加密的代码就懒得写了
 func StarTLS() {
+
 	crt, err := tls.LoadX509KeyPair(config.Instance.SSLPublicKeyPath, config.Instance.SSLPrivateKeyPath)
 	if err != nil {
 		panic(err)
 	}
-	tlsConfig := &tls.Config{}
-	tlsConfig.Certificates = []tls.Certificate{crt}
-	tlsConfig.Time = time.Now
-	tlsConfig.Rand = rand.Reader
-	instanceTLS = goimap.NewImapServer(993, "imap."+config.Instance.Domain, true, tlsConfig, action{})
-	instanceTLS.ConnectAliveTime = 30 * time.Minute
+	tlsConfig := &tls.Config{
+		Certificates: []tls.Certificate{crt},
+	}
 
-	log.Infof("IMAP With TLS Server Start On Port :993")
+	memServer := NewServer()
 
-	err = instanceTLS.Start()
-	if err != nil {
-		panic(err)
+	option := &imapserver.Options{
+		NewSession: func(conn *imapserver.Conn) (imapserver.Session, *imapserver.GreetingData, error) {
+			return memServer.NewSession(), nil, nil
+		},
+		Caps: imap.CapSet{
+			imap.CapIMAP4rev1: {},
+			imap.CapIMAP4rev2: {},
+		},
+		TLSConfig:    tlsConfig,
+		InsecureAuth: false,
 	}
-}
 
-func Stop() {
-	if instanceTLS != nil {
-		instanceTLS.Stop()
-		instanceTLS = nil
+	if config.Instance.LogLevel == "debug" {
+		option.DebugWriter = os.Stdout
+	}
+
+	instanceTLS = imapserver.New(option)
+	log.Infof("IMAP With TLS Server Start On Port :993")
+	if err := instanceTLS.ListenAndServeTLS(":993"); err != nil {
+		panic(err)
 	}
 }

+ 175 - 2
server/listen/imap_server/imap_server_test.go

@@ -4,6 +4,8 @@ import (
 	"crypto/tls"
 	"github.com/Jinnrry/pmail/config"
 	"github.com/Jinnrry/pmail/db"
+	"github.com/Jinnrry/pmail/models"
+	"github.com/Jinnrry/pmail/utils/array"
 	"github.com/emersion/go-imap/v2"
 	"github.com/emersion/go-imap/v2/imapclient"
 	"github.com/emersion/go-message/charset"
@@ -68,14 +70,52 @@ func TestLogin(t *testing.T) {
 }
 
 func TestCreate(t *testing.T) {
+	err := clientLogin.Create("一级菜单", nil).Wait()
+	if err != nil {
+		t.Error(err)
+	}
 
-}
-func TestDelete(t *testing.T) {
+	err = clientLogin.Create("一级菜单/二级菜单", nil).Wait()
+	if err != nil {
+		t.Error(err)
+	}
+
+	res, err := clientLogin.List("", "*", nil).Collect()
+	if err != nil {
+		t.Error(err)
+	}
+	var mailbox []string
+	for _, v := range res {
+		mailbox = append(mailbox, v.Mailbox)
+	}
+
+	if !array.InArray("一级菜单", mailbox) || !array.InArray("一级菜单/二级菜单", mailbox) {
+		t.Error(mailbox)
+	}
 
 }
+
 func TestRename(t *testing.T) {
 
+	err := clientLogin.Rename("一级菜单", "主菜单").Wait()
+	if err != nil {
+		t.Error(err)
+	}
+
+	res, err := clientLogin.List("", "*", nil).Collect()
+	if err != nil {
+		t.Error(err)
+	}
+	var mailbox []string
+	for _, v := range res {
+		mailbox = append(mailbox, v.Mailbox)
+	}
+
+	if !array.InArray("主菜单", mailbox) {
+		t.Error(mailbox)
+	}
 }
+
 func TestList(t *testing.T) {
 	res, err := clientUnLogin.List("", "", &imap.ListOptions{}).Collect()
 
@@ -100,7 +140,54 @@ func TestList(t *testing.T) {
 		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")
+	}
+
+	if len(res) != 1 {
+		t.Error("List Error")
+	}
+
+	res, err = clientLogin.List("", "一级菜单/*", &imap.ListOptions{}).Collect()
+	if err != nil {
+		t.Error(err)
+	}
+	if len(res) != 1 {
+		t.Error("List Error")
+	}
+	if len(res) == 0 {
+		t.Error("List Error")
+	}
+
+}
+
+func TestDelete(t *testing.T) {
+
+	clientLogin.Create("一级菜单/二级菜单", nil).Wait()
+
+	err := clientLogin.Delete("二级菜单").Wait()
+	if err != nil {
+		t.Error(err)
+	}
+	res, err := clientLogin.List("", "*", nil).Collect()
+	if err != nil {
+		t.Error(err)
+	}
+	var mailbox []string
+	for _, v := range res {
+		mailbox = append(mailbox, v.Mailbox)
+	}
+
+	if array.InArray("二级菜单", mailbox) {
+		t.Error(mailbox)
+	}
+
 }
+
 func TestAppend(t *testing.T) {
 
 }
@@ -216,6 +303,17 @@ func TestFetch(t *testing.T) {
 	}
 }
 func TestStore(t *testing.T) {
+	res, err := clientLogin.Store(
+		imap.UIDSetNum(1),
+		&imap.StoreFlags{
+			Op:    imap.StoreFlagsAdd,
+			Flags: []imap.Flag{"\\Seen"},
+		},
+		&imap.StoreOptions{}).Collect()
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+	t.Logf("%+v", res)
 
 }
 func TestClose(t *testing.T) {
@@ -223,6 +321,20 @@ func TestClose(t *testing.T) {
 }
 func TestExpunge(t *testing.T) {
 
+	clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait()
+
+	res, err := clientLogin.UIDExpunge(imap.UIDSetNum(1, 2)).Collect()
+
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+	t.Logf("%+v", res)
+	var ues []models.UserEmail
+	db.Instance.Table("user_email").Where("id=1 or id=2").Find(&ues)
+	if len(ues) > 0 {
+		t.Errorf("TestExpunge Error")
+	}
+
 }
 func TestExamine(t *testing.T) {
 
@@ -241,10 +353,71 @@ func TestCheck(t *testing.T) {
 
 }
 func TestSearch(t *testing.T) {
+	clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait()
 
+	res, err := clientLogin.Search(&imap.SearchCriteria{
+		UID: []imap.UIDSet{
+			[]imap.UIDRange{
+				{Start: 1},
+			},
+			[]imap.UIDRange{
+				{Start: 2},
+			},
+			[]imap.UIDRange{
+				{Start: 2, Stop: 5},
+			},
+		},
+	}, &imap.SearchOptions{}).Wait()
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+	t.Logf("%+v", res)
 }
+func TestMove(t *testing.T) {
+	clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait()
+
+	_, err := clientLogin.Move(imap.UIDSetNum(21), "Junk").Wait()
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+	_, err = clientLogin.Move(imap.UIDSetNum(23), "一级菜单").Wait()
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+	var ue []models.UserEmail
+	db.Instance.Table("user_email").Where("id=21 or id=23").Find(&ue)
+	for _, v := range ue {
+		if v.ID == 21 && (v.GroupId != 0 || v.Status != 5) {
+			t.Errorf("TestMove Error")
+		}
+		if v.ID == 23 && v.GroupId != 4 {
+			t.Errorf("TestMove Error")
+		}
+	}
+
+}
+
 func TestCopy(t *testing.T) {
+	clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait()
+
+	res, err := clientLogin.Copy(imap.UIDSetNum(25), "Junk").Wait()
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+	t.Logf("%+v", res)
 
+	if !res.DestUIDs.Contains(33) {
+		t.Errorf("TestCopy Error")
+	}
+
+	res, err = clientLogin.Copy(imap.UIDSetNum(27), "一级菜单").Wait()
+	if err != nil {
+		t.Errorf("%+v", err)
+	}
+	t.Logf("%+v", res)
+	if !res.DestUIDs.Contains(34) {
+		t.Errorf("TestCopy Error")
+	}
 }
 
 func TestNoop(t *testing.T) {

+ 75 - 0
server/listen/imap_server/server.go

@@ -0,0 +1,75 @@
+package imap_server
+
+import (
+	"github.com/Jinnrry/pmail/utils/context"
+	"github.com/Jinnrry/pmail/utils/id"
+	"github.com/emersion/go-imap/v2"
+	log "github.com/sirupsen/logrus"
+	"sync"
+	"time"
+
+	"github.com/emersion/go-imap/v2/imapserver"
+)
+
+// Server is a server instance.
+//
+// A server contains a list of users.
+type Server struct {
+	mutex sync.Mutex
+}
+
+// NewServer creates a new server.
+func NewServer() *Server {
+	return &Server{}
+}
+
+type Status int8
+
+const (
+	UNAUTHORIZED Status = 1
+	AUTHORIZED   Status = 2
+	SELECTED     Status = 3
+	LOGOUT       Status = 4
+)
+
+type serverSession struct {
+	server         *Server // immutable
+	ctx            *context.Context
+	status         Status
+	currentMailbox string
+	connectTime    time.Time
+}
+
+// NewSession creates a new IMAP session.
+func (s *Server) NewSession() imapserver.Session {
+	tc := &context.Context{}
+	tc.SetValue(context.LogID, id.GenLogID())
+
+	return &serverSession{
+		server:      s,
+		ctx:         tc,
+		connectTime: time.Now(),
+	}
+}
+
+func (s *serverSession) Close() error {
+	return nil
+}
+
+func (s *serverSession) Subscribe(mailbox string) error {
+	return nil
+}
+
+func (s *serverSession) Unsubscribe(mailbox string) error {
+	return nil
+}
+
+func (s *serverSession) Append(mailbox string, r imap.LiteralReader, options *imap.AppendOptions) (*imap.AppendData, error) {
+	log.WithContext(s.ctx).Errorf("Append Not Implemented")
+	return nil, nil
+}
+
+func (s *serverSession) Unselect() error {
+	s.currentMailbox = ""
+	return nil
+}

+ 126 - 0
server/listen/imap_server/session_copy.go

@@ -0,0 +1,126 @@
+package imap_server
+
+import (
+	"github.com/Jinnrry/pmail/consts"
+	"github.com/Jinnrry/pmail/db"
+	"github.com/Jinnrry/pmail/dto/response"
+	"github.com/Jinnrry/pmail/models"
+	"github.com/Jinnrry/pmail/services/group"
+	"github.com/Jinnrry/pmail/services/list"
+	"github.com/Jinnrry/pmail/utils/context"
+	"github.com/emersion/go-imap/v2"
+	"github.com/spf13/cast"
+)
+
+func (s *serverSession) Copy(numSet imap.NumSet, dest string) (*imap.CopyData, error) {
+
+	var emailList []*response.EmailResponseData
+
+	switch numSet.(type) {
+	case imap.SeqSet:
+		seqSet := numSet.(imap.SeqSet)
+		for _, seq := range seqSet {
+			emailList = list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{
+				Star: cast.ToInt(seq.Start),
+				End:  cast.ToInt(seq.Stop),
+			}, false)
+		}
+	case imap.UIDSet:
+		uidSet := numSet.(imap.UIDSet)
+		for _, uid := range uidSet {
+			emailList = list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{
+				Star: cast.ToInt(uint32(uid.Start)),
+				End:  cast.ToInt(uint32(uid.Stop)),
+			}, true)
+		}
+	}
+
+	if len(emailList) == 0 {
+		return nil, &imap.Error{
+			Type: imap.StatusResponseTypeNo,
+			Text: "Email Not Found",
+		}
+	}
+
+	var err error
+	destUid := []int{}
+	UIDValidity := 0
+	if group.IsDefaultBox(dest) {
+		UIDValidity, destUid, err = copy2defaultbox(s.ctx, emailList, dest)
+	} else {
+		UIDValidity, destUid, err = copy2userbox(s.ctx, emailList, dest)
+	}
+	data := imap.CopyData{}
+	data.UIDValidity = cast.ToUint32(UIDValidity)
+	data.DestUIDs = imap.UIDSet{}
+	data.SourceUIDs = imap.UIDSet{}
+	for _, uid := range destUid {
+		data.DestUIDs = append(data.DestUIDs, imap.UIDRange{Start: imap.UID(cast.ToUint32(uid)), Stop: imap.UID(cast.ToUint32(uid))})
+	}
+
+	for _, email := range emailList {
+		data.SourceUIDs = append(data.SourceUIDs, imap.UIDRange{Start: imap.UID(cast.ToUint32(email.UeId)), Stop: imap.UID(cast.ToUint32(email.UeId))})
+	}
+
+	return &data, err
+}
+
+func copy2defaultbox(ctx *context.Context, mails []*response.EmailResponseData, dest string) (int, []int, error) {
+
+	var destUid []int
+	for _, email := range mails {
+
+		newUe := models.UserEmail{
+			UserID:  ctx.UserID,
+			EmailID: email.Id,
+			IsRead:  email.IsRead,
+			GroupId: 0,
+		}
+		switch dest {
+		case "Deleted Messages":
+			newUe.Status = consts.EmailStatusDel
+		case "INBOX":
+			newUe.Status = consts.EmailStatusWait
+		case "Sent Messages":
+			newUe.Status = consts.EmailStatusSent
+		case "Drafts":
+			newUe.Status = consts.EmailStatusDrafts
+		case "Junk":
+			newUe.Status = consts.EmailStatusJunk
+		}
+		db.Instance.Insert(&newUe)
+		destUid = append(destUid, newUe.ID)
+	}
+
+	return models.GroupNameToCode[dest], destUid, nil
+}
+
+func copy2userbox(ctx *context.Context, mails []*response.EmailResponseData, dest string) (int, []int, error) {
+	groupInfo, err := group.GetGroupByFullPath(ctx, dest)
+	if err != nil {
+		return 0, nil, &imap.Error{
+			Type: imap.StatusResponseTypeNo,
+			Text: err.Error(),
+		}
+	}
+	if groupInfo == nil || groupInfo.ID == 0 {
+		return 0, nil, &imap.Error{
+			Type: imap.StatusResponseTypeNo,
+			Text: "Group not found",
+		}
+	}
+	var destUid []int
+	for _, email := range mails {
+		newUe := models.UserEmail{
+			UserID:  ctx.UserID,
+			EmailID: email.Id,
+			IsRead:  email.IsRead,
+			GroupId: groupInfo.ID,
+			Status:  email.Status,
+		}
+		db.Instance.Insert(&newUe)
+		destUid = append(destUid, newUe.ID)
+	}
+
+	return groupInfo.ID, destUid, nil
+}

+ 25 - 0
server/listen/imap_server/session_create.go

@@ -0,0 +1,25 @@
+package imap_server
+
+import (
+	"github.com/Jinnrry/pmail/services/group"
+	"github.com/emersion/go-imap/v2"
+	"strings"
+)
+
+func (s *serverSession) Create(mailbox string, options *imap.CreateOptions) error {
+	groupPath := strings.Split(mailbox, "/")
+
+	var parentId int
+	for _, path := range groupPath {
+		newGroup, err := group.CreateGroup(s.ctx, path, parentId)
+		if err != nil {
+			return &imap.Error{
+				Type: imap.StatusResponseTypeNo,
+				Text: err.Error(),
+			}
+		}
+		parentId = newGroup.ID
+	}
+
+	return nil
+}

+ 29 - 0
server/listen/imap_server/session_delete.go

@@ -0,0 +1,29 @@
+package imap_server
+
+import (
+	"github.com/Jinnrry/pmail/services/group"
+	"github.com/emersion/go-imap/v2"
+	"strings"
+)
+
+func (s *serverSession) Delete(mailbox string) error {
+	groupPath := strings.Split(mailbox, "/")
+
+	groupName := groupPath[len(groupPath)-1]
+	groupInfo, err := group.GetGroupByName(s.ctx, groupName)
+	if err != nil {
+		return &imap.Error{
+			Type: imap.StatusResponseTypeNo,
+			Text: err.Error(),
+		}
+	}
+	_, err = group.DelGroup(s.ctx, groupInfo.ID)
+	if err != nil {
+		return &imap.Error{
+			Type: imap.StatusResponseTypeNo,
+			Text: err.Error(),
+		}
+	}
+
+	return nil
+}

+ 33 - 0
server/listen/imap_server/session_expunge.go

@@ -0,0 +1,33 @@
+package imap_server
+
+import (
+	"github.com/Jinnrry/pmail/services/del_email"
+	"github.com/emersion/go-imap/v2"
+	"github.com/emersion/go-imap/v2/imapserver"
+	"github.com/spf13/cast"
+)
+
+func (s *serverSession) Expunge(w *imapserver.ExpungeWriter, uids *imap.UIDSet) error {
+	if uids == nil {
+		return nil
+	}
+
+	uidList := []int{}
+	for _, uidRange := range *uids {
+		if uidRange.Start > 0 && uidRange.Stop > 0 {
+			for i := uidRange.Start; i <= uidRange.Stop; i++ {
+				uidList = append(uidList, cast.ToInt(uint32(i)))
+			}
+		}
+	}
+
+	err := del_email.DelByUID(s.ctx, uidList)
+	if err != nil {
+		return &imap.Error{
+			Type: imap.StatusResponseTypeNo,
+			Text: err.Error(),
+		}
+	}
+
+	return nil
+}

+ 113 - 0
server/listen/imap_server/session_fetch.go

@@ -0,0 +1,113 @@
+package imap_server
+
+import (
+	"bytes"
+	"fmt"
+	"github.com/Jinnrry/pmail/config"
+	"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/emersion/go-imap/v2"
+	"github.com/emersion/go-imap/v2/imapserver"
+	"github.com/spf13/cast"
+	"mime"
+	"strings"
+	"time"
+)
+
+func (s *serverSession) Fetch(w *imapserver.FetchWriter, numSet imap.NumSet, options *imap.FetchOptions) error {
+	switch numSet.(type) {
+	case imap.SeqSet:
+		seqSet := numSet.(imap.SeqSet)
+		for _, seq := range seqSet {
+			emailList := list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{
+				Star: cast.ToInt(seq.Start),
+				End:  cast.ToInt(seq.Stop),
+			}, false)
+			write(s.ctx, w, emailList, options)
+		}
+
+	case imap.UIDSet:
+		uidSet := numSet.(imap.UIDSet)
+		for _, uid := range uidSet {
+			emailList := list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{
+				Star: cast.ToInt(uint32(uid.Start)),
+				End:  cast.ToInt(uint32(uid.Stop)),
+			}, true)
+			write(s.ctx, w, emailList, options)
+		}
+	}
+	return nil
+}
+
+func write(ctx *context.Context, w *imapserver.FetchWriter, emailList []*response.EmailResponseData, options *imap.FetchOptions) {
+	for _, email := range emailList {
+		writer := w.CreateMessage(cast.ToUint32(email.SerialNumber))
+		if options.UID {
+			writer.WriteUID(imap.UID(email.UeId))
+		}
+		if options.RFC822Size {
+			emailContent := parsemail.NewEmailFromModel(email.Email).BuildBytes(ctx, false)
+			writer.WriteRFC822Size(cast.ToInt64(len(emailContent)))
+		}
+		if options.Flags {
+			if email.IsRead == 1 {
+				writer.WriteFlags([]imap.Flag{imap.FlagSeen})
+			} else {
+				writer.WriteFlags([]imap.Flag{})
+			}
+		}
+		if options.InternalDate {
+			writer.WriteInternalDate(email.CreateTime)
+		}
+		for _, section := range options.BodySection {
+			if !section.Peek {
+				detail.MakeRead(ctx, email.Id, true)
+			}
+			emailContent := parsemail.NewEmailFromModel(email.Email).BuildBytes(ctx, false)
+
+			if section.Specifier == imap.PartSpecifierNone || section.Specifier == imap.PartSpecifierText {
+				bodyWriter := writer.WriteBodySection(section, cast.ToInt64(len(emailContent)))
+				bodyWriter.Write(emailContent)
+				bodyWriter.Close()
+			}
+			if section.Specifier == imap.PartSpecifierHeader {
+				var b bytes.Buffer
+				parseEmail := parsemail.NewEmailFromModel(email.Email)
+				for _, field := range section.HeaderFields {
+					switch field {
+					case "date":
+						fmt.Fprintf(&b, "Date: %s\r\n", email.CreateTime.Format(time.RFC1123Z))
+					case "subject":
+						fmt.Fprintf(&b, "Subject: %s\r\n", mime.QEncoding.Encode("utf-8", email.Subject))
+					case "from":
+						if email.FromName != "" {
+							fmt.Fprintf(&b, "From: %s <%s>\r\n", mime.QEncoding.Encode("utf-8", email.FromName), email.FromAddress)
+						} else {
+							fmt.Fprintf(&b, "From: %s\r\n", email.FromAddress)
+						}
+					case "to":
+						fmt.Fprintf(&b, "To: %s\r\n", parseEmail.BuildTo2String())
+					case "cc":
+						if len(parseEmail.Cc) > 0 {
+							fmt.Fprintf(&b, "Cc: %s\r\n", parseEmail.BuildCc2String())
+						}
+					case "message-id":
+						fmt.Fprintf(&b, "Message-ID: %s\r\n", fmt.Sprintf("%d@%s", email.Id, config.Instance.Domain))
+					case "content-type":
+						args := strings.SplitN(string(emailContent), "\r\n", 3)
+						fmt.Fprintf(&b, "%s%s\r\n", args[0], args[1])
+					}
+				}
+
+				bodyWriter := writer.WriteBodySection(section, cast.ToInt64(b.Len()))
+				bodyWriter.Write(b.Bytes())
+				bodyWriter.Close()
+			}
+
+		}
+		writer.Close()
+	}
+}

+ 52 - 0
server/listen/imap_server/session_idle.go

@@ -0,0 +1,52 @@
+package imap_server
+
+import (
+	"github.com/Jinnrry/pmail/models"
+	"github.com/Jinnrry/pmail/utils/context"
+	"github.com/emersion/go-imap/v2/imapserver"
+	"github.com/spf13/cast"
+	"sync"
+)
+
+var userConnects sync.Map
+
+func (s *serverSession) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) error {
+	connects, ok := userConnects.Load(s.ctx.UserID)
+	logId := cast.ToString(s.ctx.GetValue(context.LogID))
+
+	if !ok {
+
+		connects = map[string]*imapserver.UpdateWriter{
+			logId: w,
+		}
+		userConnects.Store(s.ctx.UserID, connects)
+	} else {
+		connects := connects.(map[string]*imapserver.UpdateWriter)
+		if _, ok := connects[logId]; !ok {
+			connects[logId] = w
+			userConnects.Store(s.ctx.UserID, connects)
+		}
+	}
+
+	go func() {
+		<-stop
+		userConnects.Delete(logId)
+	}()
+
+	return nil
+}
+
+func IdleNotice(ctx *context.Context, userId int, email *models.Email) error {
+	if userId == 0 || email == nil || email.Id == 0 {
+		return nil
+	}
+
+	connects, ok := userConnects.Load(userId)
+	if ok {
+		connects := connects.(map[string]*imapserver.UpdateWriter)
+		for _, connect := range connects {
+			connect.WriteNumMessages(1)
+		}
+	}
+	return nil
+}

+ 106 - 0
server/listen/imap_server/session_list.go

@@ -0,0 +1,106 @@
+package imap_server
+
+import (
+	"github.com/Jinnrry/pmail/db"
+	"github.com/Jinnrry/pmail/models"
+	"github.com/Jinnrry/pmail/utils/context"
+	"github.com/emersion/go-imap/v2"
+	"github.com/emersion/go-imap/v2/imapserver"
+	log "github.com/sirupsen/logrus"
+	"strings"
+)
+
+func matchGroup(ctx *context.Context, w *imapserver.ListWriter, basePath, pattern string) {
+	var groups []*models.Group
+	if basePath == "" && pattern == "*" {
+		db.Instance.Table("group").Where("user_id=?", ctx.UserID).Find(&groups)
+		//w.WriteList(&imap.ListData{
+		//	Attrs:   []imap.MailboxAttr{imap.MailboxAttrNoSelect, imap.MailboxAttrHasChildren},
+		//	Delim:   '/',
+		//	Mailbox: "[PMail]",
+		//})
+		w.WriteList(&imap.ListData{
+			Attrs:   []imap.MailboxAttr{imap.MailboxAttrHasNoChildren},
+			Delim:   '/',
+			Mailbox: "INBOX",
+		})
+		w.WriteList(&imap.ListData{
+			Attrs:   []imap.MailboxAttr{imap.MailboxAttrHasNoChildren, imap.MailboxAttrSent},
+			Delim:   '/',
+			Mailbox: "Sent Messages",
+		})
+		w.WriteList(&imap.ListData{
+			Attrs:   []imap.MailboxAttr{imap.MailboxAttrHasNoChildren, imap.MailboxAttrDrafts},
+			Delim:   '/',
+			Mailbox: "Drafts",
+		})
+
+		w.WriteList(&imap.ListData{
+			Attrs:   []imap.MailboxAttr{imap.MailboxAttrHasNoChildren, imap.MailboxAttrTrash},
+			Delim:   '/',
+			Mailbox: "Deleted Messages",
+		})
+		w.WriteList(&imap.ListData{
+			Attrs:   []imap.MailboxAttr{imap.MailboxAttrHasNoChildren, imap.MailboxAttrJunk},
+			Delim:   '/',
+			Mailbox: "Junk",
+		})
+	} else {
+		pattern = strings.ReplaceAll(pattern, "/*", "/%")
+
+		db.Instance.Table("group").Where("user_id=? and full_path like ?", ctx.UserID, pattern).Find(&groups)
+
+	}
+	for _, group := range groups {
+
+		data := &imap.ListData{
+			Attrs:   []imap.MailboxAttr{},
+			Mailbox: group.Name,
+			Delim:   '/',
+		}
+
+		if hasChildren(ctx, group.ID) {
+			data.Attrs = append(data.Attrs, imap.MailboxAttrHasChildren)
+		}
+
+		data.Mailbox = getLayerName(ctx, group, true)
+
+		w.WriteList(data)
+
+	}
+
+}
+
+func hasChildren(ctx *context.Context, id int) bool {
+	var parent []*models.Group
+	db.Instance.Table("group").Where("parent_id=?", id).Find(&parent)
+	return len(parent) > 0
+}
+func getLayerName(ctx *context.Context, item *models.Group, allPath bool) string {
+	if item.ParentId == 0 {
+		return item.Name
+	}
+	var parent models.Group
+	_, _ = db.Instance.Table("group").Where("id=?", item.ParentId).Get(&parent)
+	if allPath {
+		return getLayerName(ctx, &parent, allPath) + "/" + item.Name
+	}
+	return getLayerName(ctx, &parent, allPath)
+}
+
+func (s *serverSession) List(w *imapserver.ListWriter, ref string, patterns []string, options *imap.ListOptions) error {
+	log.WithContext(s.ctx).Debugf("imap server list, ref: %s ,patterns: %s ", ref, patterns)
+
+	if ref == "" && len(patterns) == 0 {
+		w.WriteList(&imap.ListData{
+			Attrs:   []imap.MailboxAttr{imap.MailboxAttrNoSelect, imap.MailboxAttrHasChildren},
+			Delim:   '/',
+			Mailbox: "[PMail]",
+		})
+	}
+	for _, pattern := range patterns {
+		matchGroup(s.ctx, w, ref, pattern)
+	}
+
+	return nil
+}

+ 46 - 0
server/listen/imap_server/session_login.go

@@ -0,0 +1,46 @@
+package imap_server
+
+import (
+	"database/sql"
+	"github.com/Jinnrry/pmail/db"
+	"github.com/Jinnrry/pmail/models"
+	"github.com/Jinnrry/pmail/utils/errors"
+	"github.com/Jinnrry/pmail/utils/password"
+	"github.com/emersion/go-imap/v2"
+	log "github.com/sirupsen/logrus"
+	"strings"
+)
+
+func (s *serverSession) Login(username, pwd string) error {
+	if strings.Contains(username, "@") {
+		args := strings.Split(username, "@")
+		username = args[0]
+	}
+
+	var user models.User
+
+	encodePwd := password.Encode(pwd)
+
+	_, err := db.Instance.Where("account =? and password =? and disabled = 0", username, encodePwd).Get(&user)
+	if err != nil && !errors.Is(err, sql.ErrNoRows) {
+		log.WithContext(s.ctx).Errorf("%+v", err)
+	}
+
+	if user.ID > 0 {
+		s.status = AUTHORIZED
+
+		s.ctx.UserID = user.ID
+		s.ctx.UserName = user.Name
+		s.ctx.UserAccount = user.Account
+		log.WithContext(s.ctx).Debug("Login successful")
+
+		return nil
+	}
+
+	log.WithContext(s.ctx).Info("user not found")
+	return &imap.Error{
+		Type: imap.StatusResponseTypeNo,
+		Code: imap.ResponseCodeAuthenticationFailed,
+		Text: "Invalid credentials (Failure)",
+	}
+}

+ 78 - 0
server/listen/imap_server/session_move.go

@@ -0,0 +1,78 @@
+package imap_server
+
+import (
+	"github.com/Jinnrry/pmail/dto/response"
+	"github.com/Jinnrry/pmail/services/group"
+	"github.com/Jinnrry/pmail/services/list"
+	"github.com/Jinnrry/pmail/utils/context"
+	"github.com/emersion/go-imap/v2"
+	"github.com/emersion/go-imap/v2/imapserver"
+	"github.com/spf13/cast"
+)
+
+func (s *serverSession) Move(w *imapserver.MoveWriter, numSet imap.NumSet, dest string) error {
+
+	var emailList []*response.EmailResponseData
+
+	switch numSet.(type) {
+	case imap.SeqSet:
+		seqSet := numSet.(imap.SeqSet)
+		for _, seq := range seqSet {
+			emailList = list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{
+				Star: cast.ToInt(seq.Start),
+				End:  cast.ToInt(seq.Stop),
+			}, false)
+		}
+	case imap.UIDSet:
+		uidSet := numSet.(imap.UIDSet)
+		for _, uid := range uidSet {
+			emailList = list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{
+				Star: cast.ToInt(uint32(uid.Start)),
+				End:  cast.ToInt(uint32(uid.Stop)),
+			}, true)
+		}
+	}
+
+	var mailIds []int
+	for _, email := range emailList {
+		mailIds = append(mailIds, email.Id)
+	}
+
+	if group.IsDefaultBox(dest) {
+		return move2defaultbox(s.ctx, mailIds, dest)
+	} else {
+		return move2userbox(s.ctx, mailIds, dest)
+	}
+
+}
+
+func move2defaultbox(ctx *context.Context, mailIds []int, dest string) error {
+	err := group.Move2DefaultBox(ctx, mailIds, dest)
+	if err != nil {
+		return &imap.Error{
+			Type: imap.StatusResponseTypeNo,
+			Text: err.Error(),
+		}
+	}
+	return nil
+}
+
+func move2userbox(ctx *context.Context, mailIds []int, dest string) error {
+	groupInfo, err := group.GetGroupByFullPath(ctx, dest)
+	if err != nil {
+		return &imap.Error{
+			Type: imap.StatusResponseTypeNo,
+			Text: err.Error(),
+		}
+	}
+	if groupInfo == nil || groupInfo.ID == 0 {
+		return &imap.Error{
+			Type: imap.StatusResponseTypeNo,
+			Text: "Group not found",
+		}
+	}
+
+	group.MoveMailToGroup(ctx, mailIds, groupInfo.ID)
+
+	return nil
+}

+ 16 - 0
server/listen/imap_server/session_namespace.go

@@ -0,0 +1,16 @@
+package imap_server
+
+import (
+	"github.com/emersion/go-imap/v2"
+)
+
+func (s *serverSession) Namespace() (*imap.NamespaceData, error) {
+	return &imap.NamespaceData{
+		Personal: []imap.NamespaceDescriptor{
+			{
+				Prefix: "",
+				Delim:  '/',
+			},
+		},
+	}, nil
+}

+ 20 - 0
server/listen/imap_server/session_poll.go

@@ -0,0 +1,20 @@
+package imap_server
+
+import (
+	"github.com/Jinnrry/pmail/db"
+	"github.com/Jinnrry/pmail/models"
+	"github.com/emersion/go-imap/v2/imapserver"
+	"github.com/spf13/cast"
+)
+
+func (s *serverSession) Poll(w *imapserver.UpdateWriter, allowExpunge bool) error {
+
+	var ue []models.UserEmail
+	db.Instance.Table("user_email").Where("user_id=? and create >=?", s.ctx.UserID, s.connectTime).Find(&ue)
+
+	if len(ue) > 0 {
+		w.WriteNumMessages(cast.ToUint32(len(ue)))
+	}
+
+	return nil
+}

+ 34 - 0
server/listen/imap_server/session_rename.go

@@ -0,0 +1,34 @@
+package imap_server
+
+import (
+	"github.com/Jinnrry/pmail/services/group"
+	"github.com/emersion/go-imap/v2"
+	"strings"
+)
+
+func (s *serverSession) Rename(mailbox, newName string) error {
+	if group.IsDefaultBox(mailbox) {
+		return &imap.Error{
+			Type: imap.StatusResponseTypeNo,
+			Text: "This mailbox does not support rename.",
+		}
+	}
+
+	groupPath := strings.Split(mailbox, "/")
+
+	oldGroupName := groupPath[len(groupPath)-1]
+
+	newGroupPath := strings.Split(newName, "/")
+
+	newGroupName := newGroupPath[len(newGroupPath)-1]
+
+	err := group.Rename(s.ctx, oldGroupName, newGroupName)
+
+	if err != nil {
+		return &imap.Error{
+			Type: imap.StatusResponseTypeNo,
+			Text: err.Error(),
+		}
+	}
+	return nil
+}

+ 46 - 0
server/listen/imap_server/session_search.go

@@ -0,0 +1,46 @@
+package imap_server
+
+import (
+	"github.com/Jinnrry/pmail/dto/response"
+	"github.com/Jinnrry/pmail/services/list"
+	"github.com/emersion/go-imap/v2"
+	"github.com/emersion/go-imap/v2/imapserver"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/cast"
+)
+
+func (s *serverSession) Search(kind imapserver.NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) (*imap.SearchData, error) {
+	retList := []*response.UserEmailUIDData{}
+
+	for _, uidSet := range criteria.UID {
+		for _, uid := range uidSet {
+			res := list.GetUEListByUID(s.ctx, s.currentMailbox, cast.ToInt(uid.Start), cast.ToInt(uid.Stop), nil)
+			retList = append(retList, res...)
+		}
+	}
+	ret := &imap.SearchData{}
+
+	if kind == imapserver.NumKindSeq {
+		idList := imap.SeqSet{}
+		for _, data := range retList {
+			log.WithContext(s.ctx).Debugf("Search Seq result: UID: %d  EmailID:%d", data.ID, data.EmailID)
+			idList = append(idList, imap.SeqRange{
+				Start: cast.ToUint32(data.SerialNumber),
+				Stop:  cast.ToUint32(data.SerialNumber),
+			})
+		}
+		ret.All = idList
+	} else {
+		idList := imap.UIDSet{}
+		for _, data := range retList {
+			log.WithContext(s.ctx).Debugf("Search UID result: UID: %d  EmailID:%d", data.ID, data.EmailID)
+
+			idList = append(idList, imap.UIDRange{
+				Start: imap.UID(data.ID),
+				Stop:  imap.UID(data.ID),
+			})
+		}
+		ret.All = idList
+	}
+	return ret, nil
+}

+ 32 - 0
server/listen/imap_server/session_select.go

@@ -0,0 +1,32 @@
+package imap_server
+
+import (
+	"github.com/Jinnrry/pmail/services/group"
+	"github.com/emersion/go-imap/v2"
+	"github.com/spf13/cast"
+	"strings"
+)
+
+func (s *serverSession) Select(mailbox string, options *imap.SelectOptions) (*imap.SelectData, error) {
+	if "" == mailbox {
+		return nil, &imap.Error{
+			Type: imap.StatusResponseTypeBad,
+			Text: "mailbox not found",
+		}
+	}
+
+	paths := strings.Split(mailbox, "/")
+	s.currentMailbox = strings.Trim(paths[len(paths)-1], `"`)
+	_, data := group.GetGroupStatus(s.ctx, s.currentMailbox, []string{"MESSAGES", "UNSEEN", "UIDNEXT", "UIDVALIDITY"})
+
+	ret := &imap.SelectData{
+		Flags:          []imap.Flag{imap.FlagSeen},
+		PermanentFlags: []imap.Flag{imap.FlagSeen},
+		NumMessages:    cast.ToUint32(data["MESSAGES"]),
+		UIDNext:        imap.UID(data["UIDNEXT"]),
+		UIDValidity:    cast.ToUint32(data["UIDVALIDITY"]),
+	}
+
+	return ret, nil
+
+}

+ 40 - 0
server/listen/imap_server/session_status.go

@@ -0,0 +1,40 @@
+package imap_server
+
+import (
+	"github.com/Jinnrry/pmail/services/group"
+	"github.com/emersion/go-imap/v2"
+	"github.com/spf13/cast"
+)
+
+func (s *serverSession) Status(mailbox string, options *imap.StatusOptions) (*imap.StatusData, error) {
+	category := []string{}
+	if options.UIDNext {
+		category = append(category, "UIDNEXT")
+	}
+	if options.NumMessages {
+		category = append(category, "MESSAGES")
+	}
+	if options.UIDValidity {
+		category = append(category, "UIDVALIDITY")
+	}
+	if options.NumUnseen {
+		category = append(category, "UNSEEN")
+	}
+
+	_, data := group.GetGroupStatus(s.ctx, mailbox, category)
+
+	numMessages := cast.ToUint32(data["MESSAGES"])
+	numUnseen := cast.ToUint32(data["UNSEEN"])
+	numValidity := cast.ToUint32(data["UIDVALIDITY"])
+	numUIDNext := cast.ToUint32(data["UIDNEXT"])
+
+	ret := &imap.StatusData{
+		Mailbox:     mailbox,
+		NumMessages: &numMessages,
+		UIDNext:     imap.UID(numUIDNext),
+		UIDValidity: numValidity,
+		NumUnseen:   &numUnseen,
+	}
+
+	return ret, nil
+}

+ 47 - 0
server/listen/imap_server/session_store.go

@@ -0,0 +1,47 @@
+package imap_server
+
+import (
+	"github.com/Jinnrry/pmail/services/detail"
+	"github.com/Jinnrry/pmail/services/list"
+	"github.com/Jinnrry/pmail/utils/array"
+	"github.com/emersion/go-imap/v2"
+	"github.com/emersion/go-imap/v2/imapserver"
+	"github.com/spf13/cast"
+)
+
+func (s *serverSession) Store(w *imapserver.FetchWriter, numSet imap.NumSet, flags *imap.StoreFlags, options *imap.StoreOptions) error {
+	if flags.Op == imap.StoreFlagsSet {
+		return nil
+	}
+
+	if !array.InArray(imap.FlagSeen, flags.Flags) {
+		return nil
+	}
+
+	switch numSet.(type) {
+	case imap.SeqSet:
+		seqSet := numSet.(imap.SeqSet)
+		for _, seq := range seqSet {
+			emailList := list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{
+				Star: cast.ToInt(seq.Start),
+				End:  cast.ToInt(seq.Stop),
+			}, false)
+			for _, data := range emailList {
+				detail.MakeRead(s.ctx, data.Id, flags.Op == imap.StoreFlagsAdd)
+			}
+		}
+
+	case imap.UIDSet:
+		uidSet := numSet.(imap.UIDSet)
+		for _, uid := range uidSet {
+			emailList := list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{
+				Star: cast.ToInt(uint32(uid.Start)),
+				End:  cast.ToInt(uint32(uid.Stop)),
+			}, true)
+			for _, data := range emailList {
+				detail.MakeRead(s.ctx, data.Id, flags.Op == imap.StoreFlagsAdd)
+			}
+		}
+	}
+	return nil
+}

+ 8 - 1
server/listen/pop3_server/action.go

@@ -328,8 +328,15 @@ func (a action) Noop(session *gopop.Session) error {
 
 func (a action) Quit(session *gopop.Session) error {
 	log.WithContext(session.Ctx).Debugf("POP3 CMD: QUIT ")
+
+	var DelIds []int
+
 	if len(session.DeleteIds) > 0 {
-		del_email.DelEmail(session.Ctx.(*context.Context), session.DeleteIds, false)
+		for _, delId := range session.DeleteIds {
+			DelIds = append(DelIds, cast.ToInt(delId))
+		}
+
+		del_email.DelEmail(session.Ctx.(*context.Context), DelIds, false)
 	}
 
 	return nil

+ 11 - 5
server/listen/smtp_server/read_content.go

@@ -9,6 +9,7 @@ import (
 	"github.com/Jinnrry/pmail/dto/parsemail"
 	"github.com/Jinnrry/pmail/hooks"
 	"github.com/Jinnrry/pmail/hooks/framework"
+	"github.com/Jinnrry/pmail/listen/imap_server"
 	"github.com/Jinnrry/pmail/models"
 	"github.com/Jinnrry/pmail/services/rule"
 	"github.com/Jinnrry/pmail/utils/array"
@@ -85,7 +86,7 @@ func (s *Session) Data(r io.Reader) error {
 		}
 
 		// 转发
-		_, err := saveEmail(ctx, len(emailData), email, s.Ctx.UserID, 1, nil, true, true)
+		_, _, err := saveEmail(ctx, len(emailData), email, s.Ctx.UserID, 1, nil, true, true)
 		if err != nil {
 			log.WithContext(ctx).Errorf("Email Save Error %v", err)
 		}
@@ -159,7 +160,7 @@ func (s *Session) Data(r io.Reader) error {
 			return nil
 		}
 
-		users, _ := saveEmail(ctx, len(emailData), email, 0, 0, s.To, SPFStatus, dkimStatus)
+		users, dbEmail, _ := saveEmail(ctx, len(emailData), email, 0, 0, s.To, SPFStatus, dkimStatus)
 
 		if email.MessageId > 0 {
 			log.WithContext(ctx).Debugf("开始执行邮件规则!")
@@ -192,12 +193,17 @@ func (s *Session) Data(r io.Reader) error {
 		as3.Wait()
 		log.WithContext(ctx).Debugf("开始执行插件ReceiveSaveAfter!End")
 
+		// IDLE命令通知
+		for _, user := range users {
+			imap_server.IdleNotice(ctx, user.ID, dbEmail)
+		}
+
 	}
 
 	return nil
 }
 
-func saveEmail(ctx *context.Context, size int, email *parsemail.Email, sendUserID int, emailType int, reallyTo []string, SPFStatus, dkimStatus bool) ([]*models.User, error) {
+func saveEmail(ctx *context.Context, size int, email *parsemail.Email, sendUserID int, emailType int, reallyTo []string, SPFStatus, dkimStatus bool) ([]*models.User, *models.Email, error) {
 	var dkimV, spfV int8
 	if dkimStatus {
 		dkimV = 1
@@ -209,7 +215,7 @@ func saveEmail(ctx *context.Context, size int, email *parsemail.Email, sendUserI
 	log.WithContext(ctx).Debugf("开始入库!")
 
 	if email == nil {
-		return nil, nil
+		return nil, nil, nil
 	}
 
 	modelEmail := models.Email{
@@ -305,7 +311,7 @@ func saveEmail(ctx *context.Context, size int, email *parsemail.Email, sendUserI
 		}
 	}
 
-	return users, nil
+	return users, &modelEmail, nil
 }
 
 func json2string(d any) string {

+ 46 - 1
server/main_test.go

@@ -102,6 +102,10 @@ func TestMaster(t *testing.T) {
 	t.Run("testMoverEmailSend", testSendEmail2User2ForSpam)
 	time.Sleep(3 * time.Second)
 
+	// 生成10封测试邮件
+	t.Run("genTestEmailData", genTestEmailData)
+	time.Sleep(3 * time.Second)
+
 	// 检查规则执行
 	t.Run("testCheckRule", testCheckRule)
 	time.Sleep(3 * time.Second)
@@ -308,7 +312,9 @@ func testDataBaseSet(t *testing.T) {
 		t.Error(err)
 	}
 	if data.ErrorNo != 0 {
+		t.Errorf("Response %+v", data)
 		t.Error("Get Database Config Api Error!")
+		return
 	}
 
 	argList := flag.Args()
@@ -323,7 +329,7 @@ func testDataBaseSet(t *testing.T) {
 `
 	} else if array.InArray("postgres", argList) {
 		configData = `
-{"action":"set","step":"database","db_type":"postgres","db_dsn":"postgres://postgres:githubTest@127.0.0.1:5432/pmail?sslmode=disable"}
+{"action":"set","step":"database","db_type":"postgres","db_dsn":"postgres://postgres:githubTest@postgres:5432/pmail?sslmode=disable"}
 `
 	}
 
@@ -663,6 +669,45 @@ func testSendEmail2User2ForMove(t *testing.T) {
 
 }
 
+func genTestEmailData(t *testing.T) {
+	for i := 0; i < 10; i++ {
+		ret, err := httpClient.Post(TestHost+"/api/email/send", "application/json", strings.NewReader(fmt.Sprintf(
+			`
+		{
+    "from": {
+        "name": "user2",
+        "email": "user2@test.domain"
+    },
+    "to": [
+        {
+            "name": "admin",
+            "email": "admin@test.domain"
+        }
+    ],
+    "cc": [
+        
+    ],
+    "subject": "测试邮件%d",
+    "text": "测试邮件%d",
+    "html": "<div>测试邮件%d</div>"
+}
+
+`, i, i, i)))
+		if err != nil {
+			t.Error(err)
+		}
+		data, err := readResponse(ret.Body)
+		if err != nil {
+			t.Error(err)
+		}
+		if data.ErrorNo != 0 {
+			t.Error("Send Email Api Error!")
+		}
+		time.Sleep(3 * time.Second)
+	}
+
+}
+
 func testSendEmail2User1(t *testing.T) {
 	ret, err := httpClient.Post(TestHost+"/api/email/send", "application/json", strings.NewReader(`
 		{

+ 1 - 0
server/models/group.go

@@ -5,6 +5,7 @@ type Group struct {
 	Name     string `xorm:"varchar(10) notnull default('') comment('分组名称')" json:"name"`
 	ParentId int    `xorm:"parent_id int unsigned notnull default(0) comment('父分组名称')" json:"parent_id"`
 	UserId   int    `xorm:"user_id int unsigned notnull default(0) comment('用户id')" json:"-"`
+	FullPath string `xrom:"full_path varchar(600) comment('完整路径')" json:"full_path"`
 }
 
 const (

+ 9 - 6
server/models/user_email.go

@@ -1,12 +1,15 @@
 package models
 
+import "time"
+
 type UserEmail struct {
-	ID      int  `xorm:"id int unsigned not null pk autoincr"`
-	UserID  int  `xorm:"user_id int not null index('idx_eid') index comment('用户id')"`
-	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删除 4草稿箱(Drafts)  5骚扰邮件(Junk)
+	ID      int       `xorm:"id int unsigned not null pk autoincr"`
+	UserID  int       `xorm:"user_id int not null index('idx_eid') index comment('用户id')"`
+	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删除 4草稿箱(Drafts)  5骚扰邮件(Junk)
+	Created time.Time `xorm:"create datetime created index('idx_create_time')"`
 }
 
 func (p UserEmail) TableName() string {

+ 42 - 2
server/services/del_email/del_email.go

@@ -5,11 +5,13 @@ import (
 	"github.com/Jinnrry/pmail/db"
 	"github.com/Jinnrry/pmail/models"
 	"github.com/Jinnrry/pmail/utils/context"
+	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cast"
 	"xorm.io/xorm"
 )
+import . "xorm.io/builder"
 
-func DelEmail(ctx *context.Context, ids []int64, forcedDel bool) error {
+func DelEmail(ctx *context.Context, ids []int, forcedDel bool) error {
 	session := db.Instance.NewSession()
 	defer session.Close()
 	if err := session.Begin(); err != nil {
@@ -31,7 +33,10 @@ type num struct {
 
 func deleteOne(ctx *context.Context, session *xorm.Session, id int64, forcedDel bool) error {
 	if !forcedDel {
-		_, err := session.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", id, ctx.UserID).Update(map[string]interface{}{"status": consts.EmailStatusDel})
+		_, err := session.Table(&models.UserEmail{}).Where("email_id=? and user_id=?", id, ctx.UserID).Update(map[string]interface{}{
+			"status":   consts.EmailStatusDel,
+			"group_id": 0,
+		})
 		return err
 	}
 	// 先删除关联关系
@@ -53,3 +58,38 @@ func deleteOne(ctx *context.Context, session *xorm.Session, id int64, forcedDel
 	}
 	return err
 }
+
+func DelByUID(ctx *context.Context, ids []int) error {
+	session := db.Instance.NewSession()
+	defer session.Close()
+	for _, id := range ids {
+		var ue models.UserEmail
+		session.Table("user_email").Where(Eq{"id": ids, "user_id": ctx.UserID}).Get(&ue)
+		if ue.ID == 0 {
+			log.WithContext(ctx).Warn("no user email found")
+			return nil
+		}
+		emailId := ue.EmailID
+
+		// 先删除关联关系
+		_, err := session.Table(&models.UserEmail{}).Where("id=? and user_id=?", id, ctx.UserID).Delete(&ue)
+		if err != nil {
+			session.Rollback()
+			return err
+		}
+
+		// 检查email是否还有人有权限
+		var Num num
+		_, err = session.Table(&models.UserEmail{}).Select("count(1) as num").Where("email_id=? ", emailId).Get(&Num)
+		if err != nil {
+			return err
+		}
+		if Num.Num == 0 {
+			var email models.Email
+			_, err = session.Table(&email).Where("id=?", id).Delete(&email)
+
+		}
+	}
+	session.Commit()
+	return nil
+}

+ 53 - 1
server/services/detail/detail.go

@@ -8,11 +8,14 @@ import (
 	"github.com/Jinnrry/pmail/dto/parsemail"
 	"github.com/Jinnrry/pmail/dto/response"
 	"github.com/Jinnrry/pmail/models"
+	"github.com/Jinnrry/pmail/services/list"
+	"github.com/Jinnrry/pmail/utils/array"
 	"github.com/Jinnrry/pmail/utils/context"
 	"github.com/Jinnrry/pmail/utils/errors"
 	log "github.com/sirupsen/logrus"
 	"strings"
 )
+import . "xorm.io/builder"
 
 func GetEmailDetail(ctx *context.Context, id int, markRead bool) (*response.EmailResponseData, error) {
 	// 先查是否是本人的邮件
@@ -57,11 +60,60 @@ func GetEmailDetail(ctx *context.Context, id int, markRead bool) (*response.Emai
 	return &email, nil
 }
 
-func MakeRead(ctx *context.Context, emailId int) {
+func MakeRead(ctx *context.Context, emailId int, hadRead bool) {
 	ue := models.UserEmail{
 		UserID:  ctx.UserID,
 		IsRead:  1,
 		EmailID: emailId,
 	}
+	if !hadRead {
+		ue.IsRead = 0
+	}
+
 	db.Instance.Where("email_id = ? and user_id=?", emailId, ctx.UserID).Cols("is_read").Update(&ue)
 }
+
+func FindUE(ctx *context.Context, groupName string, req list.ImapListReq, uid bool) []models.UserEmail {
+	var ue []models.UserEmail
+	if uid {
+		err := db.Instance.Where(Eq{"id": req.UidList}).Find(&ue)
+		if err != nil {
+			log.WithContext(ctx).Errorf("SQL error:%+v", err)
+		}
+		return ue
+	} else {
+		sql := fmt.Sprintf("SELECT id,email_id, is_read from (SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and status = ?)) a WHERE serial_number in (%s)",
+			array.Join(req.UidList, ","),
+		)
+		switch groupName {
+		case "INBOX":
+			db.Instance.SQL(sql, ctx.UserID, 0).Find(&ue)
+		case "Sent Messages":
+			db.Instance.SQL(sql, ctx.UserID, 1).Find(&ue)
+		case "Drafts":
+			db.Instance.SQL(sql, ctx.UserID, 4).Find(&ue)
+		case "Deleted Messages":
+			db.Instance.SQL(sql, ctx.UserID, 3).Find(&ue)
+		case "Junk":
+			db.Instance.SQL(sql, ctx.UserID, 5).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 nil
+			}
+			db.Instance.
+				SQL(fmt.Sprintf(
+					"SELECT * from (SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and group_id = ?)) a WHERE serial_number in (%s)",
+					array.Join(req.UidList, ","),
+				)).
+				Find(&ue, ctx.UserID, group.ID)
+		}
+
+		return ue
+
+	}
+}

+ 108 - 33
server/services/group/group.go

@@ -1,16 +1,19 @@
 package group
 
 import (
+	errors2 "errors"
 	"fmt"
+	"github.com/Jinnrry/pmail/consts"
 	"github.com/Jinnrry/pmail/db"
 	"github.com/Jinnrry/pmail/dto"
 	"github.com/Jinnrry/pmail/models"
+	"github.com/Jinnrry/pmail/services/del_email"
 	"github.com/Jinnrry/pmail/utils/array"
 	"github.com/Jinnrry/pmail/utils/context"
 	"github.com/Jinnrry/pmail/utils/errors"
-	"github.com/Jinnrry/pmail/utils/utf7"
 	log "github.com/sirupsen/logrus"
 	"strings"
+	"xorm.io/builder"
 )
 
 type GroupItem struct {
@@ -20,6 +23,50 @@ type GroupItem struct {
 	Children []*GroupItem `json:"children"`
 }
 
+func CreateGroup(ctx *context.Context, name string, parentId int) (*models.Group, error) {
+	// 先查询是否存在
+	var group models.Group
+	db.Instance.Table("group").Where("name = ? and user_id = ?", name, ctx.UserID).Get(&group)
+	if group.ID > 0 {
+		return &group, nil
+	}
+	group.Name = name
+	group.ParentId = parentId
+	group.UserId = ctx.UserID
+	group.FullPath = getLayerName(ctx, &group, true)
+
+	_, err := db.Instance.Insert(&group)
+	return &group, err
+}
+
+func Rename(ctx *context.Context, oldName, newName string) error {
+	oldGroupInfo, err := GetGroupByName(ctx, oldName)
+	if err != nil {
+		return err
+	}
+	if oldGroupInfo == nil || oldGroupInfo.ID == 0 {
+		return errors2.New("group not found")
+	}
+	oldGroupInfo.Name = newName
+	oldGroupInfo.FullPath = getLayerName(ctx, oldGroupInfo, true)
+	_, err = db.Instance.ID(oldGroupInfo.ID).Update(oldGroupInfo)
+	return err
+}
+
+func GetGroupByName(ctx *context.Context, name string) (*models.Group, error) {
+	var group models.Group
+	db.Instance.Table("group").Where("name = ? and user_id = ?", name, ctx.UserID).Get(&group)
+
+	return &group, nil
+}
+
+func GetGroupByFullPath(ctx *context.Context, fullPath string) (*models.Group, error) {
+	var group models.Group
+	_, err := db.Instance.Table("group").Where("full_path = ? and user_id = ?", fullPath, ctx.UserID).Get(&group)
+
+	return &group, err
+}
+
 func DelGroup(ctx *context.Context, groupId int) (bool, error) {
 	allGroupIds := getAllChildId(ctx, groupId)
 	allGroupIds = append(allGroupIds, groupId)
@@ -38,7 +85,7 @@ func DelGroup(ctx *context.Context, groupId int) (bool, error) {
 		return false, errors.Wrap(err)
 	}
 
-	_, err = trans.Exec(db.WithContext(ctx, fmt.Sprintf("update email set group_id=0 where group_id in (%s)", array.Join(allGroupIds, ","))))
+	_, err = trans.Exec(db.WithContext(ctx, fmt.Sprintf("update user_email set group_id=0 where group_id in (%s)", array.Join(allGroupIds, ","))))
 	if err != nil {
 		trans.Rollback()
 		return false, errors.Wrap(err)
@@ -74,7 +121,9 @@ func GetGroupInfoList(ctx *context.Context) []*GroupItem {
 
 // MoveMailToGroup 将某封邮件移动到某个分组中
 func MoveMailToGroup(ctx *context.Context, mailId []int, groupId int) bool {
-	res, err := db.Instance.Exec(db.WithContext(ctx, fmt.Sprintf("update email set group_id=? where id in (%s)", array.Join(mailId, ","))), groupId)
+	res, err := db.Instance.Exec(db.WithContext(ctx,
+		fmt.Sprintf("update user_email set group_id=? where email_id in (%s) and user_id =?", array.Join(mailId, ","))),
+		groupId, ctx.UserID)
 	if err != nil {
 		log.WithContext(ctx).Errorf("SQL Error:%+v", err)
 		return false
@@ -122,47 +171,26 @@ func hasChildren(ctx *context.Context, id int) bool {
 	return len(parent) > 0
 }
 
-func getLayerName(ctx *context.Context, item *models.Group) string {
+func getLayerName(ctx *context.Context, item *models.Group, allPath bool) string {
 	if item.ParentId == 0 {
-		return utf7.Encode(item.Name)
+		return item.Name
 	}
 	var parent models.Group
 	_, _ = db.Instance.Table("group").Where("id=?", item.ParentId).Get(&parent)
-	return getLayerName(ctx, &parent) + "/" + utf7.Encode(item.Name)
+	if allPath {
+		return getLayerName(ctx, &parent, allPath) + "/" + item.Name
+	}
+	return getLayerName(ctx, &parent, allPath)
 }
 
-func MatchGroup(ctx *context.Context, basePath, template string) []string {
-	var groups []*models.Group
-	var ret []string
-	if basePath == "" {
-		db.Instance.Table("group").Where("user_id=?", ctx.UserID).Find(&groups)
-		ret = append(ret, `* LIST (\NoSelect \HasChildren) "/" "[PMail]"`)
-		ret = append(ret, `* LIST (\HasNoChildren) "/" "INBOX"`)
-		ret = append(ret, `* LIST (\HasNoChildren) "/" "Sent Messages"`)
-		ret = append(ret, `* LIST (\HasNoChildren) "/" "Drafts"`)
-		ret = append(ret, `* LIST (\HasNoChildren) "/" "Deleted Messages"`)
-		ret = append(ret, `* LIST (\HasNoChildren) "/" "Junk"`)
-	} else {
-		var parent *models.Group
-		db.Instance.Table("group").Where("user_id=? and name=?", ctx.UserID, basePath).Find(&groups)
-		if parent != nil && parent.ID > 0 {
-			db.Instance.Table("group").Where("user_id=? and parent_id=?", ctx.UserID, parent.ID).Find(&groups)
-		}
-	}
-	for _, group := range groups {
-		if hasChildren(ctx, group.ID) {
-			ret = append(ret, fmt.Sprintf(`* LIST (\HasChildren) "/" "[PMail]/%s"`, getLayerName(ctx, group)))
-		} else {
-			ret = append(ret, fmt.Sprintf(`* LIST (\HasNoChildren) "/" "[PMail]/%s"`, getLayerName(ctx, group)))
-		}
-	}
-	return ret
+func IsDefaultBox(box string) bool {
+	return array.InArray(box, []string{"INBOX", "Sent Messages", "Drafts", "Deleted Messages", "Junk"})
 }
 
 func GetGroupStatus(ctx *context.Context, groupName string, params []string) (string, map[string]int) {
 	retMap := map[string]int{}
 
-	if !array.InArray(groupName, []string{"INBOX", "Sent Messages", "Drafts", "Deleted Messages", "Junk"}) {
+	if !IsDefaultBox(groupName) {
 		groupNames := strings.Split(groupName, "/")
 		groupName = groupNames[len(groupNames)-1]
 
@@ -268,3 +296,50 @@ func getGroupNum(ctx *context.Context, groupName string, mustUnread bool) int {
 	}
 	return count
 }
+
+func Move2DefaultBox(ctx *context.Context, mailIds []int, groupName string) error {
+	switch groupName {
+	case "Deleted Messages":
+		err := del_email.DelEmail(ctx, mailIds, false)
+		if err != nil {
+			return err
+		}
+	case "INBOX":
+		_, err := db.Instance.Table(&models.UserEmail{}).Where(builder.Eq{
+			"user_id":  ctx.UserID,
+			"email_id": mailIds,
+		}).Update(map[string]interface{}{
+			"status":   consts.EmailTypeReceive,
+			"group_id": 0,
+		})
+		return err
+	case "Sent Messages":
+		_, err := db.Instance.Table(&models.UserEmail{}).Where(builder.Eq{
+			"user_id":  ctx.UserID,
+			"email_id": mailIds,
+		}).Update(map[string]interface{}{
+			"status":   consts.EmailStatusSent,
+			"group_id": 0,
+		})
+		return err
+	case "Drafts":
+		_, err := db.Instance.Table(&models.UserEmail{}).Where(builder.Eq{
+			"user_id":  ctx.UserID,
+			"email_id": mailIds,
+		}).Update(map[string]interface{}{
+			"status":   consts.EmailStatusDrafts,
+			"group_id": 0,
+		})
+		return err
+	case "Junk":
+		_, err := db.Instance.Table(&models.UserEmail{}).Where(builder.Eq{
+			"user_id":  ctx.UserID,
+			"email_id": mailIds,
+		}).Update(map[string]interface{}{
+			"status":   consts.EmailStatusJunk,
+			"group_id": 0,
+		})
+		return err
+	}
+	return nil
+}

+ 122 - 12
server/services/list/list.go

@@ -6,6 +6,7 @@ import (
 	"github.com/Jinnrry/pmail/dto"
 	"github.com/Jinnrry/pmail/dto/response"
 	"github.com/Jinnrry/pmail/models"
+	"github.com/Jinnrry/pmail/utils/array"
 	"github.com/Jinnrry/pmail/utils/context"
 	log "github.com/sirupsen/logrus"
 	"strings"
@@ -49,7 +50,7 @@ func genSQL(ctx *context.Context, count bool, tagInfo dto.SearchTag, keyword str
 	if tagInfo.Status != -1 {
 		sql += " and ue.status =? "
 		sqlParams = append(sqlParams, tagInfo.Status)
-	} else {
+	} else if tagInfo.GroupId == -1 {
 		sql += " and ue.status != 3"
 	}
 
@@ -98,24 +99,127 @@ 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
+type ImapListReq struct {
+	UidList []int
+	Star    int
+	End     int
+}
+
+func GetUEListByUID(ctx *context.Context, groupName string, star, end int, uidList []int) []*response.UserEmailUIDData {
+	var ue []*response.UserEmailUIDData
+	sql := "SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE user_id = ? "
+
+	params := []any{ctx.UserID}
+
+	if len(uidList) > 0 {
+		sql += fmt.Sprintf(" and id in (%s)", array.Join(uidList, ","))
+	}
+	if star > 0 {
+		sql += " and id >=?"
+		params = append(params, star)
+	}
+	if end > 0 {
+		sql += " and id <=?"
+		params = append(params, end)
+	}
+
+	switch groupName {
+	case "INBOX":
+		sql += " and status =?"
+		params = append(params, 0)
+	case "Sent Messages":
+		sql += " and status =?"
+		params = append(params, 1)
+	case "Drafts":
+		sql += " and status =?"
+		params = append(params, 4)
+	case "Deleted Messages":
+		sql += " and status =?"
+		params = append(params, 3)
+	case "Junk":
+		sql += " and status =?"
+		params = append(params, 5)
+	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 nil
+		}
+
+		sql += " and group_id = ?"
+		params = append(params, group.ID)
+	}
+
+	db.Instance.SQL(sql, params...).Find(&ue)
+	return ue
+}
+
+func getEmailListByUidList(ctx *context.Context, groupName string, req ImapListReq, uid bool) []*response.EmailResponseData {
+	var ret []*response.EmailResponseData
+	var ue []*response.UserEmailUIDData
+	sql := fmt.Sprintf("SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and id in (%s))", array.Join(req.UidList, ","))
+	if req.Star > 0 && req.End != 0 {
+		sql = fmt.Sprintf("SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and id >=%d and id <= %d)", req.Star, req.End)
+	}
+	if req.Star > 0 && req.End == 0 {
+		sql = fmt.Sprintf("SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and id >=%d )", req.Star)
+	}
+
+	err := db.Instance.SQL(sql, ctx.UserID).Find(&ue)
+	if err != nil {
+		log.WithContext(ctx).Errorf("SQL ERROR: %s ,Error:%s", sql, err)
+	}
+	ueMap := map[int]*response.UserEmailUIDData{}
+	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
+		ret[i].SerialNumber = ueMap[data.Id].SerialNumber
+		ret[i].UeId = ueMap[data.Id].ID
+	}
+
+	return ret
+}
+
+func GetEmailListByGroup(ctx *context.Context, groupName string, req ImapListReq, uid bool) []*response.EmailResponseData {
+	if len(req.UidList) == 0 && req.Star == 0 && req.End == 0 {
+		return nil
+	}
+
+	if uid {
+		return getEmailListByUidList(ctx, groupName, req, uid)
 	}
 
 	var ret []*response.EmailResponseData
-	var ue []*models.UserEmail
+	var ue []*response.UserEmailUIDData
+
+	sql := fmt.Sprintf("SELECT * from (SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and status = ? and group_id=0 )) a WHERE serial_number in (%s)", array.Join(req.UidList, ","))
+	if req.Star > 0 && req.End == 0 {
+		sql = fmt.Sprintf("SELECT * from (SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and status = ? and group_id=0 )) a WHERE serial_number >= %d", req.Star)
+	}
+	if req.Star > 0 && req.End > 0 {
+		sql = fmt.Sprintf("SELECT * from (SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and status = ? and group_id=0 )) a WHERE serial_number >= %d and serial_number <=%d", req.Star, req.End)
+	}
+
 	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)
+		db.Instance.SQL(sql, ctx.UserID, 0).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)
+		db.Instance.SQL(sql, ctx.UserID, 1).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)
+		db.Instance.SQL(sql, ctx.UserID, 4).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)
+		db.Instance.SQL(sql, ctx.UserID, 3).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)
+		db.Instance.SQL(sql, ctx.UserID, 5).Find(&ue)
 	default:
 		groupNames := strings.Split(groupName, "/")
 		groupName = groupNames[len(groupNames)-1]
@@ -125,10 +229,14 @@ func GetEmailListByGroup(ctx *context.Context, groupName string, offset, limit i
 		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)
+		db.Instance.
+			SQL(fmt.Sprintf(
+				"SELECT * from (SELECT id,email_id, is_read, ROW_NUMBER() OVER (ORDER BY id) AS serial_number FROM `user_email` WHERE (user_id = ? and group_id = ?)) a WHERE serial_number in (%s)",
+				array.Join(req.UidList, ","))).
+			Find(&ue, ctx.UserID, group.ID)
 	}
 
-	ueMap := map[int]*models.UserEmail{}
+	ueMap := map[int]*response.UserEmailUIDData{}
 	var emailIds []int
 	for _, email := range ue {
 		ueMap[email.EmailID] = email
@@ -138,6 +246,8 @@ func GetEmailListByGroup(ctx *context.Context, groupName string, offset, limit i
 	_ = db.Instance.Table("email").Select("*").Where(Eq{"id": emailIds}).Find(&ret)
 	for i, data := range ret {
 		ret[i].IsRead = ueMap[data.Id].IsRead
+		ret[i].SerialNumber = ueMap[data.Id].SerialNumber
+		ret[i].UeId = ueMap[data.Id].ID
 	}
 
 	return ret

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

@@ -30,13 +30,13 @@ type Action interface {
 			BODY[TEXT]:返回整个邮件体,这里的邮件体并不包括邮件头。
 			**/
 	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命令设置为活动邮箱的文件
+	Store(session *Session, mailId, flags string, uid bool) 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) 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                                  // 取消邮箱选择
+	Status(session *Session, mailbox string, category []string) CommandResponse  // 查询邮箱的当前状态
+	Check(session *Session) CommandResponse                                      // sync数据
+	Search(session *Session, keyword, criteria string, uid bool) 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
 }

+ 168 - 143
server/utils/goimap/imap.go

@@ -6,9 +6,9 @@ import (
 	"encoding/base64"
 	"errors"
 	"fmt"
+	"github.com/Jinnrry/pmail/utils/context"
+	"github.com/Jinnrry/pmail/utils/id"
 	log "github.com/sirupsen/logrus"
-	"io"
-	"log/slog"
 	"net"
 	"strings"
 	"sync"
@@ -125,7 +125,7 @@ func (s *Server) Stop() {
 
 func (s *Server) authenticate(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
 	if args == "LOGIN" {
-		write(conn, "+ VXNlciBOYW1lAA=="+eol, "")
+		write(session, "+ VXNlciBOYW1lAA=="+eol, "")
 		line, err2 := reader.ReadString('\n')
 		if err2 != nil {
 			if conn != nil {
@@ -137,10 +137,10 @@ func (s *Server) authenticate(session *Session, args string, nub string, conn ne
 		}
 		account, err := base64.StdEncoding.DecodeString(line)
 		if err != nil {
-			showBad(conn, "Data Error.", nub)
+			showBad(session, "Data Error.", nub)
 			return
 		}
-		write(conn, "+ UGFzc3dvcmQA"+eol, "")
+		write(session, "+ UGFzc3dvcmQA"+eol, "")
 		line, err = reader.ReadString('\n')
 		if err2 != nil {
 			if conn != nil {
@@ -153,165 +153,170 @@ func (s *Server) authenticate(session *Session, args string, nub string, conn ne
 		password, err := base64.StdEncoding.DecodeString(line)
 		res := s.Action.Login(session, string(account), string(password))
 		if res.Type == SUCCESS {
-			showSucc(conn, res.Message, nub)
+			showSucc(session, res.Message, nub)
 		} else if res.Type == BAD {
-			showBad(conn, res.Message, nub)
+			showBad(session, res.Message, nub)
 		} else {
-			showNo(conn, res.Message, nub)
+			showNo(session, res.Message, nub)
 		}
 	} else {
-		showBad(conn, "Unsupported AUTHENTICATE mechanism.", nub)
+		showBad(session, "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)
+		write(session, 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)
+		write(session, ret, nub)
+		showSucc(session, 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	if args == "" {
-		paramsErr(conn, "CREATE", nub)
+		paramsErr(session, "CREATE", nub)
 		return
 	}
 	res := s.Action.Create(session, args)
-	showSucc(conn, res.Message, nub)
+	showSucc(session, 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	if args == "" {
-		paramsErr(conn, "DELETE", nub)
+		paramsErr(session, "DELETE", nub)
 		return
 	}
 	res := s.Action.Delete(session, args)
 	if res.Type == SUCCESS {
-		showSucc(conn, res.Message, nub)
+		showSucc(session, res.Message, nub)
 	} else if res.Type == BAD {
-		showBad(conn, res.Message, nub)
+		showBad(session, res.Message, nub)
 	} else {
-		showNo(conn, res.Message, nub)
+		showNo(session, 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	if args == "" {
-		paramsErr(conn, "RENAME", nub)
+		paramsErr(session, "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)
+			showSucc(session, res.Message, nub)
 		} else if res.Type == BAD {
-			showBad(conn, res.Message, nub)
+			showBad(session, res.Message, nub)
 		} else {
-			showNo(conn, res.Message, nub)
+			showNo(session, 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	if args == "" {
-		paramsErr(conn, "LIST", nub)
+		paramsErr(session, "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)
+			showSuccWithData(session, res.Data, res.Message, nub)
 		} else if res.Type == BAD {
-			showBad(conn, res.Message, nub)
+			showBad(session, res.Message, nub)
 		} else {
-			showNo(conn, res.Message, nub)
+			showNo(session, 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
-	log.Debugf("Append: %+v", args)
+	log.WithContext(session.Ctx).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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	res := s.Action.Select(session, args)
 	if res.Type == SUCCESS {
-		showSuccWithData(conn, res.Data, res.Message, nub)
+		showSuccWithData(session, res.Data, res.Message, nub)
 	} else if res.Type == BAD {
-		showBad(conn, res.Message, nub)
+		showBad(session, res.Message, nub)
 	} else {
-		showNo(conn, res.Message, nub)
+		showNo(session, 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	if args == "" {
-		paramsErr(conn, "FETCH", nub)
+		paramsErr(session, "FETCH", nub)
 	} else {
 		dt := strings.SplitN(args, " ", 2)
+		if len(dt) != 2 {
+			showBad(session, "Error Params", nub)
+			return
+		}
+
 		res := s.Action.Fetch(session, dt[0], dt[1], uid)
 		if res.Type == SUCCESS {
-			showSuccWithData(conn, res.Data, res.Message, nub)
+			showSuccWithData(session, res.Data, res.Message, nub)
 		} else if res.Type == BAD {
-			showBad(conn, res.Message, nub)
+			showBad(session, res.Message, nub)
 		} else {
-			showNo(conn, res.Message, nub)
+			showNo(session, res.Message, nub)
 		}
 	}
 }
 
-func (s *Server) store(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+func (s *Server) store(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader, uid bool) {
 	if session.Status != AUTHORIZED {
-		showBad(conn, "Need Login", nub)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	if args == "" {
-		paramsErr(conn, "RENAME", nub)
+		paramsErr(session, "RENAME", nub)
 	} else {
-		dt := strings.Split(args, " ")
-		res := s.Action.Store(session, dt[0], dt[1])
+		dt := strings.SplitN(args, " ", 2)
+		res := s.Action.Store(session, dt[0], dt[1], uid)
 		if res.Type == SUCCESS {
-			showSucc(conn, res.Message, nub)
+			showSucc(session, res.Message, nub)
 		} else if res.Type == BAD {
-			showBad(conn, res.Message, nub)
+			showBad(session, res.Message, nub)
 		} else {
-			showNo(conn, res.Message, nub)
+			showNo(session, res.Message, nub)
 		}
 	}
 }
@@ -319,93 +324,94 @@ func (s *Server) store(session *Session, args string, nub string, conn net.Conn,
 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)
+		showSucc(session, res.Message, nub)
 	} else if res.Type == BAD {
-		showBad(conn, res.Message, nub)
+		showBad(session, res.Message, nub)
 	} else {
-		showNo(conn, res.Message, nub)
+		showNo(session, 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	res := s.Action.Expunge(session)
 	if res.Type == SUCCESS {
-		showSucc(conn, res.Message, nub)
+		showSucc(session, res.Message, nub)
 	} else if res.Type == BAD {
-		showBad(conn, res.Message, nub)
+		showBad(session, res.Message, nub)
 	} else {
-		showNo(conn, res.Message, nub)
+		showNo(session, 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	if args == "" {
-		paramsErr(conn, "EXAMINE", nub)
+		paramsErr(session, "EXAMINE", nub)
 	}
 	res := s.Action.Examine(session, args)
 	if res.Type == SUCCESS {
-		showSucc(conn, res.Message, nub)
+		showSucc(session, res.Message, nub)
 	} else if res.Type == BAD {
-		showBad(conn, res.Message, nub)
+		showBad(session, res.Message, nub)
 	} else {
-		showNo(conn, res.Message, nub)
+		showNo(session, 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	if args == "" {
-		paramsErr(conn, "UNSUBSCRIBE", nub)
+		paramsErr(session, "UNSUBSCRIBE", nub)
 	} else {
 		res := s.Action.UnSubscribe(session, args)
 		if res.Type == SUCCESS {
-			showSucc(conn, res.Message, nub)
+			showSucc(session, res.Message, nub)
 		} else if res.Type == BAD {
-			showBad(conn, res.Message, nub)
+			showBad(session, res.Message, nub)
 		} else {
-			showNo(conn, res.Message, nub)
+			showNo(session, 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	if args == "" {
-		paramsErr(conn, "LSUB", nub)
+		paramsErr(session, "LSUB", nub)
 	} else {
 		dt := strings.Split(args, " ")
+		dt[0] = strings.Trim(dt[0], `"`)
 		res := s.Action.LSub(session, dt[0], dt[1])
 		if res.Type == SUCCESS {
-			showSucc(conn, res.Message, nub)
+			showSuccWithData(session, res.Data, res.Message, nub)
 		} else if res.Type == BAD {
-			showBad(conn, res.Message, nub)
+			showBad(session, res.Message, nub)
 		} else {
-			showNo(conn, res.Message, nub)
+			showNo(session, 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	if args == "" {
-		paramsErr(conn, "STATUS", nub)
+		paramsErr(session, "STATUS", nub)
 	} else {
 		var mailBox string
 		var params []string
@@ -426,66 +432,77 @@ func (s *Server) status(session *Session, args string, nub string, conn net.Conn
 
 		res := s.Action.Status(session, mailBox, params)
 		if res.Type == SUCCESS {
-			showSuccWithData(conn, res.Data, res.Message, nub)
+			showSuccWithData(session, res.Data, res.Message, nub)
 		} else if res.Type == BAD {
-			showBad(conn, res.Message, nub)
+			showBad(session, res.Message, nub)
 		} else {
-			showNo(conn, res.Message, nub)
+			showNo(session, 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	res := s.Action.Check(session)
 	if res.Type == SUCCESS {
-		showSucc(conn, res.Message, nub)
+		showSucc(session, res.Message, nub)
 	} else if res.Type == BAD {
-		showBad(conn, res.Message, nub)
+		showBad(session, res.Message, nub)
 	} else {
-		showNo(conn, res.Message, nub)
+		showNo(session, res.Message, nub)
 	}
 }
 
-func (s *Server) search(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader) {
+func (s *Server) search(session *Session, args string, nub string, conn net.Conn, reader *bufio.Reader, uid bool) {
 	if session.Status != AUTHORIZED {
-		showBad(conn, "Need Login", nub)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	if args == "" {
-		paramsErr(conn, "SEARCH", nub)
+		paramsErr(session, "SEARCH", nub)
 	} else {
-		dt := strings.SplitN(args, " ", 2)
-		res := s.Action.Search(session, dt[0], dt[1])
+		var res CommandResponse
+		if args == "ALL" {
+			res = s.Action.Search(session, "", "UID 1:*", uid)
+		} else {
+			res = s.Action.Search(session, "", args, uid)
+		}
+
 		if res.Type == SUCCESS {
-			showSucc(conn, res.Message, nub)
+			content := "* SEARCH"
+			for _, datum := range res.Data {
+				content += " " + datum
+			}
+			content += eol
+			content += fmt.Sprintf("%s OK SEARCH completed (Success)%s", nub, eol)
+			write(session, content, nub)
 		} else if res.Type == BAD {
-			showBad(conn, res.Message, nub)
+			showBad(session, res.Message, nub)
 		} else {
-			showNo(conn, res.Message, nub)
+			showNo(session, 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	if args == "" {
-		paramsErr(conn, "COPY", nub)
+		paramsErr(session, "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)
+			showSucc(session, res.Message, nub)
 		} else if res.Type == BAD {
-			showBad(conn, res.Message, nub)
+			showBad(session, res.Message, nub)
 		} else {
-			showNo(conn, res.Message, nub)
+			showNo(session, res.Message, nub)
 		}
 	}
 }
@@ -493,39 +510,39 @@ func (s *Server) copy(session *Session, args string, nub string, conn net.Conn,
 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)
+		showSucc(session, res.Message, nub)
 	} else if res.Type == BAD {
-		showBad(conn, res.Message, nub)
+		showBad(session, res.Message, nub)
 	} else {
-		showNo(conn, res.Message, nub)
+		showNo(session, 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)
+		paramsErr(session, "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)
+			showSucc(session, res.Message, nub)
 		} else if res.Type == BAD {
-			showBad(conn, res.Message, nub)
+			showBad(session, res.Message, nub)
 		} else {
-			showNo(conn, res.Message, nub)
+			showNo(session, 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)
+	write(session, "* BYE PMail Server logging out"+eol, nub)
 	if res.Type == SUCCESS {
-		showSucc(conn, res.Message, nub)
+		showSucc(session, res.Message, nub)
 	} else if res.Type == BAD {
-		showBad(conn, res.Message, nub)
+		showBad(session, res.Message, nub)
 	} else {
-		showNo(conn, res.Message, nub)
+		showNo(session, res.Message, nub)
 	}
 	if conn != nil {
 		_ = conn.Close()
@@ -534,77 +551,77 @@ func (s *Server) logout(session *Session, args string, nub string, conn net.Conn
 
 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	res := s.Action.Unselect(session)
 	if res.Type == SUCCESS {
-		showSucc(conn, res.Message, nub)
+		showSucc(session, res.Message, nub)
 	} else if res.Type == BAD {
-		showBad(conn, res.Message, nub)
+		showBad(session, res.Message, nub)
 	} else {
-		showNo(conn, res.Message, nub)
+		showNo(session, 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	if args == "" {
-		paramsErr(conn, "SUBSCRIBE", nub)
+		paramsErr(session, "SUBSCRIBE", nub)
 	} else {
 		res := s.Action.Subscribe(session, args)
 		if res.Type == SUCCESS {
-			showSucc(conn, res.Message, nub)
+			showSucc(session, res.Message, nub)
 		} else if res.Type == BAD {
-			showBad(conn, res.Message, nub)
+			showBad(session, res.Message, nub)
 		} else {
-			showNo(conn, res.Message, nub)
+			showNo(session, 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)
+		showBad(session, "Need Login", nub)
 		return
 	}
 	session.IN_IDLE = true
 	res := s.Action.IDLE(session)
 	if res.Type == SUCCESS {
-		write(conn, "+ idling"+eol, nub)
+		write(session, "+ idling"+eol, nub)
 	} else if res.Type == BAD {
-		showBad(conn, res.Message, nub)
+		showBad(session, res.Message, nub)
 	} else {
-		showNo(conn, res.Message, nub)
+		showNo(session, 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)
+		write(session, fmt.Sprintf("* BAD %s %s", res.Message, eol), nub)
 	} else if res.Type == NO {
-		showNo(conn, res.Message, nub)
+		showNo(session, res.Message, nub)
 	} else {
 		if len(res.Data) == 0 {
-			showSucc(conn, res.Message, nub)
+			showSucc(session, 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)
+			write(session, 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)
+	log.WithContext(session.Ctx).Debugf("Imap Input:\t %s", rawLine)
 	if cmd != "IDLE" {
 		session.IN_IDLE = false
 	}
@@ -638,7 +655,9 @@ func (s *Server) doCommand(session *Session, rawLine string, conn net.Conn, read
 	case "UID FETCH":
 		s.fetch(session, args, nub, conn, reader, true)
 	case "STORE":
-		s.store(session, args, nub, conn, reader)
+		s.store(session, args, nub, conn, reader, false)
+	case "UID STORE":
+		s.store(session, args, nub, conn, reader, true)
 	case "CLOSE":
 		s.cclose(session, args, nub, conn, reader)
 	case "EXPUNGE":
@@ -656,7 +675,9 @@ func (s *Server) doCommand(session *Session, rawLine string, conn net.Conn, read
 	case "CHECK":
 		s.check(session, args, nub, conn, reader)
 	case "SEARCH":
-		s.search(session, args, nub, conn, reader)
+		s.search(session, args, nub, conn, reader, false)
+	case "UID SEARCH":
+		s.search(session, args, nub, conn, reader, true)
 	case "COPY":
 		s.copy(session, args, nub, conn, reader)
 	case "NOOP":
@@ -675,7 +696,6 @@ func (s *Server) doCommand(session *Session, rawLine string, conn net.Conn, read
 }
 
 func (s *Server) handleClient(conn net.Conn) {
-	slog.Debug("Imap conn")
 
 	defer func() {
 		if conn != nil {
@@ -688,6 +708,11 @@ func (s *Server) handleClient(conn net.Conn) {
 		Status:    UNAUTHORIZED,
 		AliveTime: time.Now(),
 	}
+
+	tc := &context.Context{}
+	tc.SetValue(context.LogID, id.GenLogID())
+	session.Ctx = tc
+
 	if s.TlsEnabled && s.TlsConfig != nil {
 		session.InTls = true
 	}
@@ -698,7 +723,7 @@ func (s *Server) handleClient(conn net.Conn) {
 			for {
 				if time.Now().Sub(session.AliveTime) >= s.ConnectAliveTime {
 					if session.Conn != nil {
-						write(session.Conn, "* BYE AutoLogout; idle for too long", "")
+						write(session, "* BYE AutoLogout; idle for too long", "")
 						_ = session.Conn.Close()
 					}
 					session.Conn = nil
@@ -711,7 +736,7 @@ func (s *Server) handleClient(conn net.Conn) {
 	}
 
 	reader := bufio.NewReader(conn)
-	write(conn, fmt.Sprintf(`* OK [CAPABILITY IMAP4 IMAP4rev1 AUTH=LOGIN] PMail Server ready%s`, eol), "")
+	write(session, fmt.Sprintf(`* OK [CAPABILITY IMAP4 IMAP4rev1 AUTH=LOGIN] PMail Server ready%s`, eol), "")
 
 	for {
 		rawLine, err := reader.ReadString('\n')
@@ -759,47 +784,47 @@ func getSafeArg(args []string, nr int) string {
 	return ""
 }
 
-func showSucc(w io.Writer, msg, nub string) {
+func showSucc(s *Session, msg, nub string) {
 	if msg == "" {
-		write(w, fmt.Sprintf("%s OK success %s", nub, eol), nub)
+		write(s, fmt.Sprintf("%s OK success %s", nub, eol), nub)
 	} else {
-		write(w, fmt.Sprintf("%s %s %s", nub, msg, eol), nub)
+		write(s, fmt.Sprintf("%s %s %s", nub, msg, eol), nub)
 	}
 }
 
-func showSuccWithData(w io.Writer, data []string, msg string, nub string) {
+func showSuccWithData(s *Session, 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)
+	write(s, content, nub)
 }
 
-func showBad(w io.Writer, err string, nub string) {
+func showBad(s *Session, err string, nub string) {
 	if nub == "" {
 		nub = "*"
 	}
 
 	if err == "" {
-		write(w, fmt.Sprintf("%s BAD %s", nub, eol), nub)
+		write(s, fmt.Sprintf("%s BAD %s", nub, eol), nub)
 		return
 	}
-	write(w, fmt.Sprintf("%s BAD %s%s", nub, err, eol), nub)
+	write(s, fmt.Sprintf("%s BAD %s%s", nub, err, eol), nub)
 }
 
-func showNo(w io.Writer, msg string, nub string) {
-	write(w, fmt.Sprintf("%s NO %s%s", nub, msg, eol), nub)
+func showNo(s *Session, msg string, nub string) {
+	write(s, fmt.Sprintf("%s NO %s%s", nub, msg, eol), nub)
 }
 
-func paramsErr(w io.Writer, commend string, nub string) {
-	write(w, fmt.Sprintf("* BAD %s parameters! %s", commend, eol), nub)
+func paramsErr(session *Session, commend string, nub string) {
+	write(session, fmt.Sprintf("* BAD %s parameters! %s", commend, eol), nub)
 }
 
-func write(w io.Writer, content string, nub string) {
+func write(session *Session, content string, nub string) {
 	if !strings.HasSuffix(content, eol) {
-		log.Errorf("Error:返回结尾错误  %s", content)
+		log.WithContext(session.Ctx).Errorf("Error:返回结尾错误  %s", content)
 	}
-	log.Debugf("Imap Out:\t |%s", content)
-	fmt.Fprintf(w, content)
+	log.WithContext(session.Ctx).Debugf("Imap Out:\t |%s", content)
+	fmt.Fprintf(session.Conn, content)
 }

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

@@ -1,6 +1,7 @@
 package goimap
 
 import (
+	"fmt"
 	"net"
 	"net/netip"
 	"reflect"
@@ -86,6 +87,13 @@ func Test_getCommand(t *testing.T) {
 			"UID FETCH",
 			`5 BODY.PEEK[HEADER]`,
 		},
+		{
+			"UID Search命令测试",
+			args{`C117 UID SEARCH UID 46:*`},
+			"C117",
+			"UID SEARCH",
+			`UID 46:*`,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -106,6 +114,7 @@ func Test_getCommand(t *testing.T) {
 type mockConn struct{}
 
 func (m mockConn) Read(b []byte) (n int, err error) {
+	fmt.Println("Read")
 	return 0, err
 }
 

+ 16 - 0
server/utils/version/version.go

@@ -0,0 +1,16 @@
+package version
+
+func LT(version1, version2 string) bool {
+	if version2 == "test" {
+		return true
+	}
+
+	return version1 < version2
+}
+
+func GT(version1, version2 string) bool {
+	if version2 == "test" {
+		return false
+	}
+	return version1 > version2
+}

+ 55 - 0
server/utils/version/version_test.go

@@ -0,0 +1,55 @@
+package version
+
+import "testing"
+
+func TestLT(t *testing.T) {
+	type args struct {
+		version1 string
+		version2 string
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "test1",
+			args: args{
+				version1: "1.0.0",
+				version2: "1.0.0",
+			},
+			want: false,
+		},
+		{
+			name: "test1",
+			args: args{
+				version1: "2.0.0",
+				version2: "1.0.0",
+			},
+			want: false,
+		},
+		{
+			name: "test1",
+			args: args{
+				version1: "1.0.0",
+				version2: "2.0.0",
+			},
+			want: true,
+		},
+		{
+			name: "test1",
+			args: args{
+				version1: "",
+				version2: "1.0.0",
+			},
+			want: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := LT(tt.args.version1, tt.args.version2); got != tt.want {
+				t.Errorf("LT() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}