diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d348a77..50e360e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,7 +54,7 @@ jobs: run: cd fe && yarn && yarn build - name: BE Build run: | - cd server && cp -rf ../fe/dist http_server + cd server && cp -rf ../fe/dist listen/http_server go build -ldflags "-s -w -X 'main.version=${{ env.VERSION }}' -X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o ${{ env.FILENAME }} main.go go build -ldflags "-s -w" -o ${{ env.TGFILENAME }} hooks/telegram_push/telegram_push.go go build -ldflags "-s -w" -o ${{ env.WCFILENAME }} hooks/wechat_push/wechat_push.go diff --git a/.github/workflows/unitTest.yml b/.github/workflows/unitTest.yml index 2c5491d..fbbd9d2 100644 --- a/.github/workflows/unitTest.yml +++ b/.github/workflows/unitTest.yml @@ -39,6 +39,9 @@ jobs: COMMIT: ${{ github.workflow_sha }} EVENT: ${{ github.event_name}} steps: + - name: Setup Node.js environment + run: apt update && apt install -y nodejs npm + - name: Checkout uses: actions/checkout@v4 @@ -50,9 +53,6 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} issue-number: ${{ github.event.pull_request.number }} labels: 'Auto: Test Failed' - - - name: Setup Node.js environment - run: apt update && apt install -y nodejs npm - name: Install Dependencies run: npm install --global yarn @@ -60,15 +60,12 @@ jobs: - name: FE build run: make build_fe - - name: Run Test - 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 + diff --git a/Dockerfile b/Dockerfile index 5d8e95a..2d24182 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ FROM golang:alpine as serverbuild ARG VERSION WORKDIR /work COPY . . -COPY --from=febuild /work/dist /work/server/http_server/dist +COPY --from=febuild /work/dist /work/server/listen/http_server/dist RUN apk update && apk add git RUN cd /work/server && go build -ldflags "-s -w -X 'main.version=${VERSION}' -X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail main.go RUN cd /work/server/hooks/telegram_push && go build -ldflags "-s -w" -o output/telegram_push telegram_push.go @@ -34,6 +34,6 @@ COPY --from=serverbuild /work/server/hooks/telegram_push/output/* ./plugins/ COPY --from=serverbuild /work/server/hooks/wechat_push/output/* ./plugins/ COPY --from=serverbuild /work/server/hooks/spam_block/output/* ./plugins/ -EXPOSE 25 80 110 443 465 995 +EXPOSE 25 80 110 443 465 995 993 CMD /work/pmail diff --git a/DockerfileGithubAction b/DockerfileGithubAction index 0c29ec9..fb0ff2c 100644 --- a/DockerfileGithubAction +++ b/DockerfileGithubAction @@ -28,6 +28,6 @@ COPY --from=serverbuild /work/hooks/telegram_push/output/* ./plugins/ COPY --from=serverbuild /work/hooks/wechat_push/output/* ./plugins/ COPY --from=serverbuild /work/hooks/spam_block/output/* ./plugins/ -EXPOSE 25 80 110 443 465 995 +EXPOSE 25 80 110 443 465 995 993 CMD /work/pmail diff --git a/Makefile b/Makefile index 68cc118..d8126b1 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,8 @@ clean: build_fe: cd fe && yarn && yarn build - rm -rf server/http_server/dist - cd server && cp -rf ../fe/dist http_server + rm -rf server/listen/http_server/dist + cd server && cp -rf ../fe/dist listen/http_server build_server: cd server && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail_linux_amd64 main.go @@ -52,10 +52,10 @@ package: clean cp README.md output/ test: - export setup_port=17888 && cd 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 ./... \ No newline at end of file + export setup_port=17888 && cd server && export PMail_ROOT=$(CURDIR)/server/ && go test -args "postgres" -v -p 1 ./... \ No newline at end of file diff --git a/README.md b/README.md index cb7b7b7..3b5d4d2 100644 --- a/README.md +++ b/README.md @@ -57,10 +57,10 @@ First go to [spamhaus](https://check.spamhaus.org/) and check your domain name a Or -`docker run -p 25:25 -p 80:80 -p 443:443 -p 110:110 -p 465:465 -p 995:995 -v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest` +`docker run -p 25:25 -p 80:80 -p 443:443 -p 110:110 -p 465:465 -p 995:995 -p 993:993 -v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest` > [!IMPORTANT] -> If your server has a firewall turned on, you need to open ports 25, 80, 110, 443, 465, 995 +> If your server has a firewall turned on, you need to open ports 25, 80, 110, 443, 465, 993, 995 ## 3、Configuration @@ -104,6 +104,9 @@ SMTP Server Address : smtp.[Your Domain] SMTP Port: 25/465(SSL) +IMAP Server Address : imap.[Your Domain] + +IMAP Port: 993(SSL) # Plugin [WeChat Push](server/hooks/wechat_push/README.md) diff --git a/README_CN.md b/README_CN.md index 017f1c9..14c3fa6 100644 --- a/README_CN.md +++ b/README_CN.md @@ -109,6 +109,9 @@ SMTP地址: smtp.[你的域名] SMTP端口: 25/465(SSL) +IMAP地址: imap.[Your Domain] + +IMAP端口: 993(SSL) # 插件 diff --git a/fe/src/components/GroupSettings.vue b/fe/src/components/GroupSettings.vue index bb29078..b1701fd 100644 --- a/fe/src/components/GroupSettings.vue +++ b/fe/src/components/GroupSettings.vue @@ -53,7 +53,7 @@ http.get("/api/group").then((res) => { const del = function (node, data) { if (data.id !== -1) { - this.$axios.post("/api/group/del", { id: data.id }).then((res) => { + http.post("/api/group/del", { id: data.id }).then((res) => { if (res.errorNo !== 0) { ElMessage({ message: res.errorMsg, @@ -87,7 +87,7 @@ const add = function (item) { item.children.push({ children: [], label: "", - id: "-1", + id: -1, parent_id: item.id, }); }; @@ -96,15 +96,14 @@ const addRoot = function () { data.push({ children: [], label: "", - id: "-1", + id: -1, parent_id: 0, }); }; const onInputBlur = function (item) { if (item.label !== "") { - http - .post("/api/group/add", { name: item.label, parent_id: item.parent_id }) + http.post("/api/group/add", { name: item.label, parent_id: item.parent_id }) .then((res) => { if (res.errorNo !== 0) { ElMessage({ @@ -112,7 +111,7 @@ const onInputBlur = function (item) { type: "error", }); } else { - this.$axios.get("/api/group").then((res) => { + http.get("/api/group").then((res) => { data.splice(0, data.length); data.push(...res.data); }); diff --git a/server/config/config.go b/server/config/config.go index 4d943b1..0e3a832 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -1,13 +1,22 @@ package config import ( + "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "encoding/json" "encoding/pem" + "fmt" + "github.com/Jinnrry/pmail/utils/context" + "github.com/Jinnrry/pmail/utils/errors" + "github.com/Jinnrry/pmail/utils/file" + log "github.com/sirupsen/logrus" "os" + "path/filepath" + "strings" + "time" ) var IsInit bool @@ -41,6 +50,43 @@ type Config struct { setupPort int // 初始化阶段端口 } +var ROOT_PATH = "" + +func init() { + envs := os.Environ() + for _, env := range envs { + if strings.HasPrefix(env, "PMail_ROOT=") { + ROOT_PATH = strings.TrimSpace(strings.ReplaceAll(env, "PMail_ROOT=", "")) + if !strings.HasSuffix(ROOT_PATH, "/") { + ROOT_PATH += "/" + } + + fmt.Println("Env Root Path:", ROOT_PATH) + return + } + } + + ex, err := os.Executable() + if err != nil { + panic(err) + } + exPath := filepath.Dir(ex) + realPath, err := filepath.EvalSymlinks(exPath) + if err != nil { + panic(err) + } + // 如果是Goland运行,不修改根路径 + if strings.Contains(realPath, "GoLand") && strings.Contains(realPath, "JetBrains") { + return + } + + if !strings.HasSuffix(realPath, "/") { + realPath += "/" + } + ROOT_PATH = realPath + fmt.Println("Root Path:", ROOT_PATH) +} + func (c *Config) GetSetupPort() int { return c.setupPort } @@ -60,24 +106,47 @@ var DBTypes []string = []string{DBTypeMySQL, DBTypeSQLite, DBTypePostgres} var Instance *Config = &Config{} +type logFormatter struct { +} + +// Format 定义日志输出格式 +func (l *logFormatter) Format(entry *log.Entry) ([]byte, error) { + b := bytes.Buffer{} + + b.WriteString(fmt.Sprintf("[%s]", entry.Level.String())) + b.WriteString(fmt.Sprintf("[%s]", entry.Time.Format("2006-01-02 15:04:05"))) + if entry.Context != nil { + ctx := entry.Context.(*context.Context) + if ctx != nil { + b.WriteString(fmt.Sprintf("[%s]", ctx.GetValue(context.LogID))) + } + } + b.WriteString(fmt.Sprintf("[%s:%d]", entry.Caller.File, entry.Caller.Line)) + b.WriteString(entry.Message) + + b.WriteString("\n") + return b.Bytes(), nil +} func Init() { var cfgData []byte var err error args := os.Args if len(args) >= 2 && args[len(args)-1] == "dev" { - cfgData, err = os.ReadFile("./config/config.dev.json") + cfgData, err = os.ReadFile(ROOT_PATH + "./config/config.dev.json") if err != nil { return } } else { - cfgData, err = os.ReadFile("./config/config.json") + cfgData, err = os.ReadFile(ROOT_PATH + "./config/config.json") if err != nil { + log.Errorf("config file not found,%s", err.Error()) return } } err = json.Unmarshal(cfgData, &Instance) + Instance.fixPath() if err != nil { return } @@ -90,10 +159,39 @@ func Init() { IsInit = true } + // 设置日志格式为json格式 + log.SetFormatter(&logFormatter{}) + log.SetReportCaller(true) + + // 设置将日志输出到标准输出(默认的输出为stderr,标准错误) + // 日志消息输出可以是任意的io.writer类型 + log.SetOutput(os.Stdout) + + var cstZone = time.FixedZone("CST", 8*3600) + time.Local = cstZone + if Instance != nil { + switch Instance.LogLevel { + case "": + log.SetLevel(log.InfoLevel) + case "debug": + log.SetLevel(log.DebugLevel) + case "info": + log.SetLevel(log.InfoLevel) + case "warn": + log.SetLevel(log.WarnLevel) + case "error": + log.SetLevel(log.ErrorLevel) + default: + log.SetLevel(log.InfoLevel) + } + } else { + log.SetLevel(log.InfoLevel) + } + } func ReadPrivateKey() (*ecdsa.PrivateKey, bool) { - key, err := os.ReadFile("./config/ssl/account_private.pem") + key, err := os.ReadFile(ROOT_PATH + "./config/ssl/account_private.pem") if err != nil { return createNewPrivateKey(), true } @@ -114,10 +212,63 @@ func createNewPrivateKey() *ecdsa.PrivateKey { x509Encoded, _ := x509.MarshalECPrivateKey(privateKey) // 将ec 密钥写入到 pem文件里 - keypem, _ := os.OpenFile("./config/ssl/account_private.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + keypem, _ := os.OpenFile(ROOT_PATH+"./config/ssl/account_private.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) err = pem.Encode(keypem, &pem.Block{Type: "EC PRIVATE KEY", Bytes: x509Encoded}) if err != nil { panic(err) } return 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) + } + return nil +} + +func ReadConfig() (*Config, error) { + configData := Config{ + DkimPrivateKeyPath: ROOT_PATH + "config/dkim/dkim.priv", + SSLPrivateKeyPath: ROOT_PATH + "config/ssl/private.key", + SSLPublicKeyPath: ROOT_PATH + "config/ssl/public.crt", + } + 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) + } + } + return &configData, nil +} + +func (c *Config) fixPath() { + if c.DbType == DBTypeSQLite && !strings.HasPrefix(c.DbDSN, "/") { + c.DbDSN = ROOT_PATH + c.DbDSN + } + if !strings.HasPrefix(c.SSLPublicKeyPath, "/") { + c.SSLPublicKeyPath = ROOT_PATH + c.SSLPublicKeyPath + } + if !strings.HasPrefix(c.SSLPrivateKeyPath, "/") { + c.SSLPrivateKeyPath = ROOT_PATH + c.SSLPrivateKeyPath + } +} diff --git a/server/config/config.json b/server/config/config.json index 7186b54..f7e03d1 100644 --- a/server/config/config.json +++ b/server/config/config.json @@ -1,5 +1,5 @@ { - "logLevel": "", + "logLevel": "debug", "domain": "test.domain", "domains": null, "webDomain": "mail.test.domain", diff --git a/server/consts/consts.go b/server/consts/consts.go index 5e0d958..eb23722 100644 --- a/server/consts/consts.go +++ b/server/consts/consts.go @@ -17,4 +17,10 @@ const ( //EmailStatusDel 3删除 EmailStatusDel int8 = 3 + + // EmailStatusDrafts 草稿箱 + EmailStatusDrafts int8 = 4 + + // EmailStatusJunk 骚扰邮件 + EmailStatusJunk int8 = 5 ) diff --git a/server/controllers/email/delete.go b/server/controllers/email/delete.go index 2b6b560..f099cb0 100644 --- a/server/controllers/email/delete.go +++ b/server/controllers/email/delete.go @@ -11,8 +11,8 @@ import ( ) type emailDeleteRequest struct { - IDs []int64 `json:"ids"` - ForcedDel bool `json:"forcedDel"` + IDs []int `json:"ids"` + ForcedDel bool `json:"forcedDel"` } func EmailDelete(ctx *context.Context, w http.ResponseWriter, req *http.Request) { diff --git a/server/controllers/group.go b/server/controllers/group.go index 897e395..5b4723c 100644 --- a/server/controllers/group.go +++ b/server/controllers/group.go @@ -2,11 +2,9 @@ package controllers import ( "encoding/json" - "github.com/Jinnrry/pmail/db" "github.com/Jinnrry/pmail/dto" "github.com/Jinnrry/pmail/dto/response" "github.com/Jinnrry/pmail/i18n" - "github.com/Jinnrry/pmail/models" "github.com/Jinnrry/pmail/services/group" "github.com/Jinnrry/pmail/utils/array" "github.com/Jinnrry/pmail/utils/context" @@ -67,13 +65,8 @@ func AddGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) { log.WithContext(ctx).Errorf("%+v", err) } - var newGroup models.Group = models.Group{ - Name: reqData.Name, - ParentId: reqData.ParentId, - UserId: ctx.UserID, - } + newGroup, err := group.CreateGroup(ctx, reqData.Name, reqData.ParentId) - _, err = db.Instance.Insert(&newGroup) if err != nil { response.NewErrorResponse(response.ServerError, "DBError", err.Error()).FPrint(w) return diff --git a/server/db/init.go b/server/db/init.go index e453414..31aec12 100644 --- a/server/db/init.go +++ b/server/db/init.go @@ -36,6 +36,7 @@ func Init(version string) error { return errors.New("Database Type Error!") } if err != nil { + log.Errorf("DB init Error! %s", err.Error()) return errors.Wrap(err) } @@ -53,11 +54,15 @@ 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) } + if config.Instance.LogLevel == "debug" { + Instance.ShowSQL(true) + } + return nil } diff --git a/server/dto/parsemail/dkim.go b/server/dto/parsemail/dkim.go index c3678bb..464c350 100644 --- a/server/dto/parsemail/dkim.go +++ b/server/dto/parsemail/dkim.go @@ -25,7 +25,9 @@ var instance *Dkim func Init() { privateKey, err := loadPrivateKey(config.Instance.DkimPrivateKeyPath) if err != nil { - panic("DKIM load fail! Please set dkim! dkim私钥加载失败!请先设置dkim秘钥") + panic(config.Instance.DkimPrivateKeyPath + + " DKIM load fail! Please set dkim! dkim私钥加载失败!请先设置dkim秘钥" + + err.Error()) } instance = &Dkim{ diff --git a/server/dto/parsemail/email.go b/server/dto/parsemail/email.go index 1f65c93..c445a8d 100644 --- a/server/dto/parsemail/email.go +++ b/server/dto/parsemail/email.go @@ -13,6 +13,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cast" "io" + "mime" "net/textproto" "regexp" "strings" @@ -24,6 +25,13 @@ type User struct { Name string `json:"Name"` } +func (u User) Build() string { + if u.Name != "" { + return fmt.Sprintf("\"%s\" <%s>", mime.QEncoding.Encode("utf-8", u.Name), u.EmailAddress) + } + return fmt.Sprintf("<%s>", u.EmailAddress) +} + func (u User) GetDomainAccount() (string, string) { infos := strings.Split(u.EmailAddress, "@") if len(infos) >= 2 { @@ -60,6 +68,25 @@ type Email struct { Size int } +func users2String(users []*User) string { + ret := "" + for _, user := range users { + if ret != "" { + ret += ", " + } + ret += user.Build() + } + return ret +} + +func (e *Email) BuildTo2String() string { + return users2String(e.To) +} + +func (e *Email) BuildCc2String() string { + return users2String(e.Cc) +} + func NewEmailFromModel(d models.Email) *Email { var To []*User diff --git a/server/dto/parsemail/email_test.go b/server/dto/parsemail/email_test.go index 0f52bd4..96aaaf5 100644 --- a/server/dto/parsemail/email_test.go +++ b/server/dto/parsemail/email_test.go @@ -68,7 +68,7 @@ func TestEmail_builder(t *testing.T) { e := Email{ From: buildUser("i@test.com"), To: buildUsers([]string{"to@test.com"}), - Subject: "Title", + Subject: "Title中文", HTML: []byte("Html"), Text: []byte("Text"), Attachments: []*Attachment{ diff --git a/server/dto/response/email.go b/server/dto/response/email.go index d498cda..83fee1d 100644 --- a/server/dto/response/email.go +++ b/server/dto/response/email.go @@ -5,4 +5,11 @@ import "github.com/Jinnrry/pmail/models" type EmailResponseData struct { models.Email `xorm:"extends"` IsRead int8 `json:"is_read"` + SerialNumber int `json:"serial_number"` + UeId int `json:"ue_id"` +} + +type UserEmailUIDData struct { + models.UserEmail `xorm:"extends"` + SerialNumber int `json:"serial_number"` } diff --git a/server/go.mod b/server/go.mod index dea4e4d..3b67797 100644 --- a/server/go.mod +++ b/server/go.mod @@ -9,19 +9,20 @@ require ( github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885 github.com/alexedwards/scs/v2 v2.8.0 github.com/dlclark/regexp2 v1.11.4 + github.com/emersion/go-imap/v2 v2.0.0-beta.4 github.com/emersion/go-message v0.18.1 github.com/emersion/go-msgauth v0.6.8 github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 github.com/emersion/go-smtp v0.21.3 - github.com/go-acme/lego/v4 v4.18.0 + github.com/go-acme/lego/v4 v4.21.0 github.com/go-sql-driver/mysql v1.8.1 github.com/lib/pq v1.10.9 github.com/mileusna/spf v0.9.5 github.com/sirupsen/logrus v1.9.3 - github.com/spf13/cast v1.7.0 - golang.org/x/crypto v0.27.0 - golang.org/x/text v0.18.0 - modernc.org/sqlite v1.33.1 + github.com/spf13/cast v1.7.1 + golang.org/x/crypto v0.31.0 + golang.org/x/text v0.21.0 + modernc.org/sqlite v1.34.4 xorm.io/builder v0.3.13 xorm.io/xorm v1.3.9 ) @@ -31,7 +32,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // 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 @@ -47,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 ) diff --git a/server/go.sum b/server/go.sum index 2f4a258..21ea687 100644 --- a/server/go.sum +++ b/server/go.sum @@ -15,12 +15,15 @@ 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= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emersion/go-imap/v2 v2.0.0-beta.4 h1:BS7+kUVhe/jfuFWgn8li0AbCKBIDoNvqJWsRJppltcc= +github.com/emersion/go-imap/v2 v2.0.0-beta.4/go.mod h1:BZTFHsS1hmgBkFlHqbxGLXk2hnRqTItUgwjSSCsYNAk= github.com/emersion/go-message v0.18.1 h1:tfTxIoXFSFRwWaZsgnqS1DSZuGpYGzSmCZD8SK3QA2E= github.com/emersion/go-message v0.18.1/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= github.com/emersion/go-msgauth v0.6.8 h1:kW/0E9E8Zx5CdKsERC/WnAvnXvX7q9wTHia1OA4944A= @@ -35,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= @@ -110,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= @@ -135,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= @@ -155,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= @@ -186,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= @@ -197,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= @@ -228,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= diff --git a/server/cron_server/ssl_update.go b/server/listen/cron_server/ssl_update.go similarity index 92% rename from server/cron_server/ssl_update.go rename to server/listen/cron_server/ssl_update.go index 677c68e..e5f5f38 100644 --- a/server/cron_server/ssl_update.go +++ b/server/listen/cron_server/ssl_update.go @@ -33,14 +33,14 @@ func Start() { // 每天检查一遍SSL证书是否更新,更新就重启 func sslCheck() { var err error - _, expiredTime, err = ssl.CheckSSLCrtInfo() + _, expiredTime, _, err = ssl.CheckSSLCrtInfo() if err != nil { panic(err) } for { time.Sleep(24 * time.Hour) - _, newExpTime, err := ssl.CheckSSLCrtInfo() + _, newExpTime, _, err := ssl.CheckSSLCrtInfo() if err != nil { log.Errorf("SSL Check Error! %+v", err) } diff --git a/server/http_server/http_server.go b/server/listen/http_server/http_server.go similarity index 100% rename from server/http_server/http_server.go rename to server/listen/http_server/http_server.go diff --git a/server/http_server/https_server.go b/server/listen/http_server/https_server.go similarity index 100% rename from server/http_server/https_server.go rename to server/listen/http_server/https_server.go diff --git a/server/http_server/setup_server.go b/server/listen/http_server/setup_server.go similarity index 100% rename from server/http_server/setup_server.go rename to server/listen/http_server/setup_server.go diff --git a/server/listen/imap_server/imap_server.go b/server/listen/imap_server/imap_server.go new file mode 100644 index 0000000..a65b014 --- /dev/null +++ b/server/listen/imap_server/imap_server.go @@ -0,0 +1,55 @@ +package imap_server + +import ( + "crypto/tls" + "github.com/Jinnrry/pmail/config" + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/imapserver" + log "github.com/sirupsen/logrus" + "os" +) + +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{ + 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") + if err := instanceTLS.ListenAndServeTLS(":993"); err != nil { + panic(err) + } +} diff --git a/server/listen/imap_server/imap_server_test.go b/server/listen/imap_server/imap_server_test.go new file mode 100644 index 0000000..5c14fa1 --- /dev/null +++ b/server/listen/imap_server/imap_server_test.go @@ -0,0 +1,441 @@ +package imap_server + +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" + "mime" + "testing" + "time" +) + +var clientUnLogin *imapclient.Client +var clientLogin *imapclient.Client + +func TestMain(m *testing.M) { + config.Init() + db.Init("") + go StarTLS() + time.Sleep(2 * time.Second) + + options := &imapclient.Options{ + WordDecoder: &mime.WordDecoder{CharsetReader: charset.Reader}, + TLSConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + + var err error + clientUnLogin, err = imapclient.DialTLS("127.0.0.1:993", options) + if err != nil { + panic(err) + } + + clientLogin, err = imapclient.DialTLS("127.0.0.1:993", options) + if err != nil { + panic(err) + } + + err = clientLogin.Login("testCase", "testCase").Wait() + if err != nil { + panic(err) + } + + m.Run() +} + +func TestCapability(t *testing.T) { + + res, err := clientUnLogin.Capability().Wait() + if err != nil { + t.Error(err) + } + if _, ok := res["IMAP4rev1"]; !ok { + t.Error("Capability Error") + } + +} + +func TestLogin(t *testing.T) { + err := clientUnLogin.Login("testCase", "testCaseasdfsadf").Wait() + sErr := err.(*imap.Error) + if sErr.Code != "AUTHENTICATIONFAILED" { + t.Error("Login Error") + } +} + +func TestCreate(t *testing.T) { + 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 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() + + if err == nil { + t.Logf("%+v", res) + t.Error("List Unlogin error") + } + + res, err = clientLogin.List("", "", &imap.ListOptions{}).Collect() + if err != nil { + t.Error(err) + } + if len(res) == 0 { + t.Error("List Error") + } + + res, err = clientLogin.List("", "*", &imap.ListOptions{}).Collect() + if err != nil { + t.Error(err) + } + if len(res) == 0 { + t.Error("List Error") + } + + 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) { + +} +func TestSelect(t *testing.T) { + res, err := clientUnLogin.Select("INBOX", &imap.SelectOptions{}).Wait() + if err == nil { + t.Logf("%+v", res) + t.Error("Select Unlogin error") + } + + res, err = clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait() + if err != nil { + t.Logf("%+v", res) + t.Error("Select error") + } + + if res == nil || res.NumMessages == 0 { + t.Error("Select Error") + } + + res, err = clientLogin.Select("Deleted Messages", &imap.SelectOptions{}).Wait() + if err != nil { + t.Logf("%+v", res) + t.Error("Select error") + } + + if res == nil || res.NumMessages == 0 { + t.Error("Select Error") + } + +} + +func TestStatus(t *testing.T) { + res, err := clientUnLogin.Status("INBOX", &imap.StatusOptions{}).Wait() + if err == nil { + t.Logf("%+v", res) + t.Error("Select Unlogin error") + } + + res, err = clientLogin.Status("INBOX", &imap.StatusOptions{}).Wait() + if err != nil { + t.Logf("%+v", res) + t.Error("Select error") + } + +} + +func TestFetch(t *testing.T) { + res2, err := clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait() + if err != nil { + t.Logf("%+v", res2) + t.Error("Fetch error") + } + + res, err := clientLogin.Fetch(imap.SeqSetNum(1), &imap.FetchOptions{ + Envelope: true, + Flags: true, + InternalDate: true, + RFC822Size: true, + UID: true, + BodySection: []*imap.FetchItemBodySection{ + { + Specifier: imap.PartSpecifierText, + Peek: true, + }, + }, + }).Collect() + if err != nil { + t.Logf("%+v", res) + t.Error("Fetch error") + } + + res, err = clientLogin.Fetch(imap.SeqSetNum(1, 2, 3, 4, 5, 6, 7, 8, 9), &imap.FetchOptions{ + Flags: true, + UID: true, + }).Collect() + if err != nil { + t.Logf("%+v", res) + t.Error("Fetch error") + } + + res, err = clientLogin.Fetch(imap.SeqSetNum(1), &imap.FetchOptions{ + Envelope: true, + Flags: true, + InternalDate: true, + RFC822Size: true, + UID: true, + BodySection: []*imap.FetchItemBodySection{ + { + Specifier: imap.PartSpecifierHeader, + HeaderFields: []string{"subject"}, + Peek: true, + }, + }, + }).Collect() + if err != nil { + t.Logf("%+v", res) + t.Error("Fetch error") + } + + res, err = clientLogin.Fetch(imap.SeqSetNum(1), &imap.FetchOptions{ + UID: true, + BodySection: []*imap.FetchItemBodySection{ + { + Specifier: imap.PartSpecifierHeader, + Peek: true, + }, + }, + }).Collect() + if err != nil { + t.Logf("%+v", res) + t.Error("Fetch error") + } +} +func TestStore(t *testing.T) { + 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) { + +} +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) { + +} +func TestSubscribe(t *testing.T) { + +} +func TestUnSubscribe(t *testing.T) { + +} +func TestLSub(t *testing.T) { + +} + +func TestCheck(t *testing.T) { + +} +func TestSearch(t *testing.T) { + clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait() + + res, err := clientLogin.Search(&imap.SearchCriteria{ + UID: []imap.UIDSet{ + []imap.UIDRange{ + {Start: 1}, + }, + []imap.UIDRange{ + {Start: 2}, + }, + []imap.UIDRange{ + {Start: 2, Stop: 5}, + }, + }, + }, &imap.SearchOptions{}).Wait() + if err != nil { + t.Errorf("%+v", err) + } + t.Logf("%+v", res) +} +func TestMove(t *testing.T) { + clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait() + + _, err := clientLogin.Move(imap.UIDSetNum(21), "Junk").Wait() + if err != nil { + t.Errorf("%+v", err) + } + _, err = clientLogin.Move(imap.UIDSetNum(23), "一级菜单").Wait() + if err != nil { + t.Errorf("%+v", err) + } + var ue []models.UserEmail + db.Instance.Table("user_email").Where("id=21 or id=23").Find(&ue) + for _, v := range ue { + if v.ID == 21 && (v.GroupId != 0 || v.Status != 5) { + t.Errorf("TestMove Error") + } + if v.ID == 23 && v.GroupId != 4 { + t.Errorf("TestMove Error") + } + } + +} + +func TestCopy(t *testing.T) { + clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait() + + res, err := clientLogin.Copy(imap.UIDSetNum(25), "Junk").Wait() + if err != nil { + t.Errorf("%+v", err) + } + t.Logf("%+v", res) + + if !res.DestUIDs.Contains(33) { + t.Errorf("TestCopy Error") + } + + res, err = clientLogin.Copy(imap.UIDSetNum(27), "一级菜单").Wait() + if err != nil { + t.Errorf("%+v", err) + } + t.Logf("%+v", res) + if !res.DestUIDs.Contains(34) { + t.Errorf("TestCopy Error") + } +} + +func TestNoop(t *testing.T) { + err := clientLogin.Noop().Wait() + if err != nil { + t.Error(err) + } +} +func TestIDLE(t *testing.T) { + +} +func TestUnselect(t *testing.T) { + +} + +func TestLogout(t *testing.T) { + err := clientLogin.Logout().Wait() + if err != nil { + t.Error(err) + } +} diff --git a/server/listen/imap_server/server.go b/server/listen/imap_server/server.go new file mode 100644 index 0000000..9e95c9b --- /dev/null +++ b/server/listen/imap_server/server.go @@ -0,0 +1,75 @@ +package imap_server + +import ( + "github.com/Jinnrry/pmail/utils/context" + "github.com/Jinnrry/pmail/utils/id" + "github.com/emersion/go-imap/v2" + log "github.com/sirupsen/logrus" + "sync" + "time" + + "github.com/emersion/go-imap/v2/imapserver" +) + +// Server is a server instance. +// +// A server contains a list of users. +type Server struct { + mutex sync.Mutex +} + +// NewServer creates a new server. +func NewServer() *Server { + return &Server{} +} + +type Status int8 + +const ( + UNAUTHORIZED Status = 1 + AUTHORIZED Status = 2 + SELECTED Status = 3 + LOGOUT Status = 4 +) + +type serverSession struct { + server *Server // immutable + ctx *context.Context + status Status + currentMailbox string + connectTime time.Time +} + +// NewSession creates a new IMAP session. +func (s *Server) NewSession() imapserver.Session { + tc := &context.Context{} + tc.SetValue(context.LogID, id.GenLogID()) + + return &serverSession{ + server: s, + ctx: tc, + connectTime: time.Now(), + } +} + +func (s *serverSession) Close() error { + return nil +} + +func (s *serverSession) Subscribe(mailbox string) error { + return nil +} + +func (s *serverSession) Unsubscribe(mailbox string) error { + return nil +} + +func (s *serverSession) Append(mailbox string, r imap.LiteralReader, options *imap.AppendOptions) (*imap.AppendData, error) { + log.WithContext(s.ctx).Errorf("Append Not Implemented") + return nil, nil +} + +func (s *serverSession) Unselect() error { + s.currentMailbox = "" + return nil +} diff --git a/server/listen/imap_server/session_copy.go b/server/listen/imap_server/session_copy.go new file mode 100644 index 0000000..eb3aa64 --- /dev/null +++ b/server/listen/imap_server/session_copy.go @@ -0,0 +1,126 @@ +package imap_server + +import ( + "github.com/Jinnrry/pmail/consts" + "github.com/Jinnrry/pmail/db" + "github.com/Jinnrry/pmail/dto/response" + "github.com/Jinnrry/pmail/models" + "github.com/Jinnrry/pmail/services/group" + "github.com/Jinnrry/pmail/services/list" + "github.com/Jinnrry/pmail/utils/context" + "github.com/emersion/go-imap/v2" + "github.com/spf13/cast" +) + +func (s *serverSession) Copy(numSet imap.NumSet, dest string) (*imap.CopyData, error) { + + var emailList []*response.EmailResponseData + + switch numSet.(type) { + case imap.SeqSet: + seqSet := numSet.(imap.SeqSet) + for _, seq := range seqSet { + emailList = list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{ + Star: cast.ToInt(seq.Start), + End: cast.ToInt(seq.Stop), + }, false) + } + case imap.UIDSet: + uidSet := numSet.(imap.UIDSet) + for _, uid := range uidSet { + emailList = list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{ + Star: cast.ToInt(uint32(uid.Start)), + End: cast.ToInt(uint32(uid.Stop)), + }, true) + } + } + + if len(emailList) == 0 { + return nil, &imap.Error{ + Type: imap.StatusResponseTypeNo, + Text: "Email Not Found", + } + } + + var err error + destUid := []int{} + UIDValidity := 0 + if group.IsDefaultBox(dest) { + UIDValidity, destUid, err = copy2defaultbox(s.ctx, emailList, dest) + } else { + UIDValidity, destUid, err = copy2userbox(s.ctx, emailList, dest) + } + data := imap.CopyData{} + data.UIDValidity = cast.ToUint32(UIDValidity) + data.DestUIDs = imap.UIDSet{} + data.SourceUIDs = imap.UIDSet{} + for _, uid := range destUid { + data.DestUIDs = append(data.DestUIDs, imap.UIDRange{Start: imap.UID(cast.ToUint32(uid)), Stop: imap.UID(cast.ToUint32(uid))}) + } + + for _, email := range emailList { + data.SourceUIDs = append(data.SourceUIDs, imap.UIDRange{Start: imap.UID(cast.ToUint32(email.UeId)), Stop: imap.UID(cast.ToUint32(email.UeId))}) + } + + return &data, err +} + +func copy2defaultbox(ctx *context.Context, mails []*response.EmailResponseData, dest string) (int, []int, error) { + + var destUid []int + for _, email := range mails { + + newUe := models.UserEmail{ + UserID: ctx.UserID, + EmailID: email.Id, + IsRead: email.IsRead, + GroupId: 0, + } + switch dest { + case "Deleted Messages": + newUe.Status = consts.EmailStatusDel + case "INBOX": + newUe.Status = consts.EmailStatusWait + case "Sent Messages": + newUe.Status = consts.EmailStatusSent + case "Drafts": + newUe.Status = consts.EmailStatusDrafts + case "Junk": + newUe.Status = consts.EmailStatusJunk + } + db.Instance.Insert(&newUe) + destUid = append(destUid, newUe.ID) + } + + return models.GroupNameToCode[dest], destUid, nil +} + +func copy2userbox(ctx *context.Context, mails []*response.EmailResponseData, dest string) (int, []int, error) { + groupInfo, err := group.GetGroupByFullPath(ctx, dest) + if err != nil { + return 0, nil, &imap.Error{ + Type: imap.StatusResponseTypeNo, + Text: err.Error(), + } + } + if groupInfo == nil || groupInfo.ID == 0 { + return 0, nil, &imap.Error{ + Type: imap.StatusResponseTypeNo, + Text: "Group not found", + } + } + var destUid []int + for _, email := range mails { + newUe := models.UserEmail{ + UserID: ctx.UserID, + EmailID: email.Id, + IsRead: email.IsRead, + GroupId: groupInfo.ID, + Status: email.Status, + } + db.Instance.Insert(&newUe) + destUid = append(destUid, newUe.ID) + } + + return groupInfo.ID, destUid, nil +} diff --git a/server/listen/imap_server/session_create.go b/server/listen/imap_server/session_create.go new file mode 100644 index 0000000..0417954 --- /dev/null +++ b/server/listen/imap_server/session_create.go @@ -0,0 +1,25 @@ +package imap_server + +import ( + "github.com/Jinnrry/pmail/services/group" + "github.com/emersion/go-imap/v2" + "strings" +) + +func (s *serverSession) Create(mailbox string, options *imap.CreateOptions) error { + groupPath := strings.Split(mailbox, "/") + + var parentId int + for _, path := range groupPath { + newGroup, err := group.CreateGroup(s.ctx, path, parentId) + if err != nil { + return &imap.Error{ + Type: imap.StatusResponseTypeNo, + Text: err.Error(), + } + } + parentId = newGroup.ID + } + + return nil +} diff --git a/server/listen/imap_server/session_delete.go b/server/listen/imap_server/session_delete.go new file mode 100644 index 0000000..5d810a8 --- /dev/null +++ b/server/listen/imap_server/session_delete.go @@ -0,0 +1,29 @@ +package imap_server + +import ( + "github.com/Jinnrry/pmail/services/group" + "github.com/emersion/go-imap/v2" + "strings" +) + +func (s *serverSession) Delete(mailbox string) error { + groupPath := strings.Split(mailbox, "/") + + groupName := groupPath[len(groupPath)-1] + groupInfo, err := group.GetGroupByName(s.ctx, groupName) + if err != nil { + return &imap.Error{ + Type: imap.StatusResponseTypeNo, + Text: err.Error(), + } + } + _, err = group.DelGroup(s.ctx, groupInfo.ID) + if err != nil { + return &imap.Error{ + Type: imap.StatusResponseTypeNo, + Text: err.Error(), + } + } + + return nil +} diff --git a/server/listen/imap_server/session_expunge.go b/server/listen/imap_server/session_expunge.go new file mode 100644 index 0000000..982555f --- /dev/null +++ b/server/listen/imap_server/session_expunge.go @@ -0,0 +1,33 @@ +package imap_server + +import ( + "github.com/Jinnrry/pmail/services/del_email" + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/imapserver" + "github.com/spf13/cast" +) + +func (s *serverSession) Expunge(w *imapserver.ExpungeWriter, uids *imap.UIDSet) error { + if uids == nil { + return nil + } + + uidList := []int{} + for _, uidRange := range *uids { + if uidRange.Start > 0 && uidRange.Stop > 0 { + for i := uidRange.Start; i <= uidRange.Stop; i++ { + uidList = append(uidList, cast.ToInt(uint32(i))) + } + } + } + + err := del_email.DelByUID(s.ctx, uidList) + if err != nil { + return &imap.Error{ + Type: imap.StatusResponseTypeNo, + Text: err.Error(), + } + } + + return nil +} diff --git a/server/listen/imap_server/session_fetch.go b/server/listen/imap_server/session_fetch.go new file mode 100644 index 0000000..9030479 --- /dev/null +++ b/server/listen/imap_server/session_fetch.go @@ -0,0 +1,113 @@ +package imap_server + +import ( + "bytes" + "fmt" + "github.com/Jinnrry/pmail/config" + "github.com/Jinnrry/pmail/dto/parsemail" + "github.com/Jinnrry/pmail/dto/response" + "github.com/Jinnrry/pmail/services/detail" + "github.com/Jinnrry/pmail/services/list" + "github.com/Jinnrry/pmail/utils/context" + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/imapserver" + "github.com/spf13/cast" + "mime" + "strings" + "time" +) + +func (s *serverSession) Fetch(w *imapserver.FetchWriter, numSet imap.NumSet, options *imap.FetchOptions) error { + switch numSet.(type) { + case imap.SeqSet: + seqSet := numSet.(imap.SeqSet) + for _, seq := range seqSet { + emailList := list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{ + Star: cast.ToInt(seq.Start), + End: cast.ToInt(seq.Stop), + }, false) + write(s.ctx, w, emailList, options) + } + + case imap.UIDSet: + uidSet := numSet.(imap.UIDSet) + for _, uid := range uidSet { + emailList := list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{ + Star: cast.ToInt(uint32(uid.Start)), + End: cast.ToInt(uint32(uid.Stop)), + }, true) + write(s.ctx, w, emailList, options) + } + } + return nil +} + +func write(ctx *context.Context, w *imapserver.FetchWriter, emailList []*response.EmailResponseData, options *imap.FetchOptions) { + for _, email := range emailList { + writer := w.CreateMessage(cast.ToUint32(email.SerialNumber)) + if options.UID { + writer.WriteUID(imap.UID(email.UeId)) + } + if options.RFC822Size { + emailContent := parsemail.NewEmailFromModel(email.Email).BuildBytes(ctx, false) + writer.WriteRFC822Size(cast.ToInt64(len(emailContent))) + } + if options.Flags { + if email.IsRead == 1 { + writer.WriteFlags([]imap.Flag{imap.FlagSeen}) + } else { + writer.WriteFlags([]imap.Flag{}) + } + } + if options.InternalDate { + writer.WriteInternalDate(email.CreateTime) + } + for _, section := range options.BodySection { + if !section.Peek { + detail.MakeRead(ctx, email.Id, true) + } + emailContent := parsemail.NewEmailFromModel(email.Email).BuildBytes(ctx, false) + + if section.Specifier == imap.PartSpecifierNone || section.Specifier == imap.PartSpecifierText { + bodyWriter := writer.WriteBodySection(section, cast.ToInt64(len(emailContent))) + bodyWriter.Write(emailContent) + bodyWriter.Close() + } + if section.Specifier == imap.PartSpecifierHeader { + var b bytes.Buffer + parseEmail := parsemail.NewEmailFromModel(email.Email) + for _, field := range section.HeaderFields { + switch field { + case "date": + fmt.Fprintf(&b, "Date: %s\r\n", email.CreateTime.Format(time.RFC1123Z)) + case "subject": + fmt.Fprintf(&b, "Subject: %s\r\n", mime.QEncoding.Encode("utf-8", email.Subject)) + case "from": + if email.FromName != "" { + fmt.Fprintf(&b, "From: %s <%s>\r\n", mime.QEncoding.Encode("utf-8", email.FromName), email.FromAddress) + } else { + fmt.Fprintf(&b, "From: %s\r\n", email.FromAddress) + } + case "to": + fmt.Fprintf(&b, "To: %s\r\n", parseEmail.BuildTo2String()) + case "cc": + if len(parseEmail.Cc) > 0 { + fmt.Fprintf(&b, "Cc: %s\r\n", parseEmail.BuildCc2String()) + } + case "message-id": + fmt.Fprintf(&b, "Message-ID: %s\r\n", fmt.Sprintf("%d@%s", email.Id, config.Instance.Domain)) + case "content-type": + args := strings.SplitN(string(emailContent), "\r\n", 3) + fmt.Fprintf(&b, "%s%s\r\n", args[0], args[1]) + } + } + + bodyWriter := writer.WriteBodySection(section, cast.ToInt64(b.Len())) + bodyWriter.Write(b.Bytes()) + bodyWriter.Close() + } + + } + writer.Close() + } +} diff --git a/server/listen/imap_server/session_idle.go b/server/listen/imap_server/session_idle.go new file mode 100644 index 0000000..82c8711 --- /dev/null +++ b/server/listen/imap_server/session_idle.go @@ -0,0 +1,52 @@ +package imap_server + +import ( + "github.com/Jinnrry/pmail/models" + "github.com/Jinnrry/pmail/utils/context" + "github.com/emersion/go-imap/v2/imapserver" + "github.com/spf13/cast" + "sync" +) + +var userConnects sync.Map + +func (s *serverSession) Idle(w *imapserver.UpdateWriter, stop <-chan struct{}) error { + connects, ok := userConnects.Load(s.ctx.UserID) + logId := cast.ToString(s.ctx.GetValue(context.LogID)) + + if !ok { + + connects = map[string]*imapserver.UpdateWriter{ + logId: w, + } + userConnects.Store(s.ctx.UserID, connects) + } else { + connects := connects.(map[string]*imapserver.UpdateWriter) + if _, ok := connects[logId]; !ok { + connects[logId] = w + userConnects.Store(s.ctx.UserID, connects) + } + } + + go func() { + <-stop + userConnects.Delete(logId) + }() + + return nil +} + +func IdleNotice(ctx *context.Context, userId int, email *models.Email) error { + if userId == 0 || email == nil || email.Id == 0 { + return nil + } + + connects, ok := userConnects.Load(userId) + if ok { + connects := connects.(map[string]*imapserver.UpdateWriter) + for _, connect := range connects { + connect.WriteNumMessages(1) + } + } + return nil +} diff --git a/server/listen/imap_server/session_list.go b/server/listen/imap_server/session_list.go new file mode 100644 index 0000000..d9b47b1 --- /dev/null +++ b/server/listen/imap_server/session_list.go @@ -0,0 +1,106 @@ +package imap_server + +import ( + "github.com/Jinnrry/pmail/db" + "github.com/Jinnrry/pmail/models" + "github.com/Jinnrry/pmail/utils/context" + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/imapserver" + log "github.com/sirupsen/logrus" + "strings" +) + +func matchGroup(ctx *context.Context, w *imapserver.ListWriter, basePath, pattern string) { + var groups []*models.Group + if basePath == "" && pattern == "*" { + db.Instance.Table("group").Where("user_id=?", ctx.UserID).Find(&groups) + //w.WriteList(&imap.ListData{ + // Attrs: []imap.MailboxAttr{imap.MailboxAttrNoSelect, imap.MailboxAttrHasChildren}, + // Delim: '/', + // Mailbox: "[PMail]", + //}) + w.WriteList(&imap.ListData{ + Attrs: []imap.MailboxAttr{imap.MailboxAttrHasNoChildren}, + Delim: '/', + Mailbox: "INBOX", + }) + w.WriteList(&imap.ListData{ + Attrs: []imap.MailboxAttr{imap.MailboxAttrHasNoChildren, imap.MailboxAttrSent}, + Delim: '/', + Mailbox: "Sent Messages", + }) + w.WriteList(&imap.ListData{ + Attrs: []imap.MailboxAttr{imap.MailboxAttrHasNoChildren, imap.MailboxAttrDrafts}, + Delim: '/', + Mailbox: "Drafts", + }) + + w.WriteList(&imap.ListData{ + Attrs: []imap.MailboxAttr{imap.MailboxAttrHasNoChildren, imap.MailboxAttrTrash}, + Delim: '/', + Mailbox: "Deleted Messages", + }) + w.WriteList(&imap.ListData{ + Attrs: []imap.MailboxAttr{imap.MailboxAttrHasNoChildren, imap.MailboxAttrJunk}, + Delim: '/', + Mailbox: "Junk", + }) + } else { + pattern = strings.ReplaceAll(pattern, "/*", "/%") + + db.Instance.Table("group").Where("user_id=? and full_path like ?", ctx.UserID, pattern).Find(&groups) + + } + for _, group := range groups { + + data := &imap.ListData{ + Attrs: []imap.MailboxAttr{}, + Mailbox: group.Name, + Delim: '/', + } + + if hasChildren(ctx, group.ID) { + data.Attrs = append(data.Attrs, imap.MailboxAttrHasChildren) + } + + data.Mailbox = getLayerName(ctx, group, true) + + w.WriteList(data) + + } + +} + +func hasChildren(ctx *context.Context, id int) bool { + var parent []*models.Group + db.Instance.Table("group").Where("parent_id=?", id).Find(&parent) + return len(parent) > 0 +} +func getLayerName(ctx *context.Context, item *models.Group, allPath bool) string { + if item.ParentId == 0 { + return item.Name + } + var parent models.Group + _, _ = db.Instance.Table("group").Where("id=?", item.ParentId).Get(&parent) + if allPath { + return getLayerName(ctx, &parent, allPath) + "/" + item.Name + } + return getLayerName(ctx, &parent, allPath) +} + +func (s *serverSession) List(w *imapserver.ListWriter, ref string, patterns []string, options *imap.ListOptions) error { + log.WithContext(s.ctx).Debugf("imap server list, ref: %s ,patterns: %s ", ref, patterns) + + if ref == "" && len(patterns) == 0 { + w.WriteList(&imap.ListData{ + Attrs: []imap.MailboxAttr{imap.MailboxAttrNoSelect, imap.MailboxAttrHasChildren}, + Delim: '/', + Mailbox: "[PMail]", + }) + } + for _, pattern := range patterns { + matchGroup(s.ctx, w, ref, pattern) + } + + return nil +} diff --git a/server/listen/imap_server/session_login.go b/server/listen/imap_server/session_login.go new file mode 100644 index 0000000..bd21c40 --- /dev/null +++ b/server/listen/imap_server/session_login.go @@ -0,0 +1,46 @@ +package imap_server + +import ( + "database/sql" + "github.com/Jinnrry/pmail/db" + "github.com/Jinnrry/pmail/models" + "github.com/Jinnrry/pmail/utils/errors" + "github.com/Jinnrry/pmail/utils/password" + "github.com/emersion/go-imap/v2" + log "github.com/sirupsen/logrus" + "strings" +) + +func (s *serverSession) Login(username, pwd string) error { + if strings.Contains(username, "@") { + args := strings.Split(username, "@") + username = args[0] + } + + var user models.User + + encodePwd := password.Encode(pwd) + + _, err := db.Instance.Where("account =? and password =? and disabled = 0", username, encodePwd).Get(&user) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + log.WithContext(s.ctx).Errorf("%+v", err) + } + + if user.ID > 0 { + s.status = AUTHORIZED + + s.ctx.UserID = user.ID + s.ctx.UserName = user.Name + s.ctx.UserAccount = user.Account + log.WithContext(s.ctx).Debug("Login successful") + + return nil + } + + log.WithContext(s.ctx).Info("user not found") + return &imap.Error{ + Type: imap.StatusResponseTypeNo, + Code: imap.ResponseCodeAuthenticationFailed, + Text: "Invalid credentials (Failure)", + } +} diff --git a/server/listen/imap_server/session_move.go b/server/listen/imap_server/session_move.go new file mode 100644 index 0000000..58eb458 --- /dev/null +++ b/server/listen/imap_server/session_move.go @@ -0,0 +1,78 @@ +package imap_server + +import ( + "github.com/Jinnrry/pmail/dto/response" + "github.com/Jinnrry/pmail/services/group" + "github.com/Jinnrry/pmail/services/list" + "github.com/Jinnrry/pmail/utils/context" + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/imapserver" + "github.com/spf13/cast" +) + +func (s *serverSession) Move(w *imapserver.MoveWriter, numSet imap.NumSet, dest string) error { + + var emailList []*response.EmailResponseData + + switch numSet.(type) { + case imap.SeqSet: + seqSet := numSet.(imap.SeqSet) + for _, seq := range seqSet { + emailList = list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{ + Star: cast.ToInt(seq.Start), + End: cast.ToInt(seq.Stop), + }, false) + } + case imap.UIDSet: + uidSet := numSet.(imap.UIDSet) + for _, uid := range uidSet { + emailList = list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{ + Star: cast.ToInt(uint32(uid.Start)), + End: cast.ToInt(uint32(uid.Stop)), + }, true) + } + } + + var mailIds []int + for _, email := range emailList { + mailIds = append(mailIds, email.Id) + } + + if group.IsDefaultBox(dest) { + return move2defaultbox(s.ctx, mailIds, dest) + } else { + return move2userbox(s.ctx, mailIds, dest) + } + +} + +func move2defaultbox(ctx *context.Context, mailIds []int, dest string) error { + err := group.Move2DefaultBox(ctx, mailIds, dest) + if err != nil { + return &imap.Error{ + Type: imap.StatusResponseTypeNo, + Text: err.Error(), + } + } + return nil +} + +func move2userbox(ctx *context.Context, mailIds []int, dest string) error { + groupInfo, err := group.GetGroupByFullPath(ctx, dest) + if err != nil { + return &imap.Error{ + Type: imap.StatusResponseTypeNo, + Text: err.Error(), + } + } + if groupInfo == nil || groupInfo.ID == 0 { + return &imap.Error{ + Type: imap.StatusResponseTypeNo, + Text: "Group not found", + } + } + + group.MoveMailToGroup(ctx, mailIds, groupInfo.ID) + + return nil +} diff --git a/server/listen/imap_server/session_namespace.go b/server/listen/imap_server/session_namespace.go new file mode 100644 index 0000000..e6ef193 --- /dev/null +++ b/server/listen/imap_server/session_namespace.go @@ -0,0 +1,16 @@ +package imap_server + +import ( + "github.com/emersion/go-imap/v2" +) + +func (s *serverSession) Namespace() (*imap.NamespaceData, error) { + return &imap.NamespaceData{ + Personal: []imap.NamespaceDescriptor{ + { + Prefix: "", + Delim: '/', + }, + }, + }, nil +} diff --git a/server/listen/imap_server/session_poll.go b/server/listen/imap_server/session_poll.go new file mode 100644 index 0000000..0e55061 --- /dev/null +++ b/server/listen/imap_server/session_poll.go @@ -0,0 +1,20 @@ +package imap_server + +import ( + "github.com/Jinnrry/pmail/db" + "github.com/Jinnrry/pmail/models" + "github.com/emersion/go-imap/v2/imapserver" + "github.com/spf13/cast" +) + +func (s *serverSession) Poll(w *imapserver.UpdateWriter, allowExpunge bool) error { + + var ue []models.UserEmail + db.Instance.Table("user_email").Where("user_id=? and create >=?", s.ctx.UserID, s.connectTime).Find(&ue) + + if len(ue) > 0 { + w.WriteNumMessages(cast.ToUint32(len(ue))) + } + + return nil +} diff --git a/server/listen/imap_server/session_rename.go b/server/listen/imap_server/session_rename.go new file mode 100644 index 0000000..7db1568 --- /dev/null +++ b/server/listen/imap_server/session_rename.go @@ -0,0 +1,34 @@ +package imap_server + +import ( + "github.com/Jinnrry/pmail/services/group" + "github.com/emersion/go-imap/v2" + "strings" +) + +func (s *serverSession) Rename(mailbox, newName string) error { + if group.IsDefaultBox(mailbox) { + return &imap.Error{ + Type: imap.StatusResponseTypeNo, + Text: "This mailbox does not support rename.", + } + } + + groupPath := strings.Split(mailbox, "/") + + oldGroupName := groupPath[len(groupPath)-1] + + newGroupPath := strings.Split(newName, "/") + + newGroupName := newGroupPath[len(newGroupPath)-1] + + err := group.Rename(s.ctx, oldGroupName, newGroupName) + + if err != nil { + return &imap.Error{ + Type: imap.StatusResponseTypeNo, + Text: err.Error(), + } + } + return nil +} diff --git a/server/listen/imap_server/session_search.go b/server/listen/imap_server/session_search.go new file mode 100644 index 0000000..0ccfe03 --- /dev/null +++ b/server/listen/imap_server/session_search.go @@ -0,0 +1,46 @@ +package imap_server + +import ( + "github.com/Jinnrry/pmail/dto/response" + "github.com/Jinnrry/pmail/services/list" + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/imapserver" + log "github.com/sirupsen/logrus" + "github.com/spf13/cast" +) + +func (s *serverSession) Search(kind imapserver.NumKind, criteria *imap.SearchCriteria, options *imap.SearchOptions) (*imap.SearchData, error) { + retList := []*response.UserEmailUIDData{} + + for _, uidSet := range criteria.UID { + for _, uid := range uidSet { + res := list.GetUEListByUID(s.ctx, s.currentMailbox, cast.ToInt(uid.Start), cast.ToInt(uid.Stop), nil) + retList = append(retList, res...) + } + } + ret := &imap.SearchData{} + + if kind == imapserver.NumKindSeq { + idList := imap.SeqSet{} + for _, data := range retList { + log.WithContext(s.ctx).Debugf("Search Seq result: UID: %d EmailID:%d", data.ID, data.EmailID) + idList = append(idList, imap.SeqRange{ + Start: cast.ToUint32(data.SerialNumber), + Stop: cast.ToUint32(data.SerialNumber), + }) + } + ret.All = idList + } else { + idList := imap.UIDSet{} + for _, data := range retList { + log.WithContext(s.ctx).Debugf("Search UID result: UID: %d EmailID:%d", data.ID, data.EmailID) + + idList = append(idList, imap.UIDRange{ + Start: imap.UID(data.ID), + Stop: imap.UID(data.ID), + }) + } + ret.All = idList + } + return ret, nil +} diff --git a/server/listen/imap_server/session_select.go b/server/listen/imap_server/session_select.go new file mode 100644 index 0000000..6d008aa --- /dev/null +++ b/server/listen/imap_server/session_select.go @@ -0,0 +1,32 @@ +package imap_server + +import ( + "github.com/Jinnrry/pmail/services/group" + "github.com/emersion/go-imap/v2" + "github.com/spf13/cast" + "strings" +) + +func (s *serverSession) Select(mailbox string, options *imap.SelectOptions) (*imap.SelectData, error) { + if "" == mailbox { + return nil, &imap.Error{ + Type: imap.StatusResponseTypeBad, + Text: "mailbox not found", + } + } + + paths := strings.Split(mailbox, "/") + s.currentMailbox = strings.Trim(paths[len(paths)-1], `"`) + _, data := group.GetGroupStatus(s.ctx, s.currentMailbox, []string{"MESSAGES", "UNSEEN", "UIDNEXT", "UIDVALIDITY"}) + + ret := &imap.SelectData{ + Flags: []imap.Flag{imap.FlagSeen}, + PermanentFlags: []imap.Flag{imap.FlagSeen}, + NumMessages: cast.ToUint32(data["MESSAGES"]), + UIDNext: imap.UID(data["UIDNEXT"]), + UIDValidity: cast.ToUint32(data["UIDVALIDITY"]), + } + + return ret, nil + +} diff --git a/server/listen/imap_server/session_status.go b/server/listen/imap_server/session_status.go new file mode 100644 index 0000000..fc3c64f --- /dev/null +++ b/server/listen/imap_server/session_status.go @@ -0,0 +1,40 @@ +package imap_server + +import ( + "github.com/Jinnrry/pmail/services/group" + "github.com/emersion/go-imap/v2" + "github.com/spf13/cast" +) + +func (s *serverSession) Status(mailbox string, options *imap.StatusOptions) (*imap.StatusData, error) { + category := []string{} + if options.UIDNext { + category = append(category, "UIDNEXT") + } + if options.NumMessages { + category = append(category, "MESSAGES") + } + if options.UIDValidity { + category = append(category, "UIDVALIDITY") + } + if options.NumUnseen { + category = append(category, "UNSEEN") + } + + _, data := group.GetGroupStatus(s.ctx, mailbox, category) + + numMessages := cast.ToUint32(data["MESSAGES"]) + numUnseen := cast.ToUint32(data["UNSEEN"]) + numValidity := cast.ToUint32(data["UIDVALIDITY"]) + numUIDNext := cast.ToUint32(data["UIDNEXT"]) + + ret := &imap.StatusData{ + Mailbox: mailbox, + NumMessages: &numMessages, + UIDNext: imap.UID(numUIDNext), + UIDValidity: numValidity, + NumUnseen: &numUnseen, + } + + return ret, nil +} diff --git a/server/listen/imap_server/session_store.go b/server/listen/imap_server/session_store.go new file mode 100644 index 0000000..06fd54d --- /dev/null +++ b/server/listen/imap_server/session_store.go @@ -0,0 +1,47 @@ +package imap_server + +import ( + "github.com/Jinnrry/pmail/services/detail" + "github.com/Jinnrry/pmail/services/list" + "github.com/Jinnrry/pmail/utils/array" + "github.com/emersion/go-imap/v2" + "github.com/emersion/go-imap/v2/imapserver" + "github.com/spf13/cast" +) + +func (s *serverSession) Store(w *imapserver.FetchWriter, numSet imap.NumSet, flags *imap.StoreFlags, options *imap.StoreOptions) error { + if flags.Op == imap.StoreFlagsSet { + return nil + } + + if !array.InArray(imap.FlagSeen, flags.Flags) { + return nil + } + + switch numSet.(type) { + case imap.SeqSet: + seqSet := numSet.(imap.SeqSet) + for _, seq := range seqSet { + emailList := list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{ + Star: cast.ToInt(seq.Start), + End: cast.ToInt(seq.Stop), + }, false) + for _, data := range emailList { + detail.MakeRead(s.ctx, data.Id, flags.Op == imap.StoreFlagsAdd) + } + } + + case imap.UIDSet: + uidSet := numSet.(imap.UIDSet) + for _, uid := range uidSet { + emailList := list.GetEmailListByGroup(s.ctx, s.currentMailbox, list.ImapListReq{ + Star: cast.ToInt(uint32(uid.Start)), + End: cast.ToInt(uint32(uid.Stop)), + }, true) + for _, data := range emailList { + detail.MakeRead(s.ctx, data.Id, flags.Op == imap.StoreFlagsAdd) + } + } + } + return nil +} diff --git a/server/pop3_server/action.go b/server/listen/pop3_server/action.go similarity index 96% rename from server/pop3_server/action.go rename to server/listen/pop3_server/action.go index d275393..4bd4287 100644 --- a/server/pop3_server/action.go +++ b/server/listen/pop3_server/action.go @@ -2,6 +2,7 @@ package pop3_server import ( "database/sql" + errors2 "errors" "github.com/Jinnrry/gopop" "github.com/Jinnrry/pmail/consts" "github.com/Jinnrry/pmail/db" @@ -122,7 +123,7 @@ func (a action) Pass(session *gopop.Session, pwd string) error { return nil } - return errors.New("password error") + return errors2.New("password error") } // Apop APOP登陆命令 @@ -159,7 +160,7 @@ func (a action) Apop(session *gopop.Session, username, digest string) error { return nil } - return errors.New("password error") + return errors2.New("password error") } @@ -298,7 +299,7 @@ func (a action) Top(session *gopop.Session, id int64, n int) (string, error) { email, err := detail.GetEmailDetail(session.Ctx.(*context.Context), cast.ToInt(id), false) if err != nil { log.WithContext(session.Ctx.(*context.Context)).Errorf("%+v", err) - return "", errors.New("server error") + return "", errors2.New("password error") } ret := parsemail.NewEmailFromModel(email.Email).BuildBytes(session.Ctx.(*context.Context), false) @@ -327,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 diff --git a/server/listen/pop3_server/action_test.go b/server/listen/pop3_server/action_test.go new file mode 100644 index 0000000..6e081f9 --- /dev/null +++ b/server/listen/pop3_server/action_test.go @@ -0,0 +1,57 @@ +package pop3_server + +import ( + "bytes" + "fmt" + "github.com/Jinnrry/gopop" + "github.com/Jinnrry/pmail/config" + "github.com/Jinnrry/pmail/db" + "github.com/Jinnrry/pmail/utils/context" + "github.com/emersion/go-message/mail" + "io" + "testing" +) + +func Test_action_Retr(t *testing.T) { + config.Init() + config.Instance.DbType = config.DBTypeSQLite + config.Instance.DbDSN = config.ROOT_PATH + "./config/pmail_temp.db" + db.Init("") + + a := action{} + session := &gopop.Session{ + Ctx: &context.Context{ + UserID: 1, + }, + } + got, got1, err := a.Retr(session, 301) + + _, _, _ = got, got1, err +} + +func Test_email(t *testing.T) { + var b bytes.Buffer + + // Create our mail header + var h mail.Header + + // Create a new mail writer + mw, _ := mail.CreateWriter(&b, h) + + // Create a text part + tw, _ := mw.CreateInline() + + var html mail.InlineHeader + + html.Header.Set("Content-Transfer-Encoding", "base64") + w, _ := tw.CreatePart(html) + + io.WriteString(w, "=") + + w.Close() + + tw.Close() + + fmt.Printf("%s", b.String()) + +} diff --git a/server/pop3_server/pop3server.go b/server/listen/pop3_server/pop3server.go similarity index 100% rename from server/pop3_server/pop3server.go rename to server/listen/pop3_server/pop3server.go diff --git a/server/smtp_server/read_content.go b/server/listen/smtp_server/read_content.go similarity index 95% rename from server/smtp_server/read_content.go rename to server/listen/smtp_server/read_content.go index d82d312..1ea6ce7 100644 --- a/server/smtp_server/read_content.go +++ b/server/listen/smtp_server/read_content.go @@ -9,6 +9,7 @@ import ( "github.com/Jinnrry/pmail/dto/parsemail" "github.com/Jinnrry/pmail/hooks" "github.com/Jinnrry/pmail/hooks/framework" + "github.com/Jinnrry/pmail/listen/imap_server" "github.com/Jinnrry/pmail/models" "github.com/Jinnrry/pmail/services/rule" "github.com/Jinnrry/pmail/utils/array" @@ -85,7 +86,7 @@ func (s *Session) Data(r io.Reader) error { } // 转发 - _, err := saveEmail(ctx, len(emailData), email, s.Ctx.UserID, 1, nil, true, true) + _, _, err := saveEmail(ctx, len(emailData), email, s.Ctx.UserID, 1, nil, true, true) if err != nil { log.WithContext(ctx).Errorf("Email Save Error %v", err) } @@ -159,7 +160,7 @@ func (s *Session) Data(r io.Reader) error { return nil } - users, _ := saveEmail(ctx, len(emailData), email, 0, 0, s.To, SPFStatus, dkimStatus) + users, dbEmail, _ := saveEmail(ctx, len(emailData), email, 0, 0, s.To, SPFStatus, dkimStatus) if email.MessageId > 0 { log.WithContext(ctx).Debugf("开始执行邮件规则!") @@ -192,12 +193,17 @@ func (s *Session) Data(r io.Reader) error { as3.Wait() log.WithContext(ctx).Debugf("开始执行插件ReceiveSaveAfter!End") + // IDLE命令通知 + for _, user := range users { + imap_server.IdleNotice(ctx, user.ID, dbEmail) + } + } return nil } -func saveEmail(ctx *context.Context, size int, email *parsemail.Email, sendUserID int, emailType int, reallyTo []string, SPFStatus, dkimStatus bool) ([]*models.User, error) { +func saveEmail(ctx *context.Context, size int, email *parsemail.Email, sendUserID int, emailType int, reallyTo []string, SPFStatus, dkimStatus bool) ([]*models.User, *models.Email, error) { var dkimV, spfV int8 if dkimStatus { dkimV = 1 @@ -209,7 +215,7 @@ func saveEmail(ctx *context.Context, size int, email *parsemail.Email, sendUserI log.WithContext(ctx).Debugf("开始入库!") if email == nil { - return nil, nil + return nil, nil, nil } modelEmail := models.Email{ @@ -305,7 +311,7 @@ func saveEmail(ctx *context.Context, size int, email *parsemail.Email, sendUserI } } - return users, nil + return users, &modelEmail, nil } func json2string(d any) string { diff --git a/server/smtp_server/read_content_test.go b/server/listen/smtp_server/read_content_test.go similarity index 99% rename from server/smtp_server/read_content_test.go rename to server/listen/smtp_server/read_content_test.go index 6580ce2..80d2317 100644 --- a/server/smtp_server/read_content_test.go +++ b/server/listen/smtp_server/read_content_test.go @@ -38,9 +38,9 @@ func testInit() { time.Local = cst config.Init() - config.Instance.DkimPrivateKeyPath = "../config/dkim/dkim.priv" + config.Instance.DkimPrivateKeyPath = config.ROOT_PATH + "./config/dkim/dkim.priv" config.Instance.DbType = config.DBTypeSQLite - config.Instance.DbDSN = "../config/pmail_temp.db" + config.Instance.DbDSN = config.ROOT_PATH + "./config/pmail_temp.db" parsemail2.Init() db.Init("") diff --git a/server/smtp_server/smtp.go b/server/listen/smtp_server/smtp.go similarity index 100% rename from server/smtp_server/smtp.go rename to server/listen/smtp_server/smtp.go diff --git a/server/smtp_server/smtp_test/sendEmailTest.py b/server/listen/smtp_server/smtp_test/sendEmailTest.py similarity index 100% rename from server/smtp_server/smtp_test/sendEmailTest.py rename to server/listen/smtp_server/smtp_test/sendEmailTest.py diff --git a/server/main.go b/server/main.go index 08f15b6..839f1b0 100644 --- a/server/main.go +++ b/server/main.go @@ -1,39 +1,12 @@ package main import ( - "bytes" - "fmt" "github.com/Jinnrry/pmail/config" - "github.com/Jinnrry/pmail/cron_server" + "github.com/Jinnrry/pmail/listen/cron_server" "github.com/Jinnrry/pmail/res_init" - "github.com/Jinnrry/pmail/utils/context" log "github.com/sirupsen/logrus" - "os" - "time" ) -type logFormatter struct { -} - -// Format 定义日志输出格式 -func (l *logFormatter) Format(entry *log.Entry) ([]byte, error) { - b := bytes.Buffer{} - - b.WriteString(fmt.Sprintf("[%s]", entry.Level.String())) - b.WriteString(fmt.Sprintf("[%s]", entry.Time.Format("2006-01-02 15:04:05"))) - if entry.Context != nil { - ctx := entry.Context.(*context.Context) - if ctx != nil { - b.WriteString(fmt.Sprintf("[%s]", ctx.GetValue(context.LogID))) - } - } - b.WriteString(fmt.Sprintf("[%s:%d]", entry.Caller.File, entry.Caller.Line)) - b.WriteString(entry.Message) - - b.WriteString("\n") - return b.Bytes(), nil -} - var ( gitHash string buildTime string @@ -42,38 +15,9 @@ var ( ) func main() { - // 设置日志格式为json格式 - log.SetFormatter(&logFormatter{}) - log.SetReportCaller(true) - - // 设置将日志输出到标准输出(默认的输出为stderr,标准错误) - // 日志消息输出可以是任意的io.writer类型 - log.SetOutput(os.Stdout) - - var cstZone = time.FixedZone("CST", 8*3600) - time.Local = cstZone config.Init() - if config.Instance != nil { - switch config.Instance.LogLevel { - case "": - log.SetLevel(log.InfoLevel) - case "debug": - log.SetLevel(log.DebugLevel) - case "info": - log.SetLevel(log.InfoLevel) - case "warn": - log.SetLevel(log.WarnLevel) - case "error": - log.SetLevel(log.ErrorLevel) - default: - log.SetLevel(log.InfoLevel) - } - } else { - log.SetLevel(log.InfoLevel) - } - if version == "" { version = "TestVersion" } diff --git a/server/main_test.go b/server/main_test.go index 091d38d..727d313 100644 --- a/server/main_test.go +++ b/server/main_test.go @@ -4,10 +4,10 @@ import ( "encoding/json" "flag" "fmt" + "github.com/Jinnrry/pmail/config" "github.com/Jinnrry/pmail/db" "github.com/Jinnrry/pmail/dto/response" "github.com/Jinnrry/pmail/models" - "github.com/Jinnrry/pmail/services/setup" "github.com/Jinnrry/pmail/signal" "github.com/Jinnrry/pmail/utils/array" "github.com/spf13/cast" @@ -54,22 +54,25 @@ func TestMaster(t *testing.T) { t.Run("testPwdSet", testPwdSet) t.Run("testDomainSet", testDomainSet) t.Run("testDNSSet", testDNSSet) - cfg, err := setup.ReadConfig() + cfg, err := config.ReadConfig() if err != nil { t.Fatal(err) } cfg.HttpsEnabled = 2 cfg.HttpPort = TestPort - err = setup.WriteConfig(cfg) + cfg.LogLevel = "debug" + err = config.WriteConfig(cfg) if err != nil { t.Fatal(err) } t.Run("testSSLSet", testSSLSet) + t.Logf("Stop 8 Second for wating restart") time.Sleep(8 * time.Second) t.Run("testLogin", testLogin) // 登录管理员账号 t.Run("testCreateUser", testCreateUser) // 创建3个测试用户 t.Run("testEditUser", testEditUser) // 编辑user2,封禁user3 t.Run("testSendEmail", testSendEmail) + t.Logf("Stop 8 Second for wating sending") time.Sleep(8 * time.Second) t.Run("testEmailList", testEmailList) t.Run("testGetDetail", testGetEmailDetail) @@ -99,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) @@ -287,8 +294,10 @@ func testCreateUser(t *testing.T) { func testPort(t *testing.T) { if !portCheck(TestPort) { t.Error("port check failed") + } else { + t.Log("port check passed") } - t.Log("port check passed") + } func testDataBaseSet(t *testing.T) { @@ -303,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() @@ -318,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"} ` } @@ -658,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": "