Kaynağa Gözat

Merge pull request #3 from Jinnrry/v2.0

V2.0
木木的木头 2 yıl önce
ebeveyn
işleme
0a5c153c26
72 değiştirilmiş dosya ile 1964 ekleme ve 308 silme
  1. 2 1
      .gitignore
  2. 29 51
      README.md
  3. 20 62
      README_CN.md
  4. 5 8
      build.sh
  5. BIN
      docs/cn.gif
  6. BIN
      docs/en.gif
  7. 3 3
      fe/src/App.vue
  8. 12 3
      fe/src/http/http.js
  9. 56 6
      fe/src/i18n/i18n.js
  10. 8 0
      fe/src/router/index.js
  11. 3 2
      fe/src/views/EditerView.vue
  12. 2 1
      fe/src/views/EmailDetailView.vue
  13. 2 1
      fe/src/views/ListView.vue
  14. 373 0
      fe/src/views/SetupView.vue
  15. 8 3
      server/config/config.dev.json
  16. 39 17
      server/config/config.go
  17. 8 3
      server/config/config.json
  18. 0 1
      server/config/dkim/README
  19. 0 2
      server/config/tables/data/user.sql
  20. 0 2
      server/config/tables/data/user_auth.sql
  21. 0 0
      server/config/tables/mysql/data/user.sql
  22. 0 0
      server/config/tables/mysql/data/user_auth.sql
  23. 0 0
      server/config/tables/mysql/email.sql
  24. 0 0
      server/config/tables/mysql/sessions.sql
  25. 0 0
      server/config/tables/mysql/user.sql
  26. 0 0
      server/config/tables/mysql/user_auth.sql
  27. 0 0
      server/config/tables/sqlite/data/user.sql
  28. 0 0
      server/config/tables/sqlite/data/user_auth.sql
  29. 26 0
      server/config/tables/sqlite/email.sql
  30. 8 0
      server/config/tables/sqlite/sessions.sql
  31. 8 0
      server/config/tables/sqlite/user.sql
  32. 9 0
      server/config/tables/sqlite/user_auth.sql
  33. 4 4
      server/controllers/email/send.go
  34. 11 0
      server/controllers/interceptor.go
  35. 7 14
      server/controllers/login.go
  36. 4 3
      server/controllers/settings.go
  37. 145 0
      server/controllers/setup.go
  38. 34 0
      server/cron_server/ssl_update.go
  39. 24 7
      server/db/init.go
  40. 1 0
      server/dto/response/response.go
  41. 30 8
      server/go.mod
  42. 68 16
      server/go.sum
  43. 11 12
      server/hooks/wechat_push/wechat_push.go
  44. 35 0
      server/http_server/http_server.go
  45. 37 31
      server/http_server/https_server.go
  46. 63 0
      server/http_server/setup_server.go
  47. 4 0
      server/i18n/i18n.go
  48. 23 18
      server/main.go
  49. 72 0
      server/res_init/init.go
  50. 3 3
      server/services/attachments/attachments.go
  51. 80 2
      server/services/auth/auth.go
  52. 7 0
      server/services/auth/auth_test.go
  53. 3 3
      server/services/detail/detail.go
  54. 3 3
      server/services/list/list.go
  55. 120 0
      server/services/setup/db.go
  56. 54 0
      server/services/setup/dns.go
  57. 7 0
      server/services/setup/dns_test.go
  58. 40 0
      server/services/setup/domain.go
  59. 24 0
      server/services/setup/finish.go
  60. 37 0
      server/services/setup/ssl/challenge.go
  61. 172 0
      server/services/setup/ssl/ssl.go
  62. 17 0
      server/services/setup/ssl/ssl_test.go
  63. 9 3
      server/session/init.go
  64. 4 0
      server/signal/signal.go
  65. 19 11
      server/smtp_server/main.go
  66. 2 2
      server/smtp_server/read_content.go
  67. 2 2
      server/smtp_server/read_content_test.go
  68. 75 0
      server/utils/array/array.go
  69. 34 0
      server/utils/errors/error.go
  70. 29 0
      server/utils/errors/error_test.go
  71. 11 0
      server/utils/file/file.go
  72. 18 0
      server/utils/password/encode.go

+ 2 - 1
.gitignore

@@ -1,4 +1,5 @@
 .idea
 .DS_Store
 dist
-output
+output
+pmail.db

+ 29 - 51
README.md

@@ -1,12 +1,21 @@
-# PMail 
+# 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!
+> 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. 
+PMail is a personal email server that pursues a minimal deployment process and extreme resource consumption. It runs on
+a single file and contains complete send/receive mail service and web-side mail management functions. Just a server , a
+domain name , a line of code , a minute of deployment time , you will be able to build a domain name mailbox of your
+own .
+
+Any project related Issue, PR is welcome.At present, the project UI design is ugly, UI interaction experience is poor,
+welcome all UI, designers, front-end guidance. Finally, also for this project to solicit a beautiful and lovely Logo!
+
+<img src="./docs/en.gif" alt="Editor" width="800px">
 
 ## Features
 
@@ -16,36 +25,29 @@ An extremely lightweight mailbox server designed for personal use scenarios.
 
 * Support dkim, spf checksum, [Email Test](https://www.mail-tester.com/) score 10 points if correctly configured.
 
+* Implementing the ACME protocol, the program will automatically obtain and update Let's Encrypt certificates.
+
 ## 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.
+* 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
+## 1、Download
 
-Put the key in the `config/dkim` directory.
+[Click Here](https://github.com/Jinnrry/PMail/releases) Download a program file that matches you.
 
-## 2、Set DNS
+## 2、Run
 
-Add the following records to your domain DNS settings
+`double-click to open` Or `execute command to run`
 
-| 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、Configuration
 
-## 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.
+Open `http://127.0.0.1` in your browser or use your server's public IP to visit, then follow the instructions to
+configure.
 
 ## 4、Build(or download)
 
@@ -59,35 +61,15 @@ Put the certificate in the `config/ssl` directory.
 
 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`.
+## 6、Email Test
 
-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
-}
-```
+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.
 
-## 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. 
+## 7、 WeChat Message Push
 
+Open the `config/config.json` file in the run directory, edit a few configuration items at the beginning of `weChatPush`
+and restart the service.
 
 # For Developer
 
@@ -104,7 +86,3 @@ 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 !

+ 20 - 62
README_CN.md

@@ -1,7 +1,13 @@
-# PMail 
+# PMail
 
 > Welcome PR! Welcome Issues! 目前代码并不稳定,一定记录好日志!丢信或者信件解析错误可以从日志中找出邮件原始内容!
 
+PMail是一个追求极简部署流程、极致资源占用的个人域名邮箱服务器。单文件运行,包含完整的收发邮件服务和Web端邮件管理功能。只需一台服务器、一个域名、一行代码、一分钟部署时间,你就能够搭建出一个自己的域名邮箱。
+
+目前项目UI设计比较丑陋、UI交互体验较差,欢迎各位UI、设计师、前端提出指导意见。最后,也为这个项目征集一个漂亮可爱的Logo!
+
+<img src="./docs/cn.gif" alt="Editor" width="800px">
+
 ## 为什么写这个项目
 
 迫于越来越多的邮件服务商暂停了针对个人的域名邮箱服务(比如QQ邮箱、微软Outlook邮箱),因此考虑自建域名邮箱服务。
@@ -22,6 +28,10 @@
 
 支持dkim、spf校验。正确配置的情况下,Email Test得分10分。
 
+### 4、自动SSL证书
+
+实现了ACME协议,程序将自动获取并更新Let’s Encrypt证书。
+
 ## 其他
 
 ### 不足
@@ -35,74 +45,25 @@
 
 # 如何部署
 
-## 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、申请域名证书
+## 1、下载文件
 
-准备好 `smtp.你的域名` 的证书,key格式的私钥和crt格式的公钥
+[点击这里](https://github.com/Jinnrry/PMail/releases)下载一个与你匹配的程序文件。
 
-放到`config/ssl`目录中
+## 2、运行
 
-## 4、编译程序(或者直接下载编译好的二进制文件)
+双击打开 OR 执行命令运行
 
-1、前端环境:安装好node环境,配置好yarn
+## 3、配置
 
-2、后端环境:安装最新的golang
+浏览器打开 `http://127.0.0.1` 或者是用你服务器公网IP访问,然后按提示配置
 
-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、邮箱得分测试
+## 4、邮箱得分测试
 
 建议找一下邮箱测试服务(比如[https://www.mail-tester.com/](https://www.mail-tester.com/))进行邮件得分检测,避免自己某些步骤漏配,导致发件进对方垃圾箱。
 
-## 8、其他说明
-
-邮件是否进对方垃圾箱与程序无关、与你的服务器IP、服务器域名有关。我自己搭建的服务,测试了收发QQ、Gmail、Outlook、163、126均正常,无任何拦截,且不会进垃圾箱。
+## 5、微信推送
 
+打开运行目录下的 `config/config.json`文件,编辑 `weChatPush` 开头的几个配置项,重启服务即可。
 
 # 参与开发
 
@@ -120,6 +81,3 @@ MySQL库名必须叫pmail,另外,数据库必须使用utf8_general_ci字符
 
 参考微信推送插件`server/hooks/wechat_push/wechat_push.go`
 
-# 最后
-
-欢迎PR! 欢迎Issue!求个Logo!

+ 5 - 8
build.sh

@@ -4,17 +4,14 @@ 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=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=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=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=arm64
-go build -ldflags "-X 'main.goVersion=$(go version)' -X 'main.gitHash=$(git show -s --format=%H)' -X 'main.buildTime=$(TZ=UTC-8 date +%Y-%m-%d" "%H:%M:%S)'" -o pmail_mac_arm64  main.go
+CGO_ENABLED=0 GOOS=darwin GOARCH=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 ..

BIN
docs/cn.gif


BIN
docs/en.gif


+ 3 - 3
fe/src/App.vue

@@ -2,11 +2,11 @@
 import { RouterView } from 'vue-router'
 import HomeHeader from '@/components/HomeHeader.vue'
 import HomeAside from '@/components/HomeAside.vue';
-import { watch,ref } from 'vue'
+import { watch, ref } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 const route = useRoute()
 
-const pageName = ref(route.name) 
+const pageName = ref(route.name)
 
 watch(
   () => route.fullPath,
@@ -21,7 +21,7 @@ watch(
   <div id="main">
     <HomeHeader />
     <div id="content">
-      <div id="aside" v-if="pageName != 'login'">
+      <div id="aside" v-if="pageName != 'login' && pageName != 'setup'">
         <HomeAside />
       </div>
       <div id="body">

+ 12 - 3
fe/src/http/http.js

@@ -6,10 +6,10 @@ import lang from '../i18n/i18n';
 //创建axios的一个实例 
 var $http = axios.create({
     baseURL: import.meta.env.VITE_APP_URL, //接口统一域名
-    timeout: 6000, //设置超时
+    timeout: 60000, //设置超时
     headers: {
         'Content-Type': 'application/json;charset=UTF-8;',
-        'Lang':lang.lang
+        'Lang': lang.lang
     }
 })
 
@@ -27,7 +27,7 @@ $http.interceptors.request.use((config) => {
 //响应拦截器
 $http.interceptors.response.use((response) => {
     //响应成功
-    if(response.data.errorNo == 403){
+    if (response.data.errorNo == 403) {
         router.replace({
             path: '/login',
             query: {
@@ -35,6 +35,15 @@ $http.interceptors.response.use((response) => {
             }
         })
     }
+    //响应成功
+    if (response.data.errorNo == 402) {
+        router.replace({
+            path: '/setup',
+            query: {
+                redirect: router.currentRoute.fullPath
+            }
+        })
+    }
     return response.data;
 }, (error) => {
     //响应错误

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

@@ -18,7 +18,7 @@ var lang = {
     "cc_desc": "Cc's e-mail address",
     "send": "send",
     "add_att": "Add Attachment",
-    "attachment":"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!",
@@ -31,8 +31,33 @@ var lang = {
     "succ": "Success!",
     "err_pwd_diff": "The passwords entered twice do not match!",
     "fail": "Fail!",
-    "settings":"Settings",
-    "security":"Security"
+    "settings": "Settings",
+    "security": "Security",
+    "SetDomail": "Set Domain",
+    "setDNS": "Set DNS",
+    "setSSL": "Set SSL",
+    "setDatabase": "Set Database",
+    "setAdminPassword": "Set Password",
+    "admin_account": "Administrator Account",
+    "setOther": "Other",
+    "welcome": "Welcome",
+    "next": "Next",
+    "tks_pmail": "Thanks for using Pmail",
+    "guid_desc": "Next, you will be guided to perform initial configuration. If you have already configured, please use your configuration file to overwrite the config folder of the running directory. If you have not configured it, please follow this guide.",
+    "select_db": "select database",
+    "db_desc": "PMail currently supports MySQL and SQLite3 databases, you can choose according to your needs.",
+    "type": "Type",
+    "db_select_ph": "please select your database",
+    "mysql_dsn": "MySQL DSN",
+    "sqlite_db_path": "Data File Path",
+    "domain_desc": "Set your domain infomation.",
+    "smtp_domain": "SMTP Domain",
+    "web_domain": "Web Domain",
+    "dns_desc": "Please add the following information to your DNS records",
+    "ssl_auto": "Automatically configure SSL certificates (recommended)",
+    "ssl_manuallyf": "Manually configure an SSL certificate",
+    "ssl_key_path": "ssl key file path",
+    "ssl_crt_path": "ssl crt file path",
 };
 
 
@@ -57,7 +82,7 @@ var zhCN = {
     "cc_desc": "抄送人邮箱地址",
     "send": "发送",
     "add_att": "添加附件",
-    "attachment":"附件",
+    "attachment": "附件",
     "err_sender_must": "发件人邮箱前缀必填",
     "only_prefix": "只需要邮箱前缀",
     "err_email_format": "邮箱地址错误,请检查邮箱格式!",
@@ -70,8 +95,33 @@ var zhCN = {
     "succ": "成功!",
     "err_pwd_diff": "两次输入的密码不一致!",
     "fail": "失败",
-    "settings":"设置",
-    "security":"安全"
+    "settings": "设置",
+    "security": "安全",
+    "SetDomail": "域名设置",
+    "setDNS": "DNS设置",
+    "setSSL": "SSL设置",
+    "setDatabase": "数据库设置",
+    "setAdminPassword": "密码设置",
+    "admin_account": "管理员账号",
+    "setOther": "其他设置",
+    "welcome": "欢迎",
+    "next": "下一步",
+    "tks_pmail": "感谢使用PMail",
+    "guid_desc": "接下来将会指引你进行初始化配置,如果你已有配置,请使用你的配置文件覆盖运行目录的config文件夹。如果你没有配置过,请按照该指引操作。",
+    "select_db": "数据库选择",
+    "db_desc": "PMail目前支持MySQL和SQLite3两种数据库,你可根据需要选择。",
+    "type": "类型",
+    "db_select_ph": "请选择你的数据库",
+    "mysql_dsn": "MySQL DSN",
+    "sqlite_db_path": "存储位置",
+    "domain_desc": "设置你的域名信息。",
+    "smtp_domain": "SMTP域名地址",
+    "web_domain": "Web域名地址",
+    "dns_desc": "请将以下信息添加到DNS记录中",
+    "ssl_auto": "自动配置SSL证书(推荐)",
+    "ssl_manuallyf": "手动配置SSL证书",
+    "ssl_key_path": "ssl key文件位置",
+    "ssl_crt_path": "ssl crt文件位置",
 }
 
 switch (navigator.language) {

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

@@ -3,6 +3,9 @@ import ListView from '../views/ListView.vue'
 import EditerView from '../views/EditerView.vue'
 import LoginView from '../views/LoginView.vue'
 import EmailDetailView from '../views/EmailDetailView.vue'
+import SetupView from '../views/SetupView.vue'
+
+
 const router = createRouter({
   history: createWebHashHistory(import.meta.env.BASE_URL),
   routes: [
@@ -26,6 +29,11 @@ const router = createRouter({
       name: "login",
       component: LoginView
     },
+    {
+      path: '/setup',
+      name: "setup",
+      component: SetupView
+    },
     {
       path: '/detail/:id',
       name: "detail",

+ 3 - 2
fe/src/views/EditerView.vue

@@ -76,6 +76,8 @@
 
 
 <script setup>
+import $http from '../http/http';
+
 import '@wangeditor/editor/dist/css/style.css' // 引入 css
 import { ElMessage } from 'element-plus'
 import { onBeforeUnmount, ref, shallowRef, reactive, onMounted } from 'vue'
@@ -83,7 +85,6 @@ 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()
@@ -134,7 +135,7 @@ const validateSender = function (rule, value, callback) {
 }
 
 const checkEmail = function (str) {
-    var re = /^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/
+    var re = /.+@.+\..+/
     if (re.test(str)) {
         return true
     } else {

+ 2 - 1
fe/src/views/EmailDetailView.vue

@@ -35,8 +35,9 @@
 </template>
 
 <script setup>
-import { RouterLink } from 'vue-router'
 import $http from "../http/http";
+
+import { RouterLink } from 'vue-router'
 import { reactive, ref } from 'vue'
 import { useRoute } from 'vue-router'
 import router from "@/router";  //根路由对象

+ 2 - 1
fe/src/views/ListView.vue

@@ -57,8 +57,9 @@
 
 
 <script setup>
-import { RouterLink } from 'vue-router'
 import $http from "../http/http";
+
+import { RouterLink } from 'vue-router'
 import { reactive, ref, watch } from 'vue'
 import { useRoute } from 'vue-router'
 import router from "@/router";  //根路由对象

+ 373 - 0
fe/src/views/SetupView.vue

@@ -0,0 +1,373 @@
+<template>
+    <div id="main">
+        <el-steps :active="active" align-center finish-status="success" id="status">
+            <el-step :title="lang.welcome" />
+            <el-step :title="lang.setDatabase" />
+            <el-step :title="lang.setAdminPassword" />
+            <el-step :title="lang.SetDomail" />
+            <el-step :title="lang.setDNS" />
+            <el-step :title="lang.setSSL" />
+        </el-steps>
+
+
+        <div v-if="active == 0" class="ctn">
+            <div class="desc">
+                <h2>{{ lang.tks_pmail }}</h2>
+                <div style="margin-top: 10px;">{{ lang.guid_desc }}</div>
+            </div>
+        </div>
+
+
+
+
+        <div v-if="active == 1" class="ctn">
+            <div class="desc">
+                <h2>{{ lang.select_db }}</h2>
+                <div style="margin-top: 10px;">{{ lang.db_desc }}</div>
+            </div>
+            <div class="form" style="width: 400px;">
+                <el-form label-width="120px">
+                    <el-form-item :label="lang.type">
+                        <el-select :placeholder="lang.db_select_ph" v-model="dbSettings.type">
+                            <el-option label="MySQL" value="mysql" />
+                            <el-option label="SQLite3" value="sqlite" />
+                        </el-select>
+                    </el-form-item>
+
+                    <el-form-item :label="lang.mysql_dsn" v-if="dbSettings.type == 'mysql'">
+                        <el-input :rows="2" type="textarea" v-model="dbSettings.dsn"
+                            placeholder="root:12345@tcp(127.0.0.1:3306)/pmail?parseTime=True&loc=Local"></el-input>
+                    </el-form-item>
+
+                    <el-form-item :label="lang.sqlite_db_path" v-if="dbSettings.type == 'sqlite'">
+                        <el-input v-model="dbSettings.dsn" placeholder="./pmail.db"></el-input>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </div>
+
+
+        <div v-if="active == 2" class="ctn">
+            <div class="desc">
+                <h2>{{ lang.setAdminPassword }}</h2>
+                <!-- <div style="margin-top: 10px;">{{ lang.domain_desc }}</div> -->
+            </div>
+            <div class="form" style="width: 400px;">
+                <el-form label-width="120px">
+
+                    <el-form-item :label="lang.admin_account">
+                        <el-input v-bind:disabled="adminSettings.hadSeted" placeholder="admin"
+                            v-model="adminSettings.account"></el-input>
+                    </el-form-item>
+
+                    <el-form-item :label="lang.password">
+                        <el-input type="password" v-bind:disabled="adminSettings.hadSeted" placeholder=""
+                            v-model="adminSettings.password"></el-input>
+                    </el-form-item>
+
+                    <el-form-item :label="lang.enter_again">
+                        <el-input type="password" v-bind:disabled="adminSettings.hadSeted" placeholder=""
+                            v-model="adminSettings.password2"></el-input>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </div>
+
+
+        <div v-if="active == 3" class="ctn">
+            <div class="desc">
+                <h2>{{ lang.SetDomail }}</h2>
+                <!-- <div style="margin-top: 10px;">{{ lang.domain_desc }}</div> -->
+            </div>
+            <div class="form" style="width: 400px;">
+                <el-form label-width="120px">
+
+                    <el-form-item :label="lang.smtp_domain">
+                        <el-input placeholder="domaim.com" v-model="domainSettings.smtp_domain">
+                            <template #prepend>smtp.</template>
+                        </el-input>
+                    </el-form-item>
+
+                    <el-form-item :label="lang.web_domain">
+                        <el-input placeholder="pmail.domain.com" v-model="domainSettings.web_domain"></el-input>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </div>
+
+
+        <div v-if="active == 4" class="ctn_s">
+            <div class="desc">
+                <h2>{{ lang.setDNS }}</h2>
+                <div style="margin-top: 10px;">{{ lang.dns_desc }}</div>
+            </div>
+            <div class="form" width="600px">
+                <el-table :data="dnsInfos" border style="width: 100%">
+                    <el-table-column prop="host" label="HOSTNAME" width="110px" />
+                    <el-table-column prop="type" label="TYPE" width="110px" />
+                    <el-table-column prop="value" label="VALUE">
+                        <template #default="scope">
+                            <div style="display: flex; align-items: center">
+                                <el-tooltip :content="scope.row.tips" placement="top" v-if="scope.row.tips != ''">
+                                    {{ scope.row.value }}
+                                </el-tooltip>
+                                <span v-else>{{ scope.row.value }}</span>
+                            </div>
+                        </template>
+
+                    </el-table-column>
+                    <el-table-column prop="ttl" label="TTL" width="110px" />
+                </el-table>
+            </div>
+        </div>
+
+        <div v-if="active == 5" class="ctn">
+            <div class="desc">
+                <h2>{{ lang.setSSL }}</h2>
+                <div style="margin-top: 10px;">{{ lang.setSSL }}</div>
+            </div>
+            <div class="form" width="600px">
+                <el-form label-width="120px">
+                    <el-form-item :label="lang.type">
+                        <el-select :placeholder="lang.ssl_auto" v-model="sslSettings.type">
+                            <el-option :label="lang.ssl_auto" value="0" />
+                            <el-option :label="lang.ssl_manuallyf" value="1" />
+                        </el-select>
+                    </el-form-item>
+
+                    <el-form-item :label="lang.ssl_key_path" v-if="sslSettings.type == '1'">
+                        <el-input placeholder="./config/ssl/private.key" v-model="sslSettings.key_path"></el-input>
+                    </el-form-item>
+
+                    <el-form-item :label="lang.ssl_crt_path" v-if="sslSettings.type == '1'">
+                        <el-input placeholder="./config/ssl/public.crt" v-model="sslSettings.crt_path"></el-input>
+                    </el-form-item>
+
+                </el-form>
+            </div>
+        </div>
+
+        <el-button v-loading.fullscreen.lock="fullscreenLoading" id="next" style="margin-top: 12px" @click="next">{{
+            lang.next }}</el-button>
+
+    </div>
+</template>
+
+<script setup>
+import $http from "../http/http";
+
+import { reactive, ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import router from "@/router";  //根路由对象
+import lang from '../i18n/i18n';
+
+const adminSettings = reactive({
+    "account": "admin",
+    "password": "",
+    "password2": "",
+    "hadSeted": false
+})
+
+const dbSettings = reactive({
+    "type": "sqlite",
+    "dsn": "./pmail.db",
+    "lable": ""
+})
+
+const domainSettings = reactive({
+    "web_domain": "",
+    "smtp_domain": ""
+})
+
+const sslSettings = reactive({
+    "type": "0",
+    "key_path": "./config/ssl/private.key",
+    "crt_path": "./config/ssl/public.crt"
+})
+
+const active = ref(0)
+const fullscreenLoading = ref(false)
+
+
+const dnsInfos = ref([
+])
+
+const setPassword = () => {
+    if (adminSettings.hadSeted) {
+        active.value++;
+        getDomainConfig();
+        return;
+    }
+
+    if (adminSettings.password != adminSettings.password2) {
+        ElMessage.error(lang.err_pwd_diff)
+    } else {
+        $http.post("/api/setup", { "action": "set", "step": "password", "account": adminSettings.account, "password": adminSettings.password }).then((res) => {
+            if (res.errorNo != 0) {
+                ElMessage.error(res.errorMsg)
+            } else {
+                active.value++;
+                getDomainConfig();
+            }
+        })
+    }
+}
+
+const getPassword = () => {
+    $http.post("/api/setup", { "action": "get", "step": "password" }).then((res) => {
+        if (res.errorNo != 0) {
+            ElMessage.error(res.errorMsg)
+        } else {
+            adminSettings.hadSeted = res.data != ""
+            if (adminSettings.hadSeted) {
+                adminSettings.account = res.data
+                adminSettings.password = "*******"
+                adminSettings.password2 = "*******"
+            }
+
+        }
+    })
+}
+
+
+const getDbConfig = () => {
+    $http.post("/api/setup", { "action": "get", "step": "database" }).then((res) => {
+        if (res.errorNo != 0) {
+            ElMessage.error(res.errorMsg)
+        } else {
+            dbSettings.type = res.data.db_type;
+            dbSettings.dsn = res.data.db_dsn;
+        }
+    })
+}
+
+const getDomainConfig = () => {
+    $http.post("/api/setup", { "action": "get", "step": "domain" }).then((res) => {
+        if (res.errorNo != 0) {
+            ElMessage.error(res.errorMsg)
+        } else {
+            domainSettings.web_domain = res.data.web_domain;
+            domainSettings.smtp_domain = res.data.smtp_domain;
+        }
+    })
+}
+
+const setDbConfig = () => {
+    $http.post("/api/setup", { "action": "set", "step": "database", "db_type": dbSettings.type, "db_dsn": dbSettings.dsn }).then((res) => {
+        if (res.errorNo != 0) {
+            ElMessage.error(res.errorMsg)
+        } else {
+            active.value++;
+            getPassword();
+        }
+    })
+}
+
+const getDNSConfig = () => {
+    $http.post("/api/setup", { "action": "get", "step": "dns" }).then((res) => {
+        if (res.errorNo != 0) {
+            ElMessage.error(res.errorMsg)
+        } else {
+            dnsInfos.value = res.data
+        }
+    })
+}
+
+
+const getSSLConfig = () => {
+    $http.post("/api/setup", { "action": "get", "step": "ssl" }).then((res) => {
+        if (res.errorNo != 0) {
+            ElMessage.error(res.errorMsg)
+        } else {
+            sslSettings.type = res.data
+        }
+    })
+}
+
+
+const setSSLConfig = () => {
+    fullscreenLoading.value = true;
+    $http.post("/api/setup", { "action": "set", "step": "ssl", "ssl_type": sslSettings.type, "key_path": sslSettings.key_path, "crt_path": sslSettings.crt_path }).then((res) => {
+        if (res.errorNo != 0) {
+            fullscreenLoading.value = false;
+            ElMessage.error(res.errorMsg)
+        } else {
+            setTimeout(function () {
+                window.location.href = "https://" + domainSettings.web_domain;
+            }, 10000);
+
+        }
+    })
+}
+
+
+const setDomainConfig = () => {
+    $http.post("/api/setup", { "action": "set", "step": "domain", "web_domain": domainSettings.web_domain, "smtp_domain": domainSettings.smtp_domain }).then((res) => {
+        if (res.errorNo != 0) {
+            ElMessage.error(res.errorMsg)
+        } else {
+            active.value++;
+            getDNSConfig();
+        }
+    })
+}
+
+
+const next = () => {
+    switch (active.value) {
+        case 0:
+            active.value++
+            getDbConfig();
+            break
+        case 1:
+            setDbConfig();
+            break;
+        case 2:
+            setPassword();
+            break;
+        case 3:
+            setDomainConfig();
+            break;
+        case 4:
+            getSSLConfig();
+            active.value++
+            break
+        case 5:
+            setSSLConfig();
+            active.value++
+            break
+    }
+
+}
+</script>
+
+
+<style scoped>
+#main {
+    width: 100%;
+    height: 100%;
+    background-color: #f1f1f1;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+}
+
+.desc {
+    padding-right: 20px;
+}
+
+#status {}
+
+.ctn {
+    display: flex;
+    justify-content: center;
+}
+
+.ctn_s {
+    display: flex;
+    flex-direction: column;
+
+}
+
+#next {}
+</style>

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

@@ -1,11 +1,16 @@
 {
-  "domain": "jinnrry.com",
+  "logLevel": "debug",
+  "domain": "domain.com",
+  "webDomain": "mail.domain.com",
   "dkimPrivateKeyPath": "config/dkim/dkim.priv",
+  "sslType": "0",
   "SSLPrivateKeyPath": "config/ssl/private.key",
   "SSLPublicKeyPath": "config/ssl/public.crt",
-  "mysqlDSN": "",
+  "dbDSN": "./pmail.db",
+  "dbType": "sqlite",
   "weChatPushAppId": "",
   "weChatPushSecret": "",
   "weChatPushTemplateId": "",
-  "weChatPushUserId": ""
+  "weChatPushUserId": "",
+  "isInit": false
 }

+ 39 - 17
server/config/config.go

@@ -8,25 +8,39 @@ import (
 	"strings"
 )
 
+var IsInit bool
+
 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
+	LogLevel             string            `json:"logLevel"`
+	Domain               string            `json:"domain"`
+	WebDomain            string            `json:"webDomain"`
+	DkimPrivateKeyPath   string            `json:"dkimPrivateKeyPath"`
+	SSLType              string            `json:"sslType"` // 0表示自动生成证书,1表示用户上传证书
+	SSLPrivateKeyPath    string            `json:"SSLPrivateKeyPath"`
+	SSLPublicKeyPath     string            `json:"SSLPublicKeyPath"`
+	DbDSN                string            `json:"dbDSN"`
+	DbType               string            `json:"dbType"`
+	WeChatPushAppId      string            `json:"weChatPushAppId"`
+	WeChatPushSecret     string            `json:"weChatPushSecret"`
+	WeChatPushTemplateId string            `json:"weChatPushTemplateId"`
+	WeChatPushUserId     string            `json:"weChatPushUserId"`
+	IsInit               bool              `json:"isInit"`
+	Tables               map[string]string `json:"-"`
+	TablesInitData       map[string]string `json:"-"`
 }
 
 //go:embed tables/*
 var tableConfig embed.FS
 
+const Version = "2.0.0"
+
+const DBTypeMySQL = "mysql"
+const DBTypeSQLite = "sqlite"
+const SSLTypeAuto = "0" //自动生成证书
+const SSLTypeUser = "1" //用户上传证书
+
+var DBTypes []string = []string{DBTypeMySQL, DBTypeSQLite}
+
 var Instance *Config
 
 func Init() {
@@ -37,25 +51,29 @@ func Init() {
 	if len(args) >= 2 && args[len(args)-1] == "dev" {
 		cfgData, err = os.ReadFile("./config/config.dev.json")
 		if err != nil {
-			panic("dev环境配置文件加载失败" + err.Error())
+			return
 		}
 	} else {
 		cfgData, err = os.ReadFile("./config/config.json")
 		if err != nil {
-			panic("配置文件加载失败" + err.Error())
+			return
 		}
 	}
 
 	err = json.Unmarshal(cfgData, &Instance)
 	if err != nil {
-		panic("配置文件加载失败" + err.Error())
+		return
 	}
 
 	// 读取表设置
 	Instance.Tables = map[string]string{}
 	Instance.TablesInitData = map[string]string{}
 
-	err = fs.WalkDir(tableConfig, "tables", func(path string, info fs.DirEntry, err error) error {
+	root := "tables/mysql"
+	if Instance.DbType == DBTypeSQLite {
+		root = "tables/sqlite"
+	}
+	err = fs.WalkDir(tableConfig, root, 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)
@@ -76,4 +94,8 @@ func Init() {
 		panic(err)
 	}
 
+	if Instance.Domain != "" && Instance.IsInit {
+		IsInit = true
+	}
+
 }

+ 8 - 3
server/config/config.json

@@ -1,11 +1,16 @@
 {
-  "domain": "demo.com",
+  "logLevel": "info",
+  "domain": "domain.com",
+  "webDomain": "mail.domain.com",
   "dkimPrivateKeyPath": "config/dkim/dkim.priv",
+  "sslType": "0",
   "SSLPrivateKeyPath": "config/ssl/private.key",
   "SSLPublicKeyPath": "config/ssl/public.crt",
-  "mysqlDSN": "",
+  "dbDSN": "./pmail.db",
+  "dbType": "sqlite",
   "weChatPushAppId": "",
   "weChatPushSecret": "",
   "weChatPushTemplateId": "",
-  "weChatPushUserId": ""
+  "weChatPushUserId": "",
+  "isInit": false
 }

+ 0 - 1
server/config/dkim/README

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

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

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

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

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

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


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


+ 0 - 0
server/config/tables/email.sql → server/config/tables/mysql/email.sql


+ 0 - 0
server/config/tables/sessions.sql → server/config/tables/mysql/sessions.sql


+ 0 - 0
server/config/tables/user.sql → server/config/tables/mysql/user.sql


+ 0 - 0
server/config/tables/user_auth.sql → server/config/tables/mysql/user_auth.sql


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


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


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

@@ -0,0 +1,26 @@
+CREATE table email
+(
+    id             INTEGER PRIMARY KEY AUTOINCREMENT,
+    type           tinyint(4) NOT NULL DEFAULT 0 ,
+    subject        varchar(1000) NOT NULL DEFAULT '' ,
+    reply_to       json ,
+    from_name      varchar(50)   NOT NULL DEFAULT '' ,
+    from_address   varchar(150)  NOT NULL DEFAULT '' ,
+    `to`           json ,
+    bcc            json ,
+    cc             json ,
+    `text`         text ,
+    html           text ,
+    sender         json ,
+    attachments    json ,
+    spf_check      tinyint(1) DEFAULT 0 ,
+    dkim_check     tinyint(1) DEFAULT 0 ,
+    status         tinyint(4) NOT NULL DEFAULT 0 ,
+    send_user_id   int unsigned NOT NULL DEFAULT 0 ,
+    is_read        tinyint(1) NOT NULL DEFAULT 0 ,
+    error          text ,
+    cron_send_time datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP ,
+    send_date      datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP ,
+    create_time    datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP ,
+    update_time    datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP
+)

+ 8 - 0
server/config/tables/sqlite/sessions.sql

@@ -0,0 +1,8 @@
+CREATE TABLE sessions
+(
+    token  TEXT PRIMARY KEY,
+    data   BLOB NOT NULL,
+    expiry REAL NOT NULL
+);
+
+CREATE INDEX sessions_expiry_idx ON sessions (expiry);

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

@@ -0,0 +1,8 @@
+CREATE TABLE user
+(
+    id       INTEGER PRIMARY KEY AUTOINCREMENT,
+    account  varchar(20),
+    name     varchar(10),
+    password char(32)
+);
+CREATE UNIQUE INDEX udx_account on user (account);

+ 9 - 0
server/config/tables/sqlite/user_auth.sql

@@ -0,0 +1,9 @@
+CREATE TABLE user_auth
+(
+    id            INTEGER PRIMARY KEY AUTOINCREMENT,
+    user_id       int ,
+    email_account varchar(30)
+);
+
+CREATE UNIQUE INDEX udx_uid_ename on user_auth ( user_id, email_account);
+CREATE UNIQUE INDEX udx_ename_uid on user_auth ( email_account,user_id );

+ 4 - 4
server/controllers/email/send.go

@@ -7,12 +7,12 @@ import (
 	"io"
 	"net/http"
 	"pmail/config"
+	"pmail/db"
 	"pmail/dto"
 	"pmail/dto/parsemail"
 	"pmail/dto/response"
 	"pmail/hooks"
 	"pmail/i18n"
-	"pmail/mysql"
 	"pmail/smtp_server"
 	"pmail/utils/async"
 	"strings"
@@ -139,7 +139,7 @@ func Send(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 
 	// 邮件落库
 	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),
+	sqlRes, sqlerr := db.Instance.Exec(db.WithContext(ctx, sql),
 		1,
 		e.Subject,
 		json2string(e.ReplyTo),
@@ -181,12 +181,12 @@ func Send(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 
 		if err != nil {
 			errMsg = err.Error()
-			_, err := mysql.Instance.Exec(mysql.WithContext(ctx, "update email set status =2 ,error=? where id = ? "), errMsg, emailId)
+			_, err := db.Instance.Exec(db.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)
+			_, err := db.Instance.Exec(db.WithContext(ctx, "update email set status =1  where id = ? "), emailId)
 			if err != nil {
 				log.WithContext(ctx).Errorf("sql Error :%+v", err)
 			}

+ 11 - 0
server/controllers/interceptor.go

@@ -0,0 +1,11 @@
+package controllers
+
+import (
+	"net/http"
+	"pmail/config"
+)
+
+func Interceptor(w http.ResponseWriter, r *http.Request) {
+	URL := "https://" + config.Instance.WebDomain + r.URL.Path
+	http.Redirect(w, r, URL, http.StatusMovedPermanently)
+}

+ 7 - 14
server/controllers/login.go

@@ -1,19 +1,18 @@
 package controllers
 
 import (
-	"crypto/md5"
 	"database/sql"
-	"encoding/hex"
 	"encoding/json"
 	log "github.com/sirupsen/logrus"
 	"io"
 	"net/http"
+	"pmail/db"
 	"pmail/dto"
 	"pmail/dto/response"
 	"pmail/i18n"
 	"pmail/models"
-	"pmail/mysql"
 	"pmail/session"
+	"pmail/utils/password"
 )
 
 type loginRequest struct {
@@ -27,18 +26,18 @@ func Login(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 	if err != nil {
 		log.Errorf("%+v", err)
 	}
-	var retData loginRequest
-	err = json.Unmarshal(reqBytes, &retData)
+	var reqData loginRequest
+	err = json.Unmarshal(reqBytes, &reqData)
 	if err != nil {
 		log.Errorf("%+v", err)
 	}
 
 	var user models.User
 
-	encodePwd := md5Encode(md5Encode(retData.Password+"pmail") + "pmail2023")
+	encodePwd := password.Encode(reqData.Password)
 
-	err = mysql.Instance.Get(&user, mysql.WithContext(ctx, "select * from user where account =? and password =?"),
-		retData.Account, encodePwd)
+	err = db.Instance.Get(&user, db.WithContext(ctx, "select * from user where account =? and password =?"),
+		reqData.Account, encodePwd)
 	if err != nil && err != sql.ErrNoRows {
 		log.Errorf("%+v", err)
 	}
@@ -51,9 +50,3 @@ func Login(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
 		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))
-}

+ 4 - 3
server/controllers/settings.go

@@ -5,10 +5,11 @@ import (
 	log "github.com/sirupsen/logrus"
 	"io"
 	"net/http"
+	"pmail/db"
 	"pmail/dto"
 	"pmail/dto/response"
 	"pmail/i18n"
-	"pmail/mysql"
+	"pmail/utils/password"
 )
 
 type modifyPasswordRequest struct {
@@ -27,9 +28,9 @@ func ModifyPassword(ctx *dto.Context, w http.ResponseWriter, req *http.Request)
 	}
 
 	if retData.Password != "" {
-		encodePwd := md5Encode(md5Encode(retData.Password+"pmail") + "pmail2023")
+		encodePwd := password.Encode(retData.Password)
 
-		_, err := mysql.Instance.Exec(mysql.WithContext(ctx, "update user set password = ? where id =?"), encodePwd, ctx.UserInfo.ID)
+		_, err := db.Instance.Exec(db.WithContext(ctx, "update user set password = ? where id =?"), encodePwd, ctx.UserInfo.ID)
 		if err != nil {
 			response.NewErrorResponse(response.ServerError, i18n.GetText(ctx.Lang, "unknowError"), "").FPrint(w)
 			return

+ 145 - 0
server/controllers/setup.go

@@ -0,0 +1,145 @@
+package controllers
+
+import (
+	"encoding/json"
+	"io"
+	"net/http"
+	"pmail/config"
+	"pmail/dto"
+	"pmail/dto/response"
+	"pmail/services/setup"
+	"pmail/services/setup/ssl"
+	"strings"
+)
+
+func AcmeChallenge(w http.ResponseWriter, r *http.Request) {
+	instance := ssl.GetHttpChallengeInstance()
+	token := strings.ReplaceAll(r.URL.Path, "/.well-known/acme-challenge/", "")
+	auth, exist := instance.AuthInfo[token]
+	if exist {
+		w.Write([]byte(auth.KeyAuth))
+	} else {
+		http.NotFound(w, r)
+	}
+}
+
+func Setup(ctx *dto.Context, w http.ResponseWriter, req *http.Request) {
+	reqBytes, err := io.ReadAll(req.Body)
+	if err != nil {
+		response.NewSuccessResponse("").FPrint(w)
+		return
+	}
+
+	var reqData map[string]string
+	err = json.Unmarshal(reqBytes, &reqData)
+
+	if err != nil {
+		response.NewSuccessResponse("").FPrint(w)
+		return
+	}
+
+	if reqData["step"] == "database" && reqData["action"] == "get" {
+		dbType, dbDSN, err := setup.GetDatabaseSettings(ctx)
+		if err != nil {
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
+		}
+
+		response.NewSuccessResponse(map[string]string{
+			"db_type": dbType,
+			"db_dsn":  dbDSN,
+		}).FPrint(w)
+		return
+	}
+
+	if reqData["step"] == "database" && reqData["action"] == "set" {
+		err := setup.SetDatabaseSettings(ctx, reqData["db_type"], reqData["db_dsn"])
+		if err != nil {
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
+		}
+
+		response.NewSuccessResponse("Succ").FPrint(w)
+		return
+	}
+
+	if reqData["step"] == "password" && reqData["action"] == "get" {
+		ok, err := setup.GetAdminPassword(ctx)
+		if err != nil {
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
+		}
+		response.NewSuccessResponse(ok).FPrint(w)
+		return
+	}
+
+	if reqData["step"] == "password" && reqData["action"] == "set" {
+		err := setup.SetAdminPassword(ctx, reqData["account"], reqData["password"])
+		if err != nil {
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
+		}
+		response.NewSuccessResponse("Succ").FPrint(w)
+		return
+	}
+
+	if reqData["step"] == "domain" && reqData["action"] == "get" {
+		smtpDomain, webDomain, err := setup.GetDomainSettings()
+		if err != nil {
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
+		}
+		response.NewSuccessResponse(map[string]string{
+			"smtp_domain": smtpDomain,
+			"web_domain":  webDomain,
+		}).FPrint(w)
+		return
+	}
+
+	if reqData["step"] == "domain" && reqData["action"] == "set" {
+		err := setup.SetDomainSettings(reqData["smtp_domain"], reqData["web_domain"])
+		if err != nil {
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
+		}
+		response.NewSuccessResponse("Succ").FPrint(w)
+		return
+	}
+
+	if reqData["step"] == "dns" && reqData["action"] == "get" {
+		dnsInfos, err := setup.GetDNSSettings(ctx)
+		if err != nil {
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
+		}
+		response.NewSuccessResponse(dnsInfos).FPrint(w)
+		return
+	}
+
+	if reqData["step"] == "ssl" && reqData["action"] == "get" {
+		sslType := ssl.GetSSL()
+		response.NewSuccessResponse(sslType).FPrint(w)
+		return
+	}
+
+	if reqData["step"] == "ssl" && reqData["action"] == "set" {
+		err := ssl.SetSSL(reqData["ssl_type"])
+		if err != nil {
+			response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+			return
+		}
+
+		if reqData["ssl_type"] == config.SSLTypeAuto {
+			err = ssl.GenSSL(false)
+			if err != nil {
+				response.NewErrorResponse(response.ServerError, err.Error(), "").FPrint(w)
+				return
+			}
+		}
+
+		response.NewSuccessResponse("Succ").FPrint(w)
+		setup.Finish(ctx)
+		return
+	}
+
+}

+ 34 - 0
server/cron_server/ssl_update.go

@@ -0,0 +1,34 @@
+package cron_server
+
+import (
+	log "github.com/sirupsen/logrus"
+	"pmail/config"
+	"pmail/services/setup/ssl"
+	"pmail/signal"
+	"time"
+)
+
+func Start() {
+	for {
+		if config.Instance.IsInit {
+			days, err := ssl.CheckSSLCrtInfo()
+			if days < 30 || err != nil {
+				if err != nil {
+					log.Errorf("SSL Check Error, Update SSL Certificate. Error Info :%+v", err)
+				} else {
+					log.Infof("SSL certificate remaining time is only %d days, renew SSL certificate.", days)
+				}
+				err = ssl.GenSSL(true)
+				if err != nil {
+					log.Errorf("SSL Update Error! %+v", err)
+				}
+				// 更新完证书,重启服务
+				signal.RestartChan <- true
+			} else {
+				log.Debugf("SSL Check.")
+			}
+		}
+		// 每24小时检测一次证书有效期
+		time.Sleep(24 * time.Hour)
+	}
+}

+ 24 - 7
server/mysql/init.go → server/db/init.go

@@ -1,27 +1,38 @@
-package mysql
+package db
 
 import (
 	"fmt"
 	_ "github.com/go-sql-driver/mysql"
 	"github.com/jmoiron/sqlx"
 	log "github.com/sirupsen/logrus"
+	_ "modernc.org/sqlite"
 	"pmail/config"
 	"pmail/dto"
+	"pmail/utils/errors"
 )
 
 var Instance *sqlx.DB
 
-func Init() {
-	dsn := config.Instance.MysqlDSN
+func Init() error {
+	dsn := config.Instance.DbDSN
 	var err error
-	Instance, err = sqlx.Open("mysql", dsn)
+
+	switch config.Instance.DbType {
+	case "mysql":
+		Instance, err = sqlx.Open("mysql", dsn)
+	case "sqlite":
+		Instance, err = sqlx.Open("sqlite", dsn)
+	default:
+		return errors.New("Database Type Error!")
+	}
 	if err != nil {
-		panic(err)
+		return errors.Wrap(err)
 	}
 	Instance.SetMaxOpenConns(100)
 	Instance.SetMaxIdleConns(10)
-	showMySQLCharacterSet()
+	//showMySQLCharacterSet()
 	checkTable()
+	return nil
 }
 
 func WithContext(ctx *dto.Context, sql string) string {
@@ -38,7 +49,13 @@ type tables struct {
 
 func checkTable() {
 	var res []*tables
-	err := Instance.Select(&res, "show tables")
+
+	var err error
+	if config.Instance.DbType == "sqlite" {
+		err = Instance.Select(&res, "select name as `Tables_in_pmail` from sqlite_master where type='table'")
+	} else {
+		err = Instance.Select(&res, "show tables")
+	}
 	if err != nil {
 		panic(err)
 	}

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

@@ -6,6 +6,7 @@ import (
 )
 
 const (
+	NeedSetup   = 402
 	NeedLogin   = 403
 	ParamsError = 100
 	ServerError = 500

+ 30 - 8
server/go.mod

@@ -4,25 +4,47 @@ go 1.20
 
 require (
 	github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24
+	github.com/alexedwards/scs/sqlite3store v0.0.0-20230327161757-10d4299e3b24
 	github.com/alexedwards/scs/v2 v2.5.1
+	github.com/emersion/go-message v0.16.0
 	github.com/emersion/go-msgauth v0.6.6
 	github.com/emersion/go-smtp v0.16.0
+	github.com/go-acme/lego/v4 v4.13.3
 	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
+	golang.org/x/crypto v0.10.0
+	golang.org/x/text v0.10.0
+	modernc.org/sqlite v1.24.0
 )
 
+replace github.com/alexedwards/scs/sqlite3store v0.0.0-20230327161757-10d4299e3b24 => github.com/Jinnrry/scs/sqlite3store v0.0.0-20230803080525-914f01e0d379
+
 require (
-	github.com/emersion/go-message v0.16.0 // indirect
+	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
+	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
 	github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
-	github.com/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
+	github.com/go-jose/go-jose/v3 v3.0.0 // indirect
+	github.com/google/uuid v1.3.0 // indirect
+	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
+	github.com/mattn/go-isatty v0.0.19 // indirect
+	github.com/mattn/go-sqlite3 v1.14.17 // indirect
+	github.com/miekg/dns v1.1.55 // indirect
+	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+	golang.org/x/mod v0.11.0 // indirect
+	golang.org/x/net v0.11.0 // indirect
+	golang.org/x/sys v0.9.0 // indirect
+	golang.org/x/tools v0.10.0 // indirect
+	lukechampine.com/uint128 v1.2.0 // indirect
+	modernc.org/cc/v3 v3.40.0 // indirect
+	modernc.org/ccgo/v3 v3.16.13 // indirect
+	modernc.org/libc v1.22.5 // indirect
+	modernc.org/mathutil v1.5.0 // indirect
+	modernc.org/memory v1.5.0 // indirect
+	modernc.org/opt v0.1.3 // indirect
+	modernc.org/strutil v1.1.3 // indirect
+	modernc.org/token v1.0.1 // indirect
 )

+ 68 - 16
server/go.sum

@@ -1,10 +1,16 @@
+github.com/Jinnrry/scs/sqlite3store v0.0.0-20230803080525-914f01e0d379 h1:i6LB/3lgkRDupe3owyNXtH8dtQrdaReCLeAZKrWcqAE=
+github.com/Jinnrry/scs/sqlite3store v0.0.0-20230803080525-914f01e0d379/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss=
 github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24 h1:1jXpX7IE/zuf9FZQJpqZNepXqW8mq6NLzplHDCA43HY=
 github.com/alexedwards/scs/mysqlstore v0.0.0-20230327161757-10d4299e3b24/go.mod h1:ShejCOaSJCEjCWjc7YBrgy2xd0Kp+wiyBdzTNQrAGn4=
 github.com/alexedwards/scs/v2 v2.5.1 h1:EhAz3Kb3OSQzD8T+Ub23fKsiuvE0GzbF5Lgn0uTwM3Y=
 github.com/alexedwards/scs/v2 v2.5.1/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
 github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
 github.com/emersion/go-message v0.16.0 h1:uZLz8ClLv3V5fSFF/fFdW9jXjrZkXIpE1Fn8fKx7pO4=
@@ -20,26 +26,43 @@ github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:
 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-acme/lego/v4 v4.13.3 h1:aZ1S9FXIkCWG3Uw/rZKSD+MOuO8ZB1t6p9VCg6jJiNY=
+github.com/go-acme/lego/v4 v4.13.3/go.mod h1:c/iodVGMeBXG/+KiQczoNkySo3YLWTVa0kiyeVd/FHc=
+github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
+github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
 github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
 github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
 github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/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-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
-github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
+github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
+github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
+github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
+github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
 github.com/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/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
@@ -47,26 +70,29 @@ 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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/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-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 h1:SLP7Q4Di66FONjDJbCYrCRrh97focO6sLogHO7/g8F0=
 golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
+golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
 golang.org/x/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/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
+golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 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/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
+golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
 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/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
 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=
@@ -76,24 +102,50 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
 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/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
+golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/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/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
+golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 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/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
+golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
+lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
+modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
+modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
+modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
+modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
+modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
+modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
+modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
+modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
+modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
+modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sqlite v1.24.0 h1:EsClRIWHGhLTCX44p+Ri/JLD+vFGo0QGjasg2/F9TlI=
+modernc.org/sqlite v1.24.0/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
+modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
+modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
+modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
+modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
+modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=

+ 11 - 12
server/hooks/wechat_push/wechat_push.go

@@ -41,6 +41,10 @@ func (w *WeChatPushHook) ReceiveParseBefore(email []byte) {
 }
 
 func (w *WeChatPushHook) ReceiveParseAfter(email *parsemail.Email) {
+	if w.appId == "" || w.secret == "" || w.pushUser == "" {
+		return
+	}
+
 	w.sendUserMsg(nil, w.pushUser, string(email.Text))
 }
 
@@ -91,18 +95,13 @@ func (w *WeChatPushHook) sendUserMsg(ctx *dto.Context, userId string, content st
 
 }
 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
 
+	ret := &WeChatPushHook{
+		appId:      config.Instance.WeChatPushAppId,
+		secret:     config.Instance.WeChatPushSecret,
+		templateId: config.Instance.WeChatPushTemplateId,
+		pushUser:   config.Instance.WeChatPushUserId,
 	}
-	return nil
+	return ret
+
 }

+ 35 - 0
server/http_server/http_server.go

@@ -0,0 +1,35 @@
+package http_server
+
+import (
+	"fmt"
+	"net/http"
+	"pmail/controllers"
+	"time"
+)
+
+const HttpPort = 80
+
+// 这个服务是为了拦截http请求转发到https
+var httpServer *http.Server
+
+func HttpStop() {
+	if httpServer != nil {
+		httpServer.Close()
+	}
+}
+
+func HttpStart() {
+	mux := http.NewServeMux()
+	mux.HandleFunc("/", controllers.Interceptor)
+	httpServer = &http.Server{
+		Addr:         fmt.Sprintf(":%d", HttpPort),
+		Handler:      mux,
+		ReadTimeout:  time.Second * 60,
+		WriteTimeout: time.Second * 60,
+	}
+
+	err := httpServer.ListenAndServe()
+	if err != nil {
+		panic(err)
+	}
+}

+ 37 - 31
server/http_server/main.go → server/http_server/https_server.go

@@ -9,14 +9,17 @@ import (
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cast"
 	"io/fs"
+	olog "log"
 	"math/rand"
 	"net"
 	"net/http"
 	"os"
+	"pmail/config"
 	"pmail/controllers"
 	"pmail/controllers/email"
 	"pmail/dto"
 	"pmail/dto/response"
+	"pmail/i18n"
 	"pmail/session"
 	"time"
 )
@@ -24,12 +27,19 @@ import (
 //go:embed dist/*
 var local embed.FS
 
-var ip string
+const HttpsPort = 443
 
-const HttpPort = 80
+var httpsServer *http.Server
 
-func Start() {
-	log.Infof("Http Server Start at :%d", HttpPort)
+type nullWrite struct {
+}
+
+func (w *nullWrite) Write(p []byte) (int, error) {
+	return len(p), nil
+}
+
+func HttpsStart() {
+	log.Infof("Http Server Start")
 
 	mux := http.NewServeMux()
 
@@ -49,36 +59,27 @@ func Start() {
 	mux.HandleFunc("/attachments/", contextIterceptor(controllers.GetAttachments))
 	mux.HandleFunc("/attachments/download/", contextIterceptor(controllers.Download))
 
-	server := &http.Server{
-		Addr:         fmt.Sprintf(":%d", HttpPort),
+	// go http server会打一堆没用的日志,写一个空的日志处理器,屏蔽掉日志输出
+	nullLog := olog.New(&nullWrite{}, "", olog.Ldate)
+
+	httpsServer = &http.Server{
+		Addr:         fmt.Sprintf(":%d", HttpsPort),
 		Handler:      session.Instance.LoadAndSave(mux),
 		ReadTimeout:  time.Second * 60,
 		WriteTimeout: time.Second * 60,
+		ErrorLog:     nullLog,
 	}
 
-	//err := server.ListenAndServeTLS( "config/ssl/public.crt", "config/ssl/private.key", nil)
-	err = server.ListenAndServe()
+	err = httpsServer.ListenAndServeTLS("config/ssl/public.crt", "config/ssl/private.key")
 	if err != nil {
 		panic(err)
 	}
 }
 
-func getLocalIP() string {
-	ip := "127.0.0.1"
-	addrs, err := net.InterfaceAddrs()
-	if err != nil {
-		return ip
+func HttpsStop() {
+	if httpsServer != nil {
+		httpsServer.Close()
 	}
-	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 {
@@ -118,15 +119,20 @@ func contextIterceptor(h controllers.HandlerFunc) http.HandlerFunc {
 		}
 		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
+		if config.IsInit {
+			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, i18n.GetText(ctx.Lang, "login_exp"), "").FPrint(w)
+					return
+				}
 			}
+		} else if r.URL.Path != "/api/setup" {
+			response.NewErrorResponse(response.NeedSetup, "", "").FPrint(w)
+			return
 		}
 		h(ctx, w, r)
 	}

+ 63 - 0
server/http_server/setup_server.go

@@ -0,0 +1,63 @@
+package http_server
+
+import (
+	"fmt"
+	"io/fs"
+	"net"
+	"net/http"
+	"pmail/controllers"
+	"time"
+)
+
+var ip string
+
+// 项目初始化引导用的服务,初始化引导结束后即退出
+var setupServer *http.Server
+
+func SetupStart() {
+	mux := http.NewServeMux()
+	fe, err := fs.Sub(local, "dist")
+	if err != nil {
+		panic(err)
+	}
+	mux.Handle("/", http.FileServer(http.FS(fe)))
+	mux.HandleFunc("/api/", contextIterceptor(controllers.Setup))
+	// 挑战请求类似这样 /.well-known/acme-challenge/QPyMAyaWw9s5JvV1oruyqWHG7OqkHMJEHPoUz2046KM
+	mux.HandleFunc("/.well-known/", controllers.AcmeChallenge)
+
+	setupServer = &http.Server{
+		Addr:         fmt.Sprintf(":%d", HttpPort),
+		Handler:      mux,
+		ReadTimeout:  time.Second * 60,
+		WriteTimeout: time.Second * 60,
+	}
+	err = setupServer.ListenAndServe()
+	if err != nil && err != http.ErrServerClosed {
+		panic(err)
+	}
+}
+
+func SetupStop() {
+	err := setupServer.Close()
+	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
+}

+ 4 - 0
server/i18n/i18n.go

@@ -11,6 +11,8 @@ var (
 		"succ":        "成功",
 		"send_fail":   "发送失败",
 		"att_err":     "附件解码错误",
+		"login_exp":   "登录已失效",
+		"ip_taps":     "这是你服务器IP,确保这个IP正确",
 	}
 	en = map[string]string{
 		"all_email":   "All Email",
@@ -22,6 +24,8 @@ var (
 		"succ":        "Success",
 		"send_fail":   "Send Failure",
 		"att_err":     "Attachment decoding error",
+		"login_exp":   "Login has expired.",
+		"ip_taps":     "This is your server's IP, make sure it is correct.",
 	}
 )
 

+ 23 - 18
server/main.go

@@ -6,13 +6,9 @@ import (
 	log "github.com/sirupsen/logrus"
 	"os"
 	"pmail/config"
+	"pmail/cron_server"
 	"pmail/dto"
-	"pmail/dto/parsemail"
-	"pmail/hooks"
-	"pmail/http_server"
-	"pmail/mysql"
-	"pmail/session"
-	"pmail/smtp_server"
+	"pmail/res_init"
 	"time"
 )
 
@@ -52,30 +48,39 @@ func main() {
 	// 日志消息输出可以是任意的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()
+	switch config.Instance.LogLevel {
+	case "":
+		log.SetLevel(log.InfoLevel)
+	case "debug":
+		log.SetLevel(log.DebugLevel)
+	case "info":
+		log.SetLevel(log.InfoLevel)
+	case "warn":
+		log.SetLevel(log.WarnLevel)
+	case "error":
+		log.SetLevel(log.ErrorLevel)
+	default:
+		log.SetLevel(log.InfoLevel)
+	}
 
 	log.Infoln("***************************************************")
-	log.Infoln("***\tServer Start Success Version:1.0.0")
+	log.Infof("***\tServer Start Success Version:%s\n", config.Version)
 	log.Infof("***\tGit Commit Hash: %s ", gitHash)
 	log.Infof("***\tBuild TimeStamp: %s ", buildTime)
 	log.Infof("***\tBuild GoLang Version: %s ", goVersion)
 	log.Infoln("***************************************************")
 
+	// 定时任务启动
+	go cron_server.Start()
+
+	// 核心服务启动
+	res_init.Init()
+
 	s := make(chan bool)
 	<-s
 }

+ 72 - 0
server/res_init/init.go

@@ -0,0 +1,72 @@
+package res_init
+
+import (
+	log "github.com/sirupsen/logrus"
+	"os"
+	"pmail/config"
+	"pmail/db"
+	"pmail/dto/parsemail"
+	"pmail/hooks"
+	"pmail/http_server"
+	"pmail/session"
+	"pmail/signal"
+	"pmail/smtp_server"
+	"pmail/utils/file"
+)
+
+func Init() {
+
+	if !config.IsInit {
+		dirInit()
+
+		log.Infof("Please click http://127.0.0.1 to continue.\n")
+		go http_server.SetupStart()
+		<-signal.InitChan
+		http_server.SetupStop()
+	}
+
+	for {
+		config.Init()
+		parsemail.Init()
+		err := db.Init()
+		if err != nil {
+			panic(err)
+		}
+		session.Init()
+		hooks.Init()
+		// smtp server start
+		go smtp_server.Start()
+		// http server start
+		go http_server.HttpsStart()
+		go http_server.HttpStart()
+		<-signal.RestartChan
+		log.Infof("Server Restart!")
+		smtp_server.Stop()
+		http_server.HttpsStop()
+		http_server.HttpStop()
+	}
+
+}
+
+func dirInit() {
+	if !file.PathExist("./config") {
+		err := os.MkdirAll("./config", 0744)
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	if !file.PathExist("./config/dkim") {
+		err := os.MkdirAll("./config/dkim", 0744)
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	if !file.PathExist("./config/ssl") {
+		err := os.MkdirAll("./config/ssl", 0744)
+		if err != nil {
+			panic(err)
+		}
+	}
+}

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

@@ -3,10 +3,10 @@ package attachments
 import (
 	"encoding/json"
 	log "github.com/sirupsen/logrus"
+	"pmail/db"
 	"pmail/dto"
 	"pmail/dto/parsemail"
 	"pmail/models"
-	"pmail/mysql"
 	"pmail/services/auth"
 )
 
@@ -14,7 +14,7 @@ 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)
+	err := db.Instance.Get(&email, db.WithContext(ctx, "select * from email where id = ?"), emailId)
 	if err != nil {
 		log.WithContext(ctx).Errorf("SQL error:%+v", err)
 		return "", nil
@@ -39,7 +39,7 @@ func GetAttachmentsByIndex(ctx *dto.Context, emailId int, index int) (string, []
 
 	// 获取邮件内容
 	var email models.Email
-	err := mysql.Instance.Get(&email, mysql.WithContext(ctx, "select * from email where id = ?"), emailId)
+	err := db.Instance.Get(&email, db.WithContext(ctx, "select * from email where id = ?"), emailId)
 	if err != nil {
 		log.WithContext(ctx).Errorf("SQL error:%+v", err)
 		return "", nil

+ 80 - 2
server/services/auth/auth.go

@@ -1,10 +1,17 @@
 package auth
 
 import (
+	"crypto"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"encoding/base64"
+	"encoding/pem"
 	log "github.com/sirupsen/logrus"
+	"os"
+	"pmail/db"
 	"pmail/dto"
 	"pmail/models"
-	"pmail/mysql"
 	"strings"
 )
 
@@ -12,7 +19,7 @@ import (
 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)
+	err := db.Instance.Select(&auth, db.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
@@ -31,3 +38,74 @@ func HasAuth(ctx *dto.Context, email *models.Email) bool {
 
 	return hasAuth
 }
+
+func DkimGen() string {
+	privKeyStr, _ := os.ReadFile("./config/dkim/dkim.priv")
+	publicKeyStr, _ := os.ReadFile("./config/dkim/dkim.public")
+	if len(privKeyStr) > 0 && len(publicKeyStr) > 0 {
+		return string(publicKeyStr)
+	}
+
+	var (
+		privKey crypto.Signer
+		err     error
+	)
+
+	privKey, err = rsa.GenerateKey(rand.Reader, 1024)
+
+	if err != nil {
+		log.Fatalf("Failed to generate key: %v", err)
+	}
+
+	privBytes, err := x509.MarshalPKCS8PrivateKey(privKey)
+	if err != nil {
+		log.Fatalf("Failed to marshal private key: %v", err)
+	}
+
+	f, err := os.OpenFile("./config/dkim/dkim.priv", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
+	if err != nil {
+		log.Fatalf("Failed to create key file: %v", err)
+	}
+	defer f.Close()
+
+	privBlock := pem.Block{
+		Type:  "PRIVATE KEY",
+		Bytes: privBytes,
+	}
+	if err := pem.Encode(f, &privBlock); err != nil {
+		log.Fatalf("Failed to write key PEM block: %v", err)
+	}
+	if err := f.Close(); err != nil {
+		log.Fatalf("Failed to close key file: %v", err)
+	}
+
+	var pubBytes []byte
+
+	switch pubKey := privKey.Public().(type) {
+	case *rsa.PublicKey:
+		// RFC 6376 is inconsistent about whether RSA public keys should
+		// be formatted as RSAPublicKey or SubjectPublicKeyInfo.
+		// Erratum 3017 (https://www.rfc-editor.org/errata/eid3017)
+		// proposes allowing both.  We use SubjectPublicKeyInfo for
+		// consistency with other implementations including opendkim,
+		// Gmail, and Fastmail.
+		pubBytes, err = x509.MarshalPKIXPublicKey(pubKey)
+		if err != nil {
+			log.Fatalf("Failed to marshal public key: %v", err)
+		}
+	default:
+		panic("unreachable")
+	}
+
+	params := []string{
+		"v=DKIM1",
+		"k=rsa",
+		"p=" + base64.StdEncoding.EncodeToString(pubBytes),
+	}
+
+	publicKey := strings.Join(params, "; ")
+
+	os.WriteFile("./config/dkim/dkim.public", []byte(publicKey), 0666)
+
+	return publicKey
+}

+ 7 - 0
server/services/auth/auth_test.go

@@ -0,0 +1,7 @@
+package auth
+
+import "testing"
+
+func TestDkimGen(t *testing.T) {
+	DkimGen()
+}

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

@@ -5,24 +5,24 @@ import (
 	"encoding/json"
 	"fmt"
 	log "github.com/sirupsen/logrus"
+	"pmail/db"
 	"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)
+	err := db.Instance.Get(&email, db.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)
+		_, err = db.Instance.Exec(db.WithContext(ctx, "update email set is_read =1 where id =?"), email.Id)
 		if err != nil {
 			log.WithContext(ctx).Errorf("SQL error:%+v", err)
 		}

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

@@ -3,9 +3,9 @@ package list
 import (
 	"encoding/json"
 	log "github.com/sirupsen/logrus"
+	"pmail/db"
 	"pmail/dto"
 	"pmail/models"
-	"pmail/mysql"
 )
 
 func GetEmailList(ctx *dto.Context, tag string, keyword string, offset, limit int) (emailList []*models.Email, total int) {
@@ -13,12 +13,12 @@ func GetEmailList(ctx *dto.Context, tag string, keyword string, offset, limit in
 	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...)
+	err := db.Instance.Select(&emailList, querySQL, queryParams...)
 	if err != nil {
 		log.Errorf("SQL ERROR: %s ,Error:%s", querySQL, err)
 	}
 
-	err = mysql.Instance.Get(&total, counterSQL, counterParams...)
+	err = db.Instance.Get(&total, counterSQL, counterParams...)
 	if err != nil {
 		log.Errorf("SQL ERROR: %s ,Error:%s", querySQL, err)
 	}

+ 120 - 0
server/services/setup/db.go

@@ -0,0 +1,120 @@
+package setup
+
+import (
+	"encoding/json"
+	"os"
+	"pmail/config"
+	"pmail/db"
+	"pmail/dto"
+	"pmail/models"
+	"pmail/utils/array"
+	"pmail/utils/errors"
+	"pmail/utils/file"
+	"pmail/utils/password"
+)
+
+func GetDatabaseSettings(ctx *dto.Context) (string, string, error) {
+	configData, err := ReadConfig()
+	if err != nil {
+		return "", "", errors.Wrap(err)
+	}
+
+	if configData.DbType == "" && configData.DbDSN == "" {
+		return config.DBTypeSQLite, "./pmail.db", nil
+	}
+
+	return configData.DbType, configData.DbDSN, nil
+}
+
+func GetAdminPassword(ctx *dto.Context) (string, error) {
+
+	users := []*models.User{}
+	err := db.Instance.Select(&users, "select * from user")
+	if err != nil {
+		return "", errors.Wrap(err)
+	}
+
+	if len(users) > 0 {
+		return users[0].Account, nil
+	}
+
+	return "", nil
+}
+
+func SetAdminPassword(ctx *dto.Context, account, pwd string) error {
+	encodePwd := password.Encode(pwd)
+	res, err := db.Instance.Exec(db.WithContext(ctx, "INSERT INTO user (account, name, password) VALUES (?, 'admin',?)"), account, encodePwd)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+	id, err := res.LastInsertId()
+	if err != nil {
+		return errors.Wrap(err)
+	}
+	_, err = db.Instance.Exec(db.WithContext(ctx, "INSERT INTO user_auth (user_id, email_account) VALUES (?, '*')"), id)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+	return nil
+}
+
+func SetDatabaseSettings(ctx *dto.Context, dbType, dbDSN string) error {
+	configData, err := ReadConfig()
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	if !array.InArray(dbType, config.DBTypes) {
+		return errors.New("dbtype error")
+	}
+
+	configData.DbType = dbType
+	configData.DbDSN = dbDSN
+
+	err = WriteConfig(configData)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+	config.Init()
+	// 检查数据库是否能正确连接
+	err = db.Init()
+	if err != nil {
+		return errors.Wrap(err)
+	}
+	return nil
+}
+
+func WriteConfig(cfg *config.Config) error {
+	bytes, _ := json.Marshal(cfg)
+	err := os.WriteFile("./config/config.json", bytes, 0666)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+	return nil
+}
+
+func ReadConfig() (*config.Config, error) {
+	configData := config.Config{
+		DkimPrivateKeyPath: "config/dkim/dkim.priv",
+		SSLPrivateKeyPath:  "config/ssl/private.key",
+		SSLPublicKeyPath:   "config/ssl/public.crt",
+	}
+	if !file.PathExist("./config/config.json") {
+		bytes, _ := json.Marshal(configData)
+		err := os.WriteFile("./config/config.json", bytes, 0666)
+		if err != nil {
+			return nil, errors.Wrap(err)
+		}
+	} else {
+		cfgData, err := os.ReadFile("./config/config.json")
+		if err != nil {
+			return nil, errors.Wrap(err)
+		}
+
+		err = json.Unmarshal(cfgData, &configData)
+		if err != nil {
+			return nil, errors.Wrap(err)
+		}
+	}
+	return &configData, nil
+}

+ 54 - 0
server/services/setup/dns.go

@@ -0,0 +1,54 @@
+package setup
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"pmail/dto"
+	"pmail/i18n"
+	"pmail/services/auth"
+	"pmail/utils/errors"
+)
+
+type DNSItem struct {
+	Type  string `json:"type"`
+	Host  string `json:"host"`
+	Value string `json:"value"`
+	TTL   int    `json:"ttl"`
+	Tips  string `json:"tips"`
+}
+
+func GetDNSSettings(ctx *dto.Context) ([]*DNSItem, error) {
+	configData, err := ReadConfig()
+	if err != nil {
+		return nil, errors.Wrap(err)
+	}
+
+	ret := []*DNSItem{
+		{Type: "A", Host: "smtp", Value: getIp(), TTL: 3600, Tips: i18n.GetText(ctx.Lang, "ip_taps")},
+		{Type: "MX", Host: "-", Value: fmt.Sprintf("smtp.%s", configData.Domain), TTL: 3600},
+		{Type: "TXT", Host: "-", Value: "v=spf1 a mx ~all", TTL: 3600},
+		{Type: "TXT", Host: "default._domainkey", Value: auth.DkimGen(), TTL: 3600},
+	}
+	return ret, nil
+}
+
+func getIp() string {
+	resp, err := http.Get("http://ip-api.com/json/?lang=zh-CN ")
+	if err != nil {
+		return "Your Server IP"
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode == 200 {
+		body, err := io.ReadAll(resp.Body)
+		if err == nil {
+			var queryRes map[string]string
+			_ = json.Unmarshal(body, &queryRes)
+
+			return queryRes["query"]
+		}
+	}
+	return "Your Server IP"
+}

+ 7 - 0
server/services/setup/dns_test.go

@@ -0,0 +1,7 @@
+package setup
+
+import "testing"
+
+func TestGetIp(t *testing.T) {
+	getIp()
+}

+ 40 - 0
server/services/setup/domain.go

@@ -0,0 +1,40 @@
+package setup
+
+import (
+	"pmail/utils/errors"
+)
+
+func GetDomainSettings() (string, string, error) {
+	configData, err := ReadConfig()
+	if err != nil {
+		return "", "", errors.Wrap(err)
+	}
+
+	return configData.Domain, configData.WebDomain, nil
+}
+
+func SetDomainSettings(smtpDomain, webDomain string) error {
+	configData, err := ReadConfig()
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	if smtpDomain == "" {
+		return errors.New("domain must not empty!")
+	}
+
+	if webDomain == "" {
+		return errors.New("web domain must not empty!")
+	}
+
+	configData.Domain = smtpDomain
+	configData.WebDomain = webDomain
+
+	// 检查域名是否指向本机 todo
+
+	err = WriteConfig(configData)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+	return nil
+}

+ 24 - 0
server/services/setup/finish.go

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

+ 37 - 0
server/services/setup/ssl/challenge.go

@@ -0,0 +1,37 @@
+package ssl
+
+type authInfo struct {
+	Domain  string
+	Token   string
+	KeyAuth string
+}
+
+type HttpChallenge struct {
+	AuthInfo map[string]*authInfo
+}
+
+var instance *HttpChallenge
+
+func (h *HttpChallenge) Present(domain, token, keyAuth string) error {
+	h.AuthInfo[token] = &authInfo{
+		Domain:  domain,
+		Token:   token,
+		KeyAuth: keyAuth,
+	}
+
+	return nil
+}
+
+func (h *HttpChallenge) CleanUp(domain, token, keyAuth string) error {
+	delete(h.AuthInfo, token)
+	return nil
+}
+
+func GetHttpChallengeInstance() *HttpChallenge {
+	if instance == nil {
+		instance = &HttpChallenge{
+			AuthInfo: map[string]*authInfo{},
+		}
+	}
+	return instance
+}

+ 172 - 0
server/services/setup/ssl/ssl.go

@@ -0,0 +1,172 @@
+package ssl
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/tls"
+	"crypto/x509"
+	"github.com/go-acme/lego/v4/certificate"
+	"github.com/spf13/cast"
+	"os"
+	"pmail/config"
+	"pmail/services/setup"
+	"pmail/utils/errors"
+	"time"
+
+	"github.com/go-acme/lego/v4/certcrypto"
+	"github.com/go-acme/lego/v4/lego"
+	"github.com/go-acme/lego/v4/registration"
+)
+
+type MyUser struct {
+	Email        string
+	Registration *registration.Resource
+	key          crypto.PrivateKey
+}
+
+func (u *MyUser) GetEmail() string {
+	return u.Email
+}
+func (u MyUser) GetRegistration() *registration.Resource {
+	return u.Registration
+}
+func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
+	return u.key
+}
+
+func GetSSL() string {
+	cfg, err := setup.ReadConfig()
+	if err != nil {
+		panic(err)
+	}
+	if cfg.SSLType == "" {
+		return config.SSLTypeAuto
+	}
+
+	return cfg.SSLType
+}
+
+func SetSSL(sslType string) error {
+	cfg, err := setup.ReadConfig()
+	if err != nil {
+		panic(err)
+	}
+	if sslType == config.SSLTypeAuto || sslType == config.SSLTypeUser {
+		cfg.SSLType = sslType
+	} else {
+		return errors.New("SSL Type Error!")
+	}
+
+	err = setup.WriteConfig(cfg)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	return nil
+}
+
+func GenSSL(update bool) error {
+
+	cfg, err := setup.ReadConfig()
+	if err != nil {
+		panic(err)
+	}
+
+	if !update {
+		privateFile, errpi := os.ReadFile(cfg.SSLPrivateKeyPath)
+		public, errpu := os.ReadFile(cfg.SSLPublicKeyPath)
+		// 当前存在证书数据,就不生成了
+		if errpi == nil && errpu == nil && len(privateFile) > 0 && len(public) > 0 {
+			return nil
+		}
+	}
+
+	// Create a user. New accounts need an email and private key to start.
+	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	myUser := MyUser{
+		Email: "i@" + cfg.Domain,
+		key:   privateKey,
+	}
+
+	config := lego.NewConfig(&myUser)
+
+	config.Certificate.KeyType = certcrypto.RSA2048
+
+	// A client facilitates communication with the CA server.
+	client, err := lego.NewClient(config)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	err = client.Challenge.SetHTTP01Provider(GetHttpChallengeInstance())
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
+	if err != nil {
+		return errors.Wrap(err)
+	}
+	myUser.Registration = reg
+
+	request := certificate.ObtainRequest{
+		Domains: []string{"smtp." + cfg.Domain, cfg.WebDomain},
+		Bundle:  true,
+	}
+	certificates, err := client.Certificate.Obtain(request)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	err = os.WriteFile("./config/ssl/private.key", certificates.PrivateKey, 0666)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	err = os.WriteFile("./config/ssl/public.crt", certificates.Certificate, 0666)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	err = os.WriteFile("./config/ssl/issuerCert.crt", certificates.IssuerCertificate, 0666)
+	if err != nil {
+		return errors.Wrap(err)
+	}
+
+	return nil
+}
+
+// CheckSSLCrtInfo 返回证书过期剩余天数
+func CheckSSLCrtInfo() (int, error) {
+
+	cfg, err := setup.ReadConfig()
+	if err != nil {
+		panic(err)
+	}
+	// load cert and key by tls.LoadX509KeyPair
+	tlsCert, err := tls.LoadX509KeyPair(cfg.SSLPublicKeyPath, cfg.SSLPrivateKeyPath)
+	if err != nil {
+		return -1, errors.Wrap(err)
+	}
+
+	cert, err := x509.ParseCertificate(tlsCert.Certificate[0])
+
+	if err != nil {
+		return -1, errors.Wrap(err)
+	}
+
+	// 检查过期时间
+	hours := cert.NotAfter.Sub(time.Now()).Hours()
+
+	if hours <= 0 {
+		return -1, errors.New("Certificate has expired")
+	}
+
+	return cast.ToInt(hours / 24), nil
+}

+ 17 - 0
server/services/setup/ssl/ssl_test.go

@@ -0,0 +1,17 @@
+package ssl
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestGenSSL(t *testing.T) {
+	err := GenSSL(false)
+	fmt.Println(err)
+}
+
+func TestGetSSLCrtInfo(t *testing.T) {
+	days, err := CheckSSLCrtInfo()
+
+	fmt.Println(days, err)
+}

+ 9 - 3
server/session/init.go

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

+ 4 - 0
server/signal/signal.go

@@ -0,0 +1,4 @@
+package signal
+
+var InitChan = make(chan bool)
+var RestartChan = make(chan bool)

+ 19 - 11
server/smtp_server/main.go

@@ -43,19 +43,21 @@ func (s *Session) Logout() error {
 	return nil
 }
 
+var instance *smtp.Server
+
 func Start() {
 	be := &Backend{}
 
-	s := smtp.NewServer(be)
+	instance = 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
+	instance.Addr = ":25"
+	instance.Domain = config.Instance.Domain
+	instance.ReadTimeout = 10 * time.Second
+	instance.WriteTimeout = 10 * time.Second
+	instance.MaxMessageBytes = 1024 * 1024
+	instance.MaxRecipients = 50
 	// force TLS for auth
-	s.AllowInsecureAuth = false
+	instance.AllowInsecureAuth = false
 	// Load the certificate and key
 	cer, err := tls.LoadX509KeyPair(config.Instance.SSLPublicKeyPath, config.Instance.SSLPrivateKeyPath)
 	if err != nil {
@@ -63,10 +65,16 @@ func Start() {
 		return
 	}
 	// Configure the TLS support
-	s.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cer}}
+	instance.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cer}}
 
-	log.Println("Starting server at", s.Addr)
-	if err := s.ListenAndServe(); err != nil {
+	log.Println("Starting server at", instance.Addr)
+	if err := instance.ListenAndServe(); err != nil {
 		log.Fatal(err)
 	}
 }
+
+func Stop() {
+	if instance != nil {
+		instance.Close()
+	}
+}

+ 2 - 2
server/smtp_server/read_content.go

@@ -8,9 +8,9 @@ import (
 	"io"
 	"net"
 	"net/netip"
+	"pmail/db"
 	"pmail/dto/parsemail"
 	"pmail/hooks"
-	"pmail/mysql"
 	"pmail/utils/async"
 	"strings"
 	"time"
@@ -65,7 +65,7 @@ func (s *Session) Data(r io.Reader) error {
 	}
 
 	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,
+	_, err = db.Instance.Exec(sql,
 		email.Date,
 		email.Subject,
 		json2string(email.ReplyTo),

+ 2 - 2
server/smtp_server/read_content_test.go

@@ -9,8 +9,8 @@ import (
 	"os"
 	"path/filepath"
 	"pmail/config"
+	"pmail/db"
 	parsemail2 "pmail/dto/parsemail"
-	"pmail/mysql"
 	"pmail/session"
 	"testing"
 	"time"
@@ -39,7 +39,7 @@ func testInit() {
 
 	config.Init()
 	parsemail2.Init()
-	mysql.Init()
+	db.Init()
 	session.Init()
 
 }

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

@@ -17,3 +17,78 @@ func Join[T any](arg []T, str string) string {
 	}
 	return ret.String()
 }
+
+// Unique 数组去重
+func Unique[T comparable](slice []T) []T {
+	mp := map[T]bool{}
+	for _, v := range slice {
+		mp[v] = true
+	}
+	ret := []T{}
+	for t, _ := range mp {
+		ret = append(ret, t)
+	}
+	return ret
+}
+
+// Merge 求并集
+func Merge[T any](slice1, slice2 []T) []T {
+	s1Len := len(slice1)
+
+	slice3 := make([]T, s1Len+len(slice2))
+	for i, t := range slice1 {
+		slice3[i] = t
+	}
+
+	for i, t := range slice2 {
+		slice3[s1Len+i] = t
+	}
+
+	return slice3
+}
+
+// Intersect 求交集
+func Intersect[T comparable](slice1, slice2 []T) []T {
+	m := make(map[T]bool)
+	nn := make([]T, 0)
+	for _, v := range slice1 {
+		m[v] = true
+	}
+
+	for _, v := range slice2 {
+		exist, _ := m[v]
+		if exist {
+			nn = append(nn, v)
+		}
+	}
+	return nn
+}
+
+// Difference 求差集 slice1-并集
+func Difference[T comparable](slice1, slice2 []T) []T {
+	m := make(map[T]bool)
+	nn := make([]T, 0)
+	inter := Intersect(slice1, slice2)
+	for _, v := range inter {
+		m[v] = true
+	}
+
+	for _, value := range slice1 {
+		exist, _ := m[value]
+		if !exist {
+			nn = append(nn, value)
+		}
+	}
+	return nn
+}
+
+// InArray 判断元素是否在数组中
+func InArray[T comparable](needle T, haystack []T) bool {
+	for _, t := range haystack {
+		if needle == t {
+			return true
+		}
+	}
+
+	return false
+}

+ 34 - 0
server/utils/errors/error.go

@@ -0,0 +1,34 @@
+package errors
+
+import (
+	oe "errors"
+	"fmt"
+	"runtime"
+)
+
+func New(text string) error {
+	_, file, line, _ := runtime.Caller(1)
+	return oe.New(fmt.Sprintf("%s at %s:%d", text, file, line))
+}
+
+func Wrap(err error) error {
+	_, file, line, _ := runtime.Caller(1)
+	return fmt.Errorf("at %s:%d\n%w", file, line, err)
+}
+
+func WrapWithMsg(err error, msg string) error {
+	_, file, line, _ := runtime.Caller(1)
+	return fmt.Errorf("%s at %s:%d\n%w", msg, file, line, err)
+}
+
+func Unwrap(err error) error {
+	return oe.Unwrap(err)
+}
+
+func Is(err, target error) bool {
+	return oe.Is(err, target)
+}
+
+func As(err error, target any) bool {
+	return oe.As(err, target)
+}

+ 29 - 0
server/utils/errors/error_test.go

@@ -0,0 +1,29 @@
+package errors
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestNew(t *testing.T) {
+	err := New("err")
+	fmt.Println(err)
+}
+
+func TestWarp(t *testing.T) {
+	err := New("err1")
+	err = Wrap(err)
+	err = Wrap(err)
+	err = Wrap(err)
+	err = Wrap(err)
+	fmt.Println(err)
+}
+
+func TestWarpWithMsg(t *testing.T) {
+	err := New("err1")
+	err = Wrap(err)
+	err = Wrap(err)
+	err = Wrap(err)
+	err = WrapWithMsg(err, "last")
+	fmt.Println(err)
+}

+ 11 - 0
server/utils/file/file.go

@@ -0,0 +1,11 @@
+package file
+
+import "os"
+
+func PathExist(_path string) bool {
+	_, err := os.Stat(_path)
+	if err != nil && os.IsNotExist(err) {
+		return false
+	}
+	return true
+}

+ 18 - 0
server/utils/password/encode.go

@@ -0,0 +1,18 @@
+package password
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+)
+
+// Encode 对密码两次md5加盐
+func Encode(password string) string {
+	encodePwd := md5Encode(md5Encode(password+"pmail") + "pmail2023")
+	return encodePwd
+}
+
+func md5Encode(str string) string {
+	h := md5.New()
+	h.Write([]byte(str))
+	return hex.EncodeToString(h.Sum(nil))
+}