فهرست منبع

v2.4.3 (#5)

插件迭代、收信规则bug修复
修复邮箱地址解析错误
修改pop删除命令为软删除
升级所有依赖
jinnrry 2 سال پیش
والد
کامیت
096b4b8fed

+ 12 - 11
Dockerfile

@@ -9,14 +9,13 @@ RUN yarn && yarn build
 FROM golang:alpine as serverbuild
 ARG VERSION
 WORKDIR /work
-
-COPY --from=febuild /work/dist /work/http_server/dist
-
+COPY . .
+COPY --from=febuild /work/dist /work/server/http_server/dist
 RUN apk update && apk add git
-RUN 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/hooks/telegram_push && go build -ldflags "-s -w" -o output/telegram_push telegram_push.go
-RUN cd /work/hooks/web_push && go build -ldflags "-s -w" -o output/web_push web_push.go
-RUN cd /work/hooks/wechat_push && go build -ldflags "-s -w" -o output/wechat_push wechat_push.go
+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
+RUN cd /work/server/hooks/web_push && go build -ldflags "-s -w" -o output/web_push web_push.go
+RUN cd /work/server/hooks/wechat_push && go build -ldflags "-s -w" -o output/wechat_push wechat_push.go
 
 
 FROM alpine
@@ -31,9 +30,11 @@ RUN apk add --no-cache tzdata \
     &&rm -rf /var/cache/apk/* /tmp/* /var/tmp/* $HOME/.cache
 
 
-COPY --from=serverbuild /work/pmail .
-COPY --from=serverbuild /work/hooks/telegram_push/output/* ./plugins/
-COPY --from=serverbuild /work/hooks/web_push/output/* ./plugins/
-COPY --from=serverbuild /work/hooks/wechat_push/output/* ./plugins/
+COPY --from=serverbuild /work/server/pmail .
+COPY --from=serverbuild /work/server/hooks/telegram_push/output/* ./plugins/
+COPY --from=serverbuild /work/server/hooks/web_push/output/* ./plugins/
+COPY --from=serverbuild /work/server/hooks/wechat_push/output/* ./plugins/
+
+EXPOSE 25 80 110 443 465 995
 
 CMD /work/pmail

+ 2 - 0
DockerfileGithubAction

@@ -29,4 +29,6 @@ COPY --from=serverbuild /work/hooks/telegram_push/output/* ./plugins/
 COPY --from=serverbuild /work/hooks/web_push/output/* ./plugins/
 COPY --from=serverbuild /work/hooks/wechat_push/output/* ./plugins/
 
+EXPOSE 25 80 110 443 465 995
+
 CMD /work/pmail

+ 7 - 2
README.md

@@ -103,9 +103,9 @@ SMTP Port: 25/465(SSL)
 
 [WeChat Push](server/hooks/wechat_push/README.md)
 
-[Telegram Push](server/hooks/wechat_push/README.md)
+[Telegram Push](server/hooks/telegram_push/README.md)
 
-[Web Push](server/hooks/wechat_push/README.md)
+[Web Push](server/hooks/web_push/README.md)
 
 ## Plugin Install
 > [!IMPORTANT]
@@ -125,6 +125,11 @@ The code is in `fe` folder.
 
 The code is in `server` folder.
 
+3、How to build
+
+`make build`
+
+
 ## Api Documentation
 
 [go to wiki](https://github.com/Jinnrry/PMail/wiki/%E5%90%8E%E7%AB%AF%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3)

+ 6 - 2
README_CN.md

@@ -106,9 +106,9 @@ SMTP端口: 25/465(SSL)
 
 [微信推送](server/hooks/wechat_push/README.md)
 
-[Telegram推送](server/hooks/wechat_push/README.md)
+[Telegram推送](server/hooks/telegram_push/README.md)
 
-[WebHook推送](server/hooks/wechat_push/README.md)
+[WebHook推送](server/hooks/web_push/README.md)
 
 ## 插件安装
 > [!IMPORTANT]
@@ -128,6 +128,10 @@ SMTP端口: 25/465(SSL)
 
 后端代码进入 `server`文件夹,运行 `main.go`文件
 
+3、编译该项目
+
+`make build`
+
 ## 后端接口文档
 
 [go to wiki](https://github.com/Jinnrry/PMail/wiki/%E5%90%8E%E7%AB%AF%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3)

+ 1 - 1
server/config/config.json

@@ -8,7 +8,7 @@
   "SSLPublicKeyPath": "config/ssl/public.crt",
   "dbDSN": "./config/pmail.db",
   "dbType": "sqlite",
-  "spamFilterLevel": 2,
+  "spamFilterLevel": 1,
   "httpPort": 80,
   "httpsPort": 443,
   "isInit": true,

+ 1 - 9
server/config/config_mysql.json

@@ -8,17 +8,9 @@
   "SSLPublicKeyPath": "config/ssl/public.crt",
   "dbDSN": "root:root@tcp(127.0.0.1:3306)/pmail?parseTime=True&loc=Local",
   "dbType": "mysql",
-  "spamFilterLevel": 2,
+  "spamFilterLevel": 1,
   "httpPort": 80,
   "httpsPort": 443,
-  "weChatPushAppId": "",
-  "weChatPushSecret": "",
-  "weChatPushTemplateId": "",
-  "weChatPushUserId": "",
-  "tgChatId": "",
-  "tgBotToken": "",
-  "webPushUrl": "",
-  "webPushToken": "",
   "isInit": true,
   "httpsEnabled": 2
 }

+ 13 - 17
server/dto/parsemail/email.go

@@ -10,7 +10,6 @@ import (
 	"io"
 	"net/textproto"
 	"pmail/config"
-	"pmail/utils/array"
 	"pmail/utils/context"
 	"regexp"
 	"strings"
@@ -145,37 +144,34 @@ func BuilderUser(str string) *User {
 	return buildUser(str)
 }
 
+var emailAddressRe = regexp.MustCompile(`<(.*@.*)>`)
+
 func buildUser(str string) *User {
 	if str == "" {
 		return nil
 	}
 
 	ret := &User{}
-	args := strings.Split(str, " ")
-	if len(args) == 1 {
+
+	matched := emailAddressRe.FindStringSubmatch(str)
+
+	if len(matched) == 2 {
+		ret.EmailAddress = matched[1]
+	} else {
 		ret.EmailAddress = str
 		return ret
 	}
 
-	if len(args) > 2 {
-		targs := []string{
-			array.Join(args[0:len(args)-1], " "),
-			args[len(args)-1],
-		}
-		args = targs
-	}
+	str = strings.ReplaceAll(str, matched[0], "")
 
-	args[0] = strings.Trim(args[0], "\"")
-	args[1] = strings.TrimPrefix(args[1], "<")
-	args[1] = strings.TrimSuffix(args[1], ">")
+	str = strings.Trim(strings.TrimSpace(str), "\"")
 
-	name, err := (&WordDecoder{}).Decode(strings.ReplaceAll(args[0], "\"", ""))
+	name, err := (&WordDecoder{}).Decode(strings.ReplaceAll(str, "\"", ""))
 	if err == nil {
-		ret.Name = name
+		ret.Name = strings.TrimSpace(name)
 	} else {
-		ret.Name = args[0]
+		ret.Name = strings.TrimSpace(str)
 	}
-	ret.EmailAddress = args[1]
 	return ret
 }
 

+ 16 - 0
server/dto/parsemail/email_test.go

@@ -77,6 +77,22 @@ func Test_buildUser(t *testing.T) {
 	if u.Name != "Jinnrry N" {
 		t.Error("error")
 	}
+
+	u = buildUser("=?UTF-8?B?YWRtaW5AamlubnJyeS5jb20=?=<admin@jinnrry.com>")
+	if u.EmailAddress != "admin@jinnrry.com" {
+		t.Error("error")
+	}
+	if u.Name != "admin@jinnrry.com" {
+		t.Error("error")
+	}
+
+	u = buildUser("\"admin@jinnrry.com\" <admin@jinnrry.com>")
+	if u.EmailAddress != "admin@jinnrry.com" {
+		t.Error("error")
+	}
+	if u.Name != "admin@jinnrry.com" {
+		t.Error("error")
+	}
 }
 
 func TestEmailBuidlers(t *testing.T) {

+ 32 - 36
server/go.mod

@@ -1,51 +1,47 @@
 module pmail
 
-go 1.21
+go 1.22
 
 require (
 	github.com/Jinnrry/gopop v0.0.0-20231113115125-fbdf52ae39ea
-	github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24
-	github.com/alexedwards/scs/sqlite3store v0.0.0-20231113091146-cef4b05350c8
-	github.com/alexedwards/scs/v2 v2.5.1
-	github.com/emersion/go-message v0.18.0
-	github.com/emersion/go-msgauth v0.6.6
-	github.com/emersion/go-smtp v0.20.1
-	github.com/go-acme/lego/v4 v4.13.3
-	github.com/go-sql-driver/mysql v1.7.1
+	github.com/alexedwards/scs/mysqlstore v0.0.0-20240316134038-7e11d57e8885
+	github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885
+	github.com/alexedwards/scs/v2 v2.8.0
+	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.0
+	github.com/go-acme/lego/v4 v4.16.1
+	github.com/go-sql-driver/mysql v1.8.1
 	github.com/jmoiron/sqlx v1.3.5
 	github.com/mileusna/spf v0.9.5
 	github.com/sirupsen/logrus v1.9.3
-	github.com/spf13/cast v1.5.1
-	golang.org/x/crypto v0.10.0
+	github.com/spf13/cast v1.6.0
+	golang.org/x/crypto v0.22.0
 	golang.org/x/text v0.14.0
-	modernc.org/sqlite v1.24.0
+	modernc.org/sqlite v1.29.6
 )
 
-replace github.com/emersion/go-smtp v0.20.1 => github.com/jinnrry/go-smtp v0.20.1
-
 require (
-	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
+	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-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
-	github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
-	github.com/go-jose/go-jose/v3 v3.0.0 // indirect
-	github.com/google/uuid v1.3.0 // indirect
-	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
-	github.com/mattn/go-isatty v0.0.19 // indirect
-	github.com/mattn/go-sqlite3 v1.14.17 // indirect
-	github.com/miekg/dns v1.1.55 // indirect
+	github.com/go-jose/go-jose/v4 v4.0.1 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/miekg/dns v1.1.58 // indirect
+	github.com/ncruces/go-strftime v0.1.9 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
-	golang.org/x/mod v0.11.0 // indirect
-	golang.org/x/net v0.11.0 // indirect
-	golang.org/x/sys v0.9.0 // indirect
-	golang.org/x/tools v0.10.0 // indirect
-	lukechampine.com/uint128 v1.2.0 // indirect
-	modernc.org/cc/v3 v3.40.0 // indirect
-	modernc.org/ccgo/v3 v3.16.13 // indirect
-	modernc.org/libc v1.22.5 // indirect
-	modernc.org/mathutil v1.5.0 // indirect
-	modernc.org/memory v1.5.0 // indirect
-	modernc.org/opt v0.1.3 // indirect
-	modernc.org/strutil v1.1.3 // indirect
-	modernc.org/token v1.0.1 // indirect
+	golang.org/x/mod v0.17.0 // indirect
+	golang.org/x/net v0.24.0 // indirect
+	golang.org/x/sync v0.7.0 // indirect
+	golang.org/x/sys v0.19.0 // indirect
+	golang.org/x/tools v0.20.0 // indirect
+	modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect
+	modernc.org/libc v1.49.3 // indirect
+	modernc.org/mathutil v1.6.0 // indirect
+	modernc.org/memory v1.8.0 // indirect
+	modernc.org/strutil v1.2.0 // indirect
+	modernc.org/token v1.1.0 // indirect
 )

+ 76 - 92
server/go.sum

@@ -1,120 +1,108 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/Jinnrry/gopop v0.0.0-20231113115125-fbdf52ae39ea h1:GISNlu8fPa2K+aySmHPSd9X0PG8GAAFEobq4XIqodMk=
 github.com/Jinnrry/gopop v0.0.0-20231113115125-fbdf52ae39ea/go.mod h1:xcI6e+jbXWN+T8EWOJtHbAku6pzNqyCHaFvzdeL1r2o=
-github.com/Jinnrry/scs/sqlite3store v0.0.0-20230803080525-914f01e0d379 h1:i6LB/3lgkRDupe3owyNXtH8dtQrdaReCLeAZKrWcqAE=
-github.com/Jinnrry/scs/sqlite3store v0.0.0-20230803080525-914f01e0d379/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
-github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24 h1:1jXpX7IE/zuf9FZQJpqZNepXqW8mq6NLzplHDCA43HY=
-github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24/go.mod h1:ShejCOaSJCEjCWjc7YBrgy2xd0Kp+wiyBdzTNQrAGn4=
-github.com/alexedwards/scs/sqlite3store v0.0.0-20230327161757-10d4299e3b24/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
-github.com/alexedwards/scs/sqlite3store v0.0.0-20231113091146-cef4b05350c8 h1:mnXnnXEjn8QIyv4KCN0+IjDlXA64qdq2hIVOmfNFeuY=
-github.com/alexedwards/scs/sqlite3store v0.0.0-20231113091146-cef4b05350c8/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
-github.com/alexedwards/scs/v2 v2.5.1 h1:EhAz3Kb3OSQzD8T+Ub23fKsiuvE0GzbF5Lgn0uTwM3Y=
-github.com/alexedwards/scs/v2 v2.5.1/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
-github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
-github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/alexedwards/scs/mysqlstore v0.0.0-20240316134038-7e11d57e8885 h1:C7QAamNjR5yz6di4KJWAKcnxueKBgq4L/JGXhlnu35w=
+github.com/alexedwards/scs/mysqlstore v0.0.0-20240316134038-7e11d57e8885/go.mod h1:p8jK3D80sw1PFrCSdlcJF1O75bp55HqbgDyyCLM0FrE=
+github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885 h1:+DCxWg/ojncqS+TGAuRUoV7OfG/S4doh0pcpAwEcow0=
+github.com/alexedwards/scs/sqlite3store v0.0.0-20240316134038-7e11d57e8885/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
+github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
+github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
+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/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-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
-github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
-github.com/emersion/go-message v0.18.0 h1:7LxAXHRpSeoO/Wom3ZApVZYG7c3d17yCScYce8WiXA8=
-github.com/emersion/go-message v0.18.0/go.mod h1:Zi69ACvzaoV/MBnrxfVBPV3xWEuCmC2nEN39oJF4B8A=
-github.com/emersion/go-milter v0.3.3/go.mod h1:ablHK0pbLB83kMFBznp/Rj8aV+Kc3jw8cxzzmCNLIOY=
-github.com/emersion/go-msgauth v0.6.6 h1:buv5lL8v/3v4RpHnQFS2IPhE3nxSRX+AxnrEJbDbHhA=
-github.com/emersion/go-msgauth v0.6.6/go.mod h1:A+/zaz9bzukLM6tRWRgJ3BdrBi+TFKTvQ3fGMFOI9SM=
-github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
+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=
+github.com/emersion/go-msgauth v0.6.8/go.mod h1:YDwuyTCUHu9xxmAeVj0eW4INnwB6NNZoPdLerpSxRrc=
 github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
-github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
-github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
-github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
-github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
-github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
-github.com/go-acme/lego/v4 v4.13.3 h1:aZ1S9FXIkCWG3Uw/rZKSD+MOuO8ZB1t6p9VCg6jJiNY=
-github.com/go-acme/lego/v4 v4.13.3/go.mod h1:c/iodVGMeBXG/+KiQczoNkySo3YLWTVa0kiyeVd/FHc=
-github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
-github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY=
+github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
+github.com/emersion/go-smtp v0.21.0 h1:ZDZmX9aFUuPlD1lpoT0nC/nozZuIkSCyQIyxdijjCy0=
+github.com/emersion/go-smtp v0.21.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/go-acme/lego/v4 v4.16.1 h1:JxZ93s4KG0jL27rZ30UsIgxap6VGzKuREsSkkyzeoCQ=
+github.com/go-acme/lego/v4 v4.16.1/go.mod h1:AVvwdPned/IWpD/ihHhMsKnveF7HHYAz/CmtXi7OZoE=
+github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
+github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
-github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
-github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
 github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
 github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/jinnrry/go-smtp v0.20.1 h1:coy2+Ch7+ptr3J4rHwE+YVY25eaXuUbi4PNHchfc6ro=
-github.com/jinnrry/go-smtp v0.20.1/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
 github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
-github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
-github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
 github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
-github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
-github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
-github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
-github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
-github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
+github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
+github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
 github.com/mileusna/spf v0.9.5 h1:P6cmaIBwrhZaP9stXMzGOtxe+gIu65OVbZCmrAv9rgU=
 github.com/mileusna/spf v0.9.5/go.mod h1:o6IdTae6QptAbLgx/+ueXSTSpkG+f1cqLemQJSew8sI=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
 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/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 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.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 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.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
-github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
+github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
+github.com/spf13/cast v1.6.0/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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
-golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
 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.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
-golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 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=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/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.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
-golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
+golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 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=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -128,13 +116,12 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
-golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.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=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 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=
@@ -146,41 +133,38 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
 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.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
-golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
+golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
+golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
 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=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 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=
-lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
-lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
-modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
-modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
-modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
-modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
-modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
-modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
-modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
-modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
-modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
-modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
-modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
-modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
-modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
-modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
+modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk=
+modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
+modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA=
+modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI=
+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.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
+modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
+modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b h1:BnN1t+pb1cy61zbvSUV7SeI0PwosMhlAEi/vBY4qxp8=
+modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
+modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg=
+modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+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/sqlite v1.24.0 h1:EsClRIWHGhLTCX44p+Ri/JLD+vFGo0QGjasg2/F9TlI=
-modernc.org/sqlite v1.24.0/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
-modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
-modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
-modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
-modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
-modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
-modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
-modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
-modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
+modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
+modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
+modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4=
+modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U=
+modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
+modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

+ 23 - 0
server/hooks/base.go

@@ -26,6 +26,29 @@ type HookSender struct {
 	socket string
 }
 
+func (h *HookSender) ReceiveSaveAfter(ctx *context.Context, email *parsemail.Email) {
+	log.WithContext(ctx).Debugf("[%s]Plugin ReceiveSaveAfter Start", h.name)
+
+	dto := framework.HookDTO{
+		Ctx:   ctx,
+		Email: email,
+	}
+	body, _ := json.Marshal(dto)
+
+	ret, err := h.httpc.Post("http://plugin/ReceiveSaveAfter", "application/json", strings.NewReader(string(body)))
+	if err != nil {
+		log.WithContext(ctx).Errorf("[%s] Error! %v", h.name, err)
+		return
+	}
+
+	body, _ = io.ReadAll(ret.Body)
+	json.Unmarshal(body, &dto)
+
+	ctx = dto.Ctx
+	email = dto.Email
+	log.WithContext(ctx).Debugf("[%s]Plugin ReceiveSaveAfter End", h.name)
+}
+
 func (h *HookSender) SendBefore(ctx *context.Context, email *parsemail.Email) {
 	log.WithContext(ctx).Debugf("[%s]Plugin SendBefore Start", h.name)
 

+ 17 - 1
server/hooks/framework/framework.go

@@ -22,8 +22,10 @@ type EmailHook interface {
 	SendAfter(ctx *context.Context, email *parsemail.Email, err map[string]error)
 	// ReceiveParseBefore 接收到邮件,解析之前的原始数据
 	ReceiveParseBefore(ctx *context.Context, email *[]byte)
-	// ReceiveParseAfter 接收到邮件,解析之后的结构化数据
+	// ReceiveParseAfter 接收到邮件,解析之后的结构化数据 (收信规则前,写数据库前执行)
 	ReceiveParseAfter(ctx *context.Context, email *parsemail.Email)
+	// ReceiveSaveAfter 邮件落库以后执行(收信规则后执行)
+	ReceiveSaveAfter(ctx *context.Context, email *parsemail.Email)
 }
 
 // HookDTO PMail 主程序和插件通信的结构体
@@ -142,6 +144,20 @@ func (p *Plugin) Run() {
 		writer.Write(body)
 		log.Debugf("[%s] ReceiveParseAfter End", p.name)
 	})
+	mux.HandleFunc("/ReceiveSaveAfter", func(writer http.ResponseWriter, request *http.Request) {
+		log.Debugf("[%s] ReceiveSaveAfter Start", p.name)
+		var hookDTO HookDTO
+		body, _ := io.ReadAll(request.Body)
+		err := json.Unmarshal(body, &hookDTO)
+		if err != nil {
+			log.Errorf("params error %+v", err)
+			return
+		}
+		p.hook.ReceiveSaveAfter(hookDTO.Ctx, hookDTO.Email)
+		body, _ = json.Marshal(hookDTO)
+		writer.Write(body)
+		log.Debugf("[%s] ReceiveSaveAfter End", p.name)
+	})
 
 	server := http.Server{
 		ReadTimeout:  5 * time.Second,

+ 12 - 7
server/hooks/telegram_push/telegram_push.go

@@ -21,6 +21,17 @@ type TelegramPushHook struct {
 	webDomain    string
 }
 
+func (w *TelegramPushHook) ReceiveSaveAfter(ctx *context.Context, email *parsemail.Email) {
+	if w.chatId == "" || w.botToken == "" {
+		return
+	}
+	// 被标记为已读,或者是已删除,或是垃圾邮件 就不处理了
+	if email.IsRead == 1 || email.Status == 3 || email.MessageId <= 0 {
+		return
+	}
+	w.sendUserMsg(nil, email)
+}
+
 func (w *TelegramPushHook) SendBefore(ctx *context.Context, email *parsemail.Email) {
 
 }
@@ -33,13 +44,7 @@ func (w *TelegramPushHook) ReceiveParseBefore(ctx *context.Context, email *[]byt
 
 }
 
-func (w *TelegramPushHook) ReceiveParseAfter(ctx *context.Context, email *parsemail.Email) {
-	if w.chatId == "" || w.botToken == "" {
-		return
-	}
-
-	w.sendUserMsg(nil, email)
-}
+func (w *TelegramPushHook) ReceiveParseAfter(ctx *context.Context, email *parsemail.Email) {}
 
 type SendMessageRequest struct {
 	ChatID      string      `json:"chat_id"`

+ 25 - 24
server/hooks/web_push/web_push.go

@@ -3,14 +3,13 @@ package main
 import (
 	"bytes"
 	"encoding/json"
+	log "github.com/sirupsen/logrus"
 	"net/http"
 	"os"
 	"pmail/config"
 	"pmail/dto/parsemail"
 	"pmail/hooks/framework"
 	"pmail/utils/context"
-
-	log "github.com/sirupsen/logrus"
 )
 
 type WebPushHook struct {
@@ -18,28 +17,7 @@ type WebPushHook struct {
 	token string
 }
 
-// EmailData 用于存储解析后的邮件数据
-type EmailData struct {
-	From    string   `json:"from"`
-	To      []string `json:"to"`
-	Subject string   `json:"subject"`
-	Body    string   `json:"body"`
-	Token   string   `json:"token"`
-}
-
-func (w *WebPushHook) SendBefore(ctx *context.Context, email *parsemail.Email) {
-
-}
-
-func (w *WebPushHook) SendAfter(ctx *context.Context, email *parsemail.Email, err map[string]error) {
-
-}
-
-func (w *WebPushHook) ReceiveParseBefore(ctx *context.Context, email *[]byte) {
-
-}
-
-func (w *WebPushHook) ReceiveParseAfter(ctx *context.Context, email *parsemail.Email) {
+func (w *WebPushHook) ReceiveSaveAfter(ctx *context.Context, email *parsemail.Email) {
 	if w.url == "" {
 		return
 	}
@@ -78,6 +56,29 @@ func (w *WebPushHook) ReceiveParseAfter(ctx *context.Context, email *parsemail.E
 	defer resp.Body.Close()
 }
 
+// EmailData 用于存储解析后的邮件数据
+type EmailData struct {
+	From    string   `json:"from"`
+	To      []string `json:"to"`
+	Subject string   `json:"subject"`
+	Body    string   `json:"body"`
+	Token   string   `json:"token"`
+}
+
+func (w *WebPushHook) SendBefore(ctx *context.Context, email *parsemail.Email) {
+
+}
+
+func (w *WebPushHook) SendAfter(ctx *context.Context, email *parsemail.Email, err map[string]error) {
+
+}
+
+func (w *WebPushHook) ReceiveParseBefore(ctx *context.Context, email *[]byte) {
+
+}
+
+func (w *WebPushHook) ReceiveParseAfter(ctx *context.Context, email *parsemail.Email) {}
+
 type Config struct {
 	WebPushUrl   string `json:"webPushUrl"`
 	WebPushToken string `json:"webPushToken"`

+ 20 - 13
server/hooks/wechat_push/wechat_push.go

@@ -31,23 +31,16 @@ type WeChatPushHook struct {
 	mainConfig   *config.Config
 }
 
-func (w *WeChatPushHook) SendBefore(ctx *context.Context, email *parsemail.Email) {
-
-}
-
-func (w *WeChatPushHook) SendAfter(ctx *context.Context, email *parsemail.Email, err map[string]error) {
-
-}
-
-func (w *WeChatPushHook) ReceiveParseBefore(ctx *context.Context, email *[]byte) {
-
-}
-
-func (w *WeChatPushHook) ReceiveParseAfter(ctx *context.Context, email *parsemail.Email) {
+func (w *WeChatPushHook) ReceiveSaveAfter(ctx *context.Context, email *parsemail.Email) {
 	if w.appId == "" || w.secret == "" || w.pushUser == "" {
 		return
 	}
 
+	// 被标记为已读,或者是已删除,或是垃圾邮件 就不处理了
+	if email.IsRead == 1 || email.Status == 3 || email.MessageId <= 0 {
+		return
+	}
+
 	content := string(email.Text)
 
 	if content == "" {
@@ -57,6 +50,20 @@ func (w *WeChatPushHook) ReceiveParseAfter(ctx *context.Context, email *parsemai
 	w.sendUserMsg(nil, w.pushUser, content)
 }
 
+func (w *WeChatPushHook) SendBefore(ctx *context.Context, email *parsemail.Email) {
+
+}
+
+func (w *WeChatPushHook) SendAfter(ctx *context.Context, email *parsemail.Email, err map[string]error) {
+
+}
+
+func (w *WeChatPushHook) ReceiveParseBefore(ctx *context.Context, email *[]byte) {
+
+}
+
+func (w *WeChatPushHook) ReceiveParseAfter(ctx *context.Context, email *parsemail.Email) {}
+
 func (w *WeChatPushHook) getWxAccessToken() string {
 	if w.tokenExpires > time.Now().Unix() {
 		return w.token

+ 3 - 2
server/pop3_server/action.go

@@ -178,6 +178,7 @@ func (a action) Stat(session *gopop.Session) (msgNum, msgSize int64, err error)
 	return si.Num, si.Size, nil
 }
 
+// Uidl 查询某封邮件的唯一标志符
 func (a action) Uidl(session *gopop.Session, msg string) ([]gopop.UidlItem, error) {
 	log.WithContext(session.Ctx).Debugf("POP3 CMD: UIDL ,Args:%s", msg)
 
@@ -196,7 +197,7 @@ func (a action) Uidl(session *gopop.Session, msg string) ([]gopop.UidlItem, erro
 	var err error
 	var ssql string
 
-	ssql = db.WithContext(session.Ctx.(*context.Context), "SELECT id FROM email")
+	ssql = db.WithContext(session.Ctx.(*context.Context), "SELECT id FROM email where type = 0")
 	err = db.Instance.Select(&res, ssql)
 
 	if err != nil && !errors.Is(err, sql.ErrNoRows) {
@@ -319,7 +320,7 @@ func (a action) Quit(session *gopop.Session) error {
 	log.WithContext(session.Ctx).Debugf("POP3 CMD: QUIT ")
 	if len(session.DeleteIds) > 0 {
 
-		_, err := db.Instance.Exec(db.WithContext(session.Ctx.(*context.Context), "DELETE FROM email WHERE id in ?"), session.DeleteIds)
+		_, err := db.Instance.Exec(db.WithContext(session.Ctx.(*context.Context), "UPDATE email SET status=3 WHERE id in ?"), session.DeleteIds)
 		if err != nil {
 			log.WithContext(session.Ctx.(*context.Context)).Errorf("%+v", err)
 		}

+ 53 - 0
server/pop3_server/action_test.go

@@ -0,0 +1,53 @@
+package pop3_server
+
+import (
+	"fmt"
+	"github.com/Jinnrry/gopop"
+	log "github.com/sirupsen/logrus"
+	"os"
+	"pmail/config"
+	"pmail/db"
+	parsemail2 "pmail/dto/parsemail"
+	"pmail/hooks"
+	"pmail/session"
+	"pmail/utils/context"
+	"testing"
+	"time"
+)
+
+func testInit() {
+	// 设置日志格式为json格式
+	//log.SetFormatter(&log.JSONFormatter{})
+
+	log.SetReportCaller(true)
+	log.SetFormatter(&log.TextFormatter{
+		//以下设置只是为了使输出更美观
+		DisableColors:   true,
+		TimestampFormat: "2006-01-02 15:03:04",
+	})
+
+	// 设置将日志输出到标准输出(默认的输出为stderr,标准错误)
+	// 日志消息输出可以是任意的io.writer类型
+	log.SetOutput(os.Stdout)
+
+	// 设置日志级别为warn以上
+	log.SetLevel(log.TraceLevel)
+
+	var cst, _ = time.LoadLocation("Asia/Shanghai")
+	time.Local = cst
+
+	config.Init()
+	parsemail2.Init()
+	db.Init()
+	session.Init()
+	hooks.Init("dev")
+}
+
+func Test_action_Stat(t *testing.T) {
+	testInit()
+	act := action{}
+	v1, v2, v3 := act.Stat(&gopop.Session{
+		Ctx: &context.Context{},
+	})
+	fmt.Println(v1, v2, v3)
+}

+ 10 - 0
server/services/rule/rule.go

@@ -65,8 +65,14 @@ func DoRule(ctx *context.Context, rule *dto.Rule, email *parsemail.Email) {
 	switch rule.Action {
 	case dto.READ:
 		email.IsRead = 1
+		if email.MessageId > 0 {
+			db.Instance.Exec(db.WithContext(ctx, "update email set is_read=1 where id =?"), email.MessageId)
+		}
 	case dto.DELETE:
 		email.Status = 3
+		if email.MessageId > 0 {
+			db.Instance.Exec(db.WithContext(ctx, "update email set status=3 where id =?"), email.MessageId)
+		}
 	case dto.FORWARD:
 		if strings.Contains(rule.Params, config.Instance.Domain) {
 			log.WithContext(ctx).Errorf("Forward Error! loop forwarding!")
@@ -78,5 +84,9 @@ func DoRule(ctx *context.Context, rule *dto.Rule, email *parsemail.Email) {
 		}
 	case dto.MOVE:
 		email.GroupId = cast.ToInt(rule.Params)
+		if email.MessageId > 0 {
+			db.Instance.Exec(db.WithContext(ctx, "update email set group_id=? where id =?"), email.GroupId, email.MessageId)
+		}
 	}
+
 }

+ 23 - 7
server/smtp_server/read_content.go

@@ -123,14 +123,30 @@ func (s *Session) Data(r io.Reader) error {
 
 		saveEmail(ctx, email, 0, SPFStatus, dkimStatus)
 
-		log.WithContext(ctx).Debugf("开始执行邮件规则!")
-		// 执行邮件规则
-		rs := rule.GetAllRules(ctx)
-		for _, r := range rs {
-			if rule.MatchRule(ctx, r, email) {
-				rule.DoRule(ctx, r, email)
+		if email.MessageId > 0 {
+			log.WithContext(ctx).Debugf("开始执行邮件规则!")
+			// 执行邮件规则
+			rs := rule.GetAllRules(ctx)
+			for _, r := range rs {
+				if rule.MatchRule(ctx, r, email) {
+					rule.DoRule(ctx, r, email)
+				}
 			}
 		}
+
+		log.WithContext(ctx).Debugf("开始执行插件ReceiveSaveAfter!")
+		as3 := async.New(ctx)
+		for _, hook := range hooks.HookList {
+			if hook == nil {
+				continue
+			}
+			as3.WaitProcess(func(hk any) {
+				hk.(framework.EmailHook).ReceiveSaveAfter(ctx, email)
+			}, hook)
+		}
+		as3.Wait()
+		log.WithContext(ctx).Debugf("开始执行插件ReceiveSaveAfter!End")
+
 	}
 
 	return nil
@@ -186,7 +202,7 @@ func saveEmail(ctx *context.Context, email *parsemail.Email, emailType int, SPFS
 	)
 
 	if err != nil {
-		log.WithContext(ctx).Println("mysql insert error:", err.Error())
+		log.WithContext(ctx).Errorf("db insert error:%+v", err.Error())
 	}
 	insertId, _ := res.LastInsertId()
 	if insertId > 0 {

+ 39 - 5
server/smtp_server/read_content_test.go

@@ -401,11 +401,7 @@ Content-Type: text/html
 
 	s := Session{
 		RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
-		Ctx: &context.Context{
-			UserID:      1,
-			UserName:    "a",
-			UserAccount: "a",
-		},
+		Ctx:           &context.Context{},
 	}
 
 	s.Data(bytes.NewReader([]byte(deleteEmail)))
@@ -509,3 +505,41 @@ Content-Type: text/html
 
 	s.Data(bytes.NewReader([]byte(moveEmail)))
 }
+
+func TestQAEmailForward(t *testing.T) {
+	testInit()
+	data := `Mime-Version: 1.0
+X-QQ-MIME: TCMime 1.0 by Tencent
+X-Mailer: QQMail 2.x
+X-QQ-Mailer: QQMail 2.x
+Message-ID: tencent_D82739970C66D2BFBA23F4A3@qq.com
+Subject: =?UTF-8?B?5rWL6K+V5Y+R6YCB?=
+Date: Wed, 10 Apr 2024 11:11:12 +0800 (GMT+08:00)
+From: =?UTF-8?B?YWRtaW5AamlubnJyeS5jb20=?=<admin@jinnrry.com>
+To: =?UTF-8?B??=<test@jinnrry.com>
+Content-Type: multipart/alternative; 
+        boundary="----=_Part_174_107154538.1712718674768"
+
+------=_Part_174_107154538.1712718674768
+Content-Type: text/plain; charset=us-ascii
+Content-Transfer-Encoding: base64
+
+
+------=_Part_174_107154538.1712718674768
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: base64
+
+PGRpdj7ov5nph4zmmK/lhoXlrrk8L2Rpdj48ZGl2PjwhLS1lbXB0eXNpZ24tLT48L2Rpdj4=
+------=_Part_174_107154538.1712718674768--`
+
+	s := Session{
+		RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
+		Ctx: &context.Context{
+			UserID:      1,
+			UserName:    "a",
+			UserAccount: "a",
+		},
+	}
+
+	s.Data(bytes.NewReader([]byte(data)))
+}

+ 25 - 2
server/smtp_server/smtp.go

@@ -3,6 +3,7 @@ package smtp_server
 import (
 	"crypto/tls"
 	"database/sql"
+	"github.com/emersion/go-sasl"
 	"github.com/emersion/go-smtp"
 	log "github.com/sirupsen/logrus"
 	"net"
@@ -42,6 +43,30 @@ type Session struct {
 	Ctx           *context.Context
 }
 
+// AuthMechanisms returns a slice of available auth mechanisms
+// supported in this example.
+func (s *Session) AuthMechanisms() []string {
+	return []string{sasl.Plain, sasl.Login}
+}
+
+// Auth is the handler for supported authenticators.
+func (s *Session) Auth(mech string) (sasl.Server, error) {
+	log.WithContext(s.Ctx).Debugf("Auth :%s", mech)
+	if mech == sasl.Plain {
+		return sasl.NewPlainServer(func(identity, username, password string) error {
+			return s.AuthPlain(username, password)
+		}), nil
+	}
+
+	if mech == sasl.Login {
+		return sasl.NewLoginServer(func(username, password string) error {
+			return s.AuthPlain(username, password)
+		}), nil
+	}
+
+	return nil, errors.New("Auth Not Supported")
+}
+
 func (s *Session) AuthPlain(username, pwd string) error {
 	log.WithContext(s.Ctx).Debugf("Auth %s %s", username, pwd)
 
@@ -105,7 +130,6 @@ func StartWithTLS() {
 	instanceTls.Addr = ":465"
 	instanceTls.Domain = config.Instance.Domain
 	instanceTls.ReadTimeout = 10 * time.Second
-	instanceTls.AuthDisabled = false
 	instanceTls.WriteTimeout = 10 * time.Second
 	instanceTls.MaxMessageBytes = 1024 * 1024
 	instanceTls.MaxRecipients = 50
@@ -134,7 +158,6 @@ func Start() {
 	instance.Addr = ":25"
 	instance.Domain = config.Instance.Domain
 	instance.ReadTimeout = 10 * time.Second
-	instance.AuthDisabled = false
 	instance.WriteTimeout = 10 * time.Second
 	instance.MaxMessageBytes = 1024 * 1024
 	instance.MaxRecipients = 50

+ 15 - 0
server/smtp_server/smtp_test/sendEmailTest.py

@@ -0,0 +1,15 @@
+from email.mime.text import MIMEText
+import smtplib
+msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')
+
+
+from_addr = "admin@domain.com"
+password = "admin"
+to_addr = "admin@domain.com"
+smtp_server = "127.0.0.1"
+
+server = smtplib.SMTP(smtp_server, 25)
+server.starttls()
+server.login(from_addr, password)
+server.sendmail(from_addr, [to_addr], msg.as_string())
+server.quit()