Kaynağa Gözat

Merge branch 'master' of github.com:Jinnrry/PMail

jinnrry 2 yıl önce
ebeveyn
işleme
a981222bae
93 değiştirilmiş dosya ile 2952 ekleme ve 2943 silme
  1. 44 0
      .github/workflows/release.yml
  2. 2 3
      Dockerfile
  3. 55 6
      README.md
  4. 48 4
      README_CN.md
  5. 4 4
      build.sh
  6. 0 2505
      fe/package-lock.json
  7. 97 0
      fe/src/components/GroupSettings.vue
  8. 6 0
      fe/src/components/HomeAside.vue
  9. 13 4
      fe/src/components/HomeHeader.vue
  10. 237 0
      fe/src/components/RuleSettings.vue
  11. 5 0
      fe/src/components/SecuritySettings.vue
  12. 56 0
      fe/src/i18n/i18n.js
  13. 150 16
      fe/src/views/ListView.vue
  14. 1 1
      fe/src/views/LoginView.vue
  15. 3 3
      fe/src/views/SetupView.vue
  16. 0 1
      fe/vite.config.js
  17. 9 3
      server/config/config.dev.json
  18. 8 2
      server/config/config.go
  19. 8 2
      server/config/config.json
  20. 22 0
      server/config/config_mysql.json
  21. 1 0
      server/config/dkim/README.md
  22. 16 0
      server/config/dkim/dkim.priv
  23. 1 0
      server/config/dkim/dkim.public
  24. 1 0
      server/config/ssl/README.md
  25. 15 0
      server/config/ssl/private.key
  26. 16 0
      server/config/ssl/public.crt
  27. 2 1
      server/config/tables/mysql/email.sql
  28. 7 0
      server/config/tables/mysql/group.sql
  29. 10 0
      server/config/tables/mysql/rule.sql
  30. 12 11
      server/config/tables/sqlite/email.sql
  31. 7 0
      server/config/tables/sqlite/group.sql
  32. 10 0
      server/config/tables/sqlite/rule.sql
  33. 3 3
      server/controllers/attachments.go
  34. 2 2
      server/controllers/base.go
  35. 40 0
      server/controllers/email/delete.go
  36. 2 2
      server/controllers/email/detail.go
  37. 16 14
      server/controllers/email/list.go
  38. 40 0
      server/controllers/email/move.go
  39. 43 0
      server/controllers/email/read.go
  40. 17 13
      server/controllers/email/send.go
  41. 68 8
      server/controllers/group.go
  42. 2 2
      server/controllers/login.go
  43. 2 2
      server/controllers/ping.go
  44. 89 0
      server/controllers/rule.go
  45. 3 3
      server/controllers/settings.go
  46. 2 2
      server/controllers/setup.go
  47. 1 1
      server/cron_server/ssl_update.go
  48. 24 15
      server/db/init.go
  49. 83 2
      server/dto/parsemail/email.go
  50. 54 0
      server/dto/rule.go
  51. 3 2
      server/dto/tag.go
  52. 5 3
      server/hooks/base.go
  53. 96 0
      server/hooks/telegram_push/telegram_push.go
  54. 21 0
      server/hooks/telegram_push/telegram_push_test.go
  55. 20 6
      server/hooks/wechat_push/wechat_push.go
  56. 49 8
      server/http_server/http_server.go
  57. 38 16
      server/http_server/https_server.go
  58. 6 0
      server/http_server/setup_server.go
  59. 24 22
      server/i18n/i18n.go
  60. 18 16
      server/main.go
  61. 2 1
      server/models/email.go
  62. 8 0
      server/models/group.go
  63. 35 0
      server/models/rule.go
  64. 3 3
      server/services/attachments/attachments.go
  65. 4 4
      server/services/auth/auth.go
  66. 33 0
      server/services/del_email/del_email.go
  67. 2 2
      server/services/detail/detail.go
  68. 115 0
      server/services/group/group.go
  69. 13 5
      server/services/list/list.go
  70. 51 0
      server/services/rule/match/base.go
  71. 24 0
      server/services/rule/match/contains_match.go
  72. 23 0
      server/services/rule/match/equal_match.go
  73. 32 0
      server/services/rule/match/regex_match.go
  74. 18 0
      server/services/rule/match/regex_match_test.go
  75. 81 0
      server/services/rule/rule.go
  76. 9 5
      server/services/setup/db.go
  77. 10 0
      server/services/setup/db_test.go
  78. 2 2
      server/services/setup/dns.go
  79. 2 2
      server/services/setup/finish.go
  80. 1 1
      server/session/init.go
  81. 41 8
      server/smtp_server/read_content.go
  82. 349 0
      server/smtp_server/read_content_test.go
  83. 0 160
      server/smtp_server/send.go
  84. 0 24
      server/smtp_server/send_test.go
  85. 0 0
      server/smtp_server/smtp.go
  86. 12 0
      server/utils/address/address.go
  87. 42 0
      server/utils/address/address_test.go
  88. 10 10
      server/utils/async/async.go
  89. 6 5
      server/utils/context/context.go
  90. 10 0
      server/utils/password/encode_test.go
  91. 260 0
      server/utils/send/send.go
  92. 51 0
      server/utils/send/send_test.go
  93. 66 3
      server/utils/smtp/smtp.go

+ 44 - 0
.github/workflows/release.yml

@@ -0,0 +1,44 @@
+name: Docker Image CI
+
+on:
+  push:
+    tags:
+      - "*"
+
+env:
+  REGISTRY: ghcr.io
+
+jobs:
+  build-and-push-image:
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      packages: write
+    steps:
+      - name: Get version
+        id: get_version
+        run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> ${GITHUB_ENV}
+
+      - uses: actions/checkout@v3
+
+      - name: set lower case repository name
+        run: |
+          echo "REPOSITORY_LC=${REPOSITORY,,}" >> ${GITHUB_ENV}
+        env:
+          REPOSITORY: '${{ github.repository }}'
+
+      - name: Log in to the Container registry
+        uses: docker/login-action@v2.1.0
+        with:
+          registry: ${{ env.REGISTRY }}
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Build and push Docker images
+        uses: docker/build-push-action@v4
+        with:
+          context: .
+          push: true
+          tags: |
+            ${{ env.REGISTRY }}/${{ env.REPOSITORY_LC }}:${{ env.VERSION }}
+            ${{ env.REGISTRY }}/${{ env.REPOSITORY_LC }}:latest

+ 2 - 3
Dockerfile

@@ -30,7 +30,6 @@ RUN apk add --no-cache tzdata \
 
 
 COPY --from=serverbuild /work/pmail .
-COPY server/config/dkim ./config/dkim/
-COPY server/config/config.json ./config/
 
-CMD /work/pmail
+
+CMD /work/pmail

+ 55 - 6
README.md

@@ -1,7 +1,6 @@
 # PMail
 
-> The current code is not stable, be sure to record the log! Lost letters or letters parsed wrong can find out the
-> original content of the mail from the log!
+> A server, a domain, a line of code, a minute, and you'll be able to build a domain mailbox of your own.
 
 ## [中文文档](./README_CN.md)
 
@@ -12,8 +11,8 @@ a single file and contains complete send/receive mail service and web-side mail
 domain name , a line of code , a minute of deployment time , you will be able to build a domain name mailbox of your
 own .
 
-Any project related Issue, PR is welcome.At present, the project UI design is ugly, UI interaction experience is poor,
-welcome all UI, designers, front-end guidance. Finally, also for this project to solicit a beautiful and lovely Logo!
+All kinds of PR are welcome, whether you are fixing bugs, adding features, or optimizing translations. Also, call for a
+beautiful and cute Logo for this project!
 
 <img src="./docs/en.gif" alt="Editor" width="800px">
 
@@ -27,6 +26,12 @@ welcome all UI, designers, front-end guidance. Finally, also for this project to
 
 * Implementing the ACME protocol, the program will automatically obtain and update Let's Encrypt certificates.
 
+> By default, a ssl certificate is generated for the web service, allowing pages to use the https protocol.
+> If you have your own gateway or don't need https, set `httpsEnabled` to `2` in the configuration file so that the web
+> service will not use https.
+(Note: Even if you don't need https, please make sure the path to the ssl certificate file is correct, although the web
+> service doesn't use the certificate anymore, the smtp protocol still needs the certificate)
+
 ## Disadvantages
 
 * At present, only the core function of sending and receiving emails has been completed. Basically, it can only be used
@@ -38,11 +43,17 @@ welcome all UI, designers, front-end guidance. Finally, also for this project to
 
 ## 1、Download
 
-[Click Here](https://github.com/Jinnrry/PMail/releases) Download a program file that matches you.
+* [Click Here](https://github.com/Jinnrry/PMail/releases) Download a program file that matches you.
+
+* Or use Docker `docker pull ghcr.io/jinnrry/pmail:latest`
 
 ## 2、Run
 
-`double-click to open` Or `execute command to run`
+`./pmail` 
+
+Or 
+
+`docker run -p 25:25 -p 80:80 -p 443:443 -p 465:465 -v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest`
 
 ## 3、Configuration
 
@@ -59,6 +70,40 @@ use [https://www.mail-tester.com/](https://www.mail-tester.com/) for checking.
 Open the `config/config.json` file in the run directory, edit a few configuration items at the beginning of `weChatPush`
 and restart the service.
 
+## 6、Telegram Message Push
+Create bot and get token from [BotFather](https://t.me/BotFather)
+Open the `config/config.json` file in the run directory, edit a few configuration items at the beginning of `tg`and restart the service.
+
+# Configuration file format description
+
+```json
+{
+  "logLevel": "info", //log output level
+  "domain": "domain.com", // Your domain
+  "webDomain": "mail.domain.com", // web domain
+  "dkimPrivateKeyPath": "config/dkim/dkim.priv", // dkim key path
+  "sslType": "0", // ssl certificate update mode, 0 automatic, 1 manual
+  "SSLPrivateKeyPath": "config/ssl/private.key", // ssl certificate path
+  "SSLPublicKeyPath": "config/ssl/public.crt", // ssl certificate path
+  "dbDSN": "./config/pmail.db", // database connect DSN
+  "dbType": "sqlite", //database type ,`sqlite` or `mysql`
+  "httpsEnabled": 0, // enabled https , 0:enabled 1:enablde 2:disenabled
+  "httpPort": 80, // http port . default 80
+  "httpsPort": 443, // https port . default 443
+  "spamFilterLevel": 0,// Spam filter level, 0: no filter, 1: filtering when `spf` and `dkim` don't pass, 2: filtering when `spf` don't pass
+  "weChatPushAppId": "", // wechat appid
+  "weChatPushSecret": "", // weChat  Secret
+  "weChatPushTemplateId": "", // weChat TemplateId
+  "weChatPushUserId": "", // weChat UserId
+  "tgChatId": "", // telegram chatid
+  "tgBotToken": "", // telegram  token
+  "isInit": true // If false, it will enter the bootstrap process.
+}
+```
+
+
+
+
 # For Developer
 
 ## Project Framework
@@ -71,6 +116,10 @@ The code is in `fe` folder.
 
 The code is in `server` folder.
 
+## Api Documentation
+
+[go to wiki](https://github.com/Jinnrry/PMail/wiki)
+
 ## Plugin Development
 
 Reference this file. `server/hooks/wechat_push/wechat_push.go`

+ 48 - 4
README_CN.md

@@ -1,10 +1,10 @@
 # PMail
 
-> Welcome PR! Welcome Issues! 目前代码并不稳定,一定记录好日志!丢信或者信件解析错误可以从日志中找出邮件原始内容!
+> 一台服务器、一个域名、一行代码、一分钟时间,你就能够搭建出一个自己的域名邮箱。
 
 PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱服务器。单文件运行,包含完整的收发邮件服务和Web端邮件管理功能。只需一台服务器、一个域名、一行代码、一分钟部署时间,你就能够搭建出一个自己的域名邮箱。
 
-目前项目UI设计比较丑陋、UI交互体验较差,欢迎各位UI、设计师、前端提出指导意见。最后,也为这个项目征集一个漂亮可爱的Logo!
+欢迎各类PR,无论你是修复bug、新增功能、修改翻译。另外,也为这个项目征集一个漂亮可爱的Logo!
 
 <img src="./docs/cn.gif" alt="Editor" width="800px">
 
@@ -32,6 +32,9 @@ PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱
 
 实现了ACME协议,程序将自动获取并更新Let’s Encrypt证书。
 
+默认情况下,会为web后台也生成ssl证书,让后台使用https访问,如果你有自己的网关层,不需要https的话,在配置文件中将`httpsEnabled`
+设置为`2`,这样管理后台就不会使用https协议。( 注意:即使你不需要https,也请保证ssl证书文件路径正确,http协议虽然不使用证书了,但是smtp协议还需要证书)
+
 ## 其他
 
 ### 不足
@@ -44,11 +47,17 @@ PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱
 
 ## 1、下载文件
 
-[点击这里](https://github.com/Jinnrry/PMail/releases)下载一个与你匹配的程序文件。
+* [点击这里](https://github.com/Jinnrry/PMail/releases)下载一个与你匹配的程序文件。
+
+* 或者使用Docker运行 `docker pull ghcr.io/jinnrry/pmail:latest`
 
 ## 2、运行
 
-双击打开 OR 执行命令运行
+`./pmail` 
+
+或者
+
+`docker run -p 25:25 -p 80:80 -p 443:443 -p 465:465 -v $(pwd)/config:/work/config ghcr.io/jinnrry/pmail:latest`
 
 ## 3、配置
 
@@ -62,6 +71,37 @@ PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱
 
 打开运行目录下的 `config/config.json`文件,编辑 `weChatPush` 开头的几个配置项,重启服务即可。
 
+## 6、Telegram推送
+从 [BotFather](https://t.me/BotFather) 创建并获取令牌机器人。 打开运行目录下的 config/config.json 文件,编辑 `tg` 开头的几个配置项,重启服务即可。
+
+
+# 配置文件说明
+
+```json
+{
+  "logLevel": "info", //日志输出级别
+  "domain": "domain.com", // 你的域名
+  "webDomain": "mail.domain.com", // web域名
+  "dkimPrivateKeyPath": "config/dkim/dkim.priv", // dkim 私钥地址
+  "sslType": "0", // ssl证书更新模式,0自动,1手动
+  "SSLPrivateKeyPath": "config/ssl/private.key", // ssl 证书地址
+  "SSLPublicKeyPath": "config/ssl/public.crt", // ssl 证书地址
+  "dbDSN": "./config/pmail.db", // 数据库连接DSN
+  "dbType": "sqlite", //数据库类型,支持sqlite 和 mysql
+  "httpsEnabled": 0, // web后台是否启用https 0默认(启用),1启用,2不启用
+  "spamFilterLevel": 0,// 垃圾邮件过滤级别,0不过滤、1 spf dkim 校验均失败时过滤,2 spf校验不通过时过滤
+  "httpPort": 80, // http 端口 . 默认 80
+  "httpsPort": 443, // https 端口 . 默认 443
+  "weChatPushAppId": "", // 微信推送appid
+  "weChatPushSecret": "", // 微信推送秘钥
+  "weChatPushTemplateId": "", // 微信推送模板id
+  "weChatPushUserId": "", // 微信推送用户id
+  "tgChatId": "", // telegram 推送chatid
+  "tgBotToken": "", // telegram 推送 token
+  "isInit": true // 为false的时候会进入安装引导流程 
+}
+```
+
 # 参与开发
 
 ## 项目架构
@@ -74,6 +114,10 @@ PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱
 
 后端代码进入`server`文件夹,运行`main.go`文件
 
+## 后端接口文档
+
+[参见Wiki](https://github.com/Jinnrry/PMail/wiki)
+
 ## 插件开发
 
 参考微信推送插件`server/hooks/wechat_push/wechat_push.go`

+ 4 - 4
build.sh

@@ -5,13 +5,13 @@ cd fe && yarn && yarn build
 cd ../server && cp -rf ../fe/dist http_server
 
 
-CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-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
+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
 
-CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-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_windows_amd64  main.go
+CGO_ENABLED=0 GOOS=windows 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_windows_amd64  main.go
 
-CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-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_mac_amd64  main.go
+CGO_ENABLED=0 GOOS=darwin 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_mac_amd64  main.go
 
-CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-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_mac_arm64  main.go
+CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 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_mac_arm64  main.go
 
 # 整理输出文件
 cd ..

+ 0 - 2505
fe/package-lock.json

@@ -1,2505 +0,0 @@
-{
-  "name": "fe",
-  "version": "0.0.0",
-  "lockfileVersion": 3,
-  "requires": true,
-  "packages": {
-    "": {
-      "name": "fe",
-      "version": "0.0.0",
-      "dependencies": {
-        "element-plus": "^2.3.6",
-        "pinia": "^2.0.36",
-        "vue": "^3.3.2",
-        "vue-router": "^4.2.0"
-      },
-      "devDependencies": {
-        "@vitejs/plugin-vue": "^4.2.3",
-        "eslint": "^8.39.0",
-        "eslint-plugin-vue": "^9.11.0",
-        "unplugin-auto-import": "^0.16.4",
-        "unplugin-vue-components": "^0.25.0",
-        "vite": "^4.3.5"
-      }
-    },
-    "node_modules/@antfu/utils": {
-      "version": "0.7.4",
-      "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.4.tgz",
-      "integrity": "sha512-qe8Nmh9rYI/HIspLSTwtbMFPj6dISG6+dJnOguTlPNXtCvS2uezdxscVBb7/3DrmNbQK49TDqpkSQ1chbRGdpQ==",
-      "dev": true
-    },
-    "node_modules/@babel/parser": {
-      "version": "7.22.4",
-      "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.22.4.tgz",
-      "integrity": "sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA==",
-      "license": "MIT",
-      "bin": {
-        "parser": "bin/babel-parser.js"
-      },
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
-    "node_modules/@ctrl/tinycolor": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz",
-      "integrity": "sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/@element-plus/icons-vue": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.1.0.tgz",
-      "integrity": "sha512-PSBn3elNoanENc1vnCfh+3WA9fimRC7n+fWkf3rE5jvv+aBohNHABC/KAR5KWPecxWxDTVT1ERpRbOMRcOV/vA==",
-      "license": "MIT",
-      "peerDependencies": {
-        "vue": "^3.2.0"
-      }
-    },
-    "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.17.19",
-      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
-      "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/@eslint-community/eslint-utils": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
-      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "eslint-visitor-keys": "^3.3.0"
-      },
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      },
-      "peerDependencies": {
-        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
-      }
-    },
-    "node_modules/@eslint-community/regexpp": {
-      "version": "4.5.1",
-      "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz",
-      "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
-      }
-    },
-    "node_modules/@eslint/eslintrc": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz",
-      "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "ajv": "^6.12.4",
-        "debug": "^4.3.2",
-        "espree": "^9.5.2",
-        "globals": "^13.19.0",
-        "ignore": "^5.2.0",
-        "import-fresh": "^3.2.1",
-        "js-yaml": "^4.1.0",
-        "minimatch": "^3.1.2",
-        "strip-json-comments": "^3.1.1"
-      },
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/@eslint/js": {
-      "version": "8.42.0",
-      "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.42.0.tgz",
-      "integrity": "sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      }
-    },
-    "node_modules/@floating-ui/core": {
-      "version": "1.2.6",
-      "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.2.6.tgz",
-      "integrity": "sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==",
-      "license": "MIT"
-    },
-    "node_modules/@floating-ui/dom": {
-      "version": "1.2.9",
-      "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.2.9.tgz",
-      "integrity": "sha512-sosQxsqgxMNkV3C+3UqTS6LxP7isRLwX8WMepp843Rb3/b0Wz8+MdUkxJksByip3C2WwLugLHN1b4ibn//zKwQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@floating-ui/core": "^1.2.6"
-      }
-    },
-    "node_modules/@humanwhocodes/config-array": {
-      "version": "0.11.10",
-      "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
-      "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
-      "dev": true,
-      "license": "Apache-2.0",
-      "dependencies": {
-        "@humanwhocodes/object-schema": "^1.2.1",
-        "debug": "^4.1.1",
-        "minimatch": "^3.0.5"
-      },
-      "engines": {
-        "node": ">=10.10.0"
-      }
-    },
-    "node_modules/@humanwhocodes/module-importer": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
-      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
-      "dev": true,
-      "license": "Apache-2.0",
-      "engines": {
-        "node": ">=12.22"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/nzakas"
-      }
-    },
-    "node_modules/@humanwhocodes/object-schema": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
-      "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
-      "dev": true,
-      "license": "BSD-3-Clause"
-    },
-    "node_modules/@jridgewell/sourcemap-codec": {
-      "version": "1.4.15",
-      "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
-      "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
-      "license": "MIT"
-    },
-    "node_modules/@nodelib/fs.scandir": {
-      "version": "2.1.5",
-      "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
-      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@nodelib/fs.stat": "2.0.5",
-        "run-parallel": "^1.1.9"
-      },
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/@nodelib/fs.stat": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
-      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/@nodelib/fs.walk": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
-      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@nodelib/fs.scandir": "2.1.5",
-        "fastq": "^1.6.0"
-      },
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/@popperjs/core": {
-      "name": "@sxzz/popperjs-es",
-      "version": "2.11.7",
-      "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
-      "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
-      "license": "MIT",
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/popperjs"
-      }
-    },
-    "node_modules/@rollup/pluginutils": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
-      "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
-      "dev": true,
-      "dependencies": {
-        "@types/estree": "^1.0.0",
-        "estree-walker": "^2.0.2",
-        "picomatch": "^2.3.1"
-      },
-      "engines": {
-        "node": ">=14.0.0"
-      },
-      "peerDependencies": {
-        "rollup": "^1.20.0||^2.0.0||^3.0.0"
-      },
-      "peerDependenciesMeta": {
-        "rollup": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/@types/estree": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.1.tgz",
-      "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
-      "dev": true
-    },
-    "node_modules/@types/lodash": {
-      "version": "4.14.195",
-      "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.195.tgz",
-      "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==",
-      "license": "MIT"
-    },
-    "node_modules/@types/lodash-es": {
-      "version": "4.17.7",
-      "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.7.tgz",
-      "integrity": "sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/lodash": "*"
-      }
-    },
-    "node_modules/@types/web-bluetooth": {
-      "version": "0.0.16",
-      "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
-      "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
-      "license": "MIT"
-    },
-    "node_modules/@vitejs/plugin-vue": {
-      "version": "4.2.3",
-      "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz",
-      "integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": "^14.18.0 || >=16.0.0"
-      },
-      "peerDependencies": {
-        "vite": "^4.0.0",
-        "vue": "^3.2.25"
-      }
-    },
-    "node_modules/@vue/compiler-core": {
-      "version": "3.3.4",
-      "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
-      "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==",
-      "license": "MIT",
-      "dependencies": {
-        "@babel/parser": "^7.21.3",
-        "@vue/shared": "3.3.4",
-        "estree-walker": "^2.0.2",
-        "source-map-js": "^1.0.2"
-      }
-    },
-    "node_modules/@vue/compiler-dom": {
-      "version": "3.3.4",
-      "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz",
-      "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==",
-      "license": "MIT",
-      "dependencies": {
-        "@vue/compiler-core": "3.3.4",
-        "@vue/shared": "3.3.4"
-      }
-    },
-    "node_modules/@vue/compiler-sfc": {
-      "version": "3.3.4",
-      "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz",
-      "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@babel/parser": "^7.20.15",
-        "@vue/compiler-core": "3.3.4",
-        "@vue/compiler-dom": "3.3.4",
-        "@vue/compiler-ssr": "3.3.4",
-        "@vue/reactivity-transform": "3.3.4",
-        "@vue/shared": "3.3.4",
-        "estree-walker": "^2.0.2",
-        "magic-string": "^0.30.0",
-        "postcss": "^8.1.10",
-        "source-map-js": "^1.0.2"
-      }
-    },
-    "node_modules/@vue/compiler-ssr": {
-      "version": "3.3.4",
-      "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
-      "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@vue/compiler-dom": "3.3.4",
-        "@vue/shared": "3.3.4"
-      }
-    },
-    "node_modules/@vue/devtools-api": {
-      "version": "6.5.0",
-      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz",
-      "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==",
-      "license": "MIT"
-    },
-    "node_modules/@vue/reactivity": {
-      "version": "3.3.4",
-      "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.4.tgz",
-      "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@vue/shared": "3.3.4"
-      }
-    },
-    "node_modules/@vue/reactivity-transform": {
-      "version": "3.3.4",
-      "resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz",
-      "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==",
-      "license": "MIT",
-      "dependencies": {
-        "@babel/parser": "^7.20.15",
-        "@vue/compiler-core": "3.3.4",
-        "@vue/shared": "3.3.4",
-        "estree-walker": "^2.0.2",
-        "magic-string": "^0.30.0"
-      }
-    },
-    "node_modules/@vue/runtime-core": {
-      "version": "3.3.4",
-      "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.4.tgz",
-      "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==",
-      "license": "MIT",
-      "dependencies": {
-        "@vue/reactivity": "3.3.4",
-        "@vue/shared": "3.3.4"
-      }
-    },
-    "node_modules/@vue/runtime-dom": {
-      "version": "3.3.4",
-      "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz",
-      "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@vue/runtime-core": "3.3.4",
-        "@vue/shared": "3.3.4",
-        "csstype": "^3.1.1"
-      }
-    },
-    "node_modules/@vue/server-renderer": {
-      "version": "3.3.4",
-      "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.4.tgz",
-      "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@vue/compiler-ssr": "3.3.4",
-        "@vue/shared": "3.3.4"
-      },
-      "peerDependencies": {
-        "vue": "3.3.4"
-      }
-    },
-    "node_modules/@vue/shared": {
-      "version": "3.3.4",
-      "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.4.tgz",
-      "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
-      "license": "MIT"
-    },
-    "node_modules/@vueuse/core": {
-      "version": "9.13.0",
-      "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz",
-      "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/web-bluetooth": "^0.0.16",
-        "@vueuse/metadata": "9.13.0",
-        "@vueuse/shared": "9.13.0",
-        "vue-demi": "*"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/antfu"
-      }
-    },
-    "node_modules/@vueuse/metadata": {
-      "version": "9.13.0",
-      "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz",
-      "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
-      "license": "MIT",
-      "funding": {
-        "url": "https://github.com/sponsors/antfu"
-      }
-    },
-    "node_modules/@vueuse/shared": {
-      "version": "9.13.0",
-      "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz",
-      "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
-      "license": "MIT",
-      "dependencies": {
-        "vue-demi": "*"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/antfu"
-      }
-    },
-    "node_modules/acorn": {
-      "version": "8.8.2",
-      "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.8.2.tgz",
-      "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
-      "dev": true,
-      "license": "MIT",
-      "bin": {
-        "acorn": "bin/acorn"
-      },
-      "engines": {
-        "node": ">=0.4.0"
-      }
-    },
-    "node_modules/acorn-jsx": {
-      "version": "5.3.2",
-      "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
-      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
-      "dev": true,
-      "license": "MIT",
-      "peerDependencies": {
-        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
-      }
-    },
-    "node_modules/ajv": {
-      "version": "6.12.6",
-      "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
-      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "fast-deep-equal": "^3.1.1",
-        "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.4.1",
-        "uri-js": "^4.2.2"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/epoberezkin"
-      }
-    },
-    "node_modules/ansi-regex": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
-      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/ansi-styles": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
-      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "color-convert": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-      }
-    },
-    "node_modules/anymatch": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
-      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
-      "dev": true,
-      "dependencies": {
-        "normalize-path": "^3.0.0",
-        "picomatch": "^2.0.4"
-      },
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/argparse": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
-      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-      "dev": true,
-      "license": "Python-2.0"
-    },
-    "node_modules/async-validator": {
-      "version": "4.2.5",
-      "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
-      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
-      "license": "MIT"
-    },
-    "node_modules/balanced-match": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
-      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/binary-extensions": {
-      "version": "2.2.0",
-      "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz",
-      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/boolbase": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz",
-      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
-      "dev": true,
-      "license": "ISC"
-    },
-    "node_modules/brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "balanced-match": "^1.0.0",
-        "concat-map": "0.0.1"
-      }
-    },
-    "node_modules/braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-      "dev": true,
-      "dependencies": {
-        "fill-range": "^7.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/callsites": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
-      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
-      }
-    },
-    "node_modules/chokidar": {
-      "version": "3.5.3",
-      "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz",
-      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
-      "dev": true,
-      "dependencies": {
-        "anymatch": "~3.1.2",
-        "braces": "~3.0.2",
-        "glob-parent": "~5.1.2",
-        "is-binary-path": "~2.1.0",
-        "is-glob": "~4.0.1",
-        "normalize-path": "~3.0.0",
-        "readdirp": "~3.6.0"
-      },
-      "engines": {
-        "node": ">= 8.10.0"
-      },
-      "optionalDependencies": {
-        "fsevents": "~2.3.2"
-      }
-    },
-    "node_modules/chokidar/node_modules/glob-parent": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
-      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-      "dev": true,
-      "dependencies": {
-        "is-glob": "^4.0.1"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/color-convert": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
-      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "color-name": "~1.1.4"
-      },
-      "engines": {
-        "node": ">=7.0.0"
-      }
-    },
-    "node_modules/color-name": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
-      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/concat-map": {
-      "version": "0.0.1",
-      "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
-      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/cross-spawn": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
-      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "path-key": "^3.1.0",
-        "shebang-command": "^2.0.0",
-        "which": "^2.0.1"
-      },
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/cssesc": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
-      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
-      "dev": true,
-      "license": "MIT",
-      "bin": {
-        "cssesc": "bin/cssesc"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/csstype": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz",
-      "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
-      "license": "MIT"
-    },
-    "node_modules/dayjs": {
-      "version": "1.11.8",
-      "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.8.tgz",
-      "integrity": "sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ==",
-      "license": "MIT"
-    },
-    "node_modules/debug": {
-      "version": "4.3.4",
-      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz",
-      "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "ms": "2.1.2"
-      },
-      "engines": {
-        "node": ">=6.0"
-      },
-      "peerDependenciesMeta": {
-        "supports-color": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/deep-is": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz",
-      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/doctrine": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz",
-      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
-      "dev": true,
-      "license": "Apache-2.0",
-      "dependencies": {
-        "esutils": "^2.0.2"
-      },
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
-    "node_modules/element-plus": {
-      "version": "2.3.6",
-      "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.3.6.tgz",
-      "integrity": "sha512-GLz0pXUYI2zRfIgyI6W7SWmHk6dSEikP9yR++hsQUyy63+WjutoiGpA3SZD4cGPSXUzRFeKfVr8CnYhK5LqXZw==",
-      "license": "MIT",
-      "dependencies": {
-        "@ctrl/tinycolor": "^3.4.1",
-        "@element-plus/icons-vue": "^2.0.6",
-        "@floating-ui/dom": "^1.0.1",
-        "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
-        "@types/lodash": "^4.14.182",
-        "@types/lodash-es": "^4.17.6",
-        "@vueuse/core": "^9.1.0",
-        "async-validator": "^4.2.5",
-        "dayjs": "^1.11.3",
-        "escape-html": "^1.0.3",
-        "lodash": "^4.17.21",
-        "lodash-es": "^4.17.21",
-        "lodash-unified": "^1.0.2",
-        "memoize-one": "^6.0.0",
-        "normalize-wheel-es": "^1.2.0"
-      },
-      "peerDependencies": {
-        "vue": "^3.2.0"
-      }
-    },
-    "node_modules/esbuild": {
-      "version": "0.17.19",
-      "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.17.19.tgz",
-      "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
-      "dev": true,
-      "hasInstallScript": true,
-      "license": "MIT",
-      "bin": {
-        "esbuild": "bin/esbuild"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "optionalDependencies": {
-        "@esbuild/android-arm": "0.17.19",
-        "@esbuild/android-arm64": "0.17.19",
-        "@esbuild/android-x64": "0.17.19",
-        "@esbuild/darwin-arm64": "0.17.19",
-        "@esbuild/darwin-x64": "0.17.19",
-        "@esbuild/freebsd-arm64": "0.17.19",
-        "@esbuild/freebsd-x64": "0.17.19",
-        "@esbuild/linux-arm": "0.17.19",
-        "@esbuild/linux-arm64": "0.17.19",
-        "@esbuild/linux-ia32": "0.17.19",
-        "@esbuild/linux-loong64": "0.17.19",
-        "@esbuild/linux-mips64el": "0.17.19",
-        "@esbuild/linux-ppc64": "0.17.19",
-        "@esbuild/linux-riscv64": "0.17.19",
-        "@esbuild/linux-s390x": "0.17.19",
-        "@esbuild/linux-x64": "0.17.19",
-        "@esbuild/netbsd-x64": "0.17.19",
-        "@esbuild/openbsd-x64": "0.17.19",
-        "@esbuild/sunos-x64": "0.17.19",
-        "@esbuild/win32-arm64": "0.17.19",
-        "@esbuild/win32-ia32": "0.17.19",
-        "@esbuild/win32-x64": "0.17.19"
-      }
-    },
-    "node_modules/escape-html": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
-      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
-      "license": "MIT"
-    },
-    "node_modules/escape-string-regexp": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
-      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/eslint": {
-      "version": "8.42.0",
-      "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.42.0.tgz",
-      "integrity": "sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@eslint-community/eslint-utils": "^4.2.0",
-        "@eslint-community/regexpp": "^4.4.0",
-        "@eslint/eslintrc": "^2.0.3",
-        "@eslint/js": "8.42.0",
-        "@humanwhocodes/config-array": "^0.11.10",
-        "@humanwhocodes/module-importer": "^1.0.1",
-        "@nodelib/fs.walk": "^1.2.8",
-        "ajv": "^6.10.0",
-        "chalk": "^4.0.0",
-        "cross-spawn": "^7.0.2",
-        "debug": "^4.3.2",
-        "doctrine": "^3.0.0",
-        "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^7.2.0",
-        "eslint-visitor-keys": "^3.4.1",
-        "espree": "^9.5.2",
-        "esquery": "^1.4.2",
-        "esutils": "^2.0.2",
-        "fast-deep-equal": "^3.1.3",
-        "file-entry-cache": "^6.0.1",
-        "find-up": "^5.0.0",
-        "glob-parent": "^6.0.2",
-        "globals": "^13.19.0",
-        "graphemer": "^1.4.0",
-        "ignore": "^5.2.0",
-        "import-fresh": "^3.0.0",
-        "imurmurhash": "^0.1.4",
-        "is-glob": "^4.0.0",
-        "is-path-inside": "^3.0.3",
-        "js-yaml": "^4.1.0",
-        "json-stable-stringify-without-jsonify": "^1.0.1",
-        "levn": "^0.4.1",
-        "lodash.merge": "^4.6.2",
-        "minimatch": "^3.1.2",
-        "natural-compare": "^1.4.0",
-        "optionator": "^0.9.1",
-        "strip-ansi": "^6.0.1",
-        "strip-json-comments": "^3.1.0",
-        "text-table": "^0.2.0"
-      },
-      "bin": {
-        "eslint": "bin/eslint.js"
-      },
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/eslint-plugin-vue": {
-      "version": "9.14.1",
-      "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.14.1.tgz",
-      "integrity": "sha512-LQazDB1qkNEKejLe/b5a9VfEbtbczcOaui5lQ4Qw0tbRBbQYREyxxOV5BQgNDTqGPs9pxqiEpbMi9ywuIaF7vw==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "@eslint-community/eslint-utils": "^4.3.0",
-        "natural-compare": "^1.4.0",
-        "nth-check": "^2.0.1",
-        "postcss-selector-parser": "^6.0.9",
-        "semver": "^7.3.5",
-        "vue-eslint-parser": "^9.3.0",
-        "xml-name-validator": "^4.0.0"
-      },
-      "engines": {
-        "node": "^14.17.0 || >=16.0.0"
-      },
-      "peerDependencies": {
-        "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0"
-      }
-    },
-    "node_modules/eslint-scope": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.0.tgz",
-      "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
-      "dev": true,
-      "license": "BSD-2-Clause",
-      "dependencies": {
-        "esrecurse": "^4.3.0",
-        "estraverse": "^5.2.0"
-      },
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/eslint-visitor-keys": {
-      "version": "3.4.1",
-      "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
-      "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
-      "dev": true,
-      "license": "Apache-2.0",
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/espree": {
-      "version": "9.5.2",
-      "resolved": "https://registry.npmmirror.com/espree/-/espree-9.5.2.tgz",
-      "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==",
-      "dev": true,
-      "license": "BSD-2-Clause",
-      "dependencies": {
-        "acorn": "^8.8.0",
-        "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^3.4.1"
-      },
-      "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/esquery": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.5.0.tgz",
-      "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
-      "dev": true,
-      "license": "BSD-3-Clause",
-      "dependencies": {
-        "estraverse": "^5.1.0"
-      },
-      "engines": {
-        "node": ">=0.10"
-      }
-    },
-    "node_modules/esrecurse": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz",
-      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
-      "dev": true,
-      "license": "BSD-2-Clause",
-      "dependencies": {
-        "estraverse": "^5.2.0"
-      },
-      "engines": {
-        "node": ">=4.0"
-      }
-    },
-    "node_modules/estraverse": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz",
-      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
-      "dev": true,
-      "license": "BSD-2-Clause",
-      "engines": {
-        "node": ">=4.0"
-      }
-    },
-    "node_modules/estree-walker": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
-      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
-      "license": "MIT"
-    },
-    "node_modules/esutils": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz",
-      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
-      "dev": true,
-      "license": "BSD-2-Clause",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/fast-deep-equal": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
-      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/fast-glob": {
-      "version": "3.2.12",
-      "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.2.12.tgz",
-      "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
-      "dev": true,
-      "dependencies": {
-        "@nodelib/fs.stat": "^2.0.2",
-        "@nodelib/fs.walk": "^1.2.3",
-        "glob-parent": "^5.1.2",
-        "merge2": "^1.3.0",
-        "micromatch": "^4.0.4"
-      },
-      "engines": {
-        "node": ">=8.6.0"
-      }
-    },
-    "node_modules/fast-glob/node_modules/glob-parent": {
-      "version": "5.1.2",
-      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
-      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-      "dev": true,
-      "dependencies": {
-        "is-glob": "^4.0.1"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/fast-json-stable-stringify": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
-      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/fast-levenshtein": {
-      "version": "2.0.6",
-      "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
-      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/fastq": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.15.0.tgz",
-      "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
-      "dev": true,
-      "license": "ISC",
-      "dependencies": {
-        "reusify": "^1.0.4"
-      }
-    },
-    "node_modules/file-entry-cache": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
-      "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "flat-cache": "^3.0.4"
-      },
-      "engines": {
-        "node": "^10.12.0 || >=12.0.0"
-      }
-    },
-    "node_modules/fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
-      "dev": true,
-      "dependencies": {
-        "to-regex-range": "^5.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/find-up": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz",
-      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "locate-path": "^6.0.0",
-        "path-exists": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/flat-cache": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.0.4.tgz",
-      "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "flatted": "^3.1.0",
-        "rimraf": "^3.0.2"
-      },
-      "engines": {
-        "node": "^10.12.0 || >=12.0.0"
-      }
-    },
-    "node_modules/flatted": {
-      "version": "3.2.7",
-      "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.2.7.tgz",
-      "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
-      "dev": true,
-      "license": "ISC"
-    },
-    "node_modules/fs.realpath": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
-      "dev": true,
-      "license": "ISC"
-    },
-    "node_modules/fsevents": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
-      "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
-      "dev": true,
-      "license": "MIT",
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
-      }
-    },
-    "node_modules/function-bind": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
-    },
-    "node_modules/glob": {
-      "version": "7.2.3",
-      "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
-      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
-      "dev": true,
-      "license": "ISC",
-      "dependencies": {
-        "fs.realpath": "^1.0.0",
-        "inflight": "^1.0.4",
-        "inherits": "2",
-        "minimatch": "^3.1.1",
-        "once": "^1.3.0",
-        "path-is-absolute": "^1.0.0"
-      },
-      "engines": {
-        "node": "*"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
-    "node_modules/glob-parent": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
-      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
-      "dev": true,
-      "license": "ISC",
-      "dependencies": {
-        "is-glob": "^4.0.3"
-      },
-      "engines": {
-        "node": ">=10.13.0"
-      }
-    },
-    "node_modules/globals": {
-      "version": "13.20.0",
-      "resolved": "https://registry.npmmirror.com/globals/-/globals-13.20.0.tgz",
-      "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "type-fest": "^0.20.2"
-      },
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/graphemer": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz",
-      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/has": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmmirror.com/has/-/has-1.0.3.tgz",
-      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "dev": true,
-      "dependencies": {
-        "function-bind": "^1.1.1"
-      },
-      "engines": {
-        "node": ">= 0.4.0"
-      }
-    },
-    "node_modules/has-flag": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
-      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/ignore": {
-      "version": "5.2.4",
-      "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.2.4.tgz",
-      "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">= 4"
-      }
-    },
-    "node_modules/import-fresh": {
-      "version": "3.3.0",
-      "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz",
-      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "parent-module": "^1.0.0",
-        "resolve-from": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/imurmurhash": {
-      "version": "0.1.4",
-      "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz",
-      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.8.19"
-      }
-    },
-    "node_modules/inflight": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
-      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
-      "dev": true,
-      "license": "ISC",
-      "dependencies": {
-        "once": "^1.3.0",
-        "wrappy": "1"
-      }
-    },
-    "node_modules/inherits": {
-      "version": "2.0.4",
-      "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
-      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
-      "dev": true,
-      "license": "ISC"
-    },
-    "node_modules/is-binary-path": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
-      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
-      "dev": true,
-      "dependencies": {
-        "binary-extensions": "^2.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/is-core-module": {
-      "version": "2.12.1",
-      "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.12.1.tgz",
-      "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
-      "dev": true,
-      "dependencies": {
-        "has": "^1.0.3"
-      }
-    },
-    "node_modules/is-extglob": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-glob": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
-      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "is-extglob": "^2.1.1"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/is-number": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
-      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.12.0"
-      }
-    },
-    "node_modules/is-path-inside": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz",
-      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/isexe": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
-      "dev": true,
-      "license": "ISC"
-    },
-    "node_modules/js-yaml": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz",
-      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "argparse": "^2.0.1"
-      },
-      "bin": {
-        "js-yaml": "bin/js-yaml.js"
-      }
-    },
-    "node_modules/json-schema-traverse": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/json-stable-stringify-without-jsonify": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
-      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/jsonc-parser": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
-      "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
-      "dev": true
-    },
-    "node_modules/levn": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz",
-      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "prelude-ls": "^1.2.1",
-        "type-check": "~0.4.0"
-      },
-      "engines": {
-        "node": ">= 0.8.0"
-      }
-    },
-    "node_modules/local-pkg": {
-      "version": "0.4.3",
-      "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.4.3.tgz",
-      "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==",
-      "dev": true,
-      "engines": {
-        "node": ">=14"
-      }
-    },
-    "node_modules/locate-path": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz",
-      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "p-locate": "^5.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/lodash": {
-      "version": "4.17.21",
-      "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
-      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
-      "license": "MIT"
-    },
-    "node_modules/lodash-es": {
-      "version": "4.17.21",
-      "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
-      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
-      "license": "MIT"
-    },
-    "node_modules/lodash-unified": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz",
-      "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
-      "license": "MIT",
-      "peerDependencies": {
-        "@types/lodash-es": "*",
-        "lodash": "*",
-        "lodash-es": "*"
-      }
-    },
-    "node_modules/lodash.merge": {
-      "version": "4.6.2",
-      "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
-      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/lru-cache": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz",
-      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-      "dev": true,
-      "license": "ISC",
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/magic-string": {
-      "version": "0.30.0",
-      "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.0.tgz",
-      "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@jridgewell/sourcemap-codec": "^1.4.13"
-      },
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/memoize-one": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
-      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
-      "license": "MIT"
-    },
-    "node_modules/merge2": {
-      "version": "1.4.1",
-      "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
-      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
-      "dev": true,
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/micromatch": {
-      "version": "4.0.5",
-      "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz",
-      "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
-      "dev": true,
-      "dependencies": {
-        "braces": "^3.0.2",
-        "picomatch": "^2.3.1"
-      },
-      "engines": {
-        "node": ">=8.6"
-      }
-    },
-    "node_modules/minimatch": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
-      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-      "dev": true,
-      "license": "ISC",
-      "dependencies": {
-        "brace-expansion": "^1.1.7"
-      },
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/mlly": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.3.0.tgz",
-      "integrity": "sha512-HT5mcgIQKkOrZecOjOX3DJorTikWXwsBfpcr/MGBkhfWcjiqvnaL/9ppxvIUXfjT6xt4DVIAsN9fMUz1ev4bIw==",
-      "dev": true,
-      "dependencies": {
-        "acorn": "^8.8.2",
-        "pathe": "^1.1.0",
-        "pkg-types": "^1.0.3",
-        "ufo": "^1.1.2"
-      }
-    },
-    "node_modules/ms": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/nanoid": {
-      "version": "3.3.6",
-      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz",
-      "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ],
-      "license": "MIT",
-      "bin": {
-        "nanoid": "bin/nanoid.cjs"
-      },
-      "engines": {
-        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
-      }
-    },
-    "node_modules/natural-compare": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz",
-      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/normalize-path": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
-      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/normalize-wheel-es": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
-      "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
-      "license": "BSD-3-Clause"
-    },
-    "node_modules/nth-check": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz",
-      "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
-      "dev": true,
-      "license": "BSD-2-Clause",
-      "dependencies": {
-        "boolbase": "^1.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/fb55/nth-check?sponsor=1"
-      }
-    },
-    "node_modules/once": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
-      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
-      "dev": true,
-      "license": "ISC",
-      "dependencies": {
-        "wrappy": "1"
-      }
-    },
-    "node_modules/optionator": {
-      "version": "0.9.1",
-      "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.1.tgz",
-      "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "deep-is": "^0.1.3",
-        "fast-levenshtein": "^2.0.6",
-        "levn": "^0.4.1",
-        "prelude-ls": "^1.2.1",
-        "type-check": "^0.4.0",
-        "word-wrap": "^1.2.3"
-      },
-      "engines": {
-        "node": ">= 0.8.0"
-      }
-    },
-    "node_modules/p-limit": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz",
-      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "yocto-queue": "^0.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/p-locate": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz",
-      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "p-limit": "^3.0.2"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/parent-module": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
-      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "callsites": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/path-exists": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
-      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/path-is-absolute": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/path-key": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
-      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/path-parse": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
-      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
-      "dev": true
-    },
-    "node_modules/pathe": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.1.tgz",
-      "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==",
-      "dev": true
-    },
-    "node_modules/picocolors": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz",
-      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
-      "license": "ISC"
-    },
-    "node_modules/picomatch": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
-      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-      "dev": true,
-      "engines": {
-        "node": ">=8.6"
-      }
-    },
-    "node_modules/pinia": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.1.3.tgz",
-      "integrity": "sha512-XNA/z/ye4P5rU1pieVmh0g/hSuDO98/a5UC8oSP0DNdvt6YtetJNHTrXwpwsQuflkGT34qKxAEcp7lSxXNjf/A==",
-      "license": "MIT",
-      "dependencies": {
-        "@vue/devtools-api": "^6.5.0",
-        "vue-demi": ">=0.14.5"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/posva"
-      },
-      "peerDependencies": {
-        "@vue/composition-api": "^1.4.0",
-        "typescript": ">=4.4.4",
-        "vue": "^2.6.14 || ^3.3.0"
-      },
-      "peerDependenciesMeta": {
-        "@vue/composition-api": {
-          "optional": true
-        },
-        "typescript": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/pkg-types": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.0.3.tgz",
-      "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==",
-      "dev": true,
-      "dependencies": {
-        "jsonc-parser": "^3.2.0",
-        "mlly": "^1.2.0",
-        "pathe": "^1.1.0"
-      }
-    },
-    "node_modules/postcss": {
-      "version": "8.4.24",
-      "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.24.tgz",
-      "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
-      "funding": [
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/postcss/"
-        },
-        {
-          "type": "tidelift",
-          "url": "https://tidelift.com/funding/github/npm/postcss"
-        },
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/ai"
-        }
-      ],
-      "license": "MIT",
-      "dependencies": {
-        "nanoid": "^3.3.6",
-        "picocolors": "^1.0.0",
-        "source-map-js": "^1.0.2"
-      },
-      "engines": {
-        "node": "^10 || ^12 || >=14"
-      }
-    },
-    "node_modules/postcss-selector-parser": {
-      "version": "6.0.13",
-      "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
-      "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "cssesc": "^3.0.0",
-        "util-deprecate": "^1.0.2"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/prelude-ls": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
-      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.8.0"
-      }
-    },
-    "node_modules/punycode": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.0.tgz",
-      "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=6"
-      }
-    },
-    "node_modules/queue-microtask": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
-      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "MIT"
-    },
-    "node_modules/readdirp": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
-      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
-      "dev": true,
-      "dependencies": {
-        "picomatch": "^2.2.1"
-      },
-      "engines": {
-        "node": ">=8.10.0"
-      }
-    },
-    "node_modules/resolve": {
-      "version": "1.22.2",
-      "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.2.tgz",
-      "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
-      "dev": true,
-      "dependencies": {
-        "is-core-module": "^2.11.0",
-        "path-parse": "^1.0.7",
-        "supports-preserve-symlinks-flag": "^1.0.0"
-      },
-      "bin": {
-        "resolve": "bin/resolve"
-      }
-    },
-    "node_modules/resolve-from": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz",
-      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=4"
-      }
-    },
-    "node_modules/reusify": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz",
-      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "iojs": ">=1.0.0",
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/rimraf": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz",
-      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
-      "dev": true,
-      "license": "ISC",
-      "dependencies": {
-        "glob": "^7.1.3"
-      },
-      "bin": {
-        "rimraf": "bin.js"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/isaacs"
-      }
-    },
-    "node_modules/rollup": {
-      "version": "3.23.1",
-      "resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.23.1.tgz",
-      "integrity": "sha512-ybRdFVHOoljGEFILHLd2g/qateqUdjE6YS41WXq4p3C/WwD3xtWxV4FYWETA1u9TeXQc5K8L8zHE5d/scOvrOQ==",
-      "dev": true,
-      "license": "MIT",
-      "bin": {
-        "rollup": "dist/bin/rollup"
-      },
-      "engines": {
-        "node": ">=14.18.0",
-        "npm": ">=8.0.0"
-      },
-      "optionalDependencies": {
-        "fsevents": "~2.3.2"
-      }
-    },
-    "node_modules/run-parallel": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
-      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/feross"
-        },
-        {
-          "type": "patreon",
-          "url": "https://www.patreon.com/feross"
-        },
-        {
-          "type": "consulting",
-          "url": "https://feross.org/support"
-        }
-      ],
-      "license": "MIT",
-      "dependencies": {
-        "queue-microtask": "^1.2.2"
-      }
-    },
-    "node_modules/scule": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmmirror.com/scule/-/scule-1.0.0.tgz",
-      "integrity": "sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==",
-      "dev": true
-    },
-    "node_modules/semver": {
-      "version": "7.5.1",
-      "resolved": "https://registry.npmmirror.com/semver/-/semver-7.5.1.tgz",
-      "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==",
-      "dev": true,
-      "license": "ISC",
-      "dependencies": {
-        "lru-cache": "^6.0.0"
-      },
-      "bin": {
-        "semver": "bin/semver.js"
-      },
-      "engines": {
-        "node": ">=10"
-      }
-    },
-    "node_modules/shebang-command": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
-      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "shebang-regex": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/shebang-regex": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
-      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/source-map-js": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz",
-      "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
-      "license": "BSD-3-Clause",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/strip-ansi": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
-      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "ansi-regex": "^5.0.1"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/strip-json-comments": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
-      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/strip-literal": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-1.0.1.tgz",
-      "integrity": "sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==",
-      "dev": true,
-      "dependencies": {
-        "acorn": "^8.8.2"
-      }
-    },
-    "node_modules/supports-color": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "has-flag": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/supports-preserve-symlinks-flag": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
-      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
-      "dev": true,
-      "engines": {
-        "node": ">= 0.4"
-      }
-    },
-    "node_modules/text-table": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
-      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/to-regex-range": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
-      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-      "dev": true,
-      "dependencies": {
-        "is-number": "^7.0.0"
-      },
-      "engines": {
-        "node": ">=8.0"
-      }
-    },
-    "node_modules/type-check": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
-      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "prelude-ls": "^1.2.1"
-      },
-      "engines": {
-        "node": ">= 0.8.0"
-      }
-    },
-    "node_modules/type-fest": {
-      "version": "0.20.2",
-      "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz",
-      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
-      "dev": true,
-      "license": "(MIT OR CC0-1.0)",
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
-    "node_modules/ufo": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.1.2.tgz",
-      "integrity": "sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==",
-      "dev": true
-    },
-    "node_modules/unimport": {
-      "version": "3.0.7",
-      "resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.0.7.tgz",
-      "integrity": "sha512-2dVQUxJEGcrSZ0U4qtwJVODrlfyGcwmIOoHVqbAFFUx7kPoEN5JWr1cZFhLwoAwTmZOvqAm3YIkzv1engIQocg==",
-      "dev": true,
-      "dependencies": {
-        "@rollup/pluginutils": "^5.0.2",
-        "escape-string-regexp": "^5.0.0",
-        "fast-glob": "^3.2.12",
-        "local-pkg": "^0.4.3",
-        "magic-string": "^0.30.0",
-        "mlly": "^1.2.1",
-        "pathe": "^1.1.0",
-        "pkg-types": "^1.0.3",
-        "scule": "^1.0.0",
-        "strip-literal": "^1.0.1",
-        "unplugin": "^1.3.1"
-      }
-    },
-    "node_modules/unimport/node_modules/escape-string-regexp": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
-      "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
-      "dev": true,
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/unplugin": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.3.1.tgz",
-      "integrity": "sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==",
-      "dev": true,
-      "dependencies": {
-        "acorn": "^8.8.2",
-        "chokidar": "^3.5.3",
-        "webpack-sources": "^3.2.3",
-        "webpack-virtual-modules": "^0.5.0"
-      }
-    },
-    "node_modules/unplugin-auto-import": {
-      "version": "0.16.4",
-      "resolved": "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-0.16.4.tgz",
-      "integrity": "sha512-xdgBa9NAS3JG8HjkAZHSbGSMlrjKpaWKXGUzaF6RzEtr980RCl1t0Zsu0skUInNYrEQfqaHc7aGWPv41DLTK/w==",
-      "dev": true,
-      "dependencies": {
-        "@antfu/utils": "^0.7.2",
-        "@rollup/pluginutils": "^5.0.2",
-        "local-pkg": "^0.4.3",
-        "magic-string": "^0.30.0",
-        "minimatch": "^9.0.1",
-        "unimport": "^3.0.7",
-        "unplugin": "^1.3.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@nuxt/kit": "^3.2.2",
-        "@vueuse/core": "*"
-      },
-      "peerDependenciesMeta": {
-        "@nuxt/kit": {
-          "optional": true
-        },
-        "@vueuse/core": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/unplugin-auto-import/node_modules/brace-expansion": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz",
-      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-      "dev": true,
-      "dependencies": {
-        "balanced-match": "^1.0.0"
-      }
-    },
-    "node_modules/unplugin-auto-import/node_modules/minimatch": {
-      "version": "9.0.1",
-      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.1.tgz",
-      "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
-      "dev": true,
-      "dependencies": {
-        "brace-expansion": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=16 || 14 >=14.17"
-      }
-    },
-    "node_modules/unplugin-vue-components": {
-      "version": "0.25.0",
-      "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-0.25.0.tgz",
-      "integrity": "sha512-HxrQ4GMSS1RwVww2av3a42cABo/v5AmTRN9iARv6e/xwkrfTyHhLh84kFwXxKkXK61vxDHxaryn694mQmkiVBg==",
-      "dev": true,
-      "dependencies": {
-        "@antfu/utils": "^0.7.3",
-        "@rollup/pluginutils": "^5.0.2",
-        "chokidar": "^3.5.3",
-        "debug": "^4.3.4",
-        "fast-glob": "^3.2.12",
-        "local-pkg": "^0.4.3",
-        "magic-string": "^0.30.0",
-        "minimatch": "^9.0.1",
-        "resolve": "^1.22.2",
-        "unplugin": "^1.3.1"
-      },
-      "engines": {
-        "node": ">=14"
-      },
-      "peerDependencies": {
-        "@babel/parser": "^7.15.8",
-        "@nuxt/kit": "^3.2.2",
-        "vue": "2 || 3"
-      },
-      "peerDependenciesMeta": {
-        "@babel/parser": {
-          "optional": true
-        },
-        "@nuxt/kit": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/unplugin-vue-components/node_modules/brace-expansion": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz",
-      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-      "dev": true,
-      "dependencies": {
-        "balanced-match": "^1.0.0"
-      }
-    },
-    "node_modules/unplugin-vue-components/node_modules/minimatch": {
-      "version": "9.0.1",
-      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.1.tgz",
-      "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
-      "dev": true,
-      "dependencies": {
-        "brace-expansion": "^2.0.1"
-      },
-      "engines": {
-        "node": ">=16 || 14 >=14.17"
-      }
-    },
-    "node_modules/uri-js": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",
-      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
-      "dev": true,
-      "license": "BSD-2-Clause",
-      "dependencies": {
-        "punycode": "^2.1.0"
-      }
-    },
-    "node_modules/util-deprecate": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
-      "dev": true,
-      "license": "MIT"
-    },
-    "node_modules/vite": {
-      "version": "4.3.9",
-      "resolved": "https://registry.npmmirror.com/vite/-/vite-4.3.9.tgz",
-      "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "esbuild": "^0.17.5",
-        "postcss": "^8.4.23",
-        "rollup": "^3.21.0"
-      },
-      "bin": {
-        "vite": "bin/vite.js"
-      },
-      "engines": {
-        "node": "^14.18.0 || >=16.0.0"
-      },
-      "optionalDependencies": {
-        "fsevents": "~2.3.2"
-      },
-      "peerDependencies": {
-        "@types/node": ">= 14",
-        "less": "*",
-        "sass": "*",
-        "stylus": "*",
-        "sugarss": "*",
-        "terser": "^5.4.0"
-      },
-      "peerDependenciesMeta": {
-        "@types/node": {
-          "optional": true
-        },
-        "less": {
-          "optional": true
-        },
-        "sass": {
-          "optional": true
-        },
-        "stylus": {
-          "optional": true
-        },
-        "sugarss": {
-          "optional": true
-        },
-        "terser": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/vue": {
-      "version": "3.3.4",
-      "resolved": "https://registry.npmmirror.com/vue/-/vue-3.3.4.tgz",
-      "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==",
-      "license": "MIT",
-      "dependencies": {
-        "@vue/compiler-dom": "3.3.4",
-        "@vue/compiler-sfc": "3.3.4",
-        "@vue/runtime-dom": "3.3.4",
-        "@vue/server-renderer": "3.3.4",
-        "@vue/shared": "3.3.4"
-      }
-    },
-    "node_modules/vue-demi": {
-      "version": "0.14.5",
-      "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.5.tgz",
-      "integrity": "sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==",
-      "hasInstallScript": true,
-      "license": "MIT",
-      "bin": {
-        "vue-demi-fix": "bin/vue-demi-fix.js",
-        "vue-demi-switch": "bin/vue-demi-switch.js"
-      },
-      "engines": {
-        "node": ">=12"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/antfu"
-      },
-      "peerDependencies": {
-        "@vue/composition-api": "^1.0.0-rc.1",
-        "vue": "^3.0.0-0 || ^2.6.0"
-      },
-      "peerDependenciesMeta": {
-        "@vue/composition-api": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/vue-eslint-parser": {
-      "version": "9.3.0",
-      "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.3.0.tgz",
-      "integrity": "sha512-48IxT9d0+wArT1+3wNIy0tascRoywqSUe2E1YalIC1L8jsUGe5aJQItWfRok7DVFGz3UYvzEI7n5wiTXsCMAcQ==",
-      "dev": true,
-      "license": "MIT",
-      "dependencies": {
-        "debug": "^4.3.4",
-        "eslint-scope": "^7.1.1",
-        "eslint-visitor-keys": "^3.3.0",
-        "espree": "^9.3.1",
-        "esquery": "^1.4.0",
-        "lodash": "^4.17.21",
-        "semver": "^7.3.6"
-      },
-      "engines": {
-        "node": "^14.17.0 || >=16.0.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/mysticatea"
-      },
-      "peerDependencies": {
-        "eslint": ">=6.0.0"
-      }
-    },
-    "node_modules/vue-router": {
-      "version": "4.2.2",
-      "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.2.2.tgz",
-      "integrity": "sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==",
-      "license": "MIT",
-      "dependencies": {
-        "@vue/devtools-api": "^6.5.0"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/posva"
-      },
-      "peerDependencies": {
-        "vue": "^3.2.0"
-      }
-    },
-    "node_modules/webpack-sources": {
-      "version": "3.2.3",
-      "resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz",
-      "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
-      "dev": true,
-      "engines": {
-        "node": ">=10.13.0"
-      }
-    },
-    "node_modules/webpack-virtual-modules": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
-      "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
-      "dev": true
-    },
-    "node_modules/which": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
-      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
-      "dev": true,
-      "license": "ISC",
-      "dependencies": {
-        "isexe": "^2.0.0"
-      },
-      "bin": {
-        "node-which": "bin/node-which"
-      },
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/word-wrap": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.3.tgz",
-      "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/wrappy": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
-      "dev": true,
-      "license": "ISC"
-    },
-    "node_modules/xml-name-validator": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
-      "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
-      "dev": true,
-      "license": "Apache-2.0",
-      "engines": {
-        "node": ">=12"
-      }
-    },
-    "node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "dev": true,
-      "license": "ISC"
-    },
-    "node_modules/yocto-queue": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
-      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    }
-  }
-}

+ 97 - 0
fe/src/components/GroupSettings.vue

@@ -0,0 +1,97 @@
+<template>
+    <div id="main">
+        <el-tree :expand-on-click-node="false" :data="data" :props="defaultProps" :defaultExpandAll="true" :class="node">
+            <template #default="{ node, data }">
+                <div>
+                    <span v-if="data.id != -1"> {{ data.label }}</span>
+                    <el-input v-if="data.id == -1" v-model="data.label" @blur="onInputBlur(data)"></el-input>
+                    <el-button v-if="data.id != 0" @click="del(node, data)" size="small" type="danger" circle> -
+                    </el-button>
+                    <el-button v-if="data.id != 0" @click="add(data)" size="small" type="primary" circle> + </el-button>
+                </div>
+            </template>
+        </el-tree>
+
+        <el-button @click="addRoot">{{ lang.add_group }}</el-button>
+    </div>
+</template>
+
+<script setup>
+import $http from "../http/http";
+import { reactive, ref } from 'vue'
+import lang from '../i18n/i18n';
+
+const data = reactive([])
+
+$http.get('/api/group').then(res => {
+    data.push(...res.data)
+})
+
+const del = function (node, data) {
+    if (data.id != -1) {
+        $http.post("/api/group/del", { "id": data.id }).then(res => {
+            if (res.errorNo != 0) {
+                ElMessage({
+                    message: res.errorMsg,
+                    type: 'error',
+                })
+            } else {
+                const pc = node.parent.childNodes
+                for (let i = 0; i < pc.length; i++) {
+                    if (pc[i].id == node.id) {
+                        pc.splice(i, 1)
+                        return
+                    }
+                }
+            }
+        })
+    } else {
+        const pc = node.parent.childNodes
+        for (let i = 0; i < pc.length; i++) {
+            if (pc[i].id == node.id) {
+                pc.splice(i, 1)
+                return
+            }
+        }
+    }
+}
+
+const add = function (item) {
+    if (item.children == null) {
+        item.children = []
+    }
+    item.children.push({
+        "children": [],
+        "label": "",
+        "id": "-1",
+        "parent_id": item.id
+    })
+}
+
+const addRoot = function () {
+    data.push({
+        "children": [],
+        "label": "",
+        "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 }).then(res => {
+            if (res.errorNo != 0) {
+                ElMessage({
+                    message: res.errorMsg,
+                    type: 'error',
+                })
+            } else {
+                $http.get('/api/group').then(res => {
+                    data.splice(0, data.length)
+                    data.push(...res.data)
+                })
+            }
+        })
+    }
+}
+</script>

+ 6 - 0
fe/src/components/HomeAside.vue

@@ -61,4 +61,10 @@ const handleNodeClick = function (data) {
 .el-tree {
   background-color: #F1F1F1;
 }
+
+.add_group{
+  font-size: 14px;
+  text-align: left;
+  padding-left: 15px;
+}
 </style>

+ 13 - 4
fe/src/components/HomeHeader.vue

@@ -8,10 +8,18 @@
                 <Setting style="color:#FFFFFF" />
             </el-icon>
         </div>
-        <el-drawer v-model="openSettings" :title="lang.settings">
-            <el-tabs tab-position="left" >
+        <el-drawer v-model="openSettings" size="80%" :title="lang.settings">
+            <el-tabs tab-position="left">
                 <el-tab-pane :label="lang.security">
-                    <SecuritySettings/>
+                    <SecuritySettings />
+                </el-tab-pane>
+
+                <el-tab-pane :label="lang.group_settings">
+                    <GroupSettings />
+                </el-tab-pane>
+
+                <el-tab-pane :label="lang.rule_setting">
+                    <RuleSettings />
                 </el-tab-pane>
             </el-tabs>
         </el-drawer>
@@ -25,7 +33,8 @@ import { ref } from 'vue'
 import { ElMessageBox } from 'element-plus'
 import SecuritySettings from '@/components/SecuritySettings.vue'
 import lang from '../i18n/i18n';
-
+import GroupSettings from './GroupSettings.vue';
+import RuleSettings from './RuleSettings.vue';
 
 const openSettings = ref(false)
 const settings = function () {

+ 237 - 0
fe/src/components/RuleSettings.vue

@@ -0,0 +1,237 @@
+<template>
+    <el-table :data="data" :show-header="true">
+        <el-table-column prop="id" label="id" />
+        <el-table-column prop="name" :label="lang.rule_name" />
+        <el-table-column prop="action" :label="lang.rule_do">
+            <template #default="scope">
+                {{ ActionName[scope.row.action] }}
+            </template>
+        </el-table-column>
+        <el-table-column prop="params" :label="lang.rule_params" />
+        <el-table-column prop="sort" :label="lang.rule_priority" />
+        <el-table-column>
+            <template #default="scope">
+                <div style="display: flex; align-items: center">
+                    <el-button size="small" type="primary" :icon="Edit" circle @click="editRule(scope.row)" />
+                    <el-popconfirm confirm-button-text="Yes" cancel-button-text="No, Thanks" :icon="InfoFilled"
+                        @confirm="delRule(scope.row.id)" icon-color="#626AEF" :title="lang.del_rule_confirm">
+                        <template #reference>
+                            <el-button size="small" type="danger" :icon="Delete" circle />
+                        </template>
+                    </el-popconfirm>
+                </div>
+            </template>
+        </el-table-column>
+    </el-table>
+
+    <div>
+        <el-button @click="dialogVisible = true">{{ lang.new_rule }}</el-button>
+    </div>
+
+
+
+
+    <el-dialog v-model="dialogVisible" :title="lang.new_rule" width="60%">
+        <div style="text-align: left; padding-left: 20px;">
+            <el-form v-model="addRuleForm" :inline="true" label-position="top">
+                <el-form-item style="width: 400px;" :label="lang.rule_name">
+                    <el-input v-model="addRuleForm.name" />
+                </el-form-item>
+
+                <el-form-item :label="lang.rule_priority">
+                    <el-input v-model="addRuleForm.sort" type="number" oninput="value=value.replace(/[^\-\d]/g, '')" />
+                </el-form-item>
+                <el-divider />
+                <div style="width: 100%;">{{ lang.rule_desc }}</div>
+                <div style="width: 100%;">
+                    <div v-for="(rule, index) in addRuleForm.rules">
+                        <el-select v-model="rule.field" placeholder="Select">
+                            <el-option key="From" :label="lang.from" value="From" />
+                            <el-option key="Subject" :label="lang.subject" value="Subject" />
+                            <el-option key="To" :label="lang.to" value="To" />
+                            <el-option key="Cc" :label="lang.cc" value="Cc" />
+                            <el-option key="Content" :label="lang.content" value="Content" />
+                        </el-select>
+
+                        <el-select v-model="rule.type" placeholder="Select">
+                            <el-option key="equal" :label="lang.equal" value="equal" />
+                            <el-option key="contains" :label="lang.contains" value="contains" />
+                            <el-option key="regex" :label="lang.regex" value="regex" />
+                        </el-select>
+
+                        <el-input v-model="rule.rule" style="width: 350px;" />
+                        <el-button size="small" type="danger" :icon="Delete" @click="removeRuleLine(index)" circle />
+                    </div>
+                </div>
+                <div style="padding-top: 7px;">
+                    <el-button size="small" type="primary" :icon="Plus" circle @click="addRule()" />
+                </div>
+                <el-divider />
+                <div style="width: 100%;">{{ lang.rule_do }}</div>
+                <el-form-item>
+                    <el-select v-model="addRuleForm.action" placeholder="Select" @change="ruleTypeChange()">
+                        <el-option key="mark_read" :label="lang.mark_read" :value="READ" />
+                        <el-option key="move" :label="lang.move" :value="MOVE" />
+                        <el-option key="delete" :label="lang.delete" :value="DELETE" />
+                        <el-option key="forward" :label="lang.forward" :value="FORWARD" />
+                    </el-select>
+                    <el-select v-if="addRuleForm.action == 4" v-model="addRuleForm.params" @click="reflushGroupInfos">
+                        <el-option v-for="gp in groupData.list" :key="gp.id" :label="gp.name" :value="gp.id" />
+                    </el-select>
+
+                    <el-input v-if="addRuleForm.action == 2" v-model="addRuleForm.params" style="width: 250px;"
+                        placeholder="Forward Email Address" />
+
+                </el-form-item>
+
+            </el-form>
+        </div>
+        <template #footer>
+            <span class="dialog-footer">
+                <el-button type="primary" @click="submitRule()">
+                    {{ lang.submit }}
+                </el-button>
+            </span>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import $http from "../http/http";
+import lang from '../i18n/i18n';
+import {
+    Plus,
+    Delete,
+    Edit,
+    InfoFilled
+} from '@element-plus/icons-vue'
+const data = ref([])
+const dialogVisible = ref(false)
+const READ = 1
+const FORWARD = 2
+const DELETE = 3
+const MOVE = 4
+
+const ActionName = {
+    1: lang.mark_read,
+    2: lang.forward,
+    3: lang.delete,
+    4: lang.move
+}
+
+const init = function () {
+    $http.post("/api/rule/get").then((res) => {
+        data.value = res.data
+    })
+}
+
+init()
+
+const groupData = reactive({
+    list: []
+})
+
+const reflushGroupInfos = function () {
+    $http.get('/api/group/list').then(res => {
+        groupData.list = res.data
+        for (let i = 0; i < groupData.list.length; i++) {
+            groupData.list[i].id += ""
+        }
+    })
+}
+
+reflushGroupInfos()
+
+const addRuleForm = reactive({
+    "id": 0,
+    "name": "",
+    "sort": 0,
+    "rules": [
+        {
+            "field": "",
+            "type": "",
+            "rule": ""
+        }
+    ],
+    "action": "",
+    "params": ""
+})
+
+const delRule = function (id) {
+    $http.post("/api/rule/del", { "id": id }).then((res) => {
+        ElNotification({
+            title: res.errorNo == 0 ? lang.succ : lang.fail,
+            message: res.data,
+            type: res.errorNo == 0 ? 'success' : 'error',
+        })
+
+        init()
+    })
+}
+
+const editRule = function (ruleInfo) {
+    addRuleForm.id = ruleInfo.id
+    addRuleForm.name = ruleInfo.name
+    addRuleForm.rules = ruleInfo.rules
+    addRuleForm.action = ruleInfo.action
+    addRuleForm.params = ruleInfo.params
+    addRuleForm.sort = ruleInfo.sort
+    dialogVisible.value = true
+}
+
+const removeRuleLine = function (index) {
+    addRuleForm.rules.splice(index, 1);
+}
+
+const addRule = function () {
+    addRuleForm.rules.push(
+        {
+            "field": "",
+            "type": "",
+            "rule": ""
+        }
+    )
+}
+
+const submitRule = function () {
+    let api = "/api/rule/add"
+    if (addRuleForm.id > 0) {
+        api = "/api/rule/update"
+    }
+
+    addRuleForm.sort = parseInt(addRuleForm.sort)
+
+    $http.post(api, addRuleForm).then((res) => {
+        if (res.errorNo != 0) {
+            ElNotification({
+                title: lang.fail,
+                message: res.data,
+                type: 'error',
+            })
+        } else {
+            init()
+            dialogVisible.value = false
+
+            addRuleForm.id = 0
+            addRuleForm.name = ""
+            addRuleForm.sort = 0
+            addRuleForm.rules = [
+                {
+                    "field": "",
+                    "type": "",
+                    "rule": ""
+                }
+            ]
+            addRuleForm.action = ""
+            addRuleForm.params = ""
+        }
+    })
+}
+
+
+
+const ruleTypeChange = function () {
+    addRuleForm.params = ''
+}
+</script>

+ 5 - 0
fe/src/components/SecuritySettings.vue

@@ -33,6 +33,11 @@ const rules = reactive({
 })
 
 const submit = function () {
+    if (ruleForm.new_pwd == ""){
+        return
+    }
+
+
     if (ruleForm.new_pwd != ruleForm.new_pwd2) {
         ElNotification({
             title: 'Error',

+ 56 - 0
fe/src/i18n/i18n.js

@@ -58,6 +58,34 @@ var lang = {
     "ssl_manuallyf": "Manually configure an SSL certificate",
     "ssl_key_path": "ssl key file path",
     "ssl_crt_path": "ssl crt file path",
+    "group_settings": "Group",
+    "add_group": "Add Group",
+    "del_email_confirm": "Are you sure you want to delete them?",
+    "move_email_confirm": "Are you sure you want to move them?",
+    "del_btn": "Delete",
+    "move_btn": "Move",
+    "read_btn": "Readed",
+    "dangerous":"The content of this email is not secure!",
+    "rule_setting":"Rules",
+    "new_rule":"New mail receiving rules",
+    "rule_name":"Rule Name",
+    "rule_priority":"Rule Priority(Larger values are executed first)",
+    "rule_desc":"When a new message arrives that meets all these conditions:",
+    "rule_do":"Do the following:",
+    "from":"From Email Address",
+    "subject":"Email Subject",
+    "to":"Recipient's address",
+    "cc":"Cc's address",
+    "content":"Email Content",
+    "equal":"Equal",
+    "regex":"Regex Match",
+    "contains":"Contains",
+    "mark_read":"Mark Read",
+    "delete":"Delete",
+    "forward":"Forward",
+    "move":"Move to group",
+    "del_rule_confirm":"Are you sure to delete this?",
+    "rule_params":"Executed params",
 };
 
 
@@ -122,6 +150,34 @@ var zhCN = {
     "ssl_manuallyf": "手动配置SSL证书",
     "ssl_key_path": "ssl key文件位置",
     "ssl_crt_path": "ssl crt文件位置",
+    "group_settings": "分组",
+    "add_group": "新建分组",
+    "del_email_confirm": "你确定要删除吗?",
+    "move_email_confirm": "你确定要移动这些邮件吗?",
+    "del_btn": "删除",
+    "move_btn": "移动",
+    "read_btn": "已读",
+    "dangerous":"该邮件内容不安全!",
+    "rule_setting":"规则",
+    "new_rule":"新建收信规则",
+    "rule_name":"规则名称",
+    "rule_priority":"优先级(值越大越先执行)",
+    "rule_desc":"以下规则全部满足时:",
+    "rule_do":"执行操作:",
+    "from":"发件人地址",
+    "subject":"邮件主题",
+    "to":"收件人地址",
+    "cc":"抄送地址",
+    "content":"邮件内容",
+    "equal":"等于",
+    "regex":"正则匹配",
+    "contains":"包含",
+    "mark_read":"标记已读",
+    "delete":"删除",
+    "forward":"转发",
+    "move":"移动分组",
+    "del_rule_confirm":"确定要删除吗?",
+    "rule_params":"执行参数",
 }
 
 switch (navigator.language) {

+ 150 - 16
fe/src/views/ListView.vue

@@ -4,11 +4,27 @@
             <div id="action">
                 <RouterLink to="/editer">+{{ lang.compose }}</RouterLink>
             </div>
-            <!-- <div id="action">全部标记为已读</div> -->
         </div>
         <div id="title">{{ groupStore.name }}</div>
+        <div id="action">
+            <el-button @click="del" size="small">{{ lang.del_btn }}</el-button>
+            <el-button @click="markRead" size="small">{{ lang.read_btn }}</el-button>
+            <el-dropdown style="margin-left: 12px;">
+                <el-button size="small">
+                    {{ lang.move_btn }}
+                    <el-icon class="el-icon--right"><arrow-down /></el-icon>
+                </el-button>
+                <template #dropdown>
+                    <el-dropdown-menu>
+                        <el-dropdown-item @click="move(group.id)" v-for="group in groupList">{{ group.name
+                        }}</el-dropdown-item>
+                    </el-dropdown-menu>
+                </template>
+            </el-dropdown>
+        </div>
         <div id="table">
-            <el-table :data="data" :show-header="true" :border="false" @row-click="rowClick" :row-style="rowStyle">
+            <el-table ref="taskTableDataRef" @selection-change="selectionLineChange" :data="data" :show-header="true"
+                :border="false" @row-click="rowClick" :row-style="rowStyle">
                 <el-table-column type="selection" width="30" />
                 <el-table-column prop="title" label="" width="50">
                     <template #default="scope">
@@ -16,6 +32,14 @@
                             <span v-if="!scope.row.is_read">
                                 {{ lang.new }}
                             </span>
+                            <span style="font-weight: 900;color: #FF0000;" v-if="scope.row.dangerous">
+                                <el-tooltip effect="dark" 
+                                :content="lang.dangerous"
+                                    placement="top-start">
+                                    !
+                                </el-tooltip>
+                                
+                            </span>
                         </div>
                     </template>
                 </el-table-column>
@@ -49,7 +73,7 @@
             </el-table>
         </div>
         <div id="pagination">
-            <el-pagination background layout="prev, pager, next" :page-count="totalPage" />
+            <el-pagination background layout="prev, pager, next" :page-count="totalPage" @current-change="pageChange" />
         </div>
     </div>
 </template>
@@ -58,19 +82,18 @@
 
 <script setup>
 import $http from "../http/http";
-
+import { ArrowDown } from '@element-plus/icons-vue'
 import { RouterLink } from 'vue-router'
 import { reactive, ref, watch } from 'vue'
-import { useRoute } from 'vue-router'
 import router from "@/router";  //根路由对象
 import useGroupStore from '../stores/group'
 import lang from '../i18n/i18n';
 
 const groupStore = useGroupStore()
 
-const route = useRoute()
-
+const groupList = ref([])
 
+const taskTableDataRef = ref(null)
 
 let tag = groupStore.tag;
 
@@ -96,29 +119,140 @@ watch(groupStore, async (newV, oldV) => {
 const data = ref([])
 const totalPage = ref(0)
 
-$http.post("/api/email/list", { tag: tag, page_size: 10 }).then(res => {
-    data.value = res.data.list
-    totalPage.value = res.data.total_page
-})
+const updateList = function () {
+    $http.post("/api/email/list", { tag: tag, page_size: 10 }).then(res => {
+        data.value = res.data.list
+        totalPage.value = res.data.total_page
+    })
+}
+
+const updateGroupList = function () {
+    $http.post("/api/group/list").then(res => {
+        groupList.value = res.data
+    })
+}
+
+updateList()
+updateGroupList()
 
 const rowClick = function (row, column, event) {
     router.push("/detail/" + row.id)
 }
 
+const markRead = function () {
+    let rows = taskTableDataRef.value?.getSelectionRows()
+    let ids = []
+    rows.forEach(element => {
+        ids.push(element.id)
+    });
+
+    $http.post("/api/email/read", { "ids": ids }).then(res => {
+        if (res.errorNo == 0) {
+            updateList()
+        } else {
+            ElMessage({
+                type: 'error',
+                message: res.errorMsg,
+            })
+        }
+    })
+}
+
+
+const move = function (group_id) {
+    let rows = taskTableDataRef.value?.getSelectionRows()
+    let ids = []
+    rows.forEach(element => {
+        ids.push(element.id)
+    });
+
+    ElMessageBox.confirm(
+        lang.move_email_confirm,
+        'Warning',
+        {
+            confirmButtonText: 'OK',
+            cancelButtonText: 'Cancel',
+            type: 'warning',
+        }
+    )
+        .then(() => {
+            $http.post("/api/email/move", { "group_id": group_id, "ids": ids }).then(res => {
+                if (res.errorNo == 0) {
+                    updateList()
+                    ElMessage({
+                        type: 'success',
+                        message: 'Move completed',
+                    })
+                } else {
+                    ElMessage({
+                        type: 'error',
+                        message: res.errorMsg,
+                    })
+                }
+            })
+
+
+
+        })
+}
+
+
+
+const del = function () {
+    let rows = taskTableDataRef.value?.getSelectionRows()
+    let ids = []
+    rows.forEach(element => {
+        ids.push(element.id)
+    });
+
+    ElMessageBox.confirm(
+        lang.del_email_confirm,
+        'Warning',
+        {
+            confirmButtonText: 'OK',
+            cancelButtonText: 'Cancel',
+            type: 'warning',
+        }
+    )
+        .then(() => {
+            $http.post("/api/email/del", { "ids": ids }).then(res => {
+                if (res.errorNo == 0) {
+                    updateList()
+                    ElMessage({
+                        type: 'success',
+                        message: 'Delete completed',
+                    })
+                } else {
+                    ElMessage({
+                        type: 'error',
+                        message: res.errorMsg,
+                    })
+                }
+            })
+
+
+
+        })
+}
+
+
 const rowStyle = function ({ row, rowIndwx }) {
     return { 'cursor': 'pointer' }
 }
 
+const pageChange = function (p) {
+    $http.post("/api/email/list", { tag: tag, page_size: 10, current_page: p }).then(res => {
+        data.value = res.data.list
+    })
+}
+
 </script>
 
 
 <style scoped>
 #action {
-    text-align: left;
-    font-size: 20px;
-    line-height: 40px;
-    padding-left: 10px;
-    margin-right: 5px;
+    display: flex;
+    flex-direction: row;
 }
 
 

+ 1 - 1
fe/src/views/LoginView.vue

@@ -1,7 +1,7 @@
 <template>
     <div id="main">
         <div id="form">
-            <el-form :model="form" label-width="120px">
+            <el-form :model="form" label-width="120px" @keyup.enter.native="onSubmit">
                 <el-form-item :label="lang.account">
                     <el-input v-model="form.account" placeholder="User Name" />
                 </el-form-item>

+ 3 - 3
fe/src/views/SetupView.vue

@@ -28,7 +28,7 @@
             <div class="form" style="width: 400px;">
                 <el-form label-width="120px">
                     <el-form-item :label="lang.type">
-                        <el-select :placeholder="lang.db_select_ph" v-model="dbSettings.type">
+                        <el-select :placeholder="lang.db_select_ph" v-model="dbSettings.type" @change="dbSettings.dsn=''">
                             <el-option label="MySQL" value="mysql" />
                             <el-option label="SQLite3" value="sqlite" />
                         </el-select>
@@ -40,7 +40,7 @@
                     </el-form-item>
 
                     <el-form-item :label="lang.sqlite_db_path" v-if="dbSettings.type == 'sqlite'">
-                        <el-input v-model="dbSettings.dsn" placeholder="./pmail.db"></el-input>
+                        <el-input v-model="dbSettings.dsn" placeholder="./config/pmail.db"></el-input>
                     </el-form-item>
                 </el-form>
             </div>
@@ -170,7 +170,7 @@ const adminSettings = reactive({
 
 const dbSettings = reactive({
     "type": "sqlite",
-    "dsn": "./pmail.db",
+    "dsn": "./config/pmail.db",
     "lable": ""
 })
 

+ 0 - 1
fe/vite.config.js

@@ -26,7 +26,6 @@ export default defineConfig({
     proxy: {
       "/api": "http://127.0.0.1/",
       "/attachments":"http://127.0.0.1/"
-
     }
   }
 })

+ 9 - 3
server/config/config.dev.json

@@ -1,16 +1,22 @@
 {
-  "logLevel": "debug",
+  "logLevel": "info",
   "domain": "domain.com",
   "webDomain": "mail.domain.com",
   "dkimPrivateKeyPath": "config/dkim/dkim.priv",
   "sslType": "0",
   "SSLPrivateKeyPath": "config/ssl/private.key",
   "SSLPublicKeyPath": "config/ssl/public.crt",
-  "dbDSN": "./pmail.db",
+  "dbDSN": "./config/pmail.db",
   "dbType": "sqlite",
+  "spamFilterLevel": 2,
+  "httpPort": 80,
+  "httpsPort": 443,
   "weChatPushAppId": "",
   "weChatPushSecret": "",
   "weChatPushTemplateId": "",
   "weChatPushUserId": "",
-  "isInit": false
+  "tgChatId": "",
+  "tgBotToken": "",
+  "isInit": true,
+  "httpsEnabled": 2
 }

+ 8 - 2
server/config/config.go

@@ -11,7 +11,7 @@ import (
 var IsInit bool
 
 type Config struct {
-	LogLevel             string            `json:"logLevel"`
+	LogLevel             string            `json:"logLevel"` // 日志级别
 	Domain               string            `json:"domain"`
 	WebDomain            string            `json:"webDomain"`
 	DkimPrivateKeyPath   string            `json:"dkimPrivateKeyPath"`
@@ -20,10 +20,16 @@ type Config struct {
 	SSLPublicKeyPath     string            `json:"SSLPublicKeyPath"`
 	DbDSN                string            `json:"dbDSN"`
 	DbType               string            `json:"dbType"`
+	HttpsEnabled         int               `json:"httpsEnabled"`    //后台页面是否启用https,0默认(启用),1启用,2不启用
+	SpamFilterLevel      int               `json:"spamFilterLevel"` //垃圾邮件过滤级别,0不过滤、1 spf dkim 校验均失败时过滤,2 spf校验不通过时过滤
+	HttpPort             int               `json:"httpPort"`        //http服务端口设置,默认80
+	HttpsPort            int               `json:"httpsPort"`       //https服务端口,默认443
 	WeChatPushAppId      string            `json:"weChatPushAppId"`
 	WeChatPushSecret     string            `json:"weChatPushSecret"`
 	WeChatPushTemplateId string            `json:"weChatPushTemplateId"`
 	WeChatPushUserId     string            `json:"weChatPushUserId"`
+	TgBotToken           string            `json:"tgBotToken"`
+	TgChatId             string            `json:"tgChatId"`
 	IsInit               bool              `json:"isInit"`
 	Tables               map[string]string `json:"-"`
 	TablesInitData       map[string]string `json:"-"`
@@ -32,7 +38,7 @@ type Config struct {
 //go:embed tables/*
 var tableConfig embed.FS
 
-const Version = "2.0.0"
+const Version = "2.2.0"
 
 const DBTypeMySQL = "mysql"
 const DBTypeSQLite = "sqlite"

+ 8 - 2
server/config/config.json

@@ -6,11 +6,17 @@
   "sslType": "0",
   "SSLPrivateKeyPath": "config/ssl/private.key",
   "SSLPublicKeyPath": "config/ssl/public.crt",
-  "dbDSN": "./pmail.db",
+  "dbDSN": "./config/pmail.db",
   "dbType": "sqlite",
+  "spamFilterLevel": 2,
+  "httpPort": 80,
+  "httpsPort": 443,
   "weChatPushAppId": "",
   "weChatPushSecret": "",
   "weChatPushTemplateId": "",
   "weChatPushUserId": "",
-  "isInit": false
+  "tgChatId": "",
+  "tgBotToken": "",
+  "isInit": true,
+  "httpsEnabled": 1
 }

+ 22 - 0
server/config/config_mysql.json

@@ -0,0 +1,22 @@
+{
+  "logLevel": "info",
+  "domain": "domain.com",
+  "webDomain": "mail.domain.com",
+  "dkimPrivateKeyPath": "config/dkim/dkim.priv",
+  "sslType": "0",
+  "SSLPrivateKeyPath": "config/ssl/private.key",
+  "SSLPublicKeyPath": "config/ssl/public.crt",
+  "dbDSN": "root:root@tcp(127.0.0.1:3306)/pmail?parseTime=True&loc=Local",
+  "dbType": "mysql",
+  "spamFilterLevel": 2,
+  "httpPort": 80,
+  "httpsPort": 443,
+  "weChatPushAppId": "",
+  "weChatPushSecret": "",
+  "weChatPushTemplateId": "",
+  "weChatPushUserId": "",
+  "tgChatId": "",
+  "tgBotToken": "",
+  "isInit": true,
+  "httpsEnabled": 2
+}

+ 1 - 0
server/config/dkim/README.md

@@ -0,0 +1 @@
+这里存储的秘钥仅开发测试使用,线上环境请重新生成!

+ 16 - 0
server/config/dkim/dkim.priv

@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANgOYlTcSbWqw54y
+ISRYq6keu/fPj/m7CiVaXtOc9bJhI5uBsd8Jz1eIRzNmqLITL2e5lz2vpQAmpk3z
+8Qd2iGCbZBwMXcfPVhAasGH367GRnJw3QPybA0Maob8CnfKDH6Pks3bX7xeef/U3
+ufHYnwgPZUi121ECvPjZLEmXpBrvAgMBAAECgYAxdeGG4cMyBnyvy3QQ2Qe7OKD5
+Uxf3qJzi/jQ1J3qLsncvU1p/38QKmtUJ7Fd0JLY2faMk6P/R8AckU1L7TWRcpafY
+fUU4xpuTHpBnMhglOGjEoOfSUFh9iieG8cVreozOOfihFRgRUxu6zfxycymHjGxF
+WbdK1zoNLgFgM0DWEQJBAOnkLf4P+pDMCeA/60pll39gIuW+AlMpWrjuA3Yhdu4B
+BvC5ea3fIVCWiksfBKihXDZkLG9PZDipy2WiNypPx0kCQQDsep7qHiBPpcp4a9OU
+KXolG771iHODId2Nc3zez2xG+2pY5BzoHD2TFdW1/5v9d4q+6u6dUb8v/t2GrMIQ
+wrh3AkEAiO0Dm+wA1YoN8hGZjqlhArnmVDdjpwnbyc3Viu/Wb0l8par/uDGbkFFB
+Tu8uzAYDNPh6JwQEeUO2Bp7rysJ/uQJBAJKq7rsX2kRr+Gq9vaksHHS9g693ZOVU
+8LuVgEIU9fwEXQ4q1P7k3Q/HwBe0JESNiwEkZsAt/l0/PpgTt/17N7sCQBQKcd7e
+++RuJCiMc0vMSYAKAmiARJHv/YoxS4tLngtrhu0h5uhr+35c0kJvy6nX0VBb5KV8
+hUK3axAHTSO793o=
+-----END PRIVATE KEY-----

+ 1 - 0
server/config/dkim/dkim.public

@@ -0,0 +1 @@
+v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDYDmJU3Em1qsOeMiEkWKupHrv3z4/5uwolWl7TnPWyYSObgbHfCc9XiEczZqiyEy9nuZc9r6UAJqZN8/EHdohgm2QcDF3Hz1YQGrBh9+uxkZycN0D8mwNDGqG/Ap3ygx+j5LN21+8Xnn/1N7nx2J8ID2VItdtRArz42SxJl6Qa7wIDAQAB

+ 1 - 0
server/config/ssl/README.md

@@ -0,0 +1 @@
+这里存储的秘钥仅开发测试使用,线上环境请重新生成!

+ 15 - 0
server/config/ssl/private.key

@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQDhh1fqAdYCSifPaCGfm2CjngcPaS7XehYdojBPPp5oEorWZFo1
+3Rghfh7qIXYp4nruUwfKgIPM95nYNqTnXolhUm5ywYDAhfJquuquJ7cAPhyx4SwG
+o58RgcJCM04yVKdxivskMiJvgUDz1ymdf6OA7MMbgGEcdky73TdC3FkfZQIDAQAB
+AoGAe5shPPj6oVChVxSccQzIv4QqHHEqoiCgpGczEQuh6CpZe72Oj7z4r8qfCPWD
+/NrLQ3mwaHVdR2ZhJFZ2tPRkWDI6hlK3IrC7A9LGoYCeV0AiDgMvzhQrWFjaw92u
+V9OS7tgYkewimcDaK1eF1hH+GOh2aToWMKAgCCOoXjuVhsECQQD4lc/4/Km7hrEQ
+JQGLOnB4qxdFk3HflShsXjnc/ydapQ5zCzCuCO7X89Jr8KpegC1SVmFR3YRjPNG4
+R36yLR8RAkEA6EF45wYmQfAi9w0AMlmnXanHcWXBauOrbqv2M9cjpsBvxcIjRvHp
+fca/LjEBf74X4YBhCCN1P7zRPLO2tYJjFQJAFdsGF/wO6D/lXWgDhLw0m0dfmmxm
+PKQek7iNGdMNILkWViMLuqFqbm4vd/IG6JwYX/7cO5hgRWFZhvwyNXQmIQJBAOPX
+Dqb79l3rGDHpVA8Qukn8+sV4gBS+wXcxRLY4UCYOU9fZikfXmymi5fuHYaQSNFUo
+XofgWO4s6co1toA7J70CQQDym7XIGA0GTtUtBpvRU2ew3d6JItMFjzQCjgKJ/4zm
+VqHIzyGoikq8jvbAqGJqRU72F/jfWEZlQO1KZemhmd/S
+-----END RSA PRIVATE KEY-----

+ 16 - 0
server/config/ssl/public.crt

@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE-----
+MIICcjCCAdsCFDLeR0jPO+9Q/nadNrHGI3EG2eHoMA0GCSqGSIb3DQEBCwUAMHgx
+CzAJBgNVBAYTAkNOMQswCQYDVQQIDAJCSjEQMA4GA1UEBwwHQmVpSmluZzENMAsG
+A1UECgwETnVsbDENMAsGA1UECwwETnVsbDEOMAwGA1UEAwwFUE1haWwxHDAaBgkq
+hkiG9w0BCQEWDWlAamlubnJyeS5jb20wHhcNMjMwODIzMTMyMTM0WhcNMzMwODIw
+MTMyMTM0WjB4MQswCQYDVQQGEwJDTjELMAkGA1UECAwCQkoxEDAOBgNVBAcMB0Jl
+aUppbmcxDTALBgNVBAoMBE51bGwxDTALBgNVBAsMBE51bGwxDjAMBgNVBAMMBVBN
+YWlsMRwwGgYJKoZIhvcNAQkBFg1pQGppbm5ycnkuY29tMIGfMA0GCSqGSIb3DQEB
+AQUAA4GNADCBiQKBgQDhh1fqAdYCSifPaCGfm2CjngcPaS7XehYdojBPPp5oEorW
+ZFo13Rghfh7qIXYp4nruUwfKgIPM95nYNqTnXolhUm5ywYDAhfJquuquJ7cAPhyx
+4SwGo58RgcJCM04yVKdxivskMiJvgUDz1ymdf6OA7MMbgGEcdky73TdC3FkfZQID
+AQABMA0GCSqGSIb3DQEBCwUAA4GBAMR6M83L2V9YFcYLxUv3Vaf7KrSSvuiGl/6H
+e2bMxboC8NBdsmRRhuKamti+NOe7i+BXTZ9TSy3zLQGK5LNvNOnWHHGj4vmVXoUV
+rFBMY1Vf2ZiEtO0OQjEcLOpzXhVWyZuDt2HhMRj92ESeXUSCMPZWT2UfZZTm0fhv
+ORq+I8O9
+-----END CERTIFICATE-----

+ 2 - 1
server/config/tables/mysql/email.sql

@@ -2,6 +2,7 @@ CREATE table email
 (
     id             INT unsigned AUTO_INCREMENT PRIMARY KEY COMMENT '自增id',
     type           tinyint(4) NOT NULL DEFAULT 0 COMMENT '邮件类型,0:收到的邮件,1:发送的邮件',
+    group_id       int unsigned NOT NULL DEFAULT 0 COMMENT '分组id',
     subject        varchar(1000) NOT NULL DEFAULT '' COMMENT '邮件标题',
     reply_to       json COMMENT '回复人',
     from_name      varchar(50)   NOT NULL DEFAULT '' COMMENT '发件人名称',
@@ -15,7 +16,7 @@ CREATE table email
     attachments    json COMMENT '附件内容',
     spf_check      tinyint(1) DEFAULT 0 COMMENT '0未校验,1校验通过,2校验未通过',
     dkim_check     tinyint(1) DEFAULT 0 COMMENT '0未校验,1校验通过,2校验未通过',
-    status         tinyint(4) NOT NULL DEFAULT 0 COMMENT '0未发送,1已发送,2发送失败',
+    status         tinyint(4) NOT NULL DEFAULT 0 COMMENT '0未发送,1已发送,2发送失败,3删除',
     send_user_id   int unsigned NOT NULL DEFAULT 0 COMMENT '发件人用户id',
     is_read        tinyint(1) NOT NULL DEFAULT 0 COMMENT '未读0,已读1',
     error          text COMMENT '错误信息记录',

+ 7 - 0
server/config/tables/mysql/group.sql

@@ -0,0 +1,7 @@
+CREATE TABLE `group`
+(
+    id        INT unsigned AUTO_INCREMENT PRIMARY KEY COMMENT '自增id',
+    name      varchar(10) NOT NULL DEFAULT '' COMMENT '分组名称',
+    user_id   INT unsigned NOT NULL DEFAULT 0 COMMENT '用户id',
+    parent_id INT unsigned COMMENT '父级组ID'
+)COMMENT='分组信息表'

+ 10 - 0
server/config/tables/mysql/rule.sql

@@ -0,0 +1,10 @@
+CREATE TABLE `rule`
+(
+    id      INT unsigned AUTO_INCREMENT PRIMARY KEY COMMENT '自增id',
+    user_id int          NOT NULL DEFAULT 0 COMMENT '用户id',
+    `name`  varchar(255) NOT NULL DEFAULT '' COMMENT '规则名称',
+    `value` json         NOT NULL COMMENT '规则内容',
+    action  int          not null default 0 comment '执行动作,1已读,2转发,3删除',
+    params  varchar(255) not null default '' comment '执行参数',
+    sort    int          not null default 0 COMMENT '排序,越大约优先'
+) COMMENT '收信规则表'

+ 12 - 11
server/config/tables/sqlite/email.sql

@@ -1,17 +1,18 @@
 CREATE table email
 (
     id             INTEGER PRIMARY KEY AUTOINCREMENT,
-    type           tinyint(4) NOT NULL DEFAULT 0 ,
-    subject        varchar(1000) NOT NULL DEFAULT '' ,
-    reply_to       json ,
-    from_name      varchar(50)   NOT NULL DEFAULT '' ,
-    from_address   varchar(150)  NOT NULL DEFAULT '' ,
-    `to`           json ,
-    bcc            json ,
-    cc             json ,
-    `text`         text ,
-    html           text ,
-    sender         json ,
+    type           tinyint(4) NOT NULL DEFAULT 0,
+    group_id       INTEGER       NOT NULL DEFAULT 0,
+    subject        varchar(1000) NOT NULL DEFAULT '',
+    reply_to       json,
+    from_name      varchar(50)   NOT NULL DEFAULT '',
+    from_address   varchar(150)  NOT NULL DEFAULT '',
+    `to`           json,
+    bcc            json,
+    cc             json,
+    `text`         text,
+    html           text,
+    sender         json,
     attachments    json ,
     spf_check      tinyint(1) DEFAULT 0 ,
     dkim_check     tinyint(1) DEFAULT 0 ,

+ 7 - 0
server/config/tables/sqlite/group.sql

@@ -0,0 +1,7 @@
+CREATE TABLE `group`
+(
+    id        INTEGER PRIMARY KEY AUTOINCREMENT,
+    name      varchar(10) NOT NULL DEFAULT '',
+    parent_id INTEGER     NOT NULL DEFAULT 0,
+    user_id   INTEGER     NOT NULL DEFAULT 0
+)

+ 10 - 0
server/config/tables/sqlite/rule.sql

@@ -0,0 +1,10 @@
+create table rule
+(
+    id      INTEGER PRIMARY KEY AUTOINCREMENT,
+    user_id int,
+    name    varchar(255) default '' not null,
+    value   json                    not null,
+    action  int          default 0  not null,
+    params  varchar(255) default '' not null,
+    sort    int          default 0  not null
+)

+ 3 - 3
server/controllers/attachments.go

@@ -4,13 +4,13 @@ import (
 	"fmt"
 	"github.com/spf13/cast"
 	"net/http"
-	"pmail/dto"
 	"pmail/dto/response"
 	"pmail/services/attachments"
+	"pmail/utils/context"
 	"strings"
 )
 
-func GetAttachments(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+func GetAttachments(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
 	urlInfos := strings.Split(req.RequestURI, "/")
 	if len(urlInfos) != 4 {
 		response.NewErrorResponse(response.ParamsError, "", "").FPrint(w)
@@ -29,7 +29,7 @@ func GetAttachments(ctx *dto.Context, w http.ResponseWriter, req *http.Request)
 	w.Write(content)
 }
 
-func Download(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+func Download(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
 	urlInfos := strings.Split(req.RequestURI, "/")
 	if len(urlInfos) != 5 {
 		response.NewErrorResponse(response.ParamsError, "", "").FPrint(w)

+ 2 - 2
server/controllers/base.go

@@ -2,7 +2,7 @@ package controllers
 
 import (
 	"net/http"
-	"pmail/dto"
+	"pmail/utils/context"
 )
 
-type HandlerFunc func(*dto.Context, http.ResponseWriter, *http.Request)
+type HandlerFunc func(*context.Context, http.ResponseWriter, *http.Request)

+ 40 - 0
server/controllers/email/delete.go

@@ -0,0 +1,40 @@
+package email
+
+import (
+	"encoding/json"
+	log "github.com/sirupsen/logrus"
+	"io"
+	"net/http"
+	"pmail/dto/response"
+	"pmail/services/del_email"
+	"pmail/utils/context"
+)
+
+type emailDeleteRequest struct {
+	IDs []int `json:"ids"`
+}
+
+func EmailDelete(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
+	reqBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+	}
+	var reqData emailDeleteRequest
+	err = json.Unmarshal(reqBytes, &reqData)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+	}
+
+	if len(reqData.IDs) <= 0 {
+		response.NewErrorResponse(response.ParamsError, "ID错误", "").FPrint(w)
+		return
+	}
+
+	err = del_email.DelEmail(ctx, reqData.IDs)
+	if err != nil {
+		response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+		return
+	}
+	response.NewSuccessResponse("success").FPrint(w)
+
+}

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

@@ -5,17 +5,17 @@ import (
 	log "github.com/sirupsen/logrus"
 	"io"
 	"net/http"
-	"pmail/dto"
 	"pmail/dto/response"
 	"pmail/services/auth"
 	"pmail/services/detail"
+	"pmail/utils/context"
 )
 
 type emailDetailRequest struct {
 	ID int `json:"id"`
 }
 
-func EmailDetail(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+func EmailDetail(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
 	reqBytes, err := io.ReadAll(req.Body)
 	if err != nil {
 		log.WithContext(ctx).Errorf("%+v", err)

+ 16 - 14
server/controllers/email/list.go

@@ -7,9 +7,9 @@ import (
 	"io"
 	"math"
 	"net/http"
-	"pmail/dto"
 	"pmail/dto/response"
 	"pmail/services/list"
+	"pmail/utils/context"
 )
 
 type emailListResponse struct {
@@ -19,12 +19,13 @@ type emailListResponse struct {
 }
 
 type emilItem struct {
-	ID       int    `json:"id"`
-	Title    string `json:"title"`
-	Desc     string `json:"desc"`
-	Datetime string `json:"datetime"`
-	IsRead   bool   `json:"is_read"`
-	Sender   User   `json:"sender"`
+	ID        int    `json:"id"`
+	Title     string `json:"title"`
+	Desc      string `json:"desc"`
+	Datetime  string `json:"datetime"`
+	IsRead    bool   `json:"is_read"`
+	Sender    User   `json:"sender"`
+	Dangerous bool   `json:"dangerous"`
 }
 
 type User struct {
@@ -39,7 +40,7 @@ type emailRequest struct {
 	PageSize    int    `json:"page_size"`
 }
 
-func EmailList(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+func EmailList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
 	var lst []*emilItem
 	reqBytes, err := io.ReadAll(req.Body)
 	if err != nil {
@@ -67,12 +68,13 @@ func EmailList(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 		_ = json.Unmarshal([]byte(email.Sender), &sender)
 
 		lst = append(lst, &emilItem{
-			ID:       email.Id,
-			Title:    email.Subject,
-			Desc:     email.Text.String,
-			Datetime: email.SendDate.Format("2006-01-02 15:04:05"),
-			IsRead:   email.IsRead == 1,
-			Sender:   sender,
+			ID:        email.Id,
+			Title:     email.Subject,
+			Desc:      email.Text.String,
+			Datetime:  email.SendDate.Format("2006-01-02 15:04:05"),
+			IsRead:    email.IsRead == 1,
+			Sender:    sender,
+			Dangerous: email.SPFCheck == 0 && email.DKIMCheck == 0,
 		})
 	}
 

+ 40 - 0
server/controllers/email/move.go

@@ -0,0 +1,40 @@
+package email
+
+import (
+	"encoding/json"
+	log "github.com/sirupsen/logrus"
+	"io"
+	"net/http"
+	"pmail/dto/response"
+	"pmail/services/group"
+	"pmail/utils/context"
+)
+
+type moveRequest struct {
+	GroupId int   `json:"group_id"`
+	IDs     []int `json:"ids"`
+}
+
+func Move(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
+	reqBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+	}
+	var reqData moveRequest
+	err = json.Unmarshal(reqBytes, &reqData)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+	}
+
+	if len(reqData.IDs) <= 0 {
+		response.NewErrorResponse(response.ParamsError, "ID错误", "").FPrint(w)
+		return
+	}
+
+	if !group.MoveMailToGroup(ctx, reqData.IDs, reqData.GroupId) {
+		response.NewErrorResponse(response.ServerError, "Error", "").FPrint(w)
+		return
+	}
+	response.NewSuccessResponse("success").FPrint(w)
+
+}

+ 43 - 0
server/controllers/email/read.go

@@ -0,0 +1,43 @@
+package email
+
+import (
+	"encoding/json"
+	log "github.com/sirupsen/logrus"
+	"io"
+	"net/http"
+	"pmail/dto/response"
+	"pmail/services/detail"
+	"pmail/utils/context"
+)
+
+type markReadRequest struct {
+	IDs []int `json:"ids"`
+}
+
+func MarkRead(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
+	reqBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+	}
+	var reqData markReadRequest
+	err = json.Unmarshal(reqBytes, &reqData)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+	}
+
+	if len(reqData.IDs) <= 0 {
+		response.NewErrorResponse(response.ParamsError, "ID错误", "").FPrint(w)
+		return
+	}
+
+	for _, id := range reqData.IDs {
+		detail.GetEmailDetail(ctx, id, true)
+	}
+
+	if err != nil {
+		response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+		return
+	}
+	response.NewSuccessResponse("success").FPrint(w)
+
+}

+ 17 - 13
server/controllers/email/send.go

@@ -8,13 +8,13 @@ import (
 	"net/http"
 	"pmail/config"
 	"pmail/db"
-	"pmail/dto"
 	"pmail/dto/parsemail"
 	"pmail/dto/response"
 	"pmail/hooks"
 	"pmail/i18n"
-	"pmail/smtp_server"
 	"pmail/utils/async"
+	"pmail/utils/context"
+	"pmail/utils/send"
 	"strings"
 	"time"
 )
@@ -43,7 +43,7 @@ type attachment struct {
 	Data string `json:"data"`
 }
 
-func Send(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+func Send(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
 	reqBytes, err := io.ReadAll(req.Body)
 	if err != nil {
 		log.WithContext(ctx).Errorf("%+v", err)
@@ -128,14 +128,16 @@ func Send(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 
 	}
 
+	as := async.New(ctx)
 	for _, hook := range hooks.HookList {
 		if hook == nil {
 			continue
 		}
-		async.New(ctx).Process(func() {
-			hook.SendBefore(ctx, e)
-		})
+		as.WaitProcess(func(hk any) {
+			hk.(hooks.EmailHook).SendBefore(ctx, e)
+		}, hook)
 	}
+	as.Wait()
 
 	// 邮件落库
 	sql := "INSERT INTO email (type,subject, reply_to, from_name, from_address, `to`, bcc, cc, text, html, sender, attachments,spf_check, dkim_check, create_time,send_user_id,error) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
@@ -155,7 +157,7 @@ func Send(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 		1,
 		1,
 		time.Now(),
-		ctx.UserInfo.ID,
+		ctx.UserID,
 		"",
 	)
 	emailId, _ := sqlRes.LastInsertId()
@@ -166,18 +168,20 @@ func Send(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 		return
 	}
 
-	async.New(ctx).Process(func() {
+	async.New(ctx).Process(func(p any) {
 		errMsg := ""
-		err, sendErr := smtp_server.Send(ctx, e)
+		err, sendErr := send.Send(ctx, e)
 
+		as2 := async.New(ctx)
 		for _, hook := range hooks.HookList {
 			if hook == nil {
 				continue
 			}
-			async.New(ctx).Process(func() {
-				hook.SendAfter(ctx, e, sendErr)
-			})
+			as2.WaitProcess(func(hk any) {
+				hk.(hooks.EmailHook).SendAfter(ctx, e, sendErr)
+			}, hook)
 		}
+		as2.Wait()
 
 		if err != nil {
 			errMsg = err.Error()
@@ -192,7 +196,7 @@ func Send(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 			}
 		}
 
-	})
+	}, nil)
 
 	response.NewSuccessResponse(i18n.GetText(ctx.Lang, "succ")).FPrint(w)
 }

+ 68 - 8
server/controllers/group.go

@@ -1,27 +1,33 @@
 package controllers
 
 import (
+	"encoding/json"
+	log "github.com/sirupsen/logrus"
+	"io"
 	"net/http"
+	"pmail/db"
 	"pmail/dto"
 	"pmail/dto/response"
 	"pmail/i18n"
+	"pmail/services/group"
+	"pmail/utils/array"
+	"pmail/utils/context"
 )
 
-type groupItem struct {
-	Label    string       `json:"label"`
-	Tag      string       `json:"tag"`
-	Children []*groupItem `json:"children"`
+func GetUserGroupList(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
+	infos := group.GetGroupList(ctx)
+	response.NewSuccessResponse(infos).FPrint(w)
 }
 
-func GetUserGroup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+func GetUserGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
 
-	retData := []*groupItem{
+	retData := []*group.GroupItem{
 		{
 			Label: i18n.GetText(ctx.Lang, "all_email"),
-			Children: []*groupItem{
+			Children: []*group.GroupItem{
 				{
 					Label: i18n.GetText(ctx.Lang, "inbox"),
-					Tag:   dto.SearchTag{Type: 0, Status: -1}.ToString(),
+					Tag:   dto.SearchTag{Type: 0, Status: -1, GroupId: 0}.ToString(),
 				},
 				{
 					Label: i18n.GetText(ctx.Lang, "outbox"),
@@ -35,5 +41,59 @@ func GetUserGroup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 		},
 	}
 
+	retData = array.Merge(retData, group.GetGroupInfoList(ctx))
+
 	response.NewSuccessResponse(retData).FPrint(w)
 }
+
+type addGroupRequest struct {
+	Name     string `json:"name"`
+	ParentId int    `json:"parent_id"`
+}
+
+func AddGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
+	var reqData *addGroupRequest
+	reqBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+	}
+	err = json.Unmarshal(reqBytes, &reqData)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+	}
+
+	res, err := db.Instance.Exec(db.WithContext(ctx, "insert into `group` (name,parent_id,user_id) values (?,?,?)"), reqData.Name, reqData.ParentId, ctx.UserID)
+	if err != nil {
+		response.NewErrorResponse(response.ServerError, "DBError", err.Error()).FPrint(w)
+		return
+	}
+	id, err := res.LastInsertId()
+	if err != nil {
+		response.NewErrorResponse(response.ServerError, "DBError", err.Error()).FPrint(w)
+		return
+	}
+	response.NewSuccessResponse(id).FPrint(w)
+}
+
+type delGroupRequest struct {
+	Id int `json:"id"`
+}
+
+func DelGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
+	var reqData *delGroupRequest
+	reqBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+	}
+	err = json.Unmarshal(reqBytes, &reqData)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+	}
+	succ, err := group.DelGroup(ctx, reqData.Id)
+
+	if err != nil {
+		response.NewErrorResponse(response.ServerError, "DBError", err.Error()).FPrint(w)
+		return
+	}
+	response.NewSuccessResponse(succ).FPrint(w)
+}

+ 2 - 2
server/controllers/login.go

@@ -7,11 +7,11 @@ import (
 	"io"
 	"net/http"
 	"pmail/db"
-	"pmail/dto"
 	"pmail/dto/response"
 	"pmail/i18n"
 	"pmail/models"
 	"pmail/session"
+	"pmail/utils/context"
 	"pmail/utils/password"
 )
 
@@ -20,7 +20,7 @@ type loginRequest struct {
 	Password string `json:"password"`
 }
 
-func Login(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+func Login(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
 
 	reqBytes, err := io.ReadAll(req.Body)
 	if err != nil {

+ 2 - 2
server/controllers/ping.go

@@ -3,11 +3,11 @@ package controllers
 import (
 	log "github.com/sirupsen/logrus"
 	"net/http"
-	"pmail/dto"
 	"pmail/dto/response"
+	"pmail/utils/context"
 )
 
-func Ping(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+func Ping(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
 	response.NewSuccessResponse("pong").FPrint(w)
 	log.WithContext(ctx).Info("pong")
 }

+ 89 - 0
server/controllers/rule.go

@@ -0,0 +1,89 @@
+package controllers
+
+import (
+	"encoding/json"
+	log "github.com/sirupsen/logrus"
+	"io"
+	"net/http"
+	"pmail/db"
+	"pmail/dto"
+	"pmail/dto/response"
+	"pmail/i18n"
+	"pmail/services/rule"
+	"pmail/utils/address"
+	"pmail/utils/array"
+	"pmail/utils/context"
+)
+
+func GetRule(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
+	res := rule.GetAllRules(ctx)
+	response.NewSuccessResponse(res).FPrint(w)
+}
+
+func UpsertRule(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
+
+	requestBody, err := io.ReadAll(req.Body)
+	if err != nil {
+		log.WithContext(ctx).Errorf("ReadError:%v", err)
+		return
+	}
+
+	var data *dto.Rule
+	err = json.Unmarshal(requestBody, &data)
+	if err != nil {
+		response.NewErrorResponse(response.ParamsError, "params error", err).FPrint(w)
+		return
+	}
+
+	if data.Action == dto.FORWARD && !address.IsValidEmailAddress(data.Params) {
+
+		response.NewErrorResponse(response.ParamsError, "ParamsError error", i18n.GetText(ctx.Lang, "invalid_email_address")).FPrint(w)
+		return
+	}
+
+	for _, r := range data.Rules {
+		if !array.InArray(r.Field, []string{"From", "Subject", "To", "Cc", "Text", "Html", "Content"}) {
+			response.NewErrorResponse(response.ParamsError, "ParamsError error", "params error! Rule Field Error!").FPrint(w)
+			return
+		}
+	}
+
+	err = data.Encode().Save(ctx)
+	if err != nil {
+		response.NewErrorResponse(response.ServerError, "server error", err).FPrint(w)
+		return
+	}
+	response.NewSuccessResponse("succ").FPrint(w)
+}
+
+type delRuleReq struct {
+	Id int `json:"id"`
+}
+
+func DelRule(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
+	requestBody, err := io.ReadAll(req.Body)
+	if err != nil {
+		log.WithContext(ctx).Errorf("ReadError:%v", err)
+		return
+	}
+
+	var data delRuleReq
+	err = json.Unmarshal(requestBody, &data)
+	if err != nil {
+		response.NewErrorResponse(response.ParamsError, "params error", err).FPrint(w)
+		return
+	}
+
+	if data.Id <= 0 {
+		response.NewErrorResponse(response.ParamsError, "params error", "id is empty").FPrint(w)
+		return
+	}
+
+	_, err = db.Instance.Exec(db.WithContext(ctx, "delete from rule where id =? and user_id =?"), data.Id, ctx.UserID)
+	if err != nil {
+		response.NewErrorResponse(response.ServerError, "unknown error", err).FPrint(w)
+		return
+	}
+
+	response.NewSuccessResponse("succ").FPrint(w)
+}

+ 3 - 3
server/controllers/settings.go

@@ -6,9 +6,9 @@ import (
 	"io"
 	"net/http"
 	"pmail/db"
-	"pmail/dto"
 	"pmail/dto/response"
 	"pmail/i18n"
+	"pmail/utils/context"
 	"pmail/utils/password"
 )
 
@@ -16,7 +16,7 @@ type modifyPasswordRequest struct {
 	Password string `json:"password"`
 }
 
-func ModifyPassword(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+func ModifyPassword(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
 	reqBytes, err := io.ReadAll(req.Body)
 	if err != nil {
 		log.Errorf("%+v", err)
@@ -30,7 +30,7 @@ func ModifyPassword(ctx *dto.Context, w http.ResponseWriter, req *http.Request)
 	if retData.Password != "" {
 		encodePwd := password.Encode(retData.Password)
 
-		_, err := db.Instance.Exec(db.WithContext(ctx, "update user set password = ? where id =?"), encodePwd, ctx.UserInfo.ID)
+		_, err := db.Instance.Exec(db.WithContext(ctx, "update user set password = ? where id =?"), encodePwd, ctx.UserID)
 		if err != nil {
 			response.NewErrorResponse(response.ServerError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
 			return

+ 2 - 2
server/controllers/setup.go

@@ -5,10 +5,10 @@ import (
 	"io"
 	"net/http"
 	"pmail/config"
-	"pmail/dto"
 	"pmail/dto/response"
 	"pmail/services/setup"
 	"pmail/services/setup/ssl"
+	"pmail/utils/context"
 	"strings"
 )
 
@@ -23,7 +23,7 @@ func AcmeChallenge(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func Setup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+func Setup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
 	reqBytes, err := io.ReadAll(req.Body)
 	if err != nil {
 		response.NewSuccessResponse("").FPrint(w)

+ 1 - 1
server/cron_server/ssl_update.go

@@ -10,7 +10,7 @@ import (
 
 func Start() {
 	for {
-		if config.Instance.IsInit {
+		if config.Instance != nil && config.Instance.IsInit {
 			days, err := ssl.CheckSSLCrtInfo()
 			if days < 30 || err != nil {
 				if err != nil {

+ 24 - 15
server/db/init.go

@@ -7,8 +7,9 @@ import (
 	log "github.com/sirupsen/logrus"
 	_ "modernc.org/sqlite"
 	"pmail/config"
-	"pmail/dto"
+	"pmail/utils/context"
 	"pmail/utils/errors"
+	"strings"
 )
 
 var Instance *sqlx.DB
@@ -32,12 +33,14 @@ func Init() error {
 	Instance.SetMaxIdleConns(10)
 	//showMySQLCharacterSet()
 	checkTable()
+	// 处理版本升级带来的数据表变更
+	databaseUpdate()
 	return nil
 }
 
-func WithContext(ctx *dto.Context, sql string) string {
+func WithContext(ctx *context.Context, sql string) string {
 	if ctx != nil {
-		logId := ctx.GetValue(dto.LogID)
+		logId := ctx.GetValue(context.LogID)
 		return fmt.Sprintf("/* %s */ %s", logId, sql)
 	}
 	return sql
@@ -84,21 +87,27 @@ func checkTable() {
 	}
 }
 
-func showMySQLCharacterSet() {
-	var res []struct {
-		Variable_name string `db:"Variable_name"`
-		Value         string `db:"Value"`
+type tableSQL struct {
+	Table       string `db:"Table"`
+	CreateTable string `db:"Create Table"`
+}
+
+func databaseUpdate() {
+	// 检查email表是否有group id
+	var err error
+	var res []tableSQL
+	if config.Instance.DbType == "sqlite" {
+		err = Instance.Select(&res, "select sql as `Create Table` from sqlite_master where type='table' and tbl_name = 'email'")
+	} else {
+		err = Instance.Select(&res, "show create table `email`")
 	}
-	err := Instance.Select(&res, "show variables like '%character%';")
-	log.Debugf("%+v  %+v", res, err)
 
-}
+	if err != nil {
+		panic(err)
+	}
 
-func testSlowLog() {
-	var res []struct {
-		Value string `db:"Value"`
+	if len(res) > 0 && !strings.Contains(res[0].CreateTable, "group_id") {
+		Instance.Exec("alter table email add group_id integer default 0 not null;")
 	}
-	err := Instance.Select(&res, "/* asddddasad */select /* this is test */ sleep(4) as Value")
-	log.Debugf("%+v  %+v", res, err)
 
 }

+ 83 - 2
server/dto/parsemail/email.go

@@ -8,8 +8,8 @@ import (
 	log "github.com/sirupsen/logrus"
 	"io"
 	"net/textproto"
-	"pmail/dto"
 	"pmail/utils/array"
+	"pmail/utils/context"
 	"regexp"
 	"strings"
 	"time"
@@ -42,6 +42,9 @@ type Email struct {
 	Attachments []*Attachment
 	ReadReceipt []string
 	Date        string
+	IsRead      int
+	Status      int // 0未发送,1已发送,2发送失败,3删除
+	GroupId     int // 分组id
 }
 
 func NewEmailFromReader(r io.Reader) *Email {
@@ -166,7 +169,85 @@ func buildUsers(str []string) []*User {
 	return ret
 }
 
-func (e *Email) BuildBytes(ctx *dto.Context) []byte {
+func (e *Email) ForwardBuildBytes(ctx *context.Context, forwardAddress string) []byte {
+	var b bytes.Buffer
+
+	from := []*mail.Address{{e.From.Name, e.From.EmailAddress}}
+	to := []*mail.Address{
+		{
+			Address: forwardAddress,
+		},
+	}
+
+	// Create our mail header
+	var h mail.Header
+	h.SetDate(time.Now())
+	h.SetAddressList("From", from)
+	h.SetAddressList("To", to)
+	h.SetText("Subject", e.Subject)
+	if len(e.Cc) != 0 {
+		cc := []*mail.Address{}
+		for _, user := range e.Cc {
+			cc = append(cc, &mail.Address{
+				Name:    user.Name,
+				Address: user.EmailAddress,
+			})
+		}
+		h.SetAddressList("Cc", cc)
+	}
+
+	// Create a new mail writer
+	mw, err := mail.CreateWriter(&b, h)
+	if err != nil {
+		log.WithContext(ctx).Fatal(err)
+	}
+
+	// Create a text part
+	tw, err := mw.CreateInline()
+	if err != nil {
+		log.WithContext(ctx).Fatal(err)
+	}
+	var th mail.InlineHeader
+	th.Set("Content-Type", "text/plain")
+	w, err := tw.CreatePart(th)
+	if err != nil {
+		log.Fatal(err)
+	}
+	io.WriteString(w, string(e.Text))
+	w.Close()
+
+	var html mail.InlineHeader
+	html.Set("Content-Type", "text/html")
+	w, err = tw.CreatePart(html)
+	if err != nil {
+		log.Fatal(err)
+	}
+	io.WriteString(w, string(e.HTML))
+	w.Close()
+
+	tw.Close()
+
+	// Create an attachment
+	for _, attachment := range e.Attachments {
+		var ah mail.AttachmentHeader
+		ah.Set("Content-Type", attachment.ContentType)
+		ah.SetFilename(attachment.Filename)
+		w, err = mw.CreateAttachment(ah)
+		if err != nil {
+			log.WithContext(ctx).Fatal(err)
+			continue
+		}
+		w.Write(attachment.Content)
+		w.Close()
+	}
+
+	mw.Close()
+
+	// dkim 签名后返回
+	return instance.Sign(b.String())
+}
+
+func (e *Email) BuildBytes(ctx *context.Context) []byte {
 	var b bytes.Buffer
 
 	from := []*mail.Address{{e.From.Name, e.From.EmailAddress}}

+ 54 - 0
server/dto/rule.go

@@ -0,0 +1,54 @@
+package dto
+
+import (
+	"encoding/json"
+	"pmail/models"
+)
+
+type RuleType int
+
+// 1已读,2转发,3删除
+var (
+	READ    RuleType = 1
+	FORWARD RuleType = 2
+	DELETE  RuleType = 3
+	MOVE    RuleType = 4
+)
+
+type Rule struct {
+	Id     int      `json:"id"`
+	Name   string   `json:"name"`
+	Rules  []*Value `json:"rules"`
+	Action RuleType `json:"action"`
+	Params string   `json:"params"`
+	Sort   int      `json:"sort"`
+}
+
+type Value struct {
+	Field string `json:"field"`
+	Type  string `json:"type"`
+	Rule  string `json:"rule"`
+}
+
+func (p *Rule) Decode(data *models.Rule) *Rule {
+	json.Unmarshal([]byte(data.Value), &p.Rules)
+	p.Id = data.Id
+	p.Name = data.Name
+	p.Action = RuleType(data.Action)
+	p.Sort = data.Sort
+	p.Params = data.Params
+	return p
+}
+
+func (p *Rule) Encode() *models.Rule {
+	v, _ := json.Marshal(p.Rules)
+	ret := &models.Rule{
+		Id:     p.Id,
+		Name:   p.Name,
+		Value:  string(v),
+		Action: int(p.Action),
+		Sort:   p.Sort,
+		Params: p.Params,
+	}
+	return ret
+}

+ 3 - 2
server/dto/tag.go

@@ -3,8 +3,9 @@ package dto
 import "encoding/json"
 
 type SearchTag struct {
-	Type   int `json:"type"`
-	Status int `json:"status"`
+	Type    int `json:"type"`     // -1 不限
+	Status  int `json:"status"`   // -1 不限
+	GroupId int `json:"group_id"` // -1 不限
 }
 
 func (t SearchTag) ToString() string {

+ 5 - 3
server/hooks/base.go

@@ -1,16 +1,17 @@
 package hooks
 
 import (
-	"pmail/dto"
 	"pmail/dto/parsemail"
+	"pmail/hooks/telegram_push"
 	"pmail/hooks/wechat_push"
+	"pmail/utils/context"
 )
 
 type EmailHook interface {
 	// SendBefore 邮件发送前的数据
-	SendBefore(ctx *dto.Context, email *parsemail.Email)
+	SendBefore(ctx *context.Context, email *parsemail.Email)
 	// SendAfter 邮件发送后的数据,err是每个收信服务器的错误信息
-	SendAfter(ctx *dto.Context, email *parsemail.Email, err map[string]error)
+	SendAfter(ctx *context.Context, email *parsemail.Email, err map[string]error)
 	// ReceiveParseBefore 接收到邮件,解析之前的原始数据
 	ReceiveParseBefore(email []byte)
 	// ReceiveParseAfter 接收到邮件,解析之后的结构化数据
@@ -24,5 +25,6 @@ var HookList []EmailHook
 func Init() {
 	HookList = []EmailHook{
 		wechat_push.NewWechatPushHook(),
+		telegram_push.NewTelegramPushHook(),
 	}
 }

+ 96 - 0
server/hooks/telegram_push/telegram_push.go

@@ -0,0 +1,96 @@
+package telegram_push
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"pmail/config"
+	"pmail/dto/parsemail"
+	"pmail/utils/context"
+	"strings"
+
+	log "github.com/sirupsen/logrus"
+)
+
+type TelegramPushHook struct {
+	chatId       string
+	botToken     string
+	httpsEnabled int
+	webDomain    string
+}
+
+func (w *TelegramPushHook) SendBefore(ctx *context.Context, email *parsemail.Email) {
+
+}
+
+func (w *TelegramPushHook) SendAfter(ctx *context.Context, email *parsemail.Email, err map[string]error) {
+
+}
+
+func (w *TelegramPushHook) ReceiveParseBefore(email []byte) {
+
+}
+
+func (w *TelegramPushHook) ReceiveParseAfter(email *parsemail.Email) {
+	if w.chatId == "" || w.botToken == "" {
+		return
+	}
+
+	w.sendUserMsg(nil, email)
+}
+
+type SendMessageRequest struct {
+	ChatID      string      `json:"chat_id"`
+	Text        string      `json:"text"`
+	ReplyMarkup ReplyMarkup `json:"reply_markup"`
+	ParseMode   string      `json:"parse_mode"`
+}
+
+type ReplyMarkup struct {
+	InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"`
+}
+
+type InlineKeyboardButton struct {
+	Text string `json:"text"`
+	URL  string `json:"url"`
+}
+
+func (w *TelegramPushHook) sendUserMsg(ctx *context.Context, email *parsemail.Email) {
+	url := w.webDomain
+	if w.httpsEnabled > 1 {
+		url = "http://" + url
+	} else {
+		url = "https://" + url
+	}
+	sendMsgReq, _ := json.Marshal(SendMessageRequest{
+		ChatID:    w.chatId,
+		Text:      fmt.Sprintf("📧<b>%s</b>&#60;%s&#62;\n\n%s", email.Subject, email.From.EmailAddress, string(email.Text)),
+		ParseMode: "HTML",
+		ReplyMarkup: ReplyMarkup{
+			InlineKeyboard: [][]InlineKeyboardButton{
+				{
+					{
+						Text: "查收邮件",
+						URL:  url,
+					},
+				},
+			},
+		},
+	})
+
+	_, err := http.Post(fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", w.botToken), "application/json", strings.NewReader(string(sendMsgReq)))
+	if err != nil {
+		log.WithContext(ctx).Errorf("telegram push error %+v", err)
+	}
+
+}
+func NewTelegramPushHook() *TelegramPushHook {
+	ret := &TelegramPushHook{
+		botToken:     config.Instance.TgBotToken,
+		chatId:       config.Instance.TgChatId,
+		webDomain:    config.Instance.WebDomain,
+		httpsEnabled: config.Instance.HttpsEnabled,
+	}
+	return ret
+
+}

+ 21 - 0
server/hooks/telegram_push/telegram_push_test.go

@@ -0,0 +1,21 @@
+package telegram_push
+
+import (
+	"pmail/config"
+	"pmail/dto/parsemail"
+	"testing"
+)
+
+func testInit() {
+
+	config.Init()
+
+}
+func TestWeChatPushHook_ReceiveParseAfter(t *testing.T) {
+	testInit()
+
+	w := NewTelegramPushHook()
+	w.ReceiveParseAfter(&parsemail.Email{Subject: "标题", Text: []byte("文本内容"), From: &parsemail.User{
+		EmailAddress: "hello@gmail.com",
+	}})
+}

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

@@ -8,8 +8,8 @@ import (
 	"io"
 	"net/http"
 	"pmail/config"
-	"pmail/dto"
 	"pmail/dto/parsemail"
+	"pmail/utils/context"
 	"strings"
 	"time"
 )
@@ -28,11 +28,11 @@ type WeChatPushHook struct {
 	pushUser     string
 }
 
-func (w *WeChatPushHook) SendBefore(ctx *dto.Context, email *parsemail.Email) {
+func (w *WeChatPushHook) SendBefore(ctx *context.Context, email *parsemail.Email) {
 
 }
 
-func (w *WeChatPushHook) SendAfter(ctx *dto.Context, email *parsemail.Email, err map[string]error) {
+func (w *WeChatPushHook) SendAfter(ctx *context.Context, email *parsemail.Email, err map[string]error) {
 
 }
 
@@ -45,7 +45,13 @@ func (w *WeChatPushHook) ReceiveParseAfter(email *parsemail.Email) {
 		return
 	}
 
-	w.sendUserMsg(nil, w.pushUser, string(email.Text))
+	content := string(email.Text)
+
+	if content == "" {
+		content = email.Subject
+	}
+
+	w.sendUserMsg(nil, w.pushUser, content)
 }
 
 func (w *WeChatPushHook) getWxAccessToken() string {
@@ -80,11 +86,19 @@ type DataItem struct {
 	Color string `json:"color"`
 }
 
-func (w *WeChatPushHook) sendUserMsg(ctx *dto.Context, userId string, content string) {
+func (w *WeChatPushHook) sendUserMsg(ctx *context.Context, userId string, content string) {
+
+	url := config.Instance.WebDomain
+	if config.Instance.HttpsEnabled > 1 {
+		url = "http://" + url
+	} else {
+		url = "https://" + url
+	}
+
 	sendMsgReq, _ := json.Marshal(sendMsgRequest{
 		Touser:      userId,
 		Template_id: w.templateId,
-		Url:         "http://mail." + config.Instance.Domain,
+		Url:         url,
 		Data:        SendData{Content: DataItem{Value: content, Color: "#000000"}},
 	})
 

+ 49 - 8
server/http_server/http_server.go

@@ -2,13 +2,15 @@ package http_server
 
 import (
 	"fmt"
+	"io/fs"
 	"net/http"
+	"pmail/config"
 	"pmail/controllers"
+	"pmail/controllers/email"
+	"pmail/session"
 	"time"
 )
 
-const HttpPort = 80
-
 // 这个服务是为了拦截http请求转发到https
 var httpServer *http.Server
 
@@ -20,12 +22,51 @@ func HttpStop() {
 
 func HttpStart() {
 	mux := http.NewServeMux()
-	mux.HandleFunc("/", controllers.Interceptor)
-	httpServer = &http.Server{
-		Addr:         fmt.Sprintf(":%d", HttpPort),
-		Handler:      mux,
-		ReadTimeout:  time.Second * 60,
-		WriteTimeout: time.Second * 60,
+
+	HttpPort := 80
+	if config.Instance.HttpPort > 0 {
+		HttpPort = config.Instance.HttpPort
+	}
+
+	if config.Instance.HttpsEnabled != 2 {
+		mux.HandleFunc("/", controllers.Interceptor)
+		httpServer = &http.Server{
+			Addr:         fmt.Sprintf(":%d", HttpPort),
+			Handler:      mux,
+			ReadTimeout:  time.Second * 60,
+			WriteTimeout: time.Second * 60,
+		}
+	} else {
+		fe, err := fs.Sub(local, "dist")
+		if err != nil {
+			panic(err)
+		}
+		mux.Handle("/", http.FileServer(http.FS(fe)))
+		mux.HandleFunc("/api/ping", contextIterceptor(controllers.Ping))
+		mux.HandleFunc("/api/login", contextIterceptor(controllers.Login))
+		mux.HandleFunc("/api/group", contextIterceptor(controllers.GetUserGroup))
+		mux.HandleFunc("/api/group/list", contextIterceptor(controllers.GetUserGroupList))
+		mux.HandleFunc("/api/group/add", contextIterceptor(controllers.AddGroup))
+		mux.HandleFunc("/api/group/del", contextIterceptor(controllers.DelGroup))
+		mux.HandleFunc("/api/email/list", contextIterceptor(email.EmailList))
+		mux.HandleFunc("/api/email/del", contextIterceptor(email.EmailDelete))
+		mux.HandleFunc("/api/email/read", contextIterceptor(email.MarkRead))
+		mux.HandleFunc("/api/email/detail", contextIterceptor(email.EmailDetail))
+		mux.HandleFunc("/api/email/move", contextIterceptor(email.Move))
+		mux.HandleFunc("/api/email/send", contextIterceptor(email.Send))
+		mux.HandleFunc("/api/settings/modify_password", contextIterceptor(controllers.ModifyPassword))
+		mux.HandleFunc("/api/rule/get", contextIterceptor(controllers.GetRule))
+		mux.HandleFunc("/api/rule/add", contextIterceptor(controllers.UpsertRule))
+		mux.HandleFunc("/api/rule/update", contextIterceptor(controllers.UpsertRule))
+		mux.HandleFunc("/api/rule/del", contextIterceptor(controllers.DelRule))
+		mux.HandleFunc("/attachments/", contextIterceptor(controllers.GetAttachments))
+		mux.HandleFunc("/attachments/download/", contextIterceptor(controllers.Download))
+		httpServer = &http.Server{
+			Addr:         fmt.Sprintf(":%d", HttpPort),
+			Handler:      session.Instance.LoadAndSave(mux),
+			ReadTimeout:  time.Second * 60,
+			WriteTimeout: time.Second * 60,
+		}
 	}
 
 	err := httpServer.ListenAndServe()

+ 38 - 16
server/http_server/https_server.go

@@ -17,18 +17,17 @@ import (
 	"pmail/config"
 	"pmail/controllers"
 	"pmail/controllers/email"
-	"pmail/dto"
 	"pmail/dto/response"
 	"pmail/i18n"
+	"pmail/models"
 	"pmail/session"
+	"pmail/utils/context"
 	"time"
 )
 
 //go:embed dist/*
 var local embed.FS
 
-const HttpsPort = 443
-
 var httpsServer *http.Server
 
 type nullWrite struct {
@@ -52,27 +51,43 @@ func HttpsStart() {
 	mux.HandleFunc("/api/ping", contextIterceptor(controllers.Ping))
 	mux.HandleFunc("/api/login", contextIterceptor(controllers.Login))
 	mux.HandleFunc("/api/group", contextIterceptor(controllers.GetUserGroup))
+	mux.HandleFunc("/api/group/list", contextIterceptor(controllers.GetUserGroupList))
+	mux.HandleFunc("/api/group/add", contextIterceptor(controllers.AddGroup))
+	mux.HandleFunc("/api/group/del", contextIterceptor(controllers.DelGroup))
 	mux.HandleFunc("/api/email/list", contextIterceptor(email.EmailList))
+	mux.HandleFunc("/api/email/read", contextIterceptor(email.MarkRead))
+	mux.HandleFunc("/api/email/del", contextIterceptor(email.EmailDelete))
 	mux.HandleFunc("/api/email/detail", contextIterceptor(email.EmailDetail))
 	mux.HandleFunc("/api/email/send", contextIterceptor(email.Send))
+	mux.HandleFunc("/api/email/move", contextIterceptor(email.Move))
 	mux.HandleFunc("/api/settings/modify_password", contextIterceptor(controllers.ModifyPassword))
+	mux.HandleFunc("/api/rule/get", contextIterceptor(controllers.GetRule))
+	mux.HandleFunc("/api/rule/add", contextIterceptor(controllers.UpsertRule))
+	mux.HandleFunc("/api/rule/update", contextIterceptor(controllers.UpsertRule))
+	mux.HandleFunc("/api/rule/del", contextIterceptor(controllers.DelRule))
 	mux.HandleFunc("/attachments/", contextIterceptor(controllers.GetAttachments))
 	mux.HandleFunc("/attachments/download/", contextIterceptor(controllers.Download))
 
 	// go http server会打一堆没用的日志,写一个空的日志处理器,屏蔽掉日志输出
 	nullLog := olog.New(&nullWrite{}, "", olog.Ldate)
 
-	httpsServer = &http.Server{
-		Addr:         fmt.Sprintf(":%d", HttpsPort),
-		Handler:      session.Instance.LoadAndSave(mux),
-		ReadTimeout:  time.Second * 60,
-		WriteTimeout: time.Second * 60,
-		ErrorLog:     nullLog,
+	HttpsPort := 443
+	if config.Instance.HttpsPort > 0 {
+		HttpsPort = config.Instance.HttpsPort
 	}
 
-	err = httpsServer.ListenAndServeTLS("config/ssl/public.crt", "config/ssl/private.key")
-	if err != nil {
-		panic(err)
+	if config.Instance.HttpsEnabled != 2 {
+		httpsServer = &http.Server{
+			Addr:         fmt.Sprintf(":%d", HttpsPort),
+			Handler:      session.Instance.LoadAndSave(mux),
+			ReadTimeout:  time.Second * 60,
+			WriteTimeout: time.Second * 60,
+			ErrorLog:     nullLog,
+		}
+		err = httpsServer.ListenAndServeTLS("config/ssl/public.crt", "config/ssl/private.key")
+		if err != nil {
+			panic(err)
+		}
 	}
 }
 
@@ -110,9 +125,9 @@ func contextIterceptor(h controllers.HandlerFunc) http.HandlerFunc {
 			w.Header().Set("Content-Type", "application/json")
 		}
 
-		ctx := &dto.Context{}
+		ctx := &context.Context{}
 		ctx.Context = r.Context()
-		ctx.SetValue(dto.LogID, genLogID())
+		ctx.SetValue(context.LogID, genLogID())
 		lang := r.Header.Get("Lang")
 		if lang == "" {
 			lang = "en"
@@ -121,10 +136,17 @@ func contextIterceptor(h controllers.HandlerFunc) http.HandlerFunc {
 
 		if config.IsInit {
 			user := cast.ToString(session.Instance.Get(ctx, "user"))
+			var userInfo *models.User
 			if user != "" {
-				_ = json.Unmarshal([]byte(user), &ctx.UserInfo)
+				_ = json.Unmarshal([]byte(user), &userInfo)
+			}
+			if userInfo != nil && userInfo.ID > 0 {
+				ctx.UserID = userInfo.ID
+				ctx.UserName = userInfo.Name
+				ctx.UserAccount = userInfo.Account
 			}
-			if ctx.UserInfo == nil || ctx.UserInfo.ID == 0 {
+
+			if ctx.UserID == 0 {
 				if r.URL.Path != "/api/ping" && r.URL.Path != "/api/login" {
 					response.NewErrorResponse(response.NeedLogin, i18n.GetText(ctx.Lang, "login_exp"), "").FPrint(w)
 					return

+ 6 - 0
server/http_server/setup_server.go

@@ -5,6 +5,7 @@ import (
 	"io/fs"
 	"net"
 	"net/http"
+	"pmail/config"
 	"pmail/controllers"
 	"time"
 )
@@ -25,6 +26,11 @@ func SetupStart() {
 	// 挑战请求类似这样 /.well-known/acme-challenge/QPyMAyaWw9s5JvV1oruyqWHG7OqkHMJEHPoUz2046KM
 	mux.HandleFunc("/.well-known/", controllers.AcmeChallenge)
 
+	HttpPort := 80
+	if config.Instance != nil && config.Instance.HttpPort > 0 {
+		HttpPort = config.Instance.HttpPort
+	}
+
 	setupServer = &http.Server{
 		Addr:         fmt.Sprintf(":%d", HttpPort),
 		Handler:      mux,

+ 24 - 22
server/i18n/i18n.go

@@ -2,30 +2,32 @@ package i18n
 
 var (
 	cn = map[string]string{
-		"all_email":   "全部邮件数据",
-		"inbox":       "收件箱",
-		"outbox":      "发件箱",
-		"sketch":      "草稿箱",
-		"aperror":     "账号或密码错误",
-		"unknowError": "未知错误",
-		"succ":        "成功",
-		"send_fail":   "发送失败",
-		"att_err":     "附件解码错误",
-		"login_exp":   "登录已失效",
-		"ip_taps":     "这是你服务器IP,确保这个IP正确",
+		"all_email":             "全部邮件数据",
+		"inbox":                 "收件箱",
+		"outbox":                "发件箱",
+		"sketch":                "草稿箱",
+		"aperror":               "账号或密码错误",
+		"unknowError":           "未知错误",
+		"succ":                  "成功",
+		"send_fail":             "发送失败",
+		"att_err":               "附件解码错误",
+		"login_exp":             "登录已失效",
+		"ip_taps":               "这是你服务器IP,确保这个IP正确",
+		"invalid_email_address": "无效的邮箱地址!",
 	}
 	en = map[string]string{
-		"all_email":   "All Email",
-		"inbox":       "Inbox",
-		"outbox":      "Outbox",
-		"sketch":      "Sketch",
-		"aperror":     "Incorrect account number or password",
-		"unknowError": "Unknow Error",
-		"succ":        "Success",
-		"send_fail":   "Send Failure",
-		"att_err":     "Attachment decoding error",
-		"login_exp":   "Login has expired.",
-		"ip_taps":     "This is your server's IP, make sure it is correct.",
+		"all_email":             "All Email",
+		"inbox":                 "Inbox",
+		"outbox":                "Outbox",
+		"sketch":                "Sketch",
+		"aperror":               "Incorrect account number or password",
+		"unknowError":           "Unknow Error",
+		"succ":                  "Success",
+		"send_fail":             "Send Failure",
+		"att_err":               "Attachment decoding error",
+		"login_exp":             "Login has expired.",
+		"ip_taps":               "This is your server's IP, make sure it is correct.",
+		"invalid_email_address": "Invalid e-mail address!",
 	}
 )
 

+ 18 - 16
server/main.go

@@ -7,8 +7,8 @@ import (
 	"os"
 	"pmail/config"
 	"pmail/cron_server"
-	"pmail/dto"
 	"pmail/res_init"
+	"pmail/utils/context"
 	"time"
 )
 
@@ -22,7 +22,7 @@ func (l *logFormatter) Format(entry *log.Entry) ([]byte, error) {
 	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 {
-		b.WriteString(fmt.Sprintf("[%s]", entry.Context.(*dto.Context).GetValue(dto.LogID)))
+		b.WriteString(fmt.Sprintf("[%s]", entry.Context.(*context.Context).GetValue(context.LogID)))
 	}
 	b.WriteString(fmt.Sprintf("[%s:%d]", entry.Caller.File, entry.Caller.Line))
 	b.WriteString(entry.Message)
@@ -39,8 +39,6 @@ var (
 
 func main() {
 	// 设置日志格式为json格式
-	//log.SetFormatter(&log.JSONFormatter{})
-
 	log.SetFormatter(&logFormatter{})
 	log.SetReportCaller(true)
 
@@ -53,18 +51,22 @@ func main() {
 
 	config.Init()
 
-	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:
+	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)
 	}
 

+ 2 - 1
server/models/email.go

@@ -9,6 +9,7 @@ import (
 type Email struct {
 	Id           int            `db:"id" json:"id"`
 	Type         int8           `db:"type" json:"type"`
+	GroupId      int            `db:"group_id" json:"group_id"`
 	Subject      string         `db:"subject" json:"subject"`
 	ReplyTo      string         `db:"reply_to" json:"reply_to"`
 	FromName     string         `db:"from_name" json:"from_name"`
@@ -22,7 +23,7 @@ type Email struct {
 	Attachments  string         `db:"attachments" json:"attachments"`
 	SPFCheck     int8           `db:"spf_check" json:"spf_check"`
 	DKIMCheck    int8           `db:"dkim_check" json:"dkim_check"`
-	Status       int8           `db:"status" json:"status"`
+	Status       int8           `db:"status" json:"status"` // 0未发送,1已发送,2发送失败,3删除
 	CronSendTime time.Time      `db:"cron_send_time" json:"cron_send_time"`
 	UpdateTime   time.Time      `db:"update_time" json:"update_time"`
 	SendUserID   int            `db:"send_user_id" json:"send_user_id"`

+ 8 - 0
server/models/group.go

@@ -0,0 +1,8 @@
+package models
+
+type Group struct {
+	ID       int    `db:"id" json:"id"`
+	Name     string `db:"name" json:"name"`
+	ParentId int    `db:"parent_id" json:"parent_id"`
+	UserId   int    `db:"user_id" json:"-"`
+}

+ 35 - 0
server/models/rule.go

@@ -0,0 +1,35 @@
+package models
+
+import (
+	"pmail/db"
+	"pmail/utils/context"
+	"pmail/utils/errors"
+)
+
+type Rule struct {
+	Id     int    `db:"id" json:"id"`
+	UserId string `db:"user_id" json:"user_id"`
+	Name   string `db:"name" json:"name"`
+	Value  string `db:"value" json:"value"`
+	Action int    `db:"action" json:"action"`
+	Params string `db:"params" json:"params"`
+	Sort   int    `db:"sort" json:"sort"`
+}
+
+func (p *Rule) Save(ctx *context.Context) error {
+
+	if p.Id > 0 {
+		_, err := db.Instance.Exec(db.WithContext(ctx, "update rule set name=? ,value = ? ,action = ?,params = ?,sort = ? where id = ?"), p.Name, p.Value, p.Action, p.Params, p.Sort, p.Id)
+		if err != nil {
+			return errors.Wrap(err)
+		}
+		return nil
+	} else {
+		_, err := db.Instance.Exec(db.WithContext(ctx, "insert into rule (name,value,user_id,action,params,sort) values (?,?,?,?,?,?)"), p.Name, p.Value, ctx.UserID, p.Action, p.Params, p.Sort)
+		if err != nil {
+			return errors.Wrap(err)
+		}
+		return nil
+	}
+
+}

+ 3 - 3
server/services/attachments/attachments.go

@@ -4,13 +4,13 @@ import (
 	"encoding/json"
 	log "github.com/sirupsen/logrus"
 	"pmail/db"
-	"pmail/dto"
 	"pmail/dto/parsemail"
 	"pmail/models"
 	"pmail/services/auth"
+	"pmail/utils/context"
 )
 
-func GetAttachments(ctx *dto.Context, emailId int, cid string) (string, []byte) {
+func GetAttachments(ctx *context.Context, emailId int, cid string) (string, []byte) {
 
 	// 获取邮件内容
 	var email models.Email
@@ -35,7 +35,7 @@ func GetAttachments(ctx *dto.Context, emailId int, cid string) (string, []byte)
 	return "", nil
 }
 
-func GetAttachmentsByIndex(ctx *dto.Context, emailId int, index int) (string, []byte) {
+func GetAttachmentsByIndex(ctx *context.Context, emailId int, index int) (string, []byte) {
 
 	// 获取邮件内容
 	var email models.Email

+ 4 - 4
server/services/auth/auth.go

@@ -10,16 +10,16 @@ import (
 	log "github.com/sirupsen/logrus"
 	"os"
 	"pmail/db"
-	"pmail/dto"
 	"pmail/models"
+	"pmail/utils/context"
 	"strings"
 )
 
 // HasAuth 检查当前用户是否有某个邮件的auth
-func HasAuth(ctx *dto.Context, email *models.Email) bool {
+func HasAuth(ctx *context.Context, email *models.Email) bool {
 	// 获取当前用户的auth
 	var auth []models.UserAuth
-	err := db.Instance.Select(&auth, db.WithContext(ctx, "select * from user_auth where user_id = ?"), ctx.UserInfo.ID)
+	err := db.Instance.Select(&auth, db.WithContext(ctx, "select * from user_auth where user_id = ?"), ctx.UserID)
 	if err != nil {
 		log.WithContext(ctx).Errorf("SQL error:%+v", err)
 		return false
@@ -30,7 +30,7 @@ func HasAuth(ctx *dto.Context, email *models.Email) bool {
 		if userAuth.EmailAccount == "*" {
 			hasAuth = true
 			break
-		} else if strings.Contains(email.Bcc, ctx.UserInfo.Account) || strings.Contains(email.Cc, ctx.UserInfo.Account) || strings.Contains(email.To, ctx.UserInfo.Account) {
+		} else if strings.Contains(email.Bcc, ctx.UserAccount) || strings.Contains(email.Cc, ctx.UserAccount) || strings.Contains(email.To, ctx.UserAccount) {
 			hasAuth = true
 			break
 		}

+ 33 - 0
server/services/del_email/del_email.go

@@ -0,0 +1,33 @@
+package del_email
+
+import (
+	"fmt"
+	"pmail/db"
+	"pmail/models"
+	"pmail/services/auth"
+	"pmail/utils/array"
+	"pmail/utils/context"
+	"pmail/utils/errors"
+)
+
+func DelEmail(ctx *context.Context, ids []int) error {
+	var emails []*models.Email
+
+	db.Instance.Select(&emails, db.WithContext(ctx, fmt.Sprintf("select * from email where id in (%s)", array.Join(ids, ","))))
+
+	for _, email := range emails {
+		// 检查是否有权限
+		hasAuth := auth.HasAuth(ctx, email)
+		if !hasAuth {
+			return errors.New("No Auth!")
+		}
+	}
+
+	//_, err := db.Instance.Exec(db.WithContext(ctx, fmt.Sprintf("delete from email where id in (%s)", array.Join(ids, ","))))
+	_, err := db.Instance.Exec(db.WithContext(ctx, fmt.Sprintf("update email set status = 3 where id in (%s)", array.Join(ids, ","))))
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	return nil
+}

+ 2 - 2
server/services/detail/detail.go

@@ -6,13 +6,13 @@ import (
 	"fmt"
 	log "github.com/sirupsen/logrus"
 	"pmail/db"
-	"pmail/dto"
 	"pmail/dto/parsemail"
 	"pmail/models"
+	"pmail/utils/context"
 	"strings"
 )
 
-func GetEmailDetail(ctx *dto.Context, id int, markRead bool) (*models.Email, error) {
+func GetEmailDetail(ctx *context.Context, id int, markRead bool) (*models.Email, error) {
 	// 获取邮件内容
 	var email models.Email
 	err := db.Instance.Get(&email, db.WithContext(ctx, "select * from email where id = ?"), id)

+ 115 - 0
server/services/group/group.go

@@ -0,0 +1,115 @@
+package group
+
+import (
+	"fmt"
+	log "github.com/sirupsen/logrus"
+	"pmail/db"
+	"pmail/dto"
+	"pmail/models"
+	"pmail/utils/array"
+	"pmail/utils/context"
+	"pmail/utils/errors"
+)
+
+type GroupItem struct {
+	Id       int          `json:"id"`
+	Label    string       `json:"label"`
+	Tag      string       `json:"tag"`
+	Children []*GroupItem `json:"children"`
+}
+
+func DelGroup(ctx *context.Context, groupId int) (bool, error) {
+	allGroupIds := getAllChildId(ctx, groupId)
+	allGroupIds = append(allGroupIds, groupId)
+
+	// 开启一个事务
+	trans, err := db.Instance.Begin()
+	if err != nil {
+		return false, errors.Wrap(err)
+	}
+
+	res, err := trans.Exec(db.WithContext(ctx, fmt.Sprintf("delete from `group` where id in (%s) and user_id =?", array.Join(allGroupIds, ","))), ctx.UserID)
+	if err != nil {
+		trans.Rollback()
+		return false, errors.Wrap(err)
+	}
+	num, err := res.RowsAffected()
+	if err != nil {
+		trans.Rollback()
+		return false, errors.Wrap(err)
+	}
+
+	_, err = trans.Exec(db.WithContext(ctx, fmt.Sprintf("update email set group_id=0 where group_id in (%s)", array.Join(allGroupIds, ","))))
+	if err != nil {
+		trans.Rollback()
+		return false, errors.Wrap(err)
+	}
+
+	trans.Commit()
+
+	return num > 0, nil
+}
+
+type id struct {
+	Id int `db:"id"`
+}
+
+func getAllChildId(ctx *context.Context, rootId int) []int {
+	var ids []id
+	var ret []int
+	db.Instance.Select(&ids, db.WithContext(ctx, "select id from `group` where parent_id=? and user_id=?"), rootId, ctx.UserID)
+	for _, item := range ids {
+		ret = array.Merge(ret, getAllChildId(ctx, item.Id))
+		ret = append(ret, item.Id)
+	}
+	return ret
+}
+
+// GetGroupInfoList 获取全部的分组
+func GetGroupInfoList(ctx *context.Context) []*GroupItem {
+	return buildChildren(ctx, 0)
+}
+
+// MoveMailToGroup 将某封邮件移动到某个分组中
+func MoveMailToGroup(ctx *context.Context, mailId []int, groupId int) bool {
+	res, err := db.Instance.Exec(db.WithContext(ctx, fmt.Sprintf("update email set group_id=? where id in (%s)", array.Join(mailId, ","))), groupId)
+	if err != nil {
+		log.WithContext(ctx).Errorf("SQL Error:%+v", err)
+		return false
+	}
+	rowNum, err := res.RowsAffected()
+	if err != nil {
+		log.WithContext(ctx).Errorf("SQL Error:%+v", err)
+		return false
+	}
+
+	return rowNum > 0
+}
+
+func buildChildren(ctx *context.Context, parentId int) []*GroupItem {
+	var ret []*GroupItem
+	var rootGroup []*models.Group
+	err := db.Instance.Select(&rootGroup, db.WithContext(ctx, "select * from `group` where parent_id=? and user_id=?"), parentId, ctx.UserID)
+
+	if err != nil {
+		log.WithContext(ctx).Errorf("SQL Error:%v", err)
+	}
+
+	for _, group := range rootGroup {
+		ret = append(ret, &GroupItem{
+			Id:       group.ID,
+			Label:    group.Name,
+			Tag:      dto.SearchTag{GroupId: group.ID, Status: -1, Type: -1}.ToString(),
+			Children: buildChildren(ctx, group.ID),
+		})
+	}
+
+	return ret
+
+}
+
+func GetGroupList(ctx *context.Context) []*models.Group {
+	var ret []*models.Group
+	db.Instance.Select(&ret, db.WithContext(ctx, "select * from `group` where user_id=?"), ctx.UserID)
+	return ret
+}

+ 13 - 5
server/services/list/list.go

@@ -6,19 +6,20 @@ import (
 	"pmail/db"
 	"pmail/dto"
 	"pmail/models"
+	"pmail/utils/context"
 )
 
-func GetEmailList(ctx *dto.Context, tag string, keyword string, offset, limit int) (emailList []*models.Email, total int) {
+func GetEmailList(ctx *context.Context, tag string, keyword string, offset, limit int) (emailList []*models.Email, total int) {
 
 	querySQL, queryParams := genSQL(ctx, false, tag, keyword, offset, limit)
 	counterSQL, counterParams := genSQL(ctx, true, tag, keyword, offset, limit)
 
-	err := db.Instance.Select(&emailList, querySQL, queryParams...)
+	err := db.Instance.Select(&emailList, db.WithContext(ctx, querySQL), queryParams...)
 	if err != nil {
 		log.Errorf("SQL ERROR: %s ,Error:%s", querySQL, err)
 	}
 
-	err = db.Instance.Get(&total, counterSQL, counterParams...)
+	err = db.Instance.Get(&total, db.WithContext(ctx, counterSQL), counterParams...)
 	if err != nil {
 		log.Errorf("SQL ERROR: %s ,Error:%s", querySQL, err)
 	}
@@ -26,7 +27,7 @@ func GetEmailList(ctx *dto.Context, tag string, keyword string, offset, limit in
 	return
 }
 
-func genSQL(ctx *dto.Context, counter bool, tag, keyword string, offset, limit int) (string, []any) {
+func genSQL(ctx *context.Context, counter bool, tag, keyword string, offset, limit int) (string, []any) {
 
 	sql := "select * from email where 1=1 "
 	if counter {
@@ -46,6 +47,13 @@ func genSQL(ctx *dto.Context, counter bool, tag, keyword string, offset, limit i
 	if tagInfo.Status != -1 {
 		sql += " and status =? "
 		sqlParams = append(sqlParams, tagInfo.Status)
+	} else {
+		sql += " and status != 3"
+	}
+
+	if tagInfo.GroupId != -1 {
+		sql += " and group_id=? "
+		sqlParams = append(sqlParams, tagInfo.GroupId)
 	}
 
 	if keyword != "" {
@@ -53,7 +61,7 @@ func genSQL(ctx *dto.Context, counter bool, tag, keyword string, offset, limit i
 		sqlParams = append(sqlParams, "%"+keyword+"%", "%"+keyword+"%")
 	}
 
-	sql += " limit ? offset ?"
+	sql += " order by id desc limit ? offset ?"
 	sqlParams = append(sqlParams, limit, offset)
 
 	return sql, sqlParams

+ 51 - 0
server/services/rule/match/base.go

@@ -0,0 +1,51 @@
+package match
+
+import (
+	"encoding/json"
+	"pmail/dto/parsemail"
+	"pmail/utils/context"
+)
+
+const (
+	RuleTypeRegex    = "regex"
+	RuleTypeContains = "contains"
+	RuleTypeEq       = "equal"
+)
+
+type Match interface {
+	Match(ctx *context.Context, email *parsemail.Email) bool
+}
+
+func getFieldContent(field string, email *parsemail.Email) string {
+	switch field {
+	case "ReplyTo":
+		b, _ := json.Marshal(email.ReplyTo)
+		return string(b)
+	case "From":
+		b, _ := json.Marshal(email.From)
+		return string(b)
+	case "Subject":
+		return email.Subject
+	case "To":
+		b, _ := json.Marshal(email.To)
+		return string(b)
+	case "Bcc":
+		b, _ := json.Marshal(email.Bcc)
+		return string(b)
+	case "Cc":
+		b, _ := json.Marshal(email.Cc)
+		return string(b)
+	case "Text":
+		return string(email.Text)
+	case "Html":
+		return string(email.HTML)
+	case "Sender":
+		b, _ := json.Marshal(email.Sender)
+		return string(b)
+	case "Content":
+		b := string(email.HTML)
+		b2 := string(email.Text)
+		return b + b2
+	}
+	return ""
+}

+ 24 - 0
server/services/rule/match/contains_match.go

@@ -0,0 +1,24 @@
+package match
+
+import (
+	"pmail/dto/parsemail"
+	"pmail/utils/context"
+	"strings"
+)
+
+type ContainsMatch struct {
+	Rule  string
+	Field string
+}
+
+func NewContainsMatch(field, rule string) *ContainsMatch {
+	return &ContainsMatch{
+		Rule:  rule,
+		Field: field,
+	}
+}
+
+func (r *ContainsMatch) Match(ctx *context.Context, email *parsemail.Email) bool {
+	content := getFieldContent(r.Field, email)
+	return strings.Contains(content, r.Rule)
+}

+ 23 - 0
server/services/rule/match/equal_match.go

@@ -0,0 +1,23 @@
+package match
+
+import (
+	"pmail/dto/parsemail"
+	"pmail/utils/context"
+)
+
+type EqualMatch struct {
+	Rule  string
+	Field string
+}
+
+func NewEqualMatch(field, rule string) *EqualMatch {
+	return &EqualMatch{
+		Rule:  rule,
+		Field: field,
+	}
+}
+
+func (r *EqualMatch) Match(ctx *context.Context, email *parsemail.Email) bool {
+	content := getFieldContent(r.Field, email)
+	return content == r.Rule
+}

+ 32 - 0
server/services/rule/match/regex_match.go

@@ -0,0 +1,32 @@
+package match
+
+import (
+	log "github.com/sirupsen/logrus"
+	"pmail/dto/parsemail"
+	"pmail/utils/context"
+	"regexp"
+)
+
+type RegexMatch struct {
+	Rule  string
+	Field string
+}
+
+func NewRegexMatch(field, rule string) *RegexMatch {
+	return &RegexMatch{
+		Rule:  rule,
+		Field: field,
+	}
+}
+
+func (r *RegexMatch) Match(ctx *context.Context, email *parsemail.Email) bool {
+	content := getFieldContent(r.Field, email)
+
+	match, err := regexp.MatchString(r.Rule, content)
+
+	if err != nil {
+		log.WithContext(ctx).Errorf("rule regex error %v", err)
+	}
+
+	return match
+}

+ 18 - 0
server/services/rule/match/regex_match_test.go

@@ -0,0 +1,18 @@
+package match
+
+import (
+	"pmail/models"
+	"testing"
+)
+
+func TestRegexMatch_Match(t *testing.T) {
+	r := NewRegexMatch("Subject", "\\d+")
+
+	ret := r.Match(nil, &models.Email{
+		Subject: "111",
+	})
+
+	if !ret {
+		t.Errorf("失败")
+	}
+}

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

@@ -0,0 +1,81 @@
+package rule
+
+import (
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/cast"
+	"pmail/config"
+	"pmail/db"
+	"pmail/dto"
+	"pmail/dto/parsemail"
+	"pmail/models"
+	"pmail/services/rule/match"
+	"pmail/utils/context"
+	"pmail/utils/send"
+	"strings"
+)
+
+func GetAllRules(ctx *context.Context) []*dto.Rule {
+	var res []*models.Rule
+	var err error
+	if ctx == nil {
+		err = db.Instance.Select(&res, "select * from rule order by sort desc")
+	} else {
+		err = db.Instance.Select(&res, db.WithContext(ctx, "select * from rule where user_id=? order by sort desc"), ctx.UserID)
+	}
+
+	if err != nil {
+		log.WithContext(ctx).Errorf("sqlERror :%v", err)
+	}
+	var ret []*dto.Rule
+	for _, rule := range res {
+		ret = append(ret, (&dto.Rule{}).Decode(rule))
+	}
+
+	return ret
+}
+
+func MatchRule(ctx *context.Context, rule *dto.Rule, email *parsemail.Email) bool {
+
+	for _, r := range rule.Rules {
+		var m match.Match
+
+		switch r.Type {
+		case match.RuleTypeRegex:
+			m = match.NewRegexMatch(r.Field, r.Rule)
+		case match.RuleTypeContains:
+			m = match.NewContainsMatch(r.Field, r.Rule)
+		case match.RuleTypeEq:
+			m = match.NewEqualMatch(r.Field, r.Rule)
+		}
+		if m == nil {
+			continue
+		}
+
+		if !m.Match(ctx, email) {
+			return false
+		}
+	}
+
+	return true
+}
+
+func DoRule(ctx *context.Context, rule *dto.Rule, email *parsemail.Email) {
+	switch rule.Action {
+	case dto.READ:
+		email.IsRead = 1
+	case dto.DELETE:
+		email.Status = 3
+	case dto.FORWARD:
+		if strings.Contains(rule.Params, config.Instance.Domain) {
+			log.WithContext(ctx).Errorf("Forward Error! loop forwarding!")
+			return
+		}
+
+		err := send.Forward(nil, email, rule.Params)
+		if err != nil {
+			log.WithContext(ctx).Errorf("Forward Error:%v", err)
+		}
+	case dto.MOVE:
+		email.GroupId = cast.ToInt(rule.Params)
+	}
+}

+ 9 - 5
server/services/setup/db.go

@@ -5,15 +5,15 @@ import (
 	"os"
 	"pmail/config"
 	"pmail/db"
-	"pmail/dto"
 	"pmail/models"
 	"pmail/utils/array"
+	"pmail/utils/context"
 	"pmail/utils/errors"
 	"pmail/utils/file"
 	"pmail/utils/password"
 )
 
-func GetDatabaseSettings(ctx *dto.Context) (string, string, error) {
+func GetDatabaseSettings(ctx *context.Context) (string, string, error) {
 	configData, err := ReadConfig()
 	if err != nil {
 		return "", "", errors.Wrap(err)
@@ -26,7 +26,7 @@ func GetDatabaseSettings(ctx *dto.Context) (string, string, error) {
 	return configData.DbType, configData.DbDSN, nil
 }
 
-func GetAdminPassword(ctx *dto.Context) (string, error) {
+func GetAdminPassword(ctx *context.Context) (string, error) {
 
 	users := []*models.User{}
 	err := db.Instance.Select(&users, "select * from user")
@@ -41,7 +41,7 @@ func GetAdminPassword(ctx *dto.Context) (string, error) {
 	return "", nil
 }
 
-func SetAdminPassword(ctx *dto.Context, account, pwd string) error {
+func SetAdminPassword(ctx *context.Context, account, pwd string) error {
 	encodePwd := password.Encode(pwd)
 	res, err := db.Instance.Exec(db.WithContext(ctx, "INSERT INTO user (account, name, password) VALUES (?, 'admin',?)"), account, encodePwd)
 	if err != nil {
@@ -58,7 +58,7 @@ func SetAdminPassword(ctx *dto.Context, account, pwd string) error {
 	return nil
 }
 
-func SetDatabaseSettings(ctx *dto.Context, dbType, dbDSN string) error {
+func SetDatabaseSettings(ctx *context.Context, dbType, dbDSN string) error {
 	configData, err := ReadConfig()
 	if err != nil {
 		return errors.Wrap(err)
@@ -68,6 +68,10 @@ func SetDatabaseSettings(ctx *dto.Context, dbType, dbDSN string) error {
 		return errors.New("dbtype error")
 	}
 
+	if dbDSN == "" {
+		return errors.New("DSN error")
+	}
+
 	configData.DbType = dbType
 	configData.DbDSN = dbDSN
 

+ 10 - 0
server/services/setup/db_test.go

@@ -0,0 +1,10 @@
+package setup
+
+import (
+	"testing"
+)
+
+func TestSetAdminPassword(t *testing.T) {
+
+	SetAdminPassword(nil, "admin", "admin")
+}

+ 2 - 2
server/services/setup/dns.go

@@ -5,9 +5,9 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"pmail/dto"
 	"pmail/i18n"
 	"pmail/services/auth"
+	"pmail/utils/context"
 	"pmail/utils/errors"
 )
 
@@ -19,7 +19,7 @@ type DNSItem struct {
 	Tips  string `json:"tips"`
 }
 
-func GetDNSSettings(ctx *dto.Context) ([]*DNSItem, error) {
+func GetDNSSettings(ctx *context.Context) ([]*DNSItem, error) {
 	configData, err := ReadConfig()
 	if err != nil {
 		return nil, errors.Wrap(err)

+ 2 - 2
server/services/setup/finish.go

@@ -1,13 +1,13 @@
 package setup
 
 import (
-	"pmail/dto"
 	"pmail/signal"
+	"pmail/utils/context"
 	"pmail/utils/errors"
 )
 
 // Finish 标记初始化完成
-func Finish(ctx *dto.Context) error {
+func Finish(ctx *context.Context) error {
 	cfg, err := ReadConfig()
 	if err != nil {
 		return errors.Wrap(err)

+ 1 - 1
server/session/init.go

@@ -14,7 +14,7 @@ var Instance *scs.SessionManager
 
 func Init() {
 	Instance = scs.New()
-	Instance.Lifetime = 24 * time.Hour
+	Instance.Lifetime = 7 * 24 * time.Hour
 	// 使用db存储session数据,目前为了架构简单,
 	// 暂不引入redis存储,如果日后性能存在瓶颈,可以将session迁移到redis
 	if config.Instance.DbType == "mysql" {

+ 41 - 8
server/smtp_server/read_content.go

@@ -8,9 +8,11 @@ import (
 	"io"
 	"net"
 	"net/netip"
+	"pmail/config"
 	"pmail/db"
 	"pmail/dto/parsemail"
 	"pmail/hooks"
+	"pmail/services/rule"
 	"pmail/utils/async"
 	"strings"
 	"time"
@@ -23,14 +25,16 @@ func (s *Session) Data(r io.Reader) error {
 		return err
 	}
 
+	as1 := async.New(nil)
 	for _, hook := range hooks.HookList {
 		if hook == nil {
 			continue
 		}
-		async.New(nil).Process(func() {
-			hook.ReceiveParseBefore(emailData)
-		})
+		as1.WaitProcess(func(hk any) {
+			hk.(hooks.EmailHook).ReceiveParseBefore(emailData)
+		}, hook)
 	}
+	as1.Wait()
 
 	log.Infof("邮件原始内容: %s", emailData)
 
@@ -55,16 +59,41 @@ func (s *Session) Data(r io.Reader) error {
 		spfV = 1
 	}
 
+	// 垃圾过滤
+	if config.Instance.SpamFilterLevel == 1 && !SPFStatus && !dkimStatus {
+		log.Infoln("垃圾邮件,拒信")
+		return nil
+	}
+
+	if config.Instance.SpamFilterLevel == 2 && !SPFStatus {
+		log.Infoln("垃圾邮件,拒信")
+		return nil
+	}
+
+	as2 := async.New(nil)
 	for _, hook := range hooks.HookList {
 		if hook == nil {
 			continue
 		}
-		async.New(nil).Process(func() {
-			hook.ReceiveParseAfter(email)
-		})
+		as2.WaitProcess(func(hk any) {
+			hk.(hooks.EmailHook).ReceiveParseAfter(email)
+		}, hook)
+	}
+	as2.Wait()
+
+	// 执行邮件规则
+	rs := rule.GetAllRules(nil)
+	for _, r := range rs {
+		if rule.MatchRule(nil, r, email) {
+			rule.DoRule(nil, r, email)
+		}
+	}
+
+	if email == nil {
+		return nil
 	}
 
-	sql := "INSERT INTO email (send_date, subject, reply_to, from_name, from_address, `to`, bcc, cc, text, html, sender, attachments,spf_check, dkim_check, create_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+	sql := "INSERT INTO email (send_date, subject, reply_to, from_name, from_address, `to`, bcc, cc, text, html, sender, attachments,spf_check, dkim_check, create_time,is_read,status,group_id) VALUES (?,?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
 	_, err = db.Instance.Exec(sql,
 		email.Date,
 		email.Subject,
@@ -80,7 +109,11 @@ func (s *Session) Data(r io.Reader) error {
 		json2string(email.Attachments),
 		spfV,
 		dkimV,
-		time.Now())
+		time.Now(),
+		email.IsRead,
+		email.Status,
+		email.GroupId,
+	)
 
 	if err != nil {
 		log.Println("mysql insert error:", err.Error())

+ 349 - 0
server/smtp_server/read_content_test.go

@@ -70,3 +70,352 @@ func TestSession_DataGmail(t *testing.T) {
 	s.Data(bytes.NewReader(data))
 
 }
+
+func TestPmailEmail(t *testing.T) {
+	testInit()
+	emailData := `DKIM-Signature: a=rsa-sha256; bh=x7Rh+N2y2K9exccEAyKCTAGDgYKfnLZpMWc25ug5Ny4=;
+ c=simple/simple; d=domain.com;
+ h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693831868;
+ v=1;
+ b=1PZEupYvSMtGyYx42b4G65YbdnRj4y2QFo9kS7GXiTVhUM5EYzJhZzknwRMN5RL5aFY26W4E
+ DmzJ85XvPPvrDtnU/B4jkc5xthE+KEsb1Go8HcL8WQqwvsE9brepeA0t0RiPnA/x7dbTo3u72SG
+ WqtviWbJH5lPFc9PkSbEPFtc=
+Content-Type: multipart/mixed;
+ boundary=3c13260efb7bd8bad8315c21215489fe283f36cdf82813674f6e11215f6c
+Mime-Version: 1.0
+Subject: =?utf-8?q?=E6=8F=92=E4=BB=B6=E6=B5=8B=E8=AF=95?=
+To: =?utf-8?q?=E5=90=8D?= <ok@jinnrry.com>
+From: =?utf-8?q?=E5=8F=91=E9=80=81=E4=BA=BA?= <j@jinnrry.com>
+Date: Mon, 04 Sep 2023 20:51:08 +0800
+
+--3c13260efb7bd8bad8315c21215489fe283f36cdf82813674f6e11215f6c
+Content-Type: multipart/alternative;
+ boundary=9ebf2f3c4f97c51dd9a285ae28a54d2d0d84aa6d0ad28b76547e2096bb66
+
+--9ebf2f3c4f97c51dd9a285ae28a54d2d0d84aa6d0ad28b76547e2096bb66
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/plain
+
+=E8=BF=99=E6=98=AFText
+--9ebf2f3c4f97c51dd9a285ae28a54d2d0d84aa6d0ad28b76547e2096bb66
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/html
+
+<div>=E8=BF=99=E6=98=AFHtml</div>
+--9ebf2f3c4f97c51dd9a285ae28a54d2d0d84aa6d0ad28b76547e2096bb66--
+
+--3c13260efb7bd8bad8315c21215489fe283f36cdf82813674f6e11215f6c--
+`
+	s := Session{
+		RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
+	}
+
+	s.Data(bytes.NewReader([]byte(emailData)))
+
+}
+
+func TestRuleForward(t *testing.T) {
+	testInit()
+
+	forwardEmail := `DKIM-Signature: a=rsa-sha256; bh=bpOshF+iimuqAQijVxqkH6gPpWf8A+Ih30/tMjgEgS0=;
+ c=simple/simple; d=jinnrry.com;
+ h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693992640;
+ v=1;
+ b=XiOgYL9iGrkuYzXBAf7DSO0sRbFr6aPOE4VikmselNKEF1UTjMPdiqpeHyx/i6BOQlJWWZEC
+ PzceHTDFIStcZE6a5Sc1nh8Fis+gRkrheBO/zK/P5P/euK+0Fj5+0T82keNTSCgo1ZtEIubaNR0
+ JvkwJ2ZC9g8xV6Yiq+ZhRriT8lZ6zeI55PPEFJIzFgZ7xDshDgx5E7J1xRXQqcEMV1rgVq04d3c
+ 6wjU+LLtghmgtUToRp3ASn6DhVO+Bbc4QkmcQ/StQH3681+1GVMHvQSBhSSymSRA71SikE2u3a1
+ JnvbOP9fThP7h+6oFEIRuF7MwDb3JWY5BXiFFKCkecdFg==
+Content-Type: multipart/mixed;
+ boundary=8e9d5abb6bdac11b8d7d6e13280af1a87d12b904a59368d6e852b0a4ce3e
+Mime-Version: 1.0
+Subject: forward
+To: <t@jiangwei.one>
+From: "i" <i@jinnrry.com>
+Date: Wed, 06 Sep 2023 17:30:40 +0800
+
+--8e9d5abb6bdac11b8d7d6e13280af1a87d12b904a59368d6e852b0a4ce3e
+Content-Type: multipart/alternative;
+ boundary=a62ae91c159ea22e8196d57d344626eb00d1ddfa9c5064a39b01588aa992
+
+--a62ae91c159ea22e8196d57d344626eb00d1ddfa9c5064a39b01588aa992
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/plain
+
+hello pls Forward the email.
+--a62ae91c159ea22e8196d57d344626eb00d1ddfa9c5064a39b01588aa992
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/html
+
+<p>hello pls Forward the email.</p>
+--a62ae91c159ea22e8196d57d344626eb00d1ddfa9c5064a39b01588aa992--
+
+--8e9d5abb6bdac11b8d7d6e13280af1a87d12b904a59368d6e852b0a4ce3e--`
+
+	readEmail := `DKIM-Signature: a=rsa-sha256; bh=JcCDj6edb1bAwRbcFZ63plFZOeB5AdGWLE/PQ2FQ1Tc=;
+ c=simple/simple; d=jinnrry.com;
+ h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693992600;
+ v=1;
+ b=rwlqSkDFKYH42pA1jsajemaw+4YdeLHPeqV4mLQrRdihgma1VSvXl5CEOur/KuwQuUarr2cu
+ SntWrHE6+RnDaQcPEHbkgoMjEJw5+VPwkIvE6VSlMIB7jg93mGzvN2yjheWTePZ+cVPjOaIrgir
+ wiT24hkrTHp+ONT8XoS0sDuY+ieyBZp/GCv/YvgE4t0JEkNozMAVWotrXxaICDzZoWP3NNmKLqg
+ 6He6zwWAl51r3W5R5weGBi6A/FqlHgHZGroXnNi+wolDuN6pQiVAJ7MZ6hboPCbCCRrBQDTdor5
+ wEI2+MwlJ/d2f17wxoGmluCewbeYttuVcpUOVwACJKw3g==
+Content-Type: multipart/mixed;
+ boundary=9e33a130a8a976102a93e296d6408d228e151f7841ca9ee0d777234fd6f3
+Mime-Version: 1.0
+Subject: read
+To: <t@jiangwei.one>
+From: "i" <i@jinnrry.com>
+Date: Wed, 06 Sep 2023 17:30:00 +0800
+
+--9e33a130a8a976102a93e296d6408d228e151f7841ca9ee0d777234fd6f3
+Content-Type: multipart/alternative;
+ boundary=54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884
+
+--54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/plain
+
+12 aRead 1sadf
+--54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/html
+
+<p>12 aRead 1sadf</p>
+--54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884--
+
+--9e33a130a8a976102a93e296d6408d228e151f7841ca9ee0d777234fd6f3--`
+
+	moveEmail := `DKIM-Signature: a=rsa-sha256; bh=YQfG/wlHGhky6FNmpIwgDYDOc/uyivdBv+9S02Z04xY=;
+ c=simple/simple; d=jinnrry.com;
+ h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693992542;
+ v=1;
+ b=IhxswOCq8I7CmCas1EMp+n8loR7illqlF0IJC6eN1+OLjI/E5BPzpP4HWkyqaAkd0Vn9i+Bn
+ MVb5kNHZ2S7qt0rqAAc6Atc0i9WpLEI3Cng+VDn+difcMZlJSAkhLLn2sUsS4Fzqqo3Cbw62qSO
+ TgnWRmlj9aM+5xfGcl/76WOvQQpahJbGg6Go51kFMeHVom/VeGKIgFBCeMe37T/LS03c3pAV8gA
+ i6Zy3GYE57W/qU3oCzaGeS3n5zom/i74H4VipiVIMX/OBNYhdHWrP8vyjvzLFpJlXp6RvzcRl0P
+ ytyiCZfE8G7fAFntp20LW70Y5Xgqqczk1jR578UDczVoA==
+Content-Type: multipart/mixed;
+ boundary=c84d60b253aa6caee345c73e717ad59b1975448bbdfad7a23ac4d76e022d
+Mime-Version: 1.0
+Subject: Move
+To: <t@jiangwei.one>
+From: "i" <i@jinnrry.com>
+Date: Wed, 06 Sep 2023 17:29:02 +0800
+
+--c84d60b253aa6caee345c73e717ad59b1975448bbdfad7a23ac4d76e022d
+Content-Type: multipart/alternative;
+ boundary=a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966
+
+--a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/plain
+
+MOVE move Move
+--a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/html
+
+<p>MOVE move Move</p>
+--a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966--
+
+--c84d60b253aa6caee345c73e717ad59b1975448bbdfad7a23ac4d76e022d--`
+
+	deleteEmail := `DKIM-Signature: a=rsa-sha256; bh=dNtHGqd1NbRj0WSwrJmPsqAcAy3h/4kZK2HFQ0Asld8=;
+ c=simple/simple; d=jinnrry.com;
+ h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693992495;
+ v=1;
+ b=QllU8lqGdoOMaGYp8d13oWytb7+RebqKjq4y8Rs/kOeQxoE8dSEVliK3eBiXidsNTdDtkTqf
+ eiwjyRBK92NVCYprdJqLbu9qZ39BC2lk3NXttTSJ1+1ZZ/bGtIW5JIYn2pToED0MqVVkxGFUtl+
+ qFmc4mWo5a4Mbij7xaAB3uJtHpBDt7q4Ovr2hiMetQv7YrhZvCt/xrH8Q9YzZ6xzFUL5ekW40eH
+ oWElU1GyVBHWCKh31aweyhA+1XLPYojjREQYd4svRqTbSFSsBqFwFIUGdnyJh2WgmF8eucmttAw
+ oRhgzyZkHL1jAskKFBpO10SDReyk50Cvc+0kSLj+QcUpg==
+Content-Type: multipart/mixed;
+ boundary=bdfa9bf94e22e218105281e06bd59bd6df3ce70e71367bf49fbe73301af3
+Mime-Version: 1.0
+Subject: test
+To: <t@jiangwei.one>
+From: "i" <i@jinnrry.com>
+Date: Wed, 06 Sep 2023 17:28:15 +0800
+
+--bdfa9bf94e22e218105281e06bd59bd6df3ce70e71367bf49fbe73301af3
+Content-Type: multipart/alternative;
+ boundary=7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04
+
+--7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/plain
+
+Delete
+--7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/html
+
+<p>Delete</p>
+--7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04--
+
+--bdfa9bf94e22e218105281e06bd59bd6df3ce70e71367bf49fbe73301af3--`
+
+	s := Session{
+		RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
+	}
+
+	s.Data(bytes.NewReader([]byte(deleteEmail)))
+	s.Data(bytes.NewReader([]byte(readEmail)))
+	s.Data(bytes.NewReader([]byte(forwardEmail)))
+	s.Data(bytes.NewReader([]byte(moveEmail)))
+}
+
+func TestRuleRead(t *testing.T) {
+	testInit()
+
+	readEmail := `DKIM-Signature: a=rsa-sha256; bh=JcCDj6edb1bAwRbcFZ63plFZOeB5AdGWLE/PQ2FQ1Tc=;
+ c=simple/simple; d=jinnrry.com;
+ h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693992600;
+ v=1;
+ b=rwlqSkDFKYH42pA1jsajemaw+4YdeLHPeqV4mLQrRdihgma1VSvXl5CEOur/KuwQuUarr2cu
+ SntWrHE6+RnDaQcPEHbkgoMjEJw5+VPwkIvE6VSlMIB7jg93mGzvN2yjheWTePZ+cVPjOaIrgir
+ wiT24hkrTHp+ONT8XoS0sDuY+ieyBZp/GCv/YvgE4t0JEkNozMAVWotrXxaICDzZoWP3NNmKLqg
+ 6He6zwWAl51r3W5R5weGBi6A/FqlHgHZGroXnNi+wolDuN6pQiVAJ7MZ6hboPCbCCRrBQDTdor5
+ wEI2+MwlJ/d2f17wxoGmluCewbeYttuVcpUOVwACJKw3g==
+Content-Type: multipart/mixed;
+ boundary=9e33a130a8a976102a93e296d6408d228e151f7841ca9ee0d777234fd6f3
+Mime-Version: 1.0
+Subject: read
+To: <t@jiangwei.one>
+From: "i" <i@jinnrry.com>
+Date: Wed, 06 Sep 2023 17:30:00 +0800
+
+--9e33a130a8a976102a93e296d6408d228e151f7841ca9ee0d777234fd6f3
+Content-Type: multipart/alternative;
+ boundary=54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884
+
+--54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/plain
+
+12 aRead 1sadf
+--54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/html
+
+<p>12 aRead 1sadf</p>
+--54a95f3429f3cdb342383db10293780bed341f8dc20d2f876eb0853e3884--
+
+--9e33a130a8a976102a93e296d6408d228e151f7841ca9ee0d777234fd6f3--`
+
+	s := Session{
+		RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
+	}
+
+	s.Data(bytes.NewReader([]byte(readEmail)))
+
+}
+
+func TestRuleDelete(t *testing.T) {
+	testInit()
+
+	deleteEmail := `DKIM-Signature: a=rsa-sha256; bh=dNtHGqd1NbRj0WSwrJmPsqAcAy3h/4kZK2HFQ0Asld8=;
+ c=simple/simple; d=jinnrry.com;
+ h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693992495;
+ v=1;
+ b=QllU8lqGdoOMaGYp8d13oWytb7+RebqKjq4y8Rs/kOeQxoE8dSEVliK3eBiXidsNTdDtkTqf
+ eiwjyRBK92NVCYprdJqLbu9qZ39BC2lk3NXttTSJ1+1ZZ/bGtIW5JIYn2pToED0MqVVkxGFUtl+
+ qFmc4mWo5a4Mbij7xaAB3uJtHpBDt7q4Ovr2hiMetQv7YrhZvCt/xrH8Q9YzZ6xzFUL5ekW40eH
+ oWElU1GyVBHWCKh31aweyhA+1XLPYojjREQYd4svRqTbSFSsBqFwFIUGdnyJh2WgmF8eucmttAw
+ oRhgzyZkHL1jAskKFBpO10SDReyk50Cvc+0kSLj+QcUpg==
+Content-Type: multipart/mixed;
+ boundary=bdfa9bf94e22e218105281e06bd59bd6df3ce70e71367bf49fbe73301af3
+Mime-Version: 1.0
+Subject: test
+To: <t@jiangwei.one>
+From: "i" <i@jinnrry.com>
+Date: Wed, 06 Sep 2023 17:28:15 +0800
+
+--bdfa9bf94e22e218105281e06bd59bd6df3ce70e71367bf49fbe73301af3
+Content-Type: multipart/alternative;
+ boundary=7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04
+
+--7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/plain
+
+Delete
+--7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/html
+
+<p>Delete</p>
+--7352524eaae801790245f6bf095460fd1f4e01f5748b4dba48635bf59b04--
+
+--bdfa9bf94e22e218105281e06bd59bd6df3ce70e71367bf49fbe73301af3--`
+
+	s := Session{
+		RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
+	}
+
+	s.Data(bytes.NewReader([]byte(deleteEmail)))
+
+}
+
+func TestRuleMove(t *testing.T) {
+	testInit()
+
+	moveEmail := `DKIM-Signature: a=rsa-sha256; bh=YQfG/wlHGhky6FNmpIwgDYDOc/uyivdBv+9S02Z04xY=;
+ c=simple/simple; d=jinnrry.com;
+ h=Content-Type:Mime-Version:Subject:To:From:Date; s=default; t=1693992542;
+ v=1;
+ b=IhxswOCq8I7CmCas1EMp+n8loR7illqlF0IJC6eN1+OLjI/E5BPzpP4HWkyqaAkd0Vn9i+Bn
+ MVb5kNHZ2S7qt0rqAAc6Atc0i9WpLEI3Cng+VDn+difcMZlJSAkhLLn2sUsS4Fzqqo3Cbw62qSO
+ TgnWRmlj9aM+5xfGcl/76WOvQQpahJbGg6Go51kFMeHVom/VeGKIgFBCeMe37T/LS03c3pAV8gA
+ i6Zy3GYE57W/qU3oCzaGeS3n5zom/i74H4VipiVIMX/OBNYhdHWrP8vyjvzLFpJlXp6RvzcRl0P
+ ytyiCZfE8G7fAFntp20LW70Y5Xgqqczk1jR578UDczVoA==
+Content-Type: multipart/mixed;
+ boundary=c84d60b253aa6caee345c73e717ad59b1975448bbdfad7a23ac4d76e022d
+Mime-Version: 1.0
+Subject: Move
+To: <t@jiangwei.one>
+From: "i" <i@jinnrry.com>
+Date: Wed, 06 Sep 2023 17:29:02 +0800
+
+--c84d60b253aa6caee345c73e717ad59b1975448bbdfad7a23ac4d76e022d
+Content-Type: multipart/alternative;
+ boundary=a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966
+
+--a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/plain
+
+MOVE move Move
+--a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Content-Type: text/html
+
+<p>MOVE move Move</p>
+--a69985ebcf3c1c44d6e69e5a29c1044743cd9e44d4bc9bb6886f83a73966--
+
+--c84d60b253aa6caee345c73e717ad59b1975448bbdfad7a23ac4d76e022d--`
+
+	s := Session{
+		RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
+	}
+
+	s.Data(bytes.NewReader([]byte(moveEmail)))
+}

+ 0 - 160
server/smtp_server/send.go

@@ -1,160 +0,0 @@
-package smtp_server
-
-import (
-	"crypto/tls"
-	"crypto/x509"
-	"errors"
-	log "github.com/sirupsen/logrus"
-	"net"
-	"pmail/dto"
-	"pmail/dto/parsemail"
-	"pmail/utils/array"
-	"pmail/utils/async"
-	"pmail/utils/smtp"
-	"strings"
-)
-
-type mxDomain struct {
-	domain string
-	mxHost string
-}
-
-func Send(ctx *dto.Context, e *parsemail.Email) (error, map[string]error) {
-
-	b := e.BuildBytes(ctx)
-
-	var to []*parsemail.User
-	to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
-
-	// 按域名整理
-	toByDomain := map[mxDomain][]*parsemail.User{}
-	for _, s := range to {
-		args := strings.Split(s.EmailAddress, "@")
-		if len(args) == 2 {
-			//查询dns mx记录
-			mxInfo, err := net.LookupMX(args[1])
-			address := mxDomain{
-				domain: "smtp." + args[1],
-				mxHost: "smtp." + args[1],
-			}
-			if err != nil {
-				log.WithContext(ctx).Errorf(s.EmailAddress, "域名mx记录查询失败")
-			}
-			if len(mxInfo) > 0 {
-				address = mxDomain{
-					domain: args[1],
-					mxHost: mxInfo[0].Host,
-				}
-			}
-			toByDomain[address] = append(toByDomain[address], s)
-		} else {
-			log.WithContext(ctx).Errorf("邮箱地址解析错误! %s", s)
-			continue
-		}
-	}
-
-	var errEmailAddress []string
-
-	errMap := map[string]error{}
-
-	as := async.New(ctx)
-	for domain, tos := range toByDomain {
-		domain := domain
-		tos := tos
-		as.WaitProcess(func() {
-
-			err := smtp.SendMail("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
-
-			// 重新选取证书域名
-			if err != nil {
-				if certificateErr, ok := err.(*tls.CertificateVerificationError); ok {
-					if hostnameErr, is := certificateErr.Err.(x509.HostnameError); is {
-						if hostnameErr.Certificate != nil {
-							certificateHostName := hostnameErr.Certificate.DNSNames
-							err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
-						}
-					}
-				}
-			}
-
-			if err != nil {
-				log.WithContext(ctx).Errorf("%v 邮件投递失败%+v", tos, err)
-				for _, user := range tos {
-					errEmailAddress = append(errEmailAddress, user.EmailAddress)
-
-				}
-			}
-			errMap[domain.domain] = err
-		})
-	}
-	as.Wait()
-
-	if len(errEmailAddress) > 0 {
-		return errors.New("以下收件人投递失败:" + array.Join(errEmailAddress, ",")), errMap
-	}
-	return nil, errMap
-
-}
-
-func buildAddress(u []*parsemail.User) []string {
-	var ret []string
-
-	for _, user := range u {
-		ret = append(ret, user.EmailAddress)
-
-	}
-
-	return ret
-}
-
-func domainMatch(domain string, dnsNames []string) string {
-	secondMatch := ""
-
-	for _, name := range dnsNames {
-		if strings.Contains(name, "smtp") {
-			secondMatch = name
-		}
-
-		if name == domain {
-			return name
-		}
-		if strings.Contains(name, "*") {
-			nameArg := strings.Split(name, ".")
-			domainArg := strings.Split(domain, ".")
-			match := true
-			for i := 0; i < len(nameArg); i++ {
-				if nameArg[len(nameArg)-1-i] == "*" {
-					continue
-				}
-				if len(domainArg) > i {
-					if nameArg[len(nameArg)-1-i] == domainArg[len(domainArg)-1-i] {
-						continue
-					}
-				}
-				match = false
-				break
-			}
-
-			for i := 0; i < len(domainArg); i++ {
-				if len(nameArg) > i && nameArg[len(nameArg)-1-i] == domainArg[len(domainArg)-1-i] {
-					continue
-				}
-				if len(nameArg) > i && nameArg[len(nameArg)-1-i] == "*" {
-					continue
-				}
-
-				match = false
-				break
-			}
-			if match {
-				return domain
-			}
-		}
-	}
-
-	if secondMatch != "" {
-		return strings.ReplaceAll(secondMatch, "*.", "")
-	}
-
-	return strings.ReplaceAll(dnsNames[0], "*.", "")
-}

+ 0 - 24
server/smtp_server/send_test.go

@@ -1,24 +0,0 @@
-package smtp_server
-
-import (
-	"pmail/dto/parsemail"
-	"testing"
-)
-
-func TestSend(t *testing.T) {
-	testInit()
-	e := &parsemail.Email{
-		From: &parsemail.User{
-			Name:         "发送人",
-			EmailAddress: "j@jinnrry.com",
-		},
-		To: []*parsemail.User{
-			{"ok@jinnrry.com", "名"},
-			{"ok@xjiangwei.cn", "字"},
-		},
-		Subject: "你好",
-		Text:    []byte("这是Text"),
-		HTML:    []byte("<div>这是Html</div>"),
-	}
-	Send(nil, e)
-}

+ 0 - 0
server/smtp_server/main.go → server/smtp_server/smtp.go


+ 12 - 0
server/utils/address/address.go

@@ -0,0 +1,12 @@
+package address
+
+import "strings"
+
+// IsValidEmailAddress 检查是否是有效的邮箱地址
+func IsValidEmailAddress(str string) bool {
+	ars := strings.Split(str, "@")
+	if len(ars) != 2 {
+		return false
+	}
+	return strings.Contains(ars[1], ".")
+}

+ 42 - 0
server/utils/address/address_test.go

@@ -0,0 +1,42 @@
+package address
+
+import "testing"
+
+func TestIsValidEmailAddress(t *testing.T) {
+	type args struct {
+		str string
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			"",
+			args{"test@qq.com"},
+			true,
+		},
+		{
+			"",
+			args{"1000@qq.com"},
+			true,
+		},
+		{
+			"",
+			args{"1000@163.com"},
+			true,
+		},
+		{
+			"",
+			args{"1000@1631com"},
+			false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := IsValidEmailAddress(tt.args.str); got != tt.want {
+				t.Errorf("IsValidEmailAddress() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}

+ 10 - 10
server/utils/async/async.go

@@ -4,20 +4,20 @@ import (
 	"errors"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cast"
-	"pmail/dto"
+	"pmail/utils/context"
 	"runtime/debug"
 	"sync"
 )
 
-type Callback func()
+type Callback func(params any)
 
 type Async struct {
 	wg        *sync.WaitGroup
 	lastError error
-	ctx       *dto.Context
+	ctx       *context.Context
 }
 
-func New(ctx *dto.Context) *Async {
+func New(ctx *context.Context) *Async {
 	return &Async{
 		ctx: ctx,
 	}
@@ -27,25 +27,25 @@ func (as *Async) LastError() error {
 	return as.lastError
 }
 
-func (as *Async) WaitProcess(callback Callback) {
+func (as *Async) WaitProcess(callback Callback, params any) {
 	if as.wg == nil {
 		as.wg = &sync.WaitGroup{}
 	}
 	as.wg.Add(1)
-	as.Process(func() {
+	as.Process(func(params any) {
 		defer as.wg.Done()
-		callback()
-	})
+		callback(params)
+	}, params)
 }
 
-func (as *Async) Process(callback Callback) {
+func (as *Async) Process(callback Callback, params any) {
 	go func() {
 		defer func() {
 			if err := recover(); err != nil {
 				as.lastError = as.HandleErrRecover(err)
 			}
 		}()
-		callback()
+		callback(params)
 	}()
 }
 

+ 6 - 5
server/dto/context.go → server/utils/context/context.go

@@ -1,8 +1,7 @@
-package dto
+package context
 
 import (
 	"context"
-	"pmail/models"
 )
 
 const (
@@ -11,9 +10,11 @@ const (
 
 type Context struct {
 	context.Context
-	UserInfo *models.User
-	values   map[string]any
-	Lang     string
+	UserID      int
+	UserAccount string
+	UserName    string
+	values      map[string]any
+	Lang        string
 }
 
 func (c *Context) SetValue(key string, value any) {

+ 10 - 0
server/utils/password/encode_test.go

@@ -0,0 +1,10 @@
+package password
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestEncode(t *testing.T) {
+	fmt.Println(Encode("admin"))
+}

+ 260 - 0
server/utils/send/send.go

@@ -0,0 +1,260 @@
+package send
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"errors"
+	log "github.com/sirupsen/logrus"
+	"net"
+	"pmail/dto/parsemail"
+	"pmail/utils/array"
+	"pmail/utils/async"
+	"pmail/utils/context"
+	"pmail/utils/smtp"
+	"strings"
+)
+
+type mxDomain struct {
+	domain string
+	mxHost string
+}
+
+// Forward 转发邮件
+func Forward(ctx *context.Context, e *parsemail.Email, forwardAddress string) error {
+
+	b := e.ForwardBuildBytes(ctx, forwardAddress)
+
+	var to []*parsemail.User
+	to = []*parsemail.User{
+		{EmailAddress: forwardAddress},
+	}
+
+	// 按域名整理
+	toByDomain := map[mxDomain][]*parsemail.User{}
+	for _, s := range to {
+		args := strings.Split(s.EmailAddress, "@")
+		if len(args) == 2 {
+			//查询dns mx记录
+			mxInfo, err := net.LookupMX(args[1])
+			address := mxDomain{
+				domain: "smtp." + args[1],
+				mxHost: "smtp." + args[1],
+			}
+			if err != nil {
+				log.WithContext(ctx).Errorf(s.EmailAddress, "域名mx记录查询失败")
+			}
+			if len(mxInfo) > 0 {
+				address = mxDomain{
+					domain: args[1],
+					mxHost: mxInfo[0].Host,
+				}
+			}
+			toByDomain[address] = append(toByDomain[address], s)
+		} else {
+			log.WithContext(ctx).Errorf("邮箱地址解析错误! %s", s)
+			continue
+		}
+	}
+
+	var errEmailAddress []string
+
+	errMap := map[string]error{}
+
+	as := async.New(ctx)
+	for domain, tos := range toByDomain {
+		domain := domain
+		tos := tos
+		as.WaitProcess(func(p any) {
+
+			// 先使用smtps协议尝试
+			err := smtp.SendMailWithTls("", domain.mxHost+":465", nil, e.From.EmailAddress, buildAddress(tos), b)
+			if err != nil {
+				// smtps发送失败,尝试smtp
+				err = smtp.SendMail("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
+			}
+
+			// 重新选取证书域名
+			if err != nil {
+				if certificateErr, ok := err.(*tls.CertificateVerificationError); ok {
+					if hostnameErr, is := certificateErr.Err.(x509.HostnameError); is {
+						if hostnameErr.Certificate != nil {
+							certificateHostName := hostnameErr.Certificate.DNSNames
+							// 先使用smtps协议尝试
+							err = smtp.SendMailWithTls(domainMatch(domain.domain, certificateHostName), domain.mxHost+":465", nil, e.From.EmailAddress, buildAddress(tos), b)
+							if err != nil {
+								log.Infoln(err)
+								// smtps发送失败,尝试smtp
+								err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
+							}
+						}
+					}
+				}
+			}
+
+			if err != nil {
+				log.WithContext(ctx).Errorf("%v 邮件投递失败%+v", tos, err)
+				for _, user := range tos {
+					errEmailAddress = append(errEmailAddress, user.EmailAddress)
+
+				}
+			}
+			errMap[domain.domain] = err
+		}, nil)
+	}
+	as.Wait()
+
+	if len(errEmailAddress) > 0 {
+		return errors.New("以下收件人投递失败:" + array.Join(errEmailAddress, ","))
+	}
+	return nil
+}
+
+func Send(ctx *context.Context, e *parsemail.Email) (error, map[string]error) {
+
+	b := e.BuildBytes(ctx)
+
+	var to []*parsemail.User
+	to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
+
+	// 按域名整理
+	toByDomain := map[mxDomain][]*parsemail.User{}
+	for _, s := range to {
+		args := strings.Split(s.EmailAddress, "@")
+		if len(args) == 2 {
+			//查询dns mx记录
+			mxInfo, err := net.LookupMX(args[1])
+			address := mxDomain{
+				domain: "smtp." + args[1],
+				mxHost: "smtp." + args[1],
+			}
+			if err != nil {
+				log.WithContext(ctx).Errorf(s.EmailAddress, "域名mx记录查询失败")
+			}
+			if len(mxInfo) > 0 {
+				address = mxDomain{
+					domain: args[1],
+					mxHost: mxInfo[0].Host,
+				}
+			}
+			toByDomain[address] = append(toByDomain[address], s)
+		} else {
+			log.WithContext(ctx).Errorf("邮箱地址解析错误! %s", s)
+			continue
+		}
+	}
+
+	var errEmailAddress []string
+
+	errMap := map[string]error{}
+
+	as := async.New(ctx)
+	for domain, tos := range toByDomain {
+		domain := domain
+		tos := tos
+		as.WaitProcess(func(p any) {
+
+			// 先使用smtps协议尝试
+			err := smtp.SendMailWithTls("", domain.mxHost+":465", nil, e.From.EmailAddress, buildAddress(tos), b)
+			if err != nil {
+				// smtps发送失败,尝试smtp
+				err = smtp.SendMail("", domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
+			}
+
+			// 重新选取证书域名
+			if err != nil {
+				if certificateErr, ok := err.(*tls.CertificateVerificationError); ok {
+					if hostnameErr, is := certificateErr.Err.(x509.HostnameError); is {
+						if hostnameErr.Certificate != nil {
+							certificateHostName := hostnameErr.Certificate.DNSNames
+							// 先使用smtps协议尝试
+							err = smtp.SendMailWithTls(domainMatch(domain.domain, certificateHostName), domain.mxHost+":465", nil, e.From.EmailAddress, buildAddress(tos), b)
+							if err != nil {
+								// smtps发送失败,尝试smtp
+								err = smtp.SendMail(domainMatch(domain.domain, certificateHostName), domain.mxHost+":25", nil, e.From.EmailAddress, buildAddress(tos), b)
+							}
+						}
+					}
+				}
+			}
+
+			if err != nil {
+				log.WithContext(ctx).Errorf("%v 邮件投递失败%+v", tos, err)
+				for _, user := range tos {
+					errEmailAddress = append(errEmailAddress, user.EmailAddress)
+
+				}
+			}
+			errMap[domain.domain] = err
+		}, nil)
+	}
+	as.Wait()
+
+	if len(errEmailAddress) > 0 {
+		return errors.New("以下收件人投递失败:" + array.Join(errEmailAddress, ",")), errMap
+	}
+	return nil, errMap
+
+}
+
+func buildAddress(u []*parsemail.User) []string {
+	var ret []string
+
+	for _, user := range u {
+		ret = append(ret, user.EmailAddress)
+
+	}
+
+	return ret
+}
+
+func domainMatch(domain string, dnsNames []string) string {
+	secondMatch := ""
+
+	for _, name := range dnsNames {
+		if strings.Contains(name, "smtp") {
+			secondMatch = name
+		}
+
+		if name == domain {
+			return name
+		}
+		if strings.Contains(name, "*") {
+			nameArg := strings.Split(name, ".")
+			domainArg := strings.Split(domain, ".")
+			match := true
+			for i := 0; i < len(nameArg); i++ {
+				if nameArg[len(nameArg)-1-i] == "*" {
+					continue
+				}
+				if len(domainArg) > i {
+					if nameArg[len(nameArg)-1-i] == domainArg[len(domainArg)-1-i] {
+						continue
+					}
+				}
+				match = false
+				break
+			}
+
+			for i := 0; i < len(domainArg); i++ {
+				if len(nameArg) > i && nameArg[len(nameArg)-1-i] == domainArg[len(domainArg)-1-i] {
+					continue
+				}
+				if len(nameArg) > i && nameArg[len(nameArg)-1-i] == "*" {
+					continue
+				}
+
+				match = false
+				break
+			}
+			if match {
+				return domain
+			}
+		}
+	}
+
+	if secondMatch != "" {
+		return strings.ReplaceAll(secondMatch, "*.", "")
+	}
+
+	return strings.ReplaceAll(dnsNames[0], "*.", "")
+}

+ 51 - 0
server/utils/send/send_test.go

@@ -0,0 +1,51 @@
+package send
+
+import (
+	log "github.com/sirupsen/logrus"
+	"os"
+	"pmail/config"
+	"pmail/dto/parsemail"
+	"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()
+	parsemail.Init()
+}
+func TestSend(t *testing.T) {
+	testInit()
+	e := &parsemail.Email{
+		From: &parsemail.User{
+			Name:         "发送人",
+			EmailAddress: "j@jinnrry.com",
+		},
+		To: []*parsemail.User{
+			{"ok@jinnrry.com", "名"},
+		},
+		Subject: "插件测试",
+		Text:    []byte("这是Text"),
+		HTML:    []byte("<div>这是Html</div>"),
+	}
+	Send(nil, e)
+}

+ 66 - 3
server/utils/smtp/smtp.go

@@ -15,6 +15,8 @@
 // Some external packages provide more functionality. See:
 //
 //	https://godoc.org/?q=smtp
+//
+// 在go原始SMTP协议的基础上修复了TLS验证错误、支持了SMTPS协议
 package smtp
 
 import (
@@ -26,7 +28,6 @@ import (
 	"net"
 	"net/smtp"
 	"net/textproto"
-	"pmail/config"
 	"strings"
 )
 
@@ -61,6 +62,22 @@ func Dial(addr string) (*Client, error) {
 	return NewClient(conn, host)
 }
 
+// with tls
+func DialTls(addr, domain string) (*Client, error) {
+	// TLS config
+	tlsconfig := &tls.Config{
+		InsecureSkipVerify: true,
+		ServerName:         domain,
+	}
+
+	conn, err := tls.Dial("tcp", addr, tlsconfig)
+	if err != nil {
+		return nil, err
+	}
+	host, _, _ := net.SplitHostPort(addr)
+	return NewClient(conn, host)
+}
+
 // NewClient returns a new Client using an existing connection and host as a
 // server name to be used when authenticating.
 func NewClient(conn net.Conn, host string) (*Client, error) {
@@ -70,7 +87,7 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
 		text.Close()
 		return nil, err
 	}
-	c := &Client{Text: text, conn: conn, serverName: host, localName: config.Instance.Domain}
+	c := &Client{Text: text, conn: conn, serverName: host, localName: "jinnrry.com"}
 	_, c.tls = conn.(*tls.Conn)
 	return c, nil
 }
@@ -306,7 +323,53 @@ func (c *Client) Data() (io.WriteCloser, error) {
 	return &dataCloser{c, c.Text.DotWriter()}, nil
 }
 
-var testHookStartTLS func(*tls.Config) // nil, except for tests
+func SendMailWithTls(domain string, addr string, a smtp.Auth, from string, to []string, msg []byte) error {
+	if err := validateLine(from); err != nil {
+		return err
+	}
+	for _, recp := range to {
+		if err := validateLine(recp); err != nil {
+			return err
+		}
+	}
+	c, err := DialTls(addr, domain)
+	if err != nil {
+		return err
+	}
+	defer c.Close()
+	if err = c.hello(); err != nil {
+		return err
+	}
+	if a != nil && c.ext != nil {
+		if _, ok := c.ext["AUTH"]; !ok {
+			return errors.New("smtp: server doesn't support AUTH")
+		}
+		if err = c.Auth(a); err != nil {
+			return err
+		}
+	}
+	if err = c.Mail(from); err != nil {
+		return err
+	}
+	for _, addr := range to {
+		if err = c.Rcpt(addr); err != nil {
+			return err
+		}
+	}
+	w, err := c.Data()
+	if err != nil {
+		return err
+	}
+	_, err = w.Write(msg)
+	if err != nil {
+		return err
+	}
+	err = w.Close()
+	if err != nil {
+		return err
+	}
+	return c.Quit()
+}
 
 // SendMail connects to the server at addr, switches to TLS if
 // possible, authenticates with the optional mechanism a if possible,