mirror of
https://github.com/Jinnrry/PMail.git
synced 2025-02-20 11:43:09 +08:00
feature/v2.8.0
支持Imap协议 升级所有依赖 修复部分bug
This commit is contained in:
parent
ba33c0d4f8
commit
cf3bab6c9f
21
.github/workflows/unitTest.yml
vendored
21
.github/workflows/unitTest.yml
vendored
@ -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
|
||||
|
||||
@ -51,24 +54,18 @@ jobs:
|
||||
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
|
||||
|
||||
- name: FE build
|
||||
run: make build_fe
|
||||
|
||||
- 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 Test
|
||||
run: make test
|
||||
|
||||
# - name: Run postgres
|
||||
# run: make test_postgres
|
||||
|
||||
|
6
Makefile
6
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 ./...
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -17,4 +17,10 @@ const (
|
||||
|
||||
//EmailStatusDel 3删除
|
||||
EmailStatusDel int8 = 3
|
||||
|
||||
// EmailStatusDrafts 草稿箱
|
||||
EmailStatusDrafts int8 = 4
|
||||
|
||||
// EmailStatusJunk 骚扰邮件
|
||||
EmailStatusJunk int8 = 5
|
||||
)
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
type emailDeleteRequest struct {
|
||||
IDs []int64 `json:"ids"`
|
||||
IDs []int `json:"ids"`
|
||||
ForcedDel bool `json:"forcedDel"`
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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=
|
||||
|
@ -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{}
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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},
|
||||
}
|
||||
|
||||
memServer := NewServer()
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
if config.Instance.LogLevel == "debug" {
|
||||
option.DebugWriter = os.Stdout
|
||||
}
|
||||
|
||||
instanceTLS = imapserver.New(option)
|
||||
log.Infof("IMAP With TLS Server Start On Port :993")
|
||||
|
||||
err = instanceTLS.Start()
|
||||
if err != nil {
|
||||
if err := instanceTLS.ListenAndServeTLS(":993"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
if instanceTLS != nil {
|
||||
instanceTLS.Stop()
|
||||
instanceTLS = nil
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
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 TestDelete(t *testing.T) {
|
||||
|
||||
}
|
||||
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) {
|
||||
|
||||
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
server/listen/imap_server/server.go
Normal file
75
server/listen/imap_server/server.go
Normal file
@ -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
server/listen/imap_server/session_copy.go
Normal file
126
server/listen/imap_server/session_copy.go
Normal file
@ -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
server/listen/imap_server/session_create.go
Normal file
25
server/listen/imap_server/session_create.go
Normal file
@ -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
server/listen/imap_server/session_delete.go
Normal file
29
server/listen/imap_server/session_delete.go
Normal file
@ -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
server/listen/imap_server/session_expunge.go
Normal file
33
server/listen/imap_server/session_expunge.go
Normal file
@ -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
server/listen/imap_server/session_fetch.go
Normal file
113
server/listen/imap_server/session_fetch.go
Normal file
@ -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
server/listen/imap_server/session_idle.go
Normal file
52
server/listen/imap_server/session_idle.go
Normal file
@ -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
server/listen/imap_server/session_list.go
Normal file
106
server/listen/imap_server/session_list.go
Normal file
@ -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
server/listen/imap_server/session_login.go
Normal file
46
server/listen/imap_server/session_login.go
Normal file
@ -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
server/listen/imap_server/session_move.go
Normal file
78
server/listen/imap_server/session_move.go
Normal file
@ -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
server/listen/imap_server/session_namespace.go
Normal file
16
server/listen/imap_server/session_namespace.go
Normal file
@ -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
server/listen/imap_server/session_poll.go
Normal file
20
server/listen/imap_server/session_poll.go
Normal file
@ -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
server/listen/imap_server/session_rename.go
Normal file
34
server/listen/imap_server/session_rename.go
Normal file
@ -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
server/listen/imap_server/session_search.go
Normal file
46
server/listen/imap_server/session_search.go
Normal file
@ -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
server/listen/imap_server/session_select.go
Normal file
32
server/listen/imap_server/session_select.go
Normal file
@ -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
server/listen/imap_server/session_status.go
Normal file
40
server/listen/imap_server/session_status.go
Normal file
@ -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
server/listen/imap_server/session_store.go
Normal file
47
server/listen/imap_server/session_store.go
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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(`
|
||||
{
|
||||
|
@ -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 (
|
||||
|
@ -1,5 +1,7 @@
|
||||
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')"`
|
||||
@ -7,6 +9,7 @@ type UserEmail struct {
|
||||
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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,46 @@ 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)
|
||||
}
|
||||
|
||||
var ret []*response.EmailResponseData
|
||||
var ue []*models.UserEmail
|
||||
switch groupName {
|
||||
case "INBOX":
|
||||
db.Instance.Table("user_email").Select("email_id,is_read").Where("user_id=? and status=0", ctx.UserID).Limit(limit, offset).Find(&ue)
|
||||
sql += " and status =?"
|
||||
params = append(params, 0)
|
||||
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)
|
||||
sql += " and status =?"
|
||||
params = append(params, 1)
|
||||
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)
|
||||
sql += " and status =?"
|
||||
params = append(params, 4)
|
||||
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)
|
||||
sql += " and status =?"
|
||||
params = append(params, 3)
|
||||
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)
|
||||
sql += " and status =?"
|
||||
params = append(params, 5)
|
||||
default:
|
||||
groupNames := strings.Split(groupName, "/")
|
||||
groupName = groupNames[len(groupNames)-1]
|
||||
@ -123,12 +146,33 @@ func GetEmailListByGroup(ctx *context.Context, groupName string, offset, limit i
|
||||
var group models.Group
|
||||
db.Instance.Table("group").Where("user_id=? and name=?", ctx.UserID, groupName).Get(&group)
|
||||
if group.ID == 0 {
|
||||
return ret
|
||||
}
|
||||
db.Instance.Table("user_email").Select("email_id,is_read").Where("user_id=? and group_id = ?", ctx.UserID, group.ID).Limit(limit, offset).Find(&ue)
|
||||
return nil
|
||||
}
|
||||
|
||||
ueMap := map[int]*models.UserEmail{}
|
||||
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
|
||||
@ -138,6 +182,72 @@ 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
|
||||
}
|
||||
|
||||
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 []*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.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 ret
|
||||
}
|
||||
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]*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
|
||||
|
@ -30,7 +30,7 @@ type Action interface {
|
||||
BODY[TEXT]:返回整个邮件体,这里的邮件体并不包括邮件头。
|
||||
**/
|
||||
Fetch(session *Session, mailIds, dataNames string, uid bool) CommandResponse
|
||||
Store(session *Session, mailId, flags string) CommandResponse // STORE 命令用于修改指定邮件的属性,包括给邮件打上已读标记、删除标记
|
||||
Store(session *Session, mailId, flags string, uid bool) CommandResponse // STORE 命令用于修改指定邮件的属性,包括给邮件打上已读标记、删除标记
|
||||
Close(session *Session) CommandResponse // 关闭文件夹
|
||||
Expunge(session *Session) CommandResponse // 删除已经标记为删除的邮件,释放服务器上的存储空间
|
||||
Examine(session *Session, path string) CommandResponse // 只读方式打开邮箱
|
||||
@ -47,7 +47,7 @@ type Action interface {
|
||||
*/
|
||||
Status(session *Session, mailbox string, category []string) CommandResponse // 查询邮箱的当前状态
|
||||
Check(session *Session) CommandResponse // sync数据
|
||||
Search(session *Session, keyword, criteria string) CommandResponse // 命令可以根据搜索条件在处于活动状态的邮箱中搜索邮件,然后显示匹配的邮件编号
|
||||
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 // 什么都不做,连接保活
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
server/utils/version/version.go
Normal file
16
server/utils/version/version.go
Normal file
@ -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
server/utils/version/version_test.go
Normal file
55
server/utils/version/version_test.go
Normal file
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user