木木的木头 před 2 roky
rodič
revize
f4689d073c
72 změnil soubory, kde provedl 1914 přidání a 2860 odebrání
  1. 2 3
      Dockerfile
  2. 35 1
      README.md
  3. 33 1
      README_CN.md
  4. 0 2505
      fe/package-lock.json
  5. 8 4
      fe/src/components/HomeHeader.vue
  6. 237 0
      fe/src/components/RuleSettings.vue
  7. 5 0
      fe/src/components/SecuritySettings.vue
  8. 44 2
      fe/src/i18n/i18n.js
  9. 8 0
      fe/src/views/ListView.vue
  10. 3 3
      fe/src/views/SetupView.vue
  11. 4 1
      server/config/config.dev.json
  12. 5 2
      server/config/config.go
  13. 5 2
      server/config/config.json
  14. 3 0
      server/config/config_mysql.json
  15. 1 1
      server/config/tables/mysql/email.sql
  16. 10 0
      server/config/tables/mysql/rule.sql
  17. 10 0
      server/config/tables/sqlite/rule.sql
  18. 3 3
      server/controllers/attachments.go
  19. 2 2
      server/controllers/base.go
  20. 2 2
      server/controllers/email/delete.go
  21. 2 2
      server/controllers/email/detail.go
  22. 16 14
      server/controllers/email/list.go
  23. 2 2
      server/controllers/email/move.go
  24. 2 2
      server/controllers/email/read.go
  25. 5 5
      server/controllers/email/send.go
  26. 6 5
      server/controllers/group.go
  27. 2 2
      server/controllers/login.go
  28. 2 2
      server/controllers/ping.go
  29. 89 0
      server/controllers/rule.go
  30. 3 3
      server/controllers/settings.go
  31. 2 2
      server/controllers/setup.go
  32. 3 3
      server/db/init.go
  33. 83 2
      server/dto/parsemail/email.go
  34. 54 0
      server/dto/rule.go
  35. 3 3
      server/hooks/base.go
  36. 4 4
      server/hooks/telegram_push/telegram_push.go
  37. 20 6
      server/hooks/wechat_push/wechat_push.go
  38. 9 2
      server/http_server/http_server.go
  39. 22 7
      server/http_server/https_server.go
  40. 6 0
      server/http_server/setup_server.go
  41. 24 22
      server/i18n/i18n.go
  42. 4 5
      server/main.go
  43. 1 1
      server/models/email.go
  44. 35 0
      server/models/rule.go
  45. 3 3
      server/services/attachments/attachments.go
  46. 4 4
      server/services/auth/auth.go
  47. 4 3
      server/services/del_email/del_email.go
  48. 2 2
      server/services/detail/detail.go
  49. 11 10
      server/services/group/group.go
  50. 8 5
      server/services/list/list.go
  51. 51 0
      server/services/rule/match/base.go
  52. 24 0
      server/services/rule/match/contains_match.go
  53. 23 0
      server/services/rule/match/equal_match.go
  54. 32 0
      server/services/rule/match/regex_match.go
  55. 18 0
      server/services/rule/match/regex_match_test.go
  56. 81 0
      server/services/rule/rule.go
  57. 9 5
      server/services/setup/db.go
  58. 2 2
      server/services/setup/dns.go
  59. 2 2
      server/services/setup/finish.go
  60. 1 1
      server/session/init.go
  61. 31 2
      server/smtp_server/read_content.go
  62. 349 0
      server/smtp_server/read_content_test.go
  63. 0 160
      server/smtp_server/send.go
  64. 0 24
      server/smtp_server/send_test.go
  65. 0 0
      server/smtp_server/smtp.go
  66. 12 0
      server/utils/address/address.go
  67. 42 0
      server/utils/address/address_test.go
  68. 3 3
      server/utils/async/async.go
  69. 6 5
      server/utils/context/context.go
  70. 260 0
      server/utils/send/send.go
  71. 51 0
      server/utils/send/send_test.go
  72. 66 3
      server/utils/smtp/smtp.go

+ 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

+ 35 - 1
README.md

@@ -49,7 +49,11 @@ beautiful and cute Logo for this project!
 
 ## 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
 
@@ -70,6 +74,36 @@ and restart the service.
 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

+ 33 - 1
README_CN.md

@@ -53,7 +53,11 @@ PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱
 
 ## 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、配置
 
@@ -70,6 +74,34 @@ PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱
 ## 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的时候会进入安装引导流程 
+}
+```
+
 # 参与开发
 
 ## 项目架构

+ 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"
-      }
-    }
-  }
-}

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

@@ -9,13 +9,17 @@
             </el-icon>
         </div>
         <el-drawer v-model="openSettings" size="80%" :title="lang.settings">
-            <el-tabs tab-position="left" >
+            <el-tabs tab-position="left">
                 <el-tab-pane :label="lang.security">
-                    <SecuritySettings/>
+                    <SecuritySettings />
                 </el-tab-pane>
 
                 <el-tab-pane :label="lang.group_settings">
-                    <GroupSettings/>
+                    <GroupSettings />
+                </el-tab-pane>
+
+                <el-tab-pane :label="lang.rule_setting">
+                    <RuleSettings />
                 </el-tab-pane>
             </el-tabs>
         </el-drawer>
@@ -30,7 +34,7 @@ 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',

+ 44 - 2
fe/src/i18n/i18n.js

@@ -64,7 +64,28 @@ var lang = {
     "move_email_confirm": "Are you sure you want to move them?",
     "del_btn": "Delete",
     "move_btn": "Move",
-    "read_btn": "Readed"
+    "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",
 };
 
 
@@ -135,7 +156,28 @@ var zhCN = {
     "move_email_confirm": "你确定要移动这些邮件吗?",
     "del_btn": "删除",
     "move_btn": "移动",
-    "read_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) {

+ 8 - 0
fe/src/views/ListView.vue

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

+ 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": ""
 })
 

+ 4 - 1
server/config/config.dev.json

@@ -6,8 +6,11 @@
   "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": "",

+ 5 - 2
server/config/config.go

@@ -20,6 +20,10 @@ 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"`
@@ -27,7 +31,6 @@ type Config struct {
 	TgBotToken           string            `json:"tgBotToken"`
 	TgChatId             string            `json:"tgChatId"`
 	IsInit               bool              `json:"isInit"`
-	HttpsEnabled         int               `json:"httpsEnabled"` //后台页面是否启用https,0默认(启用),1启用,2不启用
 	Tables               map[string]string `json:"-"`
 	TablesInitData       map[string]string `json:"-"`
 }
@@ -35,7 +38,7 @@ type Config struct {
 //go:embed tables/*
 var tableConfig embed.FS
 
-const Version = "2.1.2"
+const Version = "2.2.0"
 
 const DBTypeMySQL = "mysql"
 const DBTypeSQLite = "sqlite"

+ 5 - 2
server/config/config.json

@@ -6,8 +6,11 @@
   "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": "",
@@ -15,5 +18,5 @@
   "tgChatId": "",
   "tgBotToken": "",
   "isInit": true,
-  "httpsEnabled": 2
+  "httpsEnabled": 1
 }

+ 3 - 0
server/config/config_mysql.json

@@ -8,6 +8,9 @@
   "SSLPublicKeyPath": "config/ssl/public.crt",
   "dbDSN": "root:root@tcp(127.0.0.1:3306)/pmail?parseTime=True&loc=Local",
   "dbType": "mysql",
+  "spamFilterLevel": 2,
+  "httpPort": 80,
+  "httpsPort": 443,
   "weChatPushAppId": "",
   "weChatPushSecret": "",
   "weChatPushTemplateId": "",

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

@@ -16,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 '错误信息记录',

+ 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 '收信规则表'

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

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

@@ -5,16 +5,16 @@ import (
 	log "github.com/sirupsen/logrus"
 	"io"
 	"net/http"
-	"pmail/dto"
 	"pmail/dto/response"
 	"pmail/services/del_email"
+	"pmail/utils/context"
 )
 
 type emailDeleteRequest struct {
 	IDs []int `json:"ids"`
 }
 
-func EmailDelete(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+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)

+ 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,
 		})
 	}
 

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

@@ -5,9 +5,9 @@ import (
 	log "github.com/sirupsen/logrus"
 	"io"
 	"net/http"
-	"pmail/dto"
 	"pmail/dto/response"
 	"pmail/services/group"
+	"pmail/utils/context"
 )
 
 type moveRequest struct {
@@ -15,7 +15,7 @@ type moveRequest struct {
 	IDs     []int `json:"ids"`
 }
 
-func Move(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+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)

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

@@ -5,16 +5,16 @@ import (
 	log "github.com/sirupsen/logrus"
 	"io"
 	"net/http"
-	"pmail/dto"
 	"pmail/dto/response"
 	"pmail/services/detail"
+	"pmail/utils/context"
 )
 
 type markReadRequest struct {
 	IDs []int `json:"ids"`
 }
 
-func MarkRead(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+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)

+ 5 - 5
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)
@@ -157,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()
@@ -170,7 +170,7 @@ func Send(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 
 	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 {

+ 6 - 5
server/controllers/group.go

@@ -11,14 +11,15 @@ import (
 	"pmail/i18n"
 	"pmail/services/group"
 	"pmail/utils/array"
+	"pmail/utils/context"
 )
 
-func GetUserGroupList(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+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 := []*group.GroupItem{
 		{
@@ -50,7 +51,7 @@ type addGroupRequest struct {
 	ParentId int    `json:"parent_id"`
 }
 
-func AddGroup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+func AddGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
 	var reqData *addGroupRequest
 	reqBytes, err := io.ReadAll(req.Body)
 	if err != nil {
@@ -61,7 +62,7 @@ func AddGroup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 		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.UserInfo.ID)
+	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
@@ -78,7 +79,7 @@ type delGroupRequest struct {
 	Id int `json:"id"`
 }
 
-func DelGroup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+func DelGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
 	var reqData *delGroupRequest
 	reqBytes, err := io.ReadAll(req.Body)
 	if err != nil {

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

+ 3 - 3
server/db/init.go

@@ -7,7 +7,7 @@ import (
 	log "github.com/sirupsen/logrus"
 	_ "modernc.org/sqlite"
 	"pmail/config"
-	"pmail/dto"
+	"pmail/utils/context"
 	"pmail/utils/errors"
 	"strings"
 )
@@ -38,9 +38,9 @@ func Init() error {
 	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

+ 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 - 3
server/hooks/base.go

@@ -1,17 +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 接收到邮件,解析之后的结构化数据

+ 4 - 4
server/hooks/telegram_push/telegram_push.go

@@ -5,8 +5,8 @@ import (
 	"fmt"
 	"net/http"
 	"pmail/config"
-	"pmail/dto"
 	"pmail/dto/parsemail"
+	"pmail/utils/context"
 	"strings"
 
 	log "github.com/sirupsen/logrus"
@@ -19,11 +19,11 @@ type TelegramPushHook struct {
 	webDomain    string
 }
 
-func (w *TelegramPushHook) SendBefore(ctx *dto.Context, email *parsemail.Email) {
+func (w *TelegramPushHook) SendBefore(ctx *context.Context, email *parsemail.Email) {
 
 }
 
-func (w *TelegramPushHook) SendAfter(ctx *dto.Context, email *parsemail.Email, err map[string]error) {
+func (w *TelegramPushHook) SendAfter(ctx *context.Context, email *parsemail.Email, err map[string]error) {
 
 }
 
@@ -55,7 +55,7 @@ type InlineKeyboardButton struct {
 	URL  string `json:"url"`
 }
 
-func (w *TelegramPushHook) sendUserMsg(ctx *dto.Context, email *parsemail.Email) {
+func (w *TelegramPushHook) sendUserMsg(ctx *context.Context, email *parsemail.Email) {
 	url := w.webDomain
 	if w.httpsEnabled > 1 {
 		url = "http://" + url

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

+ 9 - 2
server/http_server/http_server.go

@@ -11,8 +11,6 @@ import (
 	"time"
 )
 
-const HttpPort = 80
-
 // 这个服务是为了拦截http请求转发到https
 var httpServer *http.Server
 
@@ -25,6 +23,11 @@ func HttpStop() {
 func HttpStart() {
 	mux := http.NewServeMux()
 
+	HttpPort := 80
+	if config.Instance.HttpPort > 0 {
+		HttpPort = config.Instance.HttpPort
+	}
+
 	if config.Instance.HttpsEnabled != 2 {
 		mux.HandleFunc("/", controllers.Interceptor)
 		httpServer = &http.Server{
@@ -52,6 +55,10 @@ func HttpStart() {
 		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{

+ 22 - 7
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 {
@@ -62,12 +61,21 @@ func HttpsStart() {
 	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)
 
+	HttpsPort := 443
+	if config.Instance.HttpsPort > 0 {
+		HttpsPort = config.Instance.HttpsPort
+	}
+
 	if config.Instance.HttpsEnabled != 2 {
 		httpsServer = &http.Server{
 			Addr:         fmt.Sprintf(":%d", HttpsPort),
@@ -117,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"
@@ -128,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 ctx.UserInfo == nil || ctx.UserInfo.ID == 0 {
+			if userInfo != nil && userInfo.ID > 0 {
+				ctx.UserID = userInfo.ID
+				ctx.UserName = userInfo.Name
+				ctx.UserAccount = userInfo.Account
+			}
+
+			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!",
 	}
 )
 

+ 4 - 5
server/main.go

@@ -6,8 +6,9 @@ import (
 	log "github.com/sirupsen/logrus"
 	"os"
 	"pmail/config"
-	"pmail/dto"
+	"pmail/cron_server"
 	"pmail/res_init"
+	"pmail/utils/context"
 	"time"
 )
 
@@ -21,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)
@@ -38,8 +39,6 @@ var (
 
 func main() {
 	// 设置日志格式为json格式
-	//log.SetFormatter(&log.JSONFormatter{})
-
 	log.SetFormatter(&logFormatter{})
 	log.SetReportCaller(true)
 
@@ -79,7 +78,7 @@ func main() {
 	log.Infoln("***************************************************")
 
 	// 定时任务启动
-	//go cron_server.Start()
+	go cron_server.Start()
 
 	// 核心服务启动
 	res_init.Init()

+ 1 - 1
server/models/email.go

@@ -23,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"`

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

+ 4 - 3
server/services/del_email/del_email.go

@@ -3,14 +3,14 @@ package del_email
 import (
 	"fmt"
 	"pmail/db"
-	"pmail/dto"
 	"pmail/models"
 	"pmail/services/auth"
 	"pmail/utils/array"
+	"pmail/utils/context"
 	"pmail/utils/errors"
 )
 
-func DelEmail(ctx *dto.Context, ids []int) error {
+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, ","))))
@@ -23,7 +23,8 @@ func DelEmail(ctx *dto.Context, ids []int) error {
 		}
 	}
 
-	_, 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("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)
 	}

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

+ 11 - 10
server/services/group/group.go

@@ -7,6 +7,7 @@ import (
 	"pmail/dto"
 	"pmail/models"
 	"pmail/utils/array"
+	"pmail/utils/context"
 	"pmail/utils/errors"
 )
 
@@ -17,7 +18,7 @@ type GroupItem struct {
 	Children []*GroupItem `json:"children"`
 }
 
-func DelGroup(ctx *dto.Context, groupId int) (bool, error) {
+func DelGroup(ctx *context.Context, groupId int) (bool, error) {
 	allGroupIds := getAllChildId(ctx, groupId)
 	allGroupIds = append(allGroupIds, groupId)
 
@@ -27,7 +28,7 @@ func DelGroup(ctx *dto.Context, groupId int) (bool, error) {
 		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.UserInfo.ID)
+	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)
@@ -53,10 +54,10 @@ type id struct {
 	Id int `db:"id"`
 }
 
-func getAllChildId(ctx *dto.Context, rootId int) []int {
+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.UserInfo.ID)
+	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)
@@ -65,12 +66,12 @@ func getAllChildId(ctx *dto.Context, rootId int) []int {
 }
 
 // GetGroupInfoList 获取全部的分组
-func GetGroupInfoList(ctx *dto.Context) []*GroupItem {
+func GetGroupInfoList(ctx *context.Context) []*GroupItem {
 	return buildChildren(ctx, 0)
 }
 
 // MoveMailToGroup 将某封邮件移动到某个分组中
-func MoveMailToGroup(ctx *dto.Context, mailId []int, groupId int) bool {
+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)
@@ -85,10 +86,10 @@ func MoveMailToGroup(ctx *dto.Context, mailId []int, groupId int) bool {
 	return rowNum > 0
 }
 
-func buildChildren(ctx *dto.Context, parentId int) []*GroupItem {
+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.UserInfo.ID)
+	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)
@@ -107,8 +108,8 @@ func buildChildren(ctx *dto.Context, parentId int) []*GroupItem {
 
 }
 
-func GetGroupList(ctx *dto.Context) []*models.Group {
+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.UserInfo.ID)
+	db.Instance.Select(&ret, db.WithContext(ctx, "select * from `group` where user_id=?"), ctx.UserID)
 	return ret
 }

+ 8 - 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,8 @@ 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 {
@@ -58,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
 

+ 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" {

+ 31 - 2
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"
@@ -57,6 +59,17 @@ 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 {
@@ -68,7 +81,19 @@ func (s *Session) Data(r io.Reader) error {
 	}
 	as2.Wait()
 
-	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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+	// 执行邮件规则
+	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,is_read,status,group_id) VALUES (?,?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
 	_, err = db.Instance.Exec(sql,
 		email.Date,
 		email.Subject,
@@ -84,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(p any) {
-
-			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
-		}, 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], "*.", "")
-}

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

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

@@ -4,7 +4,7 @@ import (
 	"errors"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cast"
-	"pmail/dto"
+	"pmail/utils/context"
 	"runtime/debug"
 	"sync"
 )
@@ -14,10 +14,10 @@ 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,
 	}

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

+ 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,