jinnrry 2 роки тому
коміт
494940afc9
87 змінених файлів з 9589 додано та 0 видалено
  1. 4 0
      .gitignore
  2. 36 0
      Dockerfile
  3. 110 0
      README.md
  4. 125 0
      README_CN.md
  5. 29 0
      build.sh
  6. 11 0
      fe/.eslintrc.cjs
  7. 28 0
      fe/.gitignore
  8. 3 0
      fe/.vscode/extensions.json
  9. 25 0
      fe/README.md
  10. 13 0
      fe/index.html
  11. 2505 0
      fe/package-lock.json
  12. 28 0
      fe/package.json
  13. BIN
      fe/public/favicon.ico
  14. 55 0
      fe/src/App.vue
  15. 78 0
      fe/src/assets/base.css
  16. 1 0
      fe/src/assets/logo.svg
  17. 13 0
      fe/src/assets/main.css
  18. 64 0
      fe/src/components/HomeAside.vue
  19. 67 0
      fe/src/components/HomeHeader.vue
  20. 54 0
      fe/src/components/SecuritySettings.vue
  21. 92 0
      fe/src/http/http.js
  22. 90 0
      fe/src/i18n/i18n.js
  23. 16 0
      fe/src/main.js
  24. 37 0
      fe/src/router/index.js
  25. 11 0
      fe/src/stores/group.js
  26. 280 0
      fe/src/views/EditerView.vue
  27. 100 0
      fe/src/views/EmailDetailView.vue
  28. 156 0
      fe/src/views/ListView.vue
  29. 63 0
      fe/src/views/LoginView.vue
  30. 32 0
      fe/vite.config.js
  31. 1908 0
      fe/yarn.lock
  32. 11 0
      server/config/config.dev.json
  33. 79 0
      server/config/config.go
  34. 11 0
      server/config/config.json
  35. 1 0
      server/config/dkim/README
  36. 0 0
      server/config/dkim/dkim.priv
  37. 0 0
      server/config/dkim/dkim.public
  38. 0 0
      server/config/ssl/private.key
  39. 0 0
      server/config/ssl/public.crt
  40. 2 0
      server/config/tables/data/user.sql
  41. 2 0
      server/config/tables/data/user_auth.sql
  42. 26 0
      server/config/tables/email.sql
  43. 7 0
      server/config/tables/sessions.sql
  44. 8 0
      server/config/tables/user.sql
  45. 8 0
      server/config/tables/user_auth.sql
  46. 50 0
      server/controllers/attachments.go
  47. 8 0
      server/controllers/base.go
  48. 49 0
      server/controllers/email/detail.go
  49. 85 0
      server/controllers/email/list.go
  50. 203 0
      server/controllers/email/send.go
  51. 39 0
      server/controllers/group.go
  52. 59 0
      server/controllers/login.go
  53. 13 0
      server/controllers/ping.go
  54. 41 0
      server/controllers/settings.go
  55. 32 0
      server/dto/context.go
  56. 97 0
      server/dto/parsemail/dkim.go
  57. 247 0
      server/dto/parsemail/email.go
  58. 41 0
      server/dto/parsemail/email_decode_test.go
  59. 43 0
      server/dto/parsemail/email_test.go
  60. 426 0
      server/dto/parsemail/encodedword.go
  61. 37 0
      server/dto/response/response.go
  62. 13 0
      server/dto/tag.go
  63. 28 0
      server/go.mod
  64. 99 0
      server/go.sum
  65. 28 0
      server/hooks/base.go
  66. 108 0
      server/hooks/wechat_push/wechat_push.go
  67. 19 0
      server/hooks/wechat_push/wechat_push_test.go
  68. 133 0
      server/http_server/main.go
  69. 41 0
      server/i18n/i18n.go
  70. 81 0
      server/main.go
  71. 8 0
      server/models/User.go
  72. 7 0
      server/models/auth.go
  73. 79 0
      server/models/email.go
  74. 87 0
      server/mysql/init.go
  75. 60 0
      server/services/attachments/attachments.go
  76. 33 0
      server/services/auth/auth.go
  77. 43 0
      server/services/detail/detail.go
  78. 60 0
      server/services/list/list.go
  79. 19 0
      server/session/init.go
  80. 72 0
      server/smtp_server/main.go
  81. 118 0
      server/smtp_server/read_content.go
  82. 72 0
      server/smtp_server/read_content_test.go
  83. 160 0
      server/smtp_server/send.go
  84. 24 0
      server/smtp_server/send_test.go
  85. 19 0
      server/utils/array/array.go
  86. 71 0
      server/utils/async/async.go
  87. 448 0
      server/utils/smtp/smtp.go

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+.idea
+.DS_Store
+dist
+output

+ 36 - 0
Dockerfile

@@ -0,0 +1,36 @@
+FROM node:lts-alpine as febuild
+WORKDIR /work
+
+COPY fe .
+
+RUN yarn && yarn build
+
+
+FROM golang:alpine as serverbuild
+
+WORKDIR /work
+
+COPY server .
+COPY --from=febuild /work/dist /work/http_server/dist
+
+RUN apk update && apk add git
+RUN go build -ldflags "-X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail main.go
+
+
+FROM alpine
+
+WORKDIR /work
+
+# 设置时区
+RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
+RUN apk add --no-cache tzdata \
+    && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
+    && echo "Asia/Shanghai" > /etc/timezone \
+    &&rm -rf /var/cache/apk/* /tmp/* /var/tmp/* $HOME/.cache
+
+
+COPY --from=serverbuild /work/pmail .
+COPY server/config/dkim ./config/dkim/
+COPY server/config/config.json ./config/
+
+CMD /work/pmail

+ 110 - 0
README.md

@@ -0,0 +1,110 @@
+# PMail 
+
+> The current code is not stable, be sure to record the log! Lost letters or letters parsed wrong can find out the original content of the mail from the log!
+
+## [中文文档](./README_CN.md)
+
+## Introduction
+
+An extremely lightweight mailbox server designed for personal use scenarios. 
+
+## Features
+
+* Single file operation and easy deployment.
+
+* The binary file is only 15MB and takes up less than 10M of memory during the run.
+
+* Support dkim, spf checksum, [Email Test](https://www.mail-tester.com/) score 10 points if correctly configured.
+
+## Disadvantages
+
+* At present, only the core function of sending and receiving emails has been completed. Basically, it can only be used by a single person, and does not deal with issues related to permission management in the process of multiple users.
+
+* The UI is ugly
+
+# How to run
+
+## 1、Generate DKIM secret key
+
+Generate public and private keys by the dkim-keygen tool of the [go-msgauth](https://github.com/emersion/go-msgauth) project
+
+Put the key in the `config/dkim` directory.
+
+## 2、Set DNS
+
+Add the following records to your domain DNS settings
+
+| type | hostname             | address / value      |
+|------|----------------------|----------------------|
+| A    | smtp                 | server ip            |
+| MX   | _                    | smtp.YourDomain      |
+| TXT  | _                    | v=spf1 a mx ~all     |
+| TXT  | default._domainkey	  | Your DKIM public key |
+
+## 3、Domain SSL Key
+
+Prepare the certificate of `smtp.YourDomain`, the private key in ".key" format and the public key in ".crt" format
+
+Put the certificate in the `config/ssl` directory.
+
+## 4、Build(or download)
+
+1、installed `nodejs` and `yarn`
+
+2、installed `golang`
+
+3、exec `./build.sh`
+
+## 5、Config
+
+Modify the `config.json` file in the config directory and fill in your secret key and domain information.
+
+Tips:
+
+MySQL database name must is `pmail`, and charset must is `utf8_general_ci`.
+
+Configuration file description :
+```json
+{
+  "domain": "demo.com", // Your domain
+  "dkimPrivateKeyPath": "config/dkim/dkim.priv",  // dkim private key
+  "SSLPrivateKeyPath": "config/ssl/private.key",  // ssl private key
+  "SSLPublicKeyPath": "config/ssl/public.crt",    // ssl public key
+  "mysqlDSN": "username:password@tcp(127.0.0.1:3306)/pmail?parseTime=True&loc=Local", // mysql connect infonation
+  "weChatPushAppId": "",  // WeChat public account appid (for new email message push) . If you don't need it, you can make it empty.
+  "weChatPushSecret": "",   // WeChat api secret
+  "weChatPushTemplateId": "",  // push template id
+  "weChatPushUserId": "" // wechat user id
+}
+```
+
+## 6、Run
+
+exec `pmail` and check port of 25、80.
+
+The webmail service address is http://yourip. Default account is `admin` and password is `admin`
+
+## 7、Email Test
+
+Check if your mailbox has completed all the security configuration. It is recommended to use [https://www.mail-tester.com/](https://www.mail-tester.com/) for checking. 
+
+
+# For Developer
+
+## Project Framework
+
+1、 FE: vue3+element-plus
+
+The code is in `fe` folder.
+
+2、Server: golang + mysql
+
+The code is in `server` folder.
+
+## Plugin Development
+
+Reference this file. `server/hooks/wechat_push/wechat_push.go`
+
+# What's More
+
+Welcome PR! Welcome Issues! The project need a Logo !

+ 125 - 0
README_CN.md

@@ -0,0 +1,125 @@
+# PMail 
+
+> Welcome PR! Welcome Issues! 目前代码并不稳定,一定记录好日志!丢信或者信件解析错误可以从日志中找出邮件原始内容!
+
+## 为什么写这个项目
+
+迫于越来越多的邮件服务商暂停了针对个人的域名邮箱服务(比如QQ邮箱、微软Outlook邮箱),因此考虑自建域名邮箱服务。
+但是自建域名邮箱可选的程序并不多,且目标都不是针对个人使用场景设计的。个人服务器一般内存、CPU、硬盘配置都不高,针对公司场景使用的邮箱程序过于臃肿,
+白白浪费资源。就拿我自己的服务器来说,我服务器配置为1核512M 10G硬盘,市面上绝大多数邮箱服务器安装上就把磁盘占满了,根本没法正常使用
+
+## 项目优势
+
+### 1、部署简单
+
+使用Go语言编写,支持跨平台,编译后单文件运行,单文件包含完整的前后端代码。修改配置文件,运行即可。
+
+### 2、资源占用极小
+
+编译后二进制文件仅15MB,运行过程中占用内存10M以内。
+
+### 3、安全方面
+
+支持dkim、spf校验。正确配置的情况下,Email Test得分10分。
+
+## 其他
+
+### 不足
+
+1、目前只完成了最核心的收发邮件功能。基本上仅针对单人使用,没有处理多人使用、权限管理相关问题。
+
+2、目前代码并不稳定,可能存在BUG
+
+3、前端UI交互较差
+
+
+# 如何部署
+
+## 1、生成DKIM 秘钥
+
+使用[go-msgauth](https://github.com/emersion/go-msgauth)项目的dkim-keygen工具生成公钥和私钥
+
+生成以后将密钥放到`config/dkim`目录中
+
+## 2、设置域名DNS
+
+添加以下记录到你到域名解析中
+
+| 类型  | 主机记录                | 记录值              |
+|-----|---------------------|------------------|
+| A   | smtp                | 服务器IP            |
+| MX  | _                   | smtp.你的域名        |
+| TXT | _                   | v=spf1 a mx ~all |
+| TXT | default._domainkey	 | 你生成的DKIM公钥       |
+
+## 3、申请域名证书
+
+准备好 `smtp.你的域名` 的证书,key格式的私钥和crt格式的公钥
+
+放到`config/ssl`目录中
+
+## 4、编译程序(或者直接下载编译好的二进制文件)
+
+1、前端环境:安装好node环境,配置好yarn
+
+2、后端环境:安装最新的golang
+
+3、执行`./build.sh`
+
+## 5、修改配置文件
+
+修改config目录中的`config.json`文件,填入你的秘钥与域名信息
+
+Tips:
+
+MySQL库名必须叫pmail,另外,数据库必须使用utf8_general_ci字符集
+
+配置文件说明:
+```json
+{
+  "domain": "demo.com", // 你的域名
+  "dkimPrivateKeyPath": "config/dkim/dkim.priv",  // dkim私钥
+  "SSLPrivateKeyPath": "config/ssl/private.key",  // ssl证书私钥
+  "SSLPublicKeyPath": "config/ssl/public.crt",    // ssl证书公钥
+  "mysqlDSN": "username:password@tcp(127.0.0.1:3306)/pmail?parseTime=True&loc=Local", // mysql连接信息
+  "weChatPushAppId": "",  //微信公众号id(用于新消息提醒),没有留空即可
+  "weChatPushSecret": "",   // 微信公众号api秘钥
+  "weChatPushTemplateId": "",  // 微信公众号推送模板id
+  "weChatPushUserId": "" // 微信推送用户id
+}
+```
+
+## 6、启动
+
+运行`PMail`程序,检查服务器25、80端口正常即可
+
+邮箱后台, http://yourip,默认账号admin,默认密码admin
+
+## 7、邮箱得分测试
+
+建议找一下邮箱测试服务(比如[https://www.mail-tester.com/](https://www.mail-tester.com/))进行邮件得分检测,避免自己某些步骤漏配,导致发件进对方垃圾箱。
+
+## 8、其他说明
+
+邮件是否进对方垃圾箱与程序无关、与你的服务器IP、服务器域名有关。我自己搭建的服务,测试了收发QQ、Gmail、Outlook、163、126均正常,无任何拦截,且不会进垃圾箱。
+
+
+# 参与开发
+
+## 项目架构
+
+1、前端: vue3+element-plus
+
+前端代码位于`fe`目录中,运行参考`fe`目录中的README文件
+
+2、后端: golang + mysql
+
+后端代码进入`server`文件夹,运行`main.go`文件
+
+## 插件开发
+
+参考微信推送插件`server/hooks/wechat_push/wechat_push.go`
+
+# 最后
+
+欢迎PR! 欢迎Issue!求个Logo!

+ 29 - 0
build.sh

@@ -0,0 +1,29 @@
+# 编译前端代码
+cd fe && yarn && yarn build
+
+# 编译后端代码
+cd ../server && cp -rf ../fe/dist http_server
+
+CGO_ENABLED=0 GOOS=linux GOARCH=amd64
+go build -ldflags "-X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail_linux_amd64  main.go
+
+CGO_ENABLED=0 GOOS=windows GOARCH=amd64
+go build -ldflags "-X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail_windows_amd64  main.go
+
+CGO_ENABLED=0 GOOS=darwin GOARCH=amd64
+go build -ldflags "-X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail_mac_amd64  main.go
+
+CGO_ENABLED=0 GOOS=darwin GOARCH=arm64
+go build -ldflags "-X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail_mac_arm64  main.go
+
+# 整理输出文件
+cd ..
+rm -rf output
+mkdir output
+cd output
+mv ../server/pmail* .
+mkdir config
+cp -r ../server/config/dkim config/
+cp -r ../server/config/ssl config/
+cp -r ../server/config/config.json config/
+cp ../README.md .

+ 11 - 0
fe/.eslintrc.cjs

@@ -0,0 +1,11 @@
+/* eslint-env node */
+module.exports = {
+  root: true,
+  'extends': [
+    'plugin:vue/vue3-essential',
+    'eslint:recommended'
+  ],
+  parserOptions: {
+    ecmaVersion: 'latest'
+  }
+}

+ 28 - 0
fe/.gitignore

@@ -0,0 +1,28 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 3 - 0
fe/.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
+}

+ 25 - 0
fe/README.md

@@ -0,0 +1,25 @@
+# fe
+
+前端代码库
+
+```sh
+yarn
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+yarn dev
+```
+
+### Compile and Minify for Production
+
+```sh
+yarn build
+```
+
+### Lint with [ESLint](https://eslint.org/)
+
+```sh
+yarn lint
+```

+ 13 - 0
fe/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <link rel="icon" href="/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>PMail</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 2505 - 0
fe/package-lock.json

@@ -0,0 +1,2505 @@
+{
+  "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"
+      }
+    }
+  }
+}

+ 28 - 0
fe/package.json

@@ -0,0 +1,28 @@
+{
+  "name": "fe",
+  "version": "0.0.0",
+  "private": true,
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
+  },
+  "dependencies": {
+    "@wangeditor/editor": "^5.1.23",
+    "@wangeditor/editor-for-vue": "^5.1.12",
+    "axios": "^1.4.0",
+    "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"
+  }
+}

BIN
fe/public/favicon.ico


+ 55 - 0
fe/src/App.vue

@@ -0,0 +1,55 @@
+<script setup>
+import { RouterView } from 'vue-router'
+import HomeHeader from '@/components/HomeHeader.vue'
+import HomeAside from '@/components/HomeAside.vue';
+import { watch,ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+const route = useRoute()
+
+const pageName = ref(route.name) 
+
+watch(
+  () => route.fullPath,
+  (n, o) => {
+    pageName.value = route.name
+  }
+)
+
+</script>
+
+<template>
+  <div id="main">
+    <HomeHeader />
+    <div id="content">
+      <div id="aside" v-if="pageName != 'login'">
+        <HomeAside />
+      </div>
+      <div id="body">
+        <RouterView />
+      </div>
+    </div>
+  </div>
+</template>
+
+
+<style scoped>
+#aside {
+  background-color: #F1F1F1;
+}
+
+#body {
+  width: 100%;
+  height: 100%;
+}
+
+#content {
+  display: flex;
+  height: 100%;
+}
+
+#main {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+</style>

+ 78 - 0
fe/src/assets/base.css

@@ -0,0 +1,78 @@
+/* color palette from <https://github.com/vuejs/theme> */
+:root {
+  --vt-c-white: #ffffff;
+  --vt-c-white-soft: #f8f8f8;
+  --vt-c-white-mute: #f2f2f2;
+
+  --vt-c-black: #181818;
+  --vt-c-black-soft: #222222;
+  --vt-c-black-mute: #282828;
+
+  --vt-c-indigo: #2c3e50;
+
+  --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
+  --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
+  --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
+  --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
+
+  --vt-c-text-light-1: var(--vt-c-indigo);
+  --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
+  --vt-c-text-dark-1: var(--vt-c-white);
+  --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
+}
+
+/* semantic color variables for this project */
+:root {
+  --color-background: var(--vt-c-white);
+  --color-background-soft: var(--vt-c-white-soft);
+  --color-background-mute: var(--vt-c-white-mute);
+
+  --color-border: var(--vt-c-divider-light-2);
+  --color-border-hover: var(--vt-c-divider-light-1);
+
+  --color-heading: var(--vt-c-text-light-1);
+  --color-text: var(--vt-c-text-light-1);
+
+  --section-gap: 160px;
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --color-background: var(--vt-c-black);
+    --color-background-soft: var(--vt-c-black-soft);
+    --color-background-mute: var(--vt-c-black-mute);
+
+    --color-border: var(--vt-c-divider-dark-2);
+    --color-border-hover: var(--vt-c-divider-dark-1);
+
+    --color-heading: var(--vt-c-text-dark-1);
+    --color-text: var(--vt-c-text-dark-2);
+  }
+}
+
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+  margin: 0;
+  font-weight: normal;
+}
+
+html{
+  height: 100vh;
+}
+
+body {
+  height: 100vh;
+  min-height: 100vh;
+  color: var(--color-text);
+  background: var(--color-background);
+  transition: color 0.5s, background-color 0.5s;
+  line-height: 1.6;
+  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
+    Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
+  font-size: 15px;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}

+ 1 - 0
fe/src/assets/logo.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

+ 13 - 0
fe/src/assets/main.css

@@ -0,0 +1,13 @@
+@import './base.css';
+
+#app {
+  margin: 0 auto;
+  padding: 0;
+  height: 100vh;
+  font-weight: normal;
+  font-family: Avenir, Helvetica, Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  text-align: center;
+  color: #2c3e50;
+}

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

@@ -0,0 +1,64 @@
+<template>
+  <div id="main">
+    <input id="search" :placeholder="lang.search">
+    <el-tree :data="data" :props="defaultProps" :defaultExpandAll="true" @node-click="handleNodeClick" :class="node" />
+  </div>
+</template>
+
+
+<script setup>
+import { useRouter } from 'vue-router'
+import $http from "../http/http";
+import { reactive, ref } from 'vue'
+import useGroupStore from '../stores/group'
+import lang from '../i18n/i18n';
+
+const groupStore = useGroupStore()
+const router = useRouter()
+
+
+const data = ref([])
+
+$http.get('/api/group').then(res => {
+  data.value = res.data
+})
+
+
+
+
+const handleNodeClick = function (data) {
+  if (data.tag != null) {
+    groupStore.name = data.label
+    groupStore.tag = data.tag
+    router.push({
+      name: "list",
+    })
+  }
+}
+
+
+
+</script>
+
+
+<style scoped>
+#main {
+  width: 243px;
+  background-color: #F1F1F1;
+  height: 100%;
+}
+
+#search {
+  background-color: #D6E7F7;
+  width: 100%;
+  height: 40px;
+  padding-left: 10px;
+  border: none;
+  outline: none;
+  font-size: 16px;
+}
+
+.el-tree {
+  background-color: #F1F1F1;
+}
+</style>

+ 67 - 0
fe/src/components/HomeHeader.vue

@@ -0,0 +1,67 @@
+<template>
+    <div id="header_main">
+        <div id="logo">
+            <span style="padding-left: 20px;">PMail</span>
+        </div>
+        <div id="settings" @click="settings">
+            <el-icon style="font-size: 25px;">
+                <Setting style="color:#FFFFFF" />
+            </el-icon>
+        </div>
+        <el-drawer v-model="openSettings" :title="lang.settings">
+            <el-tabs tab-position="left" >
+                <el-tab-pane :label="lang.security">
+                    <SecuritySettings/>
+                </el-tab-pane>
+            </el-tabs>
+        </el-drawer>
+
+    </div>
+</template>
+
+<script setup>
+import { Setting } from '@element-plus/icons-vue';
+import { ref } from 'vue'
+import { ElMessageBox } from 'element-plus'
+import SecuritySettings from '@/components/SecuritySettings.vue'
+import lang from '../i18n/i18n';
+
+
+const openSettings = ref(false)
+const settings = function () {
+    openSettings.value = true;
+}
+
+</script>
+
+
+<style scoped>
+#header_main {
+    height: 50px;
+    background-color: #000;
+    display: flex;
+    padding: 0;
+}
+
+#logo {
+    height: 3rem;
+    line-height: 3rem;
+    font-size: 2.3rem;
+    flex-grow: 1;
+    width: 200px;
+    color: #FFF;
+    text-align: left;
+}
+
+#search {
+    height: 3rem;
+    width: 100%;
+}
+
+#settings {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    padding-right: 20px;
+}
+</style>

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

@@ -0,0 +1,54 @@
+<template>
+    <el-form :model="ruleForm" :rules="rules" status-icon>
+        <el-form-item :label="lang.modify_pwd" prop="new_pwd">
+            <el-input type="password" v-model="ruleForm.new_pwd" />
+        </el-form-item>
+
+        <el-form-item :label="lang.enter_again" prop="new_pwd2">
+            <el-input type="password" v-model="ruleForm.new_pwd2" />
+        </el-form-item>
+
+        <el-form-item>
+            <el-button type="primary" @click="submit">
+                {{ lang.submit }}
+            </el-button>
+        </el-form-item>
+    </el-form>
+</template>
+
+<script setup>
+import { reactive, ref } from 'vue'
+import { ElNotification } from 'element-plus'
+import $http from "../http/http";
+import lang from '../i18n/i18n';
+const ruleForm = reactive({
+    new_pwd: "",
+    new_pwd2: ""
+})
+
+const rules = reactive({
+    new_pwd: [{ required: true, message: lang.err_required_pwd, trigger: 'blur' },],
+    new_pwd2: [{ required: true, message: lang.err_required_pwd, trigger: 'blur' },],
+
+})
+
+const submit = function () {
+    if (ruleForm.new_pwd != ruleForm.new_pwd2) {
+        ElNotification({
+            title: 'Error',
+            message: lang.err_pwd_diff,
+            type: 'error',
+        })
+        return
+    }
+    $http.post("/api/settings/modify_password", { password: ruleForm.new_pwd }).then(res => {
+        ElNotification({
+            title: res.errorNo == 0 ? lang.succ : lang.fail,
+            message: res.data,
+            type: res.errorNo == 0 ? 'success' : 'error',
+        })
+    })
+
+
+}
+</script>

+ 92 - 0
fe/src/http/http.js

@@ -0,0 +1,92 @@
+// http/index.js
+import axios from 'axios'
+import router from "@/router";  //根路由对象
+import lang from '../i18n/i18n';
+
+//创建axios的一个实例 
+var $http = axios.create({
+    baseURL: import.meta.env.VITE_APP_URL, //接口统一域名
+    timeout: 6000, //设置超时
+    headers: {
+        'Content-Type': 'application/json;charset=UTF-8;',
+        'Lang':lang.lang
+    }
+})
+
+//请求拦截器 
+$http.interceptors.request.use((config) => {
+    //若请求方式为post,则将data参数转为JSON字符串
+    if (config.method === 'POST') {
+        config.data = JSON.stringify(config.data);
+    }
+    return config;
+}, (error) =>
+    // 对请求错误做些什么
+    Promise.reject(error));
+
+//响应拦截器
+$http.interceptors.response.use((response) => {
+    //响应成功
+    if(response.data.errorNo == 403){
+        router.replace({
+            path: '/login',
+            query: {
+                redirect: router.currentRoute.fullPath
+            }
+        })
+    }
+    return response.data;
+}, (error) => {
+    //响应错误
+    if (error.response && error.response.status) {
+        const status = error.response.status
+        let message = ""
+        switch (status) {
+            case 400:
+                message = '请求错误';
+                break;
+            case 401:
+                message = '请求错误';
+                break;
+            case 403:
+                router.replace({
+                    path: '/login',
+                    query: {
+                        redirect: router.currentRoute.fullPath
+                    }
+                })
+                break;
+            case 404:
+                message = '请求地址出错';
+                break;
+            case 408:
+                message = '请求超时';
+                break;
+            case 500:
+                message = '服务器内部错误!';
+                break;
+            case 501:
+                message = '服务未实现!';
+                break;
+            case 502:
+                message = '网关错误!';
+                break;
+            case 503:
+                message = '服务不可用!';
+                break;
+            case 504:
+                message = '网关超时!';
+                break;
+            case 505:
+                message = 'HTTP版本不受支持';
+                break;
+            default:
+                message = '请求失败'
+        }
+        return Promise.reject(error);
+    }
+    return Promise.reject(error);
+});
+
+
+export default $http;

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

@@ -0,0 +1,90 @@
+var lang = {
+    "lang": "en",
+    "submit": "submit",
+    "compose": "compose",
+    "new": "new",
+    "account": "Account",
+    "password": "Password",
+    "login": "login",
+    "search": "Search Email",
+    "inbox": "Inbox",
+    "sender": "Sender",
+    "title": "Title",
+    "date": "Date",
+    "to": "To:",
+    "cc": "Cc:",
+    "sender_desc": "Only the email prefix is required",
+    "to_desc": "Recipient's e-mail address",
+    "cc_desc": "Cc's e-mail address",
+    "send": "send",
+    "add_att": "Add Attachment",
+    "attachment":"Attachment",
+    "err_sender_must": "Sender's email prefix is required!",
+    "only_prefix": "Only the email prefix is required!",
+    "err_email_format": "Incorrect e-mail address, please check the e-mail format!",
+    "err_title_must": "Title is required!",
+    "succ_send": "Send Success!",
+    "outbox": "outbox",
+    "modify_pwd": "modify password",
+    "enter_again": "enter again",
+    "err_required_pwd": "Please Input Password!",
+    "succ": "Success!",
+    "err_pwd_diff": "The passwords entered twice do not match!",
+    "fail": "Fail!",
+    "settings":"Settings",
+    "security":"Security"
+};
+
+
+
+var zhCN = {
+    "lang": "zhCn",
+    "submit": "提交",
+    "compose": "发件",
+    "new": "新",
+    "account": "用户名",
+    "password": "密码",
+    "login": "登录",
+    "search": "搜索邮件",
+    "inbox": "收件箱",
+    "sender": "发件人",
+    "title": "主题",
+    "date": "时间",
+    "to": "收件人:",
+    "cc": "抄送:",
+    "sender_desc": "只需要邮箱前缀",
+    "to_desc": "接收人邮件地址",
+    "cc_desc": "抄送人邮箱地址",
+    "send": "发送",
+    "add_att": "添加附件",
+    "attachment":"附件",
+    "err_sender_must": "发件人邮箱前缀必填",
+    "only_prefix": "只需要邮箱前缀",
+    "err_email_format": "邮箱地址错误,请检查邮箱格式!",
+    "err_title_must": "标题必填!",
+    "succ_send": "发送成功!",
+    "outbox": "发件箱",
+    "modify_pwd": "修改密码",
+    "enter_again": "确认密码",
+    "err_required_pwd": "请输入密码!",
+    "succ": "成功!",
+    "err_pwd_diff": "两次输入的密码不一致!",
+    "fail": "失败",
+    "settings":"设置",
+    "security":"安全"
+}
+
+switch (navigator.language) {
+    case "zh":
+        lang = zhCN
+        break
+    case "zh-CN":
+        lang = zhCN
+        break
+    default:
+        break
+}
+
+
+
+export default lang;

+ 16 - 0
fe/src/main.js

@@ -0,0 +1,16 @@
+import './assets/main.css'
+import 'element-plus/dist/index.css'
+
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+
+import App from './App.vue'
+import router from './router'
+
+const app = createApp(App)
+
+app.use(createPinia())
+app.use(router)
+
+app.mount('#app')
+

+ 37 - 0
fe/src/router/index.js

@@ -0,0 +1,37 @@
+import { createRouter, createWebHashHistory } from 'vue-router'
+import ListView from '../views/ListView.vue'
+import EditerView from '../views/EditerView.vue'
+import LoginView from '../views/LoginView.vue'
+import EmailDetailView from '../views/EmailDetailView.vue'
+const router = createRouter({
+  history: createWebHashHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: '/',
+      name: 'home',
+      component: ListView
+    },
+    {
+      path: '/list',
+      name: 'list',
+      component: ListView
+    },
+    {
+      path: '/editer',
+      name: "editer",
+      component: EditerView
+    },
+    {
+      path: '/login',
+      name: "login",
+      component: LoginView
+    },
+    {
+      path: '/detail/:id',
+      name: "detail",
+      component: EmailDetailView
+    }
+  ]
+})
+
+export default router

+ 11 - 0
fe/src/stores/group.js

@@ -0,0 +1,11 @@
+import { ref, computed } from 'vue'
+import { defineStore } from 'pinia'
+import lang from '../i18n/i18n';
+
+const useGroupStore = defineStore('group', () => {
+  const tag = ref("")
+  const name = ref(lang.inbox)
+  return { tag, name }
+})
+
+export default useGroupStore

+ 280 - 0
fe/src/views/EditerView.vue

@@ -0,0 +1,280 @@
+<template>
+    <div id="main">
+        <el-form label-width="100px" :rules="rules" ref="ruleFormRef" :model="ruleForm" status-icon>
+            <el-form-item :label="lang.sender" prop="sender">
+                <el-input v-model="ruleForm.sender" :placeholder="lang.sender_desc"></el-input>
+            </el-form-item>
+
+
+            <el-form-item :label="lang.to" prop="receivers">
+                <el-select v-model="ruleForm.receivers" style="width: 100%;" multiple filterable allow-create
+                    :reserve-keyword="false" :placeholder="lang.to_desc"></el-select>
+            </el-form-item>
+
+
+            <el-form-item :label="lang.cc" prop="cc">
+                <el-select v-model="ruleForm.cc" style="width: 100%;" multiple filterable allow-create
+                    :reserve-keyword="false" :placeholder="lang.cc_desc"></el-select>
+            </el-form-item>
+
+
+            <el-form-item :label="lang.title" prop="subject">
+                <el-input v-model="ruleForm.subject" :placeholder="lang.title"></el-input>
+            </el-form-item>
+
+
+            <div id="editor">
+                <div style="border: 1px solid #ccc">
+                    <Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" :defaultConfig="toolbarConfig"
+                        :mode="mode" />
+                    <Editor style="height: 300px;" v-model="valueHtml" :defaultConfig="editorConfig" :mode="mode"
+                        @onCreated="handleCreated" />
+                </div>
+            </div>
+
+            <div id="fileList">
+                <ol>
+                    <li v-for="(item, index) in fileList">{{ item.name }} <el-icon @click="delFile(index)">
+                            <Close />
+                        </el-icon> </li>
+                </ol>
+            </div>
+
+            <div id="sendButton">
+                <el-button type="primary" @click="send(ruleFormRef)">{{ lang.send }}</el-button>
+                <!-- <el-button>定时发送</el-button> -->
+
+                <div style="margin-left: 15px">
+                    <el-button @click="upload">{{ lang.add_att }}</el-button>
+                    <input v-show="false" ref="fileRef" type="file" @change="fileChange">
+                </div>
+            </div>
+
+
+
+        </el-form>
+
+    </div>
+</template>
+
+<style scoped>
+#main {
+    text-align: left;
+    padding-right: 20px;
+}
+
+#editor {
+    padding-left: 25px;
+}
+
+#sendButton {
+    padding-left: 25px;
+    padding-top: 5px;
+    display: flex;
+}
+</style>
+
+
+<script setup>
+import '@wangeditor/editor/dist/css/style.css' // 引入 css
+import { ElMessage } from 'element-plus'
+import { onBeforeUnmount, ref, shallowRef, reactive, onMounted } from 'vue'
+import { Close } from '@element-plus/icons-vue';
+import lang from '../i18n/i18n';
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
+import { i18nChangeLanguage } from '@wangeditor/editor'
+import $http from '../http/http';
+import router from "@/router";  //根路由对象
+import useGroupStore from '../stores/group'
+const groupStore = useGroupStore()
+
+
+if (lang.lang == "zhCn"){
+    i18nChangeLanguage('zh-CN')
+}else{
+    i18nChangeLanguage('en')
+}
+
+
+
+// 内容 HTML
+const valueHtml = ref('<p>hello</p>')
+
+const toolbarConfig = {}
+const editorConfig = {
+    MENU_CONF: {},
+    placeholder: ''
+}
+
+
+editorConfig.MENU_CONF['uploadImage'] = {
+    base64LimitSize: 100 * 1024 * 1024 * 1024,  // 100G以下的文件都base64传
+}
+const mode = ref()
+const fileRef = ref();
+const pickFile = ref();
+const ruleFormRef = ref()
+const ruleForm = reactive({
+    sender: '',
+    receivers: '',
+    cc: '',
+    subject: '',
+})
+const fileList = reactive([]);
+
+
+const validateSender = function (rule, value, callback) {
+    if (typeof ruleForm.sender === "undefined" || ruleForm.sender === null || ruleForm.sender.trim() === "") {
+        callback(new Error(lang.err_sender_must))
+    } else if (ruleForm.sender.includes("@")) {
+        callback(new Error(lang.only_prefix))
+    } else {
+        callback()
+    }
+}
+
+const checkEmail = function (str) {
+    var re = /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/
+    if (re.test(str)) {
+        return true
+    } else {
+        return false
+    }
+}
+
+const validateReceivers = function (rule, value, callback) {
+    for (let index = 0; index < ruleForm.receivers.length; index++) {
+        let element = ruleForm.receivers[index];
+        if (!checkEmail(element)) {
+            callback(new Error(lang.err_email_format))
+            return
+        }
+    }
+    callback()
+}
+
+const validateCc = function (rule, value, callback) {
+    for (let index = 0; index < ruleForm.cc.length; index++) {
+        let element = ruleForm.cc[index];
+        if (!checkEmail(element)) {
+            callback(new Error(err_email_format))
+            return
+        }
+    }
+    callback()
+}
+
+const rules = reactive({
+    sender: [
+        { validator: validateSender, trigger: 'change' }
+    ],
+    receivers: [
+        { validator: validateReceivers, trigger: 'change' }
+    ],
+    cc: [
+        { validator: validateCc, trigger: 'change' }
+    ],
+    subject: [
+        { required: true, message: lang.err_title_must, trigger: 'change' },
+    ],
+})
+
+
+// 编辑器实例,必须用 shallowRef
+const editorRef = shallowRef()
+// 组件销毁时,也及时销毁编辑器
+onBeforeUnmount(() => {
+    const editor = editorRef.value
+    if (editor == null) return
+    editor.destroy()
+})
+
+const handleCreated = (editor) => {
+    editorRef.value = editor // 记录 editor 实例,重要!
+}
+
+const send = function (formEl) {
+    if (!formEl) return
+    formEl.validate((valid) => {
+        if (valid) {
+            let objectTos = []
+            for (let index = 0; index < ruleForm.receivers.length; index++) {
+                let element = ruleForm.receivers[index];
+                objectTos.push({
+                    name: "",
+                    email: element
+                })
+            }
+
+            let objectCcs = []
+            for (let index = 0; index < ruleForm.cc.length; index++) {
+                let element = ruleForm.cc[index];
+                objectCcs.push({
+                    name: "",
+                    email: element
+                })
+            }
+
+            let text = editorRef.value.getText()
+
+            $http.post("/api/email/send", {
+                from: { name: ruleForm.sender, email: "" },
+                to: objectTos,
+                cc: objectCcs,
+                subject: ruleForm.subject,
+                text: text,
+                html: valueHtml.value,
+                attrs: fileList
+            }).then(res => {
+                if (res.errorNo === 0) {
+                    ElMessage({
+                        message: lang.succ_send,
+                        type: 'success',
+                    })
+                    groupStore.name = lang.outbox
+                    groupStore.tag = '{"type":1,"status":-1}'
+                    router.replace({
+                        name: 'list',
+                    })
+                } else {
+                    ElMessage.error(res.data)
+                }
+            })
+        } else {
+            return false
+        }
+    })
+
+
+
+
+}
+
+
+
+
+const upload = function () {
+    fileRef.value.dispatchEvent(new MouseEvent('click'))
+}
+
+const fileChange = function (e) {
+    let files = e.target.files || e.dataTransfer.files;
+    if (!files.length)
+        return;
+    for (let i = 0; i < files.length; i++) {
+        const reader = new FileReader();
+        reader.onload = function fileReadCompleted() {
+            fileList.push({
+                name: files[i].name,
+                data: this.result
+            })
+        };
+        reader.readAsDataURL(files[i]);
+
+    }
+}
+
+const delFile = function (index) {
+    fileList.splice(index, 1);
+}
+</script>

+ 100 - 0
fe/src/views/EmailDetailView.vue

@@ -0,0 +1,100 @@
+<template>
+    <div id="main">
+        <div id="title">{{ detailData.subject }}</div>
+        <el-divider />
+
+        <div>
+            <span>{{ lang.to }}:
+                <span class="userItem" v-for="to in tos">{{ to.Name }} {{ to.EmailAddress }} ;</span>
+            </span>
+
+            <span v-if="showCC">{{ lang.cc }}:
+                <span class="userItem" v-for="ccs in cc">{{ cc.Name }} {{ cc.EmailAddress }} ;</span>
+            </span>
+        </div>
+        <el-divider />
+        <div class="content" id="text" v-if="detailData.html == ''">
+            {{ detailData.text }}
+        </div>
+
+        <div class="content" id="html" v-else v-html="detailData.html">
+
+        </div>
+
+        <div v-if="detailData.attachments.length > 0" style="">
+            <el-divider />
+            {{ lang.attachment }}:
+            <a class="att" v-for="item in detailData.attachments"
+                :href="'/attachments/download/' + detailData.id + '/' + item.Index"> <el-icon>
+                    <Document />
+                </el-icon> {{ item.Filename }} </a>
+        </div>
+
+
+    </div>
+</template>
+
+<script setup>
+import { RouterLink } from 'vue-router'
+import $http from "../http/http";
+import { reactive, ref } from 'vue'
+import { useRoute } from 'vue-router'
+import router from "@/router";  //根路由对象
+import { Document } from '@element-plus/icons-vue';
+import lang from '../i18n/i18n';
+
+const route = useRoute()
+const detailData = ref({
+    attachments:[]
+})
+
+const tos = ref()
+const ccs = ref()
+const showCC = ref(false)
+
+$http.post("/api/email/detail", { id: parseInt(route.params.id) }).then(res => {
+    detailData.value = res.data
+    if (res.data.to != "" && res.data.to != null) {
+        tos.value = JSON.parse(res.data.to)
+    }
+    if (res.data.cc != "" && res.data.cc != null) {
+        ccs.value = JSON.parse(res.data.cc)
+
+    }
+
+    if (ccs.value != null && ccs.value != undefined){
+         showCC.value = ccs.value.length > 0
+    }else{
+        showCC.value = false
+    }
+})
+</script>
+
+<style scoped>
+#main {
+    display: flex;
+    padding-left: 20px;
+    padding-right: 80px;
+    text-align: left;
+}
+
+#title {
+    font-size: 40px;
+    text-align: left;
+}
+
+#userItem {}
+
+.content {
+    /* background-color: aliceblue; */
+}
+
+a,a:link,a:visited,a:hover,a:active{
+    text-decoration: none;
+    color:inherit;
+}
+
+.att{
+    display:block;
+}
+</style>

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

@@ -0,0 +1,156 @@
+<template>
+    <div style="height: 100%">
+        <div id="operation">
+            <div id="action">
+                <RouterLink to="/editer">+{{ lang.compose }}</RouterLink>
+            </div>
+            <!-- <div id="action">全部标记为已读</div> -->
+        </div>
+        <div id="title">{{ groupStore.name }}</div>
+        <div id="table">
+            <el-table :data="data" :show-header="true" :border="false" @row-click="rowClick" :row-style="rowStyle">
+                <el-table-column type="selection" width="30" />
+                <el-table-column prop="title" label="" width="50">
+                    <template #default="scope">
+                        <div>
+                            <span v-if="!scope.row.is_read">
+                                {{ lang.new }}
+                            </span>
+                        </div>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="title" :label="lang.sender" width="150">
+                    <template #default="scope">
+                        <span v-if="scope.row.is_read">
+                            <div v-if="scope.row.sender.Name != ''">{{ scope.row.sender.Name }}</div>
+                            {{ scope.row.sender.EmailAddress }}
+                        </span>
+                        <span v-else style="font-weight:bolder;">
+                            <div v-if="scope.row.sender.Name != ''">{{ scope.row.sender.Name }}</div>
+                            {{ scope.row.sender.EmailAddress }}
+                        </span>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="desc" :label="lang.title">
+                    <template #default="scope">
+                        <div v-if="scope.row.is_read">{{ scope.row.title }}</div>
+                        <div v-else style="font-weight:bolder;">{{ scope.row.title }}</div>
+
+                        <div style="font-size: 12px;height: 24px;">{{ scope.row.desc }}</div>
+
+                    </template>
+                </el-table-column>
+                <el-table-column prop="datetime" :label="lang.date" width="180">
+                    <template #default="scope">
+                        <span v-if="scope.row.is_read">{{ scope.row.datetime }}</span>
+                        <span v-else style="font-weight:bolder;">{{ scope.row.datetime }}</span>
+                    </template>
+                </el-table-column>
+            </el-table>
+        </div>
+        <div id="pagination">
+            <el-pagination background layout="prev, pager, next" :page-count="totalPage" />
+        </div>
+    </div>
+</template>
+
+
+
+<script setup>
+import { RouterLink } from 'vue-router'
+import $http from "../http/http";
+import { reactive, ref, watch } from 'vue'
+import { useRoute } from 'vue-router'
+import router from "@/router";  //根路由对象
+import useGroupStore from '../stores/group'
+import lang from '../i18n/i18n';
+
+const groupStore = useGroupStore()
+
+const route = useRoute()
+
+
+
+let tag = groupStore.tag;
+
+if (tag == "") {
+    tag = '{"type":0,"status":-1}'
+}
+
+
+watch(groupStore, async (newV, oldV) => {
+    tag = newV.tag;
+    if (tag == "") {
+        tag = '{"type":0,"status":-1}'
+    }
+    data.value = []
+    $http.post("/api/email/list", { tag: tag, page_size: 10 }).then(res => {
+        data.value = res.data.list
+        totalPage.value = res.data.total_page
+    })
+})
+
+
+
+const data = ref([])
+const totalPage = ref(0)
+
+$http.post("/api/email/list", { tag: tag, page_size: 10 }).then(res => {
+    data.value = res.data.list
+    totalPage.value = res.data.total_page
+})
+
+const rowClick = function (row, column, event) {
+    router.push("/detail/" + row.id)
+}
+
+const rowStyle = function ({ row, rowIndwx }) {
+    return { 'cursor': 'pointer' }
+}
+
+</script>
+
+
+<style scoped>
+#action {
+    text-align: left;
+    font-size: 20px;
+    line-height: 40px;
+    padding-left: 10px;
+    margin-right: 5px;
+}
+
+
+#action a,
+a:visited {
+    color: #000000;
+    text-decoration: none;
+}
+
+#operation {
+    display: flex;
+    height: 40px;
+    background-color: rgb(236, 244, 251);
+}
+
+#title {
+    margin-top: 10px;
+    font-size: 23px;
+    text-align: left;
+    padding-left: 20px;
+}
+
+#table {
+    text-align: left;
+    width: 100%;
+    padding-left: 20px;
+}
+
+#pagination {
+    padding-top: 30px;
+    display: flex;
+    justify-content: center;
+    /* 水平居中 */
+    width: 100%;
+}
+</style>

+ 63 - 0
fe/src/views/LoginView.vue

@@ -0,0 +1,63 @@
+<template>
+    <div id="main">
+        <div id="form">
+            <el-form :model="form" label-width="120px">
+                <el-form-item :label="lang.account">
+                    <el-input v-model="form.account" placeholder="User Name" />
+                </el-form-item>
+                <el-form-item :label="lang.password">
+                    <el-input v-model="form.password" placeholder="Password" type="password" />
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" @click="onSubmit">{{ lang.login }}</el-button>
+                </el-form-item>
+            </el-form>
+
+        </div>
+    </div>
+</template>
+
+<script setup>
+
+import { reactive } from 'vue'
+import $http from "../http/http";
+import { ElMessage } from 'element-plus'
+import router from "@/router";  //根路由对象
+import lang from '../i18n/i18n';
+
+
+const form = reactive({
+    account: '',
+    password: '',
+})
+
+const onSubmit = () => {
+    $http.post("/api/login", form).then(res => {
+        if (res.errorNo != 0) {
+            ElMessage.error(res.errorMsg)
+        } else {
+            router.replace({
+                path: '/',
+                query: {
+                    redirect: router.currentRoute.fullPath
+                }
+            })
+        }
+    })
+
+}
+</script>
+
+
+<style scoped>
+#main {
+    width: 100%;
+    height: 100%;
+    background-color: #f1f1f1;
+    display: flex;
+    justify-content: center;
+    /* 水平居中 */
+    align-items: center;
+    /* 垂直居中 */
+}
+</style>

+ 32 - 0
fe/vite.config.js

@@ -0,0 +1,32 @@
+import { fileURLToPath, URL } from 'node:url'
+import { defineConfig } from 'vite'
+import AutoImport from 'unplugin-auto-import/vite'
+import Components from 'unplugin-vue-components/vite'
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [
+    vue(),
+    AutoImport({
+      resolvers: [ElementPlusResolver()],
+    }),
+    Components({
+      resolvers: [ElementPlusResolver()],
+    }),
+  ],
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url))
+    }
+  },
+  server: {
+    cors: true,
+    proxy: {
+      "/api": "http://127.0.0.1/",
+      "/attachments":"http://127.0.0.1/"
+
+    }
+  }
+})

+ 1908 - 0
fe/yarn.lock

@@ -0,0 +1,1908 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@antfu/utils@^0.7.2", "@antfu/utils@^0.7.3":
+  version "0.7.4"
+  resolved "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.4.tgz"
+  integrity sha512-qe8Nmh9rYI/HIspLSTwtbMFPj6dISG6+dJnOguTlPNXtCvS2uezdxscVBb7/3DrmNbQK49TDqpkSQ1chbRGdpQ==
+
+"@babel/parser@^7.20.15", "@babel/parser@^7.21.3":
+  version "7.22.4"
+  resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.22.4.tgz"
+  integrity sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA==
+
+"@babel/runtime@^7.12.0":
+  version "7.22.3"
+  resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.22.3.tgz#0a7fce51d43adbf0f7b517a71f4c3aaca92ebcbb"
+  integrity sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==
+  dependencies:
+    regenerator-runtime "^0.13.11"
+
+"@ctrl/tinycolor@^3.4.1":
+  version "3.6.0"
+  resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz"
+  integrity sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==
+
+"@element-plus/icons-vue@^2.0.6":
+  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==
+
+"@esbuild/android-arm64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd"
+  integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==
+
+"@esbuild/android-arm@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d"
+  integrity sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==
+
+"@esbuild/android-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1"
+  integrity sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==
+
+"@esbuild/darwin-arm64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz"
+  integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==
+
+"@esbuild/darwin-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb"
+  integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==
+
+"@esbuild/freebsd-arm64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2"
+  integrity sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==
+
+"@esbuild/freebsd-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4"
+  integrity sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==
+
+"@esbuild/linux-arm64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb"
+  integrity sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==
+
+"@esbuild/linux-arm@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a"
+  integrity sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==
+
+"@esbuild/linux-ia32@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a"
+  integrity sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==
+
+"@esbuild/linux-loong64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72"
+  integrity sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==
+
+"@esbuild/linux-mips64el@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289"
+  integrity sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==
+
+"@esbuild/linux-ppc64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7"
+  integrity sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==
+
+"@esbuild/linux-riscv64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09"
+  integrity sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==
+
+"@esbuild/linux-s390x@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829"
+  integrity sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==
+
+"@esbuild/linux-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4"
+  integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==
+
+"@esbuild/netbsd-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462"
+  integrity sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==
+
+"@esbuild/openbsd-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691"
+  integrity sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==
+
+"@esbuild/sunos-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273"
+  integrity sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==
+
+"@esbuild/win32-arm64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f"
+  integrity sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==
+
+"@esbuild/win32-ia32@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03"
+  integrity sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==
+
+"@esbuild/win32-x64@0.17.19":
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061"
+  integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==
+
+"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.3.0":
+  version "4.4.0"
+  resolved "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz"
+  integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
+  dependencies:
+    eslint-visitor-keys "^3.3.0"
+
+"@eslint-community/regexpp@^4.4.0":
+  version "4.5.1"
+  resolved "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz"
+  integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==
+
+"@eslint/eslintrc@^2.0.3":
+  version "2.0.3"
+  resolved "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz"
+  integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==
+  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"
+
+"@eslint/js@8.42.0":
+  version "8.42.0"
+  resolved "https://registry.npmmirror.com/@eslint/js/-/js-8.42.0.tgz"
+  integrity sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==
+
+"@floating-ui/core@^1.2.6":
+  version "1.2.6"
+  resolved "https://registry.npmmirror.com/@floating-ui/core/-/core-1.2.6.tgz"
+  integrity sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg==
+
+"@floating-ui/dom@^1.0.1":
+  version "1.2.9"
+  resolved "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.2.9.tgz"
+  integrity sha512-sosQxsqgxMNkV3C+3UqTS6LxP7isRLwX8WMepp843Rb3/b0Wz8+MdUkxJksByip3C2WwLugLHN1b4ibn//zKwQ==
+  dependencies:
+    "@floating-ui/core" "^1.2.6"
+
+"@humanwhocodes/config-array@^0.11.10":
+  version "0.11.10"
+  resolved "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz"
+  integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==
+  dependencies:
+    "@humanwhocodes/object-schema" "^1.2.1"
+    debug "^4.1.1"
+    minimatch "^3.0.5"
+
+"@humanwhocodes/module-importer@^1.0.1":
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz"
+  integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
+
+"@humanwhocodes/object-schema@^1.2.1":
+  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==
+
+"@jridgewell/sourcemap-codec@^1.4.13":
+  version "1.4.15"
+  resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz"
+  integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
+"@nodelib/fs.scandir@2.1.5":
+  version "2.1.5"
+  resolved "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
+  integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+  dependencies:
+    "@nodelib/fs.stat" "2.0.5"
+    run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+  version "2.0.5"
+  resolved "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
+  integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
+  version "1.2.8"
+  resolved "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz"
+  integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+  dependencies:
+    "@nodelib/fs.scandir" "2.1.5"
+    fastq "^1.6.0"
+
+"@popperjs/core@npm:@sxzz/popperjs-es@^2.11.7":
+  version "2.11.7"
+  resolved "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz"
+  integrity sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==
+
+"@rollup/pluginutils@^5.0.2":
+  version "5.0.2"
+  resolved "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz"
+  integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==
+  dependencies:
+    "@types/estree" "^1.0.0"
+    estree-walker "^2.0.2"
+    picomatch "^2.3.1"
+
+"@transloadit/prettier-bytes@0.0.7":
+  version "0.0.7"
+  resolved "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz#cdb5399f445fdd606ed833872fa0cabdbc51686b"
+  integrity sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==
+
+"@types/estree@^1.0.0":
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.1.tgz"
+  integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
+
+"@types/event-emitter@^0.3.3":
+  version "0.3.3"
+  resolved "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.3.tgz#727032a9fc67565f96bbd78b2e2809275c97d7e7"
+  integrity sha512-UfnOK1pIxO7P+EgPRZXD9jMpimd8QEFcEZ5R67R1UhGbv4zghU5+NE7U8M8G9H5Jc8FI51rqDWQs6FtUfq2e/Q==
+
+"@types/lodash-es@^4.17.6":
+  version "4.17.7"
+  resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.7.tgz"
+  integrity sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ==
+  dependencies:
+    "@types/lodash" "*"
+
+"@types/lodash@*", "@types/lodash@^4.14.182":
+  version "4.14.195"
+  resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.195.tgz"
+  integrity sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==
+
+"@types/web-bluetooth@^0.0.16":
+  version "0.0.16"
+  resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz"
+  integrity sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==
+
+"@uppy/companion-client@^2.2.2":
+  version "2.2.2"
+  resolved "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-2.2.2.tgz#c70b42fdcca728ef88b3eebf7ee3e2fa04b4923b"
+  integrity sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==
+  dependencies:
+    "@uppy/utils" "^4.1.2"
+    namespace-emitter "^2.0.1"
+
+"@uppy/core@^2.1.1":
+  version "2.3.4"
+  resolved "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz#260b85b6bf3aa03cdc67da231f8c69cfbfdcc84a"
+  integrity sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==
+  dependencies:
+    "@transloadit/prettier-bytes" "0.0.7"
+    "@uppy/store-default" "^2.1.1"
+    "@uppy/utils" "^4.1.3"
+    lodash.throttle "^4.1.1"
+    mime-match "^1.0.2"
+    namespace-emitter "^2.0.1"
+    nanoid "^3.1.25"
+    preact "^10.5.13"
+
+"@uppy/store-default@^2.1.1":
+  version "2.1.1"
+  resolved "https://registry.npmmirror.com/@uppy/store-default/-/store-default-2.1.1.tgz#62a656a099bdaa012306e054d093754cb2d36e3e"
+  integrity sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==
+
+"@uppy/utils@^4.1.2", "@uppy/utils@^4.1.3":
+  version "4.1.3"
+  resolved "https://registry.npmmirror.com/@uppy/utils/-/utils-4.1.3.tgz#9d0be6ece4df25f228d30ef40be0f14208258ce3"
+  integrity sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==
+  dependencies:
+    lodash.throttle "^4.1.1"
+
+"@uppy/xhr-upload@^2.0.3":
+  version "2.1.3"
+  resolved "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz#0d4e355332fe0c6eb372d7731315e04d02aeeb18"
+  integrity sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==
+  dependencies:
+    "@uppy/companion-client" "^2.2.2"
+    "@uppy/utils" "^4.1.2"
+    nanoid "^3.1.25"
+
+"@vitejs/plugin-vue@^4.2.3":
+  version "4.2.3"
+  resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz"
+  integrity sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==
+
+"@vue/compiler-core@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.4.tgz"
+  integrity sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==
+  dependencies:
+    "@babel/parser" "^7.21.3"
+    "@vue/shared" "3.3.4"
+    estree-walker "^2.0.2"
+    source-map-js "^1.0.2"
+
+"@vue/compiler-dom@3.3.4":
+  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==
+  dependencies:
+    "@vue/compiler-core" "3.3.4"
+    "@vue/shared" "3.3.4"
+
+"@vue/compiler-sfc@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz"
+  integrity sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==
+  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"
+
+"@vue/compiler-ssr@3.3.4":
+  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==
+  dependencies:
+    "@vue/compiler-dom" "3.3.4"
+    "@vue/shared" "3.3.4"
+
+"@vue/devtools-api@^6.5.0":
+  version "6.5.0"
+  resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz"
+  integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==
+
+"@vue/reactivity-transform@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz"
+  integrity sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==
+  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"
+
+"@vue/reactivity@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.4.tgz"
+  integrity sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==
+  dependencies:
+    "@vue/shared" "3.3.4"
+
+"@vue/runtime-core@3.3.4":
+  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==
+  dependencies:
+    "@vue/reactivity" "3.3.4"
+    "@vue/shared" "3.3.4"
+
+"@vue/runtime-dom@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz"
+  integrity sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==
+  dependencies:
+    "@vue/runtime-core" "3.3.4"
+    "@vue/shared" "3.3.4"
+    csstype "^3.1.1"
+
+"@vue/server-renderer@3.3.4":
+  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==
+  dependencies:
+    "@vue/compiler-ssr" "3.3.4"
+    "@vue/shared" "3.3.4"
+
+"@vue/shared@3.3.4":
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.4.tgz"
+  integrity sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==
+
+"@vueuse/core@^9.1.0":
+  version "9.13.0"
+  resolved "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz"
+  integrity sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==
+  dependencies:
+    "@types/web-bluetooth" "^0.0.16"
+    "@vueuse/metadata" "9.13.0"
+    "@vueuse/shared" "9.13.0"
+    vue-demi "*"
+
+"@vueuse/metadata@9.13.0":
+  version "9.13.0"
+  resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz"
+  integrity sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==
+
+"@vueuse/shared@9.13.0":
+  version "9.13.0"
+  resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz"
+  integrity sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==
+  dependencies:
+    vue-demi "*"
+
+"@wangeditor/basic-modules@^1.1.7":
+  version "1.1.7"
+  resolved "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz#a9c3ccf4ef53332f29550d59d3676e15f395946f"
+  integrity sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==
+  dependencies:
+    is-url "^1.2.4"
+
+"@wangeditor/code-highlight@^1.0.3":
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz#90256857714d5c0cf83ac475aea64db7bf29a7cd"
+  integrity sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==
+  dependencies:
+    prismjs "^1.23.0"
+
+"@wangeditor/core@^1.1.19":
+  version "1.1.19"
+  resolved "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz#f9155f7fd92d03cb1982405b3b82e54c31f1c2b0"
+  integrity sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==
+  dependencies:
+    "@types/event-emitter" "^0.3.3"
+    event-emitter "^0.3.5"
+    html-void-elements "^2.0.0"
+    i18next "^20.4.0"
+    scroll-into-view-if-needed "^2.2.28"
+    slate-history "^0.66.0"
+
+"@wangeditor/editor-for-vue@^5.1.12":
+  version "5.1.12"
+  resolved "https://registry.npmmirror.com/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz#f7d5f239b39cdfc01d31151488de8443fe6edc64"
+  integrity sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==
+
+"@wangeditor/editor@^5.1.23":
+  version "5.1.23"
+  resolved "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz#c9d2007b7cb0ceef6b72692b4ee87b01ee2367b3"
+  integrity sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==
+  dependencies:
+    "@uppy/core" "^2.1.1"
+    "@uppy/xhr-upload" "^2.0.3"
+    "@wangeditor/basic-modules" "^1.1.7"
+    "@wangeditor/code-highlight" "^1.0.3"
+    "@wangeditor/core" "^1.1.19"
+    "@wangeditor/list-module" "^1.0.5"
+    "@wangeditor/table-module" "^1.1.4"
+    "@wangeditor/upload-image-module" "^1.0.2"
+    "@wangeditor/video-module" "^1.1.4"
+    dom7 "^3.0.0"
+    is-hotkey "^0.2.0"
+    lodash.camelcase "^4.3.0"
+    lodash.clonedeep "^4.5.0"
+    lodash.debounce "^4.0.8"
+    lodash.foreach "^4.5.0"
+    lodash.isequal "^4.5.0"
+    lodash.throttle "^4.1.1"
+    lodash.toarray "^4.4.0"
+    nanoid "^3.2.0"
+    slate "^0.72.0"
+    snabbdom "^3.1.0"
+
+"@wangeditor/list-module@^1.0.5":
+  version "1.0.5"
+  resolved "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz#3fc0b167acddf885536b45fa0c127f9c6adaea33"
+  integrity sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==
+
+"@wangeditor/table-module@^1.1.4":
+  version "1.1.4"
+  resolved "https://registry.npmmirror.com/@wangeditor/table-module/-/table-module-1.1.4.tgz#757d4a5868b2b658041cd323854a4d707c8347e9"
+  integrity sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==
+
+"@wangeditor/upload-image-module@^1.0.2":
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz#89e9b9467e10cbc6b11dc5748e08dd23aaebee30"
+  integrity sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==
+
+"@wangeditor/video-module@^1.1.4":
+  version "1.1.4"
+  resolved "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz#b9df1b3ab2cd53f678b19b4d927e200774a6f532"
+  integrity sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==
+
+acorn-jsx@^5.3.2:
+  version "5.3.2"
+  resolved "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
+  integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
+
+acorn@^8.8.0, acorn@^8.8.2:
+  version "8.8.2"
+  resolved "https://registry.npmmirror.com/acorn/-/acorn-8.8.2.tgz"
+  integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
+
+ajv@^6.10.0, ajv@^6.12.4:
+  version "6.12.6"
+  resolved "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz"
+  integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+  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"
+
+ansi-regex@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz"
+  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^4.1.0:
+  version "4.3.0"
+  resolved "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+  dependencies:
+    color-convert "^2.0.1"
+
+anymatch@~3.1.2:
+  version "3.1.3"
+  resolved "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz"
+  integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
+argparse@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz"
+  integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+async-validator@^4.2.5:
+  version "4.2.5"
+  resolved "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz"
+  integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==
+
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
+axios@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.npmmirror.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f"
+  integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==
+  dependencies:
+    follow-redirects "^1.15.0"
+    form-data "^4.0.0"
+    proxy-from-env "^1.1.0"
+
+balanced-match@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz"
+  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+binary-extensions@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz"
+  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+boolbase@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz"
+  integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+brace-expansion@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz"
+  integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
+  dependencies:
+    balanced-match "^1.0.0"
+
+braces@^3.0.2, braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/braces/-/braces-3.0.2.tgz"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+callsites@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz"
+  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+chalk@^4.0.0:
+  version "4.1.2"
+  resolved "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chokidar@^3.5.3:
+  version "3.5.3"
+  resolved "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz"
+  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+  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"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+combined-stream@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
+compute-scroll-into-view@^1.0.20:
+  version "1.0.20"
+  resolved "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz#1768b5522d1172754f5d0c9b02de3af6be506a43"
+  integrity sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz"
+  integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+cross-spawn@^7.0.2:
+  version "7.0.3"
+  resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz"
+  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+  dependencies:
+    path-key "^3.1.0"
+    shebang-command "^2.0.0"
+    which "^2.0.1"
+
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+csstype@^3.1.1:
+  version "3.1.2"
+  resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz"
+  integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
+
+d@1, d@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
+  integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
+  dependencies:
+    es5-ext "^0.10.50"
+    type "^1.0.1"
+
+dayjs@^1.11.3:
+  version "1.11.8"
+  resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.8.tgz"
+  integrity sha512-LcgxzFoWMEPO7ggRv1Y2N31hUf2R0Vj7fuy/m+Bg1K8rr+KAs1AEy4y9jd5DXe8pbHgX+srkHNS7TH6Q6ZhYeQ==
+
+debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
+  version "4.3.4"
+  resolved "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz"
+  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+  dependencies:
+    ms "2.1.2"
+
+deep-is@^0.1.3:
+  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==
+
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
+doctrine@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz"
+  integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
+  dependencies:
+    esutils "^2.0.2"
+
+dom7@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz#b861ce5d67a6becd7aaa3ad02942ff14b1240331"
+  integrity sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==
+  dependencies:
+    ssr-window "^3.0.0-alpha.1"
+
+element-plus@^2.3.6:
+  version "2.3.6"
+  resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.3.6.tgz"
+  integrity sha512-GLz0pXUYI2zRfIgyI6W7SWmHk6dSEikP9yR++hsQUyy63+WjutoiGpA3SZD4cGPSXUzRFeKfVr8CnYhK5LqXZw==
+  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"
+
+es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@~0.10.14:
+  version "0.10.62"
+  resolved "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5"
+  integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==
+  dependencies:
+    es6-iterator "^2.0.3"
+    es6-symbol "^3.1.3"
+    next-tick "^1.1.0"
+
+es6-iterator@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.npmmirror.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+  integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==
+  dependencies:
+    d "1"
+    es5-ext "^0.10.35"
+    es6-symbol "^3.1.1"
+
+es6-symbol@^3.1.1, es6-symbol@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
+  integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
+  dependencies:
+    d "^1.0.1"
+    ext "^1.1.2"
+
+esbuild@^0.17.5:
+  version "0.17.19"
+  resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.17.19.tgz"
+  integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==
+  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"
+
+escape-html@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz"
+  integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
+
+escape-string-regexp@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz"
+  integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+escape-string-regexp@^5.0.0:
+  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==
+
+eslint-plugin-vue@^9.11.0:
+  version "9.14.1"
+  resolved "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.14.1.tgz"
+  integrity sha512-LQazDB1qkNEKejLe/b5a9VfEbtbczcOaui5lQ4Qw0tbRBbQYREyxxOV5BQgNDTqGPs9pxqiEpbMi9ywuIaF7vw==
+  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"
+
+eslint-scope@^7.1.1, eslint-scope@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.0.tgz"
+  integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==
+  dependencies:
+    esrecurse "^4.3.0"
+    estraverse "^5.2.0"
+
+eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
+  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==
+
+eslint@^8.39.0:
+  version "8.42.0"
+  resolved "https://registry.npmmirror.com/eslint/-/eslint-8.42.0.tgz"
+  integrity sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==
+  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"
+
+espree@^9.3.1, espree@^9.5.2:
+  version "9.5.2"
+  resolved "https://registry.npmmirror.com/espree/-/espree-9.5.2.tgz"
+  integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==
+  dependencies:
+    acorn "^8.8.0"
+    acorn-jsx "^5.3.2"
+    eslint-visitor-keys "^3.4.1"
+
+esquery@^1.4.0, esquery@^1.4.2:
+  version "1.5.0"
+  resolved "https://registry.npmmirror.com/esquery/-/esquery-1.5.0.tgz"
+  integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==
+  dependencies:
+    estraverse "^5.1.0"
+
+esrecurse@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz"
+  integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+  dependencies:
+    estraverse "^5.2.0"
+
+estraverse@^5.1.0, estraverse@^5.2.0:
+  version "5.3.0"
+  resolved "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz"
+  integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+estree-walker@^2.0.2:
+  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==
+
+esutils@^2.0.2:
+  version "2.0.3"
+  resolved "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz"
+  integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+event-emitter@^0.3.5:
+  version "0.3.5"
+  resolved "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+  integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==
+  dependencies:
+    d "1"
+    es5-ext "~0.10.14"
+
+ext@^1.1.2:
+  version "1.7.0"
+  resolved "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f"
+  integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==
+  dependencies:
+    type "^2.7.2"
+
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
+  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==
+
+fast-glob@^3.2.12:
+  version "3.2.12"
+  resolved "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.2.12.tgz"
+  integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
+  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"
+
+fast-json-stable-stringify@^2.0.0:
+  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==
+
+fast-levenshtein@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
+  integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
+
+fastq@^1.6.0:
+  version "1.15.0"
+  resolved "https://registry.npmmirror.com/fastq/-/fastq-1.15.0.tgz"
+  integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==
+  dependencies:
+    reusify "^1.0.4"
+
+file-entry-cache@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz"
+  integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
+  dependencies:
+    flat-cache "^3.0.4"
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.npmmirror.com/fill-range/-/fill-range-7.0.1.tgz"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+find-up@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz"
+  integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+  dependencies:
+    locate-path "^6.0.0"
+    path-exists "^4.0.0"
+
+flat-cache@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.0.4.tgz"
+  integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
+  dependencies:
+    flatted "^3.1.0"
+    rimraf "^3.0.2"
+
+flatted@^3.1.0:
+  version "3.2.7"
+  resolved "https://registry.npmmirror.com/flatted/-/flatted-3.2.7.tgz"
+  integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
+
+follow-redirects@^1.15.0:
+  version "1.15.2"
+  resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
+  integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
+
+form-data@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz"
+  integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
+
+fsevents@~2.3.2:
+  version "2.3.2"
+  resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz"
+  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+glob-parent@^5.1.2, glob-parent@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
+glob-parent@^6.0.2:
+  version "6.0.2"
+  resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz"
+  integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
+  dependencies:
+    is-glob "^4.0.3"
+
+glob@^7.1.3:
+  version "7.2.3"
+  resolved "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz"
+  integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
+  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"
+
+globals@^13.19.0:
+  version "13.20.0"
+  resolved "https://registry.npmmirror.com/globals/-/globals-13.20.0.tgz"
+  integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==
+  dependencies:
+    type-fest "^0.20.2"
+
+graphemer@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz"
+  integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/has/-/has-1.0.3.tgz"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
+html-void-elements@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f"
+  integrity sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==
+
+i18next@^20.4.0:
+  version "20.6.1"
+  resolved "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz#535e5f6e5baeb685c7d25df70db63bf3cc0aa345"
+  integrity sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==
+  dependencies:
+    "@babel/runtime" "^7.12.0"
+
+ignore@^5.2.0:
+  version "5.2.4"
+  resolved "https://registry.npmmirror.com/ignore/-/ignore-5.2.4.tgz"
+  integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
+
+immer@^9.0.6:
+  version "9.0.21"
+  resolved "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176"
+  integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==
+
+import-fresh@^3.0.0, import-fresh@^3.2.1:
+  version "3.3.0"
+  resolved "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz"
+  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.0.0"
+
+imurmurhash@^0.1.4:
+  version "0.1.4"
+  resolved "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz"
+  integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz"
+  integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2:
+  version "2.0.4"
+  resolved "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
+is-core-module@^2.11.0:
+  version "2.12.1"
+  resolved "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.12.1.tgz"
+  integrity sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==
+  dependencies:
+    has "^1.0.3"
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz"
+  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
+  version "4.0.3"
+  resolved "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-hotkey@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz#1835a68171a91e5c9460869d96336947c8340cef"
+  integrity sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-path-inside@^3.0.3:
+  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==
+
+is-plain-object@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
+  integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
+
+is-url@^1.2.4:
+  version "1.2.4"
+  resolved "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
+  integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz"
+  integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+js-yaml@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz"
+  integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+  dependencies:
+    argparse "^2.0.1"
+
+json-schema-traverse@^0.4.1:
+  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==
+
+json-stable-stringify-without-jsonify@^1.0.1:
+  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==
+
+jsonc-parser@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz"
+  integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==
+
+levn@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz"
+  integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
+  dependencies:
+    prelude-ls "^1.2.1"
+    type-check "~0.4.0"
+
+local-pkg@^0.4.3:
+  version "0.4.3"
+  resolved "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.4.3.tgz"
+  integrity sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==
+
+locate-path@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz"
+  integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+  dependencies:
+    p-locate "^5.0.0"
+
+lodash-es@^4.17.21:
+  version "4.17.21"
+  resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz"
+  integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
+lodash-unified@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz"
+  integrity sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==
+
+lodash.camelcase@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
+  integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==
+
+lodash.clonedeep@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+  integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
+
+lodash.debounce@^4.0.8:
+  version "4.0.8"
+  resolved "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+  integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
+
+lodash.foreach@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
+  integrity sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==
+
+lodash.isequal@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+  integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
+
+lodash.merge@^4.6.2:
+  version "4.6.2"
+  resolved "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz"
+  integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
+
+lodash.throttle@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
+  integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==
+
+lodash.toarray@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
+  integrity sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==
+
+lodash@^4.17.21:
+  version "4.17.21"
+  resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz"
+  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+lru-cache@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz"
+  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+  dependencies:
+    yallist "^4.0.0"
+
+magic-string@^0.30.0:
+  version "0.30.0"
+  resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.0.tgz"
+  integrity sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==
+  dependencies:
+    "@jridgewell/sourcemap-codec" "^1.4.13"
+
+memoize-one@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz"
+  integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
+
+merge2@^1.3.0:
+  version "1.4.1"
+  resolved "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz"
+  integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.4:
+  version "4.0.5"
+  resolved "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.5.tgz"
+  integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+  dependencies:
+    braces "^3.0.2"
+    picomatch "^2.3.1"
+
+mime-db@1.52.0:
+  version "1.52.0"
+  resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+  integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-match@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz#3f87c31e9af1a5fd485fb9db134428b23bbb7ba8"
+  integrity sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==
+  dependencies:
+    wildcard "^1.1.0"
+
+mime-types@^2.1.12:
+  version "2.1.35"
+  resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+  integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+  dependencies:
+    mime-db "1.52.0"
+
+minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz"
+  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimatch@^9.0.1:
+  version "9.0.1"
+  resolved "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.1.tgz"
+  integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==
+  dependencies:
+    brace-expansion "^2.0.1"
+
+mlly@^1.2.0, mlly@^1.2.1:
+  version "1.3.0"
+  resolved "https://registry.npmmirror.com/mlly/-/mlly-1.3.0.tgz"
+  integrity sha512-HT5mcgIQKkOrZecOjOX3DJorTikWXwsBfpcr/MGBkhfWcjiqvnaL/9ppxvIUXfjT6xt4DVIAsN9fMUz1ev4bIw==
+  dependencies:
+    acorn "^8.8.2"
+    pathe "^1.1.0"
+    pkg-types "^1.0.3"
+    ufo "^1.1.2"
+
+ms@2.1.2:
+  version "2.1.2"
+  resolved "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+namespace-emitter@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz#978d51361c61313b4e6b8cf6f3853d08dfa2b17c"
+  integrity sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==
+
+nanoid@^3.1.25, nanoid@^3.2.0, nanoid@^3.3.6:
+  version "3.3.6"
+  resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz"
+  integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
+
+natural-compare@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz"
+  integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
+
+next-tick@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
+  integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+normalize-wheel-es@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz"
+  integrity sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==
+
+nth-check@^2.0.1:
+  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==
+  dependencies:
+    boolbase "^1.0.0"
+
+once@^1.3.0:
+  version "1.4.0"
+  resolved "https://registry.npmmirror.com/once/-/once-1.4.0.tgz"
+  integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
+  dependencies:
+    wrappy "1"
+
+optionator@^0.9.1:
+  version "0.9.1"
+  resolved "https://registry.npmmirror.com/optionator/-/optionator-0.9.1.tgz"
+  integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
+  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"
+
+p-limit@^3.0.2:
+  version "3.1.0"
+  resolved "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz"
+  integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+  dependencies:
+    yocto-queue "^0.1.0"
+
+p-locate@^5.0.0:
+  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==
+  dependencies:
+    p-limit "^3.0.2"
+
+parent-module@^1.0.0:
+  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==
+  dependencies:
+    callsites "^3.0.0"
+
+path-exists@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz"
+  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
+  integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
+
+path-key@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz"
+  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz"
+  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+pathe@^1.1.0:
+  version "1.1.1"
+  resolved "https://registry.npmmirror.com/pathe/-/pathe-1.1.1.tgz"
+  integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==
+
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pinia@^2.0.36:
+  version "2.1.3"
+  resolved "https://registry.npmmirror.com/pinia/-/pinia-2.1.3.tgz"
+  integrity sha512-XNA/z/ye4P5rU1pieVmh0g/hSuDO98/a5UC8oSP0DNdvt6YtetJNHTrXwpwsQuflkGT34qKxAEcp7lSxXNjf/A==
+  dependencies:
+    "@vue/devtools-api" "^6.5.0"
+    vue-demi ">=0.14.5"
+
+pkg-types@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.0.3.tgz"
+  integrity sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==
+  dependencies:
+    jsonc-parser "^3.2.0"
+    mlly "^1.2.0"
+    pathe "^1.1.0"
+
+postcss-selector-parser@^6.0.9:
+  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==
+  dependencies:
+    cssesc "^3.0.0"
+    util-deprecate "^1.0.2"
+
+postcss@^8.1.10, postcss@^8.4.23:
+  version "8.4.24"
+  resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.24.tgz"
+  integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==
+  dependencies:
+    nanoid "^3.3.6"
+    picocolors "^1.0.0"
+    source-map-js "^1.0.2"
+
+preact@^10.5.13:
+  version "10.15.1"
+  resolved "https://registry.npmmirror.com/preact/-/preact-10.15.1.tgz#a1de60c9fc0c79a522d969c65dcaddc5d994eede"
+  integrity sha512-qs2ansoQEwzNiV5eAcRT1p1EC/dmEzaATVDJNiB3g2sRDWdA7b7MurXdJjB2+/WQktGWZwxvDrnuRFbWuIr64g==
+
+prelude-ls@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz"
+  integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
+
+prismjs@^1.23.0:
+  version "1.29.0"
+  resolved "https://registry.npmmirror.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12"
+  integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==
+
+proxy-from-env@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+  integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
+punycode@^2.1.0:
+  version "2.3.0"
+  resolved "https://registry.npmmirror.com/punycode/-/punycode-2.3.0.tgz"
+  integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==
+
+queue-microtask@^1.2.2:
+  version "1.2.3"
+  resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz"
+  integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+readdirp@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz"
+  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+  dependencies:
+    picomatch "^2.2.1"
+
+regenerator-runtime@^0.13.11:
+  version "0.13.11"
+  resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
+  integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
+
+resolve-from@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz"
+  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve@^1.22.2:
+  version "1.22.2"
+  resolved "https://registry.npmmirror.com/resolve/-/resolve-1.22.2.tgz"
+  integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==
+  dependencies:
+    is-core-module "^2.11.0"
+    path-parse "^1.0.7"
+    supports-preserve-symlinks-flag "^1.0.0"
+
+reusify@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz"
+  integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+
+rimraf@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz"
+  integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+  dependencies:
+    glob "^7.1.3"
+
+rollup@^3.21.0:
+  version "3.23.1"
+  resolved "https://registry.npmmirror.com/rollup/-/rollup-3.23.1.tgz"
+  integrity sha512-ybRdFVHOoljGEFILHLd2g/qateqUdjE6YS41WXq4p3C/WwD3xtWxV4FYWETA1u9TeXQc5K8L8zHE5d/scOvrOQ==
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+run-parallel@^1.1.9:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz"
+  integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+  dependencies:
+    queue-microtask "^1.2.2"
+
+scroll-into-view-if-needed@^2.2.28:
+  version "2.2.31"
+  resolved "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz#d3c482959dc483e37962d1521254e3295d0d1587"
+  integrity sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==
+  dependencies:
+    compute-scroll-into-view "^1.0.20"
+
+scule@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.npmmirror.com/scule/-/scule-1.0.0.tgz"
+  integrity sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==
+
+semver@^7.3.5, semver@^7.3.6:
+  version "7.5.1"
+  resolved "https://registry.npmmirror.com/semver/-/semver-7.5.1.tgz"
+  integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==
+  dependencies:
+    lru-cache "^6.0.0"
+
+shebang-command@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz"
+  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+  dependencies:
+    shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz"
+  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+slate-history@^0.66.0:
+  version "0.66.0"
+  resolved "https://registry.npmmirror.com/slate-history/-/slate-history-0.66.0.tgz#ac63fddb903098ceb4c944433e3f75fe63acf940"
+  integrity sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==
+  dependencies:
+    is-plain-object "^5.0.0"
+
+slate@^0.72.0:
+  version "0.72.8"
+  resolved "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz#5a018edf24e45448655293a68bfbcf563aa5ba81"
+  integrity sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==
+  dependencies:
+    immer "^9.0.6"
+    is-plain-object "^5.0.0"
+    tiny-warning "^1.0.3"
+
+snabbdom@^3.1.0:
+  version "3.5.1"
+  resolved "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.5.1.tgz#25f80ef15b194baea703d9d5441892e369de18e1"
+  integrity sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==
+
+source-map-js@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz"
+  integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+
+ssr-window@^3.0.0-alpha.1:
+  version "3.0.0"
+  resolved "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz#fd5b82801638943e0cc704c4691801435af7ac37"
+  integrity sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==
+
+strip-ansi@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
+strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
+  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==
+
+strip-literal@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/strip-literal/-/strip-literal-1.0.1.tgz"
+  integrity sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==
+  dependencies:
+    acorn "^8.8.2"
+
+supports-color@^7.1.0:
+  version "7.2.0"
+  resolved "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+  dependencies:
+    has-flag "^4.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+  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==
+
+text-table@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz"
+  integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
+
+tiny-warning@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
+  integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
+
+to-regex-range@^5.0.1:
+  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==
+  dependencies:
+    is-number "^7.0.0"
+
+type-check@^0.4.0, type-check@~0.4.0:
+  version "0.4.0"
+  resolved "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz"
+  integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
+  dependencies:
+    prelude-ls "^1.2.1"
+
+type-fest@^0.20.2:
+  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==
+
+type@^1.0.1:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
+  integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
+
+type@^2.7.2:
+  version "2.7.2"
+  resolved "https://registry.npmmirror.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0"
+  integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==
+
+ufo@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.npmmirror.com/ufo/-/ufo-1.1.2.tgz"
+  integrity sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==
+
+unimport@^3.0.7:
+  version "3.0.7"
+  resolved "https://registry.npmmirror.com/unimport/-/unimport-3.0.7.tgz"
+  integrity sha512-2dVQUxJEGcrSZ0U4qtwJVODrlfyGcwmIOoHVqbAFFUx7kPoEN5JWr1cZFhLwoAwTmZOvqAm3YIkzv1engIQocg==
+  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"
+
+unplugin-auto-import@^0.16.4:
+  version "0.16.4"
+  resolved "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-0.16.4.tgz"
+  integrity sha512-xdgBa9NAS3JG8HjkAZHSbGSMlrjKpaWKXGUzaF6RzEtr980RCl1t0Zsu0skUInNYrEQfqaHc7aGWPv41DLTK/w==
+  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"
+
+unplugin-vue-components@^0.25.0:
+  version "0.25.0"
+  resolved "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-0.25.0.tgz"
+  integrity sha512-HxrQ4GMSS1RwVww2av3a42cABo/v5AmTRN9iARv6e/xwkrfTyHhLh84kFwXxKkXK61vxDHxaryn694mQmkiVBg==
+  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"
+
+unplugin@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.npmmirror.com/unplugin/-/unplugin-1.3.1.tgz"
+  integrity sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw==
+  dependencies:
+    acorn "^8.8.2"
+    chokidar "^3.5.3"
+    webpack-sources "^3.2.3"
+    webpack-virtual-modules "^0.5.0"
+
+uri-js@^4.2.2:
+  version "4.4.1"
+  resolved "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz"
+  integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+  dependencies:
+    punycode "^2.1.0"
+
+util-deprecate@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz"
+  integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+vite@^4.3.5:
+  version "4.3.9"
+  resolved "https://registry.npmmirror.com/vite/-/vite-4.3.9.tgz"
+  integrity sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==
+  dependencies:
+    esbuild "^0.17.5"
+    postcss "^8.4.23"
+    rollup "^3.21.0"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+vue-demi@*, vue-demi@>=0.14.5:
+  version "0.14.5"
+  resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.5.tgz"
+  integrity sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==
+
+vue-eslint-parser@^9.3.0:
+  version "9.3.0"
+  resolved "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.3.0.tgz"
+  integrity sha512-48IxT9d0+wArT1+3wNIy0tascRoywqSUe2E1YalIC1L8jsUGe5aJQItWfRok7DVFGz3UYvzEI7n5wiTXsCMAcQ==
+  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"
+
+vue-router@^4.2.0:
+  version "4.2.2"
+  resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.2.2.tgz"
+  integrity sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==
+  dependencies:
+    "@vue/devtools-api" "^6.5.0"
+
+vue@^3.3.2:
+  version "3.3.4"
+  resolved "https://registry.npmmirror.com/vue/-/vue-3.3.4.tgz"
+  integrity sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==
+  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"
+
+webpack-sources@^3.2.3:
+  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==
+
+webpack-virtual-modules@^0.5.0:
+  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==
+
+which@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.npmmirror.com/which/-/which-2.0.2.tgz"
+  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+  dependencies:
+    isexe "^2.0.0"
+
+wildcard@^1.1.0:
+  version "1.1.2"
+  resolved "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz#a7020453084d8cd2efe70ba9d3696263de1710a5"
+  integrity sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==
+
+word-wrap@^1.2.3:
+  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==
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz"
+  integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
+
+xml-name-validator@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz"
+  integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
+
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yocto-queue@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz"
+  integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==

+ 11 - 0
server/config/config.dev.json

@@ -0,0 +1,11 @@
+{
+  "domain": "jinnrry.com",
+  "dkimPrivateKeyPath": "config/dkim/dkim.priv",
+  "SSLPrivateKeyPath": "config/ssl/private.key",
+  "SSLPublicKeyPath": "config/ssl/public.crt",
+  "mysqlDSN": "",
+  "weChatPushAppId": "",
+  "weChatPushSecret": "",
+  "weChatPushTemplateId": "",
+  "weChatPushUserId": ""
+}

+ 79 - 0
server/config/config.go

@@ -0,0 +1,79 @@
+package config
+
+import (
+	"embed"
+	"encoding/json"
+	"io/fs"
+	"os"
+	"strings"
+)
+
+type Config struct {
+	Domain             string `json:"domain"`
+	DkimPrivateKeyPath string `json:"dkimPrivateKeyPath"`
+	SSLPrivateKeyPath  string `json:"SSLPrivateKeyPath"`
+	SSLPublicKeyPath   string `json:"SSLPublicKeyPath"`
+	MysqlDSN           string `json:"mysqlDSN"`
+
+	WeChatPushAppId      string `json:"weChatPushAppId"`
+	WeChatPushSecret     string `json:"weChatPushSecret"`
+	WeChatPushTemplateId string `json:"weChatPushTemplateId"`
+	WeChatPushUserId     string `json:"weChatPushUserId"`
+
+	Tables         map[string]string
+	TablesInitData map[string]string
+}
+
+//go:embed tables/*
+var tableConfig embed.FS
+
+var Instance *Config
+
+func Init() {
+	var cfgData []byte
+	var err error
+	args := os.Args
+
+	if len(args) >= 2 && args[len(args)-1] == "dev" {
+		cfgData, err = os.ReadFile("./config/config.dev.json")
+		if err != nil {
+			panic("dev环境配置文件加载失败" + err.Error())
+		}
+	} else {
+		cfgData, err = os.ReadFile("./config/config.json")
+		if err != nil {
+			panic("配置文件加载失败" + err.Error())
+		}
+	}
+
+	err = json.Unmarshal(cfgData, &Instance)
+	if err != nil {
+		panic("配置文件加载失败" + err.Error())
+	}
+
+	// 读取表设置
+	Instance.Tables = map[string]string{}
+	Instance.TablesInitData = map[string]string{}
+
+	err = fs.WalkDir(tableConfig, "tables", func(path string, info fs.DirEntry, err error) error {
+		if !info.IsDir() && strings.HasSuffix(info.Name(), ".sql") {
+			tableName := strings.ReplaceAll(info.Name(), ".sql", "")
+			i, e := tableConfig.ReadFile(path)
+			if e != nil {
+				panic(e)
+			}
+			if strings.Contains(path, "data") {
+				Instance.TablesInitData[tableName] = string(i)
+			} else {
+				Instance.Tables[tableName] = string(i)
+			}
+
+		}
+		return nil
+	})
+
+	if err != nil {
+		panic(err)
+	}
+
+}

+ 11 - 0
server/config/config.json

@@ -0,0 +1,11 @@
+{
+  "domain": "demo.com",
+  "dkimPrivateKeyPath": "config/dkim/dkim.priv",
+  "SSLPrivateKeyPath": "config/ssl/private.key",
+  "SSLPublicKeyPath": "config/ssl/public.crt",
+  "mysqlDSN": "",
+  "weChatPushAppId": "",
+  "weChatPushSecret": "",
+  "weChatPushTemplateId": "",
+  "weChatPushUserId": ""
+}

+ 1 - 0
server/config/dkim/README

@@ -0,0 +1 @@
+使用[go-msgauth](https://github.com/emersion/go-msgauth)项目的dkim-keygen工具生成公钥和私钥

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


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


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


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


+ 2 - 0
server/config/tables/data/user.sql

@@ -0,0 +1,2 @@
+INSERT INTO user (account, name, password) VALUES ('admin', 'admin', 'faddb6ec2efe16116a342f5512583c48');
+

+ 2 - 0
server/config/tables/data/user_auth.sql

@@ -0,0 +1,2 @@
+INSERT INTO pmail.user_auth (user_id, email_account) VALUES (1, '*');
+

+ 26 - 0
server/config/tables/email.sql

@@ -0,0 +1,26 @@
+CREATE table email
+(
+    id             INT unsigned AUTO_INCREMENT PRIMARY KEY COMMENT '自增id',
+    type           tinyint(4) NOT NULL DEFAULT 0 COMMENT '邮件类型,0:收到的邮件,1:发送的邮件',
+    subject        varchar(1000) NOT NULL DEFAULT '' COMMENT '邮件标题',
+    reply_to       json COMMENT '回复人',
+    from_name      varchar(50)   NOT NULL DEFAULT '' COMMENT '发件人名称',
+    from_address   varchar(150)  NOT NULL DEFAULT '' COMMENT '发件人邮件地址',
+    `to`           json COMMENT '收件人信息',
+    bcc            json COMMENT '抄送',
+    cc             json COMMENT '抄送',
+    `text`         text COMMENT '邮件文本内容',
+    html           text COMMENT 'html格式内容',
+    sender         json COMMENT '发件人',
+    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发送失败',
+    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 '错误信息记录',
+    cron_send_time datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '定时发送邮件的发送时间',
+    send_date      datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发件日期',
+    create_time    datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    update_time    datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
+)COMMENT='邮件内容表'

+ 7 - 0
server/config/tables/sessions.sql

@@ -0,0 +1,7 @@
+CREATE TABLE sessions
+(
+    token  CHAR(43) PRIMARY KEY,
+    data   BLOB         NOT NULL,
+    expiry TIMESTAMP(6) NOT NULL,
+    KEY    `sessions_expiry_idx` (`expiry`)
+)COMMENT='系统session数据表';

+ 8 - 0
server/config/tables/user.sql

@@ -0,0 +1,8 @@
+CREATE TABLE user
+(
+    id       INT unsigned AUTO_INCREMENT PRIMARY KEY COMMENT '自增id',
+    account  varchar(20) COMMENT '账号登陆名',
+    name     varchar(10) COMMENT '用户名',
+    password char(32) COMMENT '登陆密码,两次md5加盐,md5(md5(password+"pmail") +"pmail2023")',
+    UNIQUE INDEX udx_account ( account )
+)COMMENT='登陆信息表'

+ 8 - 0
server/config/tables/user_auth.sql

@@ -0,0 +1,8 @@
+CREATE TABLE user_auth
+(
+    id            INT unsigned AUTO_INCREMENT PRIMARY KEY COMMENT '自增id',
+    user_id       int COMMENT '用户id',
+    email_account varchar(30) COMMENT '收件人前缀',
+    UNIQUE INDEX udx_uid_ename ( user_id, email_account),
+    UNIQUE INDEX udx_ename_uid ( email_account,user_id )
+)COMMENT='登陆信息表'

+ 50 - 0
server/controllers/attachments.go

@@ -0,0 +1,50 @@
+package controllers
+
+import (
+	"fmt"
+	"github.com/spf13/cast"
+	"net/http"
+	"pmail/dto"
+	"pmail/dto/response"
+	"pmail/services/attachments"
+	"strings"
+)
+
+func GetAttachments(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+	urlInfos := strings.Split(req.RequestURI, "/")
+	if len(urlInfos) != 4 {
+		response.NewErrorResponse(response.ParamsError, "", "").FPrint(w)
+		return
+	}
+	emailId := cast.ToInt(urlInfos[2])
+	cid := urlInfos[3]
+
+	contentType, content := attachments.GetAttachments(ctx, emailId, cid)
+
+	if len(content) == 0 {
+		response.NewErrorResponse(response.ParamsError, "", "").FPrint(w)
+		return
+	}
+	w.Header().Set("Content-Type", contentType)
+	w.Write(content)
+}
+
+func Download(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+	urlInfos := strings.Split(req.RequestURI, "/")
+	if len(urlInfos) != 5 {
+		response.NewErrorResponse(response.ParamsError, "", "").FPrint(w)
+		return
+	}
+	emailId := cast.ToInt(urlInfos[3])
+	index := cast.ToInt(urlInfos[4])
+
+	fileName, content := attachments.GetAttachmentsByIndex(ctx, emailId, index)
+
+	if len(content) == 0 {
+		response.NewErrorResponse(response.ParamsError, "", "").FPrint(w)
+		return
+	}
+	w.Header().Set("ContentType", "application/octet-stream")
+	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment;filename=%s", fileName))
+	w.Write(content)
+}

+ 8 - 0
server/controllers/base.go

@@ -0,0 +1,8 @@
+package controllers
+
+import (
+	"net/http"
+	"pmail/dto"
+)
+
+type HandlerFunc func(*dto.Context, http.ResponseWriter, *http.Request)

+ 49 - 0
server/controllers/email/detail.go

@@ -0,0 +1,49 @@
+package email
+
+import (
+	"encoding/json"
+	log "github.com/sirupsen/logrus"
+	"io"
+	"net/http"
+	"pmail/dto"
+	"pmail/dto/response"
+	"pmail/services/auth"
+	"pmail/services/detail"
+)
+
+type emailDetailRequest struct {
+	ID int `json:"id"`
+}
+
+func EmailDetail(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+	reqBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+	}
+	var retData emailDetailRequest
+	err = json.Unmarshal(reqBytes, &retData)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+	}
+
+	if retData.ID <= 0 {
+		response.NewErrorResponse(response.ParamsError, "ID错误", "").FPrint(w)
+		return
+	}
+
+	email, err := detail.GetEmailDetail(ctx, retData.ID, true)
+	if err != nil {
+		response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+		return
+	}
+
+	// 检查是否有权限
+	hasAuth := auth.HasAuth(ctx, email)
+	if !hasAuth {
+		response.NewErrorResponse(response.ParamsError, "", "").FPrint(w)
+		return
+	}
+
+	response.NewSuccessResponse(email).FPrint(w)
+
+}

+ 85 - 0
server/controllers/email/list.go

@@ -0,0 +1,85 @@
+package email
+
+import (
+	"encoding/json"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/cast"
+	"io"
+	"math"
+	"net/http"
+	"pmail/dto"
+	"pmail/dto/response"
+	"pmail/services/list"
+)
+
+type emailListResponse struct {
+	CurrentPage int         `json:"current_page"`
+	TotalPage   int         `json:"total_page"`
+	List        []*emilItem `json:"list"`
+}
+
+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"`
+}
+
+type User struct {
+	Name         string `json:"Name"`
+	EmailAddress string `json:"EmailAddress"`
+}
+
+type emailRequest struct {
+	Keyword     string `json:"keyword"`
+	Tag         string `json:"tag"`
+	CurrentPage int    `json:"current_page"`
+	PageSize    int    `json:"page_size"`
+}
+
+func EmailList(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+	var lst []*emilItem
+	reqBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+	}
+	var retData emailRequest
+	err = json.Unmarshal(reqBytes, &retData)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+	}
+
+	offset := 0
+	if retData.CurrentPage >= 1 {
+		offset = (retData.CurrentPage - 1) * retData.PageSize
+	}
+
+	if retData.PageSize == 0 {
+		retData.PageSize = 15
+	}
+
+	emailList, total := list.GetEmailList(ctx, retData.Tag, retData.Keyword, offset, retData.PageSize)
+
+	for _, email := range emailList {
+		var sender User
+		_ = 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,
+		})
+	}
+
+	ret := emailListResponse{
+		CurrentPage: retData.CurrentPage,
+		TotalPage:   cast.ToInt(math.Ceil(cast.ToFloat64(total) / cast.ToFloat64(retData.PageSize))),
+		List:        lst,
+	}
+	response.NewSuccessResponse(ret).FPrint(w)
+}

+ 203 - 0
server/controllers/email/send.go

@@ -0,0 +1,203 @@
+package email
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	log "github.com/sirupsen/logrus"
+	"io"
+	"net/http"
+	"pmail/config"
+	"pmail/dto"
+	"pmail/dto/parsemail"
+	"pmail/dto/response"
+	"pmail/hooks"
+	"pmail/i18n"
+	"pmail/mysql"
+	"pmail/smtp_server"
+	"pmail/utils/async"
+	"strings"
+	"time"
+)
+
+type sendRequest struct {
+	ReplyTo     []user       `json:"reply_to"`
+	From        user         `json:"from"`
+	To          []user       `json:"to"`
+	Bcc         []user       `json:"bcc"`
+	Cc          []user       `json:"cc"`
+	Subject     string       `json:"subject"`
+	Text        string       `json:"text"`   // Plaintext message (optional)
+	HTML        string       `json:"html"`   // Html message (optional)
+	Sender      user         `json:"sender"` // override From as SMTP envelope sender (optional)
+	ReadReceipt []string     `json:"read_receipt"`
+	Attachments []attachment `json:"attrs"`
+}
+
+type user struct {
+	Name  string `json:"name"`
+	Email string `json:"email"`
+}
+
+type attachment struct {
+	Name string `json:"name"`
+	Data string `json:"data"`
+}
+
+func Send(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+	reqBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+		response.NewErrorResponse(response.ParamsError, "params error", err.Error()).FPrint(w)
+		return
+	}
+	log.WithContext(ctx).Infof("发送邮件")
+
+	var reqData sendRequest
+	err = json.Unmarshal(reqBytes, &reqData)
+	if err != nil {
+		log.WithContext(ctx).Errorf("%+v", err)
+		response.NewErrorResponse(response.ParamsError, "params error", err.Error()).FPrint(w)
+		return
+	}
+
+	if reqData.From.Email == "" && reqData.From.Name != "" {
+		reqData.From.Email = reqData.From.Name + "@" + config.Instance.Domain
+	}
+
+	if reqData.From.Email == "" {
+		response.NewErrorResponse(response.ParamsError, "发件人必填", "发件人必填").FPrint(w)
+		return
+	}
+
+	if reqData.Subject == "" {
+		response.NewErrorResponse(response.ParamsError, "邮件标题必填", "邮件标题必填").FPrint(w)
+		return
+	}
+
+	if len(reqData.To) <= 0 {
+		response.NewErrorResponse(response.ParamsError, "收件人必填", "收件人必填").FPrint(w)
+		return
+	}
+
+	e := &parsemail.Email{}
+
+	for _, to := range reqData.To {
+		e.To = append(e.To, &parsemail.User{
+			Name:         to.Name,
+			EmailAddress: to.Email,
+		})
+	}
+
+	for _, bcc := range reqData.Bcc {
+		e.Bcc = append(e.Bcc, &parsemail.User{
+			Name:         bcc.Name,
+			EmailAddress: bcc.Email,
+		})
+	}
+
+	for _, cc := range reqData.Cc {
+		e.Cc = append(e.Cc, &parsemail.User{
+			Name:         cc.Name,
+			EmailAddress: cc.Email,
+		})
+	}
+
+	e.From = &parsemail.User{
+		Name:         reqData.From.Name,
+		EmailAddress: reqData.From.Email,
+	}
+	e.Text = []byte(reqData.Text)
+	e.HTML = []byte(reqData.HTML)
+	e.Subject = reqData.Subject
+	for _, att := range reqData.Attachments {
+		att.Data = strings.TrimPrefix(att.Data, "data:")
+		infos := strings.Split(att.Data, ";")
+		contentType := infos[0]
+		content := strings.TrimPrefix(infos[1], "base64,")
+		decoded, err := base64.StdEncoding.DecodeString(content)
+		if err != nil {
+			log.WithContext(ctx).Errorf("附件解码错误!%v", err)
+			response.NewErrorResponse(response.ParamsError, i18n.GetText(ctx.Lang, "att_err"), err.Error()).FPrint(w)
+			return
+		}
+		e.Attachments = append(e.Attachments, &parsemail.Attachment{
+			Filename:    att.Name,
+			ContentType: contentType,
+			Content:     decoded,
+		})
+
+	}
+
+	for _, hook := range hooks.HookList {
+		if hook == nil {
+			continue
+		}
+		async.New(ctx).Process(func() {
+			hook.SendBefore(ctx, e)
+		})
+	}
+
+	// 邮件落库
+	sql := "INSERT INTO email (type,subject, reply_to, from_name, from_address, `to`, bcc, cc, text, html, sender, attachments,spf_check, dkim_check, create_time,send_user_id,error) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+	sqlRes, sqlerr := mysql.Instance.Exec(mysql.WithContext(ctx, sql),
+		1,
+		e.Subject,
+		json2string(e.ReplyTo),
+		e.From.Name,
+		e.From.EmailAddress,
+		json2string(e.To),
+		json2string(e.Bcc),
+		json2string(e.Cc),
+		e.Text,
+		e.HTML,
+		json2string(e.Sender),
+		json2string(e.Attachments),
+		1,
+		1,
+		time.Now(),
+		ctx.UserInfo.ID,
+		"",
+	)
+	emailId, _ := sqlRes.LastInsertId()
+
+	if sqlerr != nil || emailId <= 0 {
+		log.Println("mysql insert error:", err.Error())
+		response.NewErrorResponse(response.ServerError, i18n.GetText(ctx.Lang, "send_fail"), err.Error()).FPrint(w)
+		return
+	}
+
+	async.New(ctx).Process(func() {
+		errMsg := ""
+		err, sendErr := smtp_server.Send(ctx, e)
+
+		for _, hook := range hooks.HookList {
+			if hook == nil {
+				continue
+			}
+			async.New(ctx).Process(func() {
+				hook.SendAfter(ctx, e, sendErr)
+			})
+		}
+
+		if err != nil {
+			errMsg = err.Error()
+			_, err := mysql.Instance.Exec(mysql.WithContext(ctx, "update email set status =2 ,error=? where id = ? "), errMsg, emailId)
+			if err != nil {
+				log.WithContext(ctx).Errorf("sql Error :%+v", err)
+			}
+		} else {
+			_, err := mysql.Instance.Exec(mysql.WithContext(ctx, "update email set status =1  where id = ? "), emailId)
+			if err != nil {
+				log.WithContext(ctx).Errorf("sql Error :%+v", err)
+			}
+		}
+
+	})
+
+	response.NewSuccessResponse(i18n.GetText(ctx.Lang, "succ")).FPrint(w)
+}
+
+func json2string(d any) string {
+	by, _ := json.Marshal(d)
+	return string(by)
+}

+ 39 - 0
server/controllers/group.go

@@ -0,0 +1,39 @@
+package controllers
+
+import (
+	"net/http"
+	"pmail/dto"
+	"pmail/dto/response"
+	"pmail/i18n"
+)
+
+type groupItem struct {
+	Label    string       `json:"label"`
+	Tag      string       `json:"tag"`
+	Children []*groupItem `json:"children"`
+}
+
+func GetUserGroup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+
+	retData := []*groupItem{
+		{
+			Label: i18n.GetText(ctx.Lang, "all_email"),
+			Children: []*groupItem{
+				{
+					Label: i18n.GetText(ctx.Lang, "inbox"),
+					Tag:   dto.SearchTag{Type: 0, Status: -1}.ToString(),
+				},
+				{
+					Label: i18n.GetText(ctx.Lang, "outbox"),
+					Tag:   dto.SearchTag{Type: 1, Status: 1}.ToString(),
+				},
+				{
+					Label: i18n.GetText(ctx.Lang, "sketch"),
+					Tag:   dto.SearchTag{Type: 1, Status: 0}.ToString(),
+				},
+			},
+		},
+	}
+
+	response.NewSuccessResponse(retData).FPrint(w)
+}

+ 59 - 0
server/controllers/login.go

@@ -0,0 +1,59 @@
+package controllers
+
+import (
+	"crypto/md5"
+	"database/sql"
+	"encoding/hex"
+	"encoding/json"
+	log "github.com/sirupsen/logrus"
+	"io"
+	"net/http"
+	"pmail/dto"
+	"pmail/dto/response"
+	"pmail/i18n"
+	"pmail/models"
+	"pmail/mysql"
+	"pmail/session"
+)
+
+type loginRequest struct {
+	Account  string `json:"account"`
+	Password string `json:"password"`
+}
+
+func Login(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+
+	reqBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		log.Errorf("%+v", err)
+	}
+	var retData loginRequest
+	err = json.Unmarshal(reqBytes, &retData)
+	if err != nil {
+		log.Errorf("%+v", err)
+	}
+
+	var user models.User
+
+	encodePwd := md5Encode(md5Encode(retData.Password+"pmail") + "pmail2023")
+
+	err = mysql.Instance.Get(&user, mysql.WithContext(ctx, "select * from user where account =? and password =?"),
+		retData.Account, encodePwd)
+	if err != nil && err != sql.ErrNoRows {
+		log.Errorf("%+v", err)
+	}
+
+	if user.ID != 0 {
+		userStr, _ := json.Marshal(user)
+		session.Instance.Put(req.Context(), "user", string(userStr))
+		response.NewSuccessResponse("").FPrint(w)
+	} else {
+		response.NewErrorResponse(response.ParamsError, i18n.GetText(ctx.Lang, "aperror"), "").FPrint(w)
+	}
+}
+
+func md5Encode(str string) string {
+	h := md5.New()
+	h.Write([]byte(str))
+	return hex.EncodeToString(h.Sum(nil))
+}

+ 13 - 0
server/controllers/ping.go

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

+ 41 - 0
server/controllers/settings.go

@@ -0,0 +1,41 @@
+package controllers
+
+import (
+	"encoding/json"
+	log "github.com/sirupsen/logrus"
+	"io"
+	"net/http"
+	"pmail/dto"
+	"pmail/dto/response"
+	"pmail/i18n"
+	"pmail/mysql"
+)
+
+type modifyPasswordRequest struct {
+	Password string `json:"password"`
+}
+
+func ModifyPassword(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+	reqBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		log.Errorf("%+v", err)
+	}
+	var retData modifyPasswordRequest
+	err = json.Unmarshal(reqBytes, &retData)
+	if err != nil {
+		log.Errorf("%+v", err)
+	}
+
+	if retData.Password != "" {
+		encodePwd := md5Encode(md5Encode(retData.Password+"pmail") + "pmail2023")
+
+		_, err := mysql.Instance.Exec(mysql.WithContext(ctx, "update user set password = ? where id =?"), encodePwd, ctx.UserInfo.ID)
+		if err != nil {
+			response.NewErrorResponse(response.ServerError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
+			return
+		}
+
+	}
+
+	response.NewSuccessResponse(i18n.GetText(ctx.Lang, "succ")).FPrint(w)
+}

+ 32 - 0
server/dto/context.go

@@ -0,0 +1,32 @@
+package dto
+
+import (
+	"context"
+	"pmail/models"
+)
+
+const (
+	LogID = "LogID"
+)
+
+type Context struct {
+	context.Context
+	UserInfo *models.User
+	values   map[string]any
+	Lang     string
+}
+
+func (c *Context) SetValue(key string, value any) {
+	if c.values == nil {
+		c.values = map[string]any{}
+	}
+	c.values[key] = value
+
+}
+
+func (c Context) GetValue(key string) any {
+	if c.values == nil {
+		return nil
+	}
+	return c.values[key]
+}

+ 97 - 0
server/dto/parsemail/dkim.go

@@ -0,0 +1,97 @@
+package parsemail
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	"github.com/emersion/go-msgauth/dkim"
+	log "github.com/sirupsen/logrus"
+	"golang.org/x/crypto/ed25519"
+	"io"
+	"os"
+	"pmail/config"
+	"strings"
+)
+
+type Dkim struct {
+	privateKey crypto.Signer
+}
+
+var instance *Dkim
+
+func Init() {
+	privateKey, err := loadPrivateKey(config.Instance.DkimPrivateKeyPath)
+	if err != nil {
+		panic("DKIM load fail! Please set dkim!  dkim私钥加载失败!请先设置dkim秘钥")
+	}
+
+	instance = &Dkim{
+		privateKey: privateKey,
+	}
+}
+
+func loadPrivateKey(path string) (crypto.Signer, error) {
+	b, err := os.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+
+	block, _ := pem.Decode(b)
+	if block == nil {
+		return nil, fmt.Errorf("no PEM data found")
+	}
+
+	switch strings.ToUpper(block.Type) {
+	case "PRIVATE KEY":
+		k, err := x509.ParsePKCS8PrivateKey(block.Bytes)
+		if err != nil {
+			return nil, err
+		}
+		return k.(crypto.Signer), nil
+	case "RSA PRIVATE KEY":
+		return x509.ParsePKCS1PrivateKey(block.Bytes)
+	case "EDDSA PRIVATE KEY":
+		if len(block.Bytes) != ed25519.PrivateKeySize {
+			return nil, fmt.Errorf("invalid Ed25519 private key size")
+		}
+		return ed25519.PrivateKey(block.Bytes), nil
+	default:
+		return nil, fmt.Errorf("unknown private key type: '%v'", block.Type)
+	}
+}
+
+func (p *Dkim) Sign(msgData string) []byte {
+	var b bytes.Buffer
+	r := strings.NewReader(msgData)
+
+	options := &dkim.SignOptions{
+		Domain:   config.Instance.Domain,
+		Selector: "default",
+		Signer:   p.privateKey,
+	}
+
+	if err := dkim.Sign(&b, r, options); err != nil {
+		log.Fatal(err)
+	}
+	return b.Bytes()
+}
+
+func Check(mail io.Reader) bool {
+
+	verifications, err := dkim.Verify(mail)
+	if err != nil {
+		log.Println(err)
+	}
+
+	for _, v := range verifications {
+		if v.Err == nil {
+			log.Println("Valid signature for:", v.Domain)
+		} else {
+			log.Println("Invalid signature for:", v.Domain, v.Err)
+			return false
+		}
+	}
+	return true
+}

+ 247 - 0
server/dto/parsemail/email.go

@@ -0,0 +1,247 @@
+package parsemail
+
+import (
+	"bytes"
+	"github.com/emersion/go-message"
+	_ "github.com/emersion/go-message/charset"
+	"github.com/emersion/go-message/mail"
+	log "github.com/sirupsen/logrus"
+	"io"
+	"net/textproto"
+	"pmail/dto"
+	"pmail/utils/array"
+	"regexp"
+	"strings"
+	"time"
+)
+
+type User struct {
+	EmailAddress string
+	Name         string
+}
+
+type Attachment struct {
+	Filename    string
+	ContentType string
+	Content     []byte
+	ContentID   string
+}
+
+// Email is the type used for email messages
+type Email struct {
+	ReplyTo     []*User
+	From        *User
+	To          []*User
+	Bcc         []*User
+	Cc          []*User
+	Subject     string
+	Text        []byte // Plaintext message (optional)
+	HTML        []byte // Html message (optional)
+	Sender      *User  // override From as SMTP envelope sender (optional)
+	Headers     textproto.MIMEHeader
+	Attachments []*Attachment
+	ReadReceipt []string
+	Date        string
+}
+
+func NewEmailFromReader(r io.Reader) *Email {
+	ret := &Email{}
+	m, err := message.Read(r)
+	if err != nil {
+		log.Errorf("email解析错误! Error %+v", err)
+	}
+
+	ret.From = buildUser(m.Header.Get("From"))
+	ret.To = buildUsers(m.Header.Values("To"))
+	ret.Cc = buildUsers(m.Header.Values("Cc"))
+	ret.ReplyTo = buildUsers(m.Header.Values("ReplyTo"))
+	ret.Sender = buildUser(m.Header.Get("Sender"))
+	if ret.Sender == nil {
+		ret.Sender = ret.From
+	}
+
+	ret.Subject, _ = m.Header.Text("Subject")
+
+	sendTime, err := time.Parse(time.RFC1123Z, m.Header.Get("Date"))
+	if err != nil {
+		sendTime = time.Now()
+	}
+	ret.Date = sendTime.Format(time.DateTime)
+	m.Walk(func(path []int, entity *message.Entity, err error) error {
+		return formatContent(entity, ret)
+	})
+	return ret
+}
+
+func formatContent(entity *message.Entity, ret *Email) error {
+	contentType, p, err := entity.Header.ContentType()
+
+	if err != nil {
+		log.Errorf("email read error! %+v", err)
+		return err
+	}
+
+	switch contentType {
+	case "multipart/alternative":
+	case "multipart/mixed":
+	case "text/plain":
+		ret.Text, _ = io.ReadAll(entity.Body)
+	case "text/html":
+		ret.HTML, _ = io.ReadAll(entity.Body)
+	case "multipart/related":
+		entity.Walk(func(path []int, entity *message.Entity, err error) error {
+			if t, _, _ := entity.Header.ContentType(); t == "multipart/related" {
+				return nil
+			}
+			return formatContent(entity, ret)
+		})
+	default:
+		c, _ := io.ReadAll(entity.Body)
+		fileName := p["name"]
+		if fileName == "" {
+			contentDisposition := entity.Header.Get("Content-Disposition")
+			r := regexp.MustCompile("filename=(.*)")
+			matchs := r.FindStringSubmatch(contentDisposition)
+			if len(matchs) == 2 {
+				fileName = matchs[1]
+			} else {
+				fileName = "no_name_file"
+			}
+		}
+
+		ret.Attachments = append(ret.Attachments, &Attachment{
+			Filename:    fileName,
+			ContentType: contentType,
+			Content:     c,
+			ContentID:   strings.TrimPrefix(strings.TrimSuffix(entity.Header.Get("Content-Id"), ">"), "<"),
+		})
+	}
+
+	return nil
+}
+
+func buildUser(str string) *User {
+	if str == "" {
+		return nil
+	}
+
+	ret := &User{}
+	args := strings.Split(str, " ")
+	if len(args) == 1 {
+		ret.EmailAddress = str
+		return ret
+	}
+
+	if len(args) > 2 {
+		targs := []string{
+			array.Join(args[0:len(args)-1], " "),
+			args[len(args)-1],
+		}
+		args = targs
+	}
+
+	args[0] = strings.Trim(args[0], "\"")
+	args[1] = strings.TrimPrefix(args[1], "<")
+	args[1] = strings.TrimSuffix(args[1], ">")
+
+	name, err := (&WordDecoder{}).Decode(strings.ReplaceAll(args[0], "\"", ""))
+	if err == nil {
+		ret.Name = name
+	} else {
+		ret.Name = args[0]
+	}
+	ret.EmailAddress = args[1]
+	return ret
+}
+
+func buildUsers(str []string) []*User {
+	var ret []*User
+	for _, s1 := range str {
+		for _, s := range strings.Split(s1, ",") {
+			s = strings.TrimSpace(s)
+			ret = append(ret, buildUser(s))
+		}
+	}
+
+	return ret
+}
+
+func (e *Email) BuildBytes(ctx *dto.Context) []byte {
+	var b bytes.Buffer
+
+	from := []*mail.Address{{e.From.Name, e.From.EmailAddress}}
+	to := []*mail.Address{}
+	for _, user := range e.To {
+		to = append(to, &mail.Address{
+			Name:    user.Name,
+			Address: user.EmailAddress,
+		})
+	}
+
+	// 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())
+}

+ 41 - 0
server/dto/parsemail/email_decode_test.go

@@ -0,0 +1,41 @@
+package parsemail
+
+import (
+	"fmt"
+	"os"
+	"strings"
+	"testing"
+)
+
+func TestDecodeEmailContentFromTxt(t *testing.T) {
+
+	c, _ := os.ReadFile("../../docs/gmail/带附件带图片.txt")
+
+	r := strings.NewReader(string(c))
+
+	email := NewEmailFromReader(r)
+
+	fmt.Println(email)
+}
+
+func TestDecodeEmailContentFromTxt3(t *testing.T) {
+
+	c, _ := os.ReadFile("../../docs/pmail/带附件.txt")
+
+	r := strings.NewReader(string(c))
+
+	email := NewEmailFromReader(r)
+
+	fmt.Println(email)
+}
+
+func TestDecodeEmailContentFromTxt2(t *testing.T) {
+	c, _ := os.ReadFile("../../docs/qqemail/带图片格式排版.txt")
+
+	r := strings.NewReader(string(c))
+
+	email := NewEmailFromReader(r)
+
+	fmt.Println(email)
+
+}

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

@@ -0,0 +1,43 @@
+package parsemail
+
+import (
+	"testing"
+)
+
+func TestEmail_domainMatch(t *testing.T) {
+	//e := &Email{}
+	//dnsNames := []string{
+	//	"*.mail.qq.com",
+	//	"993.dav.qq.com",
+	//	"993.eas.qq.com",
+	//	"993.imap.qq.com",
+	//	"993.pop.qq.com",
+	//	"993.smtp.qq.com",
+	//	"imap.qq.com",
+	//	"mx1.qq.com",
+	//	"mx2.qq.com",
+	//	"mx3.qq.com",
+	//	"pop.qq.com",
+	//	"smtp.qq.com",
+	//	"mail.qq.com",
+	//}
+	//
+	//fmt.Println(e.domainMatch("", dnsNames))
+	//fmt.Println(e.domainMatch("xjiangwei.cn", dnsNames))
+	//fmt.Println(e.domainMatch("qq.com", dnsNames))
+	//fmt.Println(e.domainMatch("test.aaa.mail.qq.com", dnsNames))
+	//fmt.Println(e.domainMatch("smtp.qq.com", dnsNames))
+	//fmt.Println(e.domainMatch("pop.qq.com", dnsNames))
+	//fmt.Println(e.domainMatch("test.mail.qq.com", dnsNames))
+
+}
+
+func Test_buildUser(t *testing.T) {
+	u := buildUser("Jinnrry N <jiangwei1995910@gmail.com>")
+	if u.EmailAddress != "jiangwei1995910@gmail.com" {
+		t.Error("error")
+	}
+	if u.Name != "Jinnrry N" {
+		t.Error("error")
+	}
+}

+ 426 - 0
server/dto/parsemail/encodedword.go

@@ -0,0 +1,426 @@
+package parsemail
+
+// copy from https://cs.opensource.google/go/go/+/refs/tags/go1.20.4:src/mime/encodedword.go
+// Golang官方库的解码函数不支持中文编码,此处实现支持了中文gbk和gb18030编码
+
+import (
+	"bytes"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"golang.org/x/text/encoding/simplifiedchinese"
+	"io"
+	"strings"
+	"unicode"
+	"unicode/utf8"
+)
+
+// A WordEncoder is an RFC 2047 encoded-word encoder.
+type WordEncoder byte
+
+const (
+	// BEncoding represents Base64 encoding scheme as defined by RFC 2045.
+	BEncoding = WordEncoder('b')
+	// QEncoding represents the Q-encoding scheme as defined by RFC 2047.
+	QEncoding = WordEncoder('q')
+)
+
+var (
+	errInvalidWord = errors.New("mime: invalid RFC 2047 encoded-word")
+)
+
+// Encode returns the encoded-word form of s. If s is ASCII without special
+// characters, it is returned unchanged. The provided charset is the IANA
+// charset name of s. It is case insensitive.
+func (e WordEncoder) Encode(charset, s string) string {
+	if !needsEncoding(s) {
+		return s
+	}
+	return e.encodeWord(charset, s)
+}
+
+func needsEncoding(s string) bool {
+	for _, b := range s {
+		if (b < ' ' || b > '~') && b != '\t' {
+			return true
+		}
+	}
+	return false
+}
+
+// encodeWord encodes a string into an encoded-word.
+func (e WordEncoder) encodeWord(charset, s string) string {
+	var buf strings.Builder
+	// Could use a hint like len(s)*3, but that's not enough for cases
+	// with word splits and too much for simpler inputs.
+	// 48 is close to maxEncodedWordLen/2, but adjusted to allocator size class.
+	buf.Grow(48)
+
+	e.openWord(&buf, charset)
+	if e == BEncoding {
+		e.bEncode(&buf, charset, s)
+	} else {
+		e.qEncode(&buf, charset, s)
+	}
+	closeWord(&buf)
+
+	return buf.String()
+}
+
+const (
+	// The maximum length of an encoded-word is 75 characters.
+	// See RFC 2047, section 2.
+	maxEncodedWordLen = 75
+	// maxContentLen is how much content can be encoded, ignoring the header and
+	// 2-byte footer.
+	maxContentLen = maxEncodedWordLen - len("=?UTF-8?q?") - len("?=")
+)
+
+var maxBase64Len = base64.StdEncoding.DecodedLen(maxContentLen)
+
+// bEncode encodes s using base64 encoding and writes it to buf.
+func (e WordEncoder) bEncode(buf *strings.Builder, charset, s string) {
+	w := base64.NewEncoder(base64.StdEncoding, buf)
+	// If the charset is not UTF-8 or if the content is short, do not bother
+	// splitting the encoded-word.
+	if !isUTF8(charset) || base64.StdEncoding.EncodedLen(len(s)) <= maxContentLen {
+		io.WriteString(w, s)
+		w.Close()
+		return
+	}
+
+	var currentLen, last, runeLen int
+	for i := 0; i < len(s); i += runeLen {
+		// Multi-byte characters must not be split across encoded-words.
+		// See RFC 2047, section 5.3.
+		_, runeLen = utf8.DecodeRuneInString(s[i:])
+
+		if currentLen+runeLen <= maxBase64Len {
+			currentLen += runeLen
+		} else {
+			io.WriteString(w, s[last:i])
+			w.Close()
+			e.splitWord(buf, charset)
+			last = i
+			currentLen = runeLen
+		}
+	}
+	io.WriteString(w, s[last:])
+	w.Close()
+}
+
+// qEncode encodes s using Q encoding and writes it to buf. It splits the
+// encoded-words when necessary.
+func (e WordEncoder) qEncode(buf *strings.Builder, charset, s string) {
+	// We only split encoded-words when the charset is UTF-8.
+	if !isUTF8(charset) {
+		writeQString(buf, s)
+		return
+	}
+
+	var currentLen, runeLen int
+	for i := 0; i < len(s); i += runeLen {
+		b := s[i]
+		// Multi-byte characters must not be split across encoded-words.
+		// See RFC 2047, section 5.3.
+		var encLen int
+		if b >= ' ' && b <= '~' && b != '=' && b != '?' && b != '_' {
+			runeLen, encLen = 1, 1
+		} else {
+			_, runeLen = utf8.DecodeRuneInString(s[i:])
+			encLen = 3 * runeLen
+		}
+
+		if currentLen+encLen > maxContentLen {
+			e.splitWord(buf, charset)
+			currentLen = 0
+		}
+		writeQString(buf, s[i:i+runeLen])
+		currentLen += encLen
+	}
+}
+
+// writeQString encodes s using Q encoding and writes it to buf.
+func writeQString(buf *strings.Builder, s string) {
+	for i := 0; i < len(s); i++ {
+		switch b := s[i]; {
+		case b == ' ':
+			buf.WriteByte('_')
+		case b >= '!' && b <= '~' && b != '=' && b != '?' && b != '_':
+			buf.WriteByte(b)
+		default:
+			buf.WriteByte('=')
+			buf.WriteByte(upperhex[b>>4])
+			buf.WriteByte(upperhex[b&0x0f])
+		}
+	}
+}
+
+// openWord writes the beginning of an encoded-word into buf.
+func (e WordEncoder) openWord(buf *strings.Builder, charset string) {
+	buf.WriteString("=?")
+	buf.WriteString(charset)
+	buf.WriteByte('?')
+	buf.WriteByte(byte(e))
+	buf.WriteByte('?')
+}
+
+// closeWord writes the end of an encoded-word into buf.
+func closeWord(buf *strings.Builder) {
+	buf.WriteString("?=")
+}
+
+// splitWord closes the current encoded-word and opens a new one.
+func (e WordEncoder) splitWord(buf *strings.Builder, charset string) {
+	closeWord(buf)
+	buf.WriteByte(' ')
+	e.openWord(buf, charset)
+}
+
+func isUTF8(charset string) bool {
+	return strings.EqualFold(charset, "UTF-8")
+}
+
+const upperhex = "0123456789ABCDEF"
+
+// A WordDecoder decodes MIME headers containing RFC 2047 encoded-words.
+type WordDecoder struct {
+	// CharsetReader, if non-nil, defines a function to generate
+	// charset-conversion readers, converting from the provided
+	// charset into UTF-8.
+	// Charsets are always lower-case. utf-8, iso-8859-1 and us-ascii charsets
+	// are handled by default.
+	// One of the CharsetReader's result values must be non-nil.
+	CharsetReader func(charset string, input io.Reader) (io.Reader, error)
+}
+
+// Decode decodes an RFC 2047 encoded-word.
+func (d *WordDecoder) Decode(word string) (string, error) {
+	// See https://tools.ietf.org/html/rfc2047#section-2 for details.
+	// Our decoder is permissive, we accept empty encoded-text.
+	if len(word) < 8 || !strings.HasPrefix(word, "=?") || !strings.HasSuffix(word, "?=") || strings.Count(word, "?") != 4 {
+		return "", errInvalidWord
+	}
+	word = word[2 : len(word)-2]
+
+	// split word "UTF-8?q?text" into "UTF-8", 'q', and "text"
+	charset, text, _ := strings.Cut(word, "?")
+	if charset == "" {
+		return "", errInvalidWord
+	}
+	encoding, text, _ := strings.Cut(text, "?")
+	if len(encoding) != 1 {
+		return "", errInvalidWord
+	}
+
+	content, err := decode(encoding[0], text)
+	if err != nil {
+		return "", err
+	}
+
+	var buf strings.Builder
+	if err := d.convert(&buf, charset, content); err != nil {
+		return "", err
+	}
+	return buf.String(), nil
+}
+
+// DecodeHeader decodes all encoded-words of the given string. It returns an
+// error if and only if CharsetReader of d returns an error.
+func (d *WordDecoder) DecodeHeader(header string) (string, error) {
+	// If there is no encoded-word, returns before creating a buffer.
+	i := strings.Index(header, "=?")
+	if i == -1 {
+		return header, nil
+	}
+
+	var buf strings.Builder
+
+	buf.WriteString(header[:i])
+	header = header[i:]
+
+	betweenWords := false
+	for {
+		start := strings.Index(header, "=?")
+		if start == -1 {
+			break
+		}
+		cur := start + len("=?")
+
+		i := strings.Index(header[cur:], "?")
+		if i == -1 {
+			break
+		}
+		charset := header[cur : cur+i]
+		cur += i + len("?")
+
+		if len(header) < cur+len("Q??=") {
+			break
+		}
+		encoding := header[cur]
+		cur++
+
+		if header[cur] != '?' {
+			break
+		}
+		cur++
+
+		j := strings.Index(header[cur:], "?=")
+		if j == -1 {
+			break
+		}
+		text := header[cur : cur+j]
+		end := cur + j + len("?=")
+
+		content, err := decode(encoding, text)
+		if err != nil {
+			betweenWords = false
+			buf.WriteString(header[:start+2])
+			header = header[start+2:]
+			continue
+		}
+
+		// Write characters before the encoded-word. White-space and newline
+		// characters separating two encoded-words must be deleted.
+		if start > 0 && (!betweenWords || hasNonWhitespace(header[:start])) {
+			buf.WriteString(header[:start])
+		}
+
+		if err := d.convert(&buf, charset, content); err != nil {
+			return "", err
+		}
+
+		header = header[end:]
+		betweenWords = true
+	}
+
+	if len(header) > 0 {
+		buf.WriteString(header)
+	}
+
+	return buf.String(), nil
+}
+
+func decode(encoding byte, text string) ([]byte, error) {
+	switch encoding {
+	case 'B', 'b':
+		return base64.StdEncoding.DecodeString(text)
+	case 'Q', 'q':
+		return qDecode(text)
+	default:
+		return nil, errInvalidWord
+	}
+}
+
+func (d *WordDecoder) convert(buf *strings.Builder, charset string, content []byte) error {
+	switch {
+	case strings.EqualFold("utf-8", charset):
+		buf.Write(content)
+	case strings.EqualFold("iso-8859-1", charset):
+		for _, c := range content {
+			buf.WriteRune(rune(c))
+		}
+	case strings.EqualFold("us-ascii", charset):
+		for _, c := range content {
+			if c >= utf8.RuneSelf {
+				buf.WriteRune(unicode.ReplacementChar)
+			} else {
+				buf.WriteByte(c)
+			}
+		}
+	case strings.EqualFold("gb18030", charset):
+		decodeBytes, err := simplifiedchinese.GB18030.NewDecoder().Bytes(content)
+		if err != nil {
+			return err
+		}
+		buf.Write(decodeBytes)
+	case strings.EqualFold("gbk", charset):
+		decodeBytes, err := simplifiedchinese.GBK.NewDecoder().Bytes(content)
+		if err != nil {
+			return err
+		}
+		buf.Write(decodeBytes)
+	default:
+		if d.CharsetReader == nil {
+			return fmt.Errorf("mime: unhandled charset %q", charset)
+		}
+		r, err := d.CharsetReader(strings.ToLower(charset), bytes.NewReader(content))
+		if err != nil {
+			return err
+		}
+		if _, err = io.Copy(buf, r); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// hasNonWhitespace reports whether s (assumed to be ASCII) contains at least
+// one byte of non-whitespace.
+func hasNonWhitespace(s string) bool {
+	for _, b := range s {
+		switch b {
+		// Encoded-words can only be separated by linear white spaces which does
+		// not include vertical tabs (\v).
+		case ' ', '\t', '\n', '\r':
+		default:
+			return true
+		}
+	}
+	return false
+}
+
+// qDecode decodes a Q encoded string.
+func qDecode(s string) ([]byte, error) {
+	dec := make([]byte, len(s))
+	n := 0
+	for i := 0; i < len(s); i++ {
+		switch c := s[i]; {
+		case c == '_':
+			dec[n] = ' '
+		case c == '=':
+			if i+2 >= len(s) {
+				return nil, errInvalidWord
+			}
+			b, err := readHexByte(s[i+1], s[i+2])
+			if err != nil {
+				return nil, err
+			}
+			dec[n] = b
+			i += 2
+		case (c <= '~' && c >= ' ') || c == '\n' || c == '\r' || c == '\t':
+			dec[n] = c
+		default:
+			return nil, errInvalidWord
+		}
+		n++
+	}
+
+	return dec[:n], nil
+}
+
+// readHexByte returns the byte from its quoted-printable representation.
+func readHexByte(a, b byte) (byte, error) {
+	var hb, lb byte
+	var err error
+	if hb, err = fromHex(a); err != nil {
+		return 0, err
+	}
+	if lb, err = fromHex(b); err != nil {
+		return 0, err
+	}
+	return hb<<4 | lb, nil
+}
+
+func fromHex(b byte) (byte, error) {
+	switch {
+	case b >= '0' && b <= '9':
+		return b - '0', nil
+	case b >= 'A' && b <= 'F':
+		return b - 'A' + 10, nil
+	// Accept badly encoded bytes.
+	case b >= 'a' && b <= 'f':
+		return b - 'a' + 10, nil
+	}
+	return 0, fmt.Errorf("mime: invalid hex byte %#02x", b)
+}

+ 37 - 0
server/dto/response/response.go

@@ -0,0 +1,37 @@
+package response
+
+import (
+	"encoding/json"
+	"net/http"
+)
+
+const (
+	NeedLogin   = 403
+	ParamsError = 100
+	ServerError = 500
+)
+
+type Response struct {
+	ErrorNo  int    `json:"errorNo"`
+	ErrorMsg string `json:"errorMsg"`
+	Data     any    `json:"data"`
+}
+
+func (p *Response) FPrint(w http.ResponseWriter) {
+	bytesData, _ := json.Marshal(p)
+	w.Write(bytesData)
+}
+
+func NewSuccessResponse(data any) *Response {
+	return &Response{
+		Data: data,
+	}
+}
+
+func NewErrorResponse(errorNo int, errorMsg string, data any) *Response {
+	return &Response{
+		ErrorNo:  errorNo,
+		ErrorMsg: errorMsg,
+		Data:     data,
+	}
+}

+ 13 - 0
server/dto/tag.go

@@ -0,0 +1,13 @@
+package dto
+
+import "encoding/json"
+
+type SearchTag struct {
+	Type   int `json:"type"`
+	Status int `json:"status"`
+}
+
+func (t SearchTag) ToString() string {
+	data, _ := json.Marshal(t)
+	return string(data)
+}

+ 28 - 0
server/go.mod

@@ -0,0 +1,28 @@
+module pmail
+
+go 1.20
+
+require (
+	github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24
+	github.com/alexedwards/scs/v2 v2.5.1
+	github.com/emersion/go-msgauth v0.6.6
+	github.com/emersion/go-smtp v0.16.0
+	github.com/go-sql-driver/mysql v1.7.1
+	github.com/jmoiron/sqlx v1.3.5
+	github.com/mileusna/spf v0.9.5
+	github.com/sirupsen/logrus v1.9.3
+	github.com/spf13/cast v1.5.1
+	golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898
+	golang.org/x/text v0.3.8
+)
+
+require (
+	github.com/emersion/go-message v0.16.0 // indirect
+	github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
+	github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
+	github.com/miekg/dns v1.1.50 // indirect
+	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
+	golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
+	golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
+	golang.org/x/tools v0.1.12 // indirect
+)

+ 99 - 0
server/go.sum

@@ -0,0 +1,99 @@
+github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24 h1:1jXpX7IE/zuf9FZQJpqZNepXqW8mq6NLzplHDCA43HY=
+github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24/go.mod h1:ShejCOaSJCEjCWjc7YBrgy2xd0Kp+wiyBdzTNQrAGn4=
+github.com/alexedwards/scs/v2 v2.5.1 h1:EhAz3Kb3OSQzD8T+Ub23fKsiuvE0GzbF5Lgn0uTwM3Y=
+github.com/alexedwards/scs/v2 v2.5.1/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
+github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
+github.com/emersion/go-message v0.16.0 h1:uZLz8ClLv3V5fSFF/fFdW9jXjrZkXIpE1Fn8fKx7pO4=
+github.com/emersion/go-message v0.16.0/go.mod h1:pDJDgf/xeUIF+eicT6B/hPX/ZbEorKkUMPOxrPVG2eQ=
+github.com/emersion/go-milter v0.3.3/go.mod h1:ablHK0pbLB83kMFBznp/Rj8aV+Kc3jw8cxzzmCNLIOY=
+github.com/emersion/go-msgauth v0.6.6 h1:buv5lL8v/3v4RpHnQFS2IPhE3nxSRX+AxnrEJbDbHhA=
+github.com/emersion/go-msgauth v0.6.6/go.mod h1:A+/zaz9bzukLM6tRWRgJ3BdrBi+TFKTvQ3fGMFOI9SM=
+github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
+github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
+github.com/emersion/go-smtp v0.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8=
+github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
+github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
+github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
+github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
+github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
+github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
+github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
+github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
+github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
+github.com/mileusna/spf v0.9.5 h1:P6cmaIBwrhZaP9stXMzGOtxe+gIu65OVbZCmrAv9rgU=
+github.com/mileusna/spf v0.9.5/go.mod h1:o6IdTae6QptAbLgx/+ueXSTSpkG+f1cqLemQJSew8sI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
+github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 h1:SLP7Q4Di66FONjDJbCYrCRrh97focO6sLogHO7/g8F0=
+golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 28 - 0
server/hooks/base.go

@@ -0,0 +1,28 @@
+package hooks
+
+import (
+	"pmail/dto"
+	"pmail/dto/parsemail"
+	"pmail/hooks/wechat_push"
+)
+
+type EmailHook interface {
+	// SendBefore 邮件发送前的数据
+	SendBefore(ctx *dto.Context, email *parsemail.Email)
+	// SendAfter 邮件发送后的数据,err是每个收信服务器的错误信息
+	SendAfter(ctx *dto.Context, email *parsemail.Email, err map[string]error)
+	// ReceiveParseBefore 接收到邮件,解析之前的原始数据
+	ReceiveParseBefore(email []byte)
+	// ReceiveParseAfter 接收到邮件,解析之后的结构化数据
+	ReceiveParseAfter(email *parsemail.Email)
+}
+
+// HookList
+var HookList []EmailHook
+
+// Init 这里注册hook对象
+func Init() {
+	HookList = []EmailHook{
+		wechat_push.NewWechatPushHook(),
+	}
+}

+ 108 - 0
server/hooks/wechat_push/wechat_push.go

@@ -0,0 +1,108 @@
+package wechat_push
+
+import (
+	"encoding/json"
+	"fmt"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/cast"
+	"io"
+	"net/http"
+	"pmail/config"
+	"pmail/dto"
+	"pmail/dto/parsemail"
+	"strings"
+	"time"
+)
+
+type accessTokenRes struct {
+	AccessToken string `db:"access_token" json:"access_token"`
+	ExpiresIn   int    `db:"expires_in" json:"expires_in"`
+}
+
+type WeChatPushHook struct {
+	appId        string
+	secret       string
+	token        string
+	tokenExpires int64
+	templateId   string
+	pushUser     string
+}
+
+func (w *WeChatPushHook) SendBefore(ctx *dto.Context, email *parsemail.Email) {
+
+}
+
+func (w *WeChatPushHook) SendAfter(ctx *dto.Context, email *parsemail.Email, err map[string]error) {
+
+}
+
+func (w *WeChatPushHook) ReceiveParseBefore(email []byte) {
+
+}
+
+func (w *WeChatPushHook) ReceiveParseAfter(email *parsemail.Email) {
+	w.sendUserMsg(nil, w.pushUser, string(email.Text))
+}
+
+func (w *WeChatPushHook) getWxAccessToken() string {
+	if w.tokenExpires > time.Now().Unix() {
+		return w.token
+	}
+	resp, err := http.Get(fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", w.appId, w.secret))
+	if err != nil {
+		return ""
+	}
+	body, _ := io.ReadAll(resp.Body)
+	var ret accessTokenRes
+	_ = json.Unmarshal(body, &ret)
+	if ret.AccessToken != "" {
+		w.token = ret.AccessToken
+		w.tokenExpires = time.Now().Unix() + cast.ToInt64(ret.ExpiresIn)
+	}
+	return ret.AccessToken
+}
+
+type sendMsgRequest struct {
+	Touser      string   `db:"touser" json:"touser"`
+	Template_id string   `db:"template_id" json:"template_id"`
+	Url         string   `db:"url" json:"url"`
+	Data        SendData `db:"data" json:"data"`
+}
+type SendData struct {
+	Content DataItem `json:"Content"`
+}
+type DataItem struct {
+	Value string `json:"value"`
+	Color string `json:"color"`
+}
+
+func (w *WeChatPushHook) sendUserMsg(ctx *dto.Context, userId string, content string) {
+	sendMsgReq, _ := json.Marshal(sendMsgRequest{
+		Touser:      userId,
+		Template_id: w.templateId,
+		Url:         "http://mail." + config.Instance.Domain,
+		Data:        SendData{Content: DataItem{Value: content, Color: "#000000"}},
+	})
+
+	_, err := http.Post("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token="+w.getWxAccessToken(), "application/json", strings.NewReader(string(sendMsgReq)))
+	if err != nil {
+		log.WithContext(ctx).Errorf("wechat push error %+v", err)
+	}
+
+}
+func NewWechatPushHook() *WeChatPushHook {
+	if config.Instance.WeChatPushAppId != "" &&
+		config.Instance.WeChatPushSecret != "" &&
+		config.Instance.WeChatPushTemplateId != "" &&
+		config.Instance.WeChatPushUserId != "" {
+		ret := &WeChatPushHook{
+			appId:      config.Instance.WeChatPushAppId,
+			secret:     config.Instance.WeChatPushSecret,
+			templateId: config.Instance.WeChatPushTemplateId,
+			pushUser:   config.Instance.WeChatPushUserId,
+		}
+		return ret
+
+	}
+	return nil
+}

+ 19 - 0
server/hooks/wechat_push/wechat_push_test.go

@@ -0,0 +1,19 @@
+package wechat_push
+
+import (
+	"pmail/config"
+	"pmail/dto/parsemail"
+	"testing"
+)
+
+func testInit() {
+
+	config.Init()
+
+}
+func TestWeChatPushHook_ReceiveParseAfter(t *testing.T) {
+	testInit()
+
+	w := NewWechatPushHook()
+	w.ReceiveParseAfter(&parsemail.Email{Subject: "标题", Text: []byte("文本内容")})
+}

+ 133 - 0
server/http_server/main.go

@@ -0,0 +1,133 @@
+package http_server
+
+import (
+	"bytes"
+	"embed"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/cast"
+	"io/fs"
+	"math/rand"
+	"net"
+	"net/http"
+	"os"
+	"pmail/controllers"
+	"pmail/controllers/email"
+	"pmail/dto"
+	"pmail/dto/response"
+	"pmail/session"
+	"time"
+)
+
+//go:embed dist/*
+var local embed.FS
+
+var ip string
+
+const HttpPort = 80
+
+func Start() {
+	log.Infof("Http Server Start at :%d", HttpPort)
+
+	mux := http.NewServeMux()
+
+	fe, err := fs.Sub(local, "dist")
+	if err != nil {
+		panic(err)
+	}
+	mux.Handle("/", http.FileServer(http.FS(fe)))
+
+	mux.HandleFunc("/api/ping", contextIterceptor(controllers.Ping))
+	mux.HandleFunc("/api/login", contextIterceptor(controllers.Login))
+	mux.HandleFunc("/api/group", contextIterceptor(controllers.GetUserGroup))
+	mux.HandleFunc("/api/email/list", contextIterceptor(email.EmailList))
+	mux.HandleFunc("/api/email/detail", contextIterceptor(email.EmailDetail))
+	mux.HandleFunc("/api/email/send", contextIterceptor(email.Send))
+	mux.HandleFunc("/api/settings/modify_password", contextIterceptor(controllers.ModifyPassword))
+	mux.HandleFunc("/attachments/", contextIterceptor(controllers.GetAttachments))
+	mux.HandleFunc("/attachments/download/", contextIterceptor(controllers.Download))
+
+	server := &http.Server{
+		Addr:         fmt.Sprintf(":%d", HttpPort),
+		Handler:      session.Instance.LoadAndSave(mux),
+		ReadTimeout:  time.Second * 60,
+		WriteTimeout: time.Second * 60,
+	}
+
+	//err := server.ListenAndServeTLS( "config/ssl/public.crt", "config/ssl/private.key", nil)
+	err = server.ListenAndServe()
+	if err != nil {
+		panic(err)
+	}
+}
+
+func getLocalIP() string {
+	ip := "127.0.0.1"
+	addrs, err := net.InterfaceAddrs()
+	if err != nil {
+		return ip
+	}
+	for _, a := range addrs {
+		if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
+			if ipnet.IP.To4() != nil {
+				ip = ipnet.IP.String()
+				break
+			}
+		}
+	}
+
+	return ip
+}
+
+func genLogID() string {
+	r := rand.New(rand.NewSource(time.Now().UnixMicro()))
+	if ip == "" {
+		ip = getLocalIP()
+	}
+	now := time.Now()
+	timestamp := uint32(now.Unix())
+	timeNano := now.UnixNano()
+	pid := os.Getpid()
+	b := bytes.Buffer{}
+
+	b.WriteString(hex.EncodeToString(net.ParseIP(ip).To4()))
+	b.WriteString(fmt.Sprintf("%x", timestamp&0xffffffff))
+	b.WriteString(fmt.Sprintf("%04x", timeNano&0xffff))
+	b.WriteString(fmt.Sprintf("%04x", pid&0xffff))
+	b.WriteString(fmt.Sprintf("%06x", r.Int31n(1<<24)))
+	b.WriteString("b0")
+
+	return b.String()
+}
+
+// 注入context
+func contextIterceptor(h controllers.HandlerFunc) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		if w.Header().Get("Content-Type") == "" {
+			w.Header().Set("Content-Type", "application/json")
+		}
+
+		ctx := &dto.Context{}
+		ctx.Context = r.Context()
+		ctx.SetValue(dto.LogID, genLogID())
+		lang := r.Header.Get("Lang")
+		if lang == "" {
+			lang = "en"
+		}
+		ctx.Lang = lang
+
+		user := cast.ToString(session.Instance.Get(ctx, "user"))
+		if user != "" {
+			_ = json.Unmarshal([]byte(user), &ctx.UserInfo)
+		}
+		if ctx.UserInfo == nil || ctx.UserInfo.ID == 0 {
+			if r.URL.Path != "/api/ping" && r.URL.Path != "/api/login" {
+				response.NewErrorResponse(response.NeedLogin, "登陆已失效!", "").FPrint(w)
+				return
+			}
+		}
+		h(ctx, w, r)
+	}
+}

+ 41 - 0
server/i18n/i18n.go

@@ -0,0 +1,41 @@
+package i18n
+
+var (
+	cn = map[string]string{
+		"all_email":   "全部邮件数据",
+		"inbox":       "收件箱",
+		"outbox":      "发件箱",
+		"sketch":      "草稿箱",
+		"aperror":     "账号或密码错误",
+		"unknowError": "未知错误",
+		"succ":        "成功",
+		"send_fail":   "发送失败",
+		"att_err":     "附件解码错误",
+	}
+	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",
+	}
+)
+
+func GetText(lang, key string) string {
+	if lang == "zhCn" {
+		text, exist := cn[key]
+		if !exist {
+			return ""
+		}
+		return text
+	}
+	text, exist := en[key]
+	if !exist {
+		return ""
+	}
+	return text
+}

+ 81 - 0
server/main.go

@@ -0,0 +1,81 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+	log "github.com/sirupsen/logrus"
+	"os"
+	"pmail/config"
+	"pmail/dto"
+	"pmail/dto/parsemail"
+	"pmail/hooks"
+	"pmail/http_server"
+	"pmail/mysql"
+	"pmail/session"
+	"pmail/smtp_server"
+	"time"
+)
+
+type logFormatter struct {
+}
+
+// Format 定义日志输出格式
+func (l *logFormatter) Format(entry *log.Entry) ([]byte, error) {
+	b := bytes.Buffer{}
+
+	b.WriteString(fmt.Sprintf("[%s]", entry.Level.String()))
+	b.WriteString(fmt.Sprintf("[%s]", entry.Time.Format("2006-01-02 15:04:05")))
+	if entry.Context != nil {
+		b.WriteString(fmt.Sprintf("[%s]", entry.Context.(*dto.Context).GetValue(dto.LogID)))
+	}
+	b.WriteString(fmt.Sprintf("[%s:%d]", entry.Caller.File, entry.Caller.Line))
+	b.WriteString(entry.Message)
+
+	b.WriteString("\n")
+	return b.Bytes(), nil
+}
+
+var (
+	gitHash   string
+	buildTime string
+	goVersion string
+)
+
+func main() {
+	// 设置日志格式为json格式
+	//log.SetFormatter(&log.JSONFormatter{})
+
+	log.SetFormatter(&logFormatter{})
+	log.SetReportCaller(true)
+
+	// 设置将日志输出到标准输出(默认的输出为stderr,标准错误)
+	// 日志消息输出可以是任意的io.writer类型
+	log.SetOutput(os.Stdout)
+
+	// 设置日志级别为warn以上
+	log.SetLevel(log.DebugLevel)
+	var cst, _ = time.LoadLocation("Asia/Shanghai")
+	time.Local = cst
+
+	config.Init()
+	parsemail.Init()
+	mysql.Init()
+	session.Init()
+	hooks.Init()
+
+	// smtp server start
+	go smtp_server.Start()
+
+	// http server start
+	go http_server.Start()
+
+	log.Infoln("***************************************************")
+	log.Infoln("***\tServer Start Success Version:1.0.0")
+	log.Infof("***\tGit Commit Hash: %s ", gitHash)
+	log.Infof("***\tBuild TimeStamp: %s ", buildTime)
+	log.Infof("***\tBuild GoLang Version: %s ", goVersion)
+	log.Infoln("***************************************************")
+
+	s := make(chan bool)
+	<-s
+}

+ 8 - 0
server/models/User.go

@@ -0,0 +1,8 @@
+package models
+
+type User struct {
+	ID       int    `db:"id"`
+	Account  string `db:"account"`
+	Name     string `db:"name"`
+	Password string `db:"password"`
+}

+ 7 - 0
server/models/auth.go

@@ -0,0 +1,7 @@
+package models
+
+type UserAuth struct {
+	ID           int    `db:"id"`
+	UserID       int    `db:"user_id"`
+	EmailAccount string `db:"email_account"`
+}

+ 79 - 0
server/models/email.go

@@ -0,0 +1,79 @@
+package models
+
+import (
+	"database/sql"
+	"encoding/json"
+	"time"
+)
+
+type Email struct {
+	Id           int            `db:"id" json:"id"`
+	Type         int8           `db:"type" json:"type"`
+	Subject      string         `db:"subject" json:"subject"`
+	ReplyTo      string         `db:"reply_to" json:"reply_to"`
+	FromName     string         `db:"from_name" json:"from_name"`
+	FromAddress  string         `db:"from_address" json:"from_address"`
+	To           string         `db:"to" json:"to"`
+	Bcc          string         `db:"bcc" json:"bcc"`
+	Cc           string         `db:"cc" json:"cc"`
+	Text         sql.NullString `db:"text" json:"text"`
+	Html         sql.NullString `db:"html" json:"html"`
+	Sender       string         `db:"sender" json:"sender"`
+	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"`
+	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"`
+	IsRead       int8           `db:"is_read" json:"is_read"`
+	Error        sql.NullString `db:"error" json:"error"`
+	SendDate     time.Time      `db:"send_date" json:"send_date"`
+	CreateTime   time.Time      `db:"create_time" json:"create_time"`
+}
+
+type attachments struct {
+	Filename    string
+	ContentType string
+	Index       int
+	//Content     []byte
+}
+
+func (d Email) MarshalJSON() ([]byte, error) {
+	type Alias Email
+
+	var allAtt = []attachments{}
+	var showAtt = []attachments{}
+	if d.Attachments != "" {
+		_ = json.Unmarshal([]byte(d.Attachments), &allAtt)
+		for i, att := range allAtt {
+			att.Index = i
+			if att.ContentType == "application/octet-stream" {
+				showAtt = append(showAtt, att)
+			}
+
+		}
+	}
+
+	return json.Marshal(&struct {
+		Alias
+		CronSendTime string        `json:"send_time"`
+		SendDate     string        `json:"send_date"`
+		UpdateTime   string        `json:"update_time"`
+		CreateTime   string        `json:"create_time"`
+		Text         string        `json:"text"`
+		Html         string        `json:"html"`
+		Error        string        `json:"error"`
+		Attachments  []attachments `json:"attachments"`
+	}{
+		Alias:        (Alias)(d),
+		CronSendTime: d.CronSendTime.Format("2006-01-02 15:04:05"),
+		UpdateTime:   d.UpdateTime.Format("2006-01-02 15:04:05"),
+		CreateTime:   d.CreateTime.Format("2006-01-02 15:04:05"),
+		SendDate:     d.SendDate.Format("2006-01-02 15:04:05"),
+		Text:         d.Text.String,
+		Html:         d.Html.String,
+		Error:        d.Error.String,
+		Attachments:  showAtt,
+	})
+}

+ 87 - 0
server/mysql/init.go

@@ -0,0 +1,87 @@
+package mysql
+
+import (
+	"fmt"
+	_ "github.com/go-sql-driver/mysql"
+	"github.com/jmoiron/sqlx"
+	log "github.com/sirupsen/logrus"
+	"pmail/config"
+	"pmail/dto"
+)
+
+var Instance *sqlx.DB
+
+func Init() {
+	dsn := config.Instance.MysqlDSN
+	var err error
+	Instance, err = sqlx.Open("mysql", dsn)
+	if err != nil {
+		panic(err)
+	}
+	Instance.SetMaxOpenConns(100)
+	Instance.SetMaxIdleConns(10)
+	showMySQLCharacterSet()
+	checkTable()
+}
+
+func WithContext(ctx *dto.Context, sql string) string {
+	if ctx != nil {
+		logId := ctx.GetValue(dto.LogID)
+		return fmt.Sprintf("/* %s */ %s", logId, sql)
+	}
+	return sql
+}
+
+type tables struct {
+	TablesInPmail string `db:"Tables_in_pmail"`
+}
+
+func checkTable() {
+	var res []*tables
+	err := Instance.Select(&res, "show tables")
+	if err != nil {
+		panic(err)
+	}
+	existTable := map[string]struct{}{}
+	for _, tableName := range res {
+		existTable[tableName.TablesInPmail] = struct{}{}
+	}
+
+	for tableName, createSQL := range config.Instance.Tables {
+		if _, ok := existTable[tableName]; !ok {
+			_, err = Instance.Exec(createSQL)
+			log.Infof("Create Table: %s", createSQL)
+			if err != nil {
+				panic(err)
+			}
+
+			if initData, ok := config.Instance.TablesInitData[tableName]; ok {
+				_, err = Instance.Exec(initData)
+				log.Infof("Init Table: %s", initData)
+				if err != nil {
+					panic(err)
+				}
+			}
+
+		}
+	}
+}
+
+func showMySQLCharacterSet() {
+	var res []struct {
+		Variable_name string `db:"Variable_name"`
+		Value         string `db:"Value"`
+	}
+	err := Instance.Select(&res, "show variables like '%character%';")
+	log.Debugf("%+v  %+v", res, err)
+
+}
+
+func testSlowLog() {
+	var res []struct {
+		Value string `db:"Value"`
+	}
+	err := Instance.Select(&res, "/* asddddasad */select /* this is test */ sleep(4) as Value")
+	log.Debugf("%+v  %+v", res, err)
+
+}

+ 60 - 0
server/services/attachments/attachments.go

@@ -0,0 +1,60 @@
+package attachments
+
+import (
+	"encoding/json"
+	log "github.com/sirupsen/logrus"
+	"pmail/dto"
+	"pmail/dto/parsemail"
+	"pmail/models"
+	"pmail/mysql"
+	"pmail/services/auth"
+)
+
+func GetAttachments(ctx *dto.Context, emailId int, cid string) (string, []byte) {
+
+	// 获取邮件内容
+	var email models.Email
+	err := mysql.Instance.Get(&email, mysql.WithContext(ctx, "select * from email where id = ?"), emailId)
+	if err != nil {
+		log.WithContext(ctx).Errorf("SQL error:%+v", err)
+		return "", nil
+	}
+
+	// 检查权限
+	if !auth.HasAuth(ctx, &email) {
+		return "", nil
+	}
+
+	var atts []parsemail.Attachment
+	_ = json.Unmarshal([]byte(email.Attachments), &atts)
+	for _, att := range atts {
+		if att.ContentID == cid {
+			return att.ContentType, att.Content
+		}
+	}
+	return "", nil
+}
+
+func GetAttachmentsByIndex(ctx *dto.Context, emailId int, index int) (string, []byte) {
+
+	// 获取邮件内容
+	var email models.Email
+	err := mysql.Instance.Get(&email, mysql.WithContext(ctx, "select * from email where id = ?"), emailId)
+	if err != nil {
+		log.WithContext(ctx).Errorf("SQL error:%+v", err)
+		return "", nil
+	}
+
+	// 检查权限
+	if !auth.HasAuth(ctx, &email) {
+		return "", nil
+	}
+
+	var atts []parsemail.Attachment
+	_ = json.Unmarshal([]byte(email.Attachments), &atts)
+
+	if len(atts) > index {
+		return atts[index].Filename, atts[index].Content
+	}
+	return "", nil
+}

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

@@ -0,0 +1,33 @@
+package auth
+
+import (
+	log "github.com/sirupsen/logrus"
+	"pmail/dto"
+	"pmail/models"
+	"pmail/mysql"
+	"strings"
+)
+
+// HasAuth 检查当前用户是否有某个邮件的auth
+func HasAuth(ctx *dto.Context, email *models.Email) bool {
+	// 获取当前用户的auth
+	var auth []models.UserAuth
+	err := mysql.Instance.Select(&auth, mysql.WithContext(ctx, "select * from user_auth where user_id = ?"), ctx.UserInfo.ID)
+	if err != nil {
+		log.WithContext(ctx).Errorf("SQL error:%+v", err)
+		return false
+	}
+
+	var hasAuth bool
+	for _, userAuth := range auth {
+		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) {
+			hasAuth = true
+			break
+		}
+	}
+
+	return hasAuth
+}

+ 43 - 0
server/services/detail/detail.go

@@ -0,0 +1,43 @@
+package detail
+
+import (
+	"database/sql"
+	"encoding/json"
+	"fmt"
+	log "github.com/sirupsen/logrus"
+	"pmail/dto"
+	"pmail/dto/parsemail"
+	"pmail/models"
+	"pmail/mysql"
+	"strings"
+)
+
+func GetEmailDetail(ctx *dto.Context, id int, markRead bool) (*models.Email, error) {
+	// 获取邮件内容
+	var email models.Email
+	err := mysql.Instance.Get(&email, mysql.WithContext(ctx, "select * from email where id = ?"), id)
+	if err != nil {
+		log.WithContext(ctx).Errorf("SQL error:%+v", err)
+		return nil, err
+	}
+
+	if markRead && email.IsRead == 0 {
+		_, err = mysql.Instance.Exec(mysql.WithContext(ctx, "update email set is_read =1 where id =?"), email.Id)
+		if err != nil {
+			log.WithContext(ctx).Errorf("SQL error:%+v", err)
+		}
+	}
+
+	// 将内容中的cid内容替换成url
+	if email.Attachments != "" {
+		var atts []parsemail.Attachment
+		_ = json.Unmarshal([]byte(email.Attachments), &atts)
+		for _, att := range atts {
+			email.Html = sql.NullString{
+				String: strings.ReplaceAll(email.Html.String, fmt.Sprintf("cid:%s", att.ContentID), fmt.Sprintf("/attachments/%d/%s", id, att.ContentID)),
+			}
+		}
+	}
+
+	return &email, nil
+}

+ 60 - 0
server/services/list/list.go

@@ -0,0 +1,60 @@
+package list
+
+import (
+	"encoding/json"
+	log "github.com/sirupsen/logrus"
+	"pmail/dto"
+	"pmail/models"
+	"pmail/mysql"
+)
+
+func GetEmailList(ctx *dto.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 := mysql.Instance.Select(&emailList, querySQL, queryParams...)
+	if err != nil {
+		log.Errorf("SQL ERROR: %s ,Error:%s", querySQL, err)
+	}
+
+	err = mysql.Instance.Get(&total, counterSQL, counterParams...)
+	if err != nil {
+		log.Errorf("SQL ERROR: %s ,Error:%s", querySQL, err)
+	}
+
+	return
+}
+
+func genSQL(ctx *dto.Context, counter bool, tag, keyword string, offset, limit int) (string, []any) {
+
+	sql := "select * from email where 1=1 "
+	if counter {
+		sql = "select count(1) from email where 1=1 "
+	}
+
+	sqlParams := []any{}
+
+	var tagInfo dto.SearchTag
+	_ = json.Unmarshal([]byte(tag), &tagInfo)
+
+	if tagInfo.Type != -1 {
+		sql += " and type =? "
+		sqlParams = append(sqlParams, tagInfo.Type)
+	}
+
+	if tagInfo.Status != -1 {
+		sql += " and status =? "
+		sqlParams = append(sqlParams, tagInfo.Status)
+	}
+
+	if keyword != "" {
+		sql += " and (subject like ? or text like ? )"
+		sqlParams = append(sqlParams, "%"+keyword+"%", "%"+keyword+"%")
+	}
+
+	sql += " limit ? offset ?"
+	sqlParams = append(sqlParams, limit, offset)
+
+	return sql, sqlParams
+}

+ 19 - 0
server/session/init.go

@@ -0,0 +1,19 @@
+package session
+
+import (
+	"github.com/alexedwards/scs/mysqlstore"
+	"github.com/alexedwards/scs/v2"
+	"pmail/mysql"
+
+	"time"
+)
+
+var Instance *scs.SessionManager
+
+func Init() {
+	Instance = scs.New()
+	Instance.Lifetime = 24 * time.Hour
+	// 使用mysql存储session数据,目前为了架构简单,
+	// 暂不引入redis存储,如果日后性能存在瓶颈,可以将session迁移到redis
+	Instance.Store = mysqlstore.New(mysql.Instance.DB)
+}

+ 72 - 0
server/smtp_server/main.go

@@ -0,0 +1,72 @@
+package smtp_server
+
+import (
+	"crypto/tls"
+	"github.com/emersion/go-smtp"
+	log "github.com/sirupsen/logrus"
+	"net"
+	"pmail/config"
+	"time"
+)
+
+// The Backend implements SMTP server methods.
+type Backend struct{}
+
+func (bkd *Backend) NewSession(conn *smtp.Conn) (smtp.Session, error) {
+	remoteAddress := conn.Conn().RemoteAddr()
+
+	return &Session{
+		RemoteAddress: remoteAddress,
+	}, nil
+}
+
+// A Session is returned after EHLO.
+type Session struct {
+	RemoteAddress net.Addr
+}
+
+func (s *Session) AuthPlain(username, password string) error {
+	return nil
+}
+
+func (s *Session) Mail(from string, opts *smtp.MailOptions) error {
+	return nil
+}
+
+func (s *Session) Rcpt(to string) error {
+	return nil
+}
+
+func (s *Session) Reset() {}
+
+func (s *Session) Logout() error {
+	return nil
+}
+
+func Start() {
+	be := &Backend{}
+
+	s := smtp.NewServer(be)
+
+	s.Addr = ":25"
+	s.Domain = config.Instance.Domain
+	s.ReadTimeout = 10 * time.Second
+	s.WriteTimeout = 10 * time.Second
+	s.MaxMessageBytes = 1024 * 1024
+	s.MaxRecipients = 50
+	// force TLS for auth
+	s.AllowInsecureAuth = false
+	// Load the certificate and key
+	cer, err := tls.LoadX509KeyPair(config.Instance.SSLPublicKeyPath, config.Instance.SSLPrivateKeyPath)
+	if err != nil {
+		log.Fatal(err)
+		return
+	}
+	// Configure the TLS support
+	s.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cer}}
+
+	log.Println("Starting server at", s.Addr)
+	if err := s.ListenAndServe(); err != nil {
+		log.Fatal(err)
+	}
+}

+ 118 - 0
server/smtp_server/read_content.go

@@ -0,0 +1,118 @@
+package smtp_server
+
+import (
+	"bytes"
+	"encoding/json"
+	"github.com/mileusna/spf"
+	log "github.com/sirupsen/logrus"
+	"io"
+	"net"
+	"net/netip"
+	"pmail/dto/parsemail"
+	"pmail/hooks"
+	"pmail/mysql"
+	"pmail/utils/async"
+	"strings"
+	"time"
+)
+
+func (s *Session) Data(r io.Reader) error {
+	emailData, err := io.ReadAll(r)
+	if err != nil {
+		log.Error("邮件内容无法读取", err)
+		return err
+	}
+
+	for _, hook := range hooks.HookList {
+		if hook == nil {
+			continue
+		}
+		async.New(nil).Process(func() {
+			hook.ReceiveParseBefore(emailData)
+		})
+	}
+
+	log.Infof("邮件原始内容: %s", emailData)
+
+	var dkimStatus, SPFStatus bool
+
+	// DKIM校验
+	dkimStatus = parsemail.Check(bytes.NewReader(emailData))
+
+	email := parsemail.NewEmailFromReader(bytes.NewReader(emailData))
+
+	if err != nil {
+		log.Fatalf("邮件内容解析失败! Error : %v \n", err)
+	}
+
+	SPFStatus = spfCheck(s.RemoteAddress.String(), email.Sender, email.Sender.EmailAddress)
+
+	var dkimV, spfV int8
+	if dkimStatus {
+		dkimV = 1
+	}
+	if SPFStatus {
+		spfV = 1
+	}
+
+	for _, hook := range hooks.HookList {
+		if hook == nil {
+			continue
+		}
+		async.New(nil).Process(func() {
+			hook.ReceiveParseAfter(email)
+		})
+	}
+
+	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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+	_, err = mysql.Instance.Exec(sql,
+		email.Date,
+		email.Subject,
+		json2string(email.ReplyTo),
+		email.From.Name,
+		email.From.EmailAddress,
+		json2string(email.To),
+		json2string(email.Bcc),
+		json2string(email.Cc),
+		email.Text,
+		email.HTML,
+		json2string(email.Sender),
+		json2string(email.Attachments),
+		spfV,
+		dkimV,
+		time.Now())
+
+	if err != nil {
+		log.Println("mysql insert error:", err.Error())
+	}
+
+	return nil
+}
+
+func json2string(d any) string {
+	by, _ := json.Marshal(d)
+	return string(by)
+}
+
+func spfCheck(remoteAddress string, sender *parsemail.User, senderString string) bool {
+	//spf校验
+	ipAddress, _ := netip.ParseAddrPort(remoteAddress)
+
+	ip := net.ParseIP(ipAddress.Addr().String())
+	if ip.IsPrivate() {
+		return true
+	}
+
+	tmp := strings.Split(sender.EmailAddress, "@")
+	if len(tmp) < 2 {
+		return false
+	}
+
+	res := spf.CheckHost(ip, tmp[1], senderString, "")
+
+	if res == spf.None || res == spf.Pass {
+		// spf校验通过
+		return true
+	}
+	return false
+}

+ 72 - 0
server/smtp_server/read_content_test.go

@@ -0,0 +1,72 @@
+package smtp_server
+
+import (
+	"bytes"
+	log "github.com/sirupsen/logrus"
+	"io/fs"
+	"net"
+	"net/netip"
+	"os"
+	"path/filepath"
+	"pmail/config"
+	parsemail2 "pmail/dto/parsemail"
+	"pmail/mysql"
+	"pmail/session"
+	"testing"
+	"time"
+)
+
+func testInit() {
+	// 设置日志格式为json格式
+	//log.SetFormatter(&log.JSONFormatter{})
+
+	log.SetReportCaller(true)
+	log.SetFormatter(&log.TextFormatter{
+		//以下设置只是为了使输出更美观
+		DisableColors:   true,
+		TimestampFormat: "2006-01-02 15:03:04",
+	})
+
+	// 设置将日志输出到标准输出(默认的输出为stderr,标准错误)
+	// 日志消息输出可以是任意的io.writer类型
+	log.SetOutput(os.Stdout)
+
+	// 设置日志级别为warn以上
+	log.SetLevel(log.TraceLevel)
+
+	var cst, _ = time.LoadLocation("Asia/Shanghai")
+	time.Local = cst
+
+	config.Init()
+	parsemail2.Init()
+	mysql.Init()
+	session.Init()
+
+}
+
+func TestSession_Data(t *testing.T) {
+	testInit()
+	s := Session{
+		RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
+	}
+
+	filepath.WalkDir("docs", func(path string, d fs.DirEntry, err error) error {
+		if !d.IsDir() {
+			data, _ := os.ReadFile(path)
+			s.Data(bytes.NewReader(data))
+		}
+		return nil
+	})
+
+}
+
+func TestSession_DataGmail(t *testing.T) {
+	testInit()
+	s := Session{
+		RemoteAddress: net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.AddrFrom4([4]byte{}), 25)),
+	}
+
+	data, _ := os.ReadFile("docs/gmail/带附件带图片.txt")
+	s.Data(bytes.NewReader(data))
+
+}

+ 160 - 0
server/smtp_server/send.go

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

+ 24 - 0
server/smtp_server/send_test.go

@@ -0,0 +1,24 @@
+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)
+}

+ 19 - 0
server/utils/array/array.go

@@ -0,0 +1,19 @@
+package array
+
+import (
+	"github.com/spf13/cast"
+	"strings"
+)
+
+func Join[T any](arg []T, str string) string {
+	var ret strings.Builder
+	for i, t := range arg {
+		if i == 0 {
+			ret.WriteString(cast.ToString(t))
+		} else {
+			ret.WriteString(str)
+			ret.WriteString(cast.ToString(t))
+		}
+	}
+	return ret.String()
+}

+ 71 - 0
server/utils/async/async.go

@@ -0,0 +1,71 @@
+package async
+
+import (
+	"errors"
+	log "github.com/sirupsen/logrus"
+	"github.com/spf13/cast"
+	"pmail/dto"
+	"runtime/debug"
+	"sync"
+)
+
+type Callback func()
+
+type Async struct {
+	wg        *sync.WaitGroup
+	lastError error
+	ctx       *dto.Context
+}
+
+func New(ctx *dto.Context) *Async {
+	return &Async{
+		ctx: ctx,
+	}
+}
+
+func (as *Async) LastError() error {
+	return as.lastError
+}
+
+func (as *Async) WaitProcess(callback Callback) {
+	if as.wg == nil {
+		as.wg = &sync.WaitGroup{}
+	}
+	as.wg.Add(1)
+	as.Process(func() {
+		defer as.wg.Done()
+		callback()
+	})
+}
+
+func (as *Async) Process(callback Callback) {
+	go func() {
+		defer func() {
+			if err := recover(); err != nil {
+				as.lastError = as.HandleErrRecover(err)
+			}
+		}()
+		callback()
+	}()
+}
+
+func (as *Async) Wait() {
+	if as.wg == nil {
+		return
+	}
+	as.wg.Wait()
+}
+
+// HandleErrRecover panic恢复处理
+func (as *Async) HandleErrRecover(err interface{}) (returnErr error) {
+	switch err.(type) {
+	case error:
+		returnErr = err.(error)
+	default:
+		returnErr = errors.New(cast.ToString(err))
+	}
+
+	log.WithContext(as.ctx).Errorf("goroutine panic:%s  \n %s", err, string(debug.Stack()))
+
+	return
+}

+ 448 - 0
server/utils/smtp/smtp.go

@@ -0,0 +1,448 @@
+// Copyright 2010 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package smtp implements the Simple Mail Transfer Protocol as defined in RFC 5321.
+// It also implements the following extensions:
+//
+//	8BITMIME  RFC 1652
+//	AUTH      RFC 2554
+//	STARTTLS  RFC 3207
+//
+// Additional extensions may be handled by clients.
+//
+// The smtp package is frozen and is not accepting new features.
+// Some external packages provide more functionality. See:
+//
+//	https://godoc.org/?q=smtp
+package smtp
+
+import (
+	"crypto/tls"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"io"
+	"net"
+	"net/smtp"
+	"net/textproto"
+	"pmail/config"
+	"strings"
+)
+
+// A Client represents a client connection to an SMTP server.
+type Client struct {
+	// Text is the textproto.Conn used by the Client. It is exported to allow for
+	// clients to add extensions.
+	Text *textproto.Conn
+	// keep a reference to the connection so it can be used to create a TLS
+	// connection later
+	conn net.Conn
+	// whether the Client is using TLS
+	tls        bool
+	serverName string
+	// map of supported extensions
+	ext map[string]string
+	// supported auth mechanisms
+	auth       []string
+	localName  string // the name to use in HELO/EHLO
+	didHello   bool   // whether we've said HELO/EHLO
+	helloError error  // the error from the hello
+}
+
+// Dial returns a new Client connected to an SMTP server at addr.
+// The addr must include a port, as in "mail.example.com:smtp".
+func Dial(addr string) (*Client, error) {
+	conn, err := net.Dial("tcp", addr)
+	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) {
+	text := textproto.NewConn(conn)
+	_, _, err := text.ReadResponse(220)
+	if err != nil {
+		text.Close()
+		return nil, err
+	}
+	c := &Client{Text: text, conn: conn, serverName: host, localName: config.Instance.Domain}
+	_, c.tls = conn.(*tls.Conn)
+	return c, nil
+}
+
+// Close closes the connection.
+func (c *Client) Close() error {
+	return c.Text.Close()
+}
+
+// hello runs a hello exchange if needed.
+func (c *Client) hello() error {
+	if !c.didHello {
+		c.didHello = true
+		err := c.ehlo()
+		if err != nil {
+			c.helloError = c.helo()
+		}
+	}
+	return c.helloError
+}
+
+// Hello sends a HELO or EHLO to the server as the given host name.
+// Calling this method is only necessary if the client needs control
+// over the host name used. The client will introduce itself as "localhost"
+// automatically otherwise. If Hello is called, it must be called before
+// any of the other methods.
+func (c *Client) Hello(localName string) error {
+	if err := validateLine(localName); err != nil {
+		return err
+	}
+	if c.didHello {
+		return errors.New("smtp: Hello called after other methods")
+	}
+	c.localName = localName
+	return c.hello()
+}
+
+// cmd is a convenience function that sends a command and returns the response
+func (c *Client) cmd(expectCode int, format string, args ...any) (int, string, error) {
+	id, err := c.Text.Cmd(format, args...)
+	if err != nil {
+		return 0, "", err
+	}
+	c.Text.StartResponse(id)
+	defer c.Text.EndResponse(id)
+	code, msg, err := c.Text.ReadResponse(expectCode)
+	return code, msg, err
+}
+
+// helo sends the HELO greeting to the server. It should be used only when the
+// server does not support ehlo.
+func (c *Client) helo() error {
+	c.ext = nil
+	_, _, err := c.cmd(250, "HELO %s", c.localName)
+	return err
+}
+
+// ehlo sends the EHLO (extended hello) greeting to the server. It
+// should be the preferred greeting for servers that support it.
+func (c *Client) ehlo() error {
+	_, msg, err := c.cmd(250, "EHLO %s", c.localName)
+	if err != nil {
+		return err
+	}
+	ext := make(map[string]string)
+	extList := strings.Split(msg, "\n")
+	if len(extList) > 1 {
+		extList = extList[1:]
+		for _, line := range extList {
+			k, v, _ := strings.Cut(line, " ")
+			ext[k] = v
+		}
+	}
+	if mechs, ok := ext["AUTH"]; ok {
+		c.auth = strings.Split(mechs, " ")
+	}
+	c.ext = ext
+	return err
+}
+
+// StartTLS sends the STARTTLS command and encrypts all further communication.
+// Only servers that advertise the STARTTLS extension support this function.
+func (c *Client) StartTLS(config *tls.Config) error {
+	if err := c.hello(); err != nil {
+		return err
+	}
+	_, _, err := c.cmd(220, "STARTTLS")
+	if err != nil {
+		return err
+	}
+	if config == nil {
+		config = &tls.Config{}
+	}
+	if config.ServerName == "" {
+		// Make a copy to avoid polluting argument
+		config = config.Clone()
+		config.ServerName = c.serverName
+	}
+	c.conn = tls.Client(c.conn, config)
+	c.Text = textproto.NewConn(c.conn)
+	c.tls = true
+	return c.ehlo()
+}
+
+// TLSConnectionState returns the client's TLS connection state.
+// The return values are their zero values if StartTLS did
+// not succeed.
+func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
+	tc, ok := c.conn.(*tls.Conn)
+	if !ok {
+		return
+	}
+	return tc.ConnectionState(), true
+}
+
+// Verify checks the validity of an email address on the server.
+// If Verify returns nil, the address is valid. A non-nil return
+// does not necessarily indicate an invalid address. Many servers
+// will not verify addresses for security reasons.
+func (c *Client) Verify(addr string) error {
+	if err := validateLine(addr); err != nil {
+		return err
+	}
+	if err := c.hello(); err != nil {
+		return err
+	}
+	_, _, err := c.cmd(250, "VRFY %s", addr)
+	return err
+}
+
+// Auth authenticates a client using the provided authentication mechanism.
+// A failed authentication closes the connection.
+// Only servers that advertise the AUTH extension support this function.
+func (c *Client) Auth(a smtp.Auth) error {
+	if err := c.hello(); err != nil {
+		return err
+	}
+	encoding := base64.StdEncoding
+	mech, resp, err := a.Start(&smtp.ServerInfo{Name: c.serverName, TLS: c.tls, Auth: c.auth})
+	if err != nil {
+		c.Quit()
+		return err
+	}
+	resp64 := make([]byte, encoding.EncodedLen(len(resp)))
+	encoding.Encode(resp64, resp)
+	code, msg64, err := c.cmd(0, strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech, resp64)))
+	for err == nil {
+		var msg []byte
+		switch code {
+		case 334:
+			msg, err = encoding.DecodeString(msg64)
+		case 235:
+			// the last message isn't base64 because it isn't a challenge
+			msg = []byte(msg64)
+		default:
+			err = &textproto.Error{Code: code, Msg: msg64}
+		}
+		if err == nil {
+			resp, err = a.Next(msg, code == 334)
+		}
+		if err != nil {
+			// abort the AUTH
+			c.cmd(501, "*")
+			c.Quit()
+			break
+		}
+		if resp == nil {
+			break
+		}
+		resp64 = make([]byte, encoding.EncodedLen(len(resp)))
+		encoding.Encode(resp64, resp)
+		code, msg64, err = c.cmd(0, string(resp64))
+	}
+	return err
+}
+
+// Mail issues a MAIL command to the server using the provided email address.
+// If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME
+// parameter. If the server supports the SMTPUTF8 extension, Mail adds the
+// SMTPUTF8 parameter.
+// This initiates a mail transaction and is followed by one or more Rcpt calls.
+func (c *Client) Mail(from string) error {
+	if err := validateLine(from); err != nil {
+		return err
+	}
+	if err := c.hello(); err != nil {
+		return err
+	}
+	cmdStr := "MAIL FROM:<%s>"
+	if c.ext != nil {
+		if _, ok := c.ext["8BITMIME"]; ok {
+			cmdStr += " BODY=8BITMIME"
+		}
+		if _, ok := c.ext["SMTPUTF8"]; ok {
+			cmdStr += " SMTPUTF8"
+		}
+	}
+	_, _, err := c.cmd(250, cmdStr, from)
+	return err
+}
+
+// Rcpt issues a RCPT command to the server using the provided email address.
+// A call to Rcpt must be preceded by a call to Mail and may be followed by
+// a Data call or another Rcpt call.
+func (c *Client) Rcpt(to string) error {
+	if err := validateLine(to); err != nil {
+		return err
+	}
+	_, _, err := c.cmd(25, "RCPT TO:<%s>", to)
+	return err
+}
+
+type dataCloser struct {
+	c *Client
+	io.WriteCloser
+}
+
+func (d *dataCloser) Close() error {
+	d.WriteCloser.Close()
+	_, _, err := d.c.Text.ReadResponse(250)
+	return err
+}
+
+// Data issues a DATA command to the server and returns a writer that
+// can be used to write the mail headers and body. The caller should
+// close the writer before calling any more methods on c. A call to
+// Data must be preceded by one or more calls to Rcpt.
+func (c *Client) Data() (io.WriteCloser, error) {
+	_, _, err := c.cmd(354, "DATA")
+	if err != nil {
+		return nil, err
+	}
+	return &dataCloser{c, c.Text.DotWriter()}, nil
+}
+
+var testHookStartTLS func(*tls.Config) // nil, except for tests
+
+// SendMail connects to the server at addr, switches to TLS if
+// possible, authenticates with the optional mechanism a if possible,
+// and then sends an email from address from, to addresses to, with
+// message msg.
+// The addr must include a port, as in "mail.example.com:smtp".
+//
+// The addresses in the to parameter are the SMTP RCPT addresses.
+//
+// The msg parameter should be an RFC 822-style email with headers
+// first, a blank line, and then the message body. The lines of msg
+// should be CRLF terminated. The msg headers should usually include
+// fields such as "From", "To", "Subject", and "Cc".  Sending "Bcc"
+// messages is accomplished by including an email address in the to
+// parameter but not including it in the msg headers.
+//
+// The SendMail function and the net/smtp package are low-level
+// mechanisms and provide no support for DKIM signing, MIME
+// attachments (see the mime/multipart package), or other mail
+// functionality. Higher-level packages exist outside of the standard
+// library.
+// 修复TSL验证问题
+func SendMail(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 := Dial(addr)
+	if err != nil {
+		return err
+	}
+	defer c.Close()
+	if err = c.hello(); err != nil {
+		return err
+	}
+	if ok, _ := c.Extension("STARTTLS"); !ok {
+		return errors.New("smtp: server doesn't support STARTTLS")
+	}
+
+	var config *tls.Config
+	if domain != "" {
+		config = &tls.Config{
+			ServerName: domain,
+		}
+	}
+
+	if err = c.StartTLS(config); 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()
+}
+
+// Extension reports whether an extension is support by the server.
+// The extension name is case-insensitive. If the extension is supported,
+// Extension also returns a string that contains any parameters the
+// server specifies for the extension.
+func (c *Client) Extension(ext string) (bool, string) {
+	if err := c.hello(); err != nil {
+		return false, ""
+	}
+	if c.ext == nil {
+		return false, ""
+	}
+	ext = strings.ToUpper(ext)
+	param, ok := c.ext[ext]
+	return ok, param
+}
+
+// Reset sends the RSET command to the server, aborting the current mail
+// transaction.
+func (c *Client) Reset() error {
+	if err := c.hello(); err != nil {
+		return err
+	}
+	_, _, err := c.cmd(250, "RSET")
+	return err
+}
+
+// Noop sends the NOOP command to the server. It does nothing but check
+// that the connection to the server is okay.
+func (c *Client) Noop() error {
+	if err := c.hello(); err != nil {
+		return err
+	}
+	_, _, err := c.cmd(250, "NOOP")
+	return err
+}
+
+// Quit sends the QUIT command and closes the connection to the server.
+func (c *Client) Quit() error {
+	if err := c.hello(); err != nil {
+		return err
+	}
+	_, _, err := c.cmd(221, "QUIT")
+	if err != nil {
+		return err
+	}
+	return c.Text.Close()
+}
+
+// validateLine checks to see if a line has CR or LF as per RFC 5321.
+func validateLine(line string) error {
+	if strings.ContainsAny(line, "\n\r") {
+		return errors.New("smtp: A line must not contain CR or LF")
+	}
+	return nil
+}