feature/v2.8.0

支持Imap协议
升级所有依赖
修复部分bug
This commit is contained in:
jinnrry 2025-01-04 16:15:35 +08:00
parent ba33c0d4f8
commit cf3bab6c9f
47 changed files with 1920 additions and 850 deletions

View File

@ -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

View File

@ -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 ./...

View File

@ -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)
}
}

View File

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

View File

@ -11,7 +11,7 @@ import (
)
type emailDeleteRequest struct {
IDs []int64 `json:"ids"`
IDs []int `json:"ids"`
ForcedDel bool `json:"forcedDel"`
}

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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"`
}

View File

@ -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
)

View File

@ -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=

View File

@ -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{}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}
}

View File

@ -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) {

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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()
}
}

View 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
}

View 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
}

View 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)",
}
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View File

@ -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

View File

@ -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("开始执行插件ReceiveSaveAfterEnd")
// 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 {

View File

@ -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(`
{

View File

@ -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 (

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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 // 什么都不做,连接保活

View File

@ -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)
}

View File

@ -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
}

View 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
}

View 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)
}
})
}
}